@datagrok/bio 2.12.0 → 2.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "id": {
23
23
  "description": "Unique ID for the monomer. There is no meaning associated with this ID value.",
24
- "type": "integer"
24
+ "type": ["string", "integer"]
25
25
  },
26
26
  "rgroups": {
27
27
  "description": "An array of the monomer R groups and required information.",
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "Aleksandr Tanas",
6
6
  "email": "atanas@datagrok.ai"
7
7
  },
8
- "version": "2.12.0",
8
+ "version": "2.12.2",
9
9
  "description": "Bioinformatics support (import/export of sequences, conversion, visualization, analysis). [See more](https://github.com/datagrok-ai/public/blob/master/packages/Bio/README.md) for details.",
10
10
  "repository": {
11
11
  "type": "git",
@@ -36,11 +36,11 @@
36
36
  "@biowasm/aioli": "^3.1.0",
37
37
  "@datagrok-libraries/bio": "5.40.0",
38
38
  "@datagrok-libraries/chem-meta": "^1.2.3",
39
- "@datagrok-libraries/ml": "^6.4.12",
39
+ "@datagrok-libraries/ml": "^6.4.13",
40
40
  "@datagrok-libraries/tutorials": "^1.3.12",
41
41
  "ajv": "^8.12.0",
42
42
  "ajv-errors": "^3.0.0",
43
- "@datagrok-libraries/utils": "^4.1.44",
43
+ "@datagrok-libraries/utils": "^4.2.0",
44
44
  "@datagrok-libraries/math": "^1.0.7",
45
45
  "cash-dom": "^8.0.0",
46
46
  "css-loader": "^6.7.3",
@@ -20,7 +20,7 @@ export async function getEncodedSeqSpaceCol(
20
20
  const rowCount = seqCol.length;
21
21
  const sh = SeqHandler.forColumn(seqCol);
22
22
  const encList = Array<string>(rowCount);
23
- let charCodeCounter = 36;
23
+ let charCodeCounter = 1; // start at 1, 0 is reserved for null.
24
24
  const charCodeMap = new Map<string, string>();
25
25
  const seqColCats = seqCol.categories;
26
26
  const seqColRawData = seqCol.getRawData();
@@ -28,7 +28,7 @@ export async function getEncodedSeqSpaceCol(
28
28
  const catI = seqColRawData[rowIdx];
29
29
  const seq = seqColCats[catI];
30
30
  if (seq === null || seqCol.isNone(rowIdx)) {
31
- // @ts-ignore
31
+ //@ts-ignore
32
32
  encList[rowIdx] = null;
33
33
  continue;
34
34
  }
@@ -44,30 +44,13 @@ export async function getEncodedSeqSpaceCol(
44
44
  }
45
45
  }
46
46
  let options = {} as mmDistanceFunctionArgs;
47
- if (similarityMetric === MmDistanceFunctionsNames.MONOMER_CHEMICAL_DISTANCE) {
47
+ if (
48
+ similarityMetric === MmDistanceFunctionsNames.MONOMER_CHEMICAL_DISTANCE ||
49
+ similarityMetric === MmDistanceFunctionsNames.NEEDLEMANN_WUNSCH
50
+ ) {
48
51
  const monomers = Array.from(charCodeMap.keys());
49
52
  const monomerRes = await getMonomerSubstitutionMatrix(monomers, fingerprintType);
50
- // the susbstitution matrix contains similarity, but we need distances
51
- monomerRes.scoringMatrix.forEach((row, i) => {
52
- row.forEach((val, j) => {
53
- monomerRes.scoringMatrix[i][j] = 1 - val;
54
- });
55
- });
56
- const monomerHashToMatrixMap: { [_: string]: number } = {};
57
- Object.entries(monomerRes.alphabetIndexes).forEach(([key, value]) => {
58
- monomerHashToMatrixMap[charCodeMap.get(key)!] = value;
59
- });
60
- // sets distance function args in place.
61
- options = {scoringMatrix: monomerRes.scoringMatrix, alphabetIndexes: monomerHashToMatrixMap};
62
- } else if (similarityMetric === MmDistanceFunctionsNames.NEEDLEMANN_WUNSCH) {
63
- const monomers = Array.from(charCodeMap.keys());
64
- const monomerRes = await getMonomerSubstitutionMatrix(monomers, fingerprintType);
65
- // the susbstitution matrix contains similarity, but we need distances
66
- // monomerRes.scoringMatrix.forEach((row, i) => {
67
- // row.forEach((val, j) => {
68
- // monomerRes.scoringMatrix[i][j] = 1 - val;
69
- // });
70
- // });
53
+
71
54
  const monomerHashToMatrixMap: { [_: string]: number } = {};
72
55
  Object.entries(monomerRes.alphabetIndexes).forEach(([key, value]) => {
73
56
  monomerHashToMatrixMap[charCodeMap.get(key)!] = value;
package/src/package.ts CHANGED
@@ -59,7 +59,7 @@ import {BioPackage, BioPackageProperties} from './package-types';
59
59
  import {getCompositionAnalysisWidget} from './widgets/composition-analysis-widget';
60
60
  import {MacromoleculeColumnWidget} from './utils/macromolecule-column-widget';
61
61
  import {addCopyMenuUI} from './utils/context-menu';
62
- import {PolyTool} from './utils/poly-tool/ui';
62
+ import {getPolyToolDialog} from './utils/poly-tool/ui';
63
63
  import {PolyToolCsvLibHandler} from './utils/poly-tool/csv-to-json-monomer-lib-converter';
64
64
  import {_setPeptideColumn} from './utils/poly-tool/utils';
65
65
  import {getRegionDo} from './utils/get-region';
@@ -471,9 +471,8 @@ export async function activityCliffs(table: DG.DataFrame, molecules: DG.Column<s
471
471
  })
472
472
  .onCancel(() => { resolve(undefined); })
473
473
  .show();
474
- } else {
474
+ } else
475
475
  runCliffs().then((res) => resolve(res)).catch((err) => reject(err));
476
- }
477
476
  }).catch((err: any) => {
478
477
  const [errMsg, errStack] = errInfo(err);
479
478
  _package.logger.error(errMsg, undefined, errStack);
@@ -647,9 +646,8 @@ export async function compositionAnalysis(): Promise<void> {
647
646
  await handler(col);
648
647
  })
649
648
  .show();
650
- } else {
649
+ } else
651
650
  col = colList[0];
652
- }
653
651
 
