@datagrok/bio 2.14.2 → 2.15.0
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.
- package/CHANGELOG.md +42 -0
- package/css/monomer-manager.css +66 -0
- package/detectors.js +7 -2
- package/dist/111.js +1 -1
- package/dist/111.js.map +1 -1
- package/dist/234.js +1 -1
- package/dist/234.js.map +1 -1
- package/dist/242.js.map +1 -1
- package/dist/603.js +1 -1
- package/dist/603.js.map +1 -1
- package/dist/682.js +1 -1
- package/dist/682.js.map +1 -1
- package/dist/705.js +1 -1
- package/dist/705.js.map +1 -1
- package/dist/778.js +1 -1
- package/dist/778.js.map +1 -1
- package/dist/793.js +1 -1
- package/dist/793.js.map +1 -1
- package/dist/801.js +2 -0
- package/dist/801.js.map +1 -0
- package/dist/950.js +1 -1
- package/dist/950.js.map +1 -1
- package/dist/980.js +2 -0
- package/dist/980.js.map +1 -0
- package/dist/package-test.js +6 -6
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +5 -5
- package/dist/package.js.map +1 -1
- package/files/monomer-libraries/polytool-lib.json +48 -0
- package/files/monomer-libraries/sample-lib-Aca-colored.json +2 -2
- package/package.json +20 -12
- package/src/analysis/sequence-space.ts +2 -1
- package/src/demo/bio05-helm-msa-sequence-space.ts +1 -1
- package/src/package-test.ts +3 -1
- package/src/package-types.ts +9 -1
- package/src/package.ts +77 -33
- package/src/seq_align.ts +1 -1
- package/src/substructure-search/substructure-search.ts +2 -2
- package/src/tests/WebLogo-project-tests.ts +3 -4
- package/src/tests/activity-cliffs-tests.ts +5 -18
- package/src/tests/detectors-benchmark-tests.ts +24 -9
- package/src/tests/mm-distance-tests.ts +4 -3
- package/src/tests/monomer-libraries-tests.ts +3 -3
- package/src/tests/seq-handler-get-helm-tests.ts +88 -0
- package/src/tests/sequence-space-test.ts +4 -3
- package/src/tests/to-atomic-level-tests.ts +2 -0
- package/src/tests/to-atomic-level-ui-tests.ts +74 -0
- package/src/utils/cell-renderer.ts +3 -0
- package/src/utils/convert.ts +2 -2
- package/src/utils/cyclized.ts +20 -1
- package/src/utils/dimerized.ts +12 -0
- package/src/utils/get-region-func-editor.ts +1 -1
- package/src/utils/helm-to-molfile/converter/converter.ts +58 -30
- package/src/utils/helm-to-molfile/converter/mol-atoms.ts +2 -0
- package/src/utils/helm-to-molfile/converter/mol-bonds.ts +2 -0
- package/src/utils/helm-to-molfile/converter/mol-wrapper.ts +5 -1
- package/src/utils/helm-to-molfile/converter/monomer-wrapper.ts +7 -3
- package/src/utils/helm-to-molfile/converter/polymer.ts +21 -6
- package/src/utils/helm-to-molfile/converter/types.ts +11 -0
- package/src/utils/helm-to-molfile/utils.ts +11 -15
- package/src/utils/monomer-lib/lib-manager.ts +15 -1
- package/src/utils/monomer-lib/library-file-manager/file-manager.ts +1 -1
- package/src/utils/monomer-lib/library-file-manager/file-validator.ts +8 -0
- package/src/utils/monomer-lib/library-file-manager/ui.ts +150 -3
- package/src/utils/monomer-lib/monomer-lib.ts +59 -21
- package/src/utils/monomer-lib/monomer-manager/duplicate-monomer-manager.ts +155 -0
- package/src/utils/monomer-lib/monomer-manager/monomer-manager.ts +924 -0
- package/src/utils/multiple-sequence-alignment-ui.ts +3 -3
- package/src/utils/seq-helper/index.ts +1 -0
- package/src/utils/seq-helper/seq-helper.ts +131 -0
- package/src/utils/sequence-to-mol.ts +47 -18
- package/src/widgets/bio-substructure-filter.ts +9 -7
- package/src/widgets/package-settings-editor-widget.ts +6 -6
- package/src/widgets/representations.ts +12 -12
- package/dist/449.js +0 -2
- package/dist/449.js.map +0 -1
- /package/src/tests/{seq-handler-get-region.ts → seq-handler-get-region-tests.ts} +0 -0
|
@@ -63,7 +63,7 @@ export class MonomerLibFileManager implements IMonomerLibFileManager {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/** Add standard .json monomer library */
|
|
66
|
-
async addLibraryFile(fileContent: string, fileName: string): Promise<void> {
|
|
66
|
+
async addLibraryFile(fileContent: string, fileName: string, reload = true): Promise<void> {
|
|
67
67
|
try {
|
|
68
68
|
const alreadyFileExists = await grok.dapi.files.exists(LIB_PATH + `${fileName}`);
|
|
69
69
|
if (alreadyFileExists) {
|
|
@@ -45,6 +45,7 @@ export class MonomerLibFileValidator {
|
|
|
45
45
|
|
|
46
46
|
private validateJsonContent(jsonContent: any[], fileName: string): boolean {
|
|
47
47
|
let isValid = true;
|
|
48
|
+
const existingMonomerSymbols = new Set<string>();
|
|
48
49
|
for (const monomer of jsonContent) {
|
|
49
50
|
const name = monomer[REQ.SYMBOL] ?? monomer[REQ.ID] ?? monomer[REQ.NAME] ?? NA_CODE;
|
|
50
51
|
isValid = this.validateMonomerSchema(monomer);
|
|
@@ -59,6 +60,13 @@ export class MonomerLibFileValidator {
|
|
|
59
60
|
);
|
|
60
61
|
break;
|
|
61
62
|
}
|
|
63
|
+
const key = `${(monomer[REQ.POLYMER_TYPE] ?? '')}-${name}`;
|
|
64
|
+
if (existingMonomerSymbols.has(key)) {
|
|
65
|
+
console.warn(`Bio: Monomer Library File Validator file ${fileName}, monomer '${name}' is duplicated.`,
|
|
66
|
+
'Please, verify that the monomer library file does not contain duplicated monomer symbols.'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
existingMonomerSymbols.add(key);
|
|
62
70
|
}
|
|
63
71
|
return isValid;
|
|
64
72
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
/* Do not change these import lines to match external modules in webpack configuration */
|
|
2
3
|
import * as grok from 'datagrok-api/grok';
|
|
3
4
|
import * as ui from 'datagrok-api/ui';
|
|
@@ -15,11 +16,18 @@ import {getMonomerLibHelper, IMonomerLibFileManager} from '@datagrok-libraries/b
|
|
|
15
16
|
|
|
16
17
|
import {MonomerLibFileEventManager} from './event-manager';
|
|
17
18
|
import {_package} from '../../../package';
|
|
19
|
+
import {MonomerManager} from '../monomer-manager/monomer-manager';
|
|
20
|
+
import {DuplicateMonomerManager} from '../monomer-manager/duplicate-monomer-manager';
|
|
21
|
+
import {MonomerLibManager} from '../lib-manager';
|
|
18
22
|
|
|
19
23
|
export async function showManageLibrariesDialog(): Promise<void> {
|
|
20
24
|
await DialogWrapper.showDialog();
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
export async function showManageLibrariesView() {
|
|
28
|
+
await LibManagerView.showView();
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
export async function getMonomerLibraryManagerLink(): Promise<DG.Widget> {
|
|
24
32
|
const link = ui.label('Manage monomer libraries');
|
|
25
33
|
$(link).addClass('d4-link-action');
|
|
@@ -52,6 +60,11 @@ class MonomerLibraryManagerWidget {
|
|
|
52
60
|
return MonomerLibraryManagerWidget.instancePromise;
|
|
53
61
|
}
|
|
54
62
|
|
|
63
|
+
static async reloadWidget(): Promise<void> {
|
|
64
|
+
const instance = await MonomerLibraryManagerWidget.getInstance();
|
|
65
|
+
instance._widget = await instance.createWidget();
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
private async createWidget() {
|
|
56
69
|
const content = await this.getWidgetContent();
|
|
57
70
|
const monomerLibHelper = await getMonomerLibHelper();
|
|
@@ -125,7 +138,7 @@ class LibraryControlsManager {
|
|
|
125
138
|
return inputsForm;
|
|
126
139
|
}
|
|
127
140
|
|
|
128
|
-
|
|
141
|
+
public updateControlsForm(): void {
|
|
129
142
|
const updatedForm = this._createControlsForm();
|
|
130
143
|
$('.monomer-lib-controls-form').replaceWith(updatedForm);
|
|
131
144
|
}
|
|
@@ -139,12 +152,16 @@ class LibraryControlsManager {
|
|
|
139
152
|
const logPrefix = `${this.toLog()}.createLibInput()`;
|
|
140
153
|
_package.logger.debug(`${logPrefix}, libFileName = '${libFileName}', start`);
|
|
141
154
|
const isMonomerLibrarySelected = !this.userLibSettings.exclude.includes(libFileName);
|
|
142
|
-
const libInput = ui.input.bool(libFileName, {value: isMonomerLibrarySelected, onValueChanged: (
|
|
143
|
-
this.fileManager.eventManager.updateLibrarySelectionStatus(libFileName,
|
|
155
|
+
const libInput = ui.input.bool(libFileName, {value: isMonomerLibrarySelected, onValueChanged: () => {
|
|
156
|
+
this.fileManager.eventManager.updateLibrarySelectionStatus(libFileName, libInput.value);
|
|
144
157
|
}});
|
|
145
158
|
ui.tooltip.bind(libInput.root, `Include monomers from ${libFileName}`);
|
|
146
159
|
const deleteIcon = ui.iconFA('trash-alt', () => this.promptForLibraryDeletion(libFileName));
|
|
160
|
+
const editIcon = ui.icons.edit(async () => {
|
|
161
|
+
grok.shell.v = await (await MonomerManager.getInstance()).getViewRoot(libFileName);
|
|
162
|
+
}, 'Edit monomer library');
|
|
147
163
|
ui.tooltip.bind(deleteIcon, `Delete ${libFileName}`);
|
|
164
|
+
libInput.addOptions(editIcon);
|
|
148
165
|
libInput.addOptions(deleteIcon);
|
|
149
166
|
_package.logger.debug(`${logPrefix}, libFileName = '${libFileName}', end`);
|
|
150
167
|
return libInput;
|
|
@@ -235,3 +252,133 @@ class DialogWrapper {
|
|
|
235
252
|
return dialog;
|
|
236
253
|
}
|
|
237
254
|
}
|
|
255
|
+
|
|
256
|
+
class LibManagerView {
|
|
257
|
+
private constructor() {};
|
|
258
|
+
private static _instance: LibManagerView;
|
|
259
|
+
private _view: DG.View;
|
|
260
|
+
private _duplicateManager: DuplicateMonomerManager;
|
|
261
|
+
private libManager: MonomerLibManager;
|
|
262
|
+
private async getView() {
|
|
263
|
+
const eventManager = MonomerLibFileEventManager.getInstance();
|
|
264
|
+
const widget = (await MonomerLibraryManagerWidget.getInstance()).widget;
|
|
265
|
+
const addButton = ui.bigButton('Add',
|
|
266
|
+
() => eventManager.addLibraryFile(), 'Upload new HELM monomer library');
|
|
267
|
+
const mergeButton =
|
|
268
|
+
ui.bigButton('Merge', () => { this.mergeSelectedLibs(); }, 'Merge selected libraries into one');
|
|
269
|
+
|
|
270
|
+
const v = ui.splitH(
|
|
271
|
+
[ui.divV([widget.root, ui.buttonsInput([addButton, mergeButton])], {classes: 'ui-form'}),
|
|
272
|
+
this._duplicateManager.root],
|
|
273
|
+
{style: {width: '100%', height: '100%'}},
|
|
274
|
+
true);
|
|
275
|
+
this._view = grok.shell.newView('Manage Monomer Libraries', [v]);
|
|
276
|
+
|
|
277
|
+
ui.tools.waitForElementInDom(v).then(() => {
|
|
278
|
+
setTimeout(() => {
|
|
279
|
+
const children = Array.from(v.children as HTMLCollectionOf<HTMLElement>)
|
|
280
|
+
.filter((el) => el.classList.contains('ui-box'));
|
|
281
|
+
if (children.length !== 2)
|
|
282
|
+
return;
|
|
283
|
+
const [left, right] = children;
|
|
284
|
+
const combinedWidth = left.getBoundingClientRect().width + right.getBoundingClientRect().width;
|
|
285
|
+
const leftWidth = combinedWidth * 0.3;
|
|
286
|
+
left.style.width = `${leftWidth}px`;
|
|
287
|
+
const rightWidth = combinedWidth - leftWidth;
|
|
288
|
+
right.style.width = `${rightWidth}px`;
|
|
289
|
+
}, 100);
|
|
290
|
+
this._view.subs.push(grok.events.onCurrentViewChanged.subscribe(async () => {
|
|
291
|
+
try {
|
|
292
|
+
const inst = LibManagerView._instance;
|
|
293
|
+
if (inst && inst._view && 'id' in grok.shell.v && grok.shell.v.id === inst._view.id)
|
|
294
|
+
inst._duplicateManager?.refresh();
|
|
295
|
+
} catch (e) {
|
|
296
|
+
console.error(e);
|
|
297
|
+
}
|
|
298
|
+
}));
|
|
299
|
+
});
|
|
300
|
+
//grok.shell.dockManager.dock(this._duplicateManager.root, DG.DOCK_TYPE.RIGHT, null, '', 0.4);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
static async showView() {
|
|
304
|
+
if (!LibManagerView._instance)
|
|
305
|
+
LibManagerView._instance = new LibManagerView();
|
|
306
|
+
if (!LibManagerView._instance._duplicateManager)
|
|
307
|
+
LibManagerView._instance._duplicateManager = await DuplicateMonomerManager.getInstance();
|
|
308
|
+
if (!LibManagerView._instance.libManager)
|
|
309
|
+
LibManagerView._instance.libManager = await MonomerLibManager.getInstance();
|
|
310
|
+
if (LibManagerView._instance._view &&
|
|
311
|
+
Array.from(grok.shell.views).find((v) => v.id && v.id === LibManagerView._instance._view.id)) {
|
|
312
|
+
grok.shell.v = LibManagerView._instance._view;
|
|
313
|
+
await LibManagerView._instance._duplicateManager.refresh();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
LibManagerView._instance.getView();
|
|
317
|
+
}
|
|
318
|
+
async mergeSelectedLibs() {
|
|
319
|
+
const libraryExistsError = 'Library with this name already exists';
|
|
320
|
+
const libManager = await MonomerLibManager.getInstance();
|
|
321
|
+
await libManager.awaitLoaded();
|
|
322
|
+
await libManager.loadLibrariesPromise;
|
|
323
|
+
if (!libManager.duplicatesHandled) {
|
|
324
|
+
grok.shell.warning(`Selected libraries contain repeating symbols with different monomers.
|
|
325
|
+
Please choose the correct monomer for each symbol using duplicate monomomer manager.`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const libJSON = libManager.getBioLib().toJSON();
|
|
329
|
+
const dialog = ui.dialog('Merge selected libraries');
|
|
330
|
+
const newFileNameInput = ui.input.string('Library Name', {
|
|
331
|
+
placeholder: 'Enter new library name',
|
|
332
|
+
nullable: false,
|
|
333
|
+
onValueChanged: () => {
|
|
334
|
+
const res = validateInput(newFileNameInput.value);
|
|
335
|
+
dialog.getButton('Download')?.classList?.toggle('d4-disabled', !!res && res !== libraryExistsError);
|
|
336
|
+
dialog.getButton('Save')?.classList?.toggle('d4-disabled', !!res);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
const validLibPaths = (await this.libManager.getFileManager()).getValidLibraryPaths();
|
|
340
|
+
newFileNameInput.addValidator(validateInput);
|
|
341
|
+
function getFileNameInputValue() {
|
|
342
|
+
let fileName = newFileNameInput.value;
|
|
343
|
+
if (!fileName.endsWith('.json'))
|
|
344
|
+
fileName += '.json';
|
|
345
|
+
return fileName;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
function validateInput(v: string) {
|
|
349
|
+
if (!v || !v.trim()) return 'Library name cannot be empty';
|
|
350
|
+
if ((v.endsWith('.json') && validLibPaths.includes(v)) || validLibPaths.includes(v + '.json'))
|
|
351
|
+
return libraryExistsError;
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
dialog
|
|
355
|
+
.add(newFileNameInput)
|
|
356
|
+
.add(ui.divText(`Total monomers: ${libJSON.length}`))
|
|
357
|
+
.addButton('Download', () => { DG.Utils.download(getFileNameInputValue(), JSON.stringify(libJSON)); })
|
|
358
|
+
.addButton('Save', async () => {
|
|
359
|
+
dialog.close();
|
|
360
|
+
const fileName = getFileNameInputValue();
|
|
361
|
+
const content = JSON.stringify(libJSON);
|
|
362
|
+
const fileManager = await this.libManager.getFileManager();
|
|
363
|
+
this._view && ui.setUpdateIndicator(this._view.root, true);
|
|
364
|
+
try {
|
|
365
|
+
await fileManager.addLibraryFile(content, fileName, false); // we will reload after updating settings
|
|
366
|
+
const settings = await getUserLibSettings();
|
|
367
|
+
settings.exclude = validLibPaths; // exclude all previous libraries
|
|
368
|
+
await setUserLibSettings(settings);
|
|
369
|
+
await this.libManager.loadLibraries(true);
|
|
370
|
+
await MonomerLibraryManagerWidget.reloadWidget();
|
|
371
|
+
} catch (e) {
|
|
372
|
+
grok.shell.error(`Failed to save library ${fileName}. see console for details.`);
|
|
373
|
+
console.error(e);
|
|
374
|
+
} finally {
|
|
375
|
+
this._view && ui.setUpdateIndicator(this._view.root, false);
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
.show();
|
|
379
|
+
dialog.getButton('Download')?.classList?.add('d4-disabled');
|
|
380
|
+
dialog.getButton('Save')?.classList?.add('d4-disabled');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
/* Do not change these import lines to match external modules in webpack configuration */
|
|
2
3
|
import * as grok from 'datagrok-api/grok';
|
|
3
4
|
import * as ui from 'datagrok-api/ui';
|
|
@@ -18,12 +19,21 @@ import {helmTypeToPolymerType} from '@datagrok-libraries/bio/src/monomer-works/m
|
|
|
18
19
|
import '../../../css/cell-renderer.css';
|
|
19
20
|
|
|
20
21
|
import {_package} from '../../package';
|
|
22
|
+
import {getUserLibSettings} from '@datagrok-libraries/bio/src/monomer-works/lib-settings';
|
|
23
|
+
import {UserLibSettings} from '@datagrok-libraries/bio/src/monomer-works/types';
|
|
21
24
|
|
|
22
25
|
/** Wrapper for monomers obtained from different sources. For managing monomere
|
|
23
26
|
* libraries, use MolfileHandler class instead */
|
|
24
27
|
export class MonomerLib implements IMonomerLib {
|
|
25
28
|
private _monomers: { [polymerType: string]: { [monomerSymbol: string]: Monomer } } = {};
|
|
26
29
|
private _onChanged = new Subject<any>();
|
|
30
|
+
private _duplicateMonomers: { [polymerType: string]: { [monomerSymbol: string]: Monomer[] } } = {};
|
|
31
|
+
public get duplicateMonomers(): { [polymerType: string]: { [monomerSymbol: string]: Monomer[] } } {
|
|
32
|
+
return this._duplicateMonomers;
|
|
33
|
+
}
|
|
34
|
+
private _duplicatesHandled = true;
|
|
35
|
+
public get duplicatesHandled() { return this._duplicatesHandled; }
|
|
36
|
+
private duplicatesNotified: boolean = false;
|
|
27
37
|
|
|
28
38
|
constructor(
|
|
29
39
|
monomers: { [polymerType: string]: { [monomerSymbol: string]: Monomer } },
|
|
@@ -37,6 +47,15 @@ export class MonomerLib implements IMonomerLib {
|
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
49
|
|
|
50
|
+
toJSON(): Monomer[] {
|
|
51
|
+
const resJSON: Monomer[] = [];
|
|
52
|
+
for (const set of Object.values(this._monomers)) {
|
|
53
|
+
for (const m of Object.values(set))
|
|
54
|
+
resJSON.push({...m, lib: undefined, wem: undefined});
|
|
55
|
+
}
|
|
56
|
+
return resJSON;
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
/** Creates missing {@link Monomer} */
|
|
41
60
|
addMissingMonomer(polymerType: PolymerType, monomerSymbol: string): Monomer {
|
|
42
61
|
let mSet = this._monomers[polymerType];
|
|
@@ -92,7 +111,7 @@ export class MonomerLib implements IMonomerLib {
|
|
|
92
111
|
if (!polymerType) {
|
|
93
112
|
_package.logger.warning(`${logPrefix} symbol '${argMonomerSymbol}', polymerType not specified.`);
|
|
94
113
|
// Assume any polymer type
|
|
95
|
-
for (const [
|
|
114
|
+
for (const [_polymerType, dict] of Object.entries(this._monomers)) {
|
|
96
115
|
res = dict[monomerSymbol];
|
|
97
116
|
if (res) break;
|
|
98
117
|
}
|
|
@@ -138,7 +157,7 @@ export class MonomerLib implements IMonomerLib {
|
|
|
138
157
|
/** Get a list of monomers with specified element attached to specified
|
|
139
158
|
* R-group
|
|
140
159
|
* WARNING: RGroup numbering starts from 1, not 0*/
|
|
141
|
-
getMonomerSymbolsByRGroup(rGroupNumber: number, polymerType: PolymerType,
|
|
160
|
+
getMonomerSymbolsByRGroup(rGroupNumber: number, polymerType: PolymerType, _element?: string): string[] {
|
|
142
161
|
const monomerSymbols = this.getMonomerSymbolsByType(polymerType);
|
|
143
162
|
let monomers = monomerSymbols.map((sym) => this.getMonomer(polymerType, sym));
|
|
144
163
|
monomers = monomers.filter((el) => el !== null);
|
|
@@ -155,7 +174,7 @@ export class MonomerLib implements IMonomerLib {
|
|
|
155
174
|
return false;
|
|
156
175
|
let criterion = monomer?.rgroups.length >= rGroupNumber;
|
|
157
176
|
const molfileHandler = MolfileHandler.getInstance(monomer.molfile);
|
|
158
|
-
const
|
|
177
|
+
const _rGroupIndices = findAllIndices(molfileHandler.atomTypes, 'R#');
|
|
159
178
|
criterion &&= true;
|
|
160
179
|
return criterion;
|
|
161
180
|
});
|
|
@@ -178,6 +197,11 @@ export class MonomerLib implements IMonomerLib {
|
|
|
178
197
|
|
|
179
198
|
const monomers = lib.getMonomerSymbolsByType(type);
|
|
180
199
|
monomers.forEach((monomerSymbol) => {
|
|
200
|
+
if (this._monomers[type][monomerSymbol]) {
|
|
201
|
+
this._duplicateMonomers[type] ??= {};
|
|
202
|
+
this._duplicateMonomers[type][monomerSymbol] ??= [this._monomers[type][monomerSymbol]];
|
|
203
|
+
this._duplicateMonomers[type][monomerSymbol].push(lib.getMonomer(type, monomerSymbol)!);
|
|
204
|
+
}
|
|
181
205
|
this._monomers[type][monomerSymbol] = lib.getMonomer(type, monomerSymbol)!;
|
|
182
206
|
});
|
|
183
207
|
});
|
|
@@ -191,11 +215,40 @@ export class MonomerLib implements IMonomerLib {
|
|
|
191
215
|
public updateLibs(libList: IMonomerLib[], reload: boolean = false): void {
|
|
192
216
|
if (reload)
|
|
193
217
|
this._monomers = {};
|
|
218
|
+
this._duplicateMonomers = {}; // Reset duplicates
|
|
194
219
|
for (const lib of libList)
|
|
195
220
|
if (!lib.error) this._updateLibInt(lib);
|
|
221
|
+
if (Object.entries(this.duplicateMonomers).length > 0) {
|
|
222
|
+
getUserLibSettings().then((settings) => {
|
|
223
|
+
this.assignDuplicatePreferances(settings);
|
|
224
|
+
});
|
|
225
|
+
} else
|
|
226
|
+
this._duplicatesHandled = true;
|
|
227
|
+
|
|
196
228
|
this._onChanged.next();
|
|
197
229
|
}
|
|
198
230
|
|
|
231
|
+
/** Checks wether all duplicated monomers have set preferences in user settings. overwrites those which have. */
|
|
232
|
+
assignDuplicatePreferances(userSettings: UserLibSettings): boolean {
|
|
233
|
+
let res = true;
|
|
234
|
+
for (const polymerType in this.duplicateMonomers) {
|
|
235
|
+
for (const monomerSymbol in this.duplicateMonomers[polymerType]) {
|
|
236
|
+
if (!userSettings.duplicateMonomerPreferences?.[polymerType]?.[monomerSymbol])
|
|
237
|
+
res = false;
|
|
238
|
+
else {
|
|
239
|
+
const source = userSettings.duplicateMonomerPreferences[polymerType][monomerSymbol];
|
|
240
|
+
const monomer = this.duplicateMonomers[polymerType][monomerSymbol].find((m) => m.lib?.source === source);
|
|
241
|
+
if (!monomer)
|
|
242
|
+
res = false;
|
|
243
|
+
else
|
|
244
|
+
this._monomers[polymerType][monomerSymbol] = monomer;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
this._duplicatesHandled = res;
|
|
249
|
+
return res;
|
|
250
|
+
}
|
|
251
|
+
|
|
199
252
|
public clear(): void {
|
|
200
253
|
this._monomers = {};
|
|
201
254
|
this._onChanged.next();
|
|
@@ -250,7 +303,7 @@ export class MonomerLib implements IMonomerLib {
|
|
|
250
303
|
if (monomer) {
|
|
251
304
|
// Symbol & Name
|
|
252
305
|
const symbol = monomer[REQ.SYMBOL];
|
|
253
|
-
const
|
|
306
|
+
const _name = monomer[REQ.NAME];
|
|
254
307
|
res.append(ui.divH([
|
|
255
308
|
ui.div([symbol], {style: {fontWeight: 'bolder', textWrap: 'nowrap', marginRight: '6px'}}),
|
|
256
309
|
ui.div([monomer.name])
|
|
@@ -259,10 +312,9 @@ export class MonomerLib implements IMonomerLib {
|
|
|
259
312
|
// Structure
|
|
260
313
|
const chemOptions = {autoCrop: true, autoCropMargin: 0, suppressChiralText: true};
|
|
261
314
|
let structureEl: HTMLElement;
|
|
262
|
-
if (monomer.molfile)
|
|
263
|
-
//
|
|
315
|
+
if (monomer.molfile)
|
|
264
316
|
structureEl = grok.chem.svgMol(monomer.molfile, undefined, undefined, chemOptions);
|
|
265
|
-
|
|
317
|
+
else if (monomer.smiles) {
|
|
266
318
|
structureEl = ui.divV([
|
|
267
319
|
grok.chem.svgMol(monomer.smiles, undefined, undefined, chemOptions),
|
|
268
320
|
ui.divText('from smiles', {style: {fontSize: 'smaller'}}),
|
|
@@ -276,20 +328,6 @@ export class MonomerLib implements IMonomerLib {
|
|
|
276
328
|
|
|
277
329
|
// Source
|
|
278
330
|
res.append(ui.divText(monomer.lib?.source ?? 'Missed in libraries'));
|
|
279
|
-
|
|
280
|
-
// const label = (s: string) => {
|
|
281
|
-
// return ui.label(s /* span ? */, {classes: 'ui-input-label'});
|
|
282
|
-
// };
|
|
283
|
-
// res.append(ui.div([
|
|
284
|
-
// label('Name'),
|
|
285
|
-
// ui.divText(monomer.name, {classes: 'ui-input-text'})
|
|
286
|
-
// ], {classes: 'ui-input-root'}));
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
// res.append(ui.div([
|
|
290
|
-
// label('Source'),
|
|
291
|
-
// ui.divText(monomer.lib?.source ?? 'unknown', {classes: 'ui-input-text'}),
|
|
292
|
-
// ], {classes: 'ui-input-root'}));
|
|
293
331
|
} else {
|
|
294
332
|
res.append(ui.divV([
|
|
295
333
|
ui.divText(`Monomer '${monomerSymbol}' of type '${polymerType}' not found.`),
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* eslint-disable max-len */
|
|
2
|
+
import * as grok from 'datagrok-api/grok';
|
|
3
|
+
import * as ui from 'datagrok-api/ui';
|
|
4
|
+
import * as DG from 'datagrok-api/dg';
|
|
5
|
+
|
|
6
|
+
import {Monomer} from '@datagrok-libraries/bio/src/types';
|
|
7
|
+
import {UserLibSettings} from '@datagrok-libraries/bio/src/monomer-works/types';
|
|
8
|
+
import {getUserLibSettings, setUserLibSettings} from '@datagrok-libraries/bio/src/monomer-works/lib-settings';
|
|
9
|
+
import '../../../../css/monomer-manager.css';
|
|
10
|
+
import {MonomerLibManager} from '../lib-manager';
|
|
11
|
+
|
|
12
|
+
class MonomerCard {
|
|
13
|
+
root: HTMLElement = ui.divV([], {classes: 'monomer-card-root'});
|
|
14
|
+
|
|
15
|
+
private _selected: boolean = false;
|
|
16
|
+
get selected(): boolean { return this._selected; }
|
|
17
|
+
set selected(value: boolean) {
|
|
18
|
+
this._selected = value;
|
|
19
|
+
this.root.style.border = value ? '2px solid var(--green-2)' : '2px solid var(--grey-2)';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor(public monomer: Monomer) {}
|
|
23
|
+
|
|
24
|
+
render() {
|
|
25
|
+
ui.empty(this.root);
|
|
26
|
+
const monomerMolSvg = this.monomer.smiles && grok.chem.checkSmiles(this.monomer.smiles) ?
|
|
27
|
+
grok.chem.drawMolecule(this.monomer.smiles, 200, 200) : grok.chem.drawMolecule(this.monomer.molfile ?? '', 200, 200);
|
|
28
|
+
this.root.appendChild(monomerMolSvg);
|
|
29
|
+
const monomerName =
|
|
30
|
+
ui.divH([ui.divText('Monomer Name: '), ui.divText(this.monomer.name)], {classes: 'monomer-card-info-row'});
|
|
31
|
+
|
|
32
|
+
this.root.appendChild(monomerName);
|
|
33
|
+
ui.tooltip.bind(monomerName, this.monomer.name);
|
|
34
|
+
if (this.monomer.lib?.source) {
|
|
35
|
+
const monomerSource =
|
|
36
|
+
ui.divH([ui.divText('Source: '), ui.divText(this.monomer.lib.source)], {classes: 'monomer-card-info-row'});
|
|
37
|
+
this.root.appendChild(monomerSource);
|
|
38
|
+
ui.tooltip.bind(monomerSource, this.monomer.lib.source);
|
|
39
|
+
}
|
|
40
|
+
const monomerType = ui.divH([ui.divText('Polymer Type: '), ui.divText(this.monomer.polymerType)], {classes: 'monomer-card-info-row'});
|
|
41
|
+
this.root.appendChild(monomerType);
|
|
42
|
+
ui.tooltip.bind(monomerType, this.monomer.polymerType);
|
|
43
|
+
|
|
44
|
+
ui.tooltip.bind(this.root, 'Select Monomer');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class DuplicateSymbolRow {
|
|
49
|
+
root: HTMLElement = ui.divH([],
|
|
50
|
+
{style: {
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
width: '100%',
|
|
53
|
+
overflow: 'hidden',
|
|
54
|
+
visibility: 'visible',
|
|
55
|
+
}, classes: 'duplicate-monomer-symbol-row'}
|
|
56
|
+
);
|
|
57
|
+
monomerCards: MonomerCard[];
|
|
58
|
+
constructor(
|
|
59
|
+
public monomerSymbol: string, private monomers: Monomer[],
|
|
60
|
+
selectedMonomer: Monomer | null, onMonomerSelected: (monomer: Monomer) => void) {
|
|
61
|
+
this.monomerCards = monomers.map((monomer) => new MonomerCard(monomer));
|
|
62
|
+
const monomerGallery = ui.divH([], {style: {overflowX: 'auto', width: '100%'}});
|
|
63
|
+
const monomerSymbolDiv = ui.h1(monomerSymbol, {style: {lineHeight: '2em', fontSize: '1.5em', marginRight: '20px', width: '100px', overflow: 'hidden', textOverflow: 'elipsis'}});
|
|
64
|
+
ui.tooltip.bind(monomerSymbolDiv, monomerSymbol);
|
|
65
|
+
this.root.appendChild(monomerSymbolDiv);
|
|
66
|
+
this.root.appendChild(monomerGallery);
|
|
67
|
+
this.monomerCards.forEach((card) => {
|
|
68
|
+
card.root.onclick = () => {
|
|
69
|
+
this.monomerCards.forEach((c) => c.selected = false);
|
|
70
|
+
card.selected = true;
|
|
71
|
+
onMonomerSelected(card.monomer);
|
|
72
|
+
};
|
|
73
|
+
if (selectedMonomer && card.monomer === selectedMonomer)
|
|
74
|
+
card.selected = true;
|
|
75
|
+
monomerGallery.appendChild(card.root);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
render() {
|
|
80
|
+
this.monomerCards.forEach((card) => card.render());
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class DuplicateMonomerManager {
|
|
85
|
+
monomerCardRows: DuplicateSymbolRow[] = [];
|
|
86
|
+
private saveSettingsPromise: Promise<void> = Promise.resolve();
|
|
87
|
+
private searchInput: DG.InputBase<string>;
|
|
88
|
+
private _root: HTMLElement;
|
|
89
|
+
private monomers: { [polymerType: string]: { [monomerSymbol: string]: Monomer[] } };
|
|
90
|
+
private settings: UserLibSettings;
|
|
91
|
+
private filteredMonomerRows: DuplicateSymbolRow[] = [];
|
|
92
|
+
private static _instance: DuplicateMonomerManager;
|
|
93
|
+
private vv: DG.VirtualView;
|
|
94
|
+
|
|
95
|
+
static async getInstance() {
|
|
96
|
+
if (!DuplicateMonomerManager._instance) {
|
|
97
|
+
DuplicateMonomerManager._instance = new DuplicateMonomerManager();
|
|
98
|
+
await DuplicateMonomerManager._instance.refresh();
|
|
99
|
+
const libManager = await MonomerLibManager.getInstance();
|
|
100
|
+
libManager.getMonomerLib().onChanged.subscribe(async () => await DuplicateMonomerManager._instance.refresh());
|
|
101
|
+
}
|
|
102
|
+
DuplicateMonomerManager._instance.refresh();
|
|
103
|
+
return DuplicateMonomerManager._instance;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public async refresh() {
|
|
107
|
+
this.settings = await getUserLibSettings();
|
|
108
|
+
const libManager = await MonomerLibManager.getInstance();
|
|
109
|
+
await libManager.awaitLoaded();
|
|
110
|
+
await libManager.loadLibrariesPromise;
|
|
111
|
+
this.monomers = libManager.duplicateMonomers;
|
|
112
|
+
this.monomerCardRows = [];
|
|
113
|
+
for (const polymerType in this.monomers) {
|
|
114
|
+
for (const monomerSymbol in this.monomers[polymerType]) {
|
|
115
|
+
const selectedMonomerSource = this.settings.duplicateMonomerPreferences?.[polymerType]?.[monomerSymbol] ?? null;
|
|
116
|
+
const selectedMonomer = selectedMonomerSource ? this.monomers[polymerType][monomerSymbol].find((monomer) => monomer.lib?.source === selectedMonomerSource) ?? null : null;
|
|
117
|
+
|
|
118
|
+
this.monomerCardRows.push(new DuplicateSymbolRow(monomerSymbol, this.monomers[polymerType][monomerSymbol], selectedMonomer,
|
|
119
|
+
async (monomer) => {
|
|
120
|
+
this.saveSettingsPromise = this.saveSettingsPromise.then(async () => {
|
|
121
|
+
if (!monomer.lib?.source)
|
|
122
|
+
return;
|
|
123
|
+
this.settings.duplicateMonomerPreferences = this.settings.duplicateMonomerPreferences ?? {};
|
|
124
|
+
this.settings.duplicateMonomerPreferences[polymerType] = this.settings.duplicateMonomerPreferences[polymerType] ?? {};
|
|
125
|
+
this.settings.duplicateMonomerPreferences[polymerType][monomerSymbol] = monomer.lib.source;
|
|
126
|
+
await setUserLibSettings(this.settings);
|
|
127
|
+
grok.shell.info(`Monomer '${monomer.name}' from source '${monomer.lib.source}' selected for symbol '${monomerSymbol}'.`);
|
|
128
|
+
libManager.assignDuplicatePreferances(this.settings);
|
|
129
|
+
});
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this.filteredMonomerRows = this.monomerCardRows;
|
|
134
|
+
if (!this.vv) {
|
|
135
|
+
this.vv = ui.virtualView(this.monomerCardRows.length, (i) => { this.monomerCardRows[i].render(); return this.monomerCardRows[i].root; });
|
|
136
|
+
this.vv.root.classList.add('duplicate-monomers-virtual-view');
|
|
137
|
+
this.searchInput = ui.input.string('Search', {placeholder: 'Monomer Symbol', value: '', onValueChanged: () => search(this.searchInput.value)});
|
|
138
|
+
this.searchInput.root.style.justifyContent = 'center';
|
|
139
|
+
this.searchInput.input.style.width = '200px';
|
|
140
|
+
this._root = ui.divV([ui.h1('Manage Duplicate Monomer Symbols', {style: {textAlign: 'center'}}),
|
|
141
|
+
this.searchInput.root, ui.divV([this.vv.root], {style: {overflowY: 'auto', height: '100%'}})], {style: {height: '100%'}});
|
|
142
|
+
|
|
143
|
+
const search = (query: string) => {
|
|
144
|
+
this.filteredMonomerRows = this.monomerCardRows.filter((row) => row.monomerSymbol.toLowerCase().includes(query.toLowerCase()));
|
|
145
|
+
this.vv.setData(this.filteredMonomerRows.length, (i) => { this.filteredMonomerRows[i].render(); return this.filteredMonomerRows[i].root; });
|
|
146
|
+
};
|
|
147
|
+
} else
|
|
148
|
+
this.vv.setData(this.filteredMonomerRows.length, (i) => { this.filteredMonomerRows[i].render(); return this.filteredMonomerRows[i].root; } );
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private constructor() {}
|
|
152
|
+
get root() {
|
|
153
|
+
return this._root;
|
|
154
|
+
}
|
|
155
|
+
}
|