import {Viewer, ViewerProps} from '@rcsb/rcsb-molstar/build/src/viewer'; import {PresetProps} from '@rcsb/rcsb-molstar/build/src/viewer/helpers/preset'; import { ChainInfo, OperatorInfo, SaguaroChain, SaguaroPluginInterface, SaguaroPluginModelMapType, SaguaroPosition, SaguaroRange, SaguaroSet } from "../SaguaroPluginInterface"; import {PluginContext} from "molstar/lib/mol-plugin/context"; import {Loci} from "molstar/lib/mol-model/loci"; import {Mat4} from "molstar/lib/mol-math/linear-algebra"; import {BuiltInTrajectoryFormat} from "molstar/lib/mol-plugin-state/formats/trajectory"; import {PluginState} from "molstar/lib/mol-plugin/state"; import { Structure, StructureElement, StructureProperties as SP, StructureSelection, Queries as Q, StructureQuery } from "molstar/lib/mol-model/structure"; import {OrderedSet} from "molstar/lib/mol-data/int"; import { PluginStateObject as PSO } from 'molstar/lib/mol-plugin-state/objects'; import {State, StateObject} from "molstar/lib/mol-state"; import {StructureComponentRef, StructureRef} from "molstar/lib/mol-plugin-state/manager/structure/hierarchy-state"; import { RcsbFvSelectorManager } from "../../RcsbFvSelection/RcsbFvSelectorManager"; import {AbstractPlugin} from "./AbstractPlugin"; import {Subscription} from "rxjs"; import {Script} from "molstar/lib/mol-script/script"; import {MolScriptBuilder as MS} from "molstar/lib/mol-script/language/builder"; import {SetUtils} from "molstar/lib/mol-util/set"; import {StructureRepresentationRegistry} from "molstar/lib/mol-repr/structure/registry"; import {ColorTheme} from "molstar/lib/mol-theme/color"; import {TrajectoryHierarchyPresetProvider} from "molstar/lib/mol-plugin-state/builder/structure/hierarchy-preset"; import {Expression} from "molstar/lib/commonjs/mol-script/language/expression"; export enum LoadMethod { loadPdbId = "loadPdbId", loadPdbIds = "loadPdbIds", loadStructureFromUrl = "loadStructureFromUrl", loadSnapshotFromUrl = "loadSnapshotFromUrl", loadStructureFromData = "loadStructureFromData" } export interface LoadMolstarInterface { loadMethod: LoadMethod; loadParams: LoadParams | Array; } interface LoadParams { pdbId?: string; props?: PresetProps; matrix?: Mat4; url?: string, format?: BuiltInTrajectoryFormat, isBinary?: boolean, type?: PluginState.SnapshotType, data?: string | number[] id?:string; reprProvider?: TrajectoryHierarchyPresetProvider; params?:P; } export class MolstarPlugin extends AbstractPlugin implements SaguaroPluginInterface { private viewer: Viewer; private innerSelectionFlag: boolean = false; private loadingFlag: boolean = false; private selectCallbackSubs: Subscription; private modelChangeCallback: (chainMap:SaguaroPluginModelMapType)=>void; private modelChangeCallbackSubs: Subscription; private modelMap: Map = new Map(); private readonly componentMap: Map = new Map(); constructor(props: RcsbFvSelectorManager) { super(props); } public init(target: string | HTMLElement, props?: Partial) { this.viewer = new Viewer(target, { ...props, layoutShowControls:false, layoutShowSequence: true, canvas3d: { multiSample: { mode: 'off' } } }); } public clear(): void{ this.viewer.clear(); } async load(loadConfig: LoadMolstarInterface|Array): Promise{ this.loadingFlag = true; for (const lC of (Array.isArray(loadConfig) ? loadConfig : [loadConfig])) { if(MolstarPlugin.checkLoadData(lC)) { if (lC.loadMethod == LoadMethod.loadPdbId) { const config: LoadParams = lC.loadParams as LoadParams; await this.viewer.loadPdbId(config.pdbId!, {props: config.props, matrix: config.matrix, reprProvider: config.reprProvider, params: config.params}); } else if (lC.loadMethod == LoadMethod.loadPdbIds) { const config: Array = lC.loadParams as Array; await this.viewer.loadPdbIds(config.map((d) => { return {pdbId: d.pdbId!, config:{props: d.props, matrix: d.matrix, reprProvider: d.reprProvider, params: d.params}} })); } else if (lC.loadMethod == LoadMethod.loadStructureFromUrl) { const config: LoadParams = lC.loadParams as LoadParams; await this.viewer.loadStructureFromUrl(config.url!, config.format!, config.isBinary!,{props: config.props, matrix: config.matrix, reprProvider: config.reprProvider, params: config.params}); } else if (lC.loadMethod == LoadMethod.loadSnapshotFromUrl) { const config: LoadParams = lC.loadParams as LoadParams; await this.viewer.loadSnapshotFromUrl(config.url!, config.type!); } else if (lC.loadMethod == LoadMethod.loadStructureFromData) { const config: LoadParams = lC.loadParams as LoadParams; await this.viewer.loadStructureFromData(config.data!, config.format!, config.isBinary!, {props: config.props, matrix: config.matrix, reprProvider: config.reprProvider, params: config.params}); } } this.viewer.plugin.selectionMode = true; (Array.isArray(lC.loadParams) ? lC.loadParams : [lC.loadParams]).forEach(lP=>{ if(typeof lP.params?.getMap === "function") { const map: Map = lP.params.getMap(); if(typeof map?.forEach === "function") map.forEach((modelId: string, key: string) => { if (typeof modelId === "string" && typeof key === "string") { this.modelMap.set(key, modelId); this.modelMap.set(modelId, key); } }) } }); this.mapModels(lC.loadParams); } this.loadingFlag = false; this.modelChangeCallback(this.getChains()); } private static checkLoadData(loadConfig: LoadMolstarInterface): boolean{ const method: LoadMethod = loadConfig.loadMethod; const params: LoadParams | Array = loadConfig.loadParams; if( method == LoadMethod.loadPdbId ){ if(params instanceof Array || params.pdbId == null) throw loadConfig.loadMethod+": missing pdbId"; }else if( method == LoadMethod.loadPdbIds ){ if(!(params instanceof Array)) throw loadConfig.loadMethod+": Array object spected"; for(const d of params){ if(d.pdbId == null) throw loadConfig.loadMethod+": missing pdbId" } }else if( method == LoadMethod.loadStructureFromUrl ){ if(params instanceof Array || params.url == null || params.isBinary == null || params.format == null) throw loadConfig.loadMethod+": arguments needed url, format, isBinary" }else if( method == LoadMethod.loadSnapshotFromUrl ){ if(params instanceof Array || params.url == null || params.type == null) throw loadConfig.loadMethod+": arguments needed url, type" }else if( method == LoadMethod.loadStructureFromData ){ if(params instanceof Array || params.data == null || params.format == null || params.isBinary == null) throw loadConfig.loadMethod+": arguments needed data, format, isBinary" } return true; } public setBackground(color: number) { } public select(modelId:string, labelAsymId: string, begin: number, end: number, mode: 'select'|'hover', operation:'add'|'set', operatorName?:string): void; public select(selection: Array, mode: 'select'|'hover', operation:'add'|'set'): void; public select(selection: Array, mode: 'select'|'hover', operation:'add'|'set'): void; public select(...args: any[]): void{ if(args.length >= 6){ this.selectRange(args[0],args[1],args[2],args[3],args[4],args[5]); }else if(args.length === 3 && (args[0] as Array).length > 0 && typeof (args[0] as Array)[0].position === 'number'){ this.selectSet(args[0],args[1],args[2]); }else if(args.length === 3 && (args[0] as Array).length > 0 && typeof (args[0] as Array)[0].begin === 'number'){ this.selectMultipleRanges(args[0],args[1],args[2]); } } private selectRange(modelId:string, labelAsymId: string, begin: number, end: number, mode: 'select'|'hover', operation:'add'|'set', operatorName?:string): void { if(mode == null || mode === 'select') { this.innerSelectionFlag = true; } this.viewer.select({modelId:this.getModelId(modelId), labelAsymId: labelAsymId, labelSeqRange:{beg: begin, end:end}, operatorName: operatorName}, mode,operation); this.innerSelectionFlag = false; } private selectSet(selection: Array, mode: 'select'|'hover', operation:'add'|'set'): void { if(mode == null || mode === 'select') { this.innerSelectionFlag = true; } this.viewer.select(selection.map(r=>({modelId: this.getModelId(r.modelId), labelSeqId:r.position, labelAsymId: r.labelAsymId, operatorName: r.operatorName})), mode, operation); this.innerSelectionFlag = false; } private selectMultipleRanges(selection: Array, mode: 'select'|'hover', operation:'add'|'set'): void { if(mode == null || mode === 'select') { this.innerSelectionFlag = true; } this.viewer.select(selection.map(r=>({modelId: this.getModelId(r.modelId), labelAsymId: r.labelAsymId, labelSeqRange:{beg:r.begin, end: r.end}, operatorName: r.operatorName})), mode, operation); this.innerSelectionFlag = false; } public clearSelection(mode:'select'|'hover', option?:SaguaroChain): void { if(mode === 'select') { this.viewer.clearFocus(); this.innerSelectionFlag = true; } if(option != null) this.viewer.clearSelection(mode, {...option, modelId: this.getModelId(option.modelId)}); else this.viewer.clearSelection(mode); this.innerSelectionFlag = false; } public setFocus(modelId: string, labelAsymId: string, begin: number, end: number, operatorName?:string): void{ this.viewer.setFocus({modelId: this.getModelId(modelId), labelAsymId: labelAsymId, labelSeqRange:{beg:begin, end: end}, operatorName: operatorName}); } public clearFocus(): void { this.viewer.clearFocus(); } public cameraFocus(modelId: string, labelAsymId: string, positions:Array, operatorName?:string): void; public cameraFocus(modelId: string, labelAsymId: string, begin: number, end: number, operatorName?:string): void; public cameraFocus(...args: any[]): void{ if(typeof args[3] === "number"){ this.focusRange(args[0],args[1],args[2],args[3],args[4]); }else{ this.focusPositions(args[0],args[1],args[2],args[3]); } } private focusPositions(modelId: string, labelAsymId: string, positions:Array, operatorName?:string): void{ const structure: Structure | undefined = getStructureWithModelId(this.viewer.plugin.managers.structure.hierarchy.current.structures, this.getModelId(modelId)); if (structure == null) return; const chainTests: Expression[] = [MS.core.rel.eq([MS.ammp('label_asym_id'), labelAsymId])]; if(operatorName) chainTests.push(MS.core.rel.eq([operatorName, MS.acp('operatorName')])); const sel: StructureSelection = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({ 'chain-test': Q.core.logic.and(chainTests), 'residue-test': Q.core.set.has([MS.set(...SetUtils.toArray(new Set(positions))), MS.ammp('label_seq_id')]) }), structure); const loci: Loci = StructureSelection.toLociWithSourceUnits(sel); if(!StructureElement.Loci.isEmpty(loci)) this.viewer.plugin.managers.camera.focusLoci(loci); else this.viewer.plugin.managers.camera.reset(); } private focusRange(modelId: string, labelAsymId: string, begin: number, end: number, operatorName?:string): void{ const seqIds: Array = new Array(); for(let n = begin; n <= end; n++){ seqIds.push(n); } this.focusPositions(modelId, labelAsymId, seqIds, operatorName); } public async createComponent(componentLabel: string, modelId:string, labelAsymId: string, begin: number, end : number, representationType: StructureRepresentationRegistry.BuiltIn, operatorName?:string): Promise; public async createComponent(componentLabel: string, modelId:string, labelAsymId: string, representationType: StructureRepresentationRegistry.BuiltIn, operatorName?:string): Promise; public async createComponent(componentLabel: string, residues: Array, representationType: StructureRepresentationRegistry.BuiltIn): Promise; public async createComponent(componentLabel: string, residues: Array, representationType: StructureRepresentationRegistry.BuiltIn): Promise; public async createComponent(...args: any[]): Promise { this.removeComponent(args[0]); if(args.length === 3){ if( args[1] instanceof Array && args[1].length > 0 ) { if(typeof args[1][0].position === "number"){ await this.viewer.createComponent(args[0], args[1].map(r=>({modelId: this.getModelId(r.modelId), labelAsymId: r.labelAsymId, labelSeqId: r.position, operatorName: r.operatorName})), args[2]); }else{ await this.viewer.createComponent(args[0], args[1].map(r=>({modelId: this.getModelId(r.modelId), labelAsymId: r.labelAsymId, labelSeqRange:{beg:r.begin, end: r.end}, operatorName: r.operatorName})), args[2]); } } }else if(args.length >= 6){ await this.viewer.createComponent(args[0], {modelId: this.getModelId(args[1]), labelAsymId: args[2], labelSeqRange:{beg:args[3], end:args[4]}, operatorName: args[6]}, args[5]); }else{ await this.viewer.createComponent(args[0], {modelId: this.getModelId(args[1]), labelAsymId:args[2], operatorName: args[4]}, args[3]); } this.componentMap.set(args[0], this.viewer.plugin.managers.structure.hierarchy.currentComponentGroups[this.viewer.plugin.managers.structure.hierarchy.currentComponentGroups.length-1][0]); } public isComponent(componentLabel: string): boolean{ for(const c of this.viewer.plugin.managers.structure.hierarchy.currentComponentGroups){ for(const comp of c){ if(comp.cell.obj?.label === componentLabel) { return true; } } } return false; } public async colorComponent(componentLabel: string, color: ColorTheme.BuiltIn): Promise{ for(const c of this.viewer.plugin.managers.structure.hierarchy.currentComponentGroups){ for(const comp of c){ if(comp.cell.obj?.label === componentLabel) { await this.viewer.plugin.managers.structure.component.updateRepresentationsTheme([comp], { color: color }); return; } } } } public getComponentSet(): Set{ const out: Set = new Set(); this.viewer.plugin.managers.structure.hierarchy.currentComponentGroups.forEach((c)=>{ for(const comp of c){ if(comp.cell.obj?.label != null && out.has(comp.cell.obj?.label)) { break; }else if(comp.cell.obj?.label != null){ out.add(comp.cell.obj?.label); } } }); return out; } public removeComponent(componentLabel?: string): void{ if(componentLabel == null){ this.componentMap.forEach((comp, id)=>{ this.viewer.removeComponent(id); this.componentMap.delete(id); }) }else{ this.viewer.removeComponent(componentLabel); this.componentMap.delete(componentLabel); } } public displayComponent(componentLabel: string): boolean; public displayComponent(componentLabel: string, visibilityFlag: boolean): void; public displayComponent(componentLabel: string, visibilityFlag?: boolean): void|boolean { if(typeof visibilityFlag === 'boolean') return this.changeComponentDisplay(componentLabel, visibilityFlag); else return this.getComponentDisplay(componentLabel); } private changeComponentDisplay(componentLabel: string, visibilityFlag: boolean): void{ if(this.componentMap.has(componentLabel) && this.getComponentDisplay(componentLabel) != visibilityFlag) { this.viewer.plugin.managers.structure.component.toggleVisibility([this.componentMap.get(componentLabel)!]); }else if(!this.componentMap.has(componentLabel)){ for (const c of this.viewer.plugin.managers.structure.hierarchy.currentComponentGroups) { for (const comp of c) { if(comp.cell.obj?.label === componentLabel) { if(!comp.cell.state.isHidden != visibilityFlag) { this.viewer.plugin.managers.structure.component.toggleVisibility(c); return void 0; } } } } } } private getComponentDisplay(componentLabel: string): boolean | undefined{ if(this.componentMap.has(componentLabel)) { return !this.componentMap.get(componentLabel)!.cell.state.isHidden!; } } public setRepresentationChangeCallback(g:()=>void){ } public setHoverCallback(g:()=>void){ this.viewer.plugin.behaviors.interaction.hover.subscribe((r)=>{ const sequenceData: Array = new Array(); const loci:Loci = r.current.loci; if(StructureElement.Loci.is(loci)){ const loc = StructureElement.Location.create(loci.structure); for (const e of loci.elements) { const modelId: string = e.unit?.model?.id; const seqIds = new Set(); loc.unit = e.unit; for (let i = 0, il = OrderedSet.size(e.indices); i < il; ++i) { loc.element = e.unit.elements[OrderedSet.getAt(e.indices, i)]; seqIds.add(SP.residue.label_seq_id(loc)); } sequenceData.push({ modelId: this.getModelId(modelId), labelAsymId: SP.chain.label_asym_id(loc), operatorName: SP.unit.operator_name(loc), seqIds }); } } this.selection.setSelectionFromResidueSelection(sequenceData, 'hover', 'structure'); g(); }); } public setSelectCallback(g:(flag?:boolean)=>void){ this.selectCallbackSubs = this.viewer.plugin.managers.structure.selection.events.changed.subscribe(()=>{ if(this.innerSelectionFlag) { return; } if(this.viewer.plugin.managers.structure.selection.additionsHistory.length > 0) { const currentLoci: Loci = this.viewer.plugin.managers.structure.selection.additionsHistory[0].loci; const loc: StructureElement.Location = StructureElement.Location.create(currentLoci.structure); StructureElement.Location.set( loc, currentLoci.structure, currentLoci.elements[0].unit, currentLoci.elements[0].unit.elements[OrderedSet.getAt(currentLoci.elements[0].indices,0)] ); const currentModelId: string = this.getModelId(currentLoci.structure.model.id); if(currentLoci.elements.length > 0) if(SP.entity.type(loc) === 'non-polymer') { const resAuthId: number = SP.residue.auth_seq_id(loc); const chainLabelId: string = SP.chain.label_asym_id(loc); const query: StructureQuery = Q.modifiers.includeSurroundings( Q.generators.residues({ residueTest:l=>SP.residue.auth_seq_id(l.element) === resAuthId, chainTest:l=>SP.chain.label_asym_id(l.element) === chainLabelId }), { radius: 5, wholeResidues: true }); this.innerSelectionFlag = true; const sel: StructureSelection = StructureQuery.run(query, currentLoci.structure); const surroundingsLoci: Loci = StructureSelection.toLociWithSourceUnits(sel); this.viewer.plugin.managers.structure.selection.fromLoci('add', surroundingsLoci); const surroundingsLoc = StructureElement.Location.create(surroundingsLoci.structure); for (const e of surroundingsLoci.elements) { StructureElement.Location.set(surroundingsLoc, surroundingsLoci.structure, e.unit, e.unit.elements[0]); if(SP.entity.type(surroundingsLoc) === 'polymer'){ this.selection.setLastSelection('select', { modelId: currentModelId, labelAsymId: SP.chain.label_asym_id(surroundingsLoc), regions: [] }); } } this.innerSelectionFlag = false; }else if( SP.entity.type(loc) === 'polymer' ) { this.selection.setLastSelection('select', { modelId: currentModelId, labelAsymId: SP.chain.label_asym_id(loc), operatorName: SP.unit.operator_name(loc), regions: [] }); }else{ this.selection.setLastSelection('select', null); } }else{ this.selection.setLastSelection('select', null); } const sequenceData: Array = new Array(); for(const structure of this.viewer.plugin.managers.structure.hierarchy.current.structures){ const data: Structure | undefined = structure.cell.obj?.data; if(data == null) return; const loci: Loci = this.viewer.plugin.managers.structure.selection.getLoci(data); if(StructureElement.Loci.is(loci)){ const loc = StructureElement.Location.create(loci.structure); for (const e of loci.elements) { StructureElement.Location.set(loc, loci.structure, e.unit, e.unit.elements[0]); const seqIds = new Set(); for (let i = 0, il = OrderedSet.size(e.indices); i < il; ++i) { loc.element = e.unit.elements[OrderedSet.getAt(e.indices, i)]; seqIds.add(SP.residue.label_seq_id(loc)); } sequenceData.push({ modelId: this.getModelId(data.model.id), labelAsymId: SP.chain.label_asym_id(loc), operatorName: SP.unit.operator_name(loc), seqIds }); } } } this.selection.setSelectionFromResidueSelection(sequenceData, 'select', 'structure'); g(); }); } public pluginCall(f: (plugin: PluginContext) => void){ this.viewer.pluginCall(f); } public setModelChangeCallback(f:(modelMap:SaguaroPluginModelMapType)=>void){ this.modelChangeCallback = f; this.modelChangeCallbackSubs = this.viewer.plugin.state.events.object.updated.subscribe((o:{obj: StateObject, action: "in-place" | "recreate"})=>{ if(this.loadingFlag) return; if(o.obj.type.name === "Behavior" && o.action === "in-place") { f(this.getChains()); }else if(o.obj.type.name === "Model" && o.action === "in-place"){ f(this.getChains()); } }); } private getChains(): SaguaroPluginModelMapType{ const structureRefList = getStructureOptions(this.viewer.plugin); const out: SaguaroPluginModelMapType = new Map; assemblyId:string;}>(); structureRefList.forEach((structureRef,i)=>{ const structure: Structure = getStructure(structureRef[0], this.viewer.plugin.state.data); let modelEntityId = getModelEntityOptions(structure)[0][0]; const chains: [{modelId:string;entryId:string;assemblyId:string;},ChainInfo[]] = getChainValues(structure, modelEntityId); out.set(this.getModelId(chains[0].modelId),{entryId:chains[0].entryId, assemblyId:chains[0].assemblyId, chains: chains[1]}); }); return out; } private mapModels(loadParams: LoadParams | Array): void{ const loadParamList: Array = loadParams instanceof Array ? loadParams : [loadParams]; const structureRefList = getStructureOptions(this.viewer.plugin); if(loadParamList.length == structureRefList.length ) structureRefList.forEach((structureRef,i)=>{ const structure = getStructure(structureRef[0], this.viewer.plugin.state.data); let modelEntityId = getModelEntityOptions(structure)[0][0]; const chains: [{modelId:string, entryId:string},ChainInfo[]] = getChainValues(structure, modelEntityId); if(!this.modelMap.has(chains[0].modelId)) { this.modelMap.set(chains[0].modelId, loadParamList[i].id); if (loadParamList[i].id != null) this.modelMap.set(loadParamList[i].id!, chains[0].modelId); } }); } private getModelId(id: string): string{ return this.modelMap.get(id) ?? id; } public unsetCallbacks(): void { this.selectCallbackSubs?.unsubscribe(); this.modelChangeCallbackSubs?.unsubscribe(); } public resetCamera(): void { this.viewer.plugin.managers.camera.reset(); } } function getStructureOptions(plugin: PluginContext): [string,string][] { const options: [string, string][] = []; plugin.managers.structure.hierarchy.current.structures.forEach(s=>{ options.push([s.cell.transform.ref, s.cell.obj!.data.label]); }) return options; } function getChainValues(structure: Structure, modelEntityId: string): [{modelId:string; entryId:string; assemblyId:string;},ChainInfo[]] { const chains: Map = new Map(); const l = StructureElement.Location.create(structure); let assemblyId:string = "-"; const [modelIdx, entityId] = splitModelEntityId(modelEntityId); for (const unit of structure.units) { StructureElement.Location.set(l, structure, unit, unit.elements[0]); assemblyId = SP.unit.pdbx_struct_assembly_id(l); if (structure.getModelIndex(unit.model) !== modelIdx) continue; const chId: number = unit.chainGroupId; if(chains.has(chId)){ chains.get(chId)!.operators.push(opKey(l)) }else{ chains.set(chId, {label:SP.chain.label_asym_id(l), auth:SP.chain.auth_asym_id(l), entityId: SP.entity.id(l), title: SP.entity.pdbx_description(l).join("|"), type: SP.entity.type(l), operators:[opKey(l)]}); } } const id: {modelId:string; entryId:string; assemblyId:string;} = {modelId:l.unit?.model?.id, entryId: l.unit?.model?.entryId, assemblyId: assemblyId}; return [id,Array.from(chains.values())]; } function getStructureWithModelId(structures: StructureRef[], modelId: string): Structure|undefined{ for(const structure of structures){ if(!structure.cell?.obj?.data?.units) continue; const unit = structure.cell.obj.data.units[0]; const id:string = unit.model.id; if(id === modelId) return structure.cell.obj.data } } function getStructure(ref: string, state: State) { const cell = state.select(ref)[0]; if (!ref || !cell || !cell.obj) return Structure.Empty; return (cell.obj as PSO.Molecule.Structure).data; } function getModelEntityOptions(structure: Structure):[string, string][] { const options: [string, string][] = []; const l = StructureElement.Location.create(structure); const seen = new Set(); for (const unit of structure.units) { StructureElement.Location.set(l, structure, unit, unit.elements[0]); const id = SP.entity.id(l); const modelIdx = structure.getModelIndex(unit.model); const key = `${modelIdx}|${id}`; if (seen.has(key)) continue; let description = SP.entity.pdbx_description(l).join(', '); if (structure.models.length) { if (structure.representativeModel) { // indicates model trajectory description += ` (Model ${structure.models[modelIdx].modelNum})`; } else if (description.startsWith('Polymer ')) { // indicates generic entity name description += ` (${structure.models[modelIdx].entry})`; } } const label = `${id}: ${description}`; options.push([ key, label ]); seen.add(key); } if (options.length === 0) options.push(['', 'No entities']); return options; } function splitModelEntityId(modelEntityId: string) { const [ modelIdx, entityId ] = modelEntityId.split('|'); return [ parseInt(modelIdx), entityId ]; } function opKey(l: StructureElement.Location): OperatorInfo { const ids = SP.unit.pdbx_struct_oper_list_ids(l); const ncs = SP.unit.struct_ncs_oper_id(l); const hkl = SP.unit.hkl(l); const spgrOp = SP.unit.spgrOp(l); const name = SP.unit.operator_name(l); return {ids:ids,name:name}; }