entry-root.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. /**
  2. * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Adam Midlik <midlik@gmail.com>
  5. */
  6. import { BehaviorSubject, distinctUntilChanged, Subject, throttleTime } from 'rxjs';
  7. import { VolsegVolumeServerConfig } from '.';
  8. import { Loci } from '../../mol-model/loci';
  9. import { ShapeGroup } from '../../mol-model/shape';
  10. import { Volume } from '../../mol-model/volume';
  11. import { LociLabelProvider } from '../../mol-plugin-state/manager/loci-label';
  12. import { PluginStateObject } from '../../mol-plugin-state/objects';
  13. import { PluginBehavior } from '../../mol-plugin/behavior';
  14. import { PluginCommands } from '../../mol-plugin/commands';
  15. import { PluginContext } from '../../mol-plugin/context';
  16. import { StateObjectCell, StateTransform } from '../../mol-state';
  17. import { shallowEqualObjects } from '../../mol-util';
  18. import { ParamDefinition } from '../../mol-util/param-definition';
  19. import { MeshlistData } from '../meshes/mesh-extension';
  20. import { DEFAULT_VOLUME_SERVER_V2, VolumeApiV2 } from './volseg-api/api';
  21. import { Segment } from './volseg-api/data';
  22. import { MetadataWrapper } from './volseg-api/utils';
  23. import { VolsegMeshSegmentationData } from './entry-meshes';
  24. import { VolsegModelData } from './entry-models';
  25. import { VolsegLatticeSegmentationData } from './entry-segmentation';
  26. import { VolsegState, VolsegStateData, VolsegStateParams } from './entry-state';
  27. import { VolsegVolumeData, SimpleVolumeParamValues } from './entry-volume';
  28. import * as ExternalAPIs from './external-api';
  29. import { VolsegGlobalStateData } from './global-state';
  30. import { applyEllipsis, Choice, isDefined, lazyGetter, splitEntryId } from './helpers';
  31. import { type VolsegStateFromEntry } from './transformers';
  32. export const MAX_VOXELS = 10 ** 7;
  33. // export const MAX_VOXELS = 10 ** 2; // DEBUG
  34. export const BOX: [[number, number, number], [number, number, number]] | null = null;
  35. // export const BOX: [[number, number, number], [number, number, number]] | null = [[-90, -90, -90], [90, 90, 90]]; // DEBUG
  36. const MAX_ANNOTATIONS_IN_LABEL = 6;
  37. const SourceChoice = new Choice({ emdb: 'EMDB', empiar: 'EMPIAR', idr: 'IDR' }, 'emdb');
  38. export type Source = Choice.Values<typeof SourceChoice>;
  39. export function createLoadVolsegParams(plugin?: PluginContext, entrylists: { [source: string]: string[] } = {}) {
  40. const defaultVolumeServer = plugin?.config.get(VolsegVolumeServerConfig.DefaultServer) ?? DEFAULT_VOLUME_SERVER_V2;
  41. return {
  42. serverUrl: ParamDefinition.Text(defaultVolumeServer),
  43. source: ParamDefinition.Mapped(SourceChoice.values[0], SourceChoice.options, src => entryParam(entrylists[src])),
  44. };
  45. }
  46. function entryParam(entries: string[] = []) {
  47. const options: [string, string][] = entries.map(e => [e, e]);
  48. options.push(['__custom__', 'Custom']);
  49. return ParamDefinition.Group({
  50. entryId: ParamDefinition.Select(options[0][0], options, { description: 'Choose an entry from the list, or choose "Custom" and type any entry ID (useful when using other than default server).' }),
  51. customEntryId: ParamDefinition.Text('', { hideIf: p => p.entryId !== '__custom__', description: 'Entry identifier, including the source prefix, e.g. "emd-1832"' }),
  52. }, { isFlat: true });
  53. }
  54. type LoadVolsegParamValues = ParamDefinition.Values<ReturnType<typeof createLoadVolsegParams>>;
  55. export function createVolsegEntryParams(plugin?: PluginContext) {
  56. const defaultVolumeServer = plugin?.config.get(VolsegVolumeServerConfig.DefaultServer) ?? DEFAULT_VOLUME_SERVER_V2;
  57. return {
  58. serverUrl: ParamDefinition.Text(defaultVolumeServer),
  59. source: SourceChoice.PDSelect(),
  60. entryId: ParamDefinition.Text('emd-1832', { description: 'Entry identifier, including the source prefix, e.g. "emd-1832"' }),
  61. };
  62. }
  63. type VolsegEntryParamValues = ParamDefinition.Values<ReturnType<typeof createVolsegEntryParams>>;
  64. export namespace VolsegEntryParamValues {
  65. export function fromLoadVolsegParamValues(params: LoadVolsegParamValues): VolsegEntryParamValues {
  66. let entryId = (params.source.params as any).entryId;
  67. if (entryId === '__custom__') {
  68. entryId = (params.source.params as any).customEntryId;
  69. }
  70. return {
  71. serverUrl: params.serverUrl,
  72. source: params.source.name as Source,
  73. entryId: entryId
  74. };
  75. }
  76. }
  77. export class VolsegEntry extends PluginStateObject.CreateBehavior<VolsegEntryData>({ name: 'Vol & Seg Entry' }) { }
  78. export class VolsegEntryData extends PluginBehavior.WithSubscribers<VolsegEntryParamValues> {
  79. plugin: PluginContext;
  80. ref: string = '';
  81. api: VolumeApiV2;
  82. source: Source;
  83. /** Number part of entry ID; e.g. '1832' */
  84. entryNumber: string;
  85. /** Full entry ID; e.g. 'emd-1832' */
  86. entryId: string;
  87. metadata: MetadataWrapper;
  88. pdbs: string[];
  89. public readonly volumeData = new VolsegVolumeData(this);
  90. private readonly latticeSegmentationData = new VolsegLatticeSegmentationData(this);
  91. private readonly meshSegmentationData = new VolsegMeshSegmentationData(this);
  92. private readonly modelData = new VolsegModelData(this);
  93. private highlightRequest = new Subject<Segment | undefined>();
  94. private getStateNode = lazyGetter(() => this.plugin.state.data.selectQ(q => q.byRef(this.ref).subtree().ofType(VolsegState))[0] as StateObjectCell<VolsegState, StateTransform<typeof VolsegStateFromEntry>>, 'Missing VolsegState node. Must first create VolsegState for this VolsegEntry.');
  95. public currentState = new BehaviorSubject(ParamDefinition.getDefaultValues(VolsegStateParams));
  96. private constructor(plugin: PluginContext, params: VolsegEntryParamValues) {
  97. super(plugin, params);
  98. this.plugin = plugin;
  99. this.api = new VolumeApiV2(params.serverUrl);
  100. this.source = params.source;
  101. this.entryId = params.entryId;
  102. this.entryNumber = splitEntryId(this.entryId).entryNumber;
  103. }
  104. private async initialize() {
  105. const metadata = await this.api.getMetadata(this.source, this.entryId);
  106. this.metadata = new MetadataWrapper(metadata);
  107. this.pdbs = await ExternalAPIs.getPdbIdsForEmdbEntry(this.metadata.raw.grid.general.source_db_id ?? this.entryId);
  108. // TODO use Asset?
  109. }
  110. static async create(plugin: PluginContext, params: VolsegEntryParamValues) {
  111. const result = new VolsegEntryData(plugin, params);
  112. await result.initialize();
  113. return result;
  114. }
  115. async register(ref: string) {
  116. this.ref = ref;
  117. this.plugin.managers.lociLabels.addProvider(this.labelProvider);
  118. try {
  119. const params = this.getStateNode().obj?.data;
  120. if (params) {
  121. this.currentState.next(params);
  122. }
  123. } catch {
  124. // do nothing
  125. }
  126. this.subscribeObservable(this.plugin.state.data.events.cell.stateUpdated, e => {
  127. try { (this.getStateNode()); } catch { return; } // if state not does not exist yet
  128. if (e.cell.transform.ref === this.getStateNode().transform.ref) {
  129. const newState = this.getStateNode().obj?.data;
  130. if (newState && !shallowEqualObjects(newState, this.currentState.value)) { // avoid repeated update
  131. this.currentState.next(newState);
  132. }
  133. }
  134. });
  135. this.subscribeObservable(this.plugin.behaviors.interaction.click, async e => {
  136. const loci = e.current.loci;
  137. const clickedSegment = this.getSegmentIdFromLoci(loci);
  138. if (clickedSegment === undefined) return;
  139. if (clickedSegment === this.currentState.value.selectedSegment) {
  140. this.actionSelectSegment(undefined);
  141. } else {
  142. this.actionSelectSegment(clickedSegment);
  143. }
  144. });
  145. this.subscribeObservable(
  146. this.highlightRequest.pipe(throttleTime(50, undefined, { leading: true, trailing: true })),
  147. async segment => await this.highlightSegment(segment)
  148. );
  149. this.subscribeObservable(
  150. this.currentState.pipe(distinctUntilChanged((a, b) => a.selectedSegment === b.selectedSegment)),
  151. async state => {
  152. if (VolsegGlobalStateData.getGlobalState(this.plugin)?.selectionMode) await this.selectSegment(state.selectedSegment);
  153. }
  154. );
  155. }
  156. async unregister() {
  157. this.plugin.managers.lociLabels.removeProvider(this.labelProvider);
  158. }
  159. async loadVolume() {
  160. const result = await this.volumeData.loadVolume();
  161. if (result) {
  162. const isovalue = result.isovalue.kind === 'relative' ? result.isovalue.relativeValue : result.isovalue.absoluteValue;
  163. await this.updateStateNode({ volumeIsovalueKind: result.isovalue.kind, volumeIsovalueValue: isovalue });
  164. }
  165. }
  166. async loadSegmentations() {
  167. await this.latticeSegmentationData.loadSegmentation();
  168. await this.meshSegmentationData.loadSegmentation();
  169. await this.actionShowSegments(this.metadata.allSegmentIds);
  170. }
  171. actionHighlightSegment(segment?: Segment) {
  172. this.highlightRequest.next(segment);
  173. }
  174. async actionToggleSegment(segment: number) {
  175. const current = this.currentState.value.visibleSegments.map(seg => seg.segmentId);
  176. if (current.includes(segment)) {
  177. await this.actionShowSegments(current.filter(s => s !== segment));
  178. } else {
  179. await this.actionShowSegments([...current, segment]);
  180. }
  181. }
  182. async actionToggleAllSegments() {
  183. const current = this.currentState.value.visibleSegments.map(seg => seg.segmentId);
  184. if (current.length !== this.metadata.allSegments.length) {
  185. await this.actionShowSegments(this.metadata.allSegmentIds);
  186. } else {
  187. await this.actionShowSegments([]);
  188. }
  189. }
  190. async actionSelectSegment(segment?: number) {
  191. if (segment !== undefined && this.currentState.value.visibleSegments.find(s => s.segmentId === segment) === undefined) {
  192. // first make the segment visible if it is not
  193. await this.actionToggleSegment(segment);
  194. }
  195. await this.updateStateNode({ selectedSegment: segment });
  196. }
  197. async actionSetOpacity(opacity: number) {
  198. if (opacity === this.getStateNode().obj?.data.segmentOpacity) return;
  199. this.latticeSegmentationData.updateOpacity(opacity);
  200. this.meshSegmentationData.updateOpacity(opacity);
  201. await this.updateStateNode({ segmentOpacity: opacity });
  202. }
  203. async actionShowFittedModel(pdbIds: string[]) {
  204. await this.modelData.showPdbs(pdbIds);
  205. await this.updateStateNode({ visibleModels: pdbIds.map(pdbId => ({ pdbId: pdbId })) });
  206. }
  207. async actionSetVolumeVisual(type: 'isosurface' | 'direct-volume' | 'off') {
  208. await this.volumeData.setVolumeVisual(type);
  209. await this.updateStateNode({ volumeType: type });
  210. }
  211. async actionUpdateVolumeVisual(params: SimpleVolumeParamValues) {
  212. await this.volumeData.updateVolumeVisual(params);
  213. await this.updateStateNode({
  214. volumeType: params.volumeType,
  215. volumeOpacity: params.opacity,
  216. });
  217. }
  218. private async actionShowSegments(segments: number[]) {
  219. await this.latticeSegmentationData.showSegments(segments);
  220. await this.meshSegmentationData.showSegments(segments);
  221. await this.updateStateNode({ visibleSegments: segments.map(s => ({ segmentId: s })) });
  222. }
  223. private async highlightSegment(segment?: Segment) {
  224. await PluginCommands.Interactivity.ClearHighlights(this.plugin);
  225. if (segment) {
  226. await this.latticeSegmentationData.highlightSegment(segment);
  227. await this.meshSegmentationData.highlightSegment(segment);
  228. }
  229. }
  230. private async selectSegment(segment: number) {
  231. this.plugin.managers.interactivity.lociSelects.deselectAll();
  232. await this.latticeSegmentationData.selectSegment(segment);
  233. await this.meshSegmentationData.selectSegment(segment);
  234. await this.highlightSegment();
  235. }
  236. private async updateStateNode(params: Partial<VolsegStateData>) {
  237. const oldParams = this.getStateNode().transform.params;
  238. const newParams = { ...oldParams, ...params };
  239. const state = this.plugin.state.data;
  240. const update = state.build().to(this.getStateNode().transform.ref).update(newParams);
  241. await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
  242. }
  243. /** Find the nodes under this entry root which have all of the given tags. */
  244. findNodesByTags(...tags: string[]) {
  245. return this.plugin.state.data.selectQ(q => {
  246. let builder = q.byRef(this.ref).subtree();
  247. for (const tag of tags) builder = builder.withTag(tag);
  248. return builder;
  249. });
  250. }
  251. newUpdate() {
  252. if (this.ref !== '') {
  253. return this.plugin.build().to(this.ref);
  254. } else {
  255. return this.plugin.build().toRoot();
  256. }
  257. }
  258. private readonly labelProvider: LociLabelProvider = {
  259. label: (loci: Loci): string | undefined => {
  260. const segmentId = this.getSegmentIdFromLoci(loci);
  261. if (segmentId === undefined) return;
  262. const segment = this.metadata.getSegment(segmentId);
  263. if (!segment) return;
  264. const annotLabels = segment.biological_annotation.external_references.map(annot => `${applyEllipsis(annot.label)} [${annot.resource}:${annot.accession}]`);
  265. if (annotLabels.length === 0) return;
  266. if (annotLabels.length > MAX_ANNOTATIONS_IN_LABEL + 1) {
  267. const nHidden = annotLabels.length - MAX_ANNOTATIONS_IN_LABEL;
  268. annotLabels.length = MAX_ANNOTATIONS_IN_LABEL;
  269. annotLabels.push(`(${nHidden} more annotations, click on the segment to see all)`);
  270. }
  271. return '<hr class="msp-highlight-info-hr"/>' + annotLabels.filter(isDefined).join('<br/>');
  272. }
  273. };
  274. private getSegmentIdFromLoci(loci: Loci): number | undefined {
  275. if (Volume.Segment.isLoci(loci) && loci.volume._propertyData.ownerId === this.ref) {
  276. if (loci.segments.length === 1) {
  277. return loci.segments[0];
  278. }
  279. }
  280. if (ShapeGroup.isLoci(loci)) {
  281. const meshData = (loci.shape.sourceData ?? {}) as MeshlistData;
  282. if (meshData.ownerId === this.ref && meshData.segmentId !== undefined) {
  283. return meshData.segmentId;
  284. }
  285. }
  286. }
  287. async setTryUseGpu(tryUseGpu: boolean) {
  288. await Promise.all([
  289. this.volumeData.setTryUseGpu(tryUseGpu),
  290. this.latticeSegmentationData.setTryUseGpu(tryUseGpu),
  291. ]);
  292. }
  293. async setSelectionMode(selectSegments: boolean) {
  294. if (selectSegments) {
  295. await this.selectSegment(this.currentState.value.selectedSegment);
  296. } else {
  297. this.plugin.managers.interactivity.lociSelects.deselectAll();
  298. }
  299. }
  300. }