UNPKG

molstar

Version:

A comprehensive macromolecular library.

203 lines (202 loc) 9.23 kB
/** * Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Adam Midlik <midlik@gmail.com> * @author David Sehnal <david.sehnal@gmail.com> */ import { ajaxGet } from '../../mol-util/data-source.js'; import { deepClone } from '../../mol-util/object.js'; import { createMVSX } from './export.js'; import { MVSAnimationSchema } from './tree/animation/animation-tree.js'; import { findUris, replaceUris, resolveUri, treeToString, windowUrl } from './tree/generic/tree-utils.js'; import { treeValidationIssues } from './tree/generic/tree-validation.js'; import { createMVSBuilder } from './tree/mvs/mvs-builder.js'; import { MVSTreeSchema } from './tree/mvs/mvs-tree.js'; export const GlobalMetadata = { create(metadata) { return { ...metadata, version: `${MVSData.SupportedVersion}`, timestamp: utcNowISO(), }; }, }; export const MVSData = { /** Currently supported major version of MolViewSpec format (e.g. 1 for version '1.0.8') */ SupportedVersion: 1, /** Parse MVSJ (MolViewSpec-JSON) format to `MVSData`. Does not include any validation. */ fromMVSJ(mvsjString) { var _a, _b; const result = JSON.parse(mvsjString); const major = majorVersion((_a = result === null || result === void 0 ? void 0 : result.metadata) === null || _a === void 0 ? void 0 : _a.version); if (major === undefined) { console.error('Loaded MVS does not contain valid version info.'); } else if (major > ((_b = majorVersion(MVSData.SupportedVersion)) !== null && _b !== void 0 ? _b : 0)) { console.warn(`Loaded MVS is of higher version (${result.metadata.version}) than currently supported version (${MVSData.SupportedVersion}). Some features may not work as expected.`); } return result; }, /** Encode `MVSData` to MVSJ (MolViewSpec-JSON) string. Use `space` parameter to control formatting (as with `JSON.stringify`). */ toMVSJ(mvsData, space) { return JSON.stringify(mvsData, undefined, space); }, /** Encode `MVSData` to MVSX (MolViewSpec JSON zipped together with referenced assets). Automatically fetches all referenced assets unless specified otherwise in `options`. */ async toMVSX(mvsData, options = {}) { var _a; let { assets, baseUri, skipExternal, cache } = options; mvsData = deepClone(mvsData); const uriParamNames = ['uri', 'url']; const trees = mvsData.kind === 'multiple' ? mvsData.snapshots.map(s => s.root) : [mvsData.root]; // Fetch assets: if (!assets) { assets = {}; cache !== null && cache !== void 0 ? cache : (cache = {}); const theWindowUrl = windowUrl(); const uris = new Set(); for (const tree of trees) { findUris(tree, uriParamNames, uris); } for (const uri of uris) { if (skipExternal && isAbsoluteUri(uri)) continue; const resolvedUri = resolveUri(uri, baseUri, theWindowUrl); const content = (_a = cache[resolvedUri]) !== null && _a !== void 0 ? _a : (cache[resolvedUri] = await ajaxGet({ url: resolvedUri, type: 'binary' }).run()); assets[uri] = content; } } // Replace URIs by asset names: const uriMapping = {}; const namedAssets = []; let counter = 0; for (const uri in assets) { const nameHint = uri.split('/').pop().replace(/[^\w\.+-]/g, '_').slice(0, 64); const assetName = `./assets/${counter++}-${nameHint}`; uriMapping[uri] = assetName; namedAssets.push({ name: assetName, content: assets[uri] }); } for (const tree of trees) { replaceUris(tree, uriMapping, uriParamNames); } // Zip: return await createMVSX(mvsData, namedAssets); }, /** Validate `MVSData`. Return `true` if OK; `false` if not OK. * If `options.noExtra` is true, presence of any extra node parameters is treated as an issue. */ isValid(mvsData, options = {}) { return MVSData.validationIssues(mvsData, options) === undefined; }, /** Validate `MVSData`. Return `undefined` if OK; list of issues if not OK. * If `options.noExtra` is true, presence of any extra node parameters is treated as an issue. */ validationIssues(mvsData, options = {}) { var _a; const version = (_a = mvsData === null || mvsData === void 0 ? void 0 : mvsData.metadata) === null || _a === void 0 ? void 0 : _a.version; if (typeof version !== 'string') return [`MVSData.metadata.version must be a string, not ${typeof version}: ${version}`]; if (mvsData.kind === 'single' || mvsData.kind === undefined) { return snapshotValidationIssues(mvsData, options); } else if (mvsData.kind === 'multiple') { if (mvsData.snapshots === undefined) return [`"snapshots" missing in MVS`]; const issues = []; for (const snapshot of mvsData.snapshots) { // would use .flatMap if it didn't work in a completely unpredictable way const snapshotIssues = snapshotValidationIssues(snapshot, options); if (snapshotIssues) issues.push(...snapshotIssues); } if (issues.length > 0) return issues; else return undefined; } else { return [`MVSData.kind must be 'single' or 'multiple', not ${mvsData.kind}`]; } }, /** Return a human-friendly textual representation of `mvsData`. */ toPrettyString(mvsData) { const type = mvsData.kind === 'multiple' ? 'multiple states' : 'single state'; const title = mvsData.metadata.title !== undefined ? ` "${mvsData.metadata.title}"` : ''; const trees = mvsData.kind === 'multiple' ? mvsData.snapshots.map((s, i) => `[Snapshot #${i}]\n${treeToString(s.root)}`).join('\n') : treeToString(mvsData.root); return `MolViewSpec ${type}${title} (version ${mvsData.metadata.version}, created ${mvsData.metadata.timestamp}):\n${trees}`; }, /** Create a new MolViewSpec builder containing only a root node. Example of MVS builder usage: * * ``` * const builder = MVSData.createBuilder(); * builder.canvas({ background_color: 'white' }); * const struct = builder.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1og2_updated.cif' }).parse({ format: 'mmcif' }).modelStructure(); * struct.component().representation().color({ color: '#3050F8' }); * console.log(MVSData.toPrettyString(builder.getState())); * ``` */ createBuilder() { return createMVSBuilder(); }, /** Create a multi-state MVS data from a list of snapshots. */ createMultistate(snapshots, metadata) { return { kind: 'multiple', snapshots: [...snapshots], metadata: GlobalMetadata.create(metadata), }; }, /** Convert single-state MVSData into multi-state MVSData with one state. */ stateToStates(state) { return { kind: 'multiple', metadata: state.metadata, snapshots: [{ metadata: { title: state.metadata.title, description: state.metadata.description, description_format: state.metadata.description_format, key: undefined, linger_duration_ms: 1000, transition_duration_ms: 250, }, root: state.root, }], }; }, }; /** Get the major version from a semantic version string, e.g. '1.0.8' -> 1 */ function majorVersion(semanticVersion) { if (typeof semanticVersion === 'string') return parseInt(semanticVersion.split('.')[0]); if (typeof semanticVersion === 'number') return Math.floor(semanticVersion); console.error(`Version should be a string, not ${typeof semanticVersion}: ${semanticVersion}`); return undefined; } function snapshotValidationIssues(snapshot, options = {}) { if (snapshot.root === undefined) return [`"root" missing in snapshot`]; const state = treeValidationIssues(MVSTreeSchema, snapshot.root, options); const animation = 'animation' in snapshot && snapshot.animation !== undefined ? treeValidationIssues(MVSAnimationSchema, snapshot.animation, options) : undefined; if (state && animation) return [...state, ...animation]; if (state) return state; if (animation) return animation; return undefined; } /** Return the current universal time, in ISO format, e.g. '2023-11-24T10:45:49.873Z' */ function utcNowISO() { return new Date().toISOString(); } function isAbsoluteUri(uri) { try { const url = new URL(uri); return !!url.protocol; } catch (_a) { return false; } }