molstar
Version:
A comprehensive macromolecular library.
305 lines (304 loc) • 15.3 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>
* @author Aliaksei Chareshneu <chareshneu.tech@gmail.com>
*/
import { PluginStateSnapshotManager } from '../../mol-plugin-state/manager/snapshots';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { Download, ParseCcp4, ParseCif } from '../../mol-plugin-state/transforms/data';
import { CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif, TrajectoryFromPDB, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
import { ShapeRepresentation3D, StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { VolumeFromCcp4, VolumeFromDensityServerCif } from '../../mol-plugin-state/transforms/volume';
import { PluginCommands } from '../../mol-plugin/commands';
import { MolViewSpec } from './behavior';
import { createPluginStateSnapshotCamera, modifyCanvasProps } from './camera';
import { MVSAnnotationsProvider } from './components/annotation-prop';
import { MVSAnnotationStructureComponent } from './components/annotation-structure-component';
import { MVSAnnotationTooltipsProvider } from './components/annotation-tooltips-prop';
import { CustomLabelRepresentationProvider } from './components/custom-label/representation';
import { CustomTooltipsProvider } from './components/custom-tooltips-prop';
import { IsMVSModelProvider } from './components/is-mvs-model-prop';
import { getPrimitiveStructureRefs, MVSBuildPrimitiveShape, MVSDownloadPrimitiveData, MVSInlinePrimitiveData } from './components/primitives';
import { IsHiddenCustomStateExtension } from './load-extensions/is-hidden-custom-state';
import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions';
import { loadTreeVirtual, UpdateTarget } from './load-generic';
import { collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformProps, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
import { MVSData } from './mvs-data';
import { validateTree } from './tree/generic/tree-schema';
import { convertMvsToMolstar, mvsSanityCheck } from './tree/molstar/conversion';
import { MolstarTreeSchema } from './tree/molstar/molstar-tree';
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
/** Load a MolViewSpec (MVS) state(s) into the Mol* plugin as plugin state snapshots. */
export async function loadMVS(plugin, data, options = {}) {
plugin.errorContext.clear('mvs');
try {
const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec);
if (!mvsExtensionLoaded)
throw new Error('MolViewSpec extension is not loaded.');
// console.log(`MVS tree:\n${MVSData.toPrettyString(data)}`)
const multiData = data.kind === 'multiple' ? data : MVSData.stateToStates(data);
const entries = [];
for (let i = 0; i < multiData.snapshots.length; i++) {
const snapshot = multiData.snapshots[i];
const previousSnapshot = i > 0 ? multiData.snapshots[i - 1] : multiData.snapshots[multiData.snapshots.length - 1];
validateTree(MVSTreeSchema, snapshot.root, 'MVS');
if (options.sanityChecks)
mvsSanityCheck(snapshot.root);
const molstarTree = convertMvsToMolstar(snapshot.root, options.sourceUrl);
validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar');
const entry = molstarTreeToEntry(plugin, molstarTree, { ...snapshot.metadata, previousTransitionDurationMs: previousSnapshot.metadata.transition_duration_ms }, options);
entries.push(entry);
}
if (!options.appendSnapshots) {
plugin.managers.snapshot.clear();
}
for (const entry of entries) {
plugin.managers.snapshot.add(entry);
}
if (entries.length > 0) {
await PluginCommands.State.Snapshots.Apply(plugin, { id: entries[0].snapshot.id });
}
}
catch (err) {
plugin.log.error(`${err}`);
throw err;
}
finally {
if (!options.doNotReportErrors) {
for (const error of plugin.errorContext.get('mvs')) {
plugin.log.warn(error);
PluginCommands.Toast.Show(plugin, {
title: 'Error',
message: error,
timeoutMs: 10000
});
}
}
plugin.errorContext.clear('mvs');
}
}
function molstarTreeToEntry(plugin, tree, metadata, options) {
var _a, _b, _c;
const context = MolstarLoadingContext.create();
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, { replaceExisting: true, extensions: (_a = options === null || options === void 0 ? void 0 : options.extensions) !== null && _a !== void 0 ? _a : BuiltinLoadingExtensions });
snapshot.canvas3d = {
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas) : undefined,
};
if (!(options === null || options === void 0 ? void 0 : options.keepCamera)) {
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, metadata);
}
snapshot.durationInMs = metadata.linger_duration_ms + ((_b = metadata.previousTransitionDurationMs) !== null && _b !== void 0 ? _b : 0);
const entryParams = {
key: metadata.key,
name: metadata.title,
description: metadata.description,
descriptionFormat: (_c = metadata.description_format) !== null && _c !== void 0 ? _c : 'markdown',
};
const entry = PluginStateSnapshotManager.Entry(snapshot, entryParams);
return entry;
}
export const MolstarLoadingContext = {
create() {
return {
annotationMap: new Map(),
camera: { focuses: [] },
};
},
};
/** Loading actions for loading a `MolstarTree`, per node kind. */
const MolstarLoadingActions = {
root(updateParent, node, context) {
context.nearestReprMap = makeNearestReprMap(node);
return updateParent;
},
download(updateParent, node) {
return UpdateTarget.apply(updateParent, Download, {
url: node.params.url,
isBinary: node.params.is_binary,
});
},
parse(updateParent, node) {
const format = node.params.format;
if (format === 'cif') {
return UpdateTarget.apply(updateParent, ParseCif, {});
}
else if (format === 'pdb') {
return updateParent;
}
else if (format === 'map') {
return UpdateTarget.apply(updateParent, ParseCcp4, {});
}
else {
console.error(`Unknown format in "parse" node: "${format}"`);
return undefined;
}
},
trajectory(updateParent, node) {
var _a, _b;
const format = node.params.format;
if (format === 'cif') {
return UpdateTarget.apply(updateParent, TrajectoryFromMmCif, {
blockHeader: (_a = node.params.block_header) !== null && _a !== void 0 ? _a : '', // Must set to '' because just undefined would get overwritten by createDefaults
blockIndex: (_b = node.params.block_index) !== null && _b !== void 0 ? _b : undefined,
});
}
else if (format === 'pdb') {
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, {});
}
else {
console.error(`Unknown format in "trajectory" node: "${format}"`);
return undefined;
}
},
model(updateParent, node, context) {
const annotations = collectAnnotationReferences(node, context);
const model = UpdateTarget.apply(updateParent, ModelFromTrajectory, {
modelIndex: node.params.model_index,
});
UpdateTarget.apply(model, CustomModelProperties, {
properties: {
[IsMVSModelProvider.descriptor.name]: { isMvs: true },
[MVSAnnotationsProvider.descriptor.name]: { annotations },
},
autoAttach: [
IsMVSModelProvider.descriptor.name,
MVSAnnotationsProvider.descriptor.name,
],
});
return model;
},
structure(updateParent, node, context) {
var _a;
const props = structureProps(node);
const struct = UpdateTarget.apply(updateParent, StructureFromModel, props);
let transformed = struct;
for (const t of transformProps(node)) {
transformed = UpdateTarget.apply(transformed, TransformStructureConformation, t); // applying to the result of previous transform, to get the correct transform order
}
const annotationTooltips = collectAnnotationTooltips(node, context);
const inlineTooltips = collectInlineTooltips(node, context);
if (annotationTooltips.length + inlineTooltips.length > 0) {
UpdateTarget.apply(struct, CustomStructureProperties, {
properties: {
[MVSAnnotationTooltipsProvider.descriptor.name]: { tooltips: annotationTooltips },
[CustomTooltipsProvider.descriptor.name]: { tooltips: inlineTooltips },
},
autoAttach: [
MVSAnnotationTooltipsProvider.descriptor.name,
CustomTooltipsProvider.descriptor.name,
],
});
}
const inlineLabels = collectInlineLabels(node, context);
if (inlineLabels.length > 0) {
const nearestReprNode = (_a = context.nearestReprMap) === null || _a === void 0 ? void 0 : _a.get(node);
UpdateTarget.apply(struct, StructureRepresentation3D, {
type: {
name: CustomLabelRepresentationProvider.name,
params: { items: inlineLabels },
},
colorTheme: colorThemeForNode(nearestReprNode, context),
});
}
return struct;
},
tooltip: undefined, // No action needed, already loaded in `structure`
tooltip_from_uri: undefined, // No action needed, already loaded in `structure`
tooltip_from_source: undefined, // No action needed, already loaded in `structure`
component(updateParent, node) {
if (isPhantomComponent(node)) {
return updateParent;
}
const selector = node.params.selector;
return UpdateTarget.apply(updateParent, StructureComponent, {
type: componentPropsFromSelector(selector),
label: prettyNameFromSelector(selector),
nullIfEmpty: false,
});
},
component_from_uri(updateParent, node, context) {
if (isPhantomComponent(node))
return undefined;
const props = componentFromXProps(node, context);
return UpdateTarget.apply(updateParent, MVSAnnotationStructureComponent, props);
},
component_from_source(updateParent, node, context) {
if (isPhantomComponent(node))
return undefined;
const props = componentFromXProps(node, context);
return UpdateTarget.apply(updateParent, MVSAnnotationStructureComponent, props);
},
representation(updateParent, node, context) {
return UpdateTarget.apply(updateParent, StructureRepresentation3D, {
...representationProps(node),
colorTheme: colorThemeForNode(node, context),
});
},
volume(updateParent, node) {
var _a, _b;
if ((_a = updateParent.transformer) === null || _a === void 0 ? void 0 : _a.definition.to.includes(PluginStateObject.Format.Ccp4)) {
return UpdateTarget.apply(updateParent, VolumeFromCcp4, {});
}
else if ((_b = updateParent.transformer) === null || _b === void 0 ? void 0 : _b.definition.to.includes(PluginStateObject.Format.Cif)) {
return UpdateTarget.apply(updateParent, VolumeFromDensityServerCif, { blockHeader: node.params.channel_id || undefined });
}
else {
console.error(`Unsupported volume format`);
return undefined;
}
},
volume_representation(updateParent, node, context) {
return UpdateTarget.apply(updateParent, VolumeRepresentation3D, {
...volumeRepresentationProps(node),
colorTheme: volumeColorThemeForNode(node, context),
});
},
color: undefined, // No action needed, already loaded in `representation`
color_from_uri: undefined, // No action needed, already loaded in `representation`
color_from_source: undefined, // No action needed, already loaded in `representation`
label: undefined, // No action needed, already loaded in `structure`
label_from_uri(updateParent, node, context) {
const props = labelFromXProps(node, context);
return UpdateTarget.apply(updateParent, StructureRepresentation3D, props);
},
label_from_source(updateParent, node, context) {
const props = labelFromXProps(node, context);
return UpdateTarget.apply(updateParent, StructureRepresentation3D, props);
},
focus(updateParent, node, context) {
context.camera.focuses.push({ target: updateParent.selector, params: node.params });
return updateParent;
},
camera(updateParent, node, context) {
context.camera.cameraParams = node.params;
return updateParent;
},
canvas(updateParent, node, context) {
context.canvas = node.params;
return updateParent;
},
primitives(updateParent, tree, context) {
const refs = getPrimitiveStructureRefs(tree);
const data = UpdateTarget.apply(updateParent, MVSInlinePrimitiveData, { node: tree });
return applyPrimitiveVisuals(data, refs);
},
primitives_from_uri(updateParent, tree, context) {
const data = UpdateTarget.apply(updateParent, MVSDownloadPrimitiveData, { uri: tree.params.uri, format: tree.params.format });
return applyPrimitiveVisuals(data, new Set(tree.params.references));
},
};
function applyPrimitiveVisuals(data, refs) {
const mesh = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'mesh' }, { state: { isGhost: true } }), refs);
UpdateTarget.apply(mesh, ShapeRepresentation3D);
const labels = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'labels' }, { state: { isGhost: true } }), refs);
UpdateTarget.apply(labels, ShapeRepresentation3D);
const lines = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'lines' }, { state: { isGhost: true } }), refs);
UpdateTarget.apply(lines, ShapeRepresentation3D);
return data;
}
export const BuiltinLoadingExtensions = [
NonCovalentInteractionsExtension,
IsHiddenCustomStateExtension,
];