molstar
Version:
A comprehensive macromolecular library.
449 lines (448 loc) • 20 kB
JavaScript
/**
* 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);
}
}