@jbrowse/plugin-linear-genome-view
Version:
JBrowse 2 linear genome view
935 lines (934 loc) • 38.2 kB
JavaScript
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";