UNPKG

@jbrowse/plugin-linear-genome-view

Version:

JBrowse 2 linear genome view

1,065 lines (1,064 loc) 43.9 kB
import { lazy } from 'react'; import { getConf } from '@jbrowse/core/configuration'; import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models'; import { VIEW_HEADER_HEIGHT } from '@jbrowse/core/ui'; import { TrackSelector as TrackSelectorIcon } from '@jbrowse/core/ui/Icons'; import { assembleLocString, clamp, findLast, getSession, isSessionModelWithWidgets, isSessionWithAddTracks, localStorageGetBoolean, localStorageGetItem, localStorageSetItem, measureText, springAnimate, sum, } from '@jbrowse/core/util'; import { bpToPx, moveTo, pxToBp } from '@jbrowse/core/util/Base1DUtils'; import Base1DView from '@jbrowse/core/util/Base1DViewModel'; import calculateDynamicBlocks from '@jbrowse/core/util/calculateDynamicBlocks'; import calculateStaticBlocks from '@jbrowse/core/util/calculateStaticBlocks'; import { getParentRenderProps } from '@jbrowse/core/util/tracks'; import { ElementId } from '@jbrowse/core/util/types/mst'; import { isSessionWithMultipleViews } from '@jbrowse/product-core'; import FolderOpenIcon from '@mui/icons-material/FolderOpen'; import LabelIcon from '@mui/icons-material/Label'; import MenuOpenIcon from '@mui/icons-material/MenuOpen'; import PaletteIcon from '@mui/icons-material/Palette'; import PhotoCameraIcon from '@mui/icons-material/PhotoCamera'; import SearchIcon from '@mui/icons-material/Search'; import SyncAltIcon from '@mui/icons-material/SyncAlt'; import VisibilityIcon from '@mui/icons-material/Visibility'; import ZoomInIcon from '@mui/icons-material/ZoomIn'; import { saveAs } from 'file-saver'; import { autorun, transaction, when } from 'mobx'; import { addDisposer, cast, getParent, getRoot, getSnapshot, resolveIdentifier, types, } from 'mobx-state-tree'; import Header from './components/Header'; import { calculateVisibleLocStrings, generateLocations, parseLocStrings, } from './util'; import { handleSelectedRegion } from '../searchUtils'; import MiniControls from './components/MiniControls'; import { HEADER_BAR_HEIGHT, HEADER_OVERVIEW_HEIGHT, INTER_REGION_PADDING_WIDTH, RESIZE_HANDLE_HEIGHT, SCALE_BAR_HEIGHT, } from './consts'; const ReturnToImportFormDialog = lazy(() => import('@jbrowse/core/ui/ReturnToImportFormDialog')); const SequenceSearchDialog = lazy(() => import('./components/SequenceSearchDialog')); const ExportSvgDialog = lazy(() => import('./components/ExportSvgDialog')); const GetSequenceDialog = lazy(() => import('./components/GetSequenceDialog')); const SearchResultsDialog = lazy(() => import('./components/SearchResultsDialog')); export function stateModelFactory(pluginManager) { return types .compose('LinearGenomeView', BaseViewModel, types.model({ id: ElementId, type: types.literal('LinearGenomeView'), offsetPx: 0, bpPerPx: 1, displayedRegions: types.optional(types.frozen(), []), tracks: types.array(pluginManager.pluggableMstType('track', 'stateModel')), hideHeader: false, hideHeaderOverview: false, hideNoTracksActive: false, trackSelectorType: types.optional(types.enumeration(['hierarchical']), 'hierarchical'), showCenterLine: types.optional(types.boolean, () => localStorageGetBoolean('lgv-showCenterLine', false)), showCytobandsSetting: types.optional(types.boolean, () => localStorageGetBoolean('lgv-showCytobands', true)), trackLabels: types.optional(types.string, () => localStorageGetItem('lgv-trackLabels') || ''), showGridlines: true, highlight: types.optional(types.array(types.frozen()), []), colorByCDS: types.optional(types.boolean, () => localStorageGetBoolean('lgv-colorByCDS', false)), showTrackOutlines: types.optional(types.boolean, () => localStorageGetBoolean('lgv-showTrackOutlines', true)), init: types.frozen(), })) .volatile(() => ({ volatileWidth: undefined, minimumBlockWidth: 3, draggingTrackId: undefined, volatileError: undefined, afterDisplayedRegionsSetCallbacks: [], scaleFactor: 1, trackRefs: {}, coarseDynamicBlocks: [], coarseTotalBp: 0, leftOffset: undefined, rightOffset: undefined, })) .views(self => ({ get pinnedTracks() { return self.tracks.filter(t => t.pinned); }, get unpinnedTracks() { return self.tracks.filter(t => !t.pinned); }, get trackLabelsSetting() { const sessionSetting = getConf(getSession(self), [ 'LinearGenomeViewPlugin', 'trackLabels', ]); return self.trackLabels || sessionSetting; }, get width() { if (self.volatileWidth === undefined) { throw new Error('width undefined, make sure to check for model.initialized'); } return self.volatileWidth; }, get interRegionPaddingWidth() { return INTER_REGION_PADDING_WIDTH; }, get assemblyNames() { return [ ...new Set(self.displayedRegions.map(region => region.assemblyName)), ]; }, get assemblyDisplayNames() { const { assemblyManager } = getSession(self); return this.assemblyNames.map(assemblyName => { var _a; const assembly = assemblyManager.get(assemblyName); return (_a = assembly === null || assembly === void 0 ? void 0 : assembly.displayName) !== null && _a !== void 0 ? _a : assemblyName; }); }, get isTopLevelView() { const session = getSession(self); return session.views.some(r => r.id === self.id); }, get stickyViewHeaders() { const session = getSession(self); return isSessionWithMultipleViews(session) ? this.isTopLevelView && session.stickyViewHeaders : false; }, get rubberbandTop() { let pinnedTracksTop = 0; if (this.stickyViewHeaders) { pinnedTracksTop = VIEW_HEADER_HEIGHT; if (!self.hideHeader) { pinnedTracksTop += HEADER_BAR_HEIGHT; if (!self.hideHeaderOverview) { pinnedTracksTop += HEADER_OVERVIEW_HEIGHT; } } } return pinnedTracksTop; }, get pinnedTracksTop() { return this.rubberbandTop + SCALE_BAR_HEIGHT; }, })) .views(self => ({ scaleBarDisplayPrefix() { return getParent(self, 2).type === 'LinearSyntenyView' ? self.assemblyDisplayNames[0] : ''; }, MiniControlsComponent() { return MiniControls; }, HeaderComponent() { return Header; }, get assembliesNotFound() { const { assemblyManager } = getSession(self); const r0 = self.assemblyNames .map(a => (!assemblyManager.get(a) ? a : undefined)) .filter(f => !!f) .join(','); return r0 ? `Assemblies ${r0} not found` : undefined; }, get assemblyErrors() { const { assemblyManager } = getSession(self); return self.assemblyNames .map(a => { var _a; return (_a = assemblyManager.get(a)) === null || _a === void 0 ? void 0 : _a.error; }) .filter(f => !!f) .join(', '); }, get assembliesInitialized() { const { assemblyManager } = getSession(self); return self.assemblyNames.every(a => { var _a; return (_a = assemblyManager.get(a)) === null || _a === void 0 ? void 0 : _a.initialized; }); }, get initialized() { return self.volatileWidth !== undefined && this.assembliesInitialized; }, get hasDisplayedRegions() { return self.displayedRegions.length > 0; }, get scaleBarHeight() { return SCALE_BAR_HEIGHT + RESIZE_HANDLE_HEIGHT; }, get headerHeight() { if (self.hideHeader) { return 0; } else if (self.hideHeaderOverview) { return HEADER_BAR_HEIGHT; } else { return HEADER_BAR_HEIGHT + HEADER_OVERVIEW_HEIGHT; } }, get trackHeights() { return sum(self.tracks.map(t => t.displays[0].height)); }, get trackHeightsWithResizeHandles() { return this.trackHeights + self.tracks.length * RESIZE_HANDLE_HEIGHT; }, get height() { return (this.trackHeightsWithResizeHandles + this.headerHeight + this.scaleBarHeight); }, get totalBp() { return sum(self.displayedRegions.map(r => r.end - r.start)); }, get maxBpPerPx() { return this.totalBp / (self.width * 0.9); }, get minBpPerPx() { return 1 / 50; }, get error() { return (self.volatileError || this.assemblyErrors || this.assembliesNotFound); }, get maxOffset() { const leftPadding = 10; return this.displayedRegionsTotalPx - leftPadding; }, get minOffset() { const rightPadding = 30; return -self.width + rightPadding; }, get displayedRegionsTotalPx() { return this.totalBp / self.bpPerPx; }, renderProps() { return { ...getParentRenderProps(self), bpPerPx: self.bpPerPx, colorByCDS: self.colorByCDS, }; }, searchScope(assemblyName) { return { assemblyName, includeAggregateIndexes: true, tracks: self.tracks, }; }, getTrack(id) { return self.tracks.find(t => t.configuration.trackId === id); }, rankSearchResults(results) { return results; }, rewriteOnClicks(trackType, viewMenuActions) { for (const action of viewMenuActions) { if ('subMenu' in action) { this.rewriteOnClicks(trackType, action.subMenu); } if ('onClick' in action) { const holdOnClick = action.onClick; action.onClick = (...args) => { for (const track of self.tracks) { if (track.type === trackType) { holdOnClick.apply(track, [track, ...args]); } } }; } } }, get trackTypeActions() { const allActions = new Map(); for (const track of self.tracks) { const trackInMap = allActions.get(track.type); if (!trackInMap) { const viewMenuActions = structuredClone(track.viewMenuActions); this.rewriteOnClicks(track.type, viewMenuActions); allActions.set(track.type, viewMenuActions); } } return allActions; }, })) .actions(self => ({ setShowTrackOutlines(arg) { self.showTrackOutlines = arg; }, setColorByCDS(flag) { self.colorByCDS = flag; }, setShowCytobands(flag) { self.showCytobandsSetting = flag; }, setWidth(newWidth) { self.volatileWidth = newWidth; }, setError(error) { self.volatileError = error; }, setHideHeader(b) { self.hideHeader = b; }, setHideHeaderOverview(b) { self.hideHeaderOverview = b; }, setHideNoTracksActive(b) { self.hideNoTracksActive = b; }, setShowGridlines(b) { self.showGridlines = b; }, addToHighlights(highlight) { self.highlight.push(highlight); }, setHighlight(highlight) { self.highlight = cast(highlight); }, removeHighlight(highlight) { self.highlight.remove(highlight); }, scrollTo(offsetPx) { const newOffsetPx = clamp(offsetPx, self.minOffset, self.maxOffset); self.offsetPx = newOffsetPx; return newOffsetPx; }, zoomTo(bpPerPx, offset = self.width / 2, centerAtOffset = false) { const newBpPerPx = clamp(bpPerPx, self.minBpPerPx, self.maxBpPerPx); if (newBpPerPx === self.bpPerPx) { return newBpPerPx; } const oldBpPerPx = self.bpPerPx; if (Math.abs(oldBpPerPx - newBpPerPx) < 0.000001) { console.warn('zoomTo bpPerPx rounding error'); return oldBpPerPx; } self.bpPerPx = newBpPerPx; this.scrollTo(Math.round(((self.offsetPx + offset) * oldBpPerPx) / newBpPerPx - (centerAtOffset ? self.width / 2 : offset))); return newBpPerPx; }, setOffsets(left, right) { self.leftOffset = left; self.rightOffset = right; }, setSearchResults(searchResults, searchQuery, assemblyName) { getSession(self).queueDialog(handleClose => [ SearchResultsDialog, { model: self, searchResults, searchQuery, handleClose, assemblyName, }, ]); }, setNewView(bpPerPx, offsetPx) { this.zoomTo(bpPerPx); this.scrollTo(offsetPx); }, horizontallyFlip() { self.displayedRegions = cast([...self.displayedRegions] .reverse() .map(region => ({ ...region, reversed: !region.reversed }))); this.scrollTo(self.totalBp / self.bpPerPx - self.offsetPx - self.width); }, showTrack(trackId, initialSnapshot = {}, displayInitialSnapshot = {}) { const schema = pluginManager.pluggableConfigSchemaType('track'); const conf = resolveIdentifier(schema, getRoot(self), trackId); if (!conf) { throw new Error(`Could not resolve identifier "${trackId}"`); } const trackType = pluginManager.getTrackType(conf === null || conf === void 0 ? void 0 : conf.type); if (!trackType) { throw new Error(`Unknown track type ${conf.type}`); } const viewType = pluginManager.getViewType(self.type); const supportedDisplays = new Set(viewType.displayTypes.map(d => d.name)); const displayConf = conf.displays.find((d) => supportedDisplays.has(d.type)); if (!displayConf) { throw new Error(`Could not find a compatible display for view type ${self.type}`); } const t = self.tracks.filter(t => t.configuration === conf); if (t.length === 0) { const track = trackType.stateModel.create({ ...initialSnapshot, type: conf.type, configuration: conf, displays: [ { type: displayConf.type, configuration: displayConf, ...displayInitialSnapshot, }, ], }); self.tracks.push(track); return track; } return t[0]; }, hideTrack(trackId) { const schema = pluginManager.pluggableConfigSchemaType('track'); const conf = resolveIdentifier(schema, getRoot(self), trackId); const tracks = self.tracks.filter(t => t.configuration === conf); transaction(() => { for (const track of tracks) { self.tracks.remove(track); } }); return tracks.length; }, })) .actions(self => ({ moveTrackDown(id) { const idx = self.tracks.findIndex(v => v.id === id); if (idx === -1) { return; } if (idx !== -1 && idx < self.tracks.length - 1) { self.tracks.splice(idx, 2, self.tracks[idx + 1], self.tracks[idx]); } }, moveTrackUp(id) { const idx = self.tracks.findIndex(track => track.id === id); if (idx > 0) { self.tracks.splice(idx - 1, 2, self.tracks[idx], self.tracks[idx - 1]); } }, moveTrackToTop(id) { const idx = self.tracks.findIndex(track => track.id === id); self.tracks = cast([ self.tracks[idx], ...self.tracks.filter(track => track.id !== id), ]); }, moveTrackToBottom(id) { const idx = self.tracks.findIndex(track => track.id === id); self.tracks = cast([ ...self.tracks.filter(track => track.id !== id), self.tracks[idx], ]); }, moveTrack(movingId, targetId) { const oldIndex = self.tracks.findIndex(track => track.id === movingId); if (oldIndex === -1) { throw new Error(`Track ID ${movingId} not found`); } const newIndex = self.tracks.findIndex(track => track.id === targetId); if (newIndex === -1) { throw new Error(`Track ID ${targetId} not found`); } const tracks = self.tracks.filter((_, idx) => idx !== oldIndex); tracks.splice(newIndex, 0, self.tracks[oldIndex]); self.tracks = cast(tracks); }, toggleTrack(trackId) { const hiddenCount = self.hideTrack(trackId); if (!hiddenCount) { self.showTrack(trackId); return true; } return false; }, setTrackLabels(setting) { localStorage.setItem('lgv-trackLabels', setting); self.trackLabels = setting; }, setShowCenterLine(b) { self.showCenterLine = b; }, setDisplayedRegions(regions) { self.displayedRegions = cast(regions); self.zoomTo(self.bpPerPx); }, activateTrackSelector() { if (self.trackSelectorType === 'hierarchical') { const session = getSession(self); if (isSessionModelWithWidgets(session)) { const selector = session.addWidget('HierarchicalTrackSelectorWidget', 'hierarchicalTrackSelector', { view: self }); session.showWidget(selector); return selector; } } throw new Error(`invalid track selector type ${self.trackSelectorType}`); }, getSelectedRegions(leftOffset, rightOffset) { const snap = getSnapshot(self); const simView = Base1DView.create({ ...snap, interRegionPaddingWidth: self.interRegionPaddingWidth, }); simView.setVolatileWidth(self.width); simView.moveTo(leftOffset, rightOffset); return simView.dynamicBlocks.contentBlocks.map(region => ({ ...region, start: Math.floor(region.start), end: Math.ceil(region.end), })); }, afterDisplayedRegionsSet(cb) { self.afterDisplayedRegionsSetCallbacks.push(cb); }, horizontalScroll(distance) { const oldOffsetPx = self.offsetPx; const newOffsetPx = self.scrollTo(self.offsetPx + distance); return newOffsetPx - oldOffsetPx; }, center() { const centerBp = self.totalBp / 2; const centerPx = centerBp / self.bpPerPx; self.scrollTo(Math.round(centerPx - self.width / 2)); }, showAllRegions() { self.zoomTo(self.maxBpPerPx); this.center(); }, showAllRegionsInAssembly(assemblyName) { const session = getSession(self); const { assemblyManager } = session; if (!assemblyName) { const names = new Set(self.displayedRegions.map(r => r.assemblyName)); if (names.size > 1) { session.notify(`Can't perform operation with multiple assemblies currently`); return; } ; [assemblyName] = [...names]; } const assembly = assemblyManager.get(assemblyName); if (assembly) { const { regions } = assembly; if (regions) { this.setDisplayedRegions(regions); self.zoomTo(self.maxBpPerPx); this.center(); } } }, setDraggingTrackId(idx) { self.draggingTrackId = idx; }, setScaleFactor(factor) { self.scaleFactor = factor; }, clearView() { this.setDisplayedRegions([]); self.tracks.clear(); self.scrollTo(0); self.zoomTo(10); }, setInit(arg) { self.init = arg; }, async exportSvg(opts = {}) { const { renderToSvg } = await import('./svgcomponents/SVGLinearGenomeView'); const html = await renderToSvg(self, opts); const blob = new Blob([html], { type: 'image/svg+xml' }); saveAs(blob, opts.filename || 'image.svg'); }, })) .actions(self => { let cancelLastAnimation = () => { }; function slide(viewWidths) { const [animate, cancelAnimation] = springAnimate(self.offsetPx, self.offsetPx + self.width * viewWidths, self.scrollTo, undefined, undefined, 200); cancelLastAnimation(); cancelLastAnimation = cancelAnimation; animate(); } return { slide }; }) .actions(self => { let cancelLastAnimation = () => { }; function zoom(targetBpPerPx) { self.zoomTo(self.bpPerPx); if ((targetBpPerPx < self.bpPerPx && self.bpPerPx === self.minBpPerPx) || (targetBpPerPx > self.bpPerPx && self.bpPerPx === self.maxBpPerPx)) { return; } const factor = self.bpPerPx / targetBpPerPx; const [animate, cancelAnimation] = springAnimate(1, factor, self.setScaleFactor, () => { self.zoomTo(targetBpPerPx); self.setScaleFactor(1); }); cancelLastAnimation(); cancelLastAnimation = cancelAnimation; animate(); } return { zoom }; }) .views(self => ({ get canShowCytobands() { return self.displayedRegions.length === 1 && this.anyCytobandsExist; }, get showCytobands() { return this.canShowCytobands && self.showCytobandsSetting; }, get anyCytobandsExist() { const { assemblyManager } = getSession(self); return self.assemblyNames.some(a => { var _a, _b; return (_b = (_a = assemblyManager.get(a)) === null || _a === void 0 ? void 0 : _a.cytobands) === null || _b === void 0 ? void 0 : _b.length; }); }, get cytobandOffset() { var _a; return this.showCytobands ? measureText(((_a = self.displayedRegions[0]) === null || _a === void 0 ? void 0 : _a.refName) || '', 12) + 15 : 0; }, })) .views(self => ({ menuItems() { const { canShowCytobands, showCytobands } = self; const session = getSession(self); const menuItems = [ { label: 'Return to import form', onClick: () => { getSession(self).queueDialog(handleClose => [ ReturnToImportFormDialog, { model: self, handleClose }, ]); }, icon: FolderOpenIcon, }, ...(isSessionWithAddTracks(session) ? [ { label: 'Sequence search', icon: SearchIcon, onClick: () => { getSession(self).queueDialog(handleClose => [ SequenceSearchDialog, { model: self, handleClose, }, ]); }, }, ] : []), { label: 'Export SVG', icon: PhotoCameraIcon, onClick: () => { getSession(self).queueDialog(handleClose => [ ExportSvgDialog, { model: self, handleClose, }, ]); }, }, { label: 'Open track selector', onClick: self.activateTrackSelector, icon: TrackSelectorIcon, }, { label: 'Horizontally flip', icon: SyncAltIcon, onClick: self.horizontallyFlip, }, { label: 'Color by CDS', type: 'checkbox', checked: self.colorByCDS, icon: PaletteIcon, onClick: () => { self.setColorByCDS(!self.colorByCDS); }, }, { label: 'Show...', icon: VisibilityIcon, subMenu: [ { label: 'Show all regions in assembly', onClick: self.showAllRegionsInAssembly, }, { label: 'Show center line', type: 'checkbox', checked: self.showCenterLine, onClick: () => { self.setShowCenterLine(!self.showCenterLine); }, }, { label: 'Show header', type: 'checkbox', checked: !self.hideHeader, onClick: () => { self.setHideHeader(!self.hideHeader); }, }, { label: 'Show track outlines', type: 'checkbox', checked: self.showTrackOutlines, onClick: () => { self.setShowTrackOutlines(!self.showTrackOutlines); }, }, { label: 'Show header overview', type: 'checkbox', checked: !self.hideHeaderOverview, onClick: () => { self.setHideHeaderOverview(!self.hideHeaderOverview); }, disabled: self.hideHeader, }, { label: 'Show no tracks active button', type: 'checkbox', checked: !self.hideNoTracksActive, onClick: () => { self.setHideNoTracksActive(!self.hideNoTracksActive); }, }, { label: 'Show guidelines', type: 'checkbox', checked: self.showGridlines, onClick: () => { self.setShowGridlines(!self.showGridlines); }, }, ...(canShowCytobands ? [ { label: 'Show ideogram', type: 'checkbox', checked: self.showCytobands, onClick: () => { self.setShowCytobands(!showCytobands); }, }, ] : []), ], }, { label: 'Track labels', icon: LabelIcon, subMenu: [ { label: 'Overlapping', icon: VisibilityIcon, type: 'radio', checked: self.trackLabelsSetting === 'overlapping', onClick: () => { self.setTrackLabels('overlapping'); }, }, { label: 'Offset', icon: VisibilityIcon, type: 'radio', checked: self.trackLabelsSetting === 'offset', onClick: () => { self.setTrackLabels('offset'); }, }, { label: 'Hidden', icon: VisibilityIcon, type: 'radio', checked: self.trackLabelsSetting === 'hidden', onClick: () => { self.setTrackLabels('hidden'); }, }, ], }, ]; for (const [key, value] of self.trackTypeActions.entries()) { if (value.length) { menuItems.push({ type: 'divider' }, { type: 'subHeader', label: key }); for (const action of value) { menuItems.push(action); } } } return menuItems; }, })) .views(self => { let currentlyCalculatedStaticBlocks; let stringifiedCurrentlyCalculatedStaticBlocks = ''; return { get staticBlocks() { const ret = calculateStaticBlocks(self); const sret = JSON.stringify(ret); if (stringifiedCurrentlyCalculatedStaticBlocks !== sret) { currentlyCalculatedStaticBlocks = ret; stringifiedCurrentlyCalculatedStaticBlocks = sret; } return currentlyCalculatedStaticBlocks; }, get dynamicBlocks() { return calculateDynamicBlocks(self); }, get roundedDynamicBlocks() { return this.dynamicBlocks.contentBlocks.map(block => ({ ...block, start: Math.floor(block.start), end: Math.ceil(block.end), })); }, get visibleLocStrings() { return calculateVisibleLocStrings(this.dynamicBlocks.contentBlocks); }, get coarseVisibleLocStrings() { return calculateVisibleLocStrings(self.coarseDynamicBlocks); }, }; }) .actions(self => ({ setCoarseDynamicBlocks(blocks) { self.coarseDynamicBlocks = blocks.contentBlocks; self.coarseTotalBp = blocks.totalBp; }, })) .actions(self => ({ moveTo(start, end) { moveTo(self, start, end); }, async navToLocString(input, optAssemblyName, grow) { const { assemblyNames } = self; const { assemblyManager } = getSession(self); const assemblyName = optAssemblyName || assemblyNames[0]; if (assemblyName) { await assemblyManager.waitForAssembly(assemblyName); } return this.navToLocations(parseLocStrings(input, assemblyName, (ref, asm) => assemblyManager.isValidRefName(ref, asm)), assemblyName, grow); }, async navToSearchString({ input, assembly, }) { await handleSelectedRegion({ input, assembly, model: self, }); }, async navToLocation(parsedLocString, assemblyName, grow) { return this.navToLocations([parsedLocString], assemblyName, grow); }, async navToLocations(regions, assemblyName, grow) { const { assemblyManager } = getSession(self); await when(() => self.volatileWidth !== undefined); const locations = await generateLocations({ regions, assemblyManager, assemblyName, grow, }); if (locations.length === 1) { const location = locations[0]; const { reversed, parentRegion, start, end } = location; self.setDisplayedRegions([ { reversed, ...parentRegion, }, ]); this.navTo({ ...location, start: clamp(start !== null && start !== void 0 ? start : 0, 0, parentRegion.end), end: clamp(end !== null && end !== void 0 ? end : parentRegion.end, 0, parentRegion.end), }); } else { self.setDisplayedRegions(locations.map(location => { const { start, end } = location; return start === undefined || end === undefined ? location.parentRegion : { ...location, start, end, }; })); self.showAllRegions(); } }, navTo(query) { this.navToMultiple([query]); }, navToMultiple(locations) { if (locations.some(l => l.start !== undefined && l.end !== undefined && l.start > l.end)) { throw new Error('found start greater than end'); } const firstLocation = locations.at(0); const lastLocation = locations.at(-1); if (!firstLocation || !lastLocation) { return; } const defaultAssemblyName = self.assemblyNames[0]; const { assemblyManager } = getSession(self); const firstAssembly = assemblyManager.get(firstLocation.assemblyName || defaultAssemblyName); const firstRefName = (firstAssembly === null || firstAssembly === void 0 ? void 0 : firstAssembly.getCanonicalRefName(firstLocation.refName)) || firstLocation.refName; const firstRegion = self.displayedRegions.find(r => r.refName === firstRefName); const lastAssembly = assemblyManager.get(lastLocation.assemblyName || defaultAssemblyName); const lastRefName = (lastAssembly === null || lastAssembly === void 0 ? void 0 : lastAssembly.getCanonicalRefName(lastLocation.refName)) || lastLocation.refName; const lastRegion = findLast(self.displayedRegions, r => r.refName === lastRefName); if (!firstRegion) { throw new Error(`could not find a region with refName "${firstRefName}"`); } if (!lastRegion) { throw new Error(`could not find a region with refName "${lastRefName}"`); } const firstStart = firstLocation.start === undefined ? firstRegion.start : firstLocation.start; const firstEnd = firstLocation.end === undefined ? firstRegion.end : firstLocation.end; const lastStart = lastLocation.start === undefined ? lastRegion.start : lastLocation.start; const lastEnd = lastLocation.end === undefined ? lastRegion.end : lastLocation.end; const firstIndex = self.displayedRegions.findIndex(r => firstRefName === r.refName && firstStart >= r.start && firstStart <= r.end && firstEnd <= r.end && firstEnd >= r.start); const lastIndex = self.displayedRegions.findIndex(r => lastRefName === r.refName && lastStart >= r.start && lastStart <= r.end && lastEnd <= r.end && lastEnd >= r.start); if (firstIndex === -1 || lastIndex === -1) { throw new Error(`could not find a region that contained "${locations.map(l => assembleLocString(l))}"`); } const startDisplayedRegion = self.displayedRegions[firstIndex]; const endDisplayedRegion = self.displayedRegions[lastIndex]; const startOffset = startDisplayedRegion.reversed ? startDisplayedRegion.end - firstEnd : firstStart - startDisplayedRegion.start; const endOffset = endDisplayedRegion.reversed ? endDisplayedRegion.end - lastStart : lastEnd - endDisplayedRegion.start; this.moveTo({ index: firstIndex, offset: startOffset, }, { index: lastIndex, offset: endOffset, }); }, })) .views(self => ({ rubberBandMenuItems() { return [ { label: 'Zoom to region', icon: ZoomInIcon, onClick: () => { self.moveTo(self.leftOffset, self.rightOffset); }, }, { label: 'Get sequence', icon: MenuOpenIcon, onClick: () => { getSession(self).queueDialog(handleClose => [ GetSequenceDialog, { model: self, handleClose }, ]); }, }, ]; }, bpToPx({ refName, coord, regionNumber, }) { return bpToPx({ refName, coord, regionNumber, self }); }, centerAt(coord, refName, regionNumber) { const centerPx = this.bpToPx({ refName, coord, regionNumber, }); if (centerPx !== undefined) { self.scrollTo(Math.round(centerPx.offsetPx - self.width / 2)); } }, pxToBp(px) { return pxToBp(self, px); }, get centerLineInfo() { return self.displayedRegions.length > 0 ? this.pxToBp(self.width / 2) : undefined; }, })) .actions(self => ({ afterCreate() { function handler(e) { const session = getSession(self); if (session.focusedViewId === self.id && (e.ctrlKey || e.metaKey)) { if (e.code === 'ArrowLeft') { e.preventDefault(); self.slide(-0.9); } else if (e.code === 'ArrowRight') { e.preventDefault(); self.slide(0.9); } else if (e.code === 'ArrowUp' && self.scaleFactor === 1) { e.preventDefault(); self.zoom(self.bpPerPx / 2); } else if (e.code === 'ArrowDown' && self.scaleFactor === 1) { e.preventDefault(); self.zoom(self.bpPerPx * 2); } } } document.addEventListener('keydown', handler); addDisposer(self, () => { document.removeEventListener('keydown', handler); }); }, afterAttach() { addDisposer(self, autorun(() => { var _a; const { init } = self; if (init) { self .navToLocString(init.loc, init.assembly) .catch((e) => { console.error(init, e); getSession(self).notifyError(`${e}`, e); }); (_a = init.tracks) === null || _a === void 0 ? void 0 : _a.map(t => self.showTrack(t)); self.setInit(undefined); } })); addDisposer(self, autorun(() => { if (self.initialized) { self.setCoarseDynamicBlocks(self.dynamicBlocks); } }, { delay: 150 })); addDisposer(self, autorun(() => { const s = (s) => JSON.stringify(s); const { showCytobandsSetting, showCenterLine, colorByCDS } = self; localStorageSetItem('lgv-showCytobands', s(showCytobandsSetting)); localStorageSetItem('lgv-showCenterLine', s(showCenterLine)); localStorageSetItem('lgv-colorByCDS', s(colorByCDS)); })); }, })) .preProcessSnapshot(snap => { if (!snap) { return snap; } const { highlight, ...rest } = snap; return { highlight: Array.isArray(highlight) || highlight === undefined ? highlight : [highlight], ...rest, }; }) .postProcessSnapshot(snap => { if (!snap) { return snap; } else { const { init, ...rest } = snap; return rest; } }); } export { default as LinearGenomeView, default as ReactComponent, } from './components/LinearGenomeView'; export { default as RefNameAutocomplete } from './components/RefNameAutocomplete'; export { default as SearchBox } from './components/SearchBox'; export { renderToSvg } from './svgcomponents/SVGLinearGenomeView';