UNPKG

molstar

Version:

A comprehensive macromolecular library.

449 lines (448 loc) 20 kB
/** * Copyright (c) 2023-2025 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 { Mat3, Mat4, Vec3 } from '../../mol-math/linear-algebra'; import { Volume } from '../../mol-model/volume'; import { arrayDistinct } from '../../mol-util/array'; import { canonicalJsonString } from '../../mol-util/json'; import { stringToWords } from '../../mol-util/string'; import { MVSAnnotationColorThemeProvider } from './components/annotation-color-theme'; import { MVSAnnotationLabelRepresentationProvider } from './components/annotation-label/representation'; import { MultilayerColorThemeName, NoColor } from './components/multilayer-color-theme'; import { SelectorAll } from './components/selector'; import { rowToExpression, rowsToExpression } from './helpers/selections'; import { decodeColor, isDefined, stringHash } from './helpers/utils'; import { getChildren } from './tree/generic/tree-schema'; import { dfs, formatObject } from './tree/generic/tree-utils'; import { DefaultColor } from './tree/mvs/mvs-tree'; export const AnnotationFromUriKinds = new Set(['color_from_uri', 'component_from_uri', 'label_from_uri', 'tooltip_from_uri']); export const AnnotationFromSourceKinds = new Set(['color_from_source', 'component_from_source', 'label_from_source', 'tooltip_from_source']); /** Return a 4x4 matrix representing a rotation followed by a translation */ export function transformFromRotationTranslation(rotation, translation) { if (rotation && rotation.length !== 9) throw new Error(`'rotation' param for 'transform' node must be array of 9 elements, found ${rotation}`); if (translation && translation.length !== 3) throw new Error(`'translation' param for 'transform' node must be array of 3 elements, found ${translation}`); const T = Mat4.identity(); if (rotation) { const rotMatrix = Mat3.fromArray(Mat3(), rotation, 0); ensureRotationMatrix(rotMatrix, rotMatrix); Mat4.fromMat3(T, rotMatrix); } if (translation) { Mat4.setTranslation(T, Vec3.fromArray(Vec3(), translation, 0)); } if (!Mat4.isRotationAndTranslation(T)) throw new Error(`'rotation' param for 'transform' is not a valid rotation matrix: ${rotation}`); return T; } /** Adjust values in a close-to-rotation matrix `a` to ensure it is a proper rotation matrix * (i.e. its columns and rows are orthonormal and determinant equal to 1, within available precission). */ function ensureRotationMatrix(out, a) { const x = Vec3.fromArray(_tmpVecX, a, 0); const y = Vec3.fromArray(_tmpVecY, a, 3); const z = Vec3.fromArray(_tmpVecZ, a, 6); Vec3.normalize(x, x); Vec3.orthogonalize(y, x, y); Vec3.normalize(z, Vec3.cross(z, x, y)); Mat3.fromColumns(out, x, y, z); return out; } const _tmpVecX = Vec3(); const _tmpVecY = Vec3(); const _tmpVecZ = Vec3(); /** Create an array of props for `TransformStructureConformation` transformers from all 'transform' nodes applied to a 'structure' node. */ export function transformProps(node) { const result = []; const transforms = getChildren(node).filter(c => c.kind === 'transform'); for (const transform of transforms) { const { rotation, translation } = transform.params; const matrix = transformFromRotationTranslation(rotation, translation); result.push({ transform: { name: 'matrix', params: { data: matrix, transpose: false } } }); } return result; } /** Collect distinct annotation specs from all nodes in `tree` and set `context.annotationMap[node]` to respective annotationIds */ export function collectAnnotationReferences(tree, context) { const distinctSpecs = {}; dfs(tree, node => { var _a, _b, _c; let spec = undefined; if (AnnotationFromUriKinds.has(node.kind)) { const p = node.params; spec = { source: { name: 'url', params: { url: p.uri, format: p.format } }, schema: p.schema, cifBlock: blockSpec(p.block_header, p.block_index), cifCategory: (_a = p.category_name) !== null && _a !== void 0 ? _a : undefined }; } else if (AnnotationFromSourceKinds.has(node.kind)) { const p = node.params; spec = { source: { name: 'source-cif', params: {} }, schema: p.schema, cifBlock: blockSpec(p.block_header, p.block_index), cifCategory: (_b = p.category_name) !== null && _b !== void 0 ? _b : undefined }; } if (spec) { const key = canonicalJsonString(spec); (_c = distinctSpecs[key]) !== null && _c !== void 0 ? _c : (distinctSpecs[key] = { ...spec, id: stringHash(key) }); context.annotationMap.set(node, distinctSpecs[key].id); } }); return Object.values(distinctSpecs); } function blockSpec(header, index) { if (isDefined(header)) { return { name: 'header', params: { header: header } }; } else { return { name: 'index', params: { index: index !== null && index !== void 0 ? index : 0 } }; } } /** Collect annotation tooltips from all nodes in `tree` and map them to annotationIds. */ export function collectAnnotationTooltips(tree, context) { const annotationTooltips = []; dfs(tree, node => { if (node.kind === 'tooltip_from_uri' || node.kind === 'tooltip_from_source') { const annotationId = context.annotationMap.get(node); if (annotationId) { annotationTooltips.push({ annotationId, fieldName: node.params.field_name }); } ; } }); return arrayDistinct(annotationTooltips); } /** Collect inline tooltips from all nodes in `tree`. */ export function collectInlineTooltips(tree, context) { const inlineTooltips = []; dfs(tree, (node, parent) => { if (node.kind === 'tooltip') { if ((parent === null || parent === void 0 ? void 0 : parent.kind) === 'component') { inlineTooltips.push({ text: node.params.text, selector: componentPropsFromSelector(parent.params.selector), }); } else if ((parent === null || parent === void 0 ? void 0 : parent.kind) === 'component_from_uri' || (parent === null || parent === void 0 ? void 0 : parent.kind) === 'component_from_source') { const p = componentFromXProps(parent, context); if (isDefined(p.annotationId) && isDefined(p.fieldName) && isDefined(p.fieldValues)) { inlineTooltips.push({ text: node.params.text, selector: { name: 'annotation', params: { annotationId: p.annotationId, fieldName: p.fieldName, fieldValues: p.fieldValues }, }, }); } } } }); return inlineTooltips; } /** Collect inline labels from all nodes in `tree`. */ export function collectInlineLabels(tree, context) { const inlineLabels = []; dfs(tree, (node, parent) => { if (node.kind === 'label') { if ((parent === null || parent === void 0 ? void 0 : parent.kind) === 'component') { inlineLabels.push({ text: node.params.text, position: { name: 'selection', params: { selector: componentPropsFromSelector(parent.params.selector), }, }, }); } else if ((parent === null || parent === void 0 ? void 0 : parent.kind) === 'component_from_uri' || (parent === null || parent === void 0 ? void 0 : parent.kind) === 'component_from_source') { const p = componentFromXProps(parent, context); if (isDefined(p.annotationId) && isDefined(p.fieldName) && isDefined(p.fieldValues)) { inlineLabels.push({ text: node.params.text, position: { name: 'selection', params: { selector: { name: 'annotation', params: { annotationId: p.annotationId, fieldName: p.fieldName, fieldValues: p.fieldValues }, }, }, }, }); } } } }); return inlineLabels; } /** Return `true` for components nodes which only serve for tooltip placement (not to be created in the MolStar object hierarchy) */ export function isPhantomComponent(node) { if (node.ref !== undefined) return false; if (node.custom !== undefined && Object.keys(node.custom).length > 0) return false; return node.children && node.children.every(child => child.kind === 'tooltip' || child.kind === 'label'); // These nodes could theoretically be removed when converting MVS to Molstar tree, but would get very tricky if we allow nested components } /** Create props for `StructureFromModel` transformer from a structure node. */ export function structureProps(node) { var _a; const params = node.params; switch (params.type) { case 'model': return { type: { name: 'model', params: {} }, }; case 'assembly': return { type: { name: 'assembly', params: { id: (_a = params.assembly_id) !== null && _a !== void 0 ? _a : undefined } }, }; case 'symmetry': return { type: { name: 'symmetry', params: { ijkMin: Vec3.ofArray(params.ijk_min), ijkMax: Vec3.ofArray(params.ijk_max) } }, }; case 'symmetry_mates': return { type: { name: 'symmetry-mates', params: { radius: params.radius } } }; default: throw new Error(`NotImplementedError: Loading action for "structure" node, type "${params.type}"`); } } /** Create value for `type` prop for `StructureComponent` transformer based on a MVS selector. */ export function componentPropsFromSelector(selector) { if (selector === undefined) { return SelectorAll; } else if (typeof selector === 'string') { return { name: 'static', params: selector }; } else if (Array.isArray(selector)) { return { name: 'expression', params: rowsToExpression(selector) }; } else { return { name: 'expression', params: rowToExpression(selector) }; } } /** Return a pretty name for a value of selector param, e.g. "protein" -> 'Protein', {label_asym_id: "A"} -> 'Custom Selection: {label_asym_id: "A"}' */ export function prettyNameFromSelector(selector) { if (selector === undefined) { return 'All'; } else if (typeof selector === 'string') { return stringToWords(selector); } else if (Array.isArray(selector)) { return `Custom Selection: [${selector.map(formatObject).join(', ')}]`; } else { return `Custom Selection: ${formatObject(selector)}`; } } /** Create props for `StructureRepresentation3D` transformer from a label_from_* node. */ export function labelFromXProps(node, context) { var _a; const annotationId = context.annotationMap.get(node); const fieldName = node.params.field_name; const nearestReprNode = (_a = context.nearestReprMap) === null || _a === void 0 ? void 0 : _a.get(node); return { type: { name: MVSAnnotationLabelRepresentationProvider.name, params: { annotationId, fieldName } }, colorTheme: colorThemeForNode(nearestReprNode, context), }; } /** Create props for `AnnotationStructureComponent` transformer from a component_from_* node. */ export function componentFromXProps(node, context) { const annotationId = context.annotationMap.get(node); const { field_name, field_values } = node.params; return { annotationId, fieldName: field_name, fieldValues: field_values ? { name: 'selected', params: field_values.map(v => ({ value: v })) } : { name: 'all', params: {} }, nullIfEmpty: false, }; } /** Create props for `StructureRepresentation3D` transformer from a representation node. */ export function representationProps(node) { var _a, _b; const alpha = alphaForNode(node); const params = node.params; switch (params.type) { case 'cartoon': return { type: { name: 'cartoon', params: { alpha, tubularHelices: params.tubular_helices } }, sizeTheme: { name: 'uniform', params: { value: params.size_factor } }, }; case 'ball_and_stick': return { type: { name: 'ball-and-stick', params: { sizeFactor: ((_a = params.size_factor) !== null && _a !== void 0 ? _a : 1) * 0.5, sizeAspectRatio: 0.5, alpha, ignoreHydrogens: params.ignore_hydrogens } }, }; case 'spacefill': return { type: { name: 'spacefill', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } }, sizeTheme: { name: 'physical', params: { scale: params.size_factor } }, }; case 'carbohydrate': return { type: { name: 'carbohydrate', params: { alpha, sizeFactor: (_b = params.size_factor) !== null && _b !== void 0 ? _b : 1 } }, }; case 'surface': return { type: { name: 'molecular-surface', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } }, sizeTheme: { name: 'physical', params: { scale: params.size_factor } }, }; default: throw new Error('NotImplementedError'); } } /** Create value for `type.params.alpha` prop for `StructureRepresentation3D` transformer from a representation node based on 'opacity' nodes in its subtree. */ export function alphaForNode(node) { const children = getChildren(node).filter(c => c.kind === 'opacity'); if (children.length > 0) { return children[children.length - 1].params.opacity; } else { return 1; } } function hasMolStarUseDefaultColoring(node) { if (!node.custom) return false; return 'molstar_use_default_coloring' in node.custom || 'molstar_color_theme_name' in node.custom; } /** Create value for `colorTheme` prop for `StructureRepresentation3D` transformer from a representation node based on color* nodes in its subtree. */ export function colorThemeForNode(node, context) { var _a, _b, _c; if ((node === null || node === void 0 ? void 0 : node.kind) === 'representation') { const children = getChildren(node).filter(c => c.kind === 'color' || c.kind === 'color_from_uri' || c.kind === 'color_from_source'); if (children.length === 0) { return { name: 'uniform', params: { value: decodeColor(DefaultColor) }, }; } else if (children.length === 1 && hasMolStarUseDefaultColoring(children[0])) { if ((_a = children[0].custom) === null || _a === void 0 ? void 0 : _a.molstar_use_default_coloring) return undefined; const custom = children[0].custom; return { name: (_b = custom === null || custom === void 0 ? void 0 : custom.molstar_color_theme_name) !== null && _b !== void 0 ? _b : undefined, params: (_c = custom === null || custom === void 0 ? void 0 : custom.molstar_color_theme_params) !== null && _c !== void 0 ? _c : {}, }; } else if (children.length === 1 && appliesColorToWholeRepr(children[0])) { return colorThemeForNode(children[0], context); } else { const layers = children.map(c => { const theme = colorThemeForNode(c, context); if (!theme) return undefined; return { theme, selection: componentPropsFromSelector(c.kind === 'color' ? c.params.selector : undefined) }; }).filter(t => !!t); return { name: MultilayerColorThemeName, params: { layers }, }; } } let annotationId = undefined; let fieldName = undefined; let color = undefined; switch (node === null || node === void 0 ? void 0 : node.kind) { case 'color_from_uri': case 'color_from_source': annotationId = context.annotationMap.get(node); fieldName = node.params.field_name; break; case 'color': color = node.params.color; break; } if (annotationId) { return { name: MVSAnnotationColorThemeProvider.name, params: { annotationId, fieldName, background: NoColor }, }; } else { return { name: 'uniform', params: { value: decodeColor(color) }, }; } } function appliesColorToWholeRepr(node) { if (node.kind === 'color') { return !isDefined(node.params.selector) || node.params.selector === 'all'; } else { return true; } } /** Create a mapping of nearest representation nodes for each node in the tree * (to transfer coloring to label nodes smartly). * Only considers nodes within the same 'structure' subtree. */ export function makeNearestReprMap(root) { const map = new Map(); // Propagate up: dfs(root, undefined, (node, parent) => { if (node.kind === 'representation') { map.set(node, node); } if (node.kind !== 'structure' && map.has(node) && parent && !map.has(parent)) { // do not propagate above the lowest structure node map.set(parent, map.get(node)); } }); // Propagate down: dfs(root, (node, parent) => { if (!map.has(node) && parent && map.has(parent)) { map.set(node, map.get(parent)); } }); return map; } /** Create props for `VolumeRepresentation3D` transformer from a representation node. */ export function volumeRepresentationProps(node) { var _a; const alpha = alphaForNode(node); const params = node.params; switch (params.type) { case 'isosurface': const isoValue = typeof params.absolute_isovalue === 'number' ? Volume.IsoValue.absolute(params.absolute_isovalue) : Volume.IsoValue.relative((_a = params.relative_isovalue) !== null && _a !== void 0 ? _a : 0); const visuals = []; if (params.show_wireframe) visuals.push('wireframe'); if (params.show_faces) visuals.push('solid'); return { type: { name: 'isosurface', params: { alpha, isoValue, visuals } }, }; default: throw new Error('NotImplementedError'); } } /** Create value for `colorTheme` prop for `StructureRepresentation3D` transformer from a representation node based on color* nodes in its subtree. */ export function volumeColorThemeForNode(node, context) { if ((node === null || node === void 0 ? void 0 : node.kind) !== 'volume_representation') return undefined; const children = getChildren(node).filter(c => c.kind === 'color'); if (children.length === 0) { return { name: 'uniform', params: { value: decodeColor(DefaultColor) }, }; } if (children.length === 1) { return colorThemeForNode(children[0], context); } }