UNPKG

@jbrowse/plugin-linear-genome-view

Version:

JBrowse 2 linear genome view

935 lines (934 loc) 38.2 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 { assembleLocString, clamp, findLast, getBpDisplayStr, getSession, isSessionModelWithWidgets, localStorageGetBoolean, localStorageGetItem, 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, hideTrackGeneric, showTrackGeneric, toggleTrackGeneric, } from '@jbrowse/core/util/tracks'; import { ElementId } from '@jbrowse/core/util/types/mst'; import { cast, getParent, getSnapshot, types } from '@jbrowse/mobx-state-tree'; import { isSessionWithMultipleViews } from '@jbrowse/product-core'; import { when } from 'mobx'; import { handleSelectedRegion } from "../searchUtils.js"; import { doAfterAttach } from "./afterAttach.js"; import Header from "./components/Header.js"; import MiniControls from "./components/MiniControls.js"; import { HEADER_BAR_HEIGHT, HEADER_OVERVIEW_HEIGHT, INTER_REGION_PADDING_WIDTH, RESIZE_HANDLE_HEIGHT, SCALE_BAR_HEIGHT, } from "./consts.js"; import { setupKeyboardHandler } from "./keyboardHandler.js"; import { buildMenuItems, buildRubberBandMenuItems, buildRubberbandClickMenuItems, rewriteOnClicks, } from "./menuItems.js"; import { calculateVisibleLocStrings, expandRegion, generateLocations, parseLocStrings, } from "./util.js"; const SearchResultsDialog = lazy(() => import("./components/SearchResultsDialog.js")); function getCenteredOffsetPx(contentPx, viewportPx) { return Math.round(contentPx / 2 - viewportPx / 2); } 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()), []), highlightsVisible: types.optional(types.boolean, true), labelsVisible: types.optional(types.boolean, true), 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, lastTrackDragY: undefined, volatileError: undefined, scaleFactor: 1, targetBpPerPx: undefined, trackRefs: {}, coarseDynamicBlocks: [], coarseTotalBp: 0, leftOffset: undefined, rightOffset: undefined, isScalebarRefNameMenuOpen: false, scalebarRefNameClickPending: false, volatileGuides: [], })) .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(r => r.assemblyName))]; }, get assemblyDisplayNames() { const { assemblyManager } = getSession(self); return this.assemblyNames.map(a => assemblyManager.get(a)?.displayName ?? a); }, get isTopLevelView() { return getSession(self).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 => assemblyManager.get(a)?.error) .filter(f => !!f) .join(', '); }, get assembliesInitialized() { const { assemblyManager } = getSession(self); return self.assemblyNames.every(a => assemblyManager.get(a)?.initialized); }, get initialized() { if (self.volatileWidth === undefined) { return false; } if (self.init) { const { assemblyManager } = getSession(self); const asm = assemblyManager.get(self.init.assembly); return !!(asm?.initialized && asm.regions); } return this.assembliesInitialized; }, get hasDisplayedRegions() { return self.displayedRegions.length > 0; }, get loadingMessage() { return this.showLoading ? 'Loading' : undefined; }, get hasSomethingToShow() { return this.hasDisplayedRegions || !!self.init; }, get showLoading() { return !this.initialized && !this.error && this.hasSomethingToShow; }, get showImportForm() { return !this.hasSomethingToShow || !!this.error; }, 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)); }, getNonElidedRegionCount(bpPerPx) { if (bpPerPx <= 0) { return self.displayedRegions.length; } return self.displayedRegions.filter(r => (r.end - r.start) / bpPerPx >= self.minimumBlockWidth).length; }, getInterRegionPaddingPx(bpPerPx) { const nonElidedCount = this.getNonElidedRegionCount(bpPerPx); const numPaddings = Math.max(0, nonElidedCount - 1); return numPaddings * self.interRegionPaddingWidth; }, get maxBpPerPx() { if (this.totalBp === 0 || self.width === 0) { return 1; } const naiveBpPerPx = this.totalBp / (self.width * 0.9); const totalPaddingPx = this.getInterRegionPaddingPx(naiveBpPerPx); const targetWidth = self.width * 0.9; const availableForBp = targetWidth - totalPaddingPx; if (availableForBp <= 0) { return naiveBpPerPx; } return this.totalBp / availableForBp; }, get minBpPerPx() { return 1 / 50; }, get error() { if (self.volatileError) { return self.volatileError; } if (this.assemblyErrors) { return this.assemblyErrors; } if (this.assembliesNotFound) { return this.assembliesNotFound; } if (self.init) { const { assemblyManager } = getSession(self); const asm = assemblyManager.get(self.init.assembly); if (asm?.error) { return asm.error; } if (!asm) { return `Assembly ${self.init.assembly} not found`; } } return undefined; }, get maxOffset() { const leftPadding = 10; return this.displayedRegionsTotalPx - leftPadding; }, get minOffset() { const rightPadding = 30; return -self.width + rightPadding; }, get displayedRegionsTotalPx() { if (self.bpPerPx === 0) { return 0; } const totalPaddingPx = this.getInterRegionPaddingPx(self.bpPerPx); return this.totalBp / self.bpPerPx + totalPaddingPx; }, renderProps() { return { ...getParentRenderProps(self), bpPerPx: self.bpPerPx, colorByCDS: self.colorByCDS, }; }, searchScope(assemblyName) { return { assemblyName, includeAggregateIndexes: true, tracks: self.tracks, }; }, get trackMap() { const map = new Map(); for (const track of self.tracks) { map.set(track.configuration.trackId, track); } return map; }, getTrack(id) { return this.trackMap.get(id); }, rankSearchResults(results) { return results; }, 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); rewriteOnClicks(self, 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; }, setIsScalebarRefNameMenuOpen(isOpen) { self.isScalebarRefNameMenuOpen = isOpen; }, setScalebarRefNameClickPending(pending) { self.scalebarRefNameClickPending = pending; }, 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); }, updateHighlight(old, updates) { const idx = self.highlight.indexOf(old); if (idx !== -1) { self.highlight.splice(idx, 1, { ...old, ...updates }); } }, setHighlightsVisible(arg) { self.highlightsVisible = arg; }, setLabelsVisible(arg) { self.labelsVisible = arg; }, setVolatileGuides(guides) { self.volatileGuides = guides; }, 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; const newOffsetPx = Math.round(((self.offsetPx + offset) * oldBpPerPx) / newBpPerPx - (centerAtOffset ? self.width / 2 : offset)); this.scrollTo(newOffsetPx); 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 = {}) { return showTrackGeneric(self, trackId, initialSnapshot, displayInitialSnapshot); }, hideTrack(trackId) { return hideTrackGeneric(self, trackId); }, })) .actions(self => ({ moveTrackDown(id) { const idx = self.tracks.findIndex(v => v.id === id); 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 track = self.tracks.find(track => track.id === id); if (track) { self.tracks = cast([track, ...self.tracks.filter(t => t.id !== id)]); } }, moveTrackToBottom(id) { const track = self.tracks.find(track => track.id === id); if (track) { self.tracks = cast([...self.tracks.filter(t => t.id !== id), track]); } }, 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) { toggleTrackGeneric(self, trackId); }, setTrackLabels(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 => ({ assemblyName: region.assemblyName, refName: region.refName, start: Math.floor(region.start), end: Math.ceil(region.end), })); }, horizontalScroll(distance) { const oldOffsetPx = self.offsetPx; const newOffsetPx = self.scrollTo(self.offsetPx + distance); return newOffsetPx - oldOffsetPx; }, showAllRegions() { self.bpPerPx = clamp(self.maxBpPerPx, self.minBpPerPx, self.maxBpPerPx); self.scrollTo(getCenteredOffsetPx(self.displayedRegionsTotalPx, self.width)); }, 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 regions = assemblyManager.get(assemblyName)?.regions; if (!regions) { return; } this.setDisplayedRegions(regions); self.zoomTo(self.maxBpPerPx); self.scrollTo(getCenteredOffsetPx(self.displayedRegionsTotalPx, self.width)); }, setDraggingTrackId(idx) { self.draggingTrackId = idx; if (idx === undefined) { self.lastTrackDragY = undefined; } }, setLastTrackDragY(y) { self.lastTrackDragY = y; }, setScaleFactor(factor) { self.scaleFactor = factor; }, setTargetBpPerPx(target) { self.targetBpPerPx = target; }, 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.js"); const html = await renderToSvg(self, opts); const { saveAs } = await import('file-saver-es'); if (opts.format === 'png') { const img = new Image(); const svgBlob = new Blob([html], { type: 'image/svg+xml' }); const url = URL.createObjectURL(svgBlob); await new Promise((resolve, reject) => { img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); URL.revokeObjectURL(url); canvas.toBlob(blob => { if (blob) { saveAs(blob, opts.filename || 'image.png'); resolve(); } else { reject(new Error(`Failed to create PNG. The image may be too large (${img.width}x${img.height}). Try reducing the view size or use SVG format.`)); } }, 'image/png'); }; img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('Failed to load SVG for PNG conversion')); }; img.src = url; }); } else { saveAs(new Blob([html], { type: 'image/svg+xml' }), 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) { cancelLastAnimation(); const intendedFactor = targetBpPerPx / self.bpPerPx; const effectiveBase = self.targetBpPerPx ?? self.bpPerPx; let effectiveTarget = effectiveBase * intendedFactor; effectiveTarget = Math.max(self.minBpPerPx, Math.min(self.maxBpPerPx, effectiveTarget)); const currentTarget = self.targetBpPerPx ?? self.bpPerPx; if (effectiveTarget === currentTarget) { return; } self.setTargetBpPerPx(effectiveTarget); const targetScaleFactor = self.bpPerPx / effectiveTarget; const [animate, cancelAnimation] = springAnimate(self.scaleFactor, targetScaleFactor, self.setScaleFactor, () => { self.zoomTo(effectiveTarget); self.setScaleFactor(1); self.setTargetBpPerPx(undefined); }, 0, 1000, 50); 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 => assemblyManager.get(a)?.cytobands?.length); }, get cytobandOffset() { return this.showCytobands ? measureText(self.displayedRegions[0]?.refName || '', 12) + 15 : 0; }, })) .views(self => ({ menuItems() { return buildMenuItems(self); }, })) .views(self => { let currentlyCalculatedStaticBlocks; let currentBlockKeys; return { get staticBlocks() { const newBlocks = calculateStaticBlocks(self); const newKeys = newBlocks.blocks.map(b => b.key).join(','); if (currentlyCalculatedStaticBlocks === undefined || currentBlockKeys !== newKeys) { currentlyCalculatedStaticBlocks = newBlocks; currentBlockKeys = newKeys; return currentlyCalculatedStaticBlocks; } else { 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); }, get coarseTotalBpDisplayStr() { return getBpDisplayStr(self.coarseTotalBp); }, get effectiveBpPerPx() { return self.targetBpPerPx ?? self.bpPerPx; }, get effectiveTotalBp() { return this.effectiveBpPerPx * self.width; }, get effectiveTotalBpDisplayStr() { return getBpDisplayStr(this.effectiveTotalBp); }, }; }) .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 ?? 0, 0, parentRegion.end), end: clamp(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, grow) { this.navToMultiple([query], grow); }, navToMultiple(locations, grow) { 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?.getCanonicalRefName(firstLocation.refName) || firstLocation.refName; const firstRegion = self.displayedRegions.find(r => r.refName === firstRefName); const lastAssembly = assemblyManager.get(lastLocation.assemblyName || defaultAssemblyName); const lastRefName = 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}"`); } let firstStart = firstLocation.start === undefined ? firstRegion.start : firstLocation.start; let firstEnd = firstLocation.end === undefined ? firstRegion.end : firstLocation.end; let lastStart = lastLocation.start === undefined ? lastRegion.start : lastLocation.start; let lastEnd = lastLocation.end === undefined ? lastRegion.end : lastLocation.end; if (grow) { const expanded = expandRegion(firstStart, lastEnd, grow, firstRegion.start, lastRegion.end); firstStart = expanded.start; firstEnd = expanded.end; lastStart = expanded.start; lastEnd = expanded.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 buildRubberBandMenuItems(self); }, bpToPx({ refName, coord, regionNumber, }) { return bpToPx({ refName, coord, regionNumber, self }); }, getHighlightCoords(region) { const { assemblyManager } = getSession(self); const asm = region.assemblyName ? assemblyManager.get(region.assemblyName) : undefined; const refName = asm?.getCanonicalRefName(region.refName) ?? region.refName; const s = this.bpToPx({ refName, coord: region.start }); const e = this.bpToPx({ refName, coord: region.end }); return s && e ? { width: Math.max(Math.abs(e.offsetPx - s.offsetPx), 3), left: Math.min(s.offsetPx, e.offsetPx) - self.offsetPx, } : undefined; }, 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; }, get visibleRegions() { return self.dynamicBlocks.contentBlocks; }, })) .views(self => ({ rubberbandClickMenuItems(clickOffset) { return buildRubberbandClickMenuItems(self, clickOffset); }, })) .actions(self => ({ afterCreate() { setupKeyboardHandler(self); }, afterAttach() { doAfterAttach(self); }, })) .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; } const { init, offsetPx, bpPerPx, hideHeader, hideHeaderOverview, hideNoTracksActive, showGridlines, trackSelectorType, displayedRegions, highlight, showCenterLine, showCytobandsSetting, trackLabels, colorByCDS, showTrackOutlines, highlightsVisible, labelsVisible, ...rest } = snap; return { ...rest, ...(offsetPx ? { offsetPx } : {}), ...(bpPerPx !== 1 ? { bpPerPx } : {}), ...(hideHeader ? { hideHeader } : {}), ...(hideHeaderOverview ? { hideHeaderOverview } : {}), ...(hideNoTracksActive ? { hideNoTracksActive } : {}), ...(!showGridlines ? { showGridlines } : {}), ...(trackSelectorType !== 'hierarchical' ? { trackSelectorType } : {}), ...(displayedRegions.length ? { displayedRegions } : {}), ...(highlight.length ? { highlight } : {}), ...(showCenterLine ? { showCenterLine } : {}), ...(!showCytobandsSetting ? { showCytobandsSetting } : {}), ...(trackLabels ? { trackLabels } : {}), ...(colorByCDS ? { colorByCDS } : {}), ...(!showTrackOutlines ? { showTrackOutlines } : {}), ...(!highlightsVisible ? { highlightsVisible } : {}), ...(!labelsVisible ? { labelsVisible } : {}), }; }); } export { default as LinearGenomeView, default as ReactComponent, } from "./components/LinearGenomeView.js"; export { default as RefNameAutocomplete } from "./components/RefNameAutocomplete/index.js"; export { default as SearchBox } from "./components/SearchBox.js"; export { renderToSvg } from "./svgcomponents/SVGLinearGenomeView.js";