diff --git a/packages/core/pluggableElementTypes/renderers/ComparativeServerSideRendererType.ts b/packages/core/pluggableElementTypes/renderers/ComparativeServerSideRendererType.ts index a7bb970c40..252e4cd701 100644 --- a/packages/core/pluggableElementTypes/renderers/ComparativeServerSideRendererType.ts +++ b/packages/core/pluggableElementTypes/renderers/ComparativeServerSideRendererType.ts @@ -52,6 +52,11 @@ export default class ComparativeServerSideRenderer extends ServerSideRenderer { * @param args - the arguments passed to render * @returns the same object */ + + async renameRegionsIfNeeded(args: RenderArgs) { + return args + } + serializeArgsInClient(args: RenderArgs) { const deserializedArgs = { ...args, diff --git a/packages/core/ui/index.ts b/packages/core/ui/index.ts index b12354d9cc..a23334b1fe 100644 --- a/packages/core/ui/index.ts +++ b/packages/core/ui/index.ts @@ -2,6 +2,7 @@ export * from './theme' export { LogoFull, Logomark } from './Logo' export { default as App } from './App' export { default as ErrorMessage } from './ErrorMessage' +export { default as AssemblySelector } from './AssemblySelector' export { default as FileSelector } from './FileSelector' export { default as PrerenderedCanvas } from './PrerenderedCanvas' export { default as ResizeHandle } from './ResizeHandle' diff --git a/plugins/comparative-adapters/.babelrc b/plugins/comparative-adapters/.babelrc new file mode 100644 index 0000000000..dde1819d9f --- /dev/null +++ b/plugins/comparative-adapters/.babelrc @@ -0,0 +1,9 @@ +{ + "presets": [ + // need this to be able to use spread operator on Set and Map + // see https://github.com/formium/tsdx/issues/376#issuecomment-566750042 + ["@babel/preset-env", { "loose": false }], + // can remove this if all .js files are converted to .ts + "@babel/preset-react" + ] +} diff --git a/plugins/comparative-adapters/package.json b/plugins/comparative-adapters/package.json new file mode 100644 index 0000000000..a94063e33b --- /dev/null +++ b/plugins/comparative-adapters/package.json @@ -0,0 +1,55 @@ +{ + "name": "@jbrowse/plugin-comparative-adapters", + "version": "1.6.5", + "description": "JBrowse 2 comparative adapters", + "keywords": [ + "jbrowse", + "jbrowse2" + ], + "license": "Apache-2.0", + "homepage": "https://jbrowse.org", + "bugs": "https://github.com/GMOD/jbrowse-components/issues", + "repository": { + "type": "git", + "url": "https://github.com/GMOD/jbrowse-components.git", + "directory": "plugins/comparative-adapters" + }, + "author": "JBrowse Team", + "distMain": "dist/index.js", + "srcMain": "src/index.ts", + "main": "src/index.ts", + "distModule": "dist/plugin-comparative-adapters.esm.js", + "module": "", + "files": [ + "dist", + "src" + ], + "scripts": { + "start": "tsdx watch --verbose --noClean", + "build": "tsdx build", + "test": "cd ../..; jest plugins/comparative-adapters", + "prepublishOnly": "yarn test", + "prepack": "yarn build; yarn useDist", + "postpack": "yarn useSrc", + "useDist": "node ../../scripts/useDist.js", + "useSrc": "node ../../scripts/useSrc.js" + }, + "dependencies": { + "@gmod/bgzf-filehandle": "^1.4.2" + }, + "peerDependencies": { + "@jbrowse/core": "^1.0.0", + "@jbrowse/plugin-alignments": "^1.0.0", + "@jbrowse/plugin-linear-genome-view": "^1.0.0", + "@material-ui/core": "^4.12.2", + "@material-ui/lab": "^4.0.0-alpha.45", + "mobx": "^5.0.0", + "mobx-react": "^6.0.0", + "mobx-state-tree": "3.14.1", + "prop-types": "^15.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "rxjs": "^6.0.0" + }, + "private": true +} diff --git a/plugins/comparative-adapters/src/ChainAdapter/ChainAdapter.ts b/plugins/comparative-adapters/src/ChainAdapter/ChainAdapter.ts new file mode 100644 index 0000000000..13ea07daf8 --- /dev/null +++ b/plugins/comparative-adapters/src/ChainAdapter/ChainAdapter.ts @@ -0,0 +1,195 @@ +import { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter' +import { NoAssemblyRegion } from '@jbrowse/core/util/types' +import { openLocation } from '@jbrowse/core/util/io' +import { readConfObject } from '@jbrowse/core/configuration' +import { unzip } from '@gmod/bgzf-filehandle' +import PAFAdapter from '../PAFAdapter/PAFAdapter' + +interface PafRecord { + records: NoAssemblyRegion[] + extra: { + blockLen: number + mappingQual: number + numMatches: number + strand: number + } +} + +function isGzip(buf: Buffer) { + return buf[0] === 31 && buf[1] === 139 && buf[2] === 8 +} + +/* adapted from chain2paf by Andrea Guarracino, license reproduced below + * + * MIT License + * + * Copyright (c) 2021 Andrea Guarracino + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +function generate_record( + q_name: string, + q_start: number, + q_end: number, + q_strand: string, + t_name: string, + t_start: number, + t_end: number, + cigar: string, + num_matches: number, +) { + return { + records: [ + { refName: q_name, start: q_start, end: q_end }, + { refName: t_name, start: t_start, end: t_end }, + ], + extra: { + numMatches: num_matches, + blockLen: Math.max(q_end - q_start, t_end - t_start), + strand: q_strand === '-' ? -1 : 1, + mappingQual: 0, + cg: cigar, + }, + } as PafRecord +} + +function paf_chain2paf(lines: string[]) { + let t_name = '' + let t_start = 0 + let t_end = 0 + let q_name = '' + let q_size = '' + let q_strand = '' + let q_start = 0 + let q_end = 0 + let num_matches = 0 + let cigar = '' + const records = [] + for (let i = 0; i < lines.length; i++) { + const l = lines[i] + const l_tab = l.replace(/ /g, '\t') // There are CHAIN files with space-separated fields + const l_vec = l_tab.split('\t') + + if (l_vec[0] === 'chain') { + // Emit previous PAF row, if available + if (cigar) { + records.push( + generate_record( + q_name, + q_start, + q_end, + q_strand, + t_name, + t_start, + t_end, + cigar, + num_matches, + ), + ) + } + + // Save query/target information + // score -- chain score + // tName -- chromosome (reference sequence) + // tSize -- chromosome size (reference sequence) + // tStrand -- strand (reference sequence) + // tStart -- alignment start position (reference sequence) + // tEnd -- alignment end position (reference sequence) + // qName -- chromosome (query sequence) + // qSize -- chromosome size (query sequence) + // qStrand -- strand (query sequence) + // qStart -- alignment start position (query sequence) + // qEnd -- alignment end position (query sequence) + // id -- chain ID + t_name = l_vec[2] + t_start = +l_vec[5] + t_end = +l_vec[6] + q_name = l_vec[7] + q_size = l_vec[8] + q_strand = l_vec[9] + q_start = +l_vec[10] + q_end = +l_vec[11] + if (q_strand === '-') { + const tmp = q_start + q_start = +q_size - q_end + q_end = +q_size - tmp + } + + // Initialize PAF fields + num_matches = 0 + cigar = '' + } else { + // size -- the size of the ungapped alignment + // + // dt -- the difference between the end of this block and the beginning + // of the next block (reference sequence) + // + // dq -- the difference between the end of this block and the beginning + // of the next block (query sequence) + const size_ungapped_alignment = +l_vec[0] || 0 + const diff_in_target = l_vec.length > 1 ? +l_vec[1] : 0 + const diff_in_query = l_vec.length > 2 ? +l_vec[2] : 0 + + if (size_ungapped_alignment !== 0) { + num_matches += +size_ungapped_alignment + cigar += size_ungapped_alignment + 'M' + } + if (diff_in_query !== 0) { + cigar += diff_in_query + 'I' + } + if (diff_in_target !== 0) { + cigar += diff_in_target + 'D' + } + } + } + + // Emit last PAF row, if available + if (cigar) { + generate_record( + q_name, + q_start, + q_end, + q_strand, + t_name, + t_start, + t_end, + cigar, + num_matches, + ) + } + return records +} + +export default class ChainAdapter extends PAFAdapter { + async setupPre(opts?: BaseOptions) { + const chainLocation = openLocation( + readConfObject(this.config, 'chainLocation'), + this.pluginManager, + ) + const buffer = (await chainLocation.readFile(opts)) as Buffer + const buf = isGzip(buffer) ? await unzip(buffer) : buffer + // 512MB max chrome string length is 512MB + if (buf.length > 536_870_888) { + throw new Error('Data exceeds maximum string length (512MB)') + } + const text = new TextDecoder('utf8', { fatal: true }).decode(buf) + return paf_chain2paf(text.split('\n').filter(line => !!line)) + } +} diff --git a/plugins/comparative-adapters/src/ChainAdapter/configSchema.ts b/plugins/comparative-adapters/src/ChainAdapter/configSchema.ts new file mode 100644 index 0000000000..8fb1fb521c --- /dev/null +++ b/plugins/comparative-adapters/src/ChainAdapter/configSchema.ts @@ -0,0 +1,16 @@ +import { ConfigurationSchema } from '@jbrowse/core/configuration' + +export default ConfigurationSchema( + 'ChainAdapter', + { + assemblyNames: { + type: 'stringArray', + defaultValue: [], + }, + chainLocation: { + type: 'fileLocation', + defaultValue: { uri: '/path/to/file.chain', locationType: 'UriLocation' }, + }, + }, + { explicitlyTyped: true }, +) diff --git a/plugins/comparative-adapters/src/ChainAdapter/index.ts b/plugins/comparative-adapters/src/ChainAdapter/index.ts new file mode 100644 index 0000000000..48694a88c7 --- /dev/null +++ b/plugins/comparative-adapters/src/ChainAdapter/index.ts @@ -0,0 +1,22 @@ +import PluginManager from '@jbrowse/core/PluginManager' +import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType' + +import AdapterClass from './ChainAdapter' +import configSchema from './configSchema' + +export default (pluginManager: PluginManager) => { + pluginManager.addAdapterType( + () => + new AdapterType({ + name: 'ChainAdapter', + configSchema, + adapterMetadata: { + category: null, + hiddenFromGUI: true, + displayName: null, + description: null, + }, + AdapterClass, + }), + ) +} diff --git a/plugins/comparative-adapters/src/DeltaAdapter/DeltaAdapter.ts b/plugins/comparative-adapters/src/DeltaAdapter/DeltaAdapter.ts new file mode 100644 index 0000000000..584430bc4d --- /dev/null +++ b/plugins/comparative-adapters/src/DeltaAdapter/DeltaAdapter.ts @@ -0,0 +1,168 @@ +import { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter' +import { NoAssemblyRegion } from '@jbrowse/core/util/types' +import { openLocation } from '@jbrowse/core/util/io' +import { readConfObject } from '@jbrowse/core/configuration' +import { unzip } from '@gmod/bgzf-filehandle' +import PAFAdapter from '../PAFAdapter/PAFAdapter' + +interface PafRecord { + records: NoAssemblyRegion[] + extra: { + blockLen: number + mappingQual: number + numMatches: number + strand: number + } +} + +function isGzip(buf: Buffer) { + return buf[0] === 31 && buf[1] === 139 && buf[2] === 8 +} + +/* paf2delta from paftools.js in the minimap2 repository, license reproduced below + * + * The MIT License + * + * Copyright (c) 2018- Dana-Farber Cancer Institute + * 2017-2018 Broad Institute, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +function paf_delta2paf(lines: string[]) { + let rname = '' + let qname = '' + let qs = 0 + let qe = 0 + let rs = 0 + let re = 0 + let strand = 0 + let NM = 0 + let cigar = [] as number[] + let x = 0 + let y = 0 + let seen_gt = false + + const records = [] + const regex = new RegExp(/^>(\S+)\s+(\S+)\s+(\d+)\s+(\d+)/) + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const m = regex.exec(line) + if (m !== null) { + rname = m[1] + qname = m[2] + seen_gt = true + continue + } + if (!seen_gt) { + continue + } + const t = line.split(' ') + if (t.length === 7) { + const t0 = +t[0] + const t1 = +t[1] + const t2 = +t[2] + const t3 = +t[3] + const t4 = +t[4] + strand = (t0 < t1 && t2 < t3) || (t0 > t1 && t2 > t3) ? 1 : -1 + rs = +(t0 < t1 ? t0 : t1) - 1 + re = +(t1 > t0 ? t1 : t0) + qs = +(t2 < t3 ? t2 : t3) - 1 + qe = +(t3 > t2 ? t3 : t2) + x = y = 0 + NM = t4 + cigar = [] + } else if (t.length === 1) { + const d = +t[0] + if (d === 0) { + let blen = 0 + const cigar_str = [] + + if (re - rs - x !== qe - qs - y) { + throw new Error(`inconsistent alignment on line ${i}`) + } + cigar.push((re - rs - x) << 4) + for (let i = 0; i < cigar.length; ++i) { + blen += cigar[i] >> 4 + cigar_str.push((cigar[i] >> 4) + 'MID'.charAt(cigar[i] & 0xf)) + } + + records.push({ + records: [ + { refName: qname, start: qs, end: qe }, + { refName: rname, start: rs, end: re }, + ], + extra: { + numMatches: blen - NM, + blockLen: blen, + strand, + mappingQual: 0, + NM, + cg: cigar_str.join(''), + }, + } as PafRecord) + } else if (d > 0) { + const l = d - 1 + x += l + 1 + y += l + if (l > 0) { + cigar.push(l << 4) + } + if (cigar.length > 0 && (cigar[cigar.length - 1] & 0xf) === 2) { + cigar[cigar.length - 1] += 1 << 4 + } else { + cigar.push((1 << 4) | 2) + } // deletion + } else { + const l = -d - 1 + x += l + y += l + 1 + if (l > 0) { + cigar.push(l << 4) + } + if (cigar.length > 0 && (cigar[cigar.length - 1] & 0xf) === 1) { + cigar[cigar.length - 1] += 1 << 4 + } else { + cigar.push((1 << 4) | 1) + } // insertion + } + } + } + return records +} + +export default class DeltaAdapter extends PAFAdapter { + async setupPre(opts?: BaseOptions) { + const deltaLocation = openLocation( + readConfObject(this.config, 'deltaLocation'), + this.pluginManager, + ) + const buffer = (await deltaLocation.readFile(opts)) as Buffer + const buf = isGzip(buffer) ? await unzip(buffer) : buffer + // 512MB max chrome string length is 512MB + if (buf.length > 536_870_888) { + throw new Error('Data exceeds maximum string length (512MB)') + } + const text = new TextDecoder('utf8', { fatal: true }).decode(buf) + + return paf_delta2paf(text.split('\n').filter(line => !!line)) + } +} diff --git a/plugins/comparative-adapters/src/DeltaAdapter/configSchema.ts b/plugins/comparative-adapters/src/DeltaAdapter/configSchema.ts new file mode 100644 index 0000000000..7bf4b82d4f --- /dev/null +++ b/plugins/comparative-adapters/src/DeltaAdapter/configSchema.ts @@ -0,0 +1,16 @@ +import { ConfigurationSchema } from '@jbrowse/core/configuration' + +export default ConfigurationSchema( + 'DeltaAdapter', + { + assemblyNames: { + type: 'stringArray', + defaultValue: [], + }, + deltaLocation: { + type: 'fileLocation', + defaultValue: { uri: '/path/to/file.delta', locationType: 'UriLocation' }, + }, + }, + { explicitlyTyped: true }, +) diff --git a/plugins/comparative-adapters/src/DeltaAdapter/index.ts b/plugins/comparative-adapters/src/DeltaAdapter/index.ts new file mode 100644 index 0000000000..2d095dbbc8 --- /dev/null +++ b/plugins/comparative-adapters/src/DeltaAdapter/index.ts @@ -0,0 +1,22 @@ +import PluginManager from '@jbrowse/core/PluginManager' +import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType' + +import AdapterClass from './DeltaAdapter' +import configSchema from './configSchema' + +export default (pluginManager: PluginManager) => { + pluginManager.addAdapterType( + () => + new AdapterType({ + name: 'DeltaAdapter', + configSchema, + adapterMetadata: { + category: null, + hiddenFromGUI: true, + displayName: null, + description: null, + }, + AdapterClass, + }), + ) +} diff --git a/plugins/dotplot-view/src/PAFAdapter/PAFAdapter.test.ts b/plugins/comparative-adapters/src/PAFAdapter/PAFAdapter.test.ts similarity index 100% rename from plugins/dotplot-view/src/PAFAdapter/PAFAdapter.test.ts rename to plugins/comparative-adapters/src/PAFAdapter/PAFAdapter.test.ts diff --git a/plugins/dotplot-view/src/PAFAdapter/PAFAdapter.ts b/plugins/comparative-adapters/src/PAFAdapter/PAFAdapter.ts similarity index 82% rename from plugins/dotplot-view/src/PAFAdapter/PAFAdapter.ts rename to plugins/comparative-adapters/src/PAFAdapter/PAFAdapter.ts index 3e459bfeea..c114dd5c41 100644 --- a/plugins/dotplot-view/src/PAFAdapter/PAFAdapter.ts +++ b/plugins/comparative-adapters/src/PAFAdapter/PAFAdapter.ts @@ -107,8 +107,20 @@ export default class PAFAdapter extends BaseFeatureDataAdapter { return true } - async getRefNames() { - // we cannot determine this accurately + async getRefNames(opts: BaseOptions = {}) { + // @ts-ignore + const r1 = opts.regions?.[0].assemblyName + const feats = await this.setup() + const assemblyNames = readConfObject(this.config, 'assemblyNames') + const idx = assemblyNames.indexOf(r1) + if (idx !== -1) { + const set = new Set() + for (let i = 0; i < feats.length; i++) { + set.add(feats[i].records[idx].refName) + } + return Array.from(set) + } + console.warn('Unable to do ref renaming on adapter') return [] } @@ -116,10 +128,11 @@ export default class PAFAdapter extends BaseFeatureDataAdapter { return ObservableCreate(async observer => { const pafRecords = await this.setup(opts) const assemblyNames = readConfObject(this.config, 'assemblyNames') + const { assemblyName } = region - // The index of the assembly name in the region list corresponds to - // the adapter in the subadapters list - const index = assemblyNames.indexOf(region.assemblyName) + // The index of the assembly name in the region list corresponds to the + // adapter in the subadapters list + const index = assemblyNames.indexOf(assemblyName) if (index !== -1) { for (let i = 0; i < pafRecords.length; i++) { const { extra, records } = pafRecords[i] @@ -128,18 +141,17 @@ export default class PAFAdapter extends BaseFeatureDataAdapter { refName === region.refName && doesIntersect2(region.start, region.end, start, end) ) { + const mate = records[+!index] + const syntenyId = i observer.next( new SimpleFeature({ - uniqueId: `row_${i}`, + uniqueId: `${i}`, start, end, refName, - syntenyId: i, - mate: { - start: records[+!index].start, - end: records[+!index].end, - refName: records[+!index].refName, - }, + assemblyName, + syntenyId, + mate, ...extra, }), ) diff --git a/plugins/dotplot-view/src/PAFAdapter/configSchema.ts b/plugins/comparative-adapters/src/PAFAdapter/configSchema.ts similarity index 100% rename from plugins/dotplot-view/src/PAFAdapter/configSchema.ts rename to plugins/comparative-adapters/src/PAFAdapter/configSchema.ts diff --git a/plugins/dotplot-view/src/PAFAdapter/index.ts b/plugins/comparative-adapters/src/PAFAdapter/index.ts similarity index 100% rename from plugins/dotplot-view/src/PAFAdapter/index.ts rename to plugins/comparative-adapters/src/PAFAdapter/index.ts diff --git a/plugins/dotplot-view/src/PAFAdapter/test_data/grape.peach.anchors b/plugins/comparative-adapters/src/PAFAdapter/test_data/grape.peach.anchors similarity index 100% rename from plugins/dotplot-view/src/PAFAdapter/test_data/grape.peach.anchors rename to plugins/comparative-adapters/src/PAFAdapter/test_data/grape.peach.anchors diff --git a/plugins/dotplot-view/src/PAFAdapter/test_data/peach_grape.paf b/plugins/comparative-adapters/src/PAFAdapter/test_data/peach_grape.paf similarity index 100% rename from plugins/dotplot-view/src/PAFAdapter/test_data/peach_grape.paf rename to plugins/comparative-adapters/src/PAFAdapter/test_data/peach_grape.paf diff --git a/plugins/comparative-adapters/src/index.ts b/plugins/comparative-adapters/src/index.ts new file mode 100644 index 0000000000..1073bc93e7 --- /dev/null +++ b/plugins/comparative-adapters/src/index.ts @@ -0,0 +1,56 @@ +import Plugin from '@jbrowse/core/Plugin' +import PluginManager from '@jbrowse/core/PluginManager' +import { FileLocation } from '@jbrowse/core/util/types' + +import PAFAdapterF from './PAFAdapter' +import DeltaAdapterF from './DeltaAdapter' +import ChainAdapterF from './ChainAdapter' + +import { + getFileName, + AdapterGuesser, + TrackTypeGuesser, +} from '@jbrowse/core/util/tracks' + +export default class ComparativeAdaptersPlugin extends Plugin { + name = 'ComparativeAdaptersPlugin' + + install(pluginManager: PluginManager) { + PAFAdapterF(pluginManager) + DeltaAdapterF(pluginManager) + ChainAdapterF(pluginManager) + + pluginManager.addToExtensionPoint( + 'Core-guessAdapterForLocation', + (adapterGuesser: AdapterGuesser) => { + return ( + file: FileLocation, + index?: FileLocation, + adapterHint?: string, + ) => { + const regexGuess = /\.paf/i + const adapterName = 'PAFAdapter' + const fileName = getFileName(file) + if (regexGuess.test(fileName) || adapterHint === adapterName) { + return { + type: adapterName, + pafLocation: file, + } + } + return adapterGuesser(file, index, adapterHint) + } + }, + ) + pluginManager.addToExtensionPoint( + 'Core-guessTrackTypeForLocation', + (trackTypeGuesser: TrackTypeGuesser) => { + return (adapterName: string) => { + if (adapterName === 'PAFAdapter') { + return 'SyntenyTrack' + } + return trackTypeGuesser(adapterName) + } + }, + ) + } +} diff --git a/plugins/comparative-adapters/tsconfig.json b/plugins/comparative-adapters/tsconfig.json new file mode 100644 index 0000000000..ca7cf4fc83 --- /dev/null +++ b/plugins/comparative-adapters/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "types"], + "compilerOptions": { + "importHelpers": true, + "declaration": true, + "sourceMap": true, + "rootDir": "./src" + } +} diff --git a/plugins/comparative-adapters/tsdx.config.js b/plugins/comparative-adapters/tsdx.config.js new file mode 100644 index 0000000000..02db3c28c4 --- /dev/null +++ b/plugins/comparative-adapters/tsdx.config.js @@ -0,0 +1,8 @@ +// Not transpiled with TypeScript or Babel, so use plain Es6/Node.js! +module.exports = { + // This function will run for each entry/format/env combination + rollup(config) { + config.inlineDynamicImports = true + return config // always return a config. + }, +} diff --git a/plugins/data-management/src/PluginStoreWidget/components/__snapshots__/PluginStoreWidget.test.js.snap b/plugins/data-management/src/PluginStoreWidget/components/__snapshots__/PluginStoreWidget.test.js.snap index 99ba49ce43..a2e72c478d 100644 --- a/plugins/data-management/src/PluginStoreWidget/components/__snapshots__/PluginStoreWidget.test.js.snap +++ b/plugins/data-management/src/PluginStoreWidget/components/__snapshots__/PluginStoreWidget.test.js.snap @@ -653,6 +653,26 @@ exports[` renders with the available plugins 1`] = ` GridBookmarkPlugin

+
  • + +

    + ComparativeAdaptersPlugin +

    +
  • diff --git a/plugins/dotplot-view/package.json b/plugins/dotplot-view/package.json index f7b59955d1..7ce37e64a4 100644 --- a/plugins/dotplot-view/package.json +++ b/plugins/dotplot-view/package.json @@ -35,21 +35,15 @@ "useSrc": "node ../../scripts/useSrc.js" }, "dependencies": { - "@gmod/bgzf-filehandle": "^1.4.2", "@material-ui/icons": "^4.9.1", - "abortable-promise-cache": "^1.5.0", - "clsx": "^1.0.0", - "generic-filehandle": "^2.2.2", - "json-stable-stringify": "^1.0.1", - "normalize-wheel": "^1.0.1", - "react-sizeme": "^3.0.2" + "@gmod/bgzf-filehandle": "^1.4.2", + "clone": "^2.1.2", + "normalize-wheel": "^1.0.1" }, "peerDependencies": { "@jbrowse/core": "^1.0.0", - "@jbrowse/plugin-alignments": "^1.0.0", - "@jbrowse/plugin-linear-genome-view": "^1.0.0", + "@jbrowse/plugin-alignments": "^4.12.2", "@material-ui/core": "^4.12.2", - "@material-ui/lab": "^4.0.0-alpha.45", "mobx": "^5.0.0", "mobx-react": "^6.0.0", "mobx-state-tree": "3.14.1", diff --git a/plugins/dotplot-view/src/DotplotDisplay/index.ts b/plugins/dotplot-view/src/DotplotDisplay/index.ts index 268ab2e9f0..35d6b73f38 100644 --- a/plugins/dotplot-view/src/DotplotDisplay/index.ts +++ b/plugins/dotplot-view/src/DotplotDisplay/index.ts @@ -5,6 +5,7 @@ import { ConfigurationReference, ConfigurationSchema, } from '@jbrowse/core/configuration' +import clone from 'clone' import DisplayType from '@jbrowse/core/pluggableElementTypes/DisplayType' import { getParentRenderProps, @@ -182,7 +183,7 @@ function renderBlockData(self: DotplotDisplayModel) { rpcManager, renderProps: { ...self.renderProps(), - view: getSnapshot(parent), + view: clone(getSnapshot(parent)), width: viewWidth, height: viewHeight, borderSize, diff --git a/plugins/dotplot-view/src/DotplotRenderer/ComparativeRenderRpc.ts b/plugins/dotplot-view/src/DotplotRenderer/ComparativeRenderRpc.ts index b6e9f00c62..967a4ff294 100644 --- a/plugins/dotplot-view/src/DotplotRenderer/ComparativeRenderRpc.ts +++ b/plugins/dotplot-view/src/DotplotRenderer/ComparativeRenderRpc.ts @@ -1,6 +1,6 @@ -import { checkAbortSignal, renameRegionsIfNeeded } from '@jbrowse/core/util' +import { checkAbortSignal } from '@jbrowse/core/util' import RpcMethodType from '@jbrowse/core/pluggableElementTypes/RpcMethodType' -import ComparativeServerSideRendererType, { +import ComparativeRenderer, { RenderArgs as ComparativeRenderArgs, RenderArgsSerialized as ComparativeRenderArgsSerialized, RenderResults, @@ -26,30 +26,27 @@ interface RenderArgsSerialized extends ComparativeRenderArgsSerialized { export default class ComparativeRender extends RpcMethodType { name = 'ComparativeRender' - async serializeArguments(args: RenderArgs, rpcDriverClassName: string) { - const assemblyManager = - this.pluginManager.rootModel?.session?.assemblyManager - const renamedArgs = assemblyManager - ? await renameRegionsIfNeeded(assemblyManager, args) - : args - - const superArgs = (await super.serializeArguments( - renamedArgs, - rpcDriverClassName, - )) as RenderArgs - if (rpcDriverClassName === 'MainThreadRpcDriver') { - return superArgs - } + async renameRegionsIfNeeeded( + args: RenderArgs, + renderer: ComparativeRenderer, + ) { + return renderer.renameRegionsIfNeeded(args) + } - const RendererType = this.pluginManager.getRendererType(args.rendererType) + async serializeArguments(args: RenderArgs, rpcDriverClassName: string) { + const { rendererType } = args + const renderer = this.pluginManager.getRendererType( + rendererType, + ) as ComparativeRenderer - if (!(RendererType instanceof ComparativeServerSideRendererType)) { - throw new Error( - 'CoreRender requires a renderer that is a subclass of ServerSideRendererType', - ) - } + const result = await this.renameRegionsIfNeeeded( + (await super.serializeArguments(args, rpcDriverClassName)) as RenderArgs, + renderer, + ) - return RendererType.serializeArgsInClient(superArgs) + return rpcDriverClassName === 'MainThreadRpcDriver' + ? result + : renderer.serializeArgsInClient(result) } async execute( @@ -70,7 +67,9 @@ export default class ComparativeRender extends RpcMethodType { checkAbortSignal(signal) - const RendererType = this.pluginManager.getRendererType(rendererType) + const RendererType = this.pluginManager.getRendererType( + rendererType, + ) as ComparativeRenderer if (!RendererType) { throw new Error(`renderer "${rendererType}" not found`) @@ -81,12 +80,6 @@ export default class ComparativeRender extends RpcMethodType { ) } - if (!(RendererType instanceof ComparativeServerSideRendererType)) { - throw new Error( - 'CoreRender requires a renderer that is a subclass of ServerSideRendererType', - ) - } - const result = rpcDriverClassName === 'MainThreadRpcDriver' ? await RendererType.render(deserializedArgs) @@ -110,7 +103,9 @@ export default class ComparativeRender extends RpcMethodType { } const { rendererType } = args - const RendererType = this.pluginManager.getRendererType(rendererType) + const RendererType = this.pluginManager.getRendererType( + rendererType, + ) as ComparativeRenderer if (!RendererType) { throw new Error(`renderer "${rendererType}" not found`) } @@ -119,11 +114,7 @@ export default class ComparativeRender extends RpcMethodType { `renderer ${rendererType} has no ReactComponent, it may not be completely implemented yet`, ) } - if (!(RendererType instanceof ComparativeServerSideRendererType)) { - throw new Error( - 'CoreRender requires a renderer that is a subclass of ServerSideRendererType', - ) - } + return RendererType.deserializeResultsInClient( superDeserialized as ResultsSerialized, args, diff --git a/plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts b/plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts index 6817a7d485..647a1f6009 100644 --- a/plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts +++ b/plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts @@ -3,10 +3,13 @@ import { createCanvas, createImageBitmap, } from '@jbrowse/core/util/offscreenCanvasPonyfill' -import { viewBpToPx } from '@jbrowse/core/util' +import { viewBpToPx, renameRegionsIfNeeded } from '@jbrowse/core/util' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import { Region } from '@jbrowse/core/util/types' import { getSnapshot, Instance } from 'mobx-state-tree' import ComparativeServerSideRendererType, { RenderArgsDeserialized as ComparativeRenderArgsDeserialized, + RenderArgs as ComparativeRenderArgs, } from '@jbrowse/core/pluggableElementTypes/renderers/ComparativeServerSideRendererType' import { MismatchParser } from '@jbrowse/plugin-alignments' import { Dotplot1DView } from '../DotplotView/model' @@ -15,7 +18,7 @@ type Dim = Instance const { parseCigar } = MismatchParser -export interface RenderArgsDeserialized +export interface DotplotRenderArgsDeserialized extends ComparativeRenderArgsDeserialized { height: number width: number @@ -23,8 +26,41 @@ export interface RenderArgsDeserialized view: { hview: Dim; vview: Dim } } +interface DotplotRenderArgs extends ComparativeRenderArgs { + adapterConfig: AnyConfigurationModel + sessionId: string + view: { + hview: { displayedRegions: Region[] } + vview: { displayedRegions: Region[] } + } +} + export default class DotplotRenderer extends ComparativeServerSideRendererType { - async makeImageData(props: RenderArgsDeserialized & { views: Dim[] }) { + async renameRegionsIfNeeded(args: DotplotRenderArgs) { + const assemblyManager = + this.pluginManager.rootModel?.session?.assemblyManager + + if (!assemblyManager) { + throw new Error('No assembly manager provided') + } + + args.view.hview.displayedRegions = ( + await renameRegionsIfNeeded(assemblyManager, { + sessionId: args.sessionId, + regions: args.view.hview.displayedRegions, + adapterConfig: args.adapterConfig, + }) + ).regions + args.view.vview.displayedRegions = ( + await renameRegionsIfNeeded(assemblyManager, { + sessionId: args.sessionId, + regions: args.view.vview.displayedRegions, + adapterConfig: args.adapterConfig, + }) + ).regions + return args + } + async makeImageData(props: DotplotRenderArgsDeserialized & { views: Dim[] }) { const { highResolutionScaling: scale = 1, width, @@ -149,7 +185,7 @@ export default class DotplotRenderer extends ComparativeServerSideRendererType { return createImageBitmap(canvas) } - async render(renderProps: RenderArgsDeserialized) { + async render(renderProps: DotplotRenderArgsDeserialized) { const { width, height, diff --git a/plugins/dotplot-view/src/DotplotRenderer/components/DotplotRendering.tsx b/plugins/dotplot-view/src/DotplotRenderer/components/DotplotRendering.tsx index 52010124e4..1ff0cd87e3 100644 --- a/plugins/dotplot-view/src/DotplotRenderer/components/DotplotRendering.tsx +++ b/plugins/dotplot-view/src/DotplotRenderer/components/DotplotRendering.tsx @@ -1,9 +1,9 @@ import { PrerenderedCanvas } from '@jbrowse/core/ui' import { observer } from 'mobx-react' import React from 'react' -import { RenderArgsDeserialized } from '../DotplotRenderer' +import { DotplotRenderArgsDeserialized } from '../DotplotRenderer' -function DotplotRendering(props: RenderArgsDeserialized) { +function DotplotRendering(props: DotplotRenderArgsDeserialized) { return } diff --git a/plugins/dotplot-view/src/DotplotView/components/ImportForm.tsx b/plugins/dotplot-view/src/DotplotView/components/ImportForm.tsx index 662c51e9be..a63d9bba2a 100644 --- a/plugins/dotplot-view/src/DotplotView/components/ImportForm.tsx +++ b/plugins/dotplot-view/src/DotplotView/components/ImportForm.tsx @@ -1,11 +1,21 @@ import React, { useState } from 'react' -import { Button, Paper, Container, Grid, makeStyles } from '@material-ui/core' -import { FileSelector } from '@jbrowse/core/ui' +import path from 'path' +import { + Button, + FormControlLabel, + Radio, + RadioGroup, + Paper, + Container, + Grid, + Typography, + makeStyles, +} from '@material-ui/core' +import { FileSelector, ErrorMessage, AssemblySelector } from '@jbrowse/core/ui' import { FileLocation } from '@jbrowse/core/util/types' import { observer } from 'mobx-react' +import { transaction } from 'mobx' import { getSession, isSessionWithAddTracks } from '@jbrowse/core/util' -import ErrorMessage from '@jbrowse/core/ui/ErrorMessage' -import AssemblySelector from '@jbrowse/core/ui/AssemblySelector' import { DotplotViewModel } from '../model' const useStyles = makeStyles(theme => ({ @@ -15,6 +25,21 @@ const useStyles = makeStyles(theme => ({ }, })) +function getName( + trackData?: { uri: string } | { localPath: string } | { name: string }, +) { + return trackData + ? // @ts-ignore + trackData.uri || trackData.localPath || trackData.name + : undefined +} + +function stripGz(fileName: string) { + return fileName.endsWith('.gz') + ? fileName.slice(0, fileName.length - 3) + : fileName +} + const DotplotImportForm = observer(({ model }: { model: DotplotViewModel }) => { const classes = useStyles() const session = getSession(model) @@ -24,6 +49,9 @@ const DotplotImportForm = observer(({ model }: { model: DotplotViewModel }) => { const [selected2, setSelected2] = useState(assemblyNames[0]) const selected = [selected1, selected2] const [error, setError] = useState() + const [value, setValue] = useState('') + const fileName = getName(trackData) + const radioOption = value || (fileName ? path.extname(stripGz(fileName)) : '') const assemblyError = assemblyNames.length ? selected @@ -32,38 +60,61 @@ const DotplotImportForm = observer(({ model }: { model: DotplotViewModel }) => { .join(', ') : 'No configured assemblies' + function getAdapter() { + if (radioOption === '.paf') { + return { + type: 'PAFAdapter', + pafLocation: trackData, + assemblyNames: selected, + } + } else if (radioOption === '.out') { + return { + type: 'PAFAdapter', + pafLocation: trackData, + assemblyNames: selected, + } + } else if (radioOption === '.delta') { + return { + type: 'DeltaAdapter', + deltaLocation: trackData, + assemblyNames: selected, + } + } else if (radioOption === '.chain') { + return { + type: 'ChainAdapter', + chainLocation: trackData, + assemblyNames: selected, + } + } else { + throw new Error('Unknown type') + } + } + function onOpenClick() { try { if (!isSessionWithAddTracks(session)) { return } - model.setViews([ - { bpPerPx: 0.1, offsetPx: 0 }, - { bpPerPx: 0.1, offsetPx: 0 }, - ]) - model.setAssemblyNames([selected1, selected2]) - - if (trackData) { - const fileName = - trackData && 'uri' in trackData && trackData.uri - ? trackData.uri.slice(trackData.uri.lastIndexOf('/') + 1) - : 'MyTrack' - - const trackId = `${fileName}-${Date.now()}` + transaction(() => { + if (trackData) { + const fileName = path.basename(getName(trackData)) || 'MyTrack' + const trackId = `${fileName}-${Date.now()}` - session.addTrackConf({ - trackId: trackId, - name: fileName, - assemblyNames: selected, - type: 'SyntenyTrack', - adapter: { - type: 'PAFAdapter', - pafLocation: trackData, + session.addTrackConf({ + trackId: trackId, + name: fileName, assemblyNames: selected, - }, - }) - model.toggleTrack(trackId) - } + type: 'SyntenyTrack', + adapter: getAdapter(), + }) + model.toggleTrack(trackId) + } + model.setViews([ + { bpPerPx: 0.1, offsetPx: 0 }, + { bpPerPx: 0.1, offsetPx: 0 }, + ]) + model.setAssemblyNames([selected1, selected2]) + }) } catch (e) { console.error(e) setError(e) @@ -87,28 +138,74 @@ const DotplotImportForm = observer(({ model }: { model: DotplotViewModel }) => {

    Select assemblies for dotplot view

    - setSelected1(val)} - session={session} - /> - setSelected2(val)} - session={session} - /> + + + Query + setSelected1(val)} + session={session} + /> + + + Target + setSelected2(val)} + session={session} + /> + + -

    - Optional: Add a PAF{' '} - - (pairwise mapping format) - {' '} - file for the dotplot view. Note that the first assembly should be - the left column of the PAF and the second assembly should be the - right column. PAF-like files from MashMap (.out) are also allowed -

    + + Optional: Add a .paf, .out (MashMap), .delta (Mummer), or + .chain file to view in the dotplot. These file types can also be + gzipped. The first assembly should be the query sequence (e.g. + left column of the PAF) and the second assembly should be the + target sequence (e.g. right column of the PAF) + + setValue(event.target.value)} + > + + + } + label="PAF" + /> + + + } + label="Out" + /> + + + } + label="Delta" + /> + + + } + label="Chain" + /> + + + { - return ( - file: FileLocation, - index?: FileLocation, - adapterHint?: string, - ) => { - const regexGuess = /\.paf/i - const adapterName = 'PAFAdapter' - const fileName = getFileName(file) - if (regexGuess.test(fileName) || adapterHint === adapterName) { - return { - type: adapterName, - pafLocation: file, - } - } - return adapterGuesser(file, index, adapterHint) - } - }, - ) - pluginManager.addToExtensionPoint( - 'Core-guessTrackTypeForLocation', - (trackTypeGuesser: TrackTypeGuesser) => { - return (adapterName: string) => { - if (adapterName === 'PAFAdapter') { - return 'SyntenyTrack' - } - return trackTypeGuesser(adapterName) - } - }, - ) // install our comparative rendering rpc callback pluginManager.addRpcMethod(() => new ComparativeRender(pluginManager)) @@ -101,7 +59,7 @@ export default class DotplotPlugin extends Plugin { }, ) - ;(pluggableElement as DisplayType).stateModel = newStateModel + ;(pluggableElement as ViewType).stateModel = newStateModel } return pluggableElement }, diff --git a/plugins/linear-comparative-view/package.json b/plugins/linear-comparative-view/package.json index c5fe6fdaae..f1194bcdbd 100644 --- a/plugins/linear-comparative-view/package.json +++ b/plugins/linear-comparative-view/package.json @@ -36,9 +36,9 @@ }, "dependencies": { "@material-ui/icons": "^4.9.1", - "@rehooks/component-size": "^1.0.3", "abortable-promise-cache": "^1.5.0", "clsx": "^1.1.0", + "clone": "^2.1.2", "generic-filehandle": "^2.2.2", "json-stable-stringify": "^1.0.1", "react-sizeme": "^3.0.2" diff --git a/plugins/linear-comparative-view/src/LinearComparativeDisplay/index.ts b/plugins/linear-comparative-view/src/LinearComparativeDisplay/index.ts index b3ab0ae9e5..e0fae7f8a1 100644 --- a/plugins/linear-comparative-view/src/LinearComparativeDisplay/index.ts +++ b/plugins/linear-comparative-view/src/LinearComparativeDisplay/index.ts @@ -6,6 +6,7 @@ import { ConfigurationSchema, } from '@jbrowse/core/configuration' import { types, getSnapshot, Instance } from 'mobx-state-tree' +import clone from 'clone' import { baseLinearDisplayConfigSchema } from '@jbrowse/plugin-linear-genome-view' import { getContainingView, @@ -152,7 +153,7 @@ function renderBlockData(self: LinearComparativeDisplay) { rpcManager, renderProps: { ...display.renderProps(), - view: getSnapshot(parent), + view: clone(getSnapshot(parent)), adapterConfig, rendererType: rendererType.name, sessionId, diff --git a/plugins/linear-comparative-view/src/LinearSyntenyRenderer/LinearSyntenyRenderer.ts b/plugins/linear-comparative-view/src/LinearSyntenyRenderer/LinearSyntenyRenderer.ts index 4ad25e2cfd..23b794263c 100644 --- a/plugins/linear-comparative-view/src/LinearSyntenyRenderer/LinearSyntenyRenderer.ts +++ b/plugins/linear-comparative-view/src/LinearSyntenyRenderer/LinearSyntenyRenderer.ts @@ -1,8 +1,34 @@ import ComparativeServerSideRendererType from '@jbrowse/core/pluggableElementTypes/renderers/ComparativeServerSideRendererType' +import { renameRegionsIfNeeded } from '@jbrowse/core/util' import Base1DView, { Base1DViewModel } from '@jbrowse/core/util/Base1DViewModel' import React from 'react' export default class LinearSyntenyRenderer extends ComparativeServerSideRendererType { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async renameRegionsIfNeeded(args: any) { + const assemblyManager = + this.pluginManager.rootModel?.session?.assemblyManager + + if (!assemblyManager) { + throw new Error('No assembly manager provided') + } + const { + view: { views }, + } = args + + for (let i = 0; i < views.length; i++) { + const view = views[i] + view.displayedRegions = ( + await renameRegionsIfNeeded(assemblyManager, { + sessionId: args.sessionId, + regions: view.displayedRegions, + adapterConfig: args.adapterConfig, + }) + ).regions + } + + return args + } async render(renderProps: { height: number width: number diff --git a/plugins/linear-comparative-view/src/LinearSyntenyRenderer/components/LinearSyntenyRendering.tsx b/plugins/linear-comparative-view/src/LinearSyntenyRenderer/components/LinearSyntenyRendering.tsx index 95b6249dbe..5bd5d24fd8 100644 --- a/plugins/linear-comparative-view/src/LinearSyntenyRenderer/components/LinearSyntenyRendering.tsx +++ b/plugins/linear-comparative-view/src/LinearSyntenyRenderer/components/LinearSyntenyRendering.tsx @@ -6,7 +6,13 @@ import SimpleFeature, { Feature, } from '@jbrowse/core/util/simpleFeature' import { getConf } from '@jbrowse/core/configuration' -import { getContainingView, viewBpToPx, ViewSnap } from '@jbrowse/core/util' +import { + getContainingView, + viewBpToPx, + getSession, + ViewSnap, + AssemblyManager, +} from '@jbrowse/core/util' import { MismatchParser } from '@jbrowse/plugin-alignments' import { interstitialYPos, overlayYPos, generateMatches } from '../../util' import { LinearSyntenyViewModel } from '../../LinearSyntenyView/model' @@ -19,10 +25,13 @@ type RectTuple = [number, number, number, number] const { parseCigar } = MismatchParser function px(view: ViewSnap, arg: { refName: string; coord: number }) { - return (viewBpToPx({ ...arg, self: view }) || {}).offsetPx || 0 + return viewBpToPx({ ...arg, self: view })?.offsetPx || 0 } -function layoutMatches(features: Feature[][]) { +function layoutMatches( + features: Feature[][], + assemblyManager?: AssemblyManager, +) { const matches = [] for (let i = 0; i < features.length; i++) { for (let j = i; j < features.length; j++) { @@ -37,17 +46,22 @@ function layoutMatches(features: Feature[][]) { if (f1.get('strand') === -1) { ;[f1e, f1s] = [f1s, f1e] } + const a1 = assemblyManager?.get(f1.get('assemblyName')) + const a2 = assemblyManager?.get(f2.get('assemblyName')) + const r1 = f1.get('refName') + const r2 = f2.get('refName') + matches.push([ { feature: f1, level: i, - refName: f1.get('refName'), + refName: a1?.getCanonicalRefName(f1.get('refName')) || r1, layout: [f1s, 0, f1e, 10] as RectTuple, }, { feature: f2, level: j, - refName: f2.get('refName'), + refName: a2?.getCanonicalRefName(f1.get('refName')) || r2, layout: [f2s, 0, f2e, 10] as RectTuple, }, ]) @@ -58,13 +72,30 @@ function layoutMatches(features: Feature[][]) { return matches } -type LSV = LinearSyntenyViewModel - /** * A block whose content is rendered outside of the main thread and hydrated by * this component. */ -function LinearSyntenyRendering(props: { + +function getResources(displayModel: LinearComparativeDisplay) { + const worker = !('type' in displayModel) + if (!worker && isAlive(displayModel)) { + const parentView = getContainingView(displayModel) as LinearSyntenyViewModel + const color = getConf(displayModel, ['renderer', 'color']) + const session = getSession(displayModel) + const { assemblyManager } = session + return { color, session, parentView, assemblyManager } + } + return {} +} +function LinearSyntenyRendering({ + height, + width, + displayModel, + highResolutionScaling = 1, + features, + trackIds, +}: { width: number height: number displayModel: LinearComparativeDisplay @@ -73,39 +104,25 @@ function LinearSyntenyRendering(props: { trackIds: string[] }) { const ref = useRef(null) - const { - height, - width, - displayModel: display = {}, - highResolutionScaling = 1, - features, - trackIds, - } = props - - const deserializedFeatures = useMemo( + const { color, assemblyManager, parentView } = getResources(displayModel) + const matches = useMemo( () => - features.map(level => { - return level - .map(f => new SimpleFeature(f)) - .sort((a, b) => a.get('syntenyId') - b.get('syntenyId')) - }), - [features], + layoutMatches( + features.map(level => + level + .map(f => new SimpleFeature(f)) + .sort((a, b) => a.get('syntenyId') - b.get('syntenyId')), + ), + assemblyManager, + ), + [features, assemblyManager], ) - const matches = layoutMatches(deserializedFeatures) - const worker = !('type' in display) - const parentView = - worker || !isAlive(display) - ? undefined - : (getContainingView(display) as LSV) - const views = worker ? undefined : parentView?.views - const drawCurves = worker ? undefined : parentView?.drawCurves - const color = - worker || !isAlive(display) - ? undefined - : getConf(display, ['renderer', 'color']) + const drawCurves = parentView?.drawCurves + const views = parentView?.views const offsets = views?.map(view => view.offsetPx) + useEffect(() => { - if (!ref.current || !offsets || !views || !isAlive(display)) { + if (!ref.current || !offsets || !views || !isAlive(displayModel)) { return } const ctx = ref.current.getContext('2d') @@ -126,6 +143,7 @@ function LinearSyntenyRendering(props: { interRegionPaddingWidth: view.interRegionPaddingWidth, minimumBlockWidth: view.minimumBlockWidth, })) + matches.forEach(m => { // we follow a path in the list of chunks, not from top to bottom, just // in series following x1,y1 -> x2,y2 @@ -160,12 +178,12 @@ function LinearSyntenyRendering(props: { ? interstitialYPos(l1 < l2, height) : // prettier-ignore // @ts-ignore - overlayYPos(trackIds[0], l1, views, c1, l1 < l2) + overlayYPos(trackIds[0], l1, viewSnaps, c1, l1 < l2) const y2 = middle ? interstitialYPos(l2 < l1, height) : // prettier-ignore // @ts-ignore - overlayYPos(trackIds[1], l2, views, c2, l2 < l1) + overlayYPos(trackIds[1], l2, viewSnaps, c2, l2 < l1) const mid = (y2 - y1) / 2 @@ -201,38 +219,42 @@ function LinearSyntenyRendering(props: { if (op === 'M' || op === '=') { ctx.fillStyle = '#f003' - cx1 += (val / views[0].bpPerPx) * rev1 - cx2 += (val / views[1].bpPerPx) * rev2 + cx1 += (val / viewSnaps[0].bpPerPx) * rev1 + cx2 += (val / viewSnaps[1].bpPerPx) * rev2 } else if (op === 'X') { ctx.fillStyle = 'brown' - cx1 += (val / views[0].bpPerPx) * rev1 - cx2 += (val / views[1].bpPerPx) * rev2 + cx1 += (val / viewSnaps[0].bpPerPx) * rev1 + cx2 += (val / viewSnaps[1].bpPerPx) * rev2 } else if (op === 'D') { ctx.fillStyle = '#00f3' - cx1 += (val / views[0].bpPerPx) * rev1 + cx1 += (val / viewSnaps[0].bpPerPx) * rev1 } else if (op === 'N') { ctx.fillStyle = '#0a03' - cx1 += (val / views[0].bpPerPx) * rev1 + cx1 += (val / viewSnaps[0].bpPerPx) * rev1 } else if (op === 'I') { ctx.fillStyle = '#ff03' - cx2 += (val / views[1].bpPerPx) * rev2 - } - ctx.beginPath() - ctx.moveTo(px1, y1) - ctx.lineTo(cx1, y1) - if (drawCurves) { - ctx.bezierCurveTo(cx1, mid, cx2, mid, cx2, y2) - } else { - ctx.lineTo(cx2, y2) + cx2 += (val / viewSnaps[1].bpPerPx) * rev2 } - ctx.lineTo(px2, y2) - if (drawCurves) { - ctx.bezierCurveTo(px2, mid, px1, mid, px1, y1) - } else { - ctx.lineTo(px1, y1) + + // only draw cigar entries that are larger than a pixel wide! + if (Math.abs(px1 - cx1) > 0.5 || Math.abs(px2 - cx2) > 0.5) { + ctx.beginPath() + ctx.moveTo(px1, y1) + ctx.lineTo(cx1, y1) + if (drawCurves) { + ctx.bezierCurveTo(cx1, mid, cx2, mid, cx2, y2) + } else { + ctx.lineTo(cx2, y2) + } + ctx.lineTo(px2, y2) + if (drawCurves) { + ctx.bezierCurveTo(px2, mid, px1, mid, px1, y1) + } else { + ctx.lineTo(px1, y1) + } + ctx.closePath() + ctx.fill() } - ctx.closePath() - ctx.fill() } } else { ctx.beginPath() @@ -256,7 +278,7 @@ function LinearSyntenyRendering(props: { } }) }, [ - display, + displayModel, highResolutionScaling, trackIds, width, diff --git a/plugins/linear-comparative-view/src/LinearSyntenyView/components/ImportForm.tsx b/plugins/linear-comparative-view/src/LinearSyntenyView/components/ImportForm.tsx index b35360d083..57109e6ce8 100644 --- a/plugins/linear-comparative-view/src/LinearSyntenyView/components/ImportForm.tsx +++ b/plugins/linear-comparative-view/src/LinearSyntenyView/components/ImportForm.tsx @@ -1,19 +1,22 @@ import React, { useState } from 'react' +import path from 'path' +import { transaction } from 'mobx' import { observer } from 'mobx-react' import { getSession, isSessionWithAddTracks } from '@jbrowse/core/util' -import AssemblySelector from '@jbrowse/core/ui/AssemblySelector' import { Button, Container, + FormControlLabel, + Radio, + RadioGroup, Grid, Paper, Typography, makeStyles, } from '@material-ui/core' import { FileLocation } from '@jbrowse/core/util/types' -import { FileSelector } from '@jbrowse/core/ui' +import { FileSelector, ErrorMessage, AssemblySelector } from '@jbrowse/core/ui' import { LinearSyntenyViewModel } from '../model' -import ErrorMessage from '@jbrowse/core/ui/ErrorMessage' const useStyles = makeStyles(theme => ({ importFormContainer: { @@ -28,6 +31,21 @@ const useStyles = makeStyles(theme => ({ }, })) +function getName( + trackData?: { uri: string } | { localPath: string } | { name: string }, +) { + return trackData + ? // @ts-ignore + trackData.uri || trackData.localPath || trackData.name + : undefined +} + +function stripGz(fileName: string) { + return fileName.endsWith('.gz') + ? fileName.slice(0, fileName.length - 3) + : fileName +} + const ImportForm = observer(({ model }: { model: LinearSyntenyViewModel }) => { const classes = useStyles() const session = getSession(model) @@ -37,6 +55,11 @@ const ImportForm = observer(({ model }: { model: LinearSyntenyViewModel }) => { const [trackData, setTrackData] = useState() const [numRows] = useState(2) const [error, setError] = useState() + + const [value, setValue] = useState('') + const fileName = getName(trackData) + const radioOption = value || (fileName ? path.extname(stripGz(fileName)) : '') + const assemblyError = assemblyNames.length ? selected .map(a => assemblyManager.get(a)?.error) @@ -44,6 +67,36 @@ const ImportForm = observer(({ model }: { model: LinearSyntenyViewModel }) => { .join(', ') : 'No configured assemblies' + function getAdapter() { + if (radioOption === '.paf') { + return { + type: 'PAFAdapter', + pafLocation: trackData, + assemblyNames: selected, + } + } else if (radioOption === '.out') { + return { + type: 'PAFAdapter', + pafLocation: trackData, + assemblyNames: selected, + } + } else if (radioOption === '.delta') { + return { + type: 'DeltaAdapter', + deltaLocation: trackData, + assemblyNames: selected, + } + } else if (radioOption === '.chain') { + return { + type: 'ChainAdapter', + chainLocation: trackData, + assemblyNames: selected, + } + } else { + throw new Error('Unknown type') + } + } + async function onOpenClick() { try { if (!isSessionWithAddTracks(session)) { @@ -69,26 +122,22 @@ const ImportForm = observer(({ model }: { model: LinearSyntenyViewModel }) => { model.views.forEach(view => view.setWidth(model.width)) - if (trackData) { - const name = - 'uri' in trackData - ? trackData.uri.slice(trackData.uri.lastIndexOf('/') + 1) - : 'MyTrack' - - const trackId = `${name}-${Date.now()}` - session.addTrackConf({ - trackId, - name, - assemblyNames: selected, - type: 'SyntenyTrack', - adapter: { - type: 'PAFAdapter', - pafLocation: trackData, + transaction(() => { + if (trackData) { + const fileName = path.basename(getName(trackData)) || 'MyTrack' + const trackId = `${fileName}-${Date.now()}` + + session.addTrackConf({ + trackId: trackId, + name: fileName, assemblyNames: selected, - }, - }) - model.toggleTrack(trackId) - } + type: 'SyntenyTrack', + adapter: getAdapter(), + }) + + model.toggleTrack(trackId) + } + }) } catch (e) { console.error(e) setError(e) @@ -128,17 +177,41 @@ const ImportForm = observer(({ model }: { model: LinearSyntenyViewModel }) => {
    + + Optional: Add a .paf, .out (MashMap), .delta (Mummer), or + .chain file to view in the dotplot. These file types can also be + gzipped. The first assembly should be the query sequence (e.g. left + column of the PAF) and the second assembly should be the target + sequence (e.g. right column of the PAF) + + setValue(event.target.value)} + > + + + } label="PAF" /> + + + } label="Out" /> + + + } + label="Delta" + /> + + + } + label="Chain" + /> + + + - - Optional: Add a PAF{' '} - - (pairwise mapping format) - {' '} - file for the linear synteny view. Note that the first assembly - should be the left column of the PAF and the second assembly should - be the right column. PAF-like files from MashMap (.out) are also - allowed - out.chain" + }, + "adapter": { + "type": "ChainAdapter", + "chainLocation": { + "uri": "https://s3.amazonaws.com/jbrowse.org/genomes/yeast/r64_vs_yjm1447/r64_vs_yjm1447.chain.gz", + "locationType": "UriLocation" + }, + "assemblyNames": ["YJM1447", "R64"] + } + }, + { + "type": "SyntenyTrack", + "trackId": "dotplot_track_chain_cigar", + "name": "r64_vs_yjm1447_chain_cigar", + "assemblyNames": ["YJM1447", "R64"], + "metadata": { + "command": "Chain file generated by paf2chain --input out.paf > out.chain" + }, + "adapter": { + "type": "ChainAdapter", + "chainLocation": { + "uri": "https://s3.amazonaws.com/jbrowse.org/genomes/yeast/r64_vs_yjm1447/r64_vs_yjm1447.cigar.chain.gz", + "locationType": "UriLocation" + }, + "assemblyNames": ["YJM1447", "R64"] + } + }, + + { + "type": "SyntenyTrack", + "trackId": "dotplot_track_delta", + "name": "r64_vs_yjm1447_delta", + "assemblyNames": ["YJM1447", "R64"], + "metadata": { + "command": "nucmer r64.fa yjm1447.fa" + }, + "adapter": { + "type": "DeltaAdapter", + "deltaLocation": { + "uri": "https://s3.amazonaws.com/jbrowse.org/genomes/yeast/r64_vs_yjm1447/r64_vs_yjm1447.delta.gz", + "locationType": "UriLocation" + }, + "assemblyNames": ["YJM1447", "R64"] + } + }, { "type": "FeatureTrack", "trackId": "r64.sorted.gff", @@ -679,10 +749,6 @@ } ], "widgets": { - "aboutWidget": { - "id": "aboutWidget", - "type": "AboutWidget" - }, "hierarchicalTrackSelector": { "id": "hierarchicalTrackSelector", "type": "HierarchicalTrackSelectorWidget", diff --git a/website/docs/faq.md b/website/docs/faq.md index 8a972ba1ca..4eb1197012 100644 --- a/website/docs/faq.md +++ b/website/docs/faq.md @@ -352,3 +352,14 @@ links without the central server Also, if you are implementing JBrowse Web on your own server and would like to create your own URL shortener, you can use the shareURL parameter in the config.json file to point at your own server instead of ours. + +### Troubleshooting + +Doing things like: + +- Changing trackIds +- Deleting tracks + +Can make user's saved sessions fail to load. If part of a session is +inconsistent, currently, the entire session will fail to load. Therefore, make +decisions to delete or change IDs carefully. diff --git a/yarn.lock b/yarn.lock index f88cbf3dbc..62cc7d3279 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3284,11 +3284,6 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9" integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA== -"@rehooks/component-size@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rehooks/component-size/-/component-size-1.0.3.tgz#823eabeb42084893d46d43e3a9d1d0e34252b3cb" - integrity sha512-pnYld+8SSF2vXwdLOqBGUyOrv/SjzwLjIUcs/4c1JJgR0q4E9eBtBfuZMD6zUD51fvSehSsbnlQMzotSmPTXPg== - "@rescripts/cli@^0.0.16": version "0.0.16" resolved "https://registry.yarnpkg.com/@rescripts/cli/-/cli-0.0.16.tgz#dcde29317874f3849b038973d43eff0aa15b0a78" @@ -7692,7 +7687,7 @@ clone@~0.1.9: resolved "https://registry.yarnpkg.com/clone/-/clone-0.1.19.tgz#613fb68639b26a494ac53253e15b1a6bd88ada85" integrity sha1-YT+2hjmyaklKxTJT4Vsaa9iK2oU= -clsx@^1.0.0, clsx@^1.0.4, clsx@^1.1.0, clsx@^1.1.1: +clsx@^1.0.4, clsx@^1.1.0, clsx@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==