/** * Copyright (C) 2022, Protein Bioinformatics Research Group, RCNS * * Licensed under CC BY-NC 4.0, see LICENSE file for more info. * * @author Gabor Tusnady * @author Csongor Gerdan */ import { StructureElement, Unit } from 'molstar/lib/mol-model/structure'; import { ColorTheme } from 'molstar/lib/mol-theme/color'; import { ThemeDataContext } from 'molstar/lib/mol-theme/theme'; import { Color } from 'molstar/lib/mol-util/color'; import { ColorNames } from 'molstar/lib/mol-util/color/names'; import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition'; import { Location } from 'molstar/lib/mol-model/location'; import { ChainList, getResidue, PDBTMChain } from './types'; import { TmDetChainListCache, TmDetDescriptorCache } from './prop'; export type TmDetColorThemeParams = { pdbtmDescriptor: any }; export function TmDetColorTheme( ctx: ThemeDataContext, props: PD.Values ): ColorTheme { let descriptorChains: PDBTMChain[] = []; let pdbtmDescriptor = props.pdbtmDescriptor; const pdbId = ctx.structure?.model.entryId; // If it is not given as parameter, // but there is one in the cache for this structure. if (!pdbtmDescriptor) { if (pdbId) { pdbtmDescriptor = TmDetDescriptorCache.get(pdbId); } } if (pdbtmDescriptor) { descriptorChains = pdbtmDescriptor.chains; } const tmType = pdbtmDescriptor.additional_entry_annotations.tm_type; const chainList = createResidueListsPerChain(descriptorChains, pdbtmDescriptor.side1, tmType); if (pdbId) { TmDetChainListCache.set(pdbId, chainList); } return { factory: TmDetColorTheme, granularity: 'group', //'residue' as any, color: (location: Location) => getColor(location, chainList), props: props, description: 'TMDet...', }; } const DefaultResidueColor = ColorNames.lightgrey; enum SiteIndexes { Side1 = 0, Side2 = 1, TmAlpha = 2, TmBeta = 3, TmReentrantLoop = 4, InterfacialHelix = 5, UnknownLocalization = 6, MembraneInside = 7, Periplasm = 8 }; // Old default values - it is overwritten by ult_* CSS classes // See below updateSiteColors(). const siteColors = [ Color.fromArray([255,100,100], 0), // Side1 Color.fromArray([100,100,255], 0), // Side2 Color.fromArray([255,255, 0], 0), // TM alpha Color.fromArray([255,255, 0], 0), // TM beta Color.fromArray([255,127, 0], 0), // TM re-entrant loop Color.fromArray([0,255, 0], 0), // Interfacial Helix Color.fromArray([196,196,196], 0), // Unknow localization Color.fromArray([0,255, 0], 0), // Membrane Inside Color.fromArray([255, 0, 255], 0) // Periplasm ]; const siteCssNames = [ "ult_side1", "ult_side2", "ult_alpha", "ult_beta", "ult_reentrant", "ult_ifh", "ult_unknown", "ult_membins", "ult_periplasm" ]; const regionColorMapFromCss = new Map(); // set default values siteCssNames.forEach((className, index) => { regionColorMapFromCss.set(className, siteColors[index]); }); function getColor(location: Location, chains: ChainList): Color { let color = DefaultResidueColor; // TODO: How to solve cases when chain operation occurs? if (StructureElement.Location.is(location) && Unit.isAtomic(location.unit)) { const { chainId, residueId } = getChainAndResidueIds(location); color = residueColor(chains, chainId!, residueId!); } return color; } export function getChainAndResidueIds(location: Location) { let chainId = undefined; let residueId = undefined; if (StructureElement.Location.is(location) && Unit.isAtomic(location.unit)) { const atomicHierarchy = location.unit.model.atomicHierarchy; const residueIdx = StructureElement.residueIndex(location); const chainIdx = StructureElement.Location.chainIndex(location); residueId = atomicHierarchy.residues.label_seq_id.value(residueIdx).toString(); chainId = atomicHierarchy.chains.label_asym_id.value(chainIdx).toString(); } return { chainId: chainId, residueId: residueId }; } export function createResidueListsPerChain(chains: PDBTMChain[], side1: string|null, tmType: string) { const chainList: ChainList = []; const hasBeta = chains.some(chain => chain.additional_chain_annotations.type == "beta"); chains.forEach((chain: PDBTMChain) => { const chainType = chain.additional_chain_annotations.type; chainList.push({ chainId: chain.chain_label, type: chainType, residues: [] }); let annotationErrorLogged = false; chain.residues.forEach((residue) => { let siteId = residue.site_data![0].site_id_ref - 1; let siteColorId = siteId; if (tmType == "Tm_Alpha" && hasBeta && side1 == "Periplasm") { if (siteId == SiteIndexes.Side1) { siteColorId = SiteIndexes.Periplasm; } else if (siteId == SiteIndexes.Side2) { if (chainType == "non_tm" || chainType == "alpha") { siteColorId = SiteIndexes.Side1; } else if (chainType == "beta") { siteColorId = SiteIndexes.Side2; } } } else if (chainType == "beta") { if (side1 == "Periplasm" && siteColorId == SiteIndexes.Side1 || side1 == "Outside" && siteColorId == SiteIndexes.Side2) { siteColorId = SiteIndexes.Periplasm; } else if (!annotationErrorLogged && side1 == "Inside" && siteColorId == SiteIndexes.Side1) { console.error(`Annotation error: beta chain has inside region ${chain.chain_label}:${residue.pdb_res_label}`); annotationErrorLogged = true; } } chainList[chainList.length - 1].residues.push({ authId: residue.pdb_res_label, siteId: siteId, siteColorId: siteColorId }); }); }); return chainList; } function residueColor(chains: ChainList, chainId: string, residueId: string): Color { let color = DefaultResidueColor; const residue = getResidue(chains, chainId, residueId); if (residue) { color = siteColors[residue.siteColorId]; } return color; } // Provider export const TmDetColorThemeProvider: ColorTheme.Provider = { name: 'tmdet-custom-color-theme', label: 'TMDet Topology Theme', category: 'TMDet', factory: TmDetColorTheme, getParams: () => ({ pdbtmDescriptor: { isOptional: true } }), defaultValues: { pdbtmDescriptor: undefined }, isApplicable: () => true, }; // Colors from CSS rules function loadRegionColorsFromStyleSheets(prefix: string = 'ult_'): void { const sheets: CSSStyleSheet[] = Array.from(document.styleSheets); sheets.forEach((sheet: CSSStyleSheet) => { const rules: CSSRule[] = Array.from(sheet.cssRules); rules.forEach((rule: CSSRule) => fetchRule(rule, prefix)); }); } export function updateSiteColors(side1: "Inside"|"Outside"|null): void { if (!side1) { console.error('updateSiteColors: side1 is undefined or null'); return; } loadRegionColorsFromStyleSheets(); if (regionColorMapFromCss.size == 0) { console.warn('Cannot read any region-related color rules'); } siteCssNames.forEach((ultClassName, index) => { const color = regionColorMapFromCss.get(ultClassName); if (color != null) { siteColors[index] = color; } }); const inside = regionColorMapFromCss.get("ult_inside"); const outside = regionColorMapFromCss.get("ult_outside"); if (side1 == "Inside") { siteColors[SiteIndexes.Side1] = inside; siteColors[SiteIndexes.Side2] = outside; } else if (side1 == "Outside") { siteColors[SiteIndexes.Side1] = outside; siteColors[SiteIndexes.Side2] = inside; } } function fetchRule(rule: CSSRule, prefix: string) { let styleRule = rule as CSSStyleRule; if (styleRule.selectorText?.startsWith('.' + prefix)) { const value = styleRule.style.getPropertyValue('fill'); const color: Color = getStyleColor(value); const key = styleRule.selectorText.slice(1); regionColorMapFromCss.set(key, color); } } function getStyleColor(cssColorText: string): Color { const values = cssColorText?.match(/\d+/g); let intValues = values?.map(value => parseInt(value)); if (!intValues) { intValues = [ 0, 0, 0 ]; } return Color.fromArray(intValues, 0); } export function getStyleColorString(cssClass: string): string { return Color.toHexStyle(regionColorMapFromCss.get(cssClass)); }