pfam-molstar
Version:
A component for embedding molstar 3D viewer in Pfam
391 lines (361 loc) • 15.3 kB
JavaScript
import { DefaultPluginSpec } from 'molstar/lib/mol-plugin/spec';
import { PluginConfig } from 'molstar/lib/mol-plugin/config';
import { PluginContext } from 'molstar/lib/mol-plugin/context';
import { PluginCommands } from 'molstar/lib/mol-plugin/commands';
import { StructureSelection } from 'molstar/lib/mol-model/structure';
import { ChainIdColorThemeProvider } from 'molstar/lib/mol-theme/color/chain-id';
import {
UniformColorThemeProvider,
UniformColorThemeParams,
} from 'molstar/lib/mol-theme/color/uniform';
import { ParamDefinition } from 'molstar/lib/mol-util/param-definition';
import { Script } from 'molstar/lib/mol-script/script';
import { Color } from 'molstar/lib/mol-util/color';
import { ColorNames } from 'molstar/lib/mol-util/color/names';
import { AfConfidenceProvider } from './af-confidence/prop.ts';
import { AfConfidenceColorThemeProvider } from './af-confidence/color.ts';
import { ColorByResidueLddtTheme } from './ColourByResidueLddtTheme';
import css from './styles.css';
class Molstar extends HTMLElement {
static URL = 'url';
static LOCATIONS = 'locations';
static TYPE = 'type';
static get observedAttributes() {
return [this.URL, this.LOCATIONS, this.TYPE];
}
constructor(...args) {
super(...args);
// properties
this.locations = [];
this.isSpinning = false;
this.accession = null;
this.type = null;
}
connectedCallback() {
console.log("connected");
// attach styles
const styles = document.createElement('style');
styles.textContent = css.toString();
this.append(styles);
// add molstar elements
const container = document.createElement('div');
this.classid = "mol-container";
const viewerDiv = document.createElement('div');
viewerDiv.id = "mol-canvas_container";
const viewerCanvas = document.createElement('canvas');
viewerCanvas.id = "mol-canvas";
const viewerLabel = document.createElement('div');
viewerLabel.id = "mol-label";
const viewerControls = document.createElement('div');
viewerControls.id = "mol-controls";
viewerLabel.textContent = "Structure details";
viewerDiv.append(viewerCanvas);
container.append(viewerDiv);
container.append(viewerLabel);
container.append(viewerControls);
this.append(container);
this.label = viewerLabel;
this.controls = viewerControls;
this.container = container;
this.canvas = viewerCanvas;
this.div = viewerDiv;
const MySpec = {
...DefaultPluginSpec(),
layout: {
isExpanded: true,
showControls: true,
},
config: [[PluginConfig.VolumeStreaming.Enabled, false]],
};
this.viewer = new PluginContext(MySpec);
this.viewer.init();
this.viewer.initViewer(
this.canvas,
this.div,
);
this.loadStructure();
this.addControls();
this.addHoverListener();
}
disconnectedCallback() {
console.log("disconnected");
}
addHoverListener() {
this.viewer.behaviors.labels.highlight.subscribe(
(args) => {
let text = "";
if (args && args.labels.length > 0) {
text = args.labels[0];
}
this.label.innerHTML = text;
}
);
// TODO generate more meaningful highlight text
// this.viewer.behaviors.interaction.hover.subscribe(
// (arg) => {
// }
// );
}
async loadStructure() {
console.log('loadStructure');
if (this.viewer) {
console.log(`Loading ${this.url} Source ${this.type}`);
await this.viewer.clear();
let url;
let format;
let theme;
if (this.type === 'structure') {
url = this.url;
theme = ChainIdColorThemeProvider.name;
format = 'mmcif';
} else if (this.type === 'tr-model') {
this.viewer.customModelProperties.unregister(AfConfidenceColorThemeProvider.name);
this.viewer.representation.structure.themes.colorThemeRegistry.add(
ColorByResidueLddtTheme.colorThemeProvider,
);
this.viewer.managers.lociLabels.addProvider(
ColorByResidueLddtTheme.labelProvider,
);
this.viewer.customModelProperties.register(
ColorByResidueLddtTheme.propertyProvider,
true,
);
url = this.url;
theme = ColorByResidueLddtTheme.propertyProvider.descriptor.name;
format = 'pdb';
} else if (this.type === 'af-model') {
this.viewer.customModelProperties.unregister(ColorByResidueLddtTheme.propertyProvider);
this.viewer.customModelProperties.register(AfConfidenceProvider, true);
// this.viewer.managers.lociLabels.addProvider(this.labelAfConfScore);
this.viewer.representation.structure.themes.colorThemeRegistry.add(
AfConfidenceColorThemeProvider,
);
const response = await (await fetch(this.url)).json();
if (response.length < 1) {
return;
}
url = response[0].cifUrl;
theme = AfConfidenceColorThemeProvider.name;
format = 'mmcif';
} else {
return;
}
const data = await this.viewer.builders.data.download(
{ url: url },
{ state: { isGhost: false } },
);
const trajectory = await this.viewer.builders.structure.parseTrajectory(
data,
format,
);
await this.viewer.builders.structure.hierarchy
.applyPreset(trajectory, 'default', {
structure: {
name: 'model',
params: { }
},
showUnitcell: false,
representationPreset: 'polymer-cartoon'
}).then(() => {
this.applyTheme(theme);
this.setThemeSelectionChoices(this.type);
});
}
}
addControls() {
this.spinIcon = document.createElement('Span');
this.spinIcon.classList.add("material-icons");
this.spinIcon.textContent = "play_arrow";
this.spinIcon.addEventListener('click', () => this.spinStructure(true));
this.stopIcon = document.createElement('Span');
this.stopIcon.classList.add("material-icons");
this.stopIcon.textContent = "stop";
this.stopIcon.addEventListener('click', () => this.spinStructure(false));
this.stopIcon.style.display = 'none';
this.themeLabel = document.createElement("label");
this.themeLabel.setAttribute('for', 'themeSelector');
this.themeLabel.classList.add("material-icons");
this.themeLabel.classList.add("theme-selection");
this.themeLabel.textContent = "palette";
this.themeSelector = document.createElement("select");
this.themeSelector.id = "themeSelector";
this.themeSelector.classList.add("theme-selection");
this.themeSelector.setAttribute("name", "themeSelector");
this.themeSelector.addEventListener('change', (e) => {
console.log(`selection change ${e.target.value}`);
if (e.target.value === "highlight") {
this.highlightLocations();
} else {
this.viewer.managers.interactivity.lociSelects.deselectAll();
this.applyTheme(e.target.value);
}
});
this.fullscreenIcon = document.createElement("Span");
this.fullscreenIcon.classList.add("material-icons");
this.fullscreenIcon.textContent = "fullscreen";
this.fullscreenIcon.style.display = 'inline-block';
this.fullscreenIcon.addEventListener('click', () => this.requestFullscreen());
this.stdscreenIcon = document.createElement("Span");
this.stdscreenIcon.classList.add("material-icons");
this.stdscreenIcon.textContent = "fullscreen_exit";
this.stdscreenIcon.style.display = 'none';
this.stdscreenIcon.addEventListener('click', () => document.exitFullscreen());
this.addEventListener('fullscreenchange', () => this.toggleFullscreen());
this.controls.append(this.spinIcon);
this.controls.append(this.stopIcon);
this.controls.append(this.themeLabel);
this.controls.append(this.themeSelector);
this.controls.append(this.fullscreenIcon);
this.controls.append(this.stdscreenIcon);
}
setThemeSelectionChoices() {
// hide selection unless multiple options added
this.themeLabel.style.display = 'none';
this.themeSelector.style.display = 'none';
// remove all existing options
const numOpt = this.themeSelector.options.length-1;
for (let i = numOpt; i >= 0; i--) {
this.themeSelector.remove(i);
}
if (this.type === 'structure') {
const item = document.createElement('option');
item.textContent = "Chain";
item.value = ChainIdColorThemeProvider.name;
this.themeSelector.add(item);
if (this.locations.length > 0) {
const locationItem = document.createElement('option');
locationItem.textContent = "Highlight";
locationItem.value = "highlight";
this.themeSelector.add(locationItem);
this.themeLabel.style.display = 'inline-block';
this.themeSelector.style.display = 'inline-block';
}
} else if (this.type === 'tr-model') {
const item = document.createElement('option');
item.textContent = "LDDT Score";
item.value = ColorByResidueLddtTheme.propertyProvider.descriptor.name;
this.themeSelector.add(item);
} else if (this.type === 'af-model') {
const item = document.createElement('option');
item.textContent = "LDDT Score";
item.value = AfConfidenceColorThemeProvider.name;
this.themeSelector.add(item);
} else {
return;
}
}
spinStructure(shouldSpin) {
if (shouldSpin) {
this.spinIcon.style.display = 'none';
this.stopIcon.style.display = 'inline-block';
} else {
this.spinIcon.style.display = 'inline-block';
this.stopIcon.style.display = 'none';
}
if (this.viewer && this.viewer.canvas3d) {
const trackball = this.viewer.canvas3d.props.trackball;
PluginCommands.Canvas3D.SetSettings(this.viewer, {
settings: { trackball: { ...trackball, spin: shouldSpin } },
});
}
}
applyTheme(theme) {
if (this.viewer) {
// TODO this.viewer.managers.interactivity.lociSelects.deselectAll();
this.viewer.dataTransaction(async () => {
for (const s of this.viewer.managers.structure.hierarchy.current.structures) {
await this.viewer.managers.structure.component.updateRepresentationsTheme(
s.components,
{ color: theme },
);
}
});
}
}
toggleFullscreen() {
if (document.fullscreenElement) {
this.canvas.style.height = "90vh";
this.fullscreenIcon.style.display = 'none';
this.stdscreenIcon.style.display = 'inline-block';
} else {
this.canvas.style.height = "50vh";
this.fullscreenIcon.style.display = 'inline-block';
this.stdscreenIcon.style.display = 'none';
}
this.viewer.handleResize();
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`attribute changed ${name} ${oldValue} ${newValue}`);
if (name === 'url' && oldValue !== newValue) {
this.url = this.getAttribute('url');
this.type = this.getAttribute('type');
this.loadStructure();
} else if (name === 'locations' && oldValue !== newValue) {
if (newValue) {
this.locations = JSON.parse(newValue);
} else {
this.locations = []
}
this.highlightLocations();
}
}
highlightLocations() {
if (!this.viewer) return;
const data = this.viewer.managers.structure.hierarchy.current.structures[0]?.cell.obj?.data;
if (!data) return;
// remove any existing hightlights
this.viewer.managers.interactivity.lociSelects.deselectAll();
this.viewer.dataTransaction(async () => {
UniformColorThemeParams.value = ParamDefinition.Color(ColorNames.white);
for (const s of this.viewer.managers.structure.hierarchy.current.structures) {
await this.viewer.managers.structure.component.updateRepresentationsTheme(
s.components,
{
color: UniformColorThemeProvider.name,
}
);
}
})
.then(() => {
const molSelection = Script.getStructureSelection((MS) => {
const atomGroups = [];
let ShouldColourChange = true;
for (const location of this.locations) {
console.log(location);
if (ShouldColourChange) {
// const hexColour = parseInt(location[0].colour.substring(1), 16);
if (this.highlightColour !== location.colour) {
this.highlightColour = location.colour;
PluginCommands.Canvas3D.SetSettings(this.viewer, {
settings: (props) => {
props.renderer.selectColor = Color(location.colour);
},
});
}
ShouldColourChange = false;
}
const positions = [];
for (let i = location.start; i <= location.end; i++) {
positions.push(i);
}
atomGroups.push(
MS.struct.generator.atomGroups({
'chain-test': MS.core.rel.eq([
location.chain,
MS.ammp('label_asym_id'),
]),
'residue-test': MS.core.set.has([
MS.set(...positions),
MS.ammp('auth_seq_id'),
]),
}),
);
}
return MS.struct.combinator.merge(atomGroups);
}, data);
const loci = StructureSelection.toLociWithSourceUnits(molSelection);
this.viewer.managers.interactivity.lociSelects.select({ loci });
});
}
}
export default Molstar;