Newer
Older
import React from 'react';
import ReactDOM from 'react-dom';
import { NtCColors } from './colors';
import { ColorPicker } from './color-picker';
import { PushButton, ToggleButton } from './controls';
import * as RefCfmr from './reference-conformers';
import { ReferenceConformersPdbs } from './reference-conformers-pdbs';
import { Step } from './step';
import { Superpose } from './superpose';
import { DnatcoConfalPyramids } from '../../extensions/dnatco';
import { ConfalPyramidsParams } from '../../extensions/dnatco/confal-pyramids/representation';
import { OrderedSet } from '../../mol-data/int/ordered-set';
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
import { Loci } from '../../mol-model/loci';
import { ElementIndex, Model, Structure, StructureElement, StructureProperties, StructureSelection, Trajectory } from '../../mol-model/structure';
import { Location } from '../../mol-model/structure/structure/element/location';
import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
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 { StateObjectCell, StateObject, 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';
import './molstar.css';
import './rednatco-molstar.css';
const Extensions = {
'ntc-balls-pyramids-prop': PluginSpec.Behavior(DnatcoConfalPyramids),
};
const AnimationDurationMsec = 150;
const BaseRef = 'rdo';
const RCRef = 'rc';
const NtCSupPrev = 'ntc-sup-prev';
const NtCSupSel = 'ntc-sup-sel';
const NtCSupNext = 'ntc-sup-next';
const SphereBoundaryHelper = new BoundaryHelper('98');
type StepInfo = {
name: string;
assignedNtC: string;
closestNtC: string; // Fallback for cases where assignedNtC is NANT
resNo1: number;
resNo2: number;
altId1?: string;
altId2?: string;
function capitalize(s: string) {
if (s.length === 0)
return s;
return s[0].toLocaleUpperCase() + s.slice(1);
}
function dinucleotideBackbone(loci: StructureElement.Loci, altId1?: string, altId2?: string) {
const es = loci.elements[0];
const loc = Location.create(loci.structure, es.unit, es.unit.elements[OrderedSet.getAt(es.indices, 0)]);
const len = OrderedSet.size(es.indices);
const indices = new Array<ElementIndex>();
const gather = (atoms: string[], start: number, end: number, altId?: string) => {
for (const atom of atoms) {
let idx = start;
for (; idx < end; idx++) {
loc.element = es.unit.elements[OrderedSet.getAt(es.indices, idx)];
const _atom = StructureProperties.atom.label_atom_id(loc);
if (atom === _atom) {
if (altId) {
const _altId = StructureProperties.atom.label_alt_id(loc);
if (_altId !== '' && _altId !== altId)
continue;
}
indices.push(loc.element);
break;
}
}
if (idx === end) {
console.error(`Cannot find backbone atom ${atom} in first residue of a step`);
return false;
}
}
return true;
};
// Find split between first and second residue
const resNo1 = StructureProperties.residue.auth_seq_id(loc);
let secondIdx = -1;
for (let idx = 0; idx < len; idx++) {
loc.element = es.unit.elements[OrderedSet.getAt(es.indices, idx)];
const resNo = StructureProperties.residue.auth_seq_id(loc);
if (resNo !== resNo1) {
secondIdx = idx;
break;
}
}
if (secondIdx === -1)
return [];
// Gather ElementIndices for backbone atoms of the first residue
loc.element = es.unit.elements[OrderedSet.getAt(es.indices, 0)];
const ring1 = RefCfmr.CompoundRings[StructureProperties.atom.label_comp_id(loc) as keyof RefCfmr.CompoundRings];
if (!ring1)
return [];
const first = RefCfmr.BackboneAtoms.first.concat(RefCfmr.BackboneAtoms[ring1]);
if (!gather(first, 0, secondIdx, altId1))
return [];
loc.element = es.unit.elements[OrderedSet.getAt(es.indices, secondIdx)];
const ring2 = RefCfmr.CompoundRings[StructureProperties.atom.label_comp_id(loc) as keyof RefCfmr.CompoundRings];
if (!ring2)
return [];
const second = RefCfmr.BackboneAtoms.second.concat(RefCfmr.BackboneAtoms[ring2]);
if (!gather(second, secondIdx, len, altId2))
return [];
return indices;
}
function rcref(c: string, where: 'sel'|'prev'|'next'|'' = '') {
return `${RCRef}-${c}-${where}`;
}
class ColorBox extends React.Component<{ caption: string, color: Color }> {
<div className='rmsp-color-box'>
<div className='rmsp-color-box-caption'>{this.props.caption}</div>
<div
className='rmsp-color-box-color'
style={{ backgroundColor: Color.toStyle(this.props.color) }}
/>
const ConformersByClass = {
A: ['AA00_Upr', 'AA00_Lwr', 'AA02_Upr', 'AA02_Lwr', 'AA03_Upr', 'AA03_Lwr', 'AA04_Upr', 'AA04_Lwr', 'AA08_Upr', 'AA08_Lwr', 'AA09_Upr', 'AA09_Lwr', 'AA01_Upr', 'AA01_Lwr', 'AA05_Upr', 'AA05_Lwr', 'AA06_Upr', 'AA06_Lwr', 'AA10_Upr', 'AA10_Lwr', 'AA11_Upr', 'AA11_Lwr', 'AA07_Upr', 'AA07_Lwr', 'AA12_Upr', 'AA12_Lwr', 'AA13_Upr', 'AA13_Lwr', 'AB01_Upr', 'AB02_Upr', 'AB03_Upr', 'AB04_Upr', 'AB05_Upr', 'BA01_Lwr', 'BA05_Lwr', 'BA09_Lwr', 'BA08_Lwr', 'BA10_Lwr', 'BA13_Lwr', 'BA16_Lwr', 'BA17_Lwr', 'AAS1_Lwr', 'AB1S_Upr'],
B: ['AB01_Lwr', 'AB02_Lwr', 'AB03_Lwr', 'AB04_Lwr', 'AB05_Lwr', 'BA09_Upr', 'BA10_Upr', 'BB00_Upr', 'BB00_Lwr', 'BB01_Upr', 'BB01_Lwr', 'BB17_Upr', 'BB17_Lwr', 'BB02_Upr', 'BB02_Lwr', 'BB03_Upr', 'BB03_Lwr', 'BB11_Upr', 'BB11_Lwr', 'BB16_Upr', 'BB16_Lwr', 'BB04_Upr', 'BB05_Upr', 'BB1S_Upr', 'BB2S_Upr', 'BBS1_Lwr'],
BII: ['BA08_Upr', 'BA13_Upr', 'BA16_Upr', 'BA17_Upr', 'BB04_Lwr', 'BB05_Lwr', 'BB07_Upr', 'BB07_Lwr', 'BB08_Upr', 'BB08_Lwr'],
miB: ['BB10_Upr', 'BB10_Lwr', 'BB12_Upr', 'BB12_Lwr', 'BB13_Upr', 'BB13_Lwr', 'BB14_Upr', 'BB14_Lwr', 'BB15_Upr', 'BB15_Lwr', 'BB20_Upr', 'BB20_Lwr'],
IC: ['IC01_Upr', 'IC01_Lwr', 'IC02_Upr', 'IC02_Lwr', 'IC03_Upr', 'IC03_Lwr', 'IC04_Upr', 'IC04_Lwr', 'IC05_Upr', 'IC05_Lwr', 'IC06_Upr', 'IC06_Lwr', 'IC07_Upr', 'IC07_Lwr'],
OPN: ['OP01_Upr', 'OP01_Lwr', 'OP02_Upr', 'OP02_Lwr', 'OP03_Upr', 'OP03_Lwr', 'OP04_Upr', 'OP04_Lwr', 'OP05_Upr', 'OP05_Lwr', 'OP06_Upr', 'OP06_Lwr', 'OP07_Upr', 'OP07_Lwr', 'OP08_Upr', 'OP08_Lwr', 'OP09_Upr', 'OP09_Lwr', 'OP10_Upr', 'OP10_Lwr', 'OP11_Upr', 'OP11_Lwr', 'OP12_Upr', 'OP12_Lwr', 'OP13_Upr', 'OP13_Lwr', 'OP14_Upr', 'OP14_Lwr', 'OP15_Upr', 'OP15_Lwr', 'OP16_Upr', 'OP16_Lwr', 'OP17_Upr', 'OP17_Lwr', 'OP18_Upr', 'OP18_Lwr', 'OP19_Upr', 'OP19_Lwr', 'OP20_Upr', 'OP20_Lwr', 'OP21_Upr', 'OP21_Lwr', 'OP22_Upr', 'OP22_Lwr', 'OP23_Upr', 'OP23_Lwr', 'OP24_Upr', 'OP24_Lwr', 'OP25_Upr', 'OP25_Lwr', 'OP26_Upr', 'OP26_Lwr', 'OP27_Upr', 'OP27_Lwr', 'OP28_Upr', 'OP28_Lwr', 'OP29_Upr', 'OP29_Lwr', 'OP30_Upr', 'OP30_Lwr', 'OP31_Upr', 'OP31_Lwr', 'OPS1_Upr', 'OPS1_Lwr', 'OP1S_Upr', 'OP1S_Lwr'],
SYN: ['AAS1_Upr', 'AB1S_Lwr', 'AB2S_Lwr', 'BB1S_Lwr', 'BB2S_Lwr', 'BBS1_Upr', 'ZZ1S_Lwr', 'ZZ2S_Lwr', 'ZZS1_Upr', 'ZZS2_Upr'],
Z: ['ZZ01_Upr', 'ZZ01_Lwr', 'ZZ02_Upr', 'ZZ02_Lwr', 'ZZ1S_Upr', 'ZZ2S_Upr', 'ZZS1_Lwr', 'ZZS2_Lwr'],
N: ['NANT_Upr', 'NANT_Lwr'],
};
type ConformersByClass = typeof ConformersByClass;
type VisualRepresentations = 'ball-and-stick'|'cartoon';
representation: 'cartoon' as VisualRepresentations,
showNucleic: true,
showProtein: false,
showWater: false,
showPyramids: true,
pyramidsTransparent: false,
showBalls: false,
ballsTransparent: false,
classColors: { ...NtCColors.Classes },
conformerColors: { ...NtCColors.Conformers },
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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}'),
clickSelectOnly: Binding([Binding.Trigger(ButtonsType.Flag.Primary)], 'Select the 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 }),
onDeselected: PD.Value(() => {}, { isHidden: true }),
onSelected: PD.Value((loci: Representation.Loci) => {}, { isHidden: true }),
type ReDNATCOLociSelectionProps = PD.Values<typeof ReDNATCOLociSelectionParams>;
const ReDNATCOLociSelectionProvider = PluginBehavior.create({
params: () => ReDNATCOLociSelectionParams,
ctor: class extends PluginBehavior.Handler<ReDNATCOLociSelectionProps> {
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
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();
this.params.onDeselected();
},
lociIsEmpty
],
[
'clickSelectOnly',
current => {
this.ctx.managers.interactivity.lociSelects.deselectAll();
if (current.loci.kind === 'element-loci') {
this.ctx.managers.interactivity.lociSelects.select(current);
this.params.onSelected(current);
}
},
lociIsNotEmpty
],
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
];
// 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: ReDNATCOLociSelectionProps) {
super(ctx, params);
this.spine = new StateTreeSpine.Impl(ctx.state.data.cells);
}
},
});
class ReDNATCOMspViewer {
private haveMultipleModels = false;
private steps: StepInfo[] = [];
private stepNames: Map<string, number> = new Map();
constructor(public plugin: PluginUIContext, interactionContext: { self?: ReDNATCOMspViewer }) {
interactionContext.self = this;
private currentModelNumber() {
const model = this.plugin.state.data.cells.get(IDs.ID('model', '', BaseRef))?.obj;
if (!model)
return -1;
return (model as StateObject<Model>).data.modelNum;
}
private getBuilder(id: IDs.ID, sub: IDs.Substructure|'' = '', ref = BaseRef) {
return this.plugin.state.data.build().to(IDs.ID(id, sub, ref));
}
private getStructureParent(cell: StateObjectCell) {
if (!cell.sourceRef)
return undefined;
const parent = this.plugin.state.data.cells.get(cell.sourceRef);
if (!parent)
return undefined;
return parent.obj?.type.name === 'Structure' ? parent.obj : undefined;
}
private ntcRef(step: StepInfo, where: 'sel'|'prev'|'next') {
return rcref(step.assignedNtC === 'NANT' ? step.closestNtC : step.assignedNtC, where);
private pyramidsParams(colors: NtCColors.Conformers, 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'];
}
return {
type: { name: 'confal-pyramids', params: { ...typeParams, alpha: transparent ? 0.5 : 1.0 } },
colorTheme: {
name: 'confal-pyramids',
params: {
colors: {
name: 'custom',
params: colors,
},
},
},
private resetCameraRadius() {
if (!this.plugin.canvas3d)
return;
const spheres = [];
for (const [ref, cell] of Array.from(this.plugin.state.data.cells)) {
if (!IDs.isVisual(ref))
continue;
const parent = this.getStructureParent(cell);
if (parent) {
const loci = StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(parent.data, parent.data));
const s = Loci.getBoundingSphere(loci);
if (s)
spheres.push(s);
}
}
if (spheres.length === 0)
return;
SphereBoundaryHelper.reset();
for (const s of spheres)
SphereBoundaryHelper.includePositionRadius(s.center, s.radius);
SphereBoundaryHelper.finishedIncludeStep();
for (const s of spheres)
SphereBoundaryHelper.radiusPositionRadius(s.center, s.radius);
const bs = SphereBoundaryHelper.getSphere();
const snapshot = this.plugin.canvas3d.camera.getSnapshot();
snapshot.radius = bs.radius;
snapshot.target = bs.center;
PluginCommands.Camera.SetSnapshot(this.plugin, { snapshot, durationMs: AnimationDurationMsec });
}
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
private stepNext(currentIdx: number) {
if (currentIdx === this.steps.length - 1)
return void 0;
const currentStep = this.steps[currentIdx];
const cand1 = this.steps[currentIdx + 1];
if (!currentStep.altId2 || !cand1.altId1 || currentStep.altId2 === cand1.altId1)
return cand1; // Current step is "altId'd" and the candidate step has a matching altId
if (currentIdx + 2 === this.steps.length)
return cand1; // Current step is "altId'd", candidate step has a mismatching altId but there are no more steps to try
const cand2 = this.steps[currentIdx + 2];
return cand2.resNo1 === currentStep.resNo2 ? cand2 : cand1;
}
private stepPrev(currentIdx: number) {
if (currentIdx === 0)
return void 0;
const currentStep = this.steps[currentIdx];
const cand1 = this.steps[currentIdx - 1];
if (!currentStep.altId1 || !cand1.altId2 || currentStep.altId1 === cand1.altId2)
return cand1; // Current step is "altId'd" and the candidate step has a matching altId
if (currentIdx - 2 < 0)
return cand1; // Current step is "altId'd", candidate step has a mismatching altId but there are no more steps to try
const cand2 = this.steps[currentIdx - 2];
return cand2.resNo2 === currentStep.resNo1 ? cand2 : cand1;
}
private substructureVisuals(representation: 'ball-and-stick'|'cartoon') {
switch (representation) {
case 'cartoon':
return {
type: {
name: 'cartoon',
params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false },
},
colorTheme: { name: 'chain-id', params: { asymId: 'auth' } },
};
case 'ball-and-stick':
return {
type: {
name: 'ball-and-stick',
params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false },
},
colorTheme: { name: 'element-symbol', params: { carbonColor: 'chain-id' } },
};
}
}
private superpose(reference: StructureElement.Loci, stru: StructureElement.Loci, altId1?: string, altId2?: string) {
const struElems = dinucleotideBackbone(stru, altId1, altId2);
return Superpose.superposition(
{ elements: refElems, conformation: reference.elements[0].unit.conformation },
{ elements: struElems, conformation: stru.elements[0].unit.conformation }
);
}
static async create(target: HTMLElement) {
const interactCtx: { self?: ReDNATCOMspViewer } = { self: undefined };
const defaultSpec = DefaultPluginUISpec();
const spec: PluginUISpec = {
...defaultSpec,
behaviors: [
PluginSpec.Behavior(ReDNATCOLociLabelProvider),
PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci),
PluginSpec.Behavior(
ReDNATCOLociSelectionProvider,
{
bindings: ReDNATCOLociSelectionBindings,
onDeselected: () => interactCtx.self!.onDeselected(),
onSelected: (loci) => interactCtx.self!.onLociSelected(loci),
}
),
...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, interactCtx);
async changeNtCColors(display: Partial<Display>) {
if (!this.has('pyramids', 'nucleic'))
return;
const b = this.plugin.state.data.build().to(IDs.ID('pyramids', 'nucleic', BaseRef));
b.update(
StateTransforms.Representation.StructureRepresentation3D,
old => ({
...old,
colorTheme: {
name: 'confal-pyramids',
params: {
colors: {
name: 'custom',
params: display.conformerColors ?? NtCColors.Conformers,
},
},
},
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
})
);
await b.commit();
}
async changePyramids(display: Partial<Display>) {
if (display.showPyramids) {
if (!this.has('pyramids', 'nucleic')) {
const b = this.getBuilder('structure', 'nucleic');
if (b) {
b.apply(
StateTransforms.Representation.StructureRepresentation3D,
this.pyramidsParams(display.conformerColors ?? NtCColors.Conformers, new Map(), display.pyramidsTransparent ?? false),
{ ref: IDs.ID('pyramids', 'nucleic', BaseRef) }
);
await b.commit();
}
} else {
const b = this.getBuilder('pyramids', 'nucleic');
b.update(
StateTransforms.Representation.StructureRepresentation3D,
old => ({
...old,
...this.pyramidsParams(display.conformerColors ?? NtCColors.Conformers, new Map(), display.pyramidsTransparent ?? false),
})
);
await b.commit();
}
await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('pyramids', 'nucleic', BaseRef) });
}
async changeRepresentation(display: Partial<Display>) {
const b = this.plugin.state.data.build();
const repr = display.representation ?? 'cartoon';
for (const sub of ['nucleic', 'protein', 'water'] as IDs.Substructure[]) {
if (this.has('visual', sub)) {
b.to(IDs.ID('visual', sub, BaseRef))
.update(
StateTransforms.Representation.StructureRepresentation3D,
old => ({
...old,
})
);
}
}
await b.commit();
}
gatherStepInfo(): { steps: StepInfo[], stepNames: Map<string, number> }|undefined {
const obj = this.plugin.state.data.cells.get(IDs.ID('model', '', BaseRef))?.obj;
if (!obj)
return void 0;
const model = (obj as StateObject<Model>);
const sourceData = model.data.sourceData;
if (!MmcifFormat.is(sourceData))
return void 0;
const tableSum = sourceData.data.frame.categories['ndb_struct_ntc_step_summary'];
const tableStep = sourceData.data.frame.categories['ndb_struct_ntc_step'];
if (!tableSum || !tableStep) {
console.warn('NtC information not present');
return void 0;
}
const _ids = tableStep.getField('id');
const _names = tableStep.getField('name');
const _authSeqId1 = tableStep.getField('auth_seq_id_1');
const _authSeqId2 = tableStep.getField('auth_seq_id_2');
const _labelAltId1 = tableStep.getField('label_alt_id_1');
const _labelAltId2 = tableStep.getField('label_alt_id_2');
const _stepIds = tableSum.getField('step_id');
const _assignedNtCs = tableSum.getField('assigned_NtC');
const _closestNtCs = tableSum.getField('closest_NtC');
if (!_ids || !_names || !_stepIds || !_assignedNtCs || !_closestNtCs || !_labelAltId1 || !_labelAltId2 || !_authSeqId1 || !_authSeqId2) {
console.warn('Expected fields are not present in NtC categories');
return void 0;
}
const ids = _ids.toIntArray();
const names = _names.toStringArray();
const authSeqId1 = _authSeqId1.toIntArray();
const authSeqId2 = _authSeqId2.toIntArray();
const labelAltId1 = _labelAltId1.toStringArray();
const labelAltId2 = _labelAltId2.toStringArray();
const stepIds = _stepIds.toIntArray();
const assignedNtCs = _assignedNtCs.toStringArray();
const closestNtCs = _closestNtCs.toStringArray();
const len = ids.length;
const stepNames = new Map<string, number>();
const steps = new Array<StepInfo>(len);
for (let idx = 0; idx < len; idx++) {
const id = ids[idx];
const name = names[idx];
for (let jdx = 0; jdx < len; jdx++) {
if (stepIds[jdx] === id) {
const assignedNtC = assignedNtCs[jdx];
const closestNtC = closestNtCs[jdx];
const resNo1 = authSeqId1[jdx];
const resNo2 = authSeqId2[jdx];
const altId1 = labelAltId1[jdx] === '' ? void 0 : labelAltId1[jdx];
const altId2 = labelAltId2[jdx] === '' ? void 0 : labelAltId2[jdx];
// We're assuming that steps are ID'd with a contigious, monotonic sequence starting from 1
steps[id - 1] = { name, assignedNtC, closestNtC, resNo1, resNo2, altId1, altId2 };
stepNames.set(name, id - 1);
break;
}
}
}
return { steps, stepNames };
}
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
getModelCount() {
const obj = this.plugin.state.data.cells.get(IDs.ID('trajectory', '', BaseRef))?.obj;
if (!obj)
return 0;
return (obj as StateObject<Trajectory>).data.frameCount;
}
getPresentConformers() {
const obj = this.plugin.state.data.cells.get(IDs.ID('model', '', BaseRef))?.obj;
if (!obj)
return [];
const model = (obj as StateObject<Model>);
const modelNum = model.data.modelNum;
const sourceData = model.data.sourceData;
if (MmcifFormat.is(sourceData)) {
const tableSum = sourceData.data.frame.categories['ndb_struct_ntc_step_summary'];
const tableStep = sourceData.data.frame.categories['ndb_struct_ntc_step'];
if (!tableSum || !tableStep) {
console.warn('NtC information not present');
return [];
}
const _stepIds = tableSum.getField('step_id');
const _assignedNtCs = tableSum.getField('assigned_NtC');
const _ids = tableStep.getField('id');
const _modelNos = tableStep.getField('PDB_model_number');
if (!_stepIds || !_assignedNtCs || !_ids || !_modelNos) {
console.warn('Expected fields are not present in NtC categories');
return [];
}
const stepIds = _stepIds.toIntArray();
const assignedNtCs = _assignedNtCs.toStringArray();
const ids = _ids.toIntArray();
const modelNos = _modelNos.toIntArray();
const present = new Array<string>();
for (let row = 0; row < stepIds.length; row++) {
const idx = ids.indexOf(stepIds[row]);
if (modelNos[idx] === modelNum) {
const ntc = assignedNtCs[row];
if (!present.includes(ntc))
present.push(ntc);
}
}
present.sort();
return present;
}
return [];
}
has(id: IDs.ID, sub: IDs.Substructure|'' = '', ref = BaseRef) {
return !!this.plugin.state.data.cells.get(IDs.ID(id, sub, ref))?.obj;
}
isReady() {
return this.has('structure', '', BaseRef);
}
async loadStructure(data: string, type: 'pdb'|'cif', display: Partial<Display>) {
const b = (t => type === 'pdb'
? t.apply(StateTransforms.Model.TrajectoryFromPDB, {}, { ref: IDs.ID('trajectory', '', BaseRef) })
: t.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif, {}, { ref: IDs.ID('trajectory', '', BaseRef) })
)(this.plugin.state.data.build().toRoot().apply(RawData, { data }, { ref: IDs.ID('data', '', BaseRef) }))
.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: display.modelNumber ? display.modelNumber - 1 : 0 }, { ref: IDs.ID('model', '', BaseRef) })
.apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', BaseRef) })
// Extract substructures
.apply(StateTransforms.Model.StructureComplexElement, { type: 'nucleic' }, { ref: IDs.ID('structure', 'nucleic', BaseRef) })
.to(IDs.ID('structure', '', BaseRef))
.apply(StateTransforms.Model.StructureComplexElement, { type: 'protein' }, { ref: IDs.ID('structure', 'protein', BaseRef) })
.to(IDs.ID('structure', '', BaseRef))
.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: IDs.ID('structure', 'water', BaseRef) });
// Commit now so that we can check whether individual substructures are available
await b.commit();
// Create default visuals
const bb = this.plugin.state.data.build();
if (display.showNucleic && this.has('structure', 'nucleic')) {
bb.to(IDs.ID('structure', 'nucleic', BaseRef))
.apply(
StateTransforms.Representation.StructureRepresentation3D,
{ ref: IDs.ID('visual', 'nucleic', BaseRef) }
);
if (display.showPyramids) {
bb.to(IDs.ID('structure', 'nucleic', BaseRef))
.apply(
StateTransforms.Representation.StructureRepresentation3D,
this.pyramidsParams(display.conformerColors ?? NtCColors.Conformers, new Map(), false),
{ ref: IDs.ID('pyramids', 'nucleic', BaseRef) }
);
}
}
if (display.showProtein && this.has('structure', 'protein')) {
bb.to(IDs.ID('structure', 'protein', BaseRef))
.apply(
StateTransforms.Representation.StructureRepresentation3D,
{ ref: IDs.ID('visual', 'protein', BaseRef) }
);
}
if (display.showWater && this.has('structure', 'water')) {
bb.to(IDs.ID('structure', 'water', BaseRef))
.apply(
StateTransforms.Representation.StructureRepresentation3D,
{ ref: IDs.ID('visual', 'water', BaseRef) }
);
}
await bb.commit();
this.haveMultipleModels = this.getModelCount() > 1;
if (!ntcInfo) {
this.steps.length = 0;
this.stepNames.clear();
} else {
this.steps = ntcInfo.steps;
this.stepNames = ntcInfo.stepNames;
}
}
async loadReferenceConformers() {
const b = this.plugin.state.data.build().toRoot();
for (const c in ReferenceConformersPdbs) {
const cfmr = ReferenceConformersPdbs[c as keyof typeof ReferenceConformersPdbs];
const bRef = rcref(c);
const mRef = IDs.ID('model', '', bRef);
b.toRoot();
b.apply(RawData, { data: cfmr, label: `Reference ${c}` }, { ref: IDs.ID('data', '', bRef) })
.apply(StateTransforms.Model.TrajectoryFromPDB, {}, { ref: IDs.ID('trajectory', '', bRef) })
.apply(StateTransforms.Model.ModelFromTrajectory, {}, { ref: mRef })
.apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', rcref(c, 'sel')) })
.to(mRef)
.apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', rcref(c, 'prev')) })
.to(mRef)
.apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', '', rcref(c, 'next')) });
}
await b.commit();
}
onDeselected() {
this.plugin.state.data.build()
.delete(IDs.ID('superposition', '', NtCSupSel))
.delete(IDs.ID('superposition', '', NtCSupPrev))
.delete(IDs.ID('superposition', '', NtCSupNext))
.commit();
}
onLociSelected(selected: Representation.Loci) {
const loci = Loci.normalize(selected.loci, 'two-residues');
if (loci.kind === 'element-loci') {
// TODO: This cannot call superposeReferences directly
// Instead, we must make a callback via the API
// and have the listener decide what to do with this event
const stepDesc = Step.describe(loci);
if (!stepDesc)
return;
const stepName = Step.name(stepDesc, this.haveMultipleModels);
this.superposeReferences(stepName, '', []);
}
if (display.modelNumber && display.modelNumber === this.currentModelNumber())
return;
const b = this.plugin.state.data.build()
.delete(IDs.ID('superposition', '', NtCSupSel))
.delete(IDs.ID('superposition', '', NtCSupPrev))
.delete(IDs.ID('superposition', '', NtCSupNext))
.to(IDs.ID('model', '', BaseRef))
.update(
StateTransforms.Model.ModelFromTrajectory,
old => ({
...old,
modelIndex: display.modelNumber ? display.modelNumber - 1 : 0
})
);
async superposeReferences(stepName: string, referenceNtc: string, references: ('sel'|'prev'|'next')[]) {
const ReferenceVisuals = (color: number) => {
return {
type: { name: 'ball-and-stick', params: { sizeFactor: 0.15, aromaticBonds: false } },
colorTheme: { name: 'uniform', params: { value: Color(color) } },
};
};
const stepDesc = Step.fromName(stepName);
if (!stepDesc)
return;
const stepIdx = this.stepNames.get(stepName);
if (stepIdx === undefined) {
console.error(`Unknown step name ${stepName}`);
return;
}
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
if (stepDesc.model !== this.currentModelNumber()) {
const b = this.getBuilder('model')
.update(
StateTransforms.Model.ModelFromTrajectory,
old => ({
...old,
modelIndex: stepDesc.model - 1,
})
);
await b.commit();
}
const entireStru = this.plugin.state.data.cells.get(IDs.ID('structure', 'nucleic', BaseRef))!.obj!;
const loci = Traverse.findResidue(
stepDesc.chain,
stepDesc.resNo1,
stepDesc.altId1,
StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(entireStru.data, entireStru.data)),
'auth'
);
if (loci.kind !== 'element-loci')
return;
const selLoci = Loci.normalize(loci, 'two-residues');
if (selLoci.kind !== 'element-loci')
return;
const step = this.steps[stepIdx];
const stepPrev = this.stepPrev(stepIdx);
const stepNext = this.stepNext(stepIdx);
const ntcRefSel = step ? this.ntcRef(step, 'sel') : void 0;
const ntcRefPrev = stepPrev ? this.ntcRef(stepPrev, 'prev') : void 0;
const ntcRefNext = stepNext ? this.ntcRef(stepNext, 'next') : void 0;
console.error(`stepIdx ${stepIdx} does not map to a known step`);
return;
}
const b = this.plugin.state.data.build()
.delete(IDs.ID('superposition', '', NtCSupSel))
.delete(IDs.ID('superposition', '', NtCSupPrev))
.delete(IDs.ID('superposition', '', NtCSupNext));
const addReference = (ntcRef: string, superposRef: string, loci: Loci, altId1: string|undefined, altId2: string|undefined, color: number) => {
const refStru = this.plugin.state.data.cells.get(IDs.ID('structure', '', ntcRef))!.obj!;
const refLoci = StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(refStru.data, refStru.data));
if (loci.kind === 'element-loci' && Step.is(loci)) {
const { bTransform, rmsd } = this.superpose(refLoci, loci, altId1, altId2);
if (isNaN(bTransform[0])) {
console.error(`Cannot superpose reference conformer ${ntcRef} onto selection`);
return;
}
b.to(IDs.ID('structure', '', ntcRef))
.apply(
StateTransforms.Model.TransformStructureConformation,
{ transform: { name: 'matrix', params: { data: bTransform, transpose: false } } },
{ ref: IDs.ID('superposition', '', superposRef) }
).apply(
StateTransforms.Representation.StructureRepresentation3D,
ReferenceVisuals(color),
{ ref: IDs.ID('visual', '', superposRef) }
);
return rmsd;
const rmsd = addReference(ntcRefSel, NtCSupSel, selLoci, stepDesc.altId1, stepDesc.altId2, 0x008000);
if (ntcRefPrev) {
const { altId1, altId2 } = stepPrev!;
addReference(ntcRefPrev, NtCSupPrev, Loci.normalize(Traverse.residue(-1, altId1, selLoci), 'two-residues'), altId1, altId2, 0x0000FF);
}
if (ntcRefNext) {
const { altId1, altId2 } = stepNext!;
addReference(ntcRefNext, NtCSupNext, Loci.normalize(Traverse.residue(1, altId1, selLoci), 'two-residues'), altId1, altId2, 0x00FFFF);
}
async toggleSubstructure(sub: IDs.Substructure, display: Partial<Display>) {
const show = sub === 'nucleic' ? !!display.showNucleic :
sub === 'protein' ? !!display.showProtein : !!display.showWater;
const repr = display.representation ?? 'cartoon';
if (show) {
const b = this.getBuilder('structure', sub);
const visuals = this.substructureVisuals(sub === 'water' ? 'ball-and-stick' : repr);
if (b) {
b.apply(
StateTransforms.Representation.StructureRepresentation3D,
{ ref: IDs.ID('visual', sub, BaseRef) }
);
await b.commit();
}
await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('visual', sub, BaseRef) });
}
class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> {
private viewer: ReDNATCOMspViewer|null = null;
private classColorToConformers(k: keyof ConformersByClass, color: Color) {
const updated: Partial<NtCColors.Conformers> = {};
ConformersByClass[k].map(cfmr => updated[cfmr as keyof NtCColors.Conformers] = color);
return updated;