654
652
  if (!col)
655
653
  return;
@@ -690,10 +688,9 @@ export function convertDialog() {
690
688
  //name: polyTool
691
689
  //description: Perform cyclization of polymers
692
690
  export async function polyTool(): Promise<void> {
693
- const polytool = new PolyTool();
694
691
  let dialog: DG.Dialog;
695
692
  try {
696
- dialog = await polytool.getPolyToolDialog();
693
+ dialog = await getPolyToolDialog();
697
694
  dialog.show();
698
695
  } catch (err: any) {
699
696
  grok.shell.warning('To run PolyTool, open a dataframe with macromolecules');
@@ -2,6 +2,10 @@ import * as grok from 'datagrok-api/grok';
2
2
  import * as ui from 'datagrok-api/ui';
3
3
  import * as DG from 'datagrok-api/dg';
4
4
 
5
+ import $ from 'cash-dom';
6
+ import wu from 'wu';
7
+
8
+
5
9
  import {after, before, category, test, expect, delay, testEvent, awaitCheck} from '@datagrok-libraries/utils/src/test';
6
10
  import {getMonomerLibHelper, IMonomerLibHelper} from '@datagrok-libraries/bio/src/monomer-works/monomer-utils';
7
11
  import {
@@ -19,7 +23,7 @@ import {HelmBioFilter} from '../widgets/bio-substructure-filter-helm';
19
23
  import {_package} from '../package-test';
20
24
 
21
25
 
22
- category('substructureFilters', async () => {
26
+ category('bio-substructure-filters', async () => {
23
27
  let monomerLibHelper: IMonomerLibHelper;
24
28
  /** Backup actual user's monomer libraries settings */
25
29
  let userLibSettings: UserLibSettings;
@@ -305,6 +309,144 @@ category('substructureFilters', async () => {
305
309
  await Promise.all([f1.awaitRendered(), f2.awaitRendered()]);
306
310
  await awaitGrid(view.grid);
307
311
  });
312
+
313
+ // two seq columns
314
+
315
+ const twoColumnsCsv: string = `id,seq1,seq2,trueSeq1,trueSeq2
316
+ 0,CGGCTACGGC,ATTGCATTCG,0,1,
317
+ 1,CGGCTGCCGC,ATAGCATTCG,1,1,
318
+ 2,CGGCTGCGCC,AATGCATACG,1,0,
319
+ 3,CGGCTGCATT,TTTGCATTCG,1,1,
320
+ 4,CGGCTGCATT,AAAGCATACG,1,0,
321
+ `;
322
+
323
+ test('two-columns-fasta', async () => {
324
+ const df = DG.DataFrame.fromCsv(twoColumnsCsv);
325
+ await grok.data.detectSemanticTypes(df);
326
+ const view = grok.shell.addTableView(df);
327
+
328
+ const fSeq1ColName: string = 'seq1';
329
+ const fSeq1SubStr: string = 'CGGCTG';
330
+ const fSeq1Trues: number[] = df.getCol('trueSeq1').toList();
331
+
332
+ const fSeq2ColName: string = 'seq2';
333
+ const fSeq2SubStr: string = 'GCATT';
334
+ const fSeq2Trues: number[] = df.getCol('trueSeq2').toList();
335
+
336
+ //const seq2Filter = new BioSubstructureFilter();
337
+ const filterList: any[] = [
338
+ {type: 'Bio:bioSubstructureFilter', columnName: fSeq1ColName},
339
+ {type: 'Bio:bioSubstructureFilter', columnName: fSeq2ColName},
340
+ ];
341
+ const fg = (await df.plot.fromType(DG.VIEWER.FILTERS,
342
+ {filters: filterList})) as DG.FilterGroup;
343
+ view.dockManager.dock(fg, DG.DOCK_TYPE.LEFT);
344
+ await delay(100);
345
+ await awaitGrid(view.grid);
346
+
347
+ const seq1Filter = fg.filters[0] as BioSubstructureFilter;
348
+ const seq2Filter = fg.filters[1] as BioSubstructureFilter;
349
+ expect(seq1Filter.column!.name, fSeq1ColName);
350
+ expect(seq2Filter.column!.name, fSeq2ColName);
351
+
352
+ const seq1Bf = seq1Filter.bioFilter as FastaBioFilter;
353
+ const seq2Bf = seq2Filter.bioFilter as FastaBioFilter;
354
+
355
+ await testEvent(df.onRowsFiltered, () => {}, () => {
356
+ seq1Bf.props = new BioFilterProps(fSeq1SubStr);
357
+ }, 1000);
358
+ await testEvent(df.onRowsFiltered, () => {}, () => {
359
+ seq2Bf.props = new BioFilterProps('');
360
+ }, 1000, 'testEvent onRowsFiltered on seq1');
361
+ expect(df.filter.trueCount, fSeq1Trues.filter((v) => v === 1).length);
362
+ expect(df.filter.toBinaryString(), fSeq1Trues.map((v) => v.toString()).join(''));
363
+
364
+ await testEvent(df.onRowsFiltered, () => {}, () => {
365
+ seq1Bf.props = new BioFilterProps('');
366
+ }, 1000);
367
+ await testEvent(df.onRowsFiltered, () => {}, () => {
368
+ seq2Bf.props = new BioFilterProps(fSeq2SubStr);
369
+ }, 1000, 'testEvent onRowsFiltered on seq2');
370
+ expect(df.filter.trueCount, fSeq2Trues.filter((v) => v === 1).length);
371
+ expect(df.filter.toBinaryString(), fSeq2Trues.map((v) => v.toString()).join(''));
372
+
373
+ await testEvent(df.onRowsFiltered, () => {}, () => {
374
+ seq1Bf.props = new BioFilterProps('');
375
+ }, 1000);
376
+ await testEvent(df.onRowsFiltered, () => {}, () => {
377
+ seq2Bf.props = new BioFilterProps('');
378
+ }, 1000, 'testEvent onRowsFiltered on neither');
379
+ expect(df.filter.trueCount, df.rowCount);
380
+
381
+ await testEvent(df.onRowsFiltered, () => {}, () => {
382
+ seq1Bf.props = new BioFilterProps(fSeq1SubStr);
383
+ }, 5000);
384
+ await testEvent(df.onRowsFiltered, () => {}, () => {
385
+ seq2Bf.props = new BioFilterProps(fSeq2SubStr);
386
+ }, 5000, 'testEvent onRowsFiltered on both');
387
+ const bothTrues: number[] = wu.count(0).take(df.rowCount)
388
+ .map((rowI) => fSeq1Trues[rowI] * fSeq2Trues[rowI]).toArray();
389
+ expect(df.filter.trueCount, bothTrues.filter((v) => v === 1).length);
390
+ expect(df.filter.toBinaryString(), bothTrues.map((v) => v.toString()).join(''));
391
+
392
+ await Promise.all([seq1Filter.awaitRendered(), seq2Filter.awaitRendered(), awaitGrid(view.grid)]);
393
+ });
394
+
395
+ // -- reset --
396
+
397
+ test('reset-fasta', async () => {
398
+ const df = await readDataframe('tests/filter_FASTA.csv');
399
+ await grok.data.detectSemanticTypes(df);
400
+ const view = grok.shell.addTableView(df);
401
+
402
+ const fSeqColName: string = 'fasta';
403
+ const fSubStr: string = 'MD';
404
+ const fTrueCount: number = 3;
405
+
406
+ const filterList = [{type: 'Bio:bioSubstructureFilter', columnName: fSeqColName}];
407
+ const fg = (await df.plot.fromType(DG.VIEWER.FILTERS,
408
+ {filters: filterList})) as DG.FilterGroup;
409
+ view.dockManager.dock(fg, DG.DOCK_TYPE.LEFT);
410
+ await delay(100);
411
+ await awaitGrid(view.grid);
412
+
413
+ const seqFilter = fg.filters[0] as BioSubstructureFilter;
414
+ const seqBf = seqFilter.bioFilter as FastaBioFilter;
415
+ await testEvent(df.onRowsFiltered, () => {}, () => {
416
+ seqBf.props = new BioFilterProps(fSubStr);
417
+ }, 1000, 'testEvent onRowsFiltered');
418
+ expect(df.filter.trueCount, fTrueCount);
419
+ expect(seqBf.props.substructure, fSubStr);
420
+ expect(seqBf.substructureInput.value, fSubStr);
421
+
422
+ const fgResetIconEl: HTMLElement = $(fg.root).find('i[name="icon-arrow-rotate-left"]')[0] as HTMLElement;
423
+ fgResetIconEl.click();
424
+ await delay(100);
425
+ await awaitGrid(view.grid);
426
+ expect(seqBf.props.substructure, '');
427
+ expect(seqBf.substructureInput.value, '');
428
+ });
429
+
430
+ test('reopen', async () => {
431
+ const df = await _package.files.readCsv('tests/filter_FASTA.csv');
432
+ const view = grok.shell.addTableView(df);
433
+
434
+ const filterList = [{type: 'Bio:bioSubstructureFilter', columnName: 'fasta'}];
435
+
436
+ const fg1 = (await df.plot.fromType(DG.VIEWER.FILTERS,
437
+ {filters: filterList})) as DG.FilterGroup;
438
+ const fg1Dn = view.dockManager.dock(fg1, DG.DOCK_TYPE.LEFT);
439
+ await delay(100);
440
+ await awaitGrid(view.grid);
441
+ fg1.close();
442
+ await awaitGrid(view.grid);
443
+
444
+ const fg2 = (await df.plot.fromType(DG.VIEWER.FILTERS,
445
+ {filters: filterList})) as DG.FilterGroup;
446
+ const fg2Dn = view.dockManager.dock(fg2, DG.DOCK_TYPE.LEFT);
447
+ await delay(100);
448
+ await awaitGrid(view.grid);
449
+ });
308
450
  });
309
451
 
310
452
  async function createFilter(colName: string, df: DG.DataFrame): Promise<BioSubstructureFilter> {
@@ -5,144 +5,55 @@ import * as DG from 'datagrok-api/dg';
5
5
 
6
6
  import {addTransformedColumn} from './transformation';
7
7
  import {RULES_PATH, RULES_STORAGE_NAME} from './transformation';
8
+ import {ActiveFiles} from '@datagrok-libraries/utils/src/settings/active-files-base';
8
9
 
9
- export type UserRuleSettings = {
10
- included: string[],
11
- notIncluded: string[],
12
- }
13
-
14
- async function getAllAvailableRuleFiles(): Promise<string[]> {
15
- const list = await grok.dapi.files.list(RULES_PATH);
16
- const paths = list.map((fileInfo) => {
17
- return fileInfo.fullPath;
18
- });
19
-
20
- return paths;
21
- }
22
-
23
- async function getUserRulesSettings(): Promise<UserRuleSettings> {
24
- const resStr: string = await grok.dapi.userDataStorage.getValue(RULES_STORAGE_NAME, 'Settings', true);
25
- const res = resStr ? JSON.parse(resStr) : {included: [], enotIncludedxplicit: []};
26
-
27
- res.included = res.included instanceof Array ? res.included : [];
28
- res.notIncluded = res.notIncluded instanceof Array ? res.notIncluded : [];
29
-
30
- return res!;
31
- }
32
-
33
- async function setUserLibSettings(value: UserRuleSettings): Promise<void> {
34
- await grok.dapi.userDataStorage.postValue(RULES_STORAGE_NAME, 'Settings', JSON.stringify(value), true);
35
- }
36
-
37
- export class PolyTool {
38
- ruleFiles: string[];
39
- userRuleSettings: UserRuleSettings;
40
- ruleFilesInputs: HTMLDivElement;// DG.InputBase<boolean | null>[];
41
- dialog: DG.Dialog;
42
-
43
- constructor() {
44
- this.ruleFiles = [];
45
- this.userRuleSettings = {included: [], notIncluded: []};
46
- }
47
-
48
- private updateRulesSelectionStatus(ruleFileName: string, isSelected: boolean): void {
49
- const isRuleFileSelected = this.userRuleSettings.included.includes(ruleFileName);
50
-
51
- if (!isRuleFileSelected && isSelected) {
52
- this.userRuleSettings.included.push(ruleFileName);
53
- this.userRuleSettings.included = this.userRuleSettings.included.sort();
54
-
55
- const index = this.userRuleSettings.notIncluded.indexOf(ruleFileName);
56
- if (index > -1)
57
- this.userRuleSettings.notIncluded.splice(index, 1);
58
- } else {
59
- const index = this.userRuleSettings.included.indexOf(ruleFileName);
60
- if (index > -1)
61
- this.userRuleSettings.included.splice(index, 1);
62
-
63
- this.userRuleSettings.notIncluded.push(ruleFileName);
64
- this.userRuleSettings.notIncluded = this.userRuleSettings.notIncluded.sort();
65
- }
66
-
67
- setUserLibSettings(this.userRuleSettings);
10
+ class RuleInputs extends ActiveFiles {
11
+ constructor(path: string, userStorageName: string, ext: string ) {
12
+ super(path, userStorageName, ext);
68
13
  }
14
+ }
69
15
 
70
- private getAddButton(): HTMLButtonElement {
71
- return ui.button('ADD RULES', () => {
72
- DG.Utils.openFile({
73
- accept: '.csv',
74
- open: async (selectedFile) => {
75
- const content = await selectedFile.text();
76
- await grok.dapi.files.writeAsText(RULES_PATH + `${selectedFile.name}`, content);
77
- this.updateRulesSelectionStatus(selectedFile.name, false);
78
- const cb = ui.boolInput(
79
- selectedFile.name,
80
- false,
81
- (isSelected: boolean) => this.updateRulesSelectionStatus(RULES_PATH + `${selectedFile.name}`, isSelected)
82
- );
83
- this.ruleFilesInputs.append(cb.root);
84
- },
85
- });
16
+ export async function getPolyToolDialog(): Promise<DG.Dialog> {
17
+ const targetColumns = grok.shell.t.columns.bySemTypeAll(DG.SEMTYPE.MACROMOLECULE);
18
+ if (!targetColumns)
19
+ throw new Error('No dataframe with macromolecule columns open');
20
+
21
+ const targetColumnInput = ui.columnInput(
22
+ 'Column', grok.shell.t, targetColumns[0], null,
23
+ {filter: (col: DG.Column) => col.semType === DG.SEMTYPE.MACROMOLECULE}
24
+ );
25
+
26
+ const generateHelmChoiceInput = ui.boolInput('Get HELM', true);
27
+ ui.tooltip.bind(generateHelmChoiceInput.root, 'Add HELM column');
28
+
29
+ const chiralityEngineInput = ui.boolInput('Chirality engine', false);
30
+ const ruleInputs = new RuleInputs(RULES_PATH, RULES_STORAGE_NAME, '.csv');
31
+ const rulesForm = await ruleInputs.getForm();
32
+
33
+ const div = ui.div([
34
+ targetColumnInput,
35
+ generateHelmChoiceInput,
36
+ chiralityEngineInput,
37
+ 'Rules used',
38
+ rulesForm
39
+ ]);
40
+
41
+ const dialog = ui.dialog('Poly Tool')
42
+ .add(div)
43
+ .onOK(async () => {
44
+ const molCol = targetColumnInput.value;
45
+ if (!molCol) {
46
+ grok.shell.warning('No marcomolecule column chosen!');
47
+ return;
48
+ }
49
+
50
+ const files = await ruleInputs.getActive();
51
+
52
+ addTransformedColumn(molCol!,
53
+ generateHelmChoiceInput.value!,
54
+ files,
55
+ chiralityEngineInput.value!);
86
56
  });
87
- }
88
-
89
- private async getRuleFilesBlock(): Promise<DG.InputBase<boolean | null>[]> {
90
- this.ruleFiles = await getAllAvailableRuleFiles();
91
- this.userRuleSettings = await getUserRulesSettings();
92
- const cBoxes: DG.InputBase<boolean | null>[] = [];
93
-
94
- for (let i = 0; i < this.ruleFiles.length; i++) {
95
- const ruleFileName = this.ruleFiles[i];
96
- const isRuleFileSelected = this.userRuleSettings.included.includes(ruleFileName);
97
- const cb = ui.boolInput(
98
- ruleFileName.replace(RULES_PATH, ''),
99
- isRuleFileSelected,
100
- (isSelected: boolean) => this.updateRulesSelectionStatus(ruleFileName, isSelected)
101
- );
102
-
103
- cBoxes.push(cb);
104
- }
105
- return cBoxes;
106
- }
107
57
 
108
- async getPolyToolDialog(): Promise<DG.Dialog> {
109
- const targetColumns = grok.shell.t.columns.bySemTypeAll(DG.SEMTYPE.MACROMOLECULE);
110
- if (!targetColumns)
111
- throw new Error('No dataframe with macromolecule columns open');
112
-
113
- const targetColumnInput = ui.columnInput(
114
- 'Column', grok.shell.t, targetColumns[0], null,
115
- {filter: (col: DG.Column) => col.semType === DG.SEMTYPE.MACROMOLECULE}
116
- );
117
-
118
- const generateHelmChoiceInput = ui.boolInput('Get HELM', true);
119
- ui.tooltip.bind(generateHelmChoiceInput.root, 'Add HELM column');
120
-
121
- const addButton = this.getAddButton();
122
- this.ruleFilesInputs = ui.div(await this.getRuleFilesBlock());
123
- //const rulesFiles = ui.div(this.ruleFilesInputs);
124
- const chiralityEngineInput = ui.boolInput('Chirality engine', false);
125
-
126
- const div = ui.div([
127
- targetColumnInput,
128
- generateHelmChoiceInput,
129
- chiralityEngineInput,
130
- 'Rules used',
131
- this.ruleFilesInputs,
132
- addButton
133
- ]);
134
-
135
- this.dialog = ui.dialog('Poly Tool')
136
- .add(div)
137
- .onOK(async () => {
138
- const molCol = targetColumnInput.value;
139
- if (!molCol) {
140
- grok.shell.warning('No marcomolecule column chosen!');
141
- return;
142
- }
143
- addTransformedColumn(molCol!, generateHelmChoiceInput.value!, this.userRuleSettings.included, chiralityEngineInput.value!);
144
- });
145
-
146
- return this.dialog;
147
- }
58
+ return dialog;
148
59
  }
@@ -6,50 +6,24 @@ import {Observable, Subject, Unsubscribable} from 'rxjs';
6
6
  import {_package} from '../package';
7
7
 
8
8
  export interface IFilterProps {
9
- get onChanged(): Observable<void>;
10
-
11
- save(): object;
12
- apply(propsObj: object): void;
13
9
  }
14
10
 
15
11
  /** Fasta and Helm */
16
12
  export class BioFilterProps implements IFilterProps {
17
- private _onChanged: Subject<void> = new Subject<void>();
18
-
19
- get onChanged(): Observable<void> { return this._onChanged; }
20
-
21
13
  constructor(
22
- public substructure: string
14
+ public readonly substructure: string,
15
+ /** Pass false from an inheritors constructor, at the end set true. */ protected readOnly: boolean = true,
23
16
  ) {
24
17
  return new Proxy(this, {
25
18
  set: (target: any, key: string | symbol, value: any) => {
26
19
  _package.logger.debug(`BioFilterProps.set ${key.toString()}( '${value}' )`);
20
+ if (this.readOnly)
21
+ throw new Error('Properties are immutable.');
27
22
  target[key] = value;
28
- this._onChanged.next();
29
23
  return true;
30
24
  }
31
25
  });
32
26
  }
33
-
34
- save(): object {
35
- const propsObj = {};
36
- for (const [key, value] of Object.entries(this)) {
37
- if (key !== '_onChanged') {
38
- // @ts-ignore
39
- propsObj[key] = this[key];
40
- }
41
- }
42
- return propsObj;
43
- }
44
-
45
- apply(propsObj: object) {
46
- for (const [key, value] of Object.entries(this)) {
47
- if (key !== '_onChanged') {
48
- // @ts-ignore
49
- this[key] = propsObj[key];
50
- }
51
- }
52
- }
53
27
  }
54
28
 
55
29
  export interface IBioFilter {
@@ -58,6 +32,9 @@ export interface IBioFilter {
58
32
  get props(): IFilterProps;
59
33
  set props(value: IFilterProps);
60
34
 
35
+ applyProps(props: IFilterProps): void;
36
+ saveProps(): IFilterProps;
37
+
61
38
  get onChanged(): Observable<void>;
62
39
  get filterPanel(): HTMLElement;
63
40
  get filterSummary(): string;
@@ -79,7 +56,6 @@ export abstract class BioFilterBase<TProps extends BioFilterProps> implements IB
79
56
 
80
57
  private _props: TProps;
81
58
  protected _propsChanging: boolean = false;
82
- private _propsOnChangedSub: Unsubscribable | null = null;
83
59
 
84
60
  abstract get type(): string;
85
61
 
@@ -91,33 +67,31 @@ export abstract class BioFilterBase<TProps extends BioFilterProps> implements IB
91
67
  set props(value: TProps) {
92
68
  this._propsChanging = true;
93
69
  try {
94
- if (this._propsOnChangedSub) {
95
- this._propsOnChangedSub.unsubscribe();
96
- this._propsOnChangedSub = null;
97
- }
98
70
  this._props = value;
99
71
  this.applyProps();
100
72
  this.onChanged.next();
101
- this._propsOnChangedSub = this._props.onChanged
102
- .subscribe(() => {
103
- this.onChanged.next();
104
- });
105
73
  } finally {
106
74
  this._propsChanging = false;
107
75
  }
108
76
  };
109
77
 
110
- abstract attach(): Promise<void>;
111
-
112
- async detach(): Promise<void> {
113
- if (this._propsOnChangedSub) {
114
- this._propsOnChangedSub.unsubscribe();
115
- this._propsOnChangedSub = null;
78
+ saveProps(): IFilterProps {
79
+ const propsObj = {};
80
+ for (const [key, value] of Object.entries(this.props)) {
81
+ if (key !== '_onChanged') {
82
+ // @ts-ignore
83
+ propsObj[key] = this.props[key];
84
+ }
116
85
  }
86
+ return propsObj;
117
87
  }
118
88
 
119
89
  abstract applyProps(): void;
120
90
 
91
+ abstract attach(): Promise<void>;
92
+
93
+ async detach(): Promise<void> { }
94
+
121
95
  get filterSummary(): string { return this.props.substructure; };
122
96
 
123
97
  get isFiltering(): boolean { return this.props.substructure !== ''; }