@jbrowse/plugin-linear-genome-view
Version: 
JBrowse 2 linear genome view
1,065 lines (1,064 loc) • 43.9 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 { 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';