diff --git a/packages/core/assemblyManager/assembly.ts b/packages/core/assemblyManager/assembly.ts index e716d56065d..3be8bd6574f 100644 --- a/packages/core/assemblyManager/assembly.ts +++ b/packages/core/assemblyManager/assembly.ts @@ -137,6 +137,10 @@ export interface Loading { lowerCaseRefNameAliases: RefNameAliases cytobands: Feature[] } + +/** + * #stateModel Assembly + */ export default function assemblyFactory( assemblyConfigType: IAnyType, pm: PluginManager, @@ -161,6 +165,9 @@ export default function assemblyFactory( }) return types .model({ + /** + * #slot + */ configuration: types.safeReference(assemblyConfigType), }) .volatile(() => ({ @@ -173,72 +180,107 @@ export default function assemblyFactory( cytobands: undefined as Feature[] | undefined, })) .views(self => ({ + /** + * #getter + */ get initialized() { // @ts-expect-error self.load() return !!self.refNameAliases }, + /** + * #getter + */ get name(): string { return getConf(self, 'name') }, - + /** + * #getter + */ get regions() { // @ts-expect-error self.load() return self.volatileRegions }, - + /** + * #getter + */ get aliases(): string[] { return getConf(self, 'aliases') }, - + /** + * #getter + */ get displayName(): string | undefined { return getConf(self, 'displayName') }, - + /** + * #getter + */ hasName(name: string) { return this.allAliases.includes(name) }, - + /** + * #getter + */ get allAliases() { return [this.name, ...this.aliases] }, - - // note: lowerCaseRefNameAliases not included here: this allows the list - // of refnames to be just the "normal casing", but things like - // getCanonicalRefName can resolve a lower-case name if needed + /** + * #getter + * note: lowerCaseRefNameAliases not included here: this allows the list + * of refnames to be just the "normal casing", but things like + * getCanonicalRefName can resolve a lower-case name if needed + */ get allRefNames() { return !self.refNameAliases ? undefined : Object.keys(self.refNameAliases) }, - + /** + * #getter + */ get lowerCaseRefNames() { return !self.lowerCaseRefNameAliases ? undefined : Object.keys(self.lowerCaseRefNameAliases || {}) }, + /** + * #getter + */ get allRefNamesWithLowerCase() { return this.allRefNames && this.lowerCaseRefNames ? [...new Set([...this.allRefNames, ...this.lowerCaseRefNames])] : undefined }, - get rpcManager() { + /** + * #getter + */ + get rpcManager(): RpcManager { // eslint-disable-next-line @typescript-eslint/no-explicit-any return getParent(self, 2).rpcManager }, + /** + * #getter + */ get refNameColors() { const colors: string[] = getConf(self, 'refNameColors') return colors.length === 0 ? refNameColors : colors }, })) .views(self => ({ + /** + * #getter + */ get refNames() { return self.regions?.map(region => region.refName) }, })) .views(self => ({ + /** + * #method + */ getCanonicalRefName(refName: string) { if (!self.refNameAliases || !self.lowerCaseRefNameAliases) { throw new Error( @@ -249,6 +291,9 @@ export default function assemblyFactory( self.refNameAliases[refName] || self.lowerCaseRefNameAliases[refName] ) }, + /** + * #method + */ getRefNameColor(refName: string) { if (!self.refNames) { return undefined @@ -259,6 +304,9 @@ export default function assemblyFactory( } return self.refNameColors[idx % self.refNameColors.length] }, + /** + * #method + */ isValidRefName(refName: string) { if (!self.refNameAliases) { throw new Error( @@ -269,6 +317,9 @@ export default function assemblyFactory( }, })) .actions(self => ({ + /** + * #action + */ setLoaded({ adapterRegionsWithAssembly, refNameAliases, @@ -280,23 +331,41 @@ export default function assemblyFactory( this.setRefNameAliases(refNameAliases, lowerCaseRefNameAliases) this.setCytobands(cytobands) }, + /** + * #action + */ setError(e: unknown) { console.error(e) self.error = e }, + /** + * #action + */ setRegions(regions: Region[]) { self.volatileRegions = regions }, + /** + * #action + */ setRefNameAliases(aliases: RefNameAliases, lcAliases: RefNameAliases) { self.refNameAliases = aliases self.lowerCaseRefNameAliases = lcAliases }, + /** + * #action + */ setCytobands(cytobands: Feature[]) { self.cytobands = cytobands }, + /** + * #action + */ setLoadingP(p?: Promise) { self.loadingP = p }, + /** + * #action + */ load() { if (!self.loadingP) { self.loadingP = this.loadPre().catch(e => { @@ -306,6 +375,9 @@ export default function assemblyFactory( } return self.loadingP }, + /** + * #action + */ async loadPre() { const conf = self.configuration const refNameAliasesAdapterConf = conf.refNameAliases?.adapter @@ -349,6 +421,9 @@ export default function assemblyFactory( }, })) .views(self => ({ + /** + * #method + */ getAdapterMapEntry(adapterConf: unknown, options: BaseOptions) { const { signal, statusCallback, ...rest } = options if (!options.sessionId) { @@ -371,6 +446,7 @@ export default function assemblyFactory( }, /** + * #method * get Map of `canonical-name -> adapter-specific-name` */ async getRefNameMapForAdapter(adapterConf: unknown, opts: BaseOptions) { @@ -382,6 +458,7 @@ export default function assemblyFactory( }, /** + * #method * get Map of `adapter-specific-name -> canonical-name` */ async getReverseRefNameMapForAdapter( diff --git a/packages/core/assemblyManager/assemblyConfigSchema.ts b/packages/core/assemblyManager/assemblyConfigSchema.ts index 2e93958d0ad..398fa46d47f 100644 --- a/packages/core/assemblyManager/assemblyConfigSchema.ts +++ b/packages/core/assemblyManager/assemblyConfigSchema.ts @@ -2,10 +2,11 @@ import { ConfigurationSchema } from '../configuration' import PluginManager from '../PluginManager' /** - * #config BaseAssembly - * This corresponds to the assemblies section of the config + * #config Assembly + * This corresponds to the assemblies section of the config, generally accessed + * through the assemblyManager */ -function assemblyConfigSchema(pluginManager: PluginManager) { +export default function assemblyConfigSchema(pluginManager: PluginManager) { return ConfigurationSchema( 'BaseAssembly', { @@ -21,8 +22,8 @@ function assemblyConfigSchema(pluginManager: PluginManager) { /** * #slot - * sequence refers to a reference sequence track that has an adapter containing, - * importantly, a sequence adapter such as IndexedFastaAdapter + * sequence refers to a reference sequence track that has an adapter + * containing, importantly, a sequence adapter such as IndexedFastaAdapter */ sequence: pluginManager.getTrackType('ReferenceSequenceTrack') .configSchema, @@ -42,9 +43,9 @@ function assemblyConfigSchema(pluginManager: PluginManager) { { /** * #slot refNameAliases.adapter - * refNameAliases help resolve e.g. chr1 and 1 as the same entity - * the data for refNameAliases are fetched from an adapter, that is - * commonly a tsv like chromAliases.txt from UCSC or similar + * refNameAliases help resolve e.g. chr1 and 1 as the same entity the + * data for refNameAliases are fetched from an adapter, that is commonly a tsv + * like chromAliases.txt from UCSC or similar */ adapter: pluginManager.pluggableConfigSchemaType('adapter'), }, @@ -64,8 +65,8 @@ function assemblyConfigSchema(pluginManager: PluginManager) { { /** * #slot cytobands.adapter - * cytoband data is fetched from an adapter, and can be displayed by a - * view type as ideograms + * cytoband data is fetched from an adapter, and can be displayed by + * a view type as ideograms */ adapter: pluginManager.pluggableConfigSchemaType('adapter'), }, @@ -94,13 +95,11 @@ function assemblyConfigSchema(pluginManager: PluginManager) { { /** * #identifier name - * the name acts as a unique identifier in the config, so it cannot be duplicated. - * it usually a short human readable "id" like hg38, but you can also optionally - * customize the assembly "displayName" config slot + * the name acts as a unique identifier in the config, so it cannot be + * duplicated. it usually a short human readable "id" like hg38, but you can + * also optionally customize the assembly "displayName" config slot */ explicitIdentifier: 'name', }, ) } - -export default assemblyConfigSchema diff --git a/packages/core/assemblyManager/assemblyManager.ts b/packages/core/assemblyManager/assemblyManager.ts index 8544139557a..cf4e70e0c96 100644 --- a/packages/core/assemblyManager/assemblyManager.ts +++ b/packages/core/assemblyManager/assemblyManager.ts @@ -6,13 +6,17 @@ import { Instance, IAnyType, } from 'mobx-state-tree' -import { when } from '../util' import { reaction } from 'mobx' +// locals +import { when } from '../util' import { readConfObject, AnyConfigurationModel } from '../configuration' import assemblyFactory, { Assembly } from './assembly' import PluginManager from '../PluginManager' -function assemblyManagerFactory(conf: IAnyType, pm: PluginManager) { +export default function assemblyManagerFactory( + conf: IAnyType, + pm: PluginManager, +) { type Conf = Instance | string return types .model({ @@ -116,21 +120,20 @@ function assemblyManagerFactory(conf: IAnyType, pm: PluginManager) { addDisposer( self, reaction( - // have to slice it to be properly reacted to () => self.assemblyList, - assemblyConfigs => { + confs => { self.assemblies.forEach(asm => { if (!asm.configuration) { this.removeAssembly(asm) } }) - assemblyConfigs.forEach(assemblyConfig => { - const existingAssemblyIdx = self.assemblies.findIndex( - assembly => - assembly.name === readConfObject(assemblyConfig, 'name'), - ) - if (existingAssemblyIdx === -1) { - this.addAssembly(assemblyConfig) + confs.forEach(conf => { + if ( + !self.assemblies.some( + a => a.name === readConfObject(conf, 'name'), + ) + ) { + this.addAssembly(conf) } }) }, @@ -154,5 +157,3 @@ function assemblyManagerFactory(conf: IAnyType, pm: PluginManager) { }, })) } - -export default assemblyManagerFactory diff --git a/packages/core/assemblyManager/loadRefNameMap.ts b/packages/core/assemblyManager/loadRefNameMap.ts new file mode 100644 index 00000000000..67928d2b7c2 --- /dev/null +++ b/packages/core/assemblyManager/loadRefNameMap.ts @@ -0,0 +1,65 @@ +import { BaseOptions, checkRefName, RefNameAliases } from './util' +import RpcManager from '../rpc/RpcManager' +import { when } from '../util' + +export interface BasicRegion { + start: number + end: number + refName: string + assemblyName: string +} + +export async function loadRefNameMap( + assembly: { + name: string + regions: BasicRegion[] | undefined + refNameAliases: RefNameAliases | undefined + getCanonicalRefName: (arg: string) => string + rpcManager: RpcManager + }, + adapterConfig: unknown, + options: BaseOptions, + signal?: AbortSignal, +) { + const { sessionId } = options + await when(() => !!(assembly.regions && assembly.refNameAliases), { + signal, + name: 'when assembly ready', + }) + + const refNames = (await assembly.rpcManager.call( + sessionId, + 'CoreGetRefNames', + { + adapterConfig, + signal, + ...options, + }, + { timeout: 1000000 }, + )) as string[] + + const { refNameAliases } = assembly + if (!refNameAliases) { + throw new Error(`error loading assembly ${assembly.name}'s refNameAliases`) + } + + const refNameMap = Object.fromEntries( + refNames.map(name => { + checkRefName(name) + return [assembly.getCanonicalRefName(name), name] + }), + ) + + // make the reversed map too + const reversed = Object.fromEntries( + Object.entries(refNameMap).map(([canonicalName, adapterName]) => [ + adapterName, + canonicalName, + ]), + ) + + return { + forwardMap: refNameMap, + reverseMap: reversed, + } +} diff --git a/packages/core/assemblyManager/util.ts b/packages/core/assemblyManager/util.ts new file mode 100644 index 00000000000..e779fec009b --- /dev/null +++ b/packages/core/assemblyManager/util.ts @@ -0,0 +1,70 @@ +import { AnyConfigurationModel } from '../configuration' +import jsonStableStringify from 'json-stable-stringify' +import { BaseRefNameAliasAdapter } from '../data_adapters/BaseAdapter' +import PluginManager from '../PluginManager' +import { BasicRegion } from './loadRefNameMap' + +export type RefNameAliases = Record + +export interface BaseOptions { + signal?: AbortSignal + sessionId: string + statusCallback?: Function +} + +export async function getRefNameAliases( + config: AnyConfigurationModel, + pm: PluginManager, + signal?: AbortSignal, +) { + const type = pm.getAdapterType(config.type) + const CLASS = await type.getAdapterClass() + const adapter = new CLASS(config, undefined, pm) as BaseRefNameAliasAdapter + return adapter.getRefNameAliases({ signal }) +} + +export async function getCytobands( + config: AnyConfigurationModel, + pm: PluginManager, +) { + const type = pm.getAdapterType(config.type) + const CLASS = await type.getAdapterClass() + const adapter = new CLASS(config, undefined, pm) + + // @ts-expect-error + return adapter.getData() +} + +export async function getAssemblyRegions( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + assembly: any, + adapterConfig: AnyConfigurationModel, + signal?: AbortSignal, +): Promise { + const sessionId = 'loadRefNames' + return assembly.rpcManager.call( + sessionId, + 'CoreGetRegions', + { + adapterConfig, + sessionId, + signal, + }, + { timeout: 1000000 }, + ) +} + +const refNameRegex = new RegExp( + '[0-9A-Za-z!#$%&+./:;?@^_|~-][0-9A-Za-z!#$%&*+./:;=?@^_|~-]*', +) + +// Valid refName pattern from https://samtools.github.io/hts-specs/SAMv1.pdf +export function checkRefName(refName: string) { + if (!refNameRegex.test(refName)) { + throw new Error(`Encountered invalid refName: "${refName}"`) + } +} + +export function getAdapterId(adapterConf: unknown) { + return jsonStableStringify(adapterConf) +} diff --git a/packages/core/package.json b/packages/core/package.json index ff96b14bdb4..215ee14eff5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -44,6 +44,7 @@ "dompurify": "^3.0.0", "escape-html": "^1.0.3", "fast-deep-equal": "^3.1.3", + "file-saver": "^2.0.0", "generic-filehandle": "^3.0.0", "http-range-fetcher": "^1.4.0", "is-object": "^1.0.1", diff --git a/packages/core/pluggableElementTypes/models/BaseTrackModel.ts b/packages/core/pluggableElementTypes/models/BaseTrackModel.ts index bae715e8d82..5885a290190 100644 --- a/packages/core/pluggableElementTypes/models/BaseTrackModel.ts +++ b/packages/core/pluggableElementTypes/models/BaseTrackModel.ts @@ -1,3 +1,4 @@ +import { lazy } from 'react' import { transaction } from 'mobx' import { getRoot, @@ -16,10 +17,14 @@ import { } from '../../configuration' import PluginManager from '../../PluginManager' import { MenuItem } from '../../ui' +import { Save } from '../../ui/Icons' import { getContainingView, getEnv, getSession } from '../../util' import { isSessionModelWithConfigEditing } from '../../util/types' import { ElementId } from '../../util/types/mst' +// lazies +const SaveTrackDataDlg = lazy(() => import('./components/SaveTrackData')) + export function getCompatibleDisplays(self: IAnyStateTreeNode) { const { pluginManager } = getEnv(self) const view = getContainingView(self) @@ -210,6 +215,16 @@ export function createBaseTrackModel( return [ ...menuItems, + { + label: 'Save track data', + icon: Save, + onClick: () => { + getSession(self).queueDialog(handleClose => [ + SaveTrackDataDlg, + { model: self, handleClose }, + ]) + }, + }, ...(compatDisp.length > 1 ? [ { diff --git a/packages/core/pluggableElementTypes/models/components/SaveTrackData.tsx b/packages/core/pluggableElementTypes/models/components/SaveTrackData.tsx new file mode 100644 index 00000000000..e32f1ec0836 --- /dev/null +++ b/packages/core/pluggableElementTypes/models/components/SaveTrackData.tsx @@ -0,0 +1,170 @@ +import React, { useEffect, useState } from 'react' +import { + Button, + DialogActions, + DialogContent, + FormControl, + FormControlLabel, + FormLabel, + Radio, + RadioGroup, + TextField, + Typography, +} from '@mui/material' +import { IAnyStateTreeNode } from 'mobx-state-tree' +import { makeStyles } from 'tss-react/mui' +import { saveAs } from 'file-saver' +import { observer } from 'mobx-react' +import { Dialog, ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui' +import { + getSession, + getContainingView, + Feature, + Region, +} from '@jbrowse/core/util' +import { getConf } from '@jbrowse/core/configuration' + +// icons +import GetAppIcon from '@mui/icons-material/GetApp' + +// locals +import { stringifyGFF3 } from './gff3' +import { stringifyGenbank } from './genbank' + +const useStyles = makeStyles()({ + root: { + width: '80em', + }, + textAreaFont: { + fontFamily: 'Courier New', + }, +}) + +async function fetchFeatures( + track: IAnyStateTreeNode, + regions: Region[], + signal?: AbortSignal, +) { + const { rpcManager } = getSession(track) + const adapterConfig = getConf(track, ['adapter']) + const sessionId = 'getFeatures' + return rpcManager.call(sessionId, 'CoreGetFeatures', { + adapterConfig, + regions, + sessionId, + signal, + }) as Promise +} + +export default observer(function SaveTrackDataDlg({ + model, + handleClose, +}: { + model: IAnyStateTreeNode + handleClose: () => void +}) { + const { classes } = useStyles() + const [error, setError] = useState() + const [features, setFeatures] = useState() + const [type, setType] = useState('gff3') + const [str, setStr] = useState('') + const options = { + gff3: { name: 'GFF3', extension: 'gff3' }, + genbank: { name: 'GenBank', extension: 'genbank' }, + } + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + ;(async () => { + try { + const view = getContainingView(model) as { visibleRegions?: Region[] } + setError(undefined) + setFeatures(await fetchFeatures(model, view.visibleRegions || [])) + } catch (e) { + console.error(e) + setError(e) + } + })() + }, [model]) + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + ;(async () => { + try { + const view = getContainingView(model) + const session = getSession(model) + if (!features) { + return + } + const str = await (type === 'gff3' + ? stringifyGFF3(features) + : stringifyGenbank({ + features, + session, + assemblyName: view.dynamicBlocks.contentBlocks[0].assemblyName, + })) + + setStr(str) + } catch (e) { + setError(e) + } + })() + }, [type, features, model]) + + return ( + + + {error ? : null} + {!features ? ( + + ) : !features.length ? ( + No features found + ) : null} + + + File type + setType(e.target.value)}> + {Object.entries(options).map(([key, val]) => ( + } + label={val.name} + /> + ))} + + + + + + + + + + + ) +}) diff --git a/packages/core/pluggableElementTypes/models/components/genbank.ts b/packages/core/pluggableElementTypes/models/components/genbank.ts new file mode 100644 index 00000000000..838e7f1a9cd --- /dev/null +++ b/packages/core/pluggableElementTypes/models/components/genbank.ts @@ -0,0 +1,184 @@ +import { + AbstractSessionModel, + Feature, + max, + min, + Region, +} from '@jbrowse/core/util' +import { getConf } from '@jbrowse/core/configuration' + +const coreFields = new Set([ + 'uniqueId', + 'refName', + 'source', + 'type', + 'start', + 'end', + 'strand', + 'parent', + 'parentId', + 'score', + 'subfeatures', + 'phase', +]) + +const blank = ' ' + +const retitle = { + name: 'Name', +} as { [key: string]: string | undefined } + +function fmt(obj: unknown): string { + if (Array.isArray(obj)) { + return obj.map(o => fmt(o)).join(',') + } else if (typeof obj === 'object') { + return JSON.stringify(obj) + } else { + return `${obj}` + } +} + +function formatTags(f: Feature, parentId?: string, parentType?: string) { + return [ + parentId && parentType ? `${blank}/${parentType}="${parentId}"` : '', + f.get('id') ? `${blank}/name=${f.get('id')}` : '', + ...f + .tags() + .filter(tag => !coreFields.has(tag)) + .map(tag => [tag, fmt(f.get(tag))]) + .filter(tag => !!tag[1] && tag[0] !== parentType) + .map(tag => `${blank}/${retitle[tag[0]] || tag[0]}="${tag[1]}"`), + ].filter(f => !!f) +} + +function rs(f: Feature, min: number) { + return f.get('start') - min + 1 +} +function re(f: Feature, min: number) { + return f.get('end') - min +} +function loc(f: Feature, min: number) { + return `${rs(f, min)}..${re(f, min)}` +} +function formatFeat( + f: Feature, + min: number, + parentType?: string, + parentId?: string, +) { + const type = `${f.get('type')}`.slice(0, 16) + const l = loc(f, min) + const locstrand = f.get('strand') === -1 ? `complement(${l})` : l + return [ + ` ${type.padEnd(16)}${locstrand}`, + ...formatTags(f, parentType, parentId), + ] +} + +function formatCDS( + feats: Feature[], + parentId: string, + parentType: string, + strand: number, + min: number, +) { + const cds = feats.map(f => loc(f, min)) + const pre = `join(${cds})` + const str = strand === -1 ? `complement(${pre})` : pre + return feats.length + ? [` ${'CDS'.padEnd(16)}${str}`, `${blank}/${parentType}="${parentId}"`] + : [] +} + +export function formatFeatWithSubfeatures( + feature: Feature, + min: number, + parentId?: string, + parentType?: string, +): string { + const primary = formatFeat(feature, min, parentId, parentType) + const subfeatures = feature.get('subfeatures') || [] + const cds = subfeatures.filter(f => f.get('type') === 'CDS') + const sansCDS = subfeatures.filter( + f => f.get('type') !== 'CDS' && f.get('type') !== 'exon', + ) + const newParentId = feature.get('id') + const newParentType = feature.get('type') + const newParentStrand = feature.get('strand') + return [ + ...primary, + ...formatCDS(cds, newParentId, newParentType, newParentStrand, min), + ...sansCDS.flatMap(sub => + formatFeatWithSubfeatures(sub, min, newParentId, newParentType), + ), + ].join('\n') +} + +export async function stringifyGenbank({ + features, + assemblyName, + session, +}: { + assemblyName: string + session: AbstractSessionModel + features: Feature[] +}) { + const today = new Date() + const month = today.toLocaleString('en-US', { month: 'short' }).toUpperCase() + const day = today.toLocaleString('en-US', { day: 'numeric' }) + const year = today.toLocaleString('en-US', { year: 'numeric' }) + const date = `${day}-${month}-${year}` + + const start = min(features.map(f => f.get('start'))) + const end = max(features.map(f => f.get('end'))) + const length = end - start + const refName = features[0].get('refName') + + const l1 = [ + `${'LOCUS'.padEnd(12)}`, + `${refName}:${start + 1}..${end}`.padEnd(20), + ` ${`${length} bp`}`.padEnd(15), + ` ${'DNA'.padEnd(10)}`, + `${'linear'.padEnd(10)}`, + `${'UNK ' + date}`, + ].join('') + const l2 = 'FEATURES Location/Qualifiers' + const seq = await fetchSequence({ + session, + assemblyName, + regions: [{ assemblyName, start, end, refName }], + }) + const contig = seq.map(f => f.get('seq') || '').join('') + const lines = features.map(feat => formatFeatWithSubfeatures(feat, start)) + const seqlines = ['ORIGIN', `\t1 ${contig}`, '//'] + return [l1, l2, ...lines, ...seqlines].join('\n') +} + +async function fetchSequence({ + session, + regions, + signal, + assemblyName, +}: { + assemblyName: string + session: AbstractSessionModel + regions: Region[] + signal?: AbortSignal +}) { + const { rpcManager, assemblyManager } = session + const assembly = assemblyManager.get(assemblyName) + if (!assembly) { + throw new Error(`assembly ${assemblyName} not found`) + } + + const sessionId = 'getSequence' + return rpcManager.call(sessionId, 'CoreGetFeatures', { + adapterConfig: getConf(assembly, ['sequence', 'adapter']), + regions: regions.map(r => ({ + ...r, + refName: assembly.getCanonicalRefName(r.refName), + })), + sessionId, + signal, + }) as Promise +} diff --git a/packages/core/pluggableElementTypes/models/components/gff3.ts b/packages/core/pluggableElementTypes/models/components/gff3.ts new file mode 100644 index 00000000000..6d4227066ea --- /dev/null +++ b/packages/core/pluggableElementTypes/models/components/gff3.ts @@ -0,0 +1,80 @@ +import { Feature } from '@jbrowse/core/util' + +const coreFields = new Set([ + 'uniqueId', + 'refName', + 'source', + 'type', + 'start', + 'end', + 'strand', + 'parent', + 'parentId', + 'score', + 'subfeatures', + 'phase', +]) + +const retitle = { + id: 'ID', + name: 'Name', + alias: 'Alias', + parent: 'Parent', + target: 'Target', + gap: 'Gap', + derives_from: 'Derives_from', + note: 'Note', + description: 'Note', + dbxref: 'Dbxref', + ontology_term: 'Ontology_term', + is_circular: 'Is_circular', +} as { [key: string]: string } + +function fmt(obj: unknown): string { + if (Array.isArray(obj)) { + return obj.map(o => fmt(o)).join(',') + } else if (typeof obj === 'object') { + return JSON.stringify(obj) + } else { + return `${obj}` + } +} + +function formatFeat(f: Feature, parentId?: string, parentRef?: string) { + return [ + f.get('refName') || parentRef, + f.get('source') || '.', + f.get('type') || '.', + f.get('start') + 1, + f.get('end'), + f.get('score') || '.', + f.get('strand') || '.', + f.get('phase') || '.', + (parentId ? `Parent=${parentId};` : '') + + f + .tags() + .filter(tag => !coreFields.has(tag)) + .map(tag => [tag, fmt(f.get(tag))]) + .filter(tag => !!tag[1]) + .map(tag => `${retitle[tag[0]] || tag[0]}=${tag[1]}`) + .join(';'), + ].join('\t') +} +export function formatMultiLevelFeat( + f: Feature, + parentId?: string, + parentRef?: string, +): string { + const fRef = parentRef || f.get('refName') + const fId = f.get('id') + const primary = formatFeat(f, parentId, fRef) + const subs = + f.get('subfeatures')?.map(sub => formatMultiLevelFeat(sub, fId, fRef)) || [] + return [primary, ...subs].join('\n') +} + +export function stringifyGFF3(feats: Feature[]) { + return ['##gff-version 3', ...feats.map(f => formatMultiLevelFeat(f))].join( + '\n', + ) +} diff --git a/packages/core/rpc/coreRpcMethods.ts b/packages/core/rpc/coreRpcMethods.ts index e81beffae91..64f8296d3e3 100644 --- a/packages/core/rpc/coreRpcMethods.ts +++ b/packages/core/rpc/coreRpcMethods.ts @@ -6,4 +6,5 @@ export { default as CoreGetFeatures } from './methods/CoreGetFeatures' export { default as CoreRender } from './methods/CoreRender' export { default as CoreFreeResources } from './methods/CoreFreeResources' export { default as CoreGetFeatureDensityStats } from './methods/CoreGetFeatureDensityStats' +export { default as CoreGetRegions } from './methods/CoreGetRegions' export { type RenderArgs } from './methods/util' diff --git a/packages/core/rpc/methods/CoreGetRefNames.ts b/packages/core/rpc/methods/CoreGetRefNames.ts index 936a2187729..2a2bf329d48 100644 --- a/packages/core/rpc/methods/CoreGetRefNames.ts +++ b/packages/core/rpc/methods/CoreGetRefNames.ts @@ -19,10 +19,8 @@ export default class CoreGetRefNames extends RpcMethodType { const deserializedArgs = await this.deserializeArguments(args, rpcDriver) const { sessionId, adapterConfig } = deserializedArgs const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) - - if (isFeatureAdapter(dataAdapter)) { - return dataAdapter.getRefNames(deserializedArgs) - } - return [] + return isFeatureAdapter(dataAdapter) + ? dataAdapter.getRefNames(deserializedArgs) + : [] } } diff --git a/packages/core/rpc/methods/CoreGetRegions.ts b/packages/core/rpc/methods/CoreGetRegions.ts new file mode 100644 index 00000000000..0761c2c62f0 --- /dev/null +++ b/packages/core/rpc/methods/CoreGetRegions.ts @@ -0,0 +1,26 @@ +import { getAdapter } from '../../data_adapters/dataAdapterCache' +import RpcMethodType from '../../pluggableElementTypes/RpcMethodType' + +import { RemoteAbortSignal } from '../remoteAbortSignals' +import { isRegionsAdapter } from '../../data_adapters/BaseAdapter' + +export default class CoreGetRegions extends RpcMethodType { + name = 'CoreGetRegions' + + async execute( + args: { + sessionId: string + signal: RemoteAbortSignal + adapterConfig: {} + }, + rpcDriver: string, + ) { + const pm = this.pluginManager + const deserializedArgs = await this.deserializeArguments(args, rpcDriver) + const { sessionId, adapterConfig } = deserializedArgs + const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) + return isRegionsAdapter(dataAdapter) + ? dataAdapter.getRegions(deserializedArgs) + : [] + } +} diff --git a/packages/core/rpc/methods/CoreSaveFeatureData.ts b/packages/core/rpc/methods/CoreSaveFeatureData.ts new file mode 100644 index 00000000000..fddc5ea49f6 --- /dev/null +++ b/packages/core/rpc/methods/CoreSaveFeatureData.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { toArray } from 'rxjs/operators' +import { firstValueFrom } from 'rxjs' + +// locals +import { getAdapter } from '../../data_adapters/dataAdapterCache' +import RpcMethodType from '../../pluggableElementTypes/RpcMethodType' +import { RenderArgs } from './util' +import { RemoteAbortSignal } from '../remoteAbortSignals' +import { isFeatureAdapter } from '../../data_adapters/BaseAdapter' +import { renameRegionsIfNeeded, Region } from '../../util' +import SimpleFeature, { + SimpleFeatureSerialized, +} from '../../util/simpleFeature' + +export default class CoreGetFeatures extends RpcMethodType { + name = 'CoreGetFeatures' + + async deserializeReturn( + feats: SimpleFeatureSerialized[], + args: unknown, + rpcDriver: string, + ) { + const superDeserialized = (await super.deserializeReturn( + feats, + args, + rpcDriver, + )) as SimpleFeatureSerialized[] + return superDeserialized.map(feat => new SimpleFeature(feat)) + } + + async serializeArguments(args: RenderArgs, rpcDriver: string) { + const { rootModel } = this.pluginManager + const assemblyManager = rootModel!.session!.assemblyManager + const renamedArgs = await renameRegionsIfNeeded(assemblyManager, args) + return super.serializeArguments( + renamedArgs, + rpcDriver, + ) as Promise + } + + async execute( + args: { + sessionId: string + regions: Region[] + adapterConfig: {} + signal?: RemoteAbortSignal + // eslint-disable-next-line @typescript-eslint/no-explicit-any + opts?: any + }, + rpcDriver: string, + ) { + const pm = this.pluginManager + const deserializedArgs = await this.deserializeArguments(args, rpcDriver) + const { signal, sessionId, adapterConfig, regions, opts } = deserializedArgs + const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) + if (!isFeatureAdapter(dataAdapter)) { + throw new Error('Adapter does not support retrieving features') + } + const ret = dataAdapter.getFeaturesInMultipleRegions(regions, { + ...opts, + signal, + }) + const r = await firstValueFrom(ret.pipe(toArray())) + return r.map(f => f.toJSON()) + } +} diff --git a/plugins/alignments/src/LinearPileupDisplay/components/SetMaxHeight.tsx b/plugins/alignments/src/LinearPileupDisplay/components/SetMaxHeight.tsx index be54004939c..1da2d271682 100644 --- a/plugins/alignments/src/LinearPileupDisplay/components/SetMaxHeight.tsx +++ b/plugins/alignments/src/LinearPileupDisplay/components/SetMaxHeight.tsx @@ -40,30 +40,30 @@ function SetMaxHeightDlg(props: { onChange={event => setMax(event.target.value)} placeholder="Enter max height for layout" /> - - - - + + + + ) } diff --git a/plugins/alignments/src/LinearReadCloudDisplay/declare.d.ts b/plugins/alignments/src/LinearReadCloudDisplay/declare.d.ts new file mode 100644 index 00000000000..6ba8b2994b2 --- /dev/null +++ b/plugins/alignments/src/LinearReadCloudDisplay/declare.d.ts @@ -0,0 +1 @@ +declare module 'canvas2svg' diff --git a/plugins/dotplot-view/src/DotplotView/1dview.ts b/plugins/dotplot-view/src/1dview.ts similarity index 100% rename from plugins/dotplot-view/src/DotplotView/1dview.ts rename to plugins/dotplot-view/src/1dview.ts diff --git a/plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts b/plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts index ea238442dd4..48fe12078a4 100644 --- a/plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts +++ b/plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts @@ -17,7 +17,7 @@ import ComparativeRenderer, { import { MismatchParser } from '@jbrowse/plugin-alignments' // locals -import { Dotplot1DView, Dotplot1DViewModel } from '../DotplotView/model' +import { Dotplot1DView, Dotplot1DViewModel } from '../1dview' import { createJBrowseTheme } from '@jbrowse/core/ui' const { parseCigar } = MismatchParser diff --git a/plugins/dotplot-view/src/DotplotView/model.ts b/plugins/dotplot-view/src/DotplotView/model.ts index c6888b97ac5..f60ad06d540 100644 --- a/plugins/dotplot-view/src/DotplotView/model.ts +++ b/plugins/dotplot-view/src/DotplotView/model.ts @@ -36,7 +36,7 @@ import FolderOpenIcon from '@mui/icons-material/FolderOpen' import PhotoCameraIcon from '@mui/icons-material/PhotoCamera' // locals -import { Dotplot1DView, DotplotHView, DotplotVView } from './1dview' +import { Dotplot1DView, DotplotHView, DotplotVView } from '../1dview' import { getBlockLabelKeysToHide, makeTicks } from './components/util' import { renderToSvg } from './svgcomponents/SVGDotplotView' import ExportSvgDlg from './components/ExportSvgDialog' @@ -711,5 +711,4 @@ export default function stateModelFactory(pm: PluginManager) { export type DotplotViewStateModel = ReturnType export type DotplotViewModel = Instance - -export { type Dotplot1DViewModel, Dotplot1DView } from './1dview' +export { type Dotplot1DViewModel, Dotplot1DView } from '../1dview' diff --git a/plugins/gff3/src/Gff3TabixAdapter/Gff3TabixAdapter.ts b/plugins/gff3/src/Gff3TabixAdapter/Gff3TabixAdapter.ts index 9d181a9d1b1..a38ddf83845 100644 --- a/plugins/gff3/src/Gff3TabixAdapter/Gff3TabixAdapter.ts +++ b/plugins/gff3/src/Gff3TabixAdapter/Gff3TabixAdapter.ts @@ -7,7 +7,7 @@ import { doesIntersect2 } from '@jbrowse/core/util/range' import { Region } from '@jbrowse/core/util/types' import { openLocation } from '@jbrowse/core/util/io' import { ObservableCreate } from '@jbrowse/core/util/rxjs' -import SimpleFeature, { Feature } from '@jbrowse/core/util/simpleFeature' +import { SimpleFeature, Feature } from '@jbrowse/core/util' import { TabixIndexedFile } from '@gmod/tabix' import gff, { GFF3Feature, GFF3FeatureLineWithRefs } from '@gmod/gff' import { Observer } from 'rxjs' @@ -36,22 +36,18 @@ export default class extends BaseFeatureDataAdapter { pluginManager?: PluginManager, ) { super(config, getSubAdapter, pluginManager) + const pm = this.pluginManager const gffGzLocation = readConfObject(config, 'gffGzLocation') const indexType = readConfObject(config, ['index', 'indexType']) const location = readConfObject(config, ['index', 'location']) const dontRedispatch = readConfObject(config, 'dontRedispatch') this.dontRedispatch = dontRedispatch || ['chromosome', 'contig', 'region'] + const loc = openLocation(location, pm) this.gff = new TabixIndexedFile({ - filehandle: openLocation(gffGzLocation, this.pluginManager), - csiFilehandle: - indexType === 'CSI' - ? openLocation(location, this.pluginManager) - : undefined, - tbiFilehandle: - indexType !== 'CSI' - ? openLocation(location, this.pluginManager) - : undefined, + filehandle: openLocation(gffGzLocation, pm), + csiFilehandle: indexType === 'CSI' ? loc : undefined, + tbiFilehandle: indexType !== 'CSI' ? loc : undefined, chunkCacheSize: 50 * 2 ** 20, renameRefSeqs: (n: string) => n, }) @@ -152,7 +148,8 @@ export default class extends BaseFeatureDataAdapter { f.get('end'), originalQuery.start, originalQuery.end, - ) + ) && + f.get('type') !== 'region' ) { observer.next(f) } diff --git a/plugins/linear-genome-view/src/LinearBasicDisplay/components/SetMaxHeight.tsx b/plugins/linear-genome-view/src/LinearBasicDisplay/components/SetMaxHeight.tsx index 52dc8bf0d64..8984c3daa64 100644 --- a/plugins/linear-genome-view/src/LinearBasicDisplay/components/SetMaxHeight.tsx +++ b/plugins/linear-genome-view/src/LinearBasicDisplay/components/SetMaxHeight.tsx @@ -42,30 +42,30 @@ function SetMaxHeightDlg({ onChange={event => setMax(event.target.value)} placeholder="Enter max score" /> - - - - + + + + ) } diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/GetSequenceDialog.tsx b/plugins/linear-genome-view/src/LinearGenomeView/components/GetSequenceDialog.tsx index 7eae2722dbb..7eb47a8b5dc 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/GetSequenceDialog.tsx +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/GetSequenceDialog.tsx @@ -44,9 +44,6 @@ const useStyles = makeStyles()({ type LGV = LinearGenomeViewModel -/** - * Fetches and returns a list features for a given list of regions - */ async function fetchSequence( model: LGV, regions: Region[], @@ -69,7 +66,6 @@ async function fetchSequence( throw new Error(`assembly ${assemblyName} not found`) } const adapterConfig = getConf(assembly, ['sequence', 'adapter']) - const sessionId = 'getSequence' return rpcManager.call(sessionId, 'CoreGetFeatures', { adapterConfig, @@ -196,7 +192,6 @@ function SequenceDialog({ ) : null} setCopied(false), 500) }} disabled={loading || !!error || sequenceTooLarge} - color="primary" startIcon={} > {copied ? 'Copied' : 'Copy to clipboard'} @@ -263,7 +257,6 @@ function SequenceDialog({ ) }} disabled={loading || !!error} - color="primary" startIcon={} > Download FASTA diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/TrackLabel.tsx b/plugins/linear-genome-view/src/LinearGenomeView/components/TrackLabel.tsx index 893c68a029c..53c4f410e82 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/TrackLabel.tsx +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/TrackLabel.tsx @@ -35,9 +35,6 @@ const useStyles = makeStyles()(theme => ({ }), }, trackName: { - margin: '0 auto', - width: '90%', - fontSize: '0.8rem', pointerEvents: 'none', }, dragHandle: { diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap b/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap index ff78281c5e5..8a0150c5eca 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap @@ -476,7 +476,7 @@ exports[`renders one track, one region 1`] = ` /> Foo Track @@ -1295,7 +1295,7 @@ exports[`renders two tracks, two regions 1`] = ` /> Foo Track @@ -1427,7 +1427,7 @@ exports[`renders two tracks, two regions 1`] = ` /> Bar Track diff --git a/plugins/linear-genome-view/src/LinearGenomeView/model.ts b/plugins/linear-genome-view/src/LinearGenomeView/model.ts index bf113e5df8e..ffe83a8969d 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/model.ts +++ b/plugins/linear-genome-view/src/LinearGenomeView/model.ts @@ -1587,6 +1587,10 @@ export function stateModelFactory(pluginManager: PluginManager) { ? this.pxToBp(self.width / 2) : undefined }, + + get visibleRegions() { + return self.dynamicBlocks.contentBlocks + }, })) } diff --git a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap index 012a73c76e5..16c1474372a 100644 --- a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap +++ b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap @@ -1,6917 +1,6916 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` renders successfully 1`] = ` -
+
+
+
+ -
-
+ - -
+ +
+
-
-
+
+
-
+ class="css-den3n5-scalebarVisibleRegion" + style="width: 267px; left: 0px; background: rgba(66, 119, 127, 0.3); border-color: rgb(66, 119, 127);" + /> +
+
+

+ ctgA +

-
+ class="css-rtx9ov-scalebarContig-scalebarContigForward-scalebarBorder" + style="left: 0px; width: 800px; border-color: rgb(153, 102, 0);" + >

- ctgA + 20

-
-

- 20 -

-

- 40 -

-

- 60 -

-

- 80 -

-

- 100 -

-

- 120 -

-
+ 40 +

+

+ 60 +

+

+ 80 +

+

+ 100 +

+

+ 120 +

-
+
+
+
+ + +
- - + - + +
-
-
- - + + + +
+
- - -
+ -
+ ​ + + +
-

- 40bp -

-
+

+ 40bp +

+
+ + + + + + + - - - - - + max="564.3856189774724" + min="258.49625007211563" + step="1" + style="border: 0px; height: 100%; margin: -1px; overflow: hidden; padding: 0px; position: absolute; white-space: nowrap; width: 100%; direction: ltr;" + type="range" + value="432.19280948873626" + /> - -
-
+ + + +
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ class="css-i794sq-tick-minorTick" + style="left: 0px;" + /> +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+ class="css-1dmfa10-contentBlock" + style="width: 800px;" + > +
+
+

+ 10 +

+
+
+

+ 20 +

+
+
+

+ 30 +

+
+
+

+ 40 +

+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

- 10 -

-
-
-

- 20 -

-
-
-

- 30 -

-
-
-

- 40 -

-
+ 40 +

-
-

- 40 -

-
-
-

- 50 -

-
-
+
+
+

-

- 60 -

-
-
+
+
+

-

- 70 -

-
-
+
+
+

-

- 80 -

-
+ 80 +

-

- ctgA -

+

+ ctgA +

+
+
-
- - + - - Reference sequence (volvox) - + class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root" + /> + + + + Reference sequence (volvox) - -
+ + + +
+
+
-
-
- - - - L - - - - L - - - - R - - - - S - - - - * - - - - T - - - - T - - - - A - - - - L - - - - G - - - - T - - - - L - - - - P - - - - - - I - - - - V - - - - A - - - - E - - - - L - - - - N - - - - N - - - - G - - - - I - - - - R - - - - N - - - - T - - - - S - - - - V - - - - H - - - - C - - - - C - - - - G - - - - V - - - - E - - - - Q - - - - R - - - - H - - - - * - - - - E - - - - H - - - - F - - - - R - - - - - - c - - - - a - - - - t - - - - t - - - - g - - - - t - - - - t - - - - g - - - - c - - - - g - - - - g - - - - a - - - - g - - - - t - - - - t - - - - g - - - - a - - - - a - - - - c - - - - a - - - - A - - - - C - - - - G - - - - G - - - - C - - - - A - - - - T - - - - T - - - - A - - - - G - - - - G - - - - A - - - - A - - - - C - - - - A - - - - C - - - - T - - - - T - - - - C - - - - C - - - - G - - - - T - - - - C - - - - g - - - - t - - - - a - - - - a - - - - c - - - - a - - - - a - - - - c - - - - g - - - - c - - - - c - - - - t - - - - c - - - - a - - - - a - - - - c - - - - t - - - - t - - - - g - - - - t - - - - T - - - - G - - - - C - - - - C - - - - G - - - - T - - - - A - - - - A - - - - T - - - - C - - - - C - - - - T - - - - T - - - - G - - - - T - - - - G - - - - A - - - - A - - - - G - - - - G - - - - C - - - - A - - - - G - - - - M - - - - T - - - - A - - - - S - - - - N - - - - F - - - - L - - - - P - - - - M - - - - L - - - - F - - - - V - - - - E - - - - T - - - - - - Q - - - - Q - - - - P - - - - T - - - - S - - - - C - - - - R - - - - C - - - - * - - - - S - - - - C - - - - K - - - - R - - - - - - N - - - - N - - - - R - - - - L - - - - Q - - - - V - - - - V - - - - A - - - - N - - - - P - - - - V - - - - S - - - - G - - - - D - - -
-
- - - - P - - - - S - - - - L - - - - T - - - - F - - - - I - - - - R - - - - L - - - - * - - - - L - - - - V - - - - L - - - - * - - - - P - - - - W - - - - S - - - - V - - - - S - - - - H - - - - F - - - - Y - - - - T - - - - I - - - - M - - - - I - - - - G - - - - S - - - - L - - - - A - - - - L - - - - - - R - - - - L - - - - S - - - - L - - - - L - - - - Y - - - - D - - - - Y - - - - D - - - - W - - - - F - - - - F - - - - S - - - - L - - - - - - T - - - - C - - - - C - - - - G - - - - T - - - - C - - - - T - - - - C - - - - t - - - - c - - - - a - - - - c - - - - t - - - - t - - - - t - - - - t - - - - a - - - - t - - - - a - - - - c - - - - g - - - - a - - - - t - - - - t - - - - a - - - - t - - - - g - - - - a - - - - t - - - - t - - - - g - - - - g - - - - t - - - - t - - - - c - - - - t - - - - t - - - - t - - - - a - - - - g - - - - c - - - - c - - - - t - - - - t - - - - g - - - - g - - - - A - - - - G - - - - G - - - - C - - - - A - - - - G - - - - A - - - - G - - - - a - - - - g - - - - t - - - - g - - - - a - - - - a - - - - a - - - - a - - - - t - - - - a - - - - t - - - - g - - - - c - - - - t - - - - a - - - - a - - - - t - - - - a - - - - c - - - - t - - - - a - - - - a - - - - c - - - - c - - - - a - - - - a - - - - g - - - - a - - - - a - - - - a - - - - t - - - - c - - - - g - - - - g - - - - a - - - - a - - - - c - - - - c - - - - T - - - - E - - - - * - - - - K - - - - * - - - - V - - - - I - - - - I - - - - I - - - - P - - - - E - - - - K - - - - A - - - - K - - - - - - R - - - - R - - - - E - - - - S - - - - K - - - - Y - - - - S - - - - * - - - - S - - - - Q - - - - N - - - - K - - - - L - - - - R - - - - P - - - - G - - - - D - - - - R - - - - V - - - - K - - - - I - - - - R - - - - N - - - - H - - - - N - - - - T - - - - R - - - - * - - - - G - - - - Q - - - - -
+ + + L + + + + L + + + + R + + + + S + + + + * + + + + T + + + + T + + + + A + + + + L + + + + G + + + + T + + + + L + + + + P + + + + + + I + + + + V + + + + A + + + + E + + + + L + + + + N + + + + N + + + + G + + + + I + + + + R + + + + N + + + + T + + + + S + + + + V + + + + H + + + + C + + + + C + + + + G + + + + V + + + + E + + + + Q + + + + R + + + + H + + + + * + + + + E + + + + H + + + + F + + + + R + + + + + + c + + + + a + + + + t + + + + t + + + + g + + + + t + + + + t + + + + g + + + + c + + + + g + + + + g + + + + a + + + + g + + + + t + + + + t + + + + g + + + + a + + + + a + + + + c + + + + a + + + + A + + + + C + + + + G + + + + G + + + + C + + + + A + + + + T + + + + T + + + + A + + + + G + + + + G + + + + A + + + + A + + + + C + + + + A + + + + C + + + + T + + + + T + + + + C + + + + C + + + + G + + + + T + + + + C + + + + g + + + + t + + + + a + + + + a + + + + c + + + + a + + + + a + + + + c + + + + g + + + + c + + + + c + + + + t + + + + c + + + + a + + + + a + + + + c + + + + t + + + + t + + + + g + + + + t + + + + T + + + + G + + + + C + + + + C + + + + G + + + + T + + + + A + + + + A + + + + T + + + + C + + + + C + + + + T + + + + T + + + + G + + + + T + + + + G + + + + A + + + + A + + + + G + + + + G + + + + C + + + + A + + + + G + + + + M + + + + T + + + + A + + + + S + + + + N + + + + F + + + + L + + + + P + + + + M + + + + L + + + + F + + + + V + + + + E + + + + T + + + + + + Q + + + + Q + + + + P + + + + T + + + + S + + + + C + + + + R + + + + C + + + + * + + + + S + + + + C + + + + K + + + + R + + + + + + N + + + + N + + + + R + + + + L + + + + Q + + + + V + + + + V + + + + A + + + + N + + + + P + + + + V + + + + S + + + + G + + + + D + + +
+
+ + + + P + + + + S + + + + L + + + + T + + + + F + + + + I + + + + R + + + + L + + + + * + + + + L + + + + V + + + + L + + + + * + + + + P + + + + W + + + + S + + + + V + + + + S + + + + H + + + + F + + + + Y + + + + T + + + + I + + + + M + + + + I + + + + G + + + + S + + + + L + + + + A + + + + L + + + + + + R + + + + L + + + + S + + + + L + + + + L + + + + Y + + + + D + + + + Y + + + + D + + + + W + + + + F + + + + F + + + + S + + + + L + + + + + + T + + + + C + + + + C + + + + G + + + + T + + + + C + + + + T + + + + C + + + + t + + + + c + + + + a + + + + c + + + + t + + + + t + + + + t + + + + t + + + + a + + + + t + + + + a + + + + c + + + + g + + + + a + + + + t + + + + t + + + + a + + + + t + + + + g + + + + a + + + + t + + + + t + + + + g + + + + g + + + + t + + + + t + + + + c + + + + t + + + + t + + + + t + + + + a + + + + g + + + + c + + + + c + + + + t + + + + t + + + + g + + + + g + + + + A + + + + G + + + + G + + + + C + + + + A + + + + G + + + + A + + + + G + + + + a + + + + g + + + + t + + + + g + + + + a + + + + a + + + + a + + + + a + + + + t + + + + a + + + + t + + + + g + + + + c + + + + t + + + + a + + + + a + + + + t + + + + a + + + + c + + + + t + + + + a + + + + a + + + + c + + + + c + + + + a + + + + a + + + + g + + + + a + + + + a + + + + a + + + + t + + + + c + + + + g + + + + g + + + + a + + + + a + + + + c + + + + c + + + + T + + + + E + + + + * + + + + K + + + + * + + + + V + + + + I + + + + I + + + + I + + + + P + + + + E + + + + K + + + + A + + + + K + + + + + + R + + + + R + + + + E + + + + S + + + + K + + + + Y + + + + S + + + + * + + + + S + + + + Q + + + + N + + + + K + + + + L + + + + R + + + + P + + + + G + + + + D + + + + R + + + + V + + + + K + + + + I + + + + R + + + + N + + + + H + + + + N + + + + T + + + + R + + + + * + + + + G + + + + Q + + + +
-
-
+
+
diff --git a/test_data/volvox/config.json b/test_data/volvox/config.json index 96a4e51bf67..ac299222354 100644 --- a/test_data/volvox/config.json +++ b/test_data/volvox/config.json @@ -176,9 +176,6 @@ } ], "configuration": { - "rpc": { - "defaultDriver": "MainThreadRpcDriver" - }, "extraThemes": { "darkForest": { "name": "Dark (forest, from config file)",