diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/BoxGlyph.ts b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/BoxGlyph.ts index f9cd63758..732e6c347 100644 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +++ b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/BoxGlyph.ts @@ -219,6 +219,31 @@ export class BoxGlyph extends Glyph { return false } + continueDrag( + stateModel: LinearApolloDisplay, + currentMousePosition: MousePosition, + ) { + const { feature, glyph, mousePosition, topLevelFeature } = + stateModel.apolloDragging?.start ?? {} + if (!(currentMousePosition && mousePosition)) { + return + } + stateModel.setDragging({ + start: { + feature, + topLevelFeature, + glyph, + mousePosition, + }, + current: { + feature, + topLevelFeature, + glyph, + mousePosition: currentMousePosition, + }, + }) + } + executeDrag(stateModel: LinearApolloDisplay) { const { apolloDragging, diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/CanonicalGeneGlyph.ts b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/CanonicalGeneGlyph.ts index 2527dccdc..b024c30fa 100644 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/CanonicalGeneGlyph.ts +++ b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/CanonicalGeneGlyph.ts @@ -1,6 +1,7 @@ import { AnnotationFeatureI } from 'apollo-mst' import { LinearApolloDisplay } from '../stateModel' +import { MousePosition } from '../stateModel/mouseEvents' import { CanvasMouseEvent } from '../types' import { Glyph } from './Glyph' @@ -446,6 +447,13 @@ export class CanonicalGeneGlyph extends Glyph { } } + continueDrag( + _display: LinearApolloDisplay, + _currentMousePosition: MousePosition, + ): void { + // pass + } + getFeatureFromLayout( feature: AnnotationFeatureI, bp: number, diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts index 458b11c35..58943610b 100644 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +++ b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts @@ -1,6 +1,7 @@ import { AnnotationFeatureI } from 'apollo-mst' import { LinearApolloDisplay } from '../stateModel' +import { MousePosition } from '../stateModel/mouseEvents' import { CanvasMouseEvent } from '../types' import { Glyph } from './Glyph' @@ -136,6 +137,13 @@ export class GenericChildGlyph extends Glyph { } } + continueDrag( + _display: LinearApolloDisplay, + _currentMousePosition: MousePosition, + ): void { + // pass + } + getFeatureFromLayout(feature: AnnotationFeatureI, bp: number, row: number) { const layoutRow = this.featuresForRow(feature)[row] return layoutRow?.find((f) => bp >= f.start && bp <= f.end) diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/Glyph.ts b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/Glyph.ts index bbed0f59a..a35c507a2 100644 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/Glyph.ts +++ b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/Glyph.ts @@ -7,7 +7,10 @@ import { DeleteFeature, ModifyFeatureAttribute, } from '../../components' -import { LinearApolloDisplayMouseEvents } from '../stateModel/mouseEvents' +import { + LinearApolloDisplayMouseEvents, + MousePosition, +} from '../stateModel/mouseEvents' import { LinearApolloDisplayRendering } from '../stateModel/rendering' import { CanvasMouseEvent } from '../types' @@ -32,6 +35,11 @@ export abstract class Glyph { row: number, ): AnnotationFeatureI | undefined + abstract continueDrag( + display: LinearApolloDisplayRendering, + currentMousePosition: MousePosition, + ): void + drawHover( _display: LinearApolloDisplayMouseEvents, _overlayCtx: CanvasRenderingContext2D, @@ -99,13 +107,44 @@ export abstract class Glyph { return } + getAdjacentFeatures( + feature?: AnnotationFeatureI, + parentFeature?: AnnotationFeatureI, + ): { + prevFeature?: AnnotationFeatureI + nextFeature?: AnnotationFeatureI + } { + let prevFeature: AnnotationFeatureI | undefined + let nextFeature: AnnotationFeatureI | undefined + let i = 0 + if (!feature || !(parentFeature && parentFeature.children)) { + return { prevFeature, nextFeature } + } + for (const [, f] of parentFeature.children) { + if (f._id === feature._id) { + break + } + i++ + } + const keys = [...parentFeature.children.keys()] + if (i > 0) { + const key = keys[i - 1] + prevFeature = parentFeature.children.get(key) + } + if (i < keys.length - 1) { + const key = keys[i + 1] + nextFeature = parentFeature.children.get(key) + } + return { prevFeature, nextFeature } + } + getParentFeature( - feature: AnnotationFeatureI, + feature?: AnnotationFeatureI, topLevelFeature?: AnnotationFeatureI, ) { let parentFeature - if (!(topLevelFeature && topLevelFeature.children)) { + if (!feature || !(topLevelFeature && topLevelFeature.children)) { return parentFeature } diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/ImplicitExonGeneGlyph.ts b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/ImplicitExonGeneGlyph.ts index 3e1f93f17..054220192 100644 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/ImplicitExonGeneGlyph.ts +++ b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/ImplicitExonGeneGlyph.ts @@ -1,5 +1,6 @@ import { alpha } from '@mui/material' import { AnnotationFeatureI } from 'apollo-mst' +import { LocationEndChange, LocationStartChange } from 'apollo-shared' import { LinearApolloDisplay } from '../stateModel' import { MousePosition } from '../stateModel/mouseEvents' @@ -386,9 +387,17 @@ export class ImplicitExonGeneGlyph extends Glyph { startDrag(stateModel: LinearApolloDisplay): boolean { // only accept the drag if we are on the edge of the feature - const { feature, mousePosition } = stateModel.apolloDragging?.start ?? {} - if (feature && mousePosition) { - const edge = this.isMouseOnFeatureEdge(mousePosition, feature, stateModel) + const { feature, mousePosition, topLevelFeature } = + stateModel.apolloDragging?.start ?? {} + const { mousePosition: currentMousePosition } = + stateModel.apolloDragging?.current ?? {} + if (feature && mousePosition && currentMousePosition) { + const edge = this.isMouseOnFeatureEdge( + mousePosition, + feature, + stateModel, + topLevelFeature, + ) if (edge) { return true } @@ -396,6 +405,147 @@ export class ImplicitExonGeneGlyph extends Glyph { return false } + continueDrag( + stateModel: LinearApolloDisplay, + currentMousePosition: MousePosition, + ): void { + const { feature, glyph, mousePosition, topLevelFeature } = + stateModel.apolloDragging?.start ?? {} + if (!(currentMousePosition && mousePosition)) { + return + } + const parentFeature = this.getParentFeature(feature, topLevelFeature) + const adjacentFeatures: { + prevFeature?: AnnotationFeatureI + nextFeature?: AnnotationFeatureI + } = this.getAdjacentFeatures(feature, parentFeature) + if (!feature) { + return + } + if ( + feature.type !== 'CDS' && + adjacentFeatures.prevFeature && + !adjacentFeatures.nextFeature + ) { + if ( + adjacentFeatures.prevFeature.type === 'CDS' && + currentMousePosition && + currentMousePosition.bp <= adjacentFeatures.prevFeature.start + 1 + ) { + return + } + if ( + adjacentFeatures.prevFeature.type !== 'CDS' && + currentMousePosition && + currentMousePosition.bp <= adjacentFeatures.prevFeature.end + 1 + ) { + return + } + } + + if ( + feature.type !== 'CDS' && + !adjacentFeatures.prevFeature && + adjacentFeatures.nextFeature + ) { + if ( + adjacentFeatures.nextFeature.type === 'CDS' && + currentMousePosition && + currentMousePosition.bp >= adjacentFeatures.nextFeature.end - 1 + ) { + return + } + if ( + adjacentFeatures.nextFeature.type !== 'CDS' && + currentMousePosition && + currentMousePosition.bp >= adjacentFeatures.nextFeature.start - 1 + ) { + return + } + } + + if (adjacentFeatures.prevFeature && adjacentFeatures.nextFeature) { + if ( + feature.type === 'CDS' && + adjacentFeatures.nextFeature.type !== 'CDS' && + currentMousePosition && + currentMousePosition.bp >= adjacentFeatures.nextFeature.end - 1 + ) { + return + } + if ( + feature.type === 'CDS' && + adjacentFeatures.nextFeature.type === 'CDS' && + currentMousePosition && + currentMousePosition.bp >= adjacentFeatures.nextFeature.start - 1 + ) { + return + } + if ( + feature.type === 'CDS' && + adjacentFeatures.prevFeature.type !== 'CDS' && + currentMousePosition && + currentMousePosition.bp <= adjacentFeatures.prevFeature.start + 1 + ) { + return + } + if ( + feature.type === 'CDS' && + adjacentFeatures.prevFeature.type === 'CDS' && + currentMousePosition && + currentMousePosition.bp <= adjacentFeatures.prevFeature.end + 1 + ) { + return + } + if ( + feature.type !== 'CDS' && + adjacentFeatures.prevFeature.type === 'CDS' && + currentMousePosition && + currentMousePosition.bp <= adjacentFeatures.prevFeature.start + 1 + ) { + return + } + if ( + feature.type !== 'CDS' && + adjacentFeatures.prevFeature.type !== 'CDS' && + currentMousePosition && + currentMousePosition.bp <= adjacentFeatures.prevFeature.end + 1 + ) { + return + } + if ( + feature.type !== 'CDS' && + adjacentFeatures.nextFeature.type !== 'CDS' && + currentMousePosition && + currentMousePosition.bp >= adjacentFeatures.nextFeature.start - 1 + ) { + return + } + if ( + feature.type !== 'CDS' && + adjacentFeatures.nextFeature.type === 'CDS' && + currentMousePosition && + currentMousePosition.bp >= adjacentFeatures.nextFeature.end - 1 + ) { + return + } + } + stateModel.setDragging({ + start: { + feature, + topLevelFeature, + glyph, + mousePosition, + }, + current: { + feature, + topLevelFeature, + glyph, + mousePosition: currentMousePosition, + }, + }) + } + getFeatureFromLayout( feature: AnnotationFeatureI, bp: number, @@ -404,4 +554,124 @@ export class ImplicitExonGeneGlyph extends Glyph { const layoutRow = this.featuresForRow(feature)[row] return layoutRow?.find((f) => bp >= f.start && bp <= f.end) } + + async executeDrag(stateModel: LinearApolloDisplay) { + const { + apolloDragging, + changeManager, + displayedRegions, + getAssemblyId, + setCursor, + } = stateModel + if (!apolloDragging) { + return + } + const { + feature, + glyph, + mousePosition: startingMousePosition, + topLevelFeature, + } = apolloDragging.start + if (!feature) { + throw new Error('no feature for drag preview??') + } + if (glyph !== this) { + throw new Error('drawDragPreview() called on wrong glyph?') + } + const edge = this.isMouseOnFeatureEdge( + startingMousePosition, + feature, + stateModel, + ) + if (!edge) { + return + } + + const { mousePosition: currentMousePosition } = apolloDragging.current + const region = displayedRegions[startingMousePosition.regionNumber] + const newBp = currentMousePosition.bp + const assembly = getAssemblyId(region.assemblyName) + + const parentFeature = this.getParentFeature(feature, topLevelFeature) + const adjacentFeatures: { + prevFeature?: AnnotationFeatureI + nextFeature?: AnnotationFeatureI + } = this.getAdjacentFeatures(feature, parentFeature) + const changes: (LocationStartChange | LocationEndChange)[] = [] + + if (edge === 'end') { + this.addEndLocation(changes, feature, newBp, assembly) + const { nextFeature } = adjacentFeatures + if (!nextFeature) { + return + } + if ( + (feature.type !== 'CDS' && nextFeature.type === 'CDS') || + (feature.type === 'CDS' && nextFeature.type !== 'CDS') + ) { + this.addStartLocation(changes, nextFeature, newBp + 1, assembly) + } + } else { + this.addStartLocation(changes, feature, newBp, assembly) + const { prevFeature } = adjacentFeatures + if (!prevFeature) { + return + } + if ( + (feature.type !== 'CDS' && prevFeature.type === 'CDS') || + (feature.type === 'CDS' && prevFeature.type !== 'CDS') + ) { + this.addEndLocation(changes, prevFeature, newBp - 1, assembly) + } + } + if (!changeManager) { + throw new Error('no change manager') + } + for (const change of changes) { + await changeManager.submit(change) + } + setCursor() + } + + addEndLocation( + changes: (LocationStartChange | LocationEndChange)[] = [], + feature: AnnotationFeatureI, + newBp: number, + assembly: string, + ) { + const featureId = feature._id + const oldEnd = feature.end + const newEnd = newBp + changes.push( + new LocationEndChange({ + typeName: 'LocationEndChange', + changedIds: [featureId], + featureId, + oldEnd, + newEnd, + assembly, + }), + ) + } + + addStartLocation( + changes: (LocationStartChange | LocationEndChange)[] = [], + feature: AnnotationFeatureI, + newBp: number, + assembly: string, + ) { + const featureId = feature._id + const oldStart = feature.start + const newStart = newBp + changes.push( + new LocationStartChange({ + typeName: 'LocationStartChange', + changedIds: [featureId], + featureId, + oldStart, + newStart, + assembly, + }), + ) + } } diff --git a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/mouseEvents.ts b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/mouseEvents.ts index 39daeb159..9039df223 100644 --- a/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/mouseEvents.ts +++ b/packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/mouseEvents.ts @@ -106,15 +106,12 @@ export function mouseEventsModelIntermediateFactory( ) } event.stopPropagation() - const { feature, glyph, mousePosition, topLevelFeature } = - self.getFeatureAndGlyphUnderMouse(event) - if (!mousePosition) { + const { glyph } = self.apolloDragging.start + const { mousePosition } = self.getFeatureAndGlyphUnderMouse(event) + if (!(mousePosition && glyph)) { return } - self.apolloDragging = { - ...self.apolloDragging, - current: { feature, topLevelFeature, glyph, mousePosition }, - } + glyph.continueDrag(self, mousePosition) }, setDragging(dragInfo?: typeof self.apolloDragging) { self.apolloDragging = dragInfo ?? null