Skip to content
Snippets Groups Projects
Commit 2b77f6d2 authored by Michal Malý's avatar Michal Malý
Browse files

Initial implementation of ReDNATCO viewer

parent 4b323a67
No related branches found
No related tags found
No related merge requests found
export type ID ='data'|'structure'|'visual'|'pyramids';
export function ID(id: ID, ref: string) {
return `${id}_${ref}`;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="molstar.css" />
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="./molstar.js"></script>
</body>
</html>
import React from 'react';
import ReactDOM from 'react-dom';
import * as IDs from './idents';
import { DnatcoConfalPyramids } from '../../extensions/dnatco';
import { ConfalPyramidsParams } from '../../extensions/dnatco/confal-pyramids/representation';
import { ConfalPyramidsColorThemeParams } from '../../extensions/dnatco/confal-pyramids/color';
import { Loci } from '../../mol-model/loci';
import { Structure } from '../../mol-model/structure';
import { PluginBehavior, PluginBehaviors } from '../../mol-plugin/behavior';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginContext } from '../../mol-plugin/context';
import { PluginSpec } from '../../mol-plugin/spec';
import { LociLabel } from '../../mol-plugin-state/manager/loci-label';
import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { RawData } from '../../mol-plugin-state/transforms/data';
import { createPluginUI } from '../../mol-plugin-ui';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
import { Representation } from '../../mol-repr/representation';
import { StateSelection } from '../../mol-state';
import { StateTreeSpine } from '../../mol-state/tree/spine';
import { lociLabel } from '../../mol-theme/label';
import { Color } from '../../mol-util/color';
import { arrayMax } from '../../mol-util/array';
import { Binding } from '../../mol-util/binding';
import { ButtonsType, ModifiersKeys } from '../../mol-util/input/input-observer';
import { MarkerAction } from '../../mol-util/marker-action';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ObjectKeys } from '../../mol-util/type-helpers';
import './index.html';
const Extensions = {
'ntc-balls-pyramids-prop': PluginSpec.Behavior(DnatcoConfalPyramids),
};
const BaseRef = 'rdo';
const AnimationDurationMsec = 150;
const ReDNATCOLociLabelProvider = PluginBehavior.create({
name: 'watlas-loci-label-provider',
category: 'interaction',
ctor: class implements PluginBehavior<undefined> {
private f = {
label: (loci: Loci) => {
switch (loci.kind) {
case 'structure-loci':
case 'element-loci':
return lociLabel(loci);
default:
return '';
}
},
group: (label: LociLabel) => label.toString().replace(/Model [0-9]+/g, 'Models'),
priority: 100
};
register() { this.ctx.managers.lociLabels.addProvider(this.f); }
unregister() { this.ctx.managers.lociLabels.removeProvider(this.f); }
constructor(protected ctx: PluginContext) { }
},
display: { name: 'ReDNATCO labeling' }
});
const ReDNATCOLociSelectionBindings = {
clickFocus: Binding([Binding.Trigger(ButtonsType.Flag.Secondary)], 'Focus camera on selected loci using ${triggers}'),
clickToggle: Binding([Binding.Trigger(ButtonsType.Flag.Primary)], 'Set selection to clicked element using ${triggers}.'),
clickDeselectAllOnEmpty: Binding([Binding.Trigger(ButtonsType.Flag.Primary)], 'Deselect all when clicking on nothing using ${triggers}.'),
};
const ReDNATCOLociSelectionParams = {
bindings: PD.Value(ReDNATCOLociSelectionBindings, { isHidden: true }),
};
type WatlasLociSelectionProps = PD.Values<typeof ReDNATCOLociSelectionParams>;
const ReDNATCOLociSelectionProvider = PluginBehavior.create({
name: 'watlas-loci-selection-provider',
category: 'interaction',
display: { name: 'Interactive loci selection' },
params: () => ReDNATCOLociSelectionParams,
ctor: class extends PluginBehavior.Handler<WatlasLociSelectionProps> {
private spine: StateTreeSpine.Impl;
private lociMarkProvider = (reprLoci: Representation.Loci, action: MarkerAction) => {
if (!this.ctx.canvas3d) return;
this.ctx.canvas3d.mark({ loci: reprLoci.loci }, action);
};
private applySelectMark(ref: string, clear?: boolean) {
const cell = this.ctx.state.data.cells.get(ref);
if (cell && PSO.isRepresentation3D(cell.obj)) {
this.spine.current = cell;
const so = this.spine.getRootOfType(PSO.Molecule.Structure);
if (so) {
if (clear) {
this.lociMarkProvider({ loci: Structure.Loci(so.data) }, MarkerAction.Deselect);
}
const loci = this.ctx.managers.structure.selection.getLoci(so.data);
this.lociMarkProvider({ loci }, MarkerAction.Select);
}
}
}
private focusOnLoci(loci: Representation.Loci) {
if (!this.ctx.canvas3d)
return;
const sphere = Loci.getBoundingSphere(loci.loci);
if (!sphere)
return;
const snapshot = this.ctx.canvas3d.camera.getSnapshot();
snapshot.target = sphere.center;
PluginCommands.Camera.SetSnapshot(this.ctx, { snapshot, durationMs: AnimationDurationMsec });
}
register() {
const lociIsEmpty = (current: Representation.Loci) => Loci.isEmpty(current.loci);
const lociIsNotEmpty = (current: Representation.Loci) => !Loci.isEmpty(current.loci);
const actions: [keyof typeof ReDNATCOLociSelectionBindings, (current: Representation.Loci) => void, ((current: Representation.Loci) => boolean) | undefined][] = [
['clickFocus', current => this.focusOnLoci(current), lociIsNotEmpty],
['clickDeselectAllOnEmpty', () => this.ctx.managers.interactivity.lociSelects.deselectAll(), lociIsEmpty],
['clickToggle', current => {
if (current.loci.kind === 'element-loci')
this.ctx.managers.interactivity.lociSelects.toggle(current, true);
},
lociIsNotEmpty],
];
// sort the action so that the ones with more modifiers trigger sooner.
actions.sort((a, b) => {
const x = this.params.bindings[a[0]], y = this.params.bindings[b[0]];
const k = x.triggers.length === 0 ? 0 : arrayMax(x.triggers.map(t => ModifiersKeys.size(t.modifiers)));
const l = y.triggers.length === 0 ? 0 : arrayMax(y.triggers.map(t => ModifiersKeys.size(t.modifiers)));
return l - k;
});
this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
if (!this.ctx.canvas3d) return;
// only trigger the 1st action that matches
for (const [binding, action, condition] of actions) {
if (Binding.match(this.params.bindings[binding], button, modifiers) && (!condition || condition(current))) {
action(current);
break;
}
}
});
this.ctx.managers.interactivity.lociSelects.addProvider(this.lociMarkProvider);
this.subscribeObservable(this.ctx.state.events.object.created, ({ ref }) => this.applySelectMark(ref));
// re-apply select-mark to all representation of an updated structure
this.subscribeObservable(this.ctx.state.events.object.updated, ({ ref, obj, oldObj, oldData, action }) => {
const cell = this.ctx.state.data.cells.get(ref);
if (cell && PSO.Molecule.Structure.is(cell.obj)) {
const structure: Structure = obj.data;
const oldStructure: Structure | undefined = action === 'recreate' ? oldObj?.data :
action === 'in-place' ? oldData : undefined;
if (oldStructure &&
Structure.areEquivalent(structure, oldStructure) &&
Structure.areHierarchiesEqual(structure, oldStructure)) return;
const reprs = this.ctx.state.data.select(StateSelection.Generators.ofType(PSO.Molecule.Structure.Representation3D, ref));
for (const repr of reprs) this.applySelectMark(repr.transform.ref, true);
}
});
}
unregister() {
}
constructor(ctx: PluginContext, params: WatlasLociSelectionProps) {
super(ctx, params);
this.spine = new StateTreeSpine.Impl(ctx.state.data.cells);
}
},
});
class ReDNATCOMspViewer {
constructor(public plugin: PluginUIContext) {
}
private pyramidsParams(colors: Map<string, Color>, visible: Map<string, boolean>, transparent: boolean) {
const typeParams = {} as PD.Values<ConfalPyramidsParams>;
for (const k of Reflect.ownKeys(ConfalPyramidsParams) as (keyof ConfalPyramidsParams)[]) {
if (ConfalPyramidsParams[k].type === 'boolean')
(typeParams[k] as any) = visible.get(k) ?? ConfalPyramidsParams[k]['defaultValue'];
}
const colorParams = {} as Record<string, Color>; // HAKZ until we implement changeable pyramid colors in Molstar !!!
for (const k of Reflect.ownKeys(ConfalPyramidsColorThemeParams) as (keyof ConfalPyramidsColorThemeParams)[])
colorParams[k] = colors.get(k) ?? ConfalPyramidsColorThemeParams[k]['defaultValue'];
return {
type: { name: 'confal-pyramids', params: { ...typeParams, alpha: transparent ? 0.5 : 1.0 } },
colorTheme: { name: 'confal-pyramids', params: colorParams }
};
}
static async create(target: HTMLElement) {
const defaultSpec = DefaultPluginUISpec();
const spec: PluginUISpec = {
...defaultSpec,
behaviors: [
PluginSpec.Behavior(ReDNATCOLociLabelProvider),
PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci),
PluginSpec.Behavior(ReDNATCOLociSelectionProvider),
...ObjectKeys(Extensions).map(k => Extensions[k]),
],
components: {
...defaultSpec.components,
controls: {
...defaultSpec.components?.controls,
top: 'none',
right: 'none',
bottom: 'none',
left: 'none'
},
},
layout: {
initial: {
isExpanded: false,
showControls: false,
},
},
};
const plugin = await createPluginUI(target, spec);
plugin.managers.interactivity.setProps({ granularity: 'two-residues' });
plugin.selectionMode = true;
return new ReDNATCOMspViewer(plugin);
}
async loadStructure(data: string, type: 'pdb'|'cif') {
await this.plugin.state.data.build().toRoot().commit();
const b = (t => type === 'pdb'
? t.apply(StateTransforms.Model.TrajectoryFromPDB)
: t.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
)(this.plugin.state.data.build().toRoot().apply(RawData, { data }, { ref: IDs.ID('data', BaseRef) }))
.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
.apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', BaseRef) })
.apply(
StateTransforms.Representation.StructureRepresentation3D,
{
type: { name: 'cartoon', params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false } },
},
{ ref: IDs.ID('visual', BaseRef) }
)
.to(IDs.ID('structure', BaseRef))
.apply(
StateTransforms.Representation.StructureRepresentation3D,
this.pyramidsParams(new Map(), new Map(), false),
{ ref: IDs.ID('pyramids', BaseRef) }
);
b.commit();
}
}
class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props> {
private viewer: ReDNATCOMspViewer|null = null;
loadStructure(data: string, type: 'pdb'|'cif') {
if (this.viewer)
this.viewer.loadStructure(data, type);
}
componentDidMount() {
if (!this.viewer) {
const elem = document.getElementById(this.props.elemId + '-viewer');
ReDNATCOMspViewer.create(elem!).then(viewer => {
this.viewer = viewer;
ReDNATCOMspApi.bind(this);
if (this.props.onInited)
this.props.onInited();
});
}
}
render() {
return (
<div className='rmsp-app'>
<div id={this.props.elemId + '-viewer'}></div>
<div>Controls</div>
</div>
);
}
}
namespace ReDNATCOMsp {
export interface Props {
elemId: string;
onInited?: () => void;
}
export function init(elemId: string, onInited?: () => void) {
const elem = document.getElementById(elemId);
if (!elem)
throw new Error(`Element ${elemId} does not exist`);
ReactDOM.render(<ReDNATCOMsp elemId={elemId} onInited={onInited} />, elem);
}
}
class _ReDNATCOMspApi {
private target: ReDNATCOMsp|null = null;
private check() {
if (!this.target)
throw new Error('ReDNATCOMsp object not bound');
}
bind(target: ReDNATCOMsp) {
this.target = target;
}
init(elemId: string, onInited?: () => void) {
ReDNATCOMsp.init(elemId, onInited);
}
loadStructure(data: string) {
this.check();
this.target!.loadStructure(data, 'cif');
}
}
export const ReDNATCOMspApi = new _ReDNATCOMspApi();
......@@ -9,8 +9,7 @@ const tests = [
];
module.exports = [
createApp('viewer', 'molstar'),
createApp('docking-viewer', 'molstar'),
createApp('rednatco', 'molstar'),
...examples.map(createExample),
...tests.map(createBrowserTest)
];
\ No newline at end of file
];
......@@ -3,7 +3,6 @@ const { createApp, createExample } = require('./webpack.config.common.js');
const examples = ['proteopedia-wrapper', 'basic-wrapper', 'lighting', 'alpha-orbitals'];
module.exports = [
createApp('viewer', 'molstar'),
createApp('docking-viewer', 'molstar'),
createApp('rednatco', 'molstar'),
...examples.map(createExample)
];
\ No newline at end of file
];
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment