diff --git a/packages/apollo-collaboration-server/.development.env b/packages/apollo-collaboration-server/.development.env index 4a746102..5fcf3227 100644 --- a/packages/apollo-collaboration-server/.development.env +++ b/packages/apollo-collaboration-server/.development.env @@ -32,10 +32,10 @@ SESSION_SECRET=g9fGaRuw06T7hs960Tm7KYyfcFaYEIaG9jfFnVEQ4QyFXmq7 ############################################################################## # Google client id and secret. -GOOGLE_CLIENT_ID=1054515969695-3hpfg1gd0ld3sgj135kfgikolu86vv30.apps.googleusercontent.com +GOOGLE_CLIENT_ID=1000521104117-bhd8r4v11cc053g0b80ui00ss9s5fitv.apps.googleusercontent.com # Alternatively, can be a path to a file with the client ID # GOOGLE_CLIENT_ID_FILE=/run/secrets/google-client-id -GOOGLE_CLIENT_SECRET=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 +GOOGLE_CLIENT_SECRET=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 # Alternatively, can be a path to a file with the client secret # GOOGLE_CLIENT_SECRET_FILE=/run/secrets/google-client-secret diff --git a/packages/apollo-mst/src/AnnotationFeatureModel.ts b/packages/apollo-mst/src/AnnotationFeatureModel.ts index 49ce671a..a75350e9 100644 --- a/packages/apollo-mst/src/AnnotationFeatureModel.ts +++ b/packages/apollo-mst/src/AnnotationFeatureModel.ts @@ -1,4 +1,4 @@ -import { intersection2 } from '@jbrowse/core/util' +import { getSession, intersection2 } from '@jbrowse/core/util' import { IAnyModelType, IMSTMap, @@ -127,7 +127,14 @@ export const AnnotationFeatureModel = types return false }, get transcriptParts(): TranscriptParts[] { - if (self.type !== 'mRNA') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any + const session = getSession(self) as any + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { apolloDataStore } = session + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const { featureTypeOntology } = apolloDataStore.ontologyManager + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + if (!featureTypeOntology.isTypeOf(self.type, 'mRNA')) { throw new Error( 'Only features of type "mRNA" or equivalent can calculate CDS locations', ) @@ -137,7 +144,8 @@ export const AnnotationFeatureModel = types throw new Error('no CDS or exons in mRNA') } const cdsChildren = [...children.values()].filter( - (child) => child.type === 'CDS', + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + (child) => featureTypeOntology.isTypeOf(child.type, 'CDS'), ) if (cdsChildren.length === 0) { throw new Error('no CDS in mRNA') @@ -149,7 +157,8 @@ export const AnnotationFeatureModel = types let hasIntersected = false const exonLocations: TranscriptPartLocation[] = [] for (const [, child] of children) { - if (child.type === 'exon') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + if (featureTypeOntology.isTypeOf(child.type, 'exon')) { exonLocations.push({ min: child.min, max: child.max }) } } diff --git a/packages/jbrowse-plugin-apollo/cypress.config.js b/packages/jbrowse-plugin-apollo/cypress.config.js index b3bf4ed0..5c905924 100644 --- a/packages/jbrowse-plugin-apollo/cypress.config.js +++ b/packages/jbrowse-plugin-apollo/cypress.config.js @@ -11,7 +11,7 @@ module.exports = defineConfig({ // Make viewport long and thin to avoid the scrollbar on the right interfere // with the coordinates viewportHeight: 2000, - viewportWidth: 1000, + viewportWidth: 1500, retries: { runMode: 2, }, diff --git a/packages/jbrowse-plugin-apollo/cypress/data/go.json.gz b/packages/jbrowse-plugin-apollo/cypress/data/go.json.gz index 04a84ed6..cd589bb7 100644 Binary files a/packages/jbrowse-plugin-apollo/cypress/data/go.json.gz and b/packages/jbrowse-plugin-apollo/cypress/data/go.json.gz differ diff --git a/packages/jbrowse-plugin-apollo/cypress/data/so.json.gz b/packages/jbrowse-plugin-apollo/cypress/data/so.json.gz new file mode 100644 index 00000000..4056964b Binary files /dev/null and b/packages/jbrowse-plugin-apollo/cypress/data/so.json.gz differ diff --git a/packages/jbrowse-plugin-apollo/cypress/e2e/editFeature.cy.ts b/packages/jbrowse-plugin-apollo/cypress/e2e/editFeature.cy.ts index 5a25a3d6..4f23b3af 100644 --- a/packages/jbrowse-plugin-apollo/cypress/e2e/editFeature.cy.ts +++ b/packages/jbrowse-plugin-apollo/cypress/e2e/editFeature.cy.ts @@ -174,7 +174,7 @@ describe('Different ways of editing features', () => { cy.contains('td', '=CDS1').should('not.exist') }) - it('Suggest only valid SO terms from dropdown', () => { + it.only('Suggest only valid SO terms from dropdown', () => { cy.addAssemblyFromGff('onegene.fasta.gff3', 'test_data/onegene.fasta.gff3') cy.selectAssemblyToView('onegene.fasta.gff3') cy.searchFeatures('gx1', 1) @@ -186,6 +186,7 @@ describe('Different ways of editing features', () => { timeout: 60_000, force: true, }) + cy.contains('li', /^start_codon$/, { timeout: 60_000, matchCase: false, diff --git a/packages/jbrowse-plugin-apollo/cypress/support/commands.ts b/packages/jbrowse-plugin-apollo/cypress/support/commands.ts index 5c071376..35fcd50b 100644 --- a/packages/jbrowse-plugin-apollo/cypress/support/commands.ts +++ b/packages/jbrowse-plugin-apollo/cypress/support/commands.ts @@ -34,6 +34,13 @@ async function loadOntology( OntologyKey, unknown[] > + // @ts-expect-error could use more typing + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + ontologyData.meta[0].storeOptions.prefixes = new Map( + // @ts-expect-error could use more typing + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access + Object.entries(ontologyData.meta[0].storeOptions.prefixes), + ) await openDB(name, version, { async upgrade(database: IDBPDatabase): Promise { const meta = database.createObjectStore('meta') @@ -76,7 +83,7 @@ Cypress.Commands.add('addOntologies', () => { }, { name: 'Sequence Ontology', - version: '3.1', + version: 'unversioned', source: { uri: 'http://localhost:9000/test_data/so-v3.1.json', locationType: 'UriLocation', @@ -94,6 +101,16 @@ Cypress.Commands.add('addOntologies', () => { { timeout: 120_000 }, ) }) + cy.readFile('cypress/data/so.json.gz', null).then((soGZip: Buffer) => { + cy.wrap>( + loadOntology( + soGZip, + 'Apollo Ontology "Sequence Ontology" "unversioned"', + 2, + ), + { timeout: 120_000 }, + ) + }) }) Cypress.Commands.add('addAssemblyFromGff', (assemblyName, fin) => { diff --git a/packages/jbrowse-plugin-apollo/src/FeatureDetailsWidget/TranscriptSequence.tsx b/packages/jbrowse-plugin-apollo/src/FeatureDetailsWidget/TranscriptSequence.tsx index 4cbe5485..e4ffba3c 100644 --- a/packages/jbrowse-plugin-apollo/src/FeatureDetailsWidget/TranscriptSequence.tsx +++ b/packages/jbrowse-plugin-apollo/src/FeatureDetailsWidget/TranscriptSequence.tsx @@ -169,7 +169,11 @@ export const TranscriptSequence = observer(function TranscriptSequence({ if (!refSeq) { return null } - if (feature.type !== 'mRNA') { + const { featureTypeOntology } = session.apolloDataStore.ontologyManager + if (!featureTypeOntology) { + throw new Error('featureTypeOntology is undefined') + } + if (featureTypeOntology.isTypeOf(feature.type, 'mRNA')) { return null } diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/GeneGlyph.ts b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/GeneGlyph.ts index f8e01be7..e35e991f 100644 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +++ b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/GeneGlyph.ts @@ -12,6 +12,7 @@ import { CanvasMouseEvent } from '../types' import { Glyph } from './Glyph' import { boxGlyph } from './BoxGlyph' import { LinearApolloDisplayRendering } from '../stateModel/rendering' +import { OntologyRecord } from '../../OntologyManager' let forwardFillLight: CanvasPattern | null = null let backwardFillLight: CanvasPattern | null = null @@ -80,6 +81,11 @@ function draw( return } const { apolloSelectedFeature } = session + const { apolloDataStore } = session + const { featureTypeOntology } = apolloDataStore.ontologyManager + if (!featureTypeOntology) { + throw new Error('featureTypeOntology is undefined') + } // Draw background for gene const topLevelFeatureMinX = @@ -93,7 +99,8 @@ function draw( ? topLevelFeatureMinX - topLevelFeatureWidthPx : topLevelFeatureMinX const topLevelFeatureTop = row * rowHeight - const topLevelFeatureHeight = getRowCount(feature) * rowHeight + const topLevelFeatureHeight = + getRowCount(feature, featureTypeOntology) * rowHeight ctx.fillStyle = alpha(theme?.palette.background.paper ?? '#ffffff', 0.6) ctx.fillRect( @@ -106,7 +113,8 @@ function draw( // Draw lines on different rows for each mRNA let currentRow = 0 for (const [, mrna] of children) { - if (mrna.type !== 'mRNA') { + const isMrna = featureTypeOntology.isTypeOf(mrna.type, 'mRNA') + if (!isMrna) { currentRow += 1 continue } @@ -115,7 +123,7 @@ function draw( continue } for (const [, cds] of childrenOfmRNA) { - if (cds.type !== 'CDS') { + if (!featureTypeOntology.isTypeOf(cds.type, 'CDS')) { continue } const minX = @@ -144,7 +152,7 @@ function draw( // Draw exon and CDS for each mRNA currentRow = 0 for (const [, child] of children) { - if (child.type !== 'mRNA') { + if (!featureTypeOntology.isTypeOf(child.type, 'mRNA')) { boxGlyph.draw(ctx, child, row, stateModel, displayedRegionIndex) currentRow += 1 continue @@ -155,7 +163,7 @@ function draw( continue } for (const [, exon] of childrenOfmRNA) { - if (exon.type !== 'exon') { + if (!featureTypeOntology.isTypeOf(exon.type, 'exon')) { continue } const minX = @@ -296,7 +304,9 @@ function drawHover( stateModel: LinearApolloDisplay, ctx: CanvasRenderingContext2D, ) { - const { apolloHover, apolloRowHeight, lgv, theme } = stateModel + const { apolloHover, apolloRowHeight, lgv, session, theme } = stateModel + const { featureTypeOntology } = session.apolloDataStore.ontologyManager + if (!apolloHover) { return } @@ -320,16 +330,26 @@ function drawHover( const top = row * apolloRowHeight const widthPx = length / bpPerPx ctx.fillStyle = theme?.palette.action.selected ?? 'rgba(0,0,0,04)' - ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount(feature)) + + if (!featureTypeOntology) { + throw new Error('featureTypeOntology is undefined') + } + ctx.fillRect( + startPx, + top, + widthPx, + apolloRowHeight * getRowCount(feature, featureTypeOntology), + ) } function getFeatureFromLayout( feature: AnnotationFeature, bp: number, row: number, + featureTypeOntology: OntologyRecord, ): AnnotationFeature | undefined { const featureInThisRow: AnnotationFeature[] = - featuresForRow(feature)[row] || [] + featuresForRow(feature, featureTypeOntology)[row] || [] for (const f of featureInThisRow) { let featureObj if (bp >= f.min && bp <= f.max && f.parent) { @@ -339,9 +359,9 @@ function getFeatureFromLayout( continue } if ( - featureObj.type === 'CDS' && + featureTypeOntology.isTypeOf(featureObj.type, 'CDS') && featureObj.parent && - featureObj.parent.type === 'mRNA' + featureTypeOntology.isTypeOf(featureObj.parent.type, 'mRNA') ) { const { cdsLocations } = featureObj.parent for (const cdsLoc of cdsLocations) { @@ -361,22 +381,28 @@ function getFeatureFromLayout( return feature } -function getRowCount(feature: AnnotationFeature, _bpPerPx?: number): number { +function getRowCount( + feature: AnnotationFeature, + featureTypeOntology: OntologyRecord, + _bpPerPx?: number, +): number { const { children, type } = feature if (!children) { return 1 } + const isMrna = featureTypeOntology.isTypeOf(type, 'mRNA') let rowCount = 0 - if (type === 'mRNA') { + if (isMrna) { for (const [, child] of children) { - if (child.type === 'CDS') { + const isCds = featureTypeOntology.isTypeOf(child.type, 'CDS') + if (isCds) { rowCount += 1 } } return rowCount } for (const [, child] of children) { - rowCount += getRowCount(child) + rowCount += getRowCount(child, featureTypeOntology) } return rowCount } @@ -387,8 +413,12 @@ function getRowCount(feature: AnnotationFeature, _bpPerPx?: number): number { * If the row contains an mRNA, the order is CDS -\> exon -\> mRNA -\> gene * If the row does not contain an mRNA, the order is subfeature -\> gene */ -function featuresForRow(feature: AnnotationFeature): AnnotationFeature[][] { - if (feature.type !== 'gene') { +function featuresForRow( + feature: AnnotationFeature, + featureTypeOntology: OntologyRecord, +): AnnotationFeature[][] { + const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene') + if (!isGene) { throw new Error('Top level feature for GeneGlyph must have type "gene"') } const { children } = feature @@ -397,7 +427,7 @@ function featuresForRow(feature: AnnotationFeature): AnnotationFeature[][] { } const features: AnnotationFeature[][] = [] for (const [, child] of children) { - if (child.type !== 'mRNA') { + if (!featureTypeOntology.isTypeOf(child.type, 'mRNA')) { features.push([child, feature]) continue } @@ -407,9 +437,9 @@ function featuresForRow(feature: AnnotationFeature): AnnotationFeature[][] { const cdss: AnnotationFeature[] = [] const exons: AnnotationFeature[] = [] for (const [, grandchild] of child.children) { - if (grandchild.type === 'CDS') { + if (featureTypeOntology.isTypeOf(grandchild.type, 'CDS')) { cdss.push(grandchild) - } else if (grandchild.type === 'exon') { + } else if (featureTypeOntology.isTypeOf(grandchild.type, 'exon')) { exons.push(grandchild) } } @@ -423,8 +453,9 @@ function featuresForRow(feature: AnnotationFeature): AnnotationFeature[][] { function getRowForFeature( feature: AnnotationFeature, childFeature: AnnotationFeature, + featureTypeOntology: OntologyRecord, ) { - const rows = featuresForRow(feature) + const rows = featuresForRow(feature, featureTypeOntology) for (const [idx, row] of rows.entries()) { if (row.some((feature) => feature._id === childFeature._id)) { return idx @@ -496,7 +527,16 @@ function getDraggableFeatureInfo( feature: AnnotationFeature, stateModel: LinearApolloDisplay, ): { feature: AnnotationFeature; edge: 'min' | 'max' } | undefined { - if (feature.type === 'gene' || feature.type === 'mRNA') { + const { session } = stateModel + const { apolloDataStore } = session + const { featureTypeOntology } = apolloDataStore.ontologyManager + if (!featureTypeOntology) { + throw new Error('featureTypeOntology is undefined') + } + const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene') + const isMrna = featureTypeOntology.isTypeOf(feature.type, 'mRNA') + const isCds = featureTypeOntology.isTypeOf(feature.type, 'CDS') + if (isGene || isMrna) { return } const { bp, refName, regionNumber, x } = mousePosition @@ -519,14 +559,19 @@ function getDraggableFeatureInfo( if (Math.abs(maxPx - x) < 4) { return { feature, edge: 'max' } } - if (feature.type === 'CDS') { + if (isCds) { const mRNA = feature.parent if (!mRNA?.children) { return } - const exonChildren = [...mRNA.children.values()].filter( - (child) => child.type === 'exon', - ) + const exonChildren: AnnotationFeature[] = [] + for (const child of mRNA.children.values()) { + const childIsExon = featureTypeOntology.isTypeOf(child.type, 'exon') + if (childIsExon) { + exonChildren.push(child) + } + } + const overlappingExon = exonChildren.find((child) => { const [start, end] = intersection2(bp, bp + 1, child.min, child.max) return start !== undefined && end !== undefined diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/Glyph.ts b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/Glyph.ts index d360518d..849e8561 100644 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/Glyph.ts +++ b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/Glyph.ts @@ -7,10 +7,15 @@ import { } from '../stateModel/mouseEvents' import { LinearApolloDisplayRendering } from '../stateModel/rendering' import { CanvasMouseEvent } from '../types' +import { OntologyRecord } from '../../OntologyManager' export interface Glyph { /** @returns number of layout rows used by this glyph with this feature and zoom level */ - getRowCount(feature: AnnotationFeature, bpPerPx: number): number + getRowCount( + feature: AnnotationFeature, + featureTypeOntology: OntologyRecord, + bpPerPx: number, + ): number /** draw the feature's primary rendering on the canvas */ draw( ctx: CanvasRenderingContext2D, @@ -24,10 +29,12 @@ export interface Glyph { feature: AnnotationFeature, bp: number, row: number, + featureTypeOntology: OntologyRecord, ): AnnotationFeature | undefined getRowForFeature( feature: AnnotationFeature, childFeature: AnnotationFeature, + featureTypeOntology: OntologyRecord, ): number | undefined drawHover( diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/getGlyph.ts b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/getGlyph.ts deleted file mode 100644 index a3db48a8..00000000 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/getGlyph.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { AnnotationFeature } from '@apollo-annotation/mst' - -import { boxGlyph, geneGlyph, genericChildGlyph } from '../glyphs' -import { Glyph } from '../glyphs/Glyph' - -/** get the appropriate glyph for the given top-level feature */ -export function getGlyph(feature: AnnotationFeature): Glyph { - if (looksLikeGene(feature)) { - return geneGlyph - } - if (feature.children?.size) { - return genericChildGlyph - } - return boxGlyph -} - -function looksLikeGene(feature: AnnotationFeature) { - const { children } = feature - if (!children?.size) { - return false - } - for (const [, child] of children) { - if (child.type === 'mRNA') { - const { children: grandChildren } = child - if (!grandChildren?.size) { - return false - } - const hasCDS = [...grandChildren.values()].some( - (grandchild) => grandchild.type === 'CDS', - ) - const hasExon = [...grandChildren.values()].some( - (grandchild) => grandchild.type === 'exon', - ) - if (hasCDS && hasExon) { - return true - } - } - } - return false -} diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/layouts.ts b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/layouts.ts index 4e1943a7..900b3751 100644 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/layouts.ts +++ b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/layouts.ts @@ -9,7 +9,7 @@ import { addDisposer, isAlive } from 'mobx-state-tree' import { ApolloSessionModel } from '../../session' import { baseModelFactory } from './base' -import { getGlyph } from './getGlyph' +import { boxGlyph, geneGlyph, genericChildGlyph } from '../glyphs' export function layoutsModelFactory( pluginManager: PluginManager, @@ -60,6 +60,48 @@ export function layoutsModelFactory( return }) }, + getGlyph(feature: AnnotationFeature) { + if (this.looksLikeGene(feature)) { + return geneGlyph + } + if (feature.children?.size) { + return genericChildGlyph + } + return boxGlyph + }, + looksLikeGene(feature: AnnotationFeature): boolean { + const { featureTypeOntology } = + self.session.apolloDataStore.ontologyManager + if (!featureTypeOntology) { + return false + } + const { children } = feature + if (!children?.size) { + return false + } + const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene') + if (!isGene) { + return false + } + for (const [, child] of children) { + if (featureTypeOntology.isTypeOf(child.type, 'mRNA')) { + const { children: grandChildren } = child + if (!grandChildren?.size) { + return false + } + const hasCDS = [...grandChildren.values()].some((grandchild) => + featureTypeOntology.isTypeOf(grandchild.type, 'CDS'), + ) + const hasExon = [...grandChildren.values()].some((grandchild) => + featureTypeOntology.isTypeOf(grandchild.type, 'exon'), + ) + if (hasCDS && hasExon) { + return true + } + } + } + return false + }, })) .actions((self) => ({ addSeenFeature(feature: AnnotationFeature) { @@ -94,10 +136,14 @@ export function layoutsModelFactory( ) { continue } - const rowCount = getGlyph(feature).getRowCount( - feature, - self.lgv.bpPerPx, - ) + const { featureTypeOntology } = + self.session.apolloDataStore.ontologyManager + if (!featureTypeOntology) { + throw new Error('featureTypeOntology is undefined') + } + const rowCount = self + .getGlyph(feature) + .getRowCount(feature, featureTypeOntology, self.lgv.bpPerPx) let startingRow = 0 let placed = false while (!placed) { @@ -158,6 +204,8 @@ export function layoutsModelFactory( }, getFeatureLayoutPosition(feature: AnnotationFeature) { const { featureLayouts } = this + const { featureTypeOntology } = + self.session.apolloDataStore.ontologyManager for (const [idx, layout] of featureLayouts.entries()) { for (const [layoutRowNum, layoutRow] of layout) { for (const [featureRowNum, layoutFeature] of layoutRow) { @@ -174,10 +222,12 @@ export function layoutsModelFactory( } } if (layoutFeature.hasDescendant(feature._id)) { - const row = getGlyph(layoutFeature).getRowForFeature( - layoutFeature, - feature, - ) + if (!featureTypeOntology) { + throw new Error('featureTypeOntology is undefined') + } + const row = self + .getGlyph(layoutFeature) + .getRowForFeature(layoutFeature, feature, featureTypeOntology) if (row !== undefined) { return { layoutIndex: idx, diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/mouseEvents.ts b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/mouseEvents.ts index 34b649e0..aad7a7f6 100644 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/mouseEvents.ts +++ b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/mouseEvents.ts @@ -15,7 +15,6 @@ import type { CSSProperties } from 'react' import { Coord } from '../components' import { Glyph } from '../glyphs/Glyph' import { CanvasMouseEvent } from '../types' -import { getGlyph } from './getGlyph' import { renderingModelFactory } from './rendering' import { Frame, getFrame } from '@jbrowse/core/util' @@ -147,11 +146,17 @@ export function mouseEventsModelIntermediateFactory( return mousePosition } const [featureRow, topLevelFeature] = foundFeature - const glyph = getGlyph(topLevelFeature) + const glyph = self.getGlyph(topLevelFeature) + const { featureTypeOntology } = + self.session.apolloDataStore.ontologyManager + if (!featureTypeOntology) { + throw new Error('featureTypeOntology is undefined') + } const feature = glyph.getFeatureFromLayout( topLevelFeature, bp, featureRow, + featureTypeOntology, ) if (!feature) { return mousePosition @@ -227,15 +232,27 @@ export function mouseEventsSeqHightlightModelFactory( self.lgv.bpPerPx <= 1 ? 125 : 95, ) - const { apolloHover, lgv, regions, sequenceRowHeight, theme } = self + const { + apolloHover, + lgv, + regions, + sequenceRowHeight, + session, + theme, + } = self if (!apolloHover) { return } const { feature } = apolloHover + const { featureTypeOntology } = + session.apolloDataStore.ontologyManager + if (!featureTypeOntology) { + throw new Error('featureTypeOntology is undefined') + } for (const [idx, region] of regions.entries()) { - if (feature.type === 'CDS') { + if (featureTypeOntology.isTypeOf(feature.type, 'CDS')) { const parentFeature = feature.parent if (!parentFeature) { continue @@ -323,7 +340,7 @@ export function mouseEventsModelFactory( return [] } const { topLevelFeature } = apolloHover - const glyph = getGlyph(topLevelFeature) + const glyph = self.getGlyph(topLevelFeature) return glyph.getContextMenuItems(self) }, })) @@ -483,7 +500,9 @@ export function mouseEventsModelFactory( if (apolloDragging) { // NOTE: the glyph where the drag started is responsible for drawing the preview. // it can call methods in other glyphs to help with this though. - const glyph = getGlyph(apolloDragging.feature.topLevelFeature) + const glyph = self.getGlyph( + apolloDragging.feature.topLevelFeature, + ) glyph.drawDragPreview(self, ctx) } }, diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/rendering.ts b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/rendering.ts index 35cb0b32..67d2e998 100644 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/rendering.ts +++ b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/rendering.ts @@ -7,7 +7,6 @@ import { autorun } from 'mobx' import { Instance, addDisposer } from 'mobx-state-tree' import { ApolloSessionModel } from '../../session' -import { getGlyph } from './getGlyph' import { layoutsModelFactory } from './layouts' export function renderingModelIntermediateFactory( @@ -428,7 +427,7 @@ export function renderingModelFactory( ) { continue } - getGlyph(feature).draw(ctx, feature, row, self, idx) + self.getGlyph(feature).draw(ctx, feature, row, self, idx) } } } diff --git a/packages/jbrowse-plugin-apollo/src/OntologyManager/OntologyStore/index.ts b/packages/jbrowse-plugin-apollo/src/OntologyManager/OntologyStore/index.ts index 09763c1a..65781120 100644 --- a/packages/jbrowse-plugin-apollo/src/OntologyManager/OntologyStore/index.ts +++ b/packages/jbrowse-plugin-apollo/src/OntologyManager/OntologyStore/index.ts @@ -76,6 +76,7 @@ export interface OntologyStoreOptions { indexFields?: { displayName: string; jsonPath: string }[] } maxSearchResults?: number + update?(message: string, progress: number): void } export interface PropertiesOptions { @@ -115,8 +116,8 @@ export default class OntologyStore { this.ontologyName = name this.ontologyVersion = version this.sourceLocation = source - this.db = this.prepareDatabase() this.options = options ?? {} + this.db = this.prepareDatabase() } /** @@ -182,9 +183,12 @@ export default class OntologyStore { } try { - const { sourceLocation, sourceType } = this + const { options, sourceLocation, sourceType } = this if (sourceType === 'obo-graph-json') { + options.update?.('', 0) + // add more updates inside `loadOboGraphJson` await this.loadOboGraphJson(db) + options.update?.('', 100) } else { throw new Error( `ontology source file ${JSON.stringify( diff --git a/packages/jbrowse-plugin-apollo/src/OntologyManager/OntologyStore/indexeddb-storage.ts b/packages/jbrowse-plugin-apollo/src/OntologyManager/OntologyStore/indexeddb-storage.ts index 2e0beab7..d4795883 100644 --- a/packages/jbrowse-plugin-apollo/src/OntologyManager/OntologyStore/indexeddb-storage.ts +++ b/packages/jbrowse-plugin-apollo/src/OntologyManager/OntologyStore/indexeddb-storage.ts @@ -81,6 +81,8 @@ function serializeWords(foundWords: Iterable<[string, string]>): string[] { export async function loadOboGraphJson(this: OntologyStore, db: Database) { const startTime = Date.now() + let percent_progress = 1 + this.options.update?.('Parsing JSON', percent_progress) // TODO: using file streaming along with an event-based json parser // instead of JSON.parse and .readFile could probably make this faster // and less memory intensive @@ -93,6 +95,9 @@ export async function loadOboGraphJson(this: OntologyStore, db: Database) { throw new Error('Error in loading ontology') } + percent_progress += 5 + this.options.update?.('Parsing JSON complete', percent_progress) + const parseTime = Date.now() const [graph, ...additionalGraphs] = oboGraph.graphs ?? [] @@ -114,29 +119,52 @@ export async function loadOboGraphJson(this: OntologyStore, db: Database) { const fullTextIndexPaths = getTextIndexFields .call(this) .map((def) => def.jsonPath) - for (const node of graph.nodes ?? []) { - if (isOntologyDBNode(node)) { - await nodeStore.add({ - ...node, - fullTextWords: serializeWords( - getWords(node, fullTextIndexPaths, this.prefixes), - ), - }) + if (graph.nodes) { + let last_progress = Math.round(percent_progress) + for (const [, node] of graph.nodes.entries()) { + percent_progress += 64 * (1 / graph.nodes.length) + if ( + Math.round(percent_progress) != last_progress && + percent_progress < 100 + ) { + this.options.update?.('Processing nodes', percent_progress) + last_progress = Math.round(percent_progress) + } + if (isOntologyDBNode(node)) { + await nodeStore.add({ + ...node, + fullTextWords: serializeWords( + getWords(node, fullTextIndexPaths, this.prefixes), + ), + }) + } } } // load edges const edgeStore = tx.objectStore('edges') - for (const edge of graph.edges ?? []) { - if (isOntologyDBEdge(edge)) { - await edgeStore.add(edge) + if (graph.edges) { + let last_progress = Math.round(percent_progress) + for (const [, edge] of graph.edges.entries()) { + percent_progress += 30 * (1 / graph.edges.length) + if ( + Math.round(percent_progress) != last_progress && + percent_progress < 100 + ) { + this.options.update?.('Processing edges', percent_progress) + last_progress = Math.round(percent_progress) + } + if (isOntologyDBEdge(edge)) { + await edgeStore.add(edge) + } } } - await tx.done // record some metadata about this ontology and load operation const tx2 = db.transaction('meta', 'readwrite') + const { ...otherOptions } = this.options + otherOptions.update = undefined await tx2.objectStore('meta').add( { ontologyRecord: { @@ -144,7 +172,7 @@ export async function loadOboGraphJson(this: OntologyStore, db: Database) { version: this.ontologyVersion, sourceLocation: this.sourceLocation, }, - storeOptions: this.options, + storeOptions: otherOptions, graphMeta: graph.meta, timestamp: String(new Date()), schemaVersion, diff --git a/packages/jbrowse-plugin-apollo/src/OntologyManager/index.ts b/packages/jbrowse-plugin-apollo/src/OntologyManager/index.ts index d10f0edf..2c3e0c87 100644 --- a/packages/jbrowse-plugin-apollo/src/OntologyManager/index.ts +++ b/packages/jbrowse-plugin-apollo/src/OntologyManager/index.ts @@ -12,6 +12,7 @@ import { autorun } from 'mobx' import { Instance, addDisposer, + flow, getRoot, getSnapshot, types, @@ -30,6 +31,7 @@ export const OntologyRecordType = types version: 'unversioned', source: types.union(LocalPathLocation, UriLocation, BlobLocation), options: types.frozen(), + equivalentTypes: types.map(types.array(types.string)), }) .volatile((_self) => ({ dataStore: undefined as undefined | OntologyStore, @@ -55,6 +57,42 @@ export const OntologyRecordType = types }), ) }, + setEquivalentTypes(type: string, equivalentTypes: string[]) { + self.equivalentTypes.set(type, equivalentTypes) + }, + })) + .actions((self) => ({ + loadEquivalentTypes: flow(function* loadEquivalentTypes(type: string) { + if (!self.dataStore) { + return + } + const terms = (yield self.dataStore.getTermsWithLabelOrSynonym( + type, + )) as unknown as OntologyTerm[] + const equivalents: string[] = terms + .map((term) => term.lbl) + .filter((term) => term != undefined) + self.setEquivalentTypes(type, equivalents) + }), + })) + .views((self) => ({ + isTypeOf(queryType: string, typeOf: string): boolean { + if (queryType === typeOf) { + return true + } + if (!self.dataStore) { + return false + } + const equivalents = self.equivalentTypes.get(typeOf) + if (!equivalents) { + void self.loadEquivalentTypes(typeOf) + return false + } + if (equivalents.includes(queryType)) { + return true + } + return false + }, })) export const OntologyManagerType = types diff --git a/packages/jbrowse-plugin-apollo/src/SixFrameFeatureDisplay/stateModel.ts b/packages/jbrowse-plugin-apollo/src/SixFrameFeatureDisplay/stateModel.ts index 41c2dad8..7e29842e 100644 --- a/packages/jbrowse-plugin-apollo/src/SixFrameFeatureDisplay/stateModel.ts +++ b/packages/jbrowse-plugin-apollo/src/SixFrameFeatureDisplay/stateModel.ts @@ -270,6 +270,11 @@ export function stateModelFactory( return codonLayout }, get featureLayout() { + const { featureTypeOntology } = + self.session.apolloDataStore.ontologyManager + if (!featureTypeOntology) { + throw new Error('featureTypeOntology is undefined') + } const featureLayout = new Map() for (const [refSeq, featuresForRefSeq] of this.features || []) { if (!featuresForRefSeq) { @@ -295,11 +300,13 @@ export function stateModelFactory( }, )) { for (const [, childFeature] of feature.children ?? new Map()) { - if (childFeature.type === 'mRNA') { + if (featureTypeOntology.isTypeOf(childFeature.type, 'mRNA')) { for (const [, grandChildFeature] of childFeature.children || new Map()) { let startingRow - if (grandChildFeature.type === 'CDS') { + if ( + featureTypeOntology.isTypeOf(grandChildFeature.type, 'CDS') + ) { let discontinuousLocations if (grandChildFeature.discontinuousLocations.length > 0) { ;({ discontinuousLocations } = grandChildFeature) diff --git a/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/Feature.tsx b/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/Feature.tsx index 3e2efdd2..6b45b55b 100644 --- a/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/Feature.tsx +++ b/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/Feature.tsx @@ -21,7 +21,6 @@ import { FeatureAttributes } from './FeatureAttributes' import { featureContextMenuItems } from './featureContextMenuItems' import type { ContextMenuState } from './HybridGrid' import { NumberCell } from './NumberCell' -import { getGlyph } from '../../LinearApolloDisplay/stateModel/getGlyph' const useStyles = makeStyles()((theme) => ({ typeContent: { @@ -134,7 +133,7 @@ export const Feature = observer(function Feature({ displayState.setApolloHover({ feature, topLevelFeature: getTopLevelFeature(feature), - glyph: getGlyph(getTopLevelFeature(feature)), + glyph: displayState.getGlyph(getTopLevelFeature(feature)), }) }} className={ diff --git a/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/featureContextMenuItems.ts b/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/featureContextMenuItems.ts index 061bde14..774d92ea 100644 --- a/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +++ b/packages/jbrowse-plugin-apollo/src/TabularEditor/HybridGrid/featureContextMenuItems.ts @@ -137,7 +137,14 @@ export function featureContextMenuItems( }, }, ) - if (feature.type === 'mRNA' && isSessionModelWithWidgets(session)) { + const { featureTypeOntology } = session.apolloDataStore.ontologyManager + if (!featureTypeOntology) { + throw new Error('featureTypeOntology is undefined') + } + if ( + featureTypeOntology.isTypeOf(feature.type, 'mRNA') && + isSessionModelWithWidgets(session) + ) { menuItems.push({ label: 'Edit transcript details', onClick: () => { diff --git a/packages/jbrowse-plugin-apollo/src/session/ClientDataStore.ts b/packages/jbrowse-plugin-apollo/src/session/ClientDataStore.ts index 20cea3a5..c17ec3c1 100644 --- a/packages/jbrowse-plugin-apollo/src/session/ClientDataStore.ts +++ b/packages/jbrowse-plugin-apollo/src/session/ClientDataStore.ts @@ -42,6 +42,7 @@ import { } from '../OntologyManager' import { ApolloRootModel } from '../types' import { autorun } from 'mobx' +import { ApolloSessionModel } from './session' export function clientDataStoreFactory( AnnotationFeatureExtended: typeof AnnotationFeatureModel, @@ -164,8 +165,36 @@ export function clientDataStoreFactory( ) as TextIndexFieldDefinition[], ] if (!ontologyManager.findOntology(name)) { + const session = getSession( + self, + ) as unknown as ApolloSessionModel + const { jobsManager } = session + const controller = new AbortController() + const jobName = `Loading ontology "${name}"` + const job = { + name: jobName, + statusMessage: `Loading ontology "${name}", version "${version}", this may take awhile`, + progressPct: 0, + cancelCallback: () => { + controller.abort() + jobsManager.abortJob(job.name) + }, + } + const update = (message: string, progress: number): void => { + if (progress === 0) { + jobsManager.runJob(job) + return + } + if (progress === 100) { + jobsManager.done(job) + return + } + jobsManager.update(jobName, message, progress) + return + } ontologyManager.addOntology(name, version, source, { textIndexing: { indexFields }, + update, }) } } diff --git a/packages/jbrowse-plugin-apollo/test_data/so_types.gff3 b/packages/jbrowse-plugin-apollo/test_data/so_types.gff3 new file mode 100644 index 00000000..e5a1348a --- /dev/null +++ b/packages/jbrowse-plugin-apollo/test_data/so_types.gff3 @@ -0,0 +1,28 @@ +##gff-version 3 +chr1 VEuPathDB protein_coding_gene 1 200 . - . ID=TGGT1_200010 +chr1 VEuPathDB mRNA_with_frameshift 1 200 . - . ID=TGGT1_200010-t26_1;Parent=TGGT1_200010 +chr1 VEuPathDB interior_exon 1 50 . - . ID=exon_TGGT1_200010-t26_1-E2;Parent=TGGT1_200010-t26_1 +chr1 VEuPathDB interior_exon 70 100 . - . ID=exon_TGGT1_200010-t26_1-E1;Parent=TGGT1_200010-t26_1 +chr1 VEuPathDB CDS 1 50 . - 1 ID=TGGT1_200010-t26_1-p1-CDS2;Parent=TGGT1_200010-t26_1 +chr1 VEuPathDB CDS 70 100 . - 0 ID=TGGT1_200010-t26_1-p1-CDS1;Parent=TGGT1_200010-t26_1 +##FASTA +>chr1 +cattgttgcggagttgaacaacggcattacgaacacttccgtctctcacttttatacgat +tatgattggttctttacccttggtttacattggtactagtagcggcgctaatgctacctg +aattgagaactcgagcgggggctaggcaaattctgattcagcctgacttctcttggaacc +ctgcccataaatcaaagggttagtgcggccaaaacgttggacaacggtattagaagacca +acctgaccaccaaaccgtcaattaaccggtatcttctcggaaacggcggttctctcctag +atagcgatctgtggtctcaccatgcaatttaaacaggtgagtaaagattgctacaaatac +gagactagctgtcaccagatgctgttcatctgttggctccttggtcgctccgttgtaccc +aggctactttgaaagagcgcagaatacttagacggtatcgatcatggtagcatagcattc +tgataacatgtatggagttcgaacatccgtctggggccggacggtccgtttgaggttggt +tgatctgggtgatagtcagcaagatagacgttagataacaaattaaaggattttacctta +gattgcgactagtacaacggtacatcggtgattcgcgctctactagatcacgctatgggt +accataaacaaacggtggaccttctcaagctggttgacgcctcagcaacataggcttcct +cctccacgcatctcagcataaaaggcttataaactgcttctttgtgccagagcaactcaa +ttaagcccttggtaccgtgggcacgcattctgtcacggtgaccaactgttcatcctgaat +cgccgaatgggactatttggtacaggaatcaagcggatggcactactgcagcttatttac +gacggtattcttaaagtttttaagacaatgtatttcatgggtagttcggtttgttttatt +gctacacaggctcttgtagacgacctacttagcactacggccgagcgcaataacccccgg +aaagcacttgctactgggaggcgggtttatccatcggcaataggggttatcagtactacc +aagaagattgtgaagatattaacagcattgaaaaaagttcggactgggcatgaaacgtgt