Skip to content

Commit

Permalink
Add click and drag to gene glyphs (#269)
Browse files Browse the repository at this point in the history
* click and drag gene glyphs

* click and drag canonical gene glyph

* ImplicitExonGeneGlyph changes

* canonical gene glyph without execute

* changes

* cds discontinuous start/end location change

* change

* Fix import of discontinuousLocations features

* Draw locations in discontinuousLocations features

* Add discontinuous locations to grid editor

---------

Co-authored-by: Garrett Stevens <[email protected]>
  • Loading branch information
shashankbrgowda and garrettjstevens authored Oct 24, 2023
1 parent 08210d8 commit 5c57631
Show file tree
Hide file tree
Showing 15 changed files with 2,013 additions and 179 deletions.
87 changes: 47 additions & 40 deletions packages/apollo-common/src/AssemblySpecificChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,49 +199,47 @@ export abstract class AssemblySpecificChange extends Change {
const { featureModel, refSeqModel, user } = backend
const { assembly, logger, refSeqCache } = this

for (const featureLine of gff3Feature) {
const { seq_id: refName } = featureLine
if (!refName) {
throw new Error(
`Valid seq_id not found in feature ${JSON.stringify(featureLine)}`,
)
}
let refSeqDoc = refSeqCache.get(refName)
if (!refSeqDoc) {
refSeqDoc =
(await refSeqModel.findOne({ assembly, name: refName }).exec()) ??
undefined
if (refSeqDoc) {
refSeqCache.set(refName, refSeqDoc)
}
}
if (!refSeqDoc) {
throw new Error(
`RefSeq was not found by assembly "${assembly}" and seq_id "${refName}" not found`,
)
const [{ seq_id: refName }] = gff3Feature
if (!refName) {
throw new Error(
`Valid seq_id not found in feature ${JSON.stringify(gff3Feature)}`,
)
}
let refSeqDoc = refSeqCache.get(refName)
if (!refSeqDoc) {
refSeqDoc =
(await refSeqModel.findOne({ assembly, name: refName }).exec()) ??
undefined
if (refSeqDoc) {
refSeqCache.set(refName, refSeqDoc)
}
// Let's add featureId to parent feature
const featureIds: string[] = []

const newFeature = createFeature(gff3Feature, refSeqDoc._id, featureIds)
logger.debug?.(`So far feature ids are: ${featureIds.toString()}`)
// Add value to gffId
newFeature.attributes?._id
? (newFeature.gffId = newFeature.attributes?._id.toString())
: (newFeature.gffId = newFeature._id)
logger.debug?.(
`********************* Assembly specific change create ${JSON.stringify(
newFeature,
)}`,
}
if (!refSeqDoc) {
throw new Error(
`RefSeq was not found by assembly "${assembly}" and seq_id "${refName}" not found`,
)

// Add into Mongo
// We cannot use Mongo 'session' / transaction here because Mongo has 16 MB limit for transaction
const [newFeatureDoc] = await featureModel.create([
{ allIds: featureIds, ...newFeature, user, status: -1 },
])
logger.verbose?.(`Added docId "${newFeatureDoc._id}"`)
}
// Let's add featureId to parent feature
const featureIds: string[] = []

const newFeature = createFeature(gff3Feature, refSeqDoc._id, featureIds)
logger.debug?.(`So far feature ids are: ${featureIds.toString()}`)
// Add value to gffId
newFeature.attributes?._id
? (newFeature.gffId = newFeature.attributes?._id.toString())
: (newFeature.gffId = newFeature._id)
logger.debug?.(
`********************* Assembly specific change create ${JSON.stringify(
newFeature,
)}`,
)

// Add into Mongo
// We cannot use Mongo 'session' / transaction here because Mongo has 16 MB limit for transaction
const [newFeatureDoc] = await featureModel.create([
{ allIds: featureIds, ...newFeature, user, status: -1 },
])
logger.verbose?.(`Added docId "${newFeatureDoc._id}"`)
}
}

Expand Down Expand Up @@ -292,6 +290,15 @@ function createFeature(
end,
}
if (gff3Feature.length > 1) {
const lastEnd = Math.max(
...gff3Feature.map((f) => {
if (f.end === null) {
throw new Error(`feature does not have end: ${JSON.stringify(f)}`)
}
return f.end
}),
)
feature.end = lastEnd
feature.discontinuousLocations = gff3Feature.map((f) => {
const { end: subEnd, phase: locationPhase, start: subStart } = f
if (subStart === null || subEnd === null) {
Expand Down
18 changes: 18 additions & 0 deletions packages/apollo-mst/src/AnnotationFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ export const AnnotationFeature = types
self.start = start
}
},
setCDSDiscontinuousLocationStart(start: number, index: number) {
const dl = self.discontinuousLocations
if (dl && dl.length > 0 && dl[index].start !== start) {
dl[index].start = start
if (index === 0) {
self.start = start
}
}
},
setEnd(end: number) {
if (end < self.start) {
throw new Error(`End "${end}" is less than start "${self.start}"`)
Expand All @@ -121,6 +130,15 @@ export const AnnotationFeature = types
self.end = end
}
},
setCDSDiscontinuousLocationEnd(end: number, index: number) {
const dl = self.discontinuousLocations
if (dl && dl.length > 0 && dl[index].end !== end) {
dl[index].end = end
if (index === dl.length - 1) {
self.end = end
}
}
},
setStrand(strand?: 1 | -1) {
self.strand = strand
},
Expand Down
175 changes: 175 additions & 0 deletions packages/apollo-shared/src/Changes/DiscontinuousLocationEndChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import {
ChangeOptions,
ClientDataStore,
FeatureChange,
LocalGFF3DataStore,
SerializedFeatureChange,
ServerDataStore,
} from 'apollo-common'

interface SerializedDiscontinuousLocationEndChangeBase
extends SerializedFeatureChange {
typeName: 'DiscontinuousLocationEndChange'
}

interface DiscontinuousLocationEndChangeDetails {
featureId: string
oldEnd: number
newEnd: number
index: number
}

interface SerializedDiscontinuousLocationEndChangeSingle
extends SerializedDiscontinuousLocationEndChangeBase,
DiscontinuousLocationEndChangeDetails {}

interface SerializedDiscontinuousLocationEndChangeMultiple
extends SerializedDiscontinuousLocationEndChangeBase {
changes: DiscontinuousLocationEndChangeDetails[]
}

type SerializedDiscontinuousLocationEndChange =
| SerializedDiscontinuousLocationEndChangeSingle
| SerializedDiscontinuousLocationEndChangeMultiple

export class DiscontinuousLocationEndChange extends FeatureChange {
typeName = 'DiscontinuousLocationEndChange' as const
changes: DiscontinuousLocationEndChangeDetails[]

constructor(
json: SerializedDiscontinuousLocationEndChange,
options?: ChangeOptions,
) {
super(json, options)
this.changes = 'changes' in json ? json.changes : [json]
}

toJSON(): SerializedDiscontinuousLocationEndChange {
const { assembly, changedIds, changes, typeName } = this
if (changes.length === 1) {
const [{ featureId, index, newEnd, oldEnd }] = changes
return {
typeName,
changedIds,
assembly,
featureId,
oldEnd,
newEnd,
index,
}
}
return { typeName, changedIds, assembly, changes }
}

async executeOnServer(backend: ServerDataStore) {
const { featureModel, session } = backend
const { changes, logger } = this
for (const change of changes) {
const { featureId, index, newEnd, oldEnd: expectedOldEnd } = change
const topLevelFeature = await featureModel
.findOne({ allIds: featureId })
.session(session)
.exec()

if (!topLevelFeature) {
const errMsg = `ERROR: The following featureId was not found in database ='${featureId}'`
logger.error(errMsg)
throw new Error(errMsg)
}

const feature = this.getFeatureFromId(topLevelFeature, featureId)
if (!feature) {
const errMsg = 'ERROR when searching feature by featureId'
logger.error(errMsg)
throw new Error(errMsg)
}
logger.debug?.(`*** Found feature: ${JSON.stringify(feature)}`)
if (
!feature.discontinuousLocations ||
feature.discontinuousLocations.length === 0
) {
const errMsg =
'Must use "LocationEndChange" to change a feature end that does not have discontinuous locations'
logger.error(errMsg)
throw new Error(errMsg)
}
const oldEnd = feature.discontinuousLocations[index].end
if (oldEnd !== expectedOldEnd) {
const errMsg = `Location's current end value ${oldEnd} doesn't match with expected value ${expectedOldEnd}`
logger.error(errMsg)
throw new Error(errMsg)
}
const { start } = feature.discontinuousLocations[index]
if (newEnd <= start) {
const errMsg = `location end (${newEnd}) can't be smaller than location start (${start})`
logger.error(errMsg)
throw new Error(errMsg)
}
const nextLocation = feature.discontinuousLocations[index + 1]
if (nextLocation && newEnd >= nextLocation.start) {
const errMsg = `Location end (${newEnd}) can't be larger than the next location's start (${nextLocation.start})`
logger.error(errMsg)
throw new Error(errMsg)
}
feature.discontinuousLocations[index].end = newEnd
if (index === feature.discontinuousLocations.length - 1) {
feature.end = newEnd
}

try {
topLevelFeature.markModified('discontinuousLocations')
await topLevelFeature.save()
} catch (error) {
logger.debug?.(`*** FAILED: ${error}`)
throw error
}
}
}

async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
throw new Error('executeOnLocalGFF3 not implemented')
}

async executeOnClient(dataStore: ClientDataStore) {
if (!dataStore) {
throw new Error('No data store')
}
for (const [idx, changedId] of this.changedIds.entries()) {
const feature = dataStore.getFeature(changedId)
if (!feature) {
throw new Error(`Could not find feature with identifier "${changedId}"`)
}
const { index, newEnd } = this.changes[idx]
feature.setCDSDiscontinuousLocationEnd(newEnd, index)
}
}

getInverse() {
const { assembly, changedIds, changes, logger, typeName } = this
const inverseChangedIds = [...changedIds].reverse()
const inverseChanges = [...changes].reverse().map((change) => ({
featureId: change.featureId,
oldEnd: change.newEnd,
newEnd: change.oldEnd,
index: change.index,
}))
return new DiscontinuousLocationEndChange(
{
changedIds: inverseChangedIds,
typeName,
changes: inverseChanges,
assembly,
},
{ logger },
)
}
}

export function isDiscontinuousLocationEndChange(
change: unknown,
): change is DiscontinuousLocationEndChange {
return (
(change as DiscontinuousLocationEndChange).typeName ===
'DiscontinuousLocationEndChange'
)
}
Loading

0 comments on commit 5c57631

Please sign in to comment.