UNPKG

@jbrowse/core

Version:

JBrowse 2 core libraries used by plugins

347 lines (346 loc) 11.7 kB
import { getParent, getSnapshot, isRoot, isStateTreeNode, } from '@jbrowse/mobx-state-tree'; import { getFileHandle, storeFileHandle, verifyPermission, } from "./fileHandleStore.js"; import { getEnv, getSession, objectHash } from "./index.js"; import { readConfObject } from "../configuration/index.js"; const trackAssemblyNamesCache = new WeakMap(); export function getTrackAssemblyNames(track) { let cached = trackAssemblyNamesCache.get(track); if (!cached) { cached = getConfAssemblyNames(track.configuration); trackAssemblyNamesCache.set(track, cached); } return cached; } export function getConfAssemblyNames(conf) { const trackAssemblyNames = readConfObject(conf, 'assemblyNames'); if (!trackAssemblyNames) { const parent = getParent(conf); if ('sequence' in parent) { return [readConfObject(parent, 'name')]; } else { throw new Error('unknown assembly names'); } } return trackAssemblyNames; } export function getRpcSessionId(thisNode) { let highestRpcSessionId; for (let node = thisNode; !isRoot(node); node = getParent(node)) { if ('rpcSessionId' in node) { highestRpcSessionId = node.rpcSessionId; } } if (!highestRpcSessionId) { throw new Error('getRpcSessionId failed, no parent node in the state tree has an `rpcSessionId` attribute'); } return highestRpcSessionId; } export function getParentRenderProps(node) { for (let currentNode = getParent(node); !isRoot(currentNode); currentNode = getParent(currentNode)) { if ('renderProps' in currentNode) { return currentNode.renderProps(); } } return {}; } export const UNKNOWN = 'UNKNOWN'; export const UNSUPPORTED = 'UNSUPPORTED'; let blobMap = {}; export function getBlob(id) { return blobMap[id]; } export function getBlobMap() { return blobMap; } export function setBlobMap(map) { blobMap = map; } let counter = 0; export function storeBlobLocation(location) { if ('blob' in location) { const blobId = `b${Date.now()}-${counter++}`; blobMap[blobId] = location.blob; return { name: location.blob.name, blobId, locationType: 'BlobLocation', }; } return location; } let fileHandleCache = {}; export function getFileFromCache(handleId) { return fileHandleCache[handleId]; } export function setFileInCache(handleId, file) { fileHandleCache[handleId] = file; } export function clearFileFromCache(handleId) { delete fileHandleCache[handleId]; } export function getFileHandleCache() { return fileHandleCache; } export function setFileHandleCache(cache) { fileHandleCache = cache; } export async function ensureFileHandleReady(handleId, requestPermission = true) { const cached = fileHandleCache[handleId]; if (cached) { return cached; } const handle = await getFileHandle(handleId); if (!handle) { throw new Error(`File handle not found for handleId: ${handleId}. The file may have been opened in a different browser or the IndexedDB was cleared.`); } const hasPermission = await verifyPermission(handle, requestPermission); if (!hasPermission) { throw new Error(`Permission denied for file "${handle.name}". Click "Restore access" to grant permission.`); } const file = await handle.getFile(); fileHandleCache[handleId] = file; return file; } export async function storeFileHandleLocation(handle) { const handleId = await storeFileHandle(handle); const file = await handle.getFile(); fileHandleCache[handleId] = file; return { locationType: 'FileHandleLocation', name: handle.name, handleId, }; } export async function restoreFileHandles(handleIds, requestPermission = false) { const results = []; for (const handleId of handleIds) { try { await ensureFileHandleReady(handleId, requestPermission); results.push({ handleId, success: true }); } catch (e) { results.push({ handleId, success: false, error: e }); } } return results; } export function findFileHandleIds(obj, handleIds = new Set(), seen = new WeakSet()) { if (!obj || typeof obj !== 'object') { return handleIds; } if (seen.has(obj)) { return handleIds; } seen.add(obj); if (Array.isArray(obj)) { for (const item of obj) { findFileHandleIds(item, handleIds, seen); } } else { const record = obj; if (record.locationType === 'FileHandleLocation' && typeof record.handleId === 'string') { handleIds.add(record.handleId); } for (const value of Object.values(record)) { findFileHandleIds(value, handleIds, seen); } } return handleIds; } export async function restoreFileHandlesFromSnapshot(sessionSnapshot, requestPermission = false) { const handleIds = findFileHandleIds(sessionSnapshot); if (handleIds.size > 0) { return restoreFileHandles([...handleIds], requestPermission); } return []; } let pendingFileHandleIds = []; export function getPendingFileHandleIds() { return pendingFileHandleIds; } export function setPendingFileHandleIds(ids) { pendingFileHandleIds = ids; } export function clearPendingFileHandleIds() { pendingFileHandleIds = []; } export async function restorePendingFileHandles() { if (pendingFileHandleIds.length === 0) { return []; } const results = await restoreFileHandles(pendingFileHandleIds, true); const stillFailed = results.filter(r => !r.success).map(r => r.handleId); pendingFileHandleIds = stillFailed; return results; } export function makeIndex(location, suffix) { if ('uri' in location) { return { uri: location.uri + suffix, locationType: 'UriLocation', }; } else if ('localPath' in location) { return { localPath: location.localPath + suffix, locationType: 'LocalPathLocation', }; } else { return location; } } export function makeIndexType(name, typeA, typeB) { return name?.toUpperCase().endsWith(typeA) ? typeA : typeB; } export function getFileName(track) { const uri = 'uri' in track ? track.uri : undefined; const localPath = 'localPath' in track ? track.localPath : undefined; const blob = 'blobId' in track ? track : undefined; const fileHandle = 'handleId' in track ? track : undefined; if (blob?.name) { return blob.name; } if (fileHandle?.name) { return fileHandle.name; } if (uri) { const normalized = uri.replace(/\\/g, '/'); return normalized.slice(normalized.lastIndexOf('/') + 1); } if (localPath) { const normalized = localPath.replace(/\\/g, '/'); return normalized.slice(normalized.lastIndexOf('/') + 1); } return ''; } export function guessAdapter(file, index, adapterHint, model) { if (model) { const { pluginManager } = getEnv(model); const adapterGuesser = pluginManager.evaluateExtensionPoint('Core-guessAdapterForLocation', (_file, _index, _adapterHint) => { return undefined; }); const adapter = adapterGuesser(file, index, adapterHint); if (adapter) { return adapter; } } return { type: UNKNOWN, }; } export function guessTrackType(adapterType, model, file) { if (model) { const session = getSession(model); const trackTypeGuesser = getEnv(session).pluginManager.evaluateExtensionPoint('Core-guessTrackTypeForLocation', (_adapterName) => { return undefined; }); const trackType = trackTypeGuesser(adapterType, file); if (trackType) { return trackType; } } return 'FeatureTrack'; } export function generateUnsupportedTrackConf(trackName, trackUrl, categories) { const conf = { type: 'FeatureTrack', name: `${trackName} (Unsupported)`, description: `Support not yet implemented for "${trackUrl}"`, category: categories, trackId: '', }; conf.trackId = objectHash(conf); return conf; } export function generateUnknownTrackConf(trackName, trackUrl, categories) { const conf = { type: 'FeatureTrack', name: `${trackName} (Unknown)`, description: `Could not determine track type for "${trackUrl}"`, category: categories, trackId: '', }; conf.trackId = objectHash(conf); return conf; } export function getTrackName(conf, session) { const trackName = isStateTreeNode(conf) ? readConfObject(conf, 'name') : (conf.name ?? ''); const trackType = isStateTreeNode(conf) ? readConfObject(conf, 'type') : conf.type; if (!trackName && trackType === 'ReferenceSequenceTrack') { const asm = session.assemblies.find(a => a.sequence === conf); return asm ? `Reference sequence (${readConfObject(asm, 'displayName') || readConfObject(asm, 'name')})` : 'Reference sequence'; } return trackName; } export function showTrackGeneric(self, trackId, initialSnapshot = {}, displayInitialSnapshot = {}) { const { pluginManager } = getEnv(self); const session = getSession(self); const found = self.tracks.find(t => t.configuration.trackId === trackId); if (found) { return found; } const rawConf = session.getTracksById()[trackId]; if (!rawConf) { throw new Error(`Could not resolve identifier "${trackId}"`); } const confSnapshot = isStateTreeNode(rawConf) ? getSnapshot(rawConf) : structuredClone(rawConf); const conf = pluginManager.evaluateExtensionPoint('Core-preProcessTrackConfig', confSnapshot); const trackType = pluginManager.getTrackType(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 displays = conf.displays ?? []; const displayConf = displays.find((d) => supportedDisplays.has(d.type)); const snapshotType = displayInitialSnapshot.type; const defaultDisplayType = displayConf?.type ?? trackType.displayTypes.find(d => supportedDisplays.has(d.name))?.name; const displayType = snapshotType ?? defaultDisplayType; if (!displayType) { throw new Error(`Could not find a compatible display for view type ${self.type}`); } const displayId = displayConf?.displayId ?? `${trackId}-${displayType}`; const { type: _type, displayId: _displayId, ...displayConfState } = displayConf ?? {}; const track = trackType.stateModel.create({ ...initialSnapshot, type: conf.type, configuration: trackId, displays: [ { type: displayType, configuration: displayId, ...displayConfState, ...displayInitialSnapshot, }, ], }); self.tracks.push(track); return track; } export function hideTrackGeneric(self, trackId) { const t = self.tracks.find(t => t.configuration.trackId === trackId); if (t) { self.tracks.remove(t); return 1; } return 0; } export function toggleTrackGeneric(self, trackId) { const hiddenCount = hideTrackGeneric(self, trackId); if (!hiddenCount) { showTrackGeneric(self, trackId); } }