@datagrok/sequence-translator 1.0.17 → 1.1.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.
Files changed (96) hide show
  1. package/.eslintrc.json +4 -3
  2. package/CHANGELOG.md +3 -0
  3. package/detectors.js +8 -0
  4. package/dist/package-test.js +2 -73079
  5. package/dist/package-test.js.map +1 -0
  6. package/dist/package.js +2 -72284
  7. package/dist/package.js.map +1 -0
  8. package/files/axolabs-style.json +97 -0
  9. package/files/codes-to-symbols.json +66 -0
  10. package/files/formats-to-helm.json +59 -0
  11. package/files/linkers.json +22 -0
  12. package/files/monomer-lib.json +1094 -0
  13. package/link-bio +7 -0
  14. package/package.json +30 -28
  15. package/scripts/build-monomer-lib.py +391 -122
  16. package/src/demo/demo-st-ui.ts +71 -0
  17. package/src/demo/handle-error.ts +12 -0
  18. package/src/model/axolabs/axolabs-tab.ts +111 -0
  19. package/src/model/axolabs/const.ts +33 -0
  20. package/src/{axolabs-tab → model/axolabs}/draw-svg.ts +1 -1
  21. package/src/{axolabs-tab → model/axolabs}/helpers.ts +7 -5
  22. package/src/model/const.ts +19 -0
  23. package/src/model/data-loading-utils/const.ts +8 -0
  24. package/src/model/data-loading-utils/json-loader.ts +38 -0
  25. package/src/model/data-loading-utils/types.ts +30 -0
  26. package/src/model/format-translation/const.ts +8 -0
  27. package/src/model/format-translation/conversion-utils.ts +48 -0
  28. package/src/model/format-translation/format-converter.ts +107 -0
  29. package/src/model/helpers.ts +12 -0
  30. package/src/model/monomer-lib/const.ts +3 -0
  31. package/src/model/monomer-lib/lib-wrapper.ts +106 -0
  32. package/src/model/parsing-validation/format-detector.ts +57 -0
  33. package/src/model/parsing-validation/sequence-validator.ts +52 -0
  34. package/src/model/sequence-to-structure-utils/const.ts +1 -0
  35. package/src/{utils/structures-works → model/sequence-to-structure-utils}/mol-transformations.ts +33 -41
  36. package/src/model/sequence-to-structure-utils/monomer-code-parser.ts +92 -0
  37. package/src/model/sequence-to-structure-utils/sdf-tab.ts +94 -0
  38. package/src/model/sequence-to-structure-utils/sequence-to-molfile.ts +409 -0
  39. package/src/package.ts +104 -92
  40. package/src/tests/const.ts +17 -0
  41. package/src/tests/smiles-tests.ts +32 -457
  42. package/src/view/const/main-tab.ts +3 -0
  43. package/src/view/const/view.ts +10 -0
  44. package/src/view/css/axolabs-tab.css +1 -0
  45. package/src/view/css/colored-text-input.css +27 -0
  46. package/src/view/css/main-tab.css +46 -0
  47. package/src/view/css/sdf-tab.css +39 -0
  48. package/src/view/monomer-lib-viewer/viewer.ts +22 -0
  49. package/src/view/tabs/axolabs.ts +720 -0
  50. package/src/view/tabs/main.ts +174 -0
  51. package/src/view/tabs/sdf.ts +173 -0
  52. package/src/view/utils/app-info-dialog.ts +18 -0
  53. package/src/view/utils/colored-input/colored-text-input.ts +56 -0
  54. package/src/view/utils/colored-input/input-painters.ts +44 -0
  55. package/src/view/utils/draw-molecule.ts +86 -0
  56. package/src/view/utils/molecule-img.ts +106 -0
  57. package/src/view/view.ts +129 -0
  58. package/tsconfig.json +12 -18
  59. package/webpack.config.js +17 -4
  60. package/README.md +0 -84
  61. package/css/style.css +0 -18
  62. package/img/Sequence Translator Axolabs.png +0 -0
  63. package/jest.config.js +0 -33
  64. package/setup-unlink-clean.cmd +0 -14
  65. package/setup-unlink-clean.sh +0 -21
  66. package/setup.cmd +0 -14
  67. package/setup.sh +0 -37
  68. package/src/__jest__/remote.test.ts +0 -77
  69. package/src/__jest__/test-node.ts +0 -97
  70. package/src/apps/oligo-sd-file-app.ts +0 -58
  71. package/src/autostart/calculations.ts +0 -40
  72. package/src/autostart/constants.ts +0 -37
  73. package/src/autostart/registration.ts +0 -306
  74. package/src/axolabs-tab/axolabs-tab.ts +0 -873
  75. package/src/axolabs-tab/define-pattern.ts +0 -874
  76. package/src/hardcode-to-be-eliminated/ICDs.ts +0 -3
  77. package/src/hardcode-to-be-eliminated/IDPs.ts +0 -3
  78. package/src/hardcode-to-be-eliminated/const.ts +0 -5
  79. package/src/hardcode-to-be-eliminated/constants.ts +0 -101
  80. package/src/hardcode-to-be-eliminated/converters.ts +0 -323
  81. package/src/hardcode-to-be-eliminated/map.ts +0 -720
  82. package/src/hardcode-to-be-eliminated/salts.ts +0 -2
  83. package/src/hardcode-to-be-eliminated/sources.ts +0 -3
  84. package/src/hardcode-to-be-eliminated/users.ts +0 -3
  85. package/src/main-tab/main-tab.ts +0 -210
  86. package/src/sdf-tab/sdf-tab.ts +0 -163
  87. package/src/sdf-tab/sequence-codes-tools.ts +0 -347
  88. package/src/utils/const.ts +0 -0
  89. package/src/utils/helpers.ts +0 -28
  90. package/src/utils/parse.ts +0 -27
  91. package/src/utils/sdf-add-columns.ts +0 -118
  92. package/src/utils/sdf-save-table.ts +0 -56
  93. package/src/utils/structures-works/draw-molecule.ts +0 -84
  94. package/src/utils/structures-works/from-monomers.ts +0 -266
  95. package/test-SequenceTranslator-6288c2fbe346-695b7b55.html +0 -259
  96. package/vendors/openchemlib-full.js +0 -293
