molstar
Version:
A comprehensive macromolecular library.
470 lines (469 loc) • 21.8 kB
JavaScript
"use strict";
/**
* 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>
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AnnotationFromSourceKinds = exports.AnnotationFromUriKinds = void 0;
exports.transformFromRotationTranslation = transformFromRotationTranslation;
exports.transformProps = transformProps;
exports.collectAnnotationReferences = collectAnnotationReferences;
exports.collectAnnotationTooltips = collectAnnotationTooltips;
exports.collectInlineTooltips = collectInlineTooltips;
exports.collectInlineLabels = collectInlineLabels;
exports.isPhantomComponent = isPhantomComponent;
exports.structureProps = structureProps;
exports.componentPropsFromSelector = componentPropsFromSelector;
exports.prettyNameFromSelector = prettyNameFromSelector;
exports.labelFromXProps = labelFromXProps;
exports.componentFromXProps = componentFromXProps;
exports.representationProps = representationProps;
exports.alphaForNode = alphaForNode;
exports.colorThemeForNode = colorThemeForNode;
exports.makeNearestReprMap = makeNearestReprMap;
exports.volumeRepresentationProps = volumeRepresentationProps;
exports.volumeColorThemeForNode = volumeColorThemeForNode;
const linear_algebra_1 = require("../../mol-math/linear-algebra");
const volume_1 = require("../../mol-model/volume");
const array_1 = require("../../mol-util/array");
const json_1 = require("../../mol-util/json");
const string_1 = require("../../mol-util/string");
const annotation_color_theme_1 = require("./components/annotation-color-theme");
const representation_1 = require("./components/annotation-label/representation");
const multilayer_color_theme_1 = require("./components/multilayer-color-theme");
const selector_1 = require("./components/selector");
const selections_1 = require("./helpers/selections");
const utils_1 = require("./helpers/utils");
const tree_schema_1 = require("./tree/generic/tree-schema");
const tree_utils_1 = require("./tree/generic/tree-utils");
const mvs_tree_1 = require("./tree/mvs/mvs-tree");
exports.AnnotationFromUriKinds = new Set(['color_from_uri', 'component_from_uri', 'label_from_uri', 'tooltip_from_uri']);
exports.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 */
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 = linear_algebra_1.Mat4.identity();
if (rotation) {
const rotMatrix = linear_algebra_1.Mat3.fromArray((0, linear_algebra_1.Mat3)(), rotation, 0);
ensureRotationMatrix(rotMatrix, rotMatrix);
linear_algebra_1.Mat4.fromMat3(T, rotMatrix);
}
if (translation) {
linear_algebra_1.Mat4.setTranslation(T, linear_algebra_1.Vec3.fromArray((0, linear_algebra_1.Vec3)(), translation, 0));
}
if (!linear_algebra_1.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 = linear_algebra_1.Vec3.fromArray(_tmpVecX, a, 0);
const y = linear_algebra_1.Vec3.fromArray(_tmpVecY, a, 3);
const z = linear_algebra_1.Vec3.fromArray(_tmpVecZ, a, 6);
linear_algebra_1.Vec3.normalize(x, x);
linear_algebra_1.Vec3.orthogonalize(y, x, y);
linear_algebra_1.Vec3.normalize(z, linear_algebra_1.Vec3.cross(z, x, y));
linear_algebra_1.Mat3.fromColumns(out, x, y, z);
return out;
}
const _tmpVecX = (0, linear_algebra_1.Vec3)();
const _tmpVecY = (0, linear_algebra_1.Vec3)();
const _tmpVecZ = (0, linear_algebra_1.Vec3)();
/** Create an array of props for `TransformStructureConformation` transformers from all 'transform' nodes applied to a 'structure' node. */
function transformProps(node) {
const result = [];
const transforms = (0, tree_schema_1.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 */
function collectAnnotationReferences(tree, context) {
const distinctSpecs = {};
(0, tree_utils_1.dfs)(tree, node => {
var _a, _b, _c;
let spec = undefined;
if (exports.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 (exports.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 = (0, json_1.canonicalJsonString)(spec);
(_c = distinctSpecs[key]) !== null && _c !== void 0 ? _c : (distinctSpecs[key] = { ...spec, id: (0, utils_1.stringHash)(key) });
context.annotationMap.set(node, distinctSpecs[key].id);
}
});
return Object.values(distinctSpecs);
}
function blockSpec(header, index) {
if ((0, utils_1.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. */
function collectAnnotationTooltips(tree, context) {
const annotationTooltips = [];
(0, tree_utils_1.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 (0, array_1.arrayDistinct)(annotationTooltips);
}
/** Collect inline tooltips from all nodes in `tree`. */
function collectInlineTooltips(tree, context) {
const inlineTooltips = [];
(0, tree_utils_1.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 ((0, utils_1.isDefined)(p.annotationId) && (0, utils_1.isDefined)(p.fieldName) && (0, utils_1.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`. */
function collectInlineLabels(tree, context) {
const inlineLabels = [];
(0, tree_utils_1.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 ((0, utils_1.isDefined)(p.annotationId) && (0, utils_1.isDefined)(p.fieldName) && (0, utils_1.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) */
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. */
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: linear_algebra_1.Vec3.ofArray(params.ijk_min), ijkMax: linear_algebra_1.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. */
function componentPropsFromSelector(selector) {
if (selector === undefined) {
return selector_1.SelectorAll;
}
else if (typeof selector === 'string') {
return { name: 'static', params: selector };
}
else if (Array.isArray(selector)) {
return { name: 'expression', params: (0, selections_1.rowsToExpression)(selector) };
}
else {
return { name: 'expression', params: (0, selections_1.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"}' */
function prettyNameFromSelector(selector) {
if (selector === undefined) {
return 'All';
}
else if (typeof selector === 'string') {
return (0, string_1.stringToWords)(selector);
}
else if (Array.isArray(selector)) {
return `Custom Selection: [${selector.map(tree_utils_1.formatObject).join(', ')}]`;
}
else {
return `Custom Selection: ${(0, tree_utils_1.formatObject)(selector)}`;
}
}
/** Create props for `StructureRepresentation3D` transformer from a label_from_* node. */
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: representation_1.MVSAnnotationLabelRepresentationProvider.name, params: { annotationId, fieldName } },
colorTheme: colorThemeForNode(nearestReprNode, context),
};
}
/** Create props for `AnnotationStructureComponent` transformer from a component_from_* node. */
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. */
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. */
function alphaForNode(node) {
const children = (0, tree_schema_1.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. */
function colorThemeForNode(node, context) {
var _a, _b, _c;
if ((node === null || node === void 0 ? void 0 : node.kind) === 'representation') {
const children = (0, tree_schema_1.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: (0, utils_1.decodeColor)(mvs_tree_1.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: multilayer_color_theme_1.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: annotation_color_theme_1.MVSAnnotationColorThemeProvider.name,
params: { annotationId, fieldName, background: multilayer_color_theme_1.NoColor },
};
}
else {
return {
name: 'uniform',
params: { value: (0, utils_1.decodeColor)(color) },
};
}
}
function appliesColorToWholeRepr(node) {
if (node.kind === 'color') {
return !(0, utils_1.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. */
function makeNearestReprMap(root) {
const map = new Map();
// Propagate up:
(0, tree_utils_1.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:
(0, tree_utils_1.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. */
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_1.Volume.IsoValue.absolute(params.absolute_isovalue) : volume_1.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. */
function volumeColorThemeForNode(node, context) {
if ((node === null || node === void 0 ? void 0 : node.kind) !== 'volume_representation')
return undefined;
const children = (0, tree_schema_1.getChildren)(node).filter(c => c.kind === 'color');
if (children.length === 0) {
return {
name: 'uniform',
params: { value: (0, utils_1.decodeColor)(mvs_tree_1.DefaultColor) },
};
}
if (children.length === 1) {
return colorThemeForNode(children[0], context);
}
}