UNPKG

pfam-molstar

Version:

A component for embedding molstar 3D viewer in Pfam

391 lines (361 loc) 15.3 kB
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;