/* globals NGL, $ */ if (typeof window.structureData === 'undefined') { // eslint-disable-next-line no-undef window.structureData = {}; } if (typeof antibodyColorMap === 'undefined') { // eslint-disable-next-line no-undef antibodyColorMap = {}; } function AAshortToLong(abbr, long) { let translate = { A: {s: 'Ala', l: 'Alanine'}, R: {s: 'Arg', l: 'Arginine'}, N: {s: 'Asn', l: 'Asparagine'}, D: {s: 'Asp', l: 'Aspartate'}, C: {s: 'Cys', l: 'Cysteine'}, Q: {s: 'Gln', l: 'Glutamine'}, E: {s: 'Glu', l: 'Glutamate'}, G: {s: 'Gly', l: 'Glycine'}, H: {s: 'His', l: 'Histidine'}, I: {s: 'Ile', l: 'Isoleucine'}, L: {s: 'Leu', l: 'Leucine'}, K: {s: 'Lys', l: 'Lysine'}, M: {s: 'Met', l: 'Methionine'}, F: {s: 'Phe', l: 'Phenylalanine'}, P: {s: 'Pro', l: 'Proline'}, S: {s: 'Ser', l: 'Serine'}, T: {s: 'Thr', l: 'Threonine'}, W: {s: 'Trp', l: 'Tryptophan'}, Y: {s: 'Tyr', l: 'Tyrosine'}, V: {s: 'Val', l: 'Valine'}, }; return translate[abbr][long?'l':'s']; } function translateShortToLong(str, long) { let s = str.split(''); let n = s.map(function(c) { return AAshortToLong(c, long); }); return n.join(''); } // eslint-disable-next-line no-unused-vars function createNGLViewer(top, options) { options = options || {}; let stage; let loaded = {}; let shapeComp = {}; let missenseData = {}; let colorSchemes = {}; let combinedSchemeKey = ''; let volumeRepr = {}; let highlightParts = {}; let currentStructure; // eslint-disable-next-line no-underscore-dangle let _events = {}; // eslint-disable-next-line no-underscore-dangle let _structureData = {}; if (options.structureData) { addStructureData(options.structureData); } const defaultColorScheme = 'bfactor'; const structureVisType = 'cartoon'; let colorNames = [ 0xFF0000, // Red 0x00FF00, // Green 0x0000FF, // Blue 0xFFA500, // Orange 0x762A83, // Purple-ish 0x8e7f00, // Yellow-ish ]; let transcriptColor = 0xdddddd; let defaultColor = 0xFF67EE; let keyClinical = 'c'; let keyPopulation = 'p'; let keyAMClinical = 'ac'; let keyAMPopulation = 'ap'; let alphaFoldColors = { 90: ['Very high (pLDDT > 90)', 0x0053d6, '#0053d6'], 70: ['Confident (90 > pLDDT > 70)', 0x65cbf3, '#65cbf3'], 50: ['Low (70 > pLDDT > 50)', 0xffdb13, '#ffdb13'], 0: ['Very low (pLDDT < 50)', 0xff7d45, '#ff7d45'] }; let alphaFoldConfidenceSchemeId; // eslint-disable-next-line no-underscore-dangle function _isFunction(obj) { return typeof obj === "function"; } /* Event functions taken from OpenSeadragon */ function addHandler(eventName, handler, userData) { if (!_events[eventName]) { _events[eventName] = []; } let events = _events[eventName]; if (handler && _isFunction(handler)) { events.push({handler: handler, userData: userData || null}); } return true; } function addOnceHandler(eventName, handler, userData, times) { times = times || 1; var count = 0; var onceHandler = function(event) { count++; if (count === times) { removeHandler(eventName, onceHandler); } return handler(event); }; return addHandler(eventName, onceHandler, userData); } function removeHandler(eventName, handler) { var events = _events[eventName]; var handlers = []; if (!events) { return; } if (Array.isArray(events)) { for (let i = 0; i < events.length; i++) { if (events[i].handler !== handler) { handlers.push(events[i]); } } _events[eventName] = handlers; } } // eslint-disable-next-line no-underscore-dangle function _getHandler(eventName) { var events = _events[eventName]; if (!events || !events.length) { return null; } events = events.length === 1 ? [events[0]] : Array(...events); return function (args) { let length = events.length; for (let i = 0; i < length; i++) { if (events[i]) { args.userData = events[i].userData; events[i].handler(args); } } }; } function raiseEvent(eventName, eventArgs) { var handler = _getHandler(eventName); if (handler) { handler(eventArgs || {}); } } function addStructureData(newStructureData) { Object.entries(newStructureData).forEach(([idx, val]) => _structureData[idx] = val); } function getStructureData(structure) { return _structureData[structure]; } function getStructureDataEntries() { return _structureData; } function getSelectedColor() { let scheme = $('.color.active').attr('class').replace('color', '').replace('active', '').trim(); if (scheme) { return scheme; } return undefined; } function hideComponents() { stage.eachComponent(function (comp) { // if (comp.name !== structure) { comp.setVisibility(false); // } }); } async function loadmodel(modelToLoad) { hideComponents(); if (!modelToLoad) { return; } let structure = modelToLoad; let thisStructureData = getStructureData(structure); stage.removeAllComponents(); combinedSchemeKey = ''; highlightParts = {}; if (loaded[structure]) { setLoadedStructure(modelToLoad); await handleMissense(); handleColors(); stage.getComponentsByName(structure).forEach(function (e) { e.setVisibility(true); e.setDefaultAssembly(''); e.autoView(); }); raiseEvent('model-loaded', {structure: structure}); } else { let pr = stage.loadFile(thisStructureData.file, {defaultRepresentation: false}); let s = await pr; // pr.then(function (s) { setLoadedStructure(modelToLoad); // loaded[structure] = 1; s.setVisibility(false); s.setName(structure); setRepresentation(s, getSelectedColor()); try { await handleMissense(); } catch (e) { // eslint-disable-next-line no-console console.log(e); } try { handleColors(); } catch (e) { // eslint-disable-next-line no-console console.log(e); } stage.getComponentsByName(structure).forEach(function (e) { e.setVisibility(true); e.setDefaultAssembly(''); e.autoView(); }); raiseEvent('model-loaded', {structure: structure}); // }); // await pr; } } function getPositionForResidue(structure, residueID, chainName) { let pos; stage.getComponentsByName(structure).forEach(function (e) { e.structure.getResidueProxy(residueID - 1).eachAtom(function (atom) { if (atom.chainname !== chainName) { return; } if (atom.atomname === 'CA') { pos = atom.positionToVector3(); } }); }); return pos; } async function getMissenseDataForStructure(structure) { if (!missenseData.hasOwnProperty(structure)) { missenseData[structure] = fetchMissenseData(structure); } return missenseData[structure]; } function fetchMissenseData(structure) { return new Promise((resolve) => { $.getJSON('/structure/structure_data.php?structure_id='+structure, (json) => { resolve(json); }); }); } function getAntigensForStructure(structure) { let ds = getStructureData(structure); if (ds) { return ds.antigens; } return []; } function loadStructure(structure_id, showAntigens, assembly, showClinical, showPopulation, showAMClinical, showAMPopulation) { loadmodelsimple(structure_id, '/proteinstructures/'+structure_id+'.mmtf', showAntigens, assembly, showClinical, showPopulation, showAMClinical, showAMPopulation); } async function loadmodelsimple(structure, file, showAntigens, assembly, showClinical, showPopulation, showAMClinical, showAMPopulation) { assembly = assembly || ""; hideComponents(); if (loaded[structure]) { setLoadedStructure(structure); if (showAntigens) { handleColors(showAntigens); } if (showClinical || showPopulation || showAMClinical || showAMPopulation) { await handleMissense(showClinical, showPopulation, showAMClinical, showAMPopulation); } stage.getComponentsByName(structure).forEach(function (e) { e.setVisibility(true); e.setDefaultAssembly(assembly); e.autoView(); }); raiseEvent('model-loaded', {structure: structure}); } else { let pr = stage.loadFile(file, {defaultRepresentation: false}); pr.then(function (s) { loaded[structure] = 1; setLoadedStructure(structure); s.setVisibility(false); s.setName(structure); setRepresentation(s, defaultColorScheme); if (showAntigens) { handleColors(showAntigens); } if (showClinical || showPopulation || showAMClinical || showAMPopulation) { handleMissense(showClinical, showPopulation, showAMClinical, showAMPopulation); } stage.getComponentsByName(structure).forEach(function (e) { e.setVisibility(true); e.setDefaultAssembly(assembly); e.autoView(); }); raiseEvent('model-loaded', {structure: structure}); }); } } async function handleMissense(showClinical, showPopulation, showAMClinical, showAMPopulation) { if (typeof showClinical === 'undefined') { showClinical = $('.variants.active').hasClass('clinical'); } if (typeof showPopulation === 'undefined') { showPopulation = $('.variants.active').hasClass('population'); } if (typeof showAMClinical === 'undefined') { showAMClinical = $('.alphavariants.active').hasClass('pathogenic'); } if (typeof showAMPopulation === 'undefined') { showAMPopulation = $('.alphavariants.active').hasClass('benign'); } let structure = getLoadedStructure(); let name = structure + (showClinical ? keyClinical : "_") + (showPopulation ? keyPopulation : "_") + (showAMClinical ? keyAMClinical : "_") + (showAMPopulation ? keyAMPopulation : "_"); if (showClinical || showPopulation || showAMClinical || showAMPopulation) { if (!shapeComp.hasOwnProperty(name)) { let shape = new NGL.Shape('shape'); let missense = await getMissenseDataForStructure(structure); Object.keys(missense).forEach(function (chain) { let residues = missense[chain]; Object.keys(residues).forEach(function(residue) { let variants = residues[residue]; let pos = getPositionForResidue(structure, residue, chain); if (typeof pos === "undefined") { return; } let title_parts = { c: [], p: [], ac: [], ap: [] }; let createShape = false; let oldaa = ''; Object.keys(variants).forEach(function(k) { let d = variants[k]; let t = '=>' + translateShortToLong(d.n, true) + (d.s_t ? '\nSIFT: '+d.s_t + ' ' + d.s_v : '') + (d.p_t ? '\nPolyPhen: '+d.p_t + ' ' + d.p_v : '') + (d.a_t ? ': '+d.a_t + ' ' + d.a_v : ''); if (d.v_t === keyClinical && d.pa.length > 0) { t += '\nPathology: '+d.pa.join(', '); } title_parts[d.v_t].push(t); if ( (d.v_t === keyClinical && showClinical) || (d.v_t === keyPopulation && showPopulation) || (d.v_t === keyAMClinical && showAMClinical) || (d.v_t === keyAMPopulation && showAMPopulation)) { createShape = true; if (oldaa === '') { oldaa = translateShortToLong(d.o, true); } } }); if (createShape) { let title = oldaa+':\n'; let haveText = false; if (title_parts.c.length) { title += 'Clinical variations:\n' + title_parts.c.join('\n'); haveText = true; } if (title_parts.p.length) { title += (haveText ? '\n\n' : '')+'Population variations:\n' + title_parts.p.join('\n'); haveText = true; } if (title_parts.ap.length) { title += (haveText ? '\n\n' : '')+'AlphaMissense benign variations:\n' + title_parts.ap.join('\n'); haveText = true; } if (title_parts.ac.length) { title += (haveText ? '\n\n' : '')+'AlphaMissense pathogenic variations:\n' + title_parts.ac.join('\n'); } let withClinical = showClinical && title_parts.c.length>0; let withPopulation = showPopulation && title_parts.p.length>0; let withAMClinical = showAMClinical && title_parts.ac.length>0; let withAMPopulation = showAMPopulation && title_parts.ap.length>0; let color; if ( (withClinical || withPopulation) && (withAMClinical || withAMPopulation) ) { // Color pink when both type of variants in the same position color = [1, 0, 1]; } else if (withClinical || withPopulation) { color = [0, 0, 1]; } else { color = [1, 0, 0]; } shape.addSphere(pos, color, 1, title.trim()); } }); }); shapeComp[name] = stage.addComponentFromObject(shape); shapeComp[name].addRepresentation('buffer'); } } Object.keys(shapeComp).forEach(function(v) { shapeComp[v].setVisibility(name===v); }); let legend = []; if (showClinical) { legend.push(keyClinical); } if (showPopulation) { legend.push(keyPopulation); } if (showAMClinical) { legend.push(keyAMClinical); } if (showAMPopulation) { legend.push(keyAMPopulation); } let legendHTML = updateVariantLegend(structure, legend); raiseEvent('variant-changed', {legendData: legendHTML, legend: legend}); } function handleColors(showAntigens) { if (typeof showAntigens === 'undefined') { showAntigens = $('.antigens.active').hasClass('on'); } let legendType; let structure = getLoadedStructure(); if (showAntigens) { let antigens = getAntigensForStructure(structure); let key = structure+'-'+antigens.join('-'); if (!colorSchemes.hasOwnProperty(key)) { let colors = {}; antigens.forEach(function(v) { if (!colors.hasOwnProperty(v.chain_name)) { colors[v.chain_name] = []; } if (!window.antibodyColorMap.hasOwnProperty(v.ab)) { window.antibodyColorMap[v.ab] = colorNames[Object.keys(window.antibodyColorMap).length] || 0xFF0000; } let color = window.antibodyColorMap[v.ab]; colors[v.chain_name].push([color, v.start_pos, v.end_pos]); }); colorSchemes[key] = NGL.ColormakerRegistry.addScheme(function () { this.atomColor = function (atom) { let r = colors[atom.chainname]; if (r) { for (let i = 0; i < r.length; i++) { if (atom.resno >= r[i][1] && atom.resno <= r[i][2]) { return r[i][0]; } } } return 0xFFFFFF; }; }); } stage.getComponentsByName(structure).forEach(function (e) { setRepresentation(e, colorSchemes[key]); }); // show legendType = 'antibody'; } else { // default color let color = getSelectedColor(); stage.getComponentsByName(structure).forEach(function (e) { setRepresentation(e, color); }); legendType = color; } let legendData = createColorLegend(legendType); raiseEvent('color-changed', {legendType: legendType, legendData: legendData}); } function handleSurface() { var showSurface = $('.surface.active').hasClass('on'); let structure = getLoadedStructure(); if (showSurface) { stage.getComponentsByName(structure).forEach(function (e) { if (volumeRepr.hasOwnProperty(structure)) { volumeRepr[structure].setVisibility(true); } else { var molsurf = new NGL.MolecularSurface(e.structure); var surf = molsurf.getSurface({ type: 'ses', probeRadius: 1.4, name: 'molsurf' }); var o = stage.addComponentFromObject(surf); volumeRepr[structure] = o.addRepresentation('surface'); } }); } else if (volumeRepr.hasOwnProperty(structure)) { volumeRepr[structure].setVisibility(false); } raiseEvent('surface-toggled', {display: showSurface}); } function highlightSection(transcript, startPos, stopPos, highlightedName, in_color, in_key) { let structure = getLoadedStructure(); let key; let ds = getStructureData(structure); if (!ds) { return; } if (typeof in_key !== 'undefined') { key = in_key; startPos = highlightParts[key].startPos; stopPos = highlightParts[key].stopPos; highlightedName = highlightParts[key].highlightedName; transcript = highlightParts[key].transcript; } else if (typeof transcript !== 'undefined') { key = structure + '-' + transcript + '-' + highlightedName + '-' + startPos + '-' + stopPos; } else { transcript = $('.tab_content:not(.hidden)').find('svg').data('transcript'); } // toggle the highlight if (highlightParts.hasOwnProperty(key)) { delete highlightParts[key]; } else if (typeof startPos !== 'undefined') { let nextColor; if (in_color) { nextColor = in_color; } else { nextColor = colorNames.find(function (v) { for (var k in highlightParts) { if (highlightParts[k].color === v) { return false; } } return true; }); } if (nextColor === undefined) { nextColor = 0xFF0000; } highlightParts[key] = { s: structure, startPos: startPos, stopPos: stopPos, highlightedName: highlightedName, key: key, transcript: transcript, color: nextColor }; } let keys = Object.keys(highlightParts); combinedSchemeKey = transcript+'-'+structure; if (keys.length) { combinedSchemeKey = keys.reduce(function(previousValue, currentValue) { return previousValue + highlightParts[currentValue].key; }); } let transcripts = ds.transcriptMappings; let offset; let haveOverlaps = false; if (transcripts.hasOwnProperty(transcript)) { offset = transcripts[transcript].offset; } else { throw Error('Transcript does not map'); } if (!colorSchemes.hasOwnProperty(combinedSchemeKey)) { let resCount = stage.getComponentsByName(structure).list[0].structure.residueStore.length; let residues = Array(resCount+1).fill(defaultColor); // There is probably a more performant way to do this... keys.forEach(function (v) { let hStartPos = highlightParts[v].startPos - 1 + offset; let hStopPos = highlightParts[v].stopPos - 1 + offset; let color = highlightParts[v].color; for (let k = hStartPos; k <= hStopPos; k++) { if (residues[k] === defaultColor) { residues[k] = color; } else { // Show overlaps in black residues[k] = 0x000000; haveOverlaps = true; } } }); let aa_seq_length = transcripts[transcript].aa_seq_length; for (let i=offset; i <= offset+aa_seq_length; i++) { if (residues[i] === defaultColor) { residues[i] = transcriptColor; } } let schemaId = NGL.ColormakerRegistry.addScheme(function () { this.atomColor = function (atom) { return residues[atom.resno]; }; }); colorSchemes[combinedSchemeKey] = { overlaps: haveOverlaps, schemaId: schemaId }; } raiseEvent('highlight-changed'); } function setRepresentation(s, color) { if (color === 'highlight') { if (combinedSchemeKey === '') { highlightSection(); } let structure = getLoadedStructure(); stage.getComponentsByName(structure).forEach(function (e) { setRepresentation(e, colorSchemes[combinedSchemeKey].schemaId); }); return; } let opts = {color: color}; let structureData = getStructureData(s.name); if (color === 'bfactor' && structureData['structureType'] === 'prediction') { if (!alphaFoldConfidenceSchemeId) { alphaFoldConfidenceSchemeId = NGL.ColormakerRegistry.addScheme(function () { this.atomColor = function (atom) { let bfactor = atom.bfactor; if (bfactor >= 90) { return alphaFoldColors[90][1]; } if (bfactor >= 70) { return alphaFoldColors[70][1]; } if (bfactor >= 50) { return alphaFoldColors[50][1]; } return alphaFoldColors[0][1]; }; }); } opts['color'] = alphaFoldConfidenceSchemeId; } s.addRepresentation(structureVisType, opts); raiseEvent('representation-changed'); } function getLoadedStructure() { return currentStructure; } function setLoadedStructure(newStructure) { currentStructure = newStructure; } function init() { let viewer = top.querySelector('div.viewport'); // Create NGL Stage object stage = new NGL.Stage(viewer, {'backgroundColor': 'white'}); // Handle window resizing window.addEventListener('resize', function () { stage.handleResize(); }, false); viewer.addEventListener('resize', function() { stage.handleResize(); }); const resizeObserver = new ResizeObserver(function(entries) { for (const entry of entries) { if (document.fullscreenElement) { return; } if (entry.target.classList.contains('viewport')) { stage.handleResize(); } } }); resizeObserver.observe(viewer); if (typeof options.skipEvents !== "undefined" && options.skipEvents) { return; } $('div.structureSelector select').on('change', function() { loadmodel($(this).val()); }); // $('div.structureSelector select.assembly').on('change', function () { // let opt = viewer.querySelector('.assembly').value; // stage.compList.forEach(function (e) { // if (e.parameters.visible) { // e.setDefaultAssembly(opt); // } // }); // }); $(top).on('resetHighlight', function() { highlightParts = {}; if (getSelectedColor() === 'highlight') { highlightSection(); } }); $('.variants, .alphavariants').on('toggle', function() { handleMissense(); }); $('.antigens').on('toggle', function() { handleColors(); }); $('.color').on('toggle', function() { handleColors(); }); $('.surface').on('toggle', function() { handleSurface(); }); $('.autorotate').on('toggle', function() { stage.setSpin($('.autorotate.active').hasClass('on')); }); $(top).on('highlightSection', function(e, data) { highlightSection(data.transcript, data.startPos, data.stopPos, data.name, data.color, data.key); }); } async function run(modelToLoad) { if (modelToLoad) { await loadmodel(modelToLoad); } else { hideComponents(); } } function createColorLegend(showLegend) { let newHtml = ""; if (showLegend === 'antibody') { Object.keys(window.antibodyColorMap).forEach(function (v) { let color = "#" + window.antibodyColorMap[v].toString(16).padStart(6, "0"); newHtml += `<div class="colorLegendEntry"><div class='colorLegendColor' style='background-color: ${color}'></div><div class='colorLegendText'>${v}</div></div>`; }); if (newHtml) { newHtml = "<div class='title'>Legend:</div>"+newHtml; } } else if (showLegend === 'bfactor') { if ($(".viewport.Predicted").length) { newHtml = "<div class='title'>Confidence for predicted structure:</div>" + newHtml; Object.keys(alphaFoldColors).sort(function (a, b) { return b - a; }).forEach(function (v) { let color = alphaFoldColors[v][2]; let text = alphaFoldColors[v][0]; newHtml += `<div class="colorLegendEntry"><div class='colorLegendColor' style='background-color: ${color}'></div><div class='colorLegendText'>${text}</div></div>`; }); } } else if (showLegend === 'highlight') { newHtml = $("<div><div class='title'>Highlighted:</div></div>"); let keys = Object.keys(highlightParts); let legendItems = []; keys.forEach(function(v) { legendItems.push({ name: highlightParts[v].highlightedName, color: '#'+highlightParts[v].color.toString(16).padStart(6, '0'), key: v }); }); if (!legendItems.length) { newHtml.append('<div class="center italic">-- Select items in the protein browser above to highlight --</div>'); } legendItems.push({ name: "Transcript", color: '#'+transcriptColor.toString(16).padStart(6, '0') }); // FIXME: Implement a better check but this is good enough for now if (!getLoadedStructure().startsWith('ENSP')) { legendItems.push({ name: "Region outside transcript", color: '#' + defaultColor.toString(16).padStart(6, '0') }); } if (combinedSchemeKey && colorSchemes[combinedSchemeKey].overlaps) { legendItems.push({ name: 'Overlapping items', color: '#000000' }); } Object.keys(legendItems).sort(function (a, b) { return legendItems[b]['name'] - legendItems[a]['name']; }).forEach(function (v) { let color = legendItems[v]['color']; let title = legendItems[v]['name']; let entry = $(`<div class="colorLegendEntry"><div class='colorLegendColor' style='background-color: ${color}'></div><div class='colorLegendText'>${title}</div></div>`); if (typeof legendItems[v]['key'] !== 'undefined' && legendItems[v]['key'].length) { entry.attr('data-key', legendItems[v]['key']); entry.attr('title', 'Click to remove highlight'); entry.addClass('structureHL'); } newHtml.append(entry); }); } return newHtml; } function updateVariantLegend(structure, showLegend) { if (!showLegend.length) { return ""; } let data = getStructureData(structure); let o = ""; for (let i in showLegend) { let ltype = showLegend[i]; let counts = (data.variantCount && typeof data.variantCount[ltype] !== "undefined") ? data.variantCount[ltype] : {}; let sites = (typeof counts['sites'] !== "undefined") ? counts['sites'] : 'NA'; let variants = (typeof counts['variants'] !== "undefined") ? counts['variants'] : 'NA'; let prefix; let color; if (ltype === 'c' || ltype === 'p') { prefix = 'Experimental'; color = 'blue'; } else { prefix = 'AlphaMissense'; color = 'red'; } o += `<div class='title'>${prefix} variants for structure (${color} spheres):</div><div>${variants} variants in ${sites} sites</div>`; } return o; } init(); return { stage: stage, loadStructure: loadStructure, run: run, getLoadedStructure: getLoadedStructure, addHandler: addHandler, addOnceHandler: addOnceHandler, removeHandler: removeHandler, getStructureData: getStructureData, addStructureData: addStructureData, getStructureDataEntries: getStructureDataEntries, loadmodel: loadmodel, }; }