MsaBehaviour.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. /*
  2. * Copyright (c) 2021 RCSB PDB and contributors, licensed under MIT, See LICENSE file for more info.
  3. * @author Joan Segura Mora <joan.segura@rcsb.org>
  4. */
  5. import {
  6. StructureViewerBehaviourInterface,
  7. StructureViewerBehaviourObserverInterface
  8. } from "../StructureViewerBehaviourInterface";
  9. import {
  10. ChainInfo,
  11. OperatorInfo,
  12. SaguaroRange,
  13. ViewerActionManagerInterface,
  14. ViewerCallbackManagerInterface
  15. } from "../StructureViewerInterface";
  16. import {RcsbFvStateInterface} from "../../RcsbFvState/RcsbFvStateInterface";
  17. import {asyncScheduler, Subscription} from "rxjs";
  18. import {StructureLoaderInterface} from "../StructureUtils/StructureLoaderInterface";
  19. import {TagDelimiter} from "@rcsb/rcsb-saguaro-app";
  20. import {createSelectionExpressions} from "@rcsb/rcsb-molstar/build/src/viewer/helpers/selection";
  21. import {RegionSelectionInterface} from "../../RcsbFvState/RcsbFvSelectorManager";
  22. import {TargetAlignment} from "@rcsb/rcsb-api-tools/build/RcsbGraphQL/Types/Borrego/GqlTypes";
  23. import {FunctionCall} from "../../Utils/FunctionCall";
  24. import onetimeCall = FunctionCall.onetimeCall;
  25. type MsaBehaviourType<R> = StructureLoaderInterface<[
  26. ViewerCallbackManagerInterface & ViewerActionManagerInterface<R>,
  27. {entryId:string;entityId:string;},
  28. TargetAlignment,
  29. RcsbFvStateInterface
  30. ]>;
  31. export class MsaBehaviourObserver<R> implements StructureViewerBehaviourObserverInterface<R> {
  32. private structureBehaviour: StructureViewerBehaviourInterface;
  33. private readonly structureLoader: MsaBehaviourType<R>;
  34. constructor(structureLoader: MsaBehaviourType<R>) {
  35. this.structureLoader = structureLoader
  36. }
  37. public observe(
  38. structureViewer: ViewerCallbackManagerInterface & ViewerActionManagerInterface<R>,
  39. stateManager: RcsbFvStateInterface
  40. ): void {
  41. this.structureBehaviour = new MsaBehaviour(structureViewer, stateManager, this.structureLoader);
  42. }
  43. public unsubscribe(): void {
  44. this.structureBehaviour.unsubscribe();
  45. }
  46. }
  47. type SelectedRegion = {modelId: string, labelAsymId: string, region: RegionSelectionInterface, operatorName?: string};
  48. type AlignmentDataType = {
  49. pdb:{
  50. entryId:string;
  51. entityId:string;
  52. },
  53. targetAlignment: TargetAlignment;
  54. };
  55. class MsaBehaviour<R> implements StructureViewerBehaviourInterface {
  56. private readonly structureViewer: ViewerCallbackManagerInterface & ViewerActionManagerInterface<R>;
  57. private readonly stateManager: RcsbFvStateInterface;
  58. private readonly subscription: Subscription;
  59. private readonly structureLoader: MsaBehaviourType<R>;
  60. private readonly componentList: string[] = [];
  61. private readonly CREATE_COMPONENT_THR: number = 5;
  62. constructor(
  63. structureViewer: ViewerCallbackManagerInterface & ViewerActionManagerInterface<R>,
  64. stateManager: RcsbFvStateInterface,
  65. structureLoader: MsaBehaviourType<R>
  66. ) {
  67. this.structureViewer = structureViewer;
  68. this.stateManager = stateManager;
  69. this.structureLoader = structureLoader;
  70. this.subscription = this.subscribe();
  71. }
  72. private subscribe(): Subscription {
  73. return this.stateManager.subscribe<"model-change"|"representation-change"|"feature-click",AlignmentDataType & {tag:"polymer"|"non-polymer";isHidden:boolean;} & SelectedRegion[]>(async o=>{
  74. if(o.type == "model-change" && o.view == "1d-view" && o.data)
  75. await this.modelChange(o.data);
  76. if(o.type == "representation-change" && o.view == "1d-view" && o.data)
  77. this.reprChange(o.data);
  78. if(o.type == "selection-change" && o.view == "1d-view")
  79. this.selectionChange();
  80. if(o.type == "hover-change" && o.view == "1d-view")
  81. this.hoverChange();
  82. if(o.type == "feature-click" && o.view == "1d-view" && o.data)
  83. await this.featureClick(o.data)
  84. if(o.type == "selection-change" && o.view == "3d-view")
  85. await this.isSelectionEmpty();
  86. });
  87. }
  88. async featureClick(data?: SelectedRegion[]): Promise<void> {
  89. const cameraFocus = onetimeCall<SelectedRegion>((d: SelectedRegion) => {
  90. const {modelId, labelAsymId, region, operatorName} = d;
  91. const regions = [region];
  92. const residues: number[] = regions.map(r=> r.begin == r.end ? [r.begin] : [r.begin,r.end]).flat().filter(r=>r!=null);
  93. this.structureViewer.cameraFocus(modelId, labelAsymId, residues, operatorName);
  94. });
  95. await this.removeComponent();
  96. if(!data || data.length == 0)
  97. this.resetPluginView();
  98. const numRes = data?.map(d=>(d.region.end-d.region.begin+1)).reduce((prev,curr)=>prev+curr,0);
  99. if(!numRes)
  100. return;
  101. data?.forEach(d=>{
  102. const {modelId, labelAsymId, region, operatorName} = d;
  103. const regions = [region];
  104. if(modelId && labelAsymId && Array.isArray(regions) && regions.length > 0) {
  105. const residues: number[] = regions.map(r=> r.begin == r.end ? [r.begin] : [r.begin,r.end]).flat().filter(r=>r!=null);
  106. if(residues.length == 0)
  107. return;
  108. if(numRes == data?.length)
  109. this.structureViewer.setFocus(modelId,labelAsymId,residues[0],residues[0],operatorName);
  110. cameraFocus(d);
  111. const ranges: SaguaroRange[] = regions.map(r=>({
  112. modelId,
  113. labelAsymId,
  114. begin: r.begin,
  115. end: r.end,
  116. operatorName
  117. }));
  118. if(
  119. data?.map( d => (d.region.end-d.region.begin+1) < this.CREATE_COMPONENT_THR ? 1 : 0)
  120. .reduce((prev,curr)=>prev+curr,0) == data?.length
  121. )
  122. asyncScheduler.schedule(async ()=>{
  123. const x = residues[0];
  124. const y = residues[residues.length-1];
  125. const selectedComponentId = `${modelId}${TagDelimiter.instance}${labelAsymId +":"+ ((x === y) ? x.toString() : x.toString()+","+y.toString())}`;
  126. await this.structureViewer.createComponent(selectedComponentId!,ranges, "ball-and-stick");
  127. this.componentList.push(selectedComponentId);
  128. });
  129. }else{
  130. this.structureViewer.clearSelection("select", {modelId, labelAsymId});
  131. }
  132. })
  133. }
  134. hoverChange(): void {
  135. this.select("hover")
  136. }
  137. selectionChange(): void {
  138. this.select("select")
  139. }
  140. unsubscribe(): void {
  141. }
  142. reprChange(data?:{pdb:{entryId:string;entityId:string;}} & {tag:"aligned"|"polymer"|"non-polymer";isHidden:boolean;}): void {
  143. if(data){
  144. switch (data.tag){
  145. case "aligned":
  146. const chain: ChainInfo|undefined = this.stateManager.assemblyModelSate.getModelChainInfo(`${data.pdb.entryId}${TagDelimiter.entity}${data.pdb.entityId}`)?.chains.find(ch=>ch.entityId==data.pdb.entityId);
  147. if(chain){
  148. const asymId: string|undefined = chain.label;
  149. const operatorInfo: OperatorInfo[] = chain.operators ?? [];
  150. const componentId: string = `${data.pdb.entryId}${TagDelimiter.entity}${data.pdb.entityId}${TagDelimiter.instance}${asymId}${TagDelimiter.assembly}${operatorInfo[0].ids.join(",")}${TagDelimiter.assembly}${"polymer"}`;
  151. this.structureViewer.displayComponent(componentId, !data.isHidden);
  152. }
  153. break;
  154. case "polymer":
  155. const componentId: string = `${data.pdb.entryId}${TagDelimiter.entity}${data.pdb.entityId}${TagDelimiter.assembly}${data.tag}`;
  156. this.structureViewer.displayComponent(componentId, !data.isHidden);
  157. break;
  158. case "non-polymer":
  159. createSelectionExpressions(data.pdb.entryId).map(expression=>expression.tag).filter(tag=>(tag!="water" && tag != "polymer")).forEach(tag=>{
  160. const componentId: string = `${data.pdb.entryId}${TagDelimiter.entity}${data.pdb.entityId}${TagDelimiter.assembly}${tag}`;
  161. this.structureViewer.displayComponent(componentId, !data.isHidden);
  162. });
  163. break;
  164. }
  165. }
  166. }
  167. async modelChange(data?:AlignmentDataType): Promise<void> {
  168. if(data)
  169. await this.structureLoader.load(this.structureViewer, data.pdb, data.targetAlignment, this.stateManager);
  170. }
  171. private select(mode:"select"|"hover"): void{
  172. if(mode == "select")
  173. this.structureViewer.clearFocus();
  174. if(this.stateManager.selectionState.getSelection(mode).length == 0)
  175. this.structureViewer.clearSelection(mode);
  176. this.structureViewer.select(this.stateManager.selectionState.getSelection(mode).map(selectedRegion=>{
  177. return selectedRegion.regions.map(region=>{
  178. return {
  179. modelId: selectedRegion.modelId,
  180. labelAsymId: selectedRegion.labelAsymId,
  181. operatorName: selectedRegion.operatorName,
  182. begin: region.begin,
  183. end: region.end
  184. };
  185. })
  186. }).flat(), mode, "set");
  187. }
  188. private resetPluginView(): void {
  189. this.structureViewer.clearFocus();
  190. this.structureViewer.resetCamera();
  191. }
  192. private async isSelectionEmpty(): Promise<void> {
  193. if(this.stateManager.selectionState.getLastSelection() == null) {
  194. await this.removeComponent();
  195. this.resetPluginView();
  196. }
  197. }
  198. private async removeComponent(): Promise<void> {
  199. await Promise.all(this.componentList.map(async compId=>{
  200. await this.structureViewer.removeComponent(compId);
  201. }));
  202. }
  203. }