From b64caab125b83b36e811f777c639845650d81c5d Mon Sep 17 00:00:00 2001 From: Shashank Budhanuru Ramaraju Date: Wed, 30 Oct 2024 14:53:02 +0000 Subject: [PATCH 1/6] adding whole new feature from gff/gtf track --- .../annotationFromJBrowseFeature.ts | 265 ++++++++++++++++++ .../src/extensions/index.ts | 1 + packages/jbrowse-plugin-apollo/src/index.ts | 9 +- 3 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts diff --git a/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts b/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts new file mode 100644 index 000000000..c6254402a --- /dev/null +++ b/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts @@ -0,0 +1,265 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import DisplayType from '@jbrowse/core/pluggableElementTypes/DisplayType' +import PluggableElementBase from '@jbrowse/core/pluggableElementTypes/PluggableElementBase' +import AddIcon from '@mui/icons-material/Add' +import { + doesIntersect2, + getContainingView, + getSession, +} from '@jbrowse/core/util' +import { Assembly } from '@jbrowse/core/assemblyManager/assembly' +import { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view' +import { AnnotationFeatureSnapshot } from '@apollo-annotation/mst' +import ObjectID from 'bson-objectid' +import { SimpleFeatureSerializedNoId } from '@jbrowse/core/util/simpleFeature' +import { AddFeatureChange } from '@apollo-annotation/shared' +import { ApolloSessionModel } from '../session' + +// Map Jbrowse SimpleFeature to Apollo AnnotationFeature. This is similar to gff3ToAnnotationFeature.ts +function simpleFeatureToAnnotationFeature( + feature: SimpleFeatureSerializedNoId, + refSeqId: string, + featureIds: string[], +) { + if (!feature.type) { + throw new Error(`feature does not have type: ${JSON.stringify(feature)}`) + } + + const { end, start, strand } = feature + const f: AnnotationFeatureSnapshot = { + _id: ObjectID().toHexString(), + refSeq: refSeqId, + min: start, + max: end, + type: feature.type, + strand: strand as 1 | -1 | undefined, + } + const convertedChildren = convertSubFeatures(feature, refSeqId, featureIds) + + if (convertedChildren) { + f.children = convertedChildren + } + featureIds.push(f._id) + return f +} + +function convertSubFeatures( + feature: SimpleFeatureSerializedNoId, + refSeqId: string, + featureIds: string[], +) { + if (!feature.subfeatures) { + return + } + const children: Record = {} + const cdsFeatures: SimpleFeatureSerializedNoId[] = [] + for (const subFeature of feature.subfeatures) { + if ( + subFeature.type === 'three_prime_UTR' || + subFeature.type === 'five_prime_UTR' || + subFeature.type === 'intron' || + subFeature.type === 'start_codon' || + subFeature.type === 'stop_codon' + ) { + continue + } + if (subFeature.type === 'CDS') { + cdsFeatures.push(subFeature) + } else { + const child = simpleFeatureToAnnotationFeature( + subFeature, + refSeqId, + featureIds, + ) + children[child._id] = child + } + } + const processedCDS = + cdsFeatures.length > 0 ? processCDS(cdsFeatures, refSeqId, featureIds) : [] + for (const cds of processedCDS) { + children[cds._id] = cds + } + + if (Object.keys(children).length > 0) { + return children + } + return +} + +function getFeatureMinMax( + cdsFeatures: SimpleFeatureSerializedNoId[], +): [number, number] { + const mins = cdsFeatures.map((f) => f.start) + const maxes = cdsFeatures.map((f) => f.end) + const min = Math.min(...mins) + const max = Math.max(...maxes) + return [min, max] +} + +function processCDS( + cdsFeatures: SimpleFeatureSerializedNoId[], + refSeqId: string, + featureIds: string[], +): AnnotationFeatureSnapshot[] { + const annotationFeatures: AnnotationFeatureSnapshot[] = [] + const cdsWithIds: Record = {} + const cdsWithoutIds: SimpleFeatureSerializedNoId[] = [] + + for (const cds of cdsFeatures) { + if ('id' in cds) { + const id = cds.id as string + cdsWithIds[id] = cdsWithIds[id] ?? [] + cdsWithIds[id].push(cds) + } else { + cdsWithoutIds.push(cds) + } + } + + for (const [, cds] of Object.entries(cdsWithIds)) { + const [min, max] = getFeatureMinMax(cds) + const f: AnnotationFeatureSnapshot = { + _id: ObjectID().toHexString(), + refSeq: refSeqId, + min, + max, + type: 'CDS', + strand: cds[0].strand as 1 | -1 | undefined, + } + featureIds.push(f._id) + annotationFeatures.push(f) + } + + if (cdsWithoutIds.length === 0) { + return annotationFeatures + } + + // If we don't have ID CDS features then check If there are overlapping CDS + // features, If they're not overlapping then assume it's a single CDS feature + const sortedCDSLocations = cdsWithoutIds.sort( + (cdsA, cdsB) => cdsA.start - cdsB.start, + ) + const overlapping = sortedCDSLocations.some((loc, idx) => { + const nextLoc = sortedCDSLocations.at(idx + 1) + if (!nextLoc) { + return false + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return doesIntersect2(loc.start, loc.end, nextLoc.start, nextLoc.end) + }) + // If no overlaps, assume it's a single CDS feature + if (!overlapping) { + const [min, max] = getFeatureMinMax(sortedCDSLocations) + const f: AnnotationFeatureSnapshot = { + _id: ObjectID().toHexString(), + refSeq: refSeqId, + min, + max, + type: 'CDS', + strand: sortedCDSLocations[0].strand as 1 | -1 | undefined, + } + featureIds.push(f._id) + annotationFeatures.push(f) + } + // TODO: Handle overlapping CDS features + + return annotationFeatures +} + +export function annotationFromJBrowseFeature( + pluggableElement: PluggableElementBase, +) { + if (pluggableElement.name !== 'LinearBasicDisplay') { + return pluggableElement + } + const { stateModel } = pluggableElement as DisplayType + + const newStateModel = stateModel + .views((self) => ({ + getFirstRegion() { + const lgv = getContainingView(self) as unknown as LinearGenomeViewModel + return lgv.dynamicBlocks.contentBlocks[0] + }, + getAssembly() { + const firstRegion = self.getFirstRegion() + const session = getSession(self) + const { assemblyManager } = session + const { assemblyName } = firstRegion + const assembly = assemblyManager.get(assemblyName) + if (!assembly) { + throw new Error(`Could not find assembly named ${assemblyName}`) + } + return assembly + }, + getRefSeqId(assembly: Assembly) { + const firstRegion = self.getFirstRegion() + const { refName } = firstRegion + const { refNameAliases } = assembly + if (!refNameAliases) { + throw new Error(`Could not find aliases for ${assembly.name}`) + } + const newRefNames = [...Object.entries(refNameAliases)] + .filter(([id, refName]) => id !== refName) + .map(([id, refName]) => ({ + _id: id, + name: refName ?? '', + })) + const refSeqId = newRefNames.find((item) => item.name === refName)?._id + if (!refSeqId) { + throw new Error(`Could not find refSeqId named ${refName}`) + } + return refSeqId + }, + async createApolloAnnotation() { + const session = getSession(self) + // Map SimpleFeature to Apollo AnnotationFeature + const feature: SimpleFeatureSerializedNoId = + self.contextMenuFeature.data + const assembly = self.getAssembly() + const refSeqId = self.getRefSeqId(assembly) + const featureIds: string[] = [] + const annotationFeature = simpleFeatureToAnnotationFeature( + feature, + refSeqId, + featureIds, + ) + + // Add feature to Apollo + const change = new AddFeatureChange({ + changedIds: [annotationFeature._id], + typeName: 'AddFeatureChange', + assembly: assembly.name, + addedFeature: annotationFeature, + }) + await ( + session as unknown as ApolloSessionModel + ).apolloDataStore.changeManager.submit(change) + session.notify('Annotation added successfully', 'success') + }, + })) + .views((self) => { + const superContextMenuItems = self.contextMenuItems + return { + contextMenuItems() { + const feature = self.contextMenuFeature + if (!feature) { + return superContextMenuItems() + } + return [ + ...superContextMenuItems(), + { + label: 'Create Apollo annotation', + icon: AddIcon, + onClick: self.createApolloAnnotation, + }, + ] + }, + } + }) + + ;(pluggableElement as DisplayType).stateModel = newStateModel + return pluggableElement +} diff --git a/packages/jbrowse-plugin-apollo/src/extensions/index.ts b/packages/jbrowse-plugin-apollo/src/extensions/index.ts index 263910ef7..b3c5d2d30 100644 --- a/packages/jbrowse-plugin-apollo/src/extensions/index.ts +++ b/packages/jbrowse-plugin-apollo/src/extensions/index.ts @@ -1 +1,2 @@ export * from './annotationFromPileup' +export * from './annotationFromJBrowseFeature' diff --git a/packages/jbrowse-plugin-apollo/src/index.ts b/packages/jbrowse-plugin-apollo/src/index.ts index 849ff5739..0289b9f71 100644 --- a/packages/jbrowse-plugin-apollo/src/index.ts +++ b/packages/jbrowse-plugin-apollo/src/index.ts @@ -55,7 +55,10 @@ import { ViewCheckResults, } from './components' import ApolloPluginConfigurationSchema from './config' -import { annotationFromPileup } from './extensions' +import { + annotationFromPileup, + annotationFromJBrowseFeature, +} from './extensions' import { ApolloFeatureDetailsWidget, ApolloFeatureDetailsWidgetModel, @@ -276,6 +279,10 @@ export default class ApolloPlugin extends Plugin { 'Core-extendPluggableElement', annotationFromPileup, ) + pluginManager.addToExtensionPoint( + 'Core-extendPluggableElement', + annotationFromJBrowseFeature, + ) if (!inWebWorker) { pluginManager.addToExtensionPoint( 'Core-extendWorker', From 91897bc14d43cba1262bd6ed5b186f2d5f578942 Mon Sep 17 00:00:00 2001 From: Shashank Budhanuru Ramaraju Date: Wed, 30 Oct 2024 15:32:43 +0000 Subject: [PATCH 2/6] create whole new feature from gff/gtf track --- .../annotationFromJBrowseFeature.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts b/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts index c6254402a..5bf55e0a1 100644 --- a/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts +++ b/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts @@ -43,10 +43,38 @@ function simpleFeatureToAnnotationFeature( if (convertedChildren) { f.children = convertedChildren } + + const convertedAttributes = convertFeatureAttributes(feature) + f.attributes = convertedAttributes featureIds.push(f._id) return f } +function convertFeatureAttributes( + feature: SimpleFeatureSerializedNoId, +): Record { + const attributes: Record = {} + const defaultFields = new Set([ + 'start', + 'end', + 'type', + 'strand', + 'refName', + 'subfeatures', + 'derived_features', + 'phase', + ]) + for (const [key, value] of Object.entries(feature)) { + if (defaultFields.has(key)) { + continue + } + attributes[key] = Array.isArray(value) + ? value.map((v) => (typeof v === 'string' ? v : String(v))) + : [typeof value === 'string' ? value : String(value)] + } + return attributes +} + function convertSubFeatures( feature: SimpleFeatureSerializedNoId, refSeqId: string, @@ -128,6 +156,7 @@ function processCDS( max, type: 'CDS', strand: cds[0].strand as 1 | -1 | undefined, + attributes: convertFeatureAttributes(cds[0]), } featureIds.push(f._id) annotationFeatures.push(f) @@ -160,6 +189,7 @@ function processCDS( max, type: 'CDS', strand: sortedCDSLocations[0].strand as 1 | -1 | undefined, + attributes: convertFeatureAttributes(sortedCDSLocations[0]), } featureIds.push(f._id) annotationFeatures.push(f) From fbe64fef8b9ff3c3382f433aa0bbd4d223b2d6b3 Mon Sep 17 00:00:00 2001 From: Shashank Budhanuru Ramaraju Date: Mon, 4 Nov 2024 11:00:30 +0000 Subject: [PATCH 3/6] create annotation from gff track --- .../src/components/CreateApolloAnnotation.tsx | 298 ++++++++++++++++++ .../annotationFromJBrowseFeature.ts | 92 ++++-- 2 files changed, 360 insertions(+), 30 deletions(-) create mode 100644 packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx diff --git a/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx b/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx new file mode 100644 index 000000000..03da4f386 --- /dev/null +++ b/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx @@ -0,0 +1,298 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable unicorn/no-useless-undefined */ +/* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-misused-promises */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import React, { useEffect, useMemo, useState } from 'react' + +import { + Box, + Button, + Checkbox, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + FormControlLabel, + MenuItem, + Select, + SelectChangeEvent, + Typography, +} from '@mui/material' + +import { Dialog } from './Dialog' +import { ApolloSessionModel } from '../session' +import { AnnotationFeatureSnapshot } from '@apollo-annotation/mst' +import { getRoot } from 'mobx-state-tree' +import { ApolloRootModel } from '../types' +import { ApolloInternetAccountModel } from '../ApolloInternetAccount/model' +import { AddFeatureChange } from '@apollo-annotation/shared' +import { Assembly } from '@jbrowse/core/assemblyManager/assembly' +import { AbstractSessionModel } from '@jbrowse/core/util' + +interface CreateApolloAnnotationProps { + session: AbstractSessionModel + handleClose(): void + annotationFeature: AnnotationFeatureSnapshot + assembly: Assembly +} + +// TODO: Integrate SO +const isGeneOrTranscript = (annotationFeature: AnnotationFeatureSnapshot) => { + return ( + annotationFeature.type === 'gene' || + annotationFeature.type === 'mRNA' || + annotationFeature.type === 'transcript' + ) +} + +// TODO: Integrate SO +const isTranscript = (annotationFeature: AnnotationFeatureSnapshot) => { + return ( + annotationFeature.type === 'mRNA' || annotationFeature.type === 'transcript' + ) +} + +export function CreateApolloAnnotation({ + annotationFeature, + assembly, + handleClose, + session, +}: CreateApolloAnnotationProps) { + const apolloSessionModel = session as unknown as ApolloSessionModel + const { internetAccounts } = getRoot(session) + const apolloInternetAccounts = internetAccounts.filter( + (ia) => ia.type === 'ApolloInternetAccount', + ) as ApolloInternetAccountModel[] + if (apolloInternetAccounts.length === 0) { + throw new Error('No Apollo internet account found') + } + const [apolloInternetAccount] = apolloInternetAccounts + const childIds = useMemo( + () => Object.keys(annotationFeature.children ?? {}), + [annotationFeature], + ) + + const [parentFeatureChecked, setParentFeatureChecked] = useState(true) + const [checkedChildrens, setCheckedChildrens] = useState(childIds) + const [errorMessage, setErrorMessage] = useState('') + const [destinationFeatures, setDestinationFeatures] = useState< + AnnotationFeatureSnapshot[] + >([]) + const [selectedDestinationFeature, setSelectedDestinationFeature] = + useState() + + useEffect(() => { + const getFeatures = async (min: number, max: number) => { + const { baseURL } = apolloInternetAccount + const uri = new URL('features/getFeatures', baseURL) + const searchParams = new URLSearchParams({ + refSeq: annotationFeature.refSeq, + start: min.toString(), + end: max.toString(), + }) + uri.search = searchParams.toString() + const fetch = apolloInternetAccount.getFetcher({ + locationType: 'UriLocation', + uri: uri.toString(), + }) + + const response = await fetch(uri.toString(), { method: 'GET' }) + + if (response.ok) { + const [features] = (await response.json()) as [ + AnnotationFeatureSnapshot[], + ] + return features + } + return [] + } + + if (childIds.every((childId) => checkedChildrens.includes(childId))) { + setParentFeatureChecked(true) + } else { + setSelectedDestinationFeature(undefined) + setParentFeatureChecked(false) + + if (annotationFeature.children) { + const checkedAnnotationFeatureChildren = Object.values( + annotationFeature.children, + ) + .filter((child) => isTranscript(child)) + .filter((child) => checkedChildrens.includes(child._id)) + const mins = checkedAnnotationFeatureChildren.map((f) => f.min) + const maxes = checkedAnnotationFeatureChildren.map((f) => f.max) + const min = Math.min(...mins) + const max = Math.max(...maxes) + + getFeatures(min, max) + .then((features) => { + setDestinationFeatures(features) + }) + .catch(() => { + setErrorMessage('Failed to fetch destination features') + }) + } + } + }, [checkedChildrens]) + + const handleParentFeatureCheck = ( + event: React.ChangeEvent, + ) => { + const isChecked = event.target.checked + setParentFeatureChecked(isChecked) + setCheckedChildrens(isChecked ? childIds : []) + } + + const handleChildFeatureCheck = ( + event: React.ChangeEvent, + child: AnnotationFeatureSnapshot, + ) => { + setCheckedChildrens((prevChecked) => + event.target.checked + ? [...prevChecked, child._id] + : prevChecked.filter((childId) => childId !== child._id), + ) + } + + const handleDestinationFeatureChange = (e: SelectChangeEvent) => { + const selectedFeature = destinationFeatures.find( + (f) => f._id === e.target.value, + ) + setSelectedDestinationFeature(selectedFeature) + } + + const handleCreateApolloAnnotation = async () => { + if (parentFeatureChecked) { + const change = new AddFeatureChange({ + changedIds: [annotationFeature._id], + typeName: 'AddFeatureChange', + assembly: assembly.name, + addedFeature: annotationFeature, + }) + await apolloSessionModel.apolloDataStore.changeManager.submit(change) + session.notify('Annotation added successfully', 'success') + handleClose() + } else { + if (!annotationFeature.children) { + return + } + if (!selectedDestinationFeature) { + return + } + + for (const childId of checkedChildrens) { + const child = annotationFeature.children[childId] + const change = new AddFeatureChange({ + parentFeatureId: selectedDestinationFeature._id, + changedIds: [selectedDestinationFeature._id], + typeName: 'AddFeatureChange', + assembly: assembly.name, + addedFeature: child, + }) + await apolloSessionModel.apolloDataStore.changeManager.submit(change) + session.notify('Annotation added successfully', 'success') + handleClose() + } + } + } + + return ( + + + Select the feature to be copied to apollo track + + + + {isGeneOrTranscript(annotationFeature) && ( + + } + label={`${annotationFeature.type}:${annotationFeature.min}..${annotationFeature.max}`} + /> + )} + {annotationFeature.children && ( + + {Object.values(annotationFeature.children) + .filter((child) => isTranscript(child)) + .map((child) => ( + { + handleChildFeatureCheck(e, child) + }} + /> + } + label={`${child.type}:${child.min}..${child.max}`} + /> + ))} + + )} + + {!parentFeatureChecked && + checkedChildrens.length > 0 && + destinationFeatures.length > 0 && ( + + + Select the destination feature to copy the selected features + + + + + + + )} + + + + + + {errorMessage ? ( + + {errorMessage} + + ) : null} + + ) +} diff --git a/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts b/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts index 5bf55e0a1..9dc1ddc69 100644 --- a/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts +++ b/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts @@ -7,6 +7,7 @@ import DisplayType from '@jbrowse/core/pluggableElementTypes/DisplayType' import PluggableElementBase from '@jbrowse/core/pluggableElementTypes/PluggableElementBase' import AddIcon from '@mui/icons-material/Add' import { + AbstractSessionModel, doesIntersect2, getContainingView, getSession, @@ -16,8 +17,7 @@ import { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view' import { AnnotationFeatureSnapshot } from '@apollo-annotation/mst' import ObjectID from 'bson-objectid' import { SimpleFeatureSerializedNoId } from '@jbrowse/core/util/simpleFeature' -import { AddFeatureChange } from '@apollo-annotation/shared' -import { ApolloSessionModel } from '../session' +import { CreateApolloAnnotation } from '../components/CreateApolloAnnotation' // Map Jbrowse SimpleFeature to Apollo AnnotationFeature. This is similar to gff3ToAnnotationFeature.ts function simpleFeatureToAnnotationFeature( @@ -44,8 +44,7 @@ function simpleFeatureToAnnotationFeature( f.children = convertedChildren } - const convertedAttributes = convertFeatureAttributes(feature) - f.attributes = convertedAttributes + f.attributes = convertFeatureAttributes(feature) featureIds.push(f._id) return f } @@ -68,9 +67,7 @@ function convertFeatureAttributes( if (defaultFields.has(key)) { continue } - attributes[key] = Array.isArray(value) - ? value.map((v) => (typeof v === 'string' ? v : String(v))) - : [typeof value === 'string' ? value : String(value)] + attributes[key] = Array.isArray(value) ? value.map(String) : [String(value)] } return attributes } @@ -176,7 +173,6 @@ function processCDS( if (!nextLoc) { return false } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return doesIntersect2(loc.start, loc.end, nextLoc.start, nextLoc.end) }) // If no overlaps, assume it's a single CDS feature @@ -194,7 +190,44 @@ function processCDS( featureIds.push(f._id) annotationFeatures.push(f) } - // TODO: Handle overlapping CDS features + + const groupedLocations: SimpleFeatureSerializedNoId[][] = [] + for (const location of cdsWithoutIds) { + const lastGroup = groupedLocations.at(-1) + if (!lastGroup) { + groupedLocations.push([location]) + continue + } + const overlaps = lastGroup.some((lastGroupLoc) => + doesIntersect2( + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + lastGroupLoc.start, + lastGroupLoc.end, + location.start, + location.end, + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + ), + ) + if (overlaps) { + groupedLocations.push([location]) + } else { + lastGroup.push(location) + } + } + for (const group of groupedLocations) { + const [min, max] = getFeatureMinMax(group) + const f: AnnotationFeatureSnapshot = { + _id: ObjectID().toHexString(), + refSeq: refSeqId, + min, + max, + type: 'CDS', + strand: group[0].strand as 1 | -1 | undefined, + attributes: convertFeatureAttributes(group[0]), + } + featureIds.push(f._id) + annotationFeatures.push(f) + } return annotationFeatures } @@ -243,35 +276,20 @@ export function annotationFromJBrowseFeature( } return refSeqId }, - async createApolloAnnotation() { - const session = getSession(self) + getAnnotationFeature(assembly: Assembly) { // Map SimpleFeature to Apollo AnnotationFeature const feature: SimpleFeatureSerializedNoId = self.contextMenuFeature.data - const assembly = self.getAssembly() const refSeqId = self.getRefSeqId(assembly) const featureIds: string[] = [] - const annotationFeature = simpleFeatureToAnnotationFeature( - feature, - refSeqId, - featureIds, - ) - - // Add feature to Apollo - const change = new AddFeatureChange({ - changedIds: [annotationFeature._id], - typeName: 'AddFeatureChange', - assembly: assembly.name, - addedFeature: annotationFeature, - }) - await ( - session as unknown as ApolloSessionModel - ).apolloDataStore.changeManager.submit(change) - session.notify('Annotation added successfully', 'success') + return simpleFeatureToAnnotationFeature(feature, refSeqId, featureIds) }, })) .views((self) => { const superContextMenuItems = self.contextMenuItems + const session = getSession(self) + const assembly = self.getAssembly() + return { contextMenuItems() { const feature = self.contextMenuFeature @@ -283,7 +301,21 @@ export function annotationFromJBrowseFeature( { label: 'Create Apollo annotation', icon: AddIcon, - onClick: self.createApolloAnnotation, + onClick: () => { + ;(session as unknown as AbstractSessionModel).queueDialog( + (doneCallback) => [ + CreateApolloAnnotation, + { + session, + handleClose: () => { + doneCallback() + }, + annotationFeature: self.getAnnotationFeature(assembly), + assembly, + }, + ], + ) + }, }, ] }, From 907dba9f29d45e85b4e88fc35172aa9cf633866f Mon Sep 17 00:00:00 2001 From: Shashank Budhanuru Ramaraju Date: Mon, 4 Nov 2024 11:14:44 +0000 Subject: [PATCH 4/6] fix lint errors --- .../src/components/CreateApolloAnnotation.tsx | 3 --- .../src/extensions/annotationFromJBrowseFeature.ts | 2 -- 2 files changed, 5 deletions(-) diff --git a/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx b/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx index 03da4f386..4abdfdf71 100644 --- a/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx +++ b/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx @@ -1,9 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable unicorn/no-useless-undefined */ -/* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-misused-promises */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import React, { useEffect, useMemo, useState } from 'react' import { diff --git a/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts b/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts index 9dc1ddc69..2c837cce6 100644 --- a/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts +++ b/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts @@ -200,12 +200,10 @@ function processCDS( } const overlaps = lastGroup.some((lastGroupLoc) => doesIntersect2( - /* eslint-disable @typescript-eslint/no-non-null-assertion */ lastGroupLoc.start, lastGroupLoc.end, location.start, location.end, - /* eslint-enable @typescript-eslint/no-non-null-assertion */ ), ) if (overlaps) { From ade202ffafd818035d1617940473b2c35af75638 Mon Sep 17 00:00:00 2001 From: Shashank Budhanuru Ramaraju Date: Mon, 18 Nov 2024 11:15:15 +0000 Subject: [PATCH 5/6] fixes --- .../src/components/CreateApolloAnnotation.tsx | 102 +++++++++--------- .../annotationFromJBrowseFeature.ts | 1 + 2 files changed, 49 insertions(+), 54 deletions(-) diff --git a/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx b/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx index 4abdfdf71..2b36bdc71 100644 --- a/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx +++ b/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx @@ -21,9 +21,7 @@ import { import { Dialog } from './Dialog' import { ApolloSessionModel } from '../session' import { AnnotationFeatureSnapshot } from '@apollo-annotation/mst' -import { getRoot } from 'mobx-state-tree' -import { ApolloRootModel } from '../types' -import { ApolloInternetAccountModel } from '../ApolloInternetAccount/model' +import { getSnapshot } from 'mobx-state-tree' import { AddFeatureChange } from '@apollo-annotation/shared' import { Assembly } from '@jbrowse/core/assemblyManager/assembly' import { AbstractSessionModel } from '@jbrowse/core/util' @@ -33,6 +31,7 @@ interface CreateApolloAnnotationProps { handleClose(): void annotationFeature: AnnotationFeatureSnapshot assembly: Assembly + refSeqId: string } // TODO: Integrate SO @@ -55,22 +54,28 @@ export function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, + refSeqId, session, }: CreateApolloAnnotationProps) { const apolloSessionModel = session as unknown as ApolloSessionModel - const { internetAccounts } = getRoot(session) - const apolloInternetAccounts = internetAccounts.filter( - (ia) => ia.type === 'ApolloInternetAccount', - ) as ApolloInternetAccountModel[] - if (apolloInternetAccounts.length === 0) { - throw new Error('No Apollo internet account found') - } - const [apolloInternetAccount] = apolloInternetAccounts const childIds = useMemo( () => Object.keys(annotationFeature.children ?? {}), [annotationFeature], ) + const features = useMemo(() => { + for (const [, asm] of apolloSessionModel.apolloDataStore.assemblies) { + if (asm._id === assembly.name) { + for (const [, refSeq] of asm.refSeqs) { + if (refSeq._id === refSeqId) { + return refSeq.features + } + } + } + } + return [] + }, []) + const [parentFeatureChecked, setParentFeatureChecked] = useState(true) const [checkedChildrens, setCheckedChildrens] = useState(childIds) const [errorMessage, setErrorMessage] = useState('') @@ -80,56 +85,45 @@ export function CreateApolloAnnotation({ const [selectedDestinationFeature, setSelectedDestinationFeature] = useState() - useEffect(() => { - const getFeatures = async (min: number, max: number) => { - const { baseURL } = apolloInternetAccount - const uri = new URL('features/getFeatures', baseURL) - const searchParams = new URLSearchParams({ - refSeq: annotationFeature.refSeq, - start: min.toString(), - end: max.toString(), - }) - uri.search = searchParams.toString() - const fetch = apolloInternetAccount.getFetcher({ - locationType: 'UriLocation', - uri: uri.toString(), - }) - - const response = await fetch(uri.toString(), { method: 'GET' }) + const getFeatures = (min: number, max: number) => { + const filteredFeatures: AnnotationFeatureSnapshot[] = [] - if (response.ok) { - const [features] = (await response.json()) as [ - AnnotationFeatureSnapshot[], - ] - return features + for (const [, f] of features) { + const featureSnapshot = getSnapshot(f) + if (min >= featureSnapshot.min && max <= featureSnapshot.max) { + filteredFeatures.push(featureSnapshot) } - return [] } - if (childIds.every((childId) => checkedChildrens.includes(childId))) { - setParentFeatureChecked(true) - } else { - setSelectedDestinationFeature(undefined) + return filteredFeatures + } + + useEffect(() => { + setErrorMessage('') + if (checkedChildrens.length === 0) { setParentFeatureChecked(false) + return + } - if (annotationFeature.children) { - const checkedAnnotationFeatureChildren = Object.values( - annotationFeature.children, - ) - .filter((child) => isTranscript(child)) - .filter((child) => checkedChildrens.includes(child._id)) - const mins = checkedAnnotationFeatureChildren.map((f) => f.min) - const maxes = checkedAnnotationFeatureChildren.map((f) => f.max) - const min = Math.min(...mins) - const max = Math.max(...maxes) + if (annotationFeature.children) { + const checkedAnnotationFeatureChildren = Object.values( + annotationFeature.children, + ) + .filter((child) => isTranscript(child)) + .filter((child) => checkedChildrens.includes(child._id)) + const mins = checkedAnnotationFeatureChildren.map((f) => f.min) + const maxes = checkedAnnotationFeatureChildren.map((f) => f.max) + const min = Math.min(...mins) + const max = Math.max(...maxes) + const filteredFeatures = getFeatures(min, max) + setDestinationFeatures(filteredFeatures) - getFeatures(min, max) - .then((features) => { - setDestinationFeatures(features) - }) - .catch(() => { - setErrorMessage('Failed to fetch destination features') - }) + if ( + filteredFeatures.length === 0 && + checkedChildrens.length > 0 && + !parentFeatureChecked + ) { + setErrorMessage('No destination features found') } } }, [checkedChildrens]) diff --git a/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts b/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts index 2c837cce6..40da3d2de 100644 --- a/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts +++ b/packages/jbrowse-plugin-apollo/src/extensions/annotationFromJBrowseFeature.ts @@ -310,6 +310,7 @@ export function annotationFromJBrowseFeature( }, annotationFeature: self.getAnnotationFeature(assembly), assembly, + refSeqId: self.getRefSeqId(assembly), }, ], ) From 414688e7805d1cbbafb987681ba0dd7f9733f767 Mon Sep 17 00:00:00 2001 From: Shashank Budhanuru Ramaraju Date: Mon, 18 Nov 2024 11:24:44 +0000 Subject: [PATCH 6/6] fix lint --- .../src/components/CreateApolloAnnotation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx b/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx index 2b36bdc71..659ea8aaa 100644 --- a/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx +++ b/packages/jbrowse-plugin-apollo/src/components/CreateApolloAnnotation.tsx @@ -1,5 +1,4 @@ /* eslint-disable react-hooks/exhaustive-deps */ -/* eslint-disable unicorn/no-useless-undefined */ /* eslint-disable @typescript-eslint/no-misused-promises */ import React, { useEffect, useMemo, useState } from 'react'