@@ -0,0 +1,720 @@
1
+ /* Do not change these import lines to match external modules in webpack configuration */
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 {axolabsStyleMap} from '../../model/data-loading-utils/json-loader';
7
+ import {
8
+ DEFAULT_PTO, DEFAULT_SEQUENCE_LENGTH, MAX_SEQUENCE_LENGTH, USER_STORAGE_KEY, EXAMPLE_MIN_WIDTH, SS, AS, STRAND_NAME, STRANDS, TERMINAL, TERMINAL_KEYS, THREE_PRIME, FIVE_PRIME, JSON_FIELD as FIELD
9
+ } from '../../model/axolabs/const';
10
+ import {isOverhang} from '../../model/axolabs/helpers';
11
+ import {generateExample, translateSequence, getShortName, isCurrentUserCreatedThisPattern, findDuplicates, addColumnWithIds, addColumnWithTranslatedSequences} from '../../model/axolabs/axolabs-tab';
12
+ import {drawAxolabsPattern} from '../../model/axolabs/draw-svg';
13
+ // todo: remove ts-ignore
14
+ //@ts-ignore
15
+ import * as svg from 'save-svg-as-png';
16
+ import $ from 'cash-dom';
17
+
18
+ type BooleanInput = DG.InputBase<boolean | null>;
19
+ type StringInput = DG.InputBase<string | null>;
20
+
21
+ export class AxolabsTabUI {
22
+ get htmlDivElement() {
23
+ function updateModification(strand: string) {
24
+ modificationItems[strand].innerHTML = '';
25
+ ptoLinkages[strand] = ptoLinkages[strand].concat(Array(maxStrandLength[strand] - baseInputsObject[strand].length).fill(fullyPto));
26
+ baseInputsObject[strand] = baseInputsObject[strand].concat(Array(maxStrandLength[strand] - baseInputsObject[strand].length).fill(sequenceBase));
27
+ let nucleotideCounter = 0;
28
+ for (let i = 0; i < strandLengthInput[strand].value!; i++) {
29
+ ptoLinkages[strand][i] = ui.boolInput('', ptoLinkages[strand][i].value!, () => {
30
+ updateSvgScheme();
31
+ updateOutputExamples();
32
+ });
33
+ baseInputsObject[strand][i] = ui.choiceInput('', baseInputsObject[strand][i].value, baseChoices, (v: string) => {
34
+ if (!enumerateModifications.includes(v)) {
35
+ enumerateModifications.push(v);
36
+ isEnumerateModificationsDiv.append(
37
+ ui.divText('', {style: {width: '25px'}}),
38
+ ui.boolInput(v, true, (boolV: boolean) => {
39
+ if (boolV) {
40
+ if (!enumerateModifications.includes(v))
41
+ enumerateModifications.push(v);
42
+ } else {
43
+ const index = enumerateModifications.indexOf(v, 0);
44
+ if (index > -1)
45
+ enumerateModifications.splice(index, 1);
46
+ }
47
+ updateSvgScheme();
48
+ }).root,
49
+ );
50
+ }
51
+ updateModification(AS);
52
+ updateSvgScheme();
53
+ updateOutputExamples();
54
+ });
55
+ $(baseInputsObject[strand][i].root).addClass('st-pattern-choice-input');
56
+ if (!isOverhang(baseInputsObject[strand][i].value!))
57
+ nucleotideCounter++;
58
+
59
+ modificationItems[strand].append(
60
+ ui.divH([
61
+ ui.div([ui.label(isOverhang(baseInputsObject[strand][i].value!) ? '' : String(nucleotideCounter))],
62
+ {style: {width: '20px'}})!,
63
+ ui.block75([baseInputsObject[strand][i].root])!,
64
+ ui.div([ptoLinkages[strand][i]])!,
65
+ ], {style: {alignItems: 'center'}}),
66
+ );
67
+ }
68
+ }
69
+
70
+ function updateUiForNewSequenceLength() {
71
+ if (Object.values(strandLengthInput).every((input) => input.value! < MAX_SEQUENCE_LENGTH)) {
72
+ STRANDS.forEach((strand) => {
73
+ if (strandLengthInput[strand].value! > maxStrandLength[strand])
74
+ maxStrandLength[strand] = strandLengthInput[strand].value!;
75
+ updateModification(strand);
76
+ })
77
+
78
+ updateSvgScheme();
79
+ updateInputExamples();
80
+ updateOutputExamples();
81
+ } else {
82
+ ui.dialog('Sequence length is out of range')
83
+ .add(ui.divText('Sequence length should be less than ' +
84
+ MAX_SEQUENCE_LENGTH.toString() + ' due to UI constrains.'))
85
+ .add(ui.divText('Please change sequence length in order to define new pattern.'))
86
+ .show();
87
+ }
88
+ }
89
+
90
+ // todo: unify with updateBases
91
+ function updatePto(newPtoValue: boolean): void {
92
+ STRANDS.forEach((strand) => {
93
+ for (let i = 0; i < ptoLinkages[strand].length; i++)
94
+ ptoLinkages[strand][i].value = newPtoValue;
95
+ })
96
+ updateSvgScheme();
97
+ }
98
+
99
+ function updateBases(newBasisValue: string): void {
100
+ STRANDS.forEach((strand) => {
101
+ for (let i = 0; i < baseInputsObject[strand].length; i++)
102
+ baseInputsObject[strand][i].value = newBasisValue;
103
+ })
104
+ updateSvgScheme();
105
+ }
106
+
107
+ function updateInputExamples() {
108
+ STRANDS.forEach((s) => {
109
+ if (inputStrandColumn[s].value === '')
110
+ inputExample[s].value = generateExample(strandLengthInput[s].value!, sequenceBase.value!);
111
+ });
112
+ }
113
+
114
+ function updateOutputExamples() {
115
+ const conditions = [true, createAsStrand.value];
116
+ STRANDS.forEach((strand, i) => {
117
+ if (conditions[i]) {
118
+ outputExample[strand].value = translateSequence(inputExample[strand].value, baseInputsObject[strand], ptoLinkages[strand], terminalModification[strand][FIVE_PRIME], terminalModification[strand][THREE_PRIME], firstPto[strand].value!);
119
+ }
120
+ })
121
+ }
122
+
123
+ function updateSvgScheme() {
124
+ svgDiv.innerHTML = '';
125
+ svgDiv.append(
126
+ ui.span([
127
+
128
+ // todo: refactor the funciton, reduce # of args
129
+ drawAxolabsPattern(
130
+ getShortName(saveAs.value),
131
+ createAsStrand.value!,
132
+
133
+ baseInputsObject[SS].slice(0, strandLengthInput[SS].value!).map((e) => e.value!),
134
+ baseInputsObject[AS].slice(0, strandLengthInput[AS].value!).map((e) => e.value!),
135
+
136
+ [firstPto[SS].value!].concat(ptoLinkages[SS].slice(0, strandLengthInput[SS].value!).map((e) => e.value!)),
137
+ [firstPto[AS].value!].concat(ptoLinkages[AS].slice(0, strandLengthInput[AS].value!).map((e) => e.value!)),
138
+
139
+ terminalModification[SS][THREE_PRIME].value,
140
+ terminalModification[SS][FIVE_PRIME].value,
141
+
142
+ terminalModification[AS][THREE_PRIME].value,
143
+ terminalModification[AS][FIVE_PRIME].value,
144
+
145
+ comment.value,
146
+ enumerateModifications,
147
+ ),
148
+ ]),
149
+ );
150
+ }
151
+
152
+ // todo: rename
153
+ function detectDefaultBasis(array: string[]) {
154
+ const modeMap: {[index: string]: number} = {};
155
+ let maxEl = array[0];
156
+ let maxCount = 1;
157
+ for (let i = 0; i < array.length; i++) {
158
+ const el = array[i];
159
+ if (modeMap[el] === null)
160
+ modeMap[el] = 1;
161
+ else
162
+ modeMap[el]++;
163
+ if (modeMap[el] > maxCount) {
164
+ maxEl = el;
165
+ maxCount = modeMap[el];
166
+ }
167
+ }
168
+ return maxEl;
169
+ }
170
+
171
+ async function parsePatternAndUpdateUi(newName: string) {
172
+ const pi = DG.TaskBarProgressIndicator.create('Loading pattern...');
173
+ await grok.dapi.userDataStorage.get(USER_STORAGE_KEY, false).then((entities) => {
174
+ const obj = JSON.parse(entities[newName]);
175
+ sequenceBase.value = detectDefaultBasis(obj[FIELD.AS_BASES].concat(obj[FIELD.SS_BASES]));
176
+ createAsStrand.value = (obj[FIELD.AS_BASES].length > 0);
177
+ saveAs.value = newName;
178
+
179
+ let fields = [FIELD.SS_BASES, FIELD.AS_BASES];
180
+ STRANDS.forEach((strand, i) => {
181
+ baseInputsObject[strand] = [];
182
+ const field = fields[i];
183
+ for (let j = 0; j < obj[field].length; j++)
184
+ baseInputsObject[strand].push(ui.choiceInput('', obj[field][j], baseChoices));
185
+ })
186
+
187
+ fields = [FIELD.SS_PTO, FIELD.AS_PTO];
188
+ STRANDS.forEach((s, i) => {
189
+ const field = fields[i];
190
+ firstPto[s].value = obj[field][0];
191
+ ptoLinkages[s] = [];
192
+ for (let j = 1; j < obj[field].length; j++)
193
+ ptoLinkages[s].push(ui.boolInput('', obj[field][j]));
194
+ });
195
+
196
+ fields = [FIELD.SS_BASES, FIELD.AS_BASES];
197
+ STRANDS.forEach((strand, i) => {
198
+ strandLengthInput[strand].value = obj[fields[i]].length;
199
+ })
200
+
201
+ const field = [[FIELD.SS_3, FIELD.SS_5], [FIELD.AS_3, FIELD.AS_5]];
202
+ STRANDS.forEach((strand, i) => {
203
+ TERMINAL_KEYS.forEach((terminal, j) => {
204
+ terminalModification[strand][terminal].value = obj[field[i][j]];
205
+ })
206
+ })
207
+ comment.value = obj[FIELD.COMMENT];
208
+ });
209
+ pi.close();
210
+ }
211
+
212
+ function allColumnValuesOfEqualLength(colName: string): boolean {
213
+ const col = tables.value!.getCol(colName);
214
+ let allLengthsAreTheSame = true;
215
+ for (let i = 1; i < col.length; i++) {
216
+ if (col.get(i - 1).length !== col.get(i).length && col.get(i).length !== 0) {
217
+ allLengthsAreTheSame = false;
218
+ break;
219
+ }
220
+ }
221
+ if (!allLengthsAreTheSame) {
222
+ const dialog = ui.dialog('Sequences lengths mismatch');
223
+ $(dialog.getButton('OK')).hide();
224
+ dialog
225
+ .add(ui.divText('The sequence length should match the number of Raw sequences in the input file'))
226
+ .add(ui.divText('\'ADD COLUMN\' to see sequences lengths'))
227
+ .addButton('ADD COLUMN', () => {
228
+ tables.value!.columns.addNewInt('Sequences lengths in ' + colName).init((j: number) => col.get(j).length);
229
+ grok.shell.info('Column with lengths added to \'' + tables.value!.name + '\'');
230
+ dialog.close();
231
+ grok.shell.v = grok.shell.getTableView(tables.value!.name);
232
+ })
233
+ .show();
234
+ }
235
+ if (col.get(0) !== strandLengthInput[SS].value) {
236
+ const d = ui.dialog('Length was updated by value to from imported file');
237
+ d.add(ui.divText('Latest modifications may not take effect during translation'))
238
+ .onOK(() => grok.shell.info('Lengths changed')).show();
239
+ }
240
+ return allLengthsAreTheSame;
241
+ }
242
+
243
+ async function getCurrentUserName(): Promise<string> {
244
+ return await grok.dapi.users.current().then((user) => {
245
+ return ' (created by ' + user.friendlyName + ')';
246
+ });
247
+ }
248
+
249
+ async function postPatternToUserStorage() {
250
+ const currUserName = await getCurrentUserName();
251
+ saveAs.value = (saveAs.stringValue.includes('(created by ')) ?
252
+ getShortName(saveAs.value) + currUserName :
253
+ saveAs.stringValue + currUserName;
254
+ return grok.dapi.userDataStorage.postValue(
255
+ USER_STORAGE_KEY,
256
+ saveAs.value,
257
+ JSON.stringify({
258
+ [FIELD.SS_BASES]: baseInputsObject[SS].slice(0, strandLengthInput[SS].value!).map((e) => e.value),
259
+ [FIELD.AS_BASES]: baseInputsObject[AS].slice(0, strandLengthInput[AS].value!).map((e) => e.value),
260
+ [FIELD.SS_PTO]: [firstPto[SS].value].concat(ptoLinkages[SS].slice(0, strandLengthInput[SS].value!).map((e) => e.value)),
261
+ [FIELD.AS_PTO]: [firstPto[AS].value].concat(ptoLinkages[AS].slice(0, strandLengthInput[AS].value!).map((e) => e.value)),
262
+ [FIELD.SS_3]: terminalModification[SS][THREE_PRIME].value,
263
+ [FIELD.SS_5]:terminalModification[SS][FIVE_PRIME].value,
264
+ [FIELD.AS_3]: terminalModification[AS][THREE_PRIME].value,
265
+ [FIELD.AS_5]: terminalModification[AS][FIVE_PRIME].value,
266
+ [FIELD.COMMENT]: comment.value,
267
+ }),
268
+ false,
269
+ ).then(() => grok.shell.info('Pattern \'' + saveAs.value + '\' was successfully uploaded!'));
270
+ }
271
+
272
+ async function updatePatternsList() {
273
+ grok.dapi.userDataStorage.get(USER_STORAGE_KEY, false).then(async (entities) => {
274
+ const lstMy: string[] = [];
275
+ const lstOthers: string[] = [];
276
+
277
+ // TODO: display short name, but use long for querying userdataStorage
278
+ for (const ent of Object.keys(entities)) {
279
+ if (await isCurrentUserCreatedThisPattern(ent))
280
+ lstOthers.push(ent);
281
+ else
282
+ lstMy.push(ent);//getShortName(ent));
283
+ }
284
+
285
+ let loadPattern = ui.choiceInput('Load Pattern', '', lstMy, (v: string) => parsePatternAndUpdateUi(v));
286
+
287
+ const currentUserName = (await grok.dapi.users.current()).friendlyName;
288
+ const otherUsers = 'Other users';
289
+
290
+ const patternListChoiceInput = ui.choiceInput('', currentUserName, [currentUserName, otherUsers], (v: string) => {
291
+ const currentList = v === currentUserName ? lstMy : lstOthers;
292
+ loadPattern = ui.choiceInput('Load Pattern', '', currentList, (v: string) => parsePatternAndUpdateUi(v));
293
+
294
+ loadPattern.root.append(patternListChoiceInput.input);
295
+ loadPattern.root.append(loadPattern.input);
296
+ // @ts-ignore
297
+ loadPattern.input.style.maxWidth = '100px';
298
+ loadPattern.setTooltip('Apply Existing Pattern');
299
+
300
+ loadPatternDiv.innerHTML = '';
301
+ loadPatternDiv.append(loadPattern.root);
302
+ loadPattern.root.append(
303
+ ui.div([
304
+ ui.button(ui.iconFA('trash-alt', () => {}), async () => {
305
+ if (loadPattern.value === null)
306
+ grok.shell.warning('Choose pattern to delete');
307
+ else if (await isCurrentUserCreatedThisPattern(saveAs.value))
308
+ grok.shell.warning('Cannot delete pattern, created by other user');
309
+ else {
310
+ await grok.dapi.userDataStorage.remove(USER_STORAGE_KEY, loadPattern.value, false)
311
+ .then(() => grok.shell.info('Pattern \'' + loadPattern.value + '\' deleted'));
312
+ }
313
+ await updatePatternsList();
314
+ }),
315
+ ], 'ui-input-options'),
316
+ );
317
+ });
318
+ loadPattern.root.append(patternListChoiceInput.input);
319
+ loadPattern.root.append(loadPattern.input);
320
+ // @ts-ignore
321
+ loadPattern.input.style.maxWidth = '100px';
322
+ loadPattern.setTooltip('Apply Existing Pattern');
323
+
324
+ loadPatternDiv.innerHTML = '';
325
+ loadPatternDiv.append(loadPattern.root);
326
+ loadPattern.root.append(
327
+ ui.div([
328
+ ui.button(ui.iconFA('trash-alt', () => {}), async () => {
329
+ if (loadPattern.value === null)
330
+ grok.shell.warning('Choose pattern to delete');
331
+ else if (await isCurrentUserCreatedThisPattern(saveAs.value))
332
+ grok.shell.warning('Cannot delete pattern, created by other user');
333
+ else {
334
+ await grok.dapi.userDataStorage.remove(USER_STORAGE_KEY, loadPattern.value, false)
335
+ .then(() => grok.shell.info('Pattern \'' + loadPattern.value + '\' deleted'));
336
+ }
337
+ await updatePatternsList();
338
+ }),
339
+ ], 'ui-input-options'),
340
+ );
341
+ });
342
+ }
343
+
344
+ async function savePattern() {
345
+ await grok.dapi.userDataStorage.get(USER_STORAGE_KEY, false)
346
+ .then((entities) => {
347
+ if (Object.keys(entities).includes(saveAs.value)) {
348
+ const dialog = ui.dialog('Pattern already exists');
349
+ $(dialog.getButton('OK')).hide();
350
+ dialog
351
+ .add(ui.divText('Pattern name \'' + saveAs.value + '\' already exists.'))
352
+ .add(ui.divText('Replace pattern?'))
353
+ .addButton('YES', async () => {
354
+ await grok.dapi.userDataStorage.remove(USER_STORAGE_KEY, saveAs.value, false)
355
+ .then(() => postPatternToUserStorage());
356
+ dialog.close();
357
+ })
358
+ .show();
359
+ } else
360
+ postPatternToUserStorage();
361
+ });
362
+ await updatePatternsList();
363
+ }
364
+
365
+ function validateStrandColumn(colName: string, strand: string): void {
366
+ const allLengthsAreTheSame: boolean = allColumnValuesOfEqualLength(colName);
367
+ const firstSequence = tables.value!.getCol(colName).get(0);
368
+ if (allLengthsAreTheSame && firstSequence.length !== strandLengthInput[strand].value)
369
+ strandLengthInput[strand].value = tables.value!.getCol(colName).get(0).length;
370
+ inputExample[strand].value = firstSequence;
371
+ }
372
+
373
+ function validateIdsColumn(colName: string) {
374
+ const col = tables.value!.getCol(colName);
375
+ if (col.type !== DG.TYPE.INT)
376
+ grok.shell.error('Column should contain integers only');
377
+ else if (col.categories.filter((e) => e !== '').length < col.toList().filter((e) => e !== '').length) {
378
+ const duplicates = findDuplicates(col.getRawData());
379
+ ui.dialog('Non-unique IDs')
380
+ .add(ui.divText('Press \'OK\' to select rows with non-unique values'))
381
+ .onOK(() => {
382
+ const selection = tables.value!.selection;
383
+ selection.init((i: number) => duplicates.indexOf(col.get(i)) > -1);
384
+ grok.shell.v = grok.shell.getTableView(tables.value!.name);
385
+ grok.shell.info('Rows are selected in table \'' + tables.value!.name + '\'');
386
+ })
387
+ .show();
388
+ }
389
+ }
390
+
391
+ const baseChoices: string[] = Object.keys(axolabsStyleMap);
392
+ const defaultBase: string = baseChoices[0];
393
+ const enumerateModifications = [defaultBase];
394
+ const sequenceBase = ui.choiceInput('Sequence Basis', defaultBase, baseChoices, (v: string) => {
395
+ updateBases(v);
396
+ updateOutputExamples();
397
+ });
398
+ const fullyPto = ui.boolInput('Fully PTO', DEFAULT_PTO, (v: boolean) => {
399
+ STRANDS.forEach((s) => { firstPto[s].value = v; })
400
+ updatePto(v);
401
+ updateOutputExamples();
402
+ });
403
+
404
+ const maxStrandLength = Object.fromEntries(STRANDS.map(
405
+ (strand) => [strand, DEFAULT_SEQUENCE_LENGTH]
406
+ ));
407
+ // todo: remove vague legacy 'items' from name
408
+ const modificationItems = Object.fromEntries(STRANDS.map(
409
+ (strand) => [strand, ui.div([])]
410
+ ));
411
+ const ptoLinkages = Object.fromEntries(STRANDS.map(
412
+ (strand) => [strand, Array<BooleanInput>(DEFAULT_SEQUENCE_LENGTH)
413
+ .fill(ui.boolInput('', DEFAULT_PTO))]
414
+ ));
415
+ const baseInputsObject = Object.fromEntries(STRANDS.map(
416
+ (strand) => {
417
+ const choiceInputs = Array<StringInput>(DEFAULT_SEQUENCE_LENGTH)
418
+ .fill(ui.choiceInput('', defaultBase, baseChoices));
419
+ return [strand, choiceInputs];
420
+ }
421
+ ));
422
+ const strandLengthInput = Object.fromEntries(STRANDS.map(
423
+ (strand) => {
424
+ const input = ui.intInput(`${strand} Length`, DEFAULT_SEQUENCE_LENGTH, () => updateUiForNewSequenceLength());
425
+ input.setTooltip(`Length of ${STRAND_NAME[strand].toLowerCase()}, including overhangs`);
426
+ return [strand, input];
427
+ }));
428
+ const strandVar = Object.fromEntries(STRANDS.map((strand) => [strand, '']));
429
+ // todo: rename to strandColumnInputDiv
430
+ const inputStrandColumnDiv = Object.fromEntries(STRANDS.map(
431
+ (strand) => [strand, ui.div([])]
432
+ ));
433
+ const inputExample = Object.fromEntries(STRANDS.map(
434
+ (strand) => [strand, ui.textInput(
435
+ `${STRAND_NAME[strand]}`, generateExample(strandLengthInput[strand].value!, sequenceBase.value!))
436
+ ]));
437
+
438
+ // todo: rename to strandColumnInput
439
+ const inputStrandColumn = Object.fromEntries(STRANDS.map((strand) => {
440
+ const input: StringInput = ui.choiceInput(`${STRAND_NAME[strand]} Column`, '', [], (colName: string) => {
441
+ validateStrandColumn(colName, strand);
442
+ strandVar[strand] = colName;
443
+ });
444
+ inputStrandColumnDiv[strand].append(input.root);
445
+ return [strand, input];
446
+ }));
447
+
448
+ const firstPto = Object.fromEntries(STRANDS.map((strand) => {
449
+ const input = ui.boolInput(`First ${strand} PTO`, fullyPto.value!, () => updateSvgScheme());
450
+ input.setTooltip(`ps linkage before first nucleotide of ${STRAND_NAME[strand].toLowerCase()}`);
451
+ return [strand, input];
452
+ }));
453
+
454
+ const terminalModification = Object.fromEntries(STRANDS.map((strand) => {
455
+ const inputs = Object.fromEntries(TERMINAL_KEYS.map((key) => {
456
+ const input = ui.stringInput(`${strand} ${TERMINAL[key]}\' Modification`, '', () => {
457
+ updateSvgScheme();
458
+ updateOutputExamples();
459
+ });
460
+ input.setTooltip(`Additional ${strand} ${TERMINAL[key]}\' Modification`);
461
+ return [key, input];
462
+ }));
463
+ return [strand, inputs];
464
+ }));
465
+
466
+ const outputExample = Object.fromEntries(STRANDS.map((strand) => {
467
+ const input = ui.textInput(' ', translateSequence(
468
+ inputExample[strand].value, baseInputsObject[strand], ptoLinkages[strand], terminalModification[strand][THREE_PRIME],terminalModification[strand][FIVE_PRIME], firstPto[strand].value!
469
+ ));
470
+ return [strand, input];
471
+ }));
472
+
473
+ const modificationSection = Object.fromEntries(STRANDS.map((strand) => {
474
+ const panel = ui.panel([
475
+ ui.h1(`${STRAND_NAME[strand]}`),
476
+ ui.divH([
477
+ ui.div([ui.divText('#')], {style: {width: '20px'}})!,
478
+ ui.block75([ui.divText('Modification')])!,
479
+ ui.div([ui.divText('PTO')])!,
480
+ ]),
481
+ modificationItems[strand],
482
+ ], {style: {paddingTop: '12px'}});
483
+ return [strand, panel];
484
+ }));
485
+
486
+ STRANDS.forEach((s) => {
487
+ inputExample[s].input.style.resize = 'none';
488
+ inputExample[s].input.style.minWidth = EXAMPLE_MIN_WIDTH;
489
+ outputExample[s].input.style.resize = 'none';
490
+ outputExample[s].input.style.minWidth = EXAMPLE_MIN_WIDTH;
491
+ // todo: remove ts-ignore
492
+ // @ts-ignore
493
+ outputExample[s].input.disabled = 'true';
494
+ outputExample[s].root.append(
495
+ ui.div([
496
+ ui.button(ui.iconFA('copy', () => {}), () => {
497
+ navigator.clipboard.writeText(outputExample[s].value).then(() =>
498
+ grok.shell.info('Sequence was copied to clipboard'));
499
+ }),
500
+ ], 'ui-input-options'),
501
+ );
502
+ })
503
+
504
+ const inputIdColumnDiv = ui.div([]);
505
+ const svgDiv = ui.div([]);
506
+ const asExampleDiv = ui.div([]);
507
+ const appAxolabsDescription = ui.div([]);
508
+ const loadPatternDiv = ui.div([]);
509
+ const asModificationDiv = ui.div([]);
510
+ const isEnumerateModificationsDiv = ui.divH([
511
+ ui.boolInput(defaultBase, true, (v: boolean) => {
512
+ if (v) {
513
+ if (!enumerateModifications.includes(defaultBase))
514
+ enumerateModifications.push(defaultBase);
515
+ } else {
516
+ const index = enumerateModifications.indexOf(defaultBase, 0);
517
+ if (index > -1)
518
+ enumerateModifications.splice(index, 1);
519
+ }
520
+ updateSvgScheme();
521
+ updateOutputExamples();
522
+ }).root,
523
+ ]);
524
+
525
+ const asLengthDiv = ui.div([strandLengthInput[AS].root]);
526
+
527
+ const tables = ui.tableInput('Tables', grok.shell.tables[0], grok.shell.tables, (t: DG.DataFrame) => {
528
+ STRANDS.forEach((strand) => {
529
+ inputStrandColumn[strand] = ui.choiceInput(`${strand} Column`, '', t.columns.names(), (colName: string) => {
530
+ validateStrandColumn(colName, strand);
531
+ strandVar[strand] = colName;
532
+ });
533
+ inputStrandColumnDiv[strand].innerHTML = '';
534
+ inputStrandColumnDiv[strand].append(inputStrandColumn[strand].root);
535
+ })
536
+
537
+ // todo: unify with inputStrandColumn
538
+ const inputIdColumn = ui.choiceInput('ID Column', '', t.columns.names(), (colName: string) => {
539
+ validateIdsColumn(colName);
540
+ idVar = colName;
541
+ });
542
+ inputIdColumnDiv.innerHTML = '';
543
+ inputIdColumnDiv.append(inputIdColumn.root);
544
+ });
545
+
546
+
547
+ // todo: unify with strandVar
548
+ let idVar = '';
549
+ const inputIdColumn = ui.choiceInput('ID Column', '', [], (colName: string) => {
550
+ validateIdsColumn(colName);
551
+ idVar = colName;
552
+ });
553
+ inputIdColumnDiv.append(inputIdColumn.root);
554
+
555
+ updatePatternsList();
556
+
557
+ const createAsStrand = ui.boolInput('Create AS Strand', true, (v: boolean) => {
558
+ modificationSection[AS].hidden = !v;
559
+ inputStrandColumnDiv[AS].hidden = !v;
560
+ asLengthDiv.hidden = !v;
561
+ asModificationDiv.hidden = !v;
562
+ asExampleDiv.hidden = !v;
563
+ firstPto[AS].root.hidden = !v;
564
+ updateSvgScheme();
565
+ });
566
+ createAsStrand.setTooltip('Create antisense strand sections on SVG and table to the right');
567
+
568
+ const saveAs = ui.textInput('Save As', 'Pattern Name', () => updateSvgScheme());
569
+ saveAs.setTooltip('Name Of New Pattern');
570
+
571
+
572
+ TERMINAL_KEYS.forEach((terminal) => {
573
+ asModificationDiv.append(terminalModification[AS][terminal].root);
574
+ })
575
+
576
+ const comment = ui.textInput('Comment', '', () => updateSvgScheme());
577
+
578
+ const savePatternButton = ui.button('Save', () => {
579
+ if (saveAs.value !== '')
580
+ savePattern().then(() => grok.shell.info('Pattern saved'));
581
+ else {
582
+ const name = ui.stringInput('Enter Name', '');
583
+ ui.dialog('Pattern Name')
584
+ .add(name.root)
585
+ .onOK(() => {
586
+ saveAs.value = name.value;
587
+ savePattern().then(() => grok.shell.info('Pattern saved'));
588
+ })
589
+ .show();
590
+ }
591
+ });
592
+
593
+ const convertSequenceButton = ui.button('Convert Sequences', () => {
594
+ const condition = [true, createAsStrand.value];
595
+ if (STRANDS.some((s, i) => condition[i] && strandVar[s] === ''))
596
+ grok.shell.info('Please select table and columns on which to apply pattern');
597
+ else if (STRANDS.some((s) => strandLengthInput[s].value !== inputExample[s].value.length)) {
598
+ const dialog = ui.dialog('Length Mismatch');
599
+ $(dialog.getButton('OK')).hide();
600
+ dialog
601
+ .add(ui.divText('Length of sequences in columns doesn\'t match entered length. Update length value?'))
602
+ .addButton('YES', () => {
603
+ STRANDS.forEach((s) => {
604
+ strandLengthInput[s].value = tables.value!.getCol(inputStrandColumn[s].value!).getString(0).length;
605
+ })
606
+ dialog.close();
607
+ })
608
+ .show();
609
+ } else {
610
+ if (idVar !== '')
611
+ addColumnWithIds(tables.value!.name, idVar, getShortName(saveAs.value));
612
+ const condition = [true, createAsStrand.value];
613
+ STRANDS.forEach((strand, i) => {
614
+ if (condition[i])
615
+ addColumnWithTranslatedSequences(
616
+ tables.value!.name, strandVar[strand], baseInputsObject[strand], ptoLinkages[strand],
617
+ terminalModification[strand][FIVE_PRIME], terminalModification[strand][THREE_PRIME], firstPto[strand].value!);
618
+ })
619
+ grok.shell.v = grok.shell.getTableView(tables.value!.name);
620
+ grok.shell.info(((createAsStrand.value) ? 'Columns were' : 'Column was') +
621
+ ' added to table \'' + tables.value!.name + '\'');
622
+ updateOutputExamples();
623
+ }
624
+ });
625
+
626
+ asExampleDiv.append(inputExample[AS].root);
627
+ asExampleDiv.append(outputExample[AS].root);
628
+
629
+ updateUiForNewSequenceLength();
630
+
631
+ const exampleSection = ui.div([
632
+ ui.h1('Conversion preview'),
633
+ inputExample[SS].root,
634
+ outputExample[SS].root,
635
+ asExampleDiv,
636
+ ], 'ui-form');
637
+
638
+ const inputsSection = ui.div([
639
+ ui.h1('Convert options'),
640
+ ui.divH([
641
+ tables.root,
642
+ inputStrandColumnDiv[SS],
643
+ ]),
644
+ ui.divH([
645
+ inputStrandColumnDiv[AS],
646
+ inputIdColumnDiv,
647
+ ]),
648
+ ui.buttonsInput([
649
+ convertSequenceButton,
650
+ ]),
651
+ ], 'ui-form');
652
+
653
+ const downloadButton = ui.button('Download', () => svg.saveSvgAsPng(document.getElementById('mySvg'), saveAs.value,
654
+ {backgroundColor: 'white'}));
655
+
656
+ const mainSection = ui.panel([
657
+ ui.block([
658
+ svgDiv,
659
+ ], {style: {overflowX: 'scroll'}}),
660
+ downloadButton,
661
+ isEnumerateModificationsDiv,
662
+ ui.div([
663
+ ui.div([
664
+ ui.divH([
665
+ ui.h1('Pattern options'),
666
+ ]),
667
+ ui.divH([
668
+ ui.div([
669
+ strandLengthInput[SS].root,
670
+ asLengthDiv,
671
+ sequenceBase.root,
672
+ comment.root,
673
+ loadPatternDiv,
674
+ saveAs.root,
675
+ ui.buttonsInput([
676
+ savePatternButton,
677
+ ]),
678
+ ], 'ui-form'),
679
+ ui.div([
680
+ createAsStrand.root,
681
+ fullyPto.root,
682
+ firstPto[SS].root,
683
+ firstPto[AS].root,
684
+ terminalModification[SS][FIVE_PRIME].root,
685
+ terminalModification[SS][THREE_PRIME].root,
686
+ asModificationDiv,
687
+ ], 'ui-form'),
688
+ ], 'ui-form'),
689
+ ], 'ui-form'),
690
+ inputsSection,
691
+ exampleSection,
692
+ ], {style: {flexWrap: 'wrap'}}),
693
+ ]);
694
+
695
+ const info = ui.info(
696
+ [
697
+ ui.divText('\n How to define new pattern:', {style: {'font-weight': 'bolder'}}),
698
+ ui.divText('1. Choose table and columns with sense and antisense strands'),
699
+ ui.divText('2. Choose lengths of both strands by editing checkboxes below'),
700
+ ui.divText('3. Choose basis and PTO status for each nucleotide'),
701
+ ui.divText('4. Set additional modifications for sequence edges'),
702
+ ui.divText('5. Press \'Convert Sequences\' button'),
703
+ ui.divText('This will add the result column(s) to the right of the table'),
704
+ ], 'Create and apply Axolabs translation patterns.',
705
+ );
706
+
707
+ return ui.splitH([
708
+ ui.div([
709
+ appAxolabsDescription,
710
+ mainSection!,
711
+ ])!,
712
+ ui.box(
713
+ ui.divH([
714
+ modificationSection[SS],
715
+ modificationSection[AS],
716
+ ]), {style: {maxWidth: '360px'}},
717
+ ),
718
+ ]);
719
+ }
720
+ }