@datagrok/helm 2.1.34 → 2.2.1
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/.eslintrc.json +1 -1
- package/CHANGELOG.md +30 -0
- package/dist/package-test.js +1 -1
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/package.json +12 -12
- package/src/cell-renderer.ts +21 -153
- package/src/constants.ts +0 -11
- package/src/helm-helper.ts +9 -10
- package/src/helm-monomer-placer.ts +21 -85
- package/src/helm-web-editor.ts +5 -2
- package/src/package-test.ts +6 -2
- package/src/package-utils.ts +150 -0
- package/src/package.ts +96 -100
- package/src/tests/get-monomer-tests.ts +210 -0
- package/src/tests/helm-service-tests.ts +97 -0
- package/src/tests/helm-tests.ts +1 -0
- package/src/tests/properties-widget-tests.ts +4 -2
- package/src/tests/renderers-tests.ts +97 -22
- package/src/types/dojo.ts +3 -0
- package/src/types/index.ts +0 -0
- package/src/utils/dummy-monomer.ts +184 -0
- package/src/utils/get-hovered.ts +104 -0
- package/src/utils/get-monomer.ts +111 -0
- package/src/utils/helm-grid-cell-renderer.ts +199 -0
- package/src/utils/helm-service.ts +130 -0
- package/src/utils/index.ts +7 -7
- package/webpack.config.js +7 -0
- /package/src/tests/{get-molfiles.ts → get-molfiles-tests.ts} +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
import * as DG from 'datagrok-api/dg';
|
|
4
|
+
|
|
5
|
+
import * as org from 'org';
|
|
6
|
+
|
|
7
|
+
import {Monomer} from '@datagrok-libraries/bio/src/types/index';
|
|
8
|
+
import {
|
|
9
|
+
HELM_REQUIRED_FIELD as REQ, HELM_OPTIONAL_FIELDS as OPT, HELM_RGROUP_FIELDS as RGP,
|
|
10
|
+
} from '@datagrok-libraries/bio/src/utils/const';
|
|
11
|
+
|
|
12
|
+
export function getRS(smiles: string) {
|
|
13
|
+
const newS = smiles.match(/(?<=\[)[^\][]*(?=])/gm);
|
|
14
|
+
const res: { [name: string]: string } = {};
|
|
15
|
+
let el = '';
|
|
16
|
+
let digit;
|
|
17
|
+
if (!!newS) {
|
|
18
|
+
for (let i = 0; i < newS.length; i++) {
|
|
19
|
+
if (newS[i] != null) {
|
|
20
|
+
if (/\d/.test(newS[i])) {
|
|
21
|
+
digit = newS[i][newS[i].length - 1];
|
|
22
|
+
newS[i] = newS[i].replace(/[0-9]/g, '');
|
|
23
|
+
for (let j = 0; j < newS[i].length; j++) {
|
|
24
|
+
if (newS[i][j] != ':')
|
|
25
|
+
el += newS[i][j];
|
|
26
|
+
}
|
|
27
|
+
res['R' + digit] = el;
|
|
28
|
+
el = '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return res;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class LibraryWebEditorMonomer implements org.helm.WebEditorMonomer {
|
|
37
|
+
public get rs(): number { return Object.keys(this.at).length; }
|
|
38
|
+
|
|
39
|
+
/* eslint-disable max-params */
|
|
40
|
+
protected constructor(
|
|
41
|
+
public readonly id: string,
|
|
42
|
+
public readonly m: string,
|
|
43
|
+
public readonly n: string,
|
|
44
|
+
public readonly na: string | undefined,
|
|
45
|
+
public readonly type: org.helm.PolymerType,
|
|
46
|
+
public readonly mt: org.helm.MonomerType,
|
|
47
|
+
public readonly at: org.helm.WebEditorRGroups,
|
|
48
|
+
) /* eslint-enable max-params */ {}
|
|
49
|
+
|
|
50
|
+
static fromMonomer(biotype: org.helm.HelmType, monomer: Monomer): org.helm.WebEditorMonomer {
|
|
51
|
+
let at: org.helm.WebEditorRGroups = {};
|
|
52
|
+
const smiles = monomer[REQ.SMILES];
|
|
53
|
+
if (monomer.rgroups.length > 0) {
|
|
54
|
+
monomer.rgroups.forEach((it) => {
|
|
55
|
+
at[it[RGP.LABEL]] = it[RGP.CAP_GROUP_NAME];
|
|
56
|
+
});
|
|
57
|
+
} else if (smiles) {
|
|
58
|
+
// Generate R-Groups from SMILES
|
|
59
|
+
at = getRS(smiles);
|
|
60
|
+
} else {
|
|
61
|
+
// broken
|
|
62
|
+
return new BrokenWebEditorMonomer(biotype, monomer[REQ.SYMBOL]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return new LibraryWebEditorMonomer(
|
|
66
|
+
monomer[REQ.SYMBOL],
|
|
67
|
+
monomer[REQ.MOLFILE],
|
|
68
|
+
monomer[REQ.NAME],
|
|
69
|
+
monomer[OPT.NATURAL_ANALOG],
|
|
70
|
+
monomer[REQ.POLYMER_TYPE],
|
|
71
|
+
monomer[REQ.MONOMER_TYPE],
|
|
72
|
+
at);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* eslint-disable camelcase */
|
|
77
|
+
export abstract class DummyWebEditorMonomer implements org.helm.WebEditorMonomer {
|
|
78
|
+
//@formatter:off
|
|
79
|
+
public abstract get backgroundcolor(): string | undefined;
|
|
80
|
+
public abstract get linecolor(): string | undefined;
|
|
81
|
+
public abstract get textcolor(): string | undefined;
|
|
82
|
+
//@formatter:on
|
|
83
|
+
|
|
84
|
+
/** R-Group index os single digit only is allowed in Pistoia code */
|
|
85
|
+
public readonly at: org.helm.WebEditorRGroups = {
|
|
86
|
+
R1: 'H', R2: 'H', R3: 'H', R4: 'H', R5: 'H', R6: 'H', R7: 'H', R8: 'H', R9: 'H'
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
public get rs(): number { return Object.keys(this.at).length; }
|
|
90
|
+
|
|
91
|
+
protected constructor(
|
|
92
|
+
public readonly biotype: string,
|
|
93
|
+
/** symbol */ public readonly id: string,
|
|
94
|
+
/** name */ public readonly n: string | undefined = 'missing',
|
|
95
|
+
/** molfile */ public readonly m: string | undefined = undefined,
|
|
96
|
+
/* Pistoia.HELM deletes .type and .mt in Monomers.addOneMonomer() */
|
|
97
|
+
/** polymer type */ public readonly type: org.helm.PolymerType | undefined = undefined,
|
|
98
|
+
/** monomer type */ public readonly mt: org.helm.MonomerType | undefined = undefined,
|
|
99
|
+
) {
|
|
100
|
+
if (!this.id)
|
|
101
|
+
throw new Error('Invalid arg undefined [id].');
|
|
102
|
+
|
|
103
|
+
// return new Proxy(this, {
|
|
104
|
+
// get: (target: any, prop: string | symbol, _receiver: any) => {
|
|
105
|
+
// const logPrefix = `${this.toLog()}.proxy.get( '${String(prop)}' )`;
|
|
106
|
+
// switch (prop) {
|
|
107
|
+
// case 'id':
|
|
108
|
+
// case 'at':
|
|
109
|
+
// case 'na':
|
|
110
|
+
// case 'nature':
|
|
111
|
+
// case 'backgroundcolor':
|
|
112
|
+
// case 'linecolor':
|
|
113
|
+
// case 'textcolor': {
|
|
114
|
+
// // these attributes are known and handled
|
|
115
|
+
// return target[prop];
|
|
116
|
+
// }
|
|
117
|
+
// case 'rs': {
|
|
118
|
+
// /* R-Group count (?), 2 is default for monomer "?" */
|
|
119
|
+
// throw new Error('Not supported get "rs" attribute.');
|
|
120
|
+
// }
|
|
121
|
+
// default:
|
|
122
|
+
// // Unexpected attribute requested
|
|
123
|
+
// _package.logger.warning(`${logPrefix}`);
|
|
124
|
+
// return target[prop];
|
|
125
|
+
// }
|
|
126
|
+
// },
|
|
127
|
+
// set: (target, prop, value: any, _receiver: any): boolean => {
|
|
128
|
+
// throw new Error(`Not supported set '${String(prop)}' attribute, any.`);
|
|
129
|
+
// },
|
|
130
|
+
// apply: (target: any, thisArg: any, argArray: any[]): any | undefined => {
|
|
131
|
+
// throw new Error(`Not supported call (apply) '${thisArg.toString()}' method, any.`);
|
|
132
|
+
// }
|
|
133
|
+
// });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private static objCounter: number = -1;
|
|
137
|
+
private readonly objId: number = ++DummyWebEditorMonomer.objCounter;
|
|
138
|
+
|
|
139
|
+
private readonly className: string = 'DummyWebEditorMonomer';
|
|
140
|
+
|
|
141
|
+
protected toLog(): string {
|
|
142
|
+
return `Helm: ${this.className}<${this.objId}>`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export class GapWebEditorMonomer extends DummyWebEditorMonomer {
|
|
147
|
+
public readonly backgroundcolor: string = '#FFFFFF';
|
|
148
|
+
public readonly linecolor: string = '#808080';
|
|
149
|
+
public readonly textcolor: string = '#808080';
|
|
150
|
+
|
|
151
|
+
constructor(biotype: string, id: string) {
|
|
152
|
+
super(biotype, id, 'gap');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export class AmbiguousWebEditorMonomer extends DummyWebEditorMonomer {
|
|
157
|
+
public readonly backgroundcolor: string = '#808080';
|
|
158
|
+
public readonly linecolor: string = '#000000';
|
|
159
|
+
public readonly textcolor: string = '#000000';
|
|
160
|
+
|
|
161
|
+
constructor(biotype: string, id: string) {
|
|
162
|
+
super(biotype, id, 'ambiguous');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export class MissingWebEditorMonomer extends DummyWebEditorMonomer {
|
|
167
|
+
public readonly backgroundcolor: string = '#FF4444';
|
|
168
|
+
public readonly linecolor: string = '#800000';
|
|
169
|
+
public readonly textcolor: string = '#FFFFFF';
|
|
170
|
+
|
|
171
|
+
constructor(biotype: string, id: string) {
|
|
172
|
+
super(biotype, id, 'missing');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class BrokenWebEditorMonomer extends DummyWebEditorMonomer {
|
|
177
|
+
public readonly backgroundcolor: string = '#FFFF44';
|
|
178
|
+
public readonly linecolor: string = '#800000';
|
|
179
|
+
public readonly textcolor: string = '#000000';
|
|
180
|
+
|
|
181
|
+
constructor(biotype: string, id: string) {
|
|
182
|
+
super(biotype, id, 'broken');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as DG from 'datagrok-api/dg';
|
|
3
|
+
import * as ui from 'datagrok-api/ui';
|
|
4
|
+
|
|
5
|
+
import * as JSDraw2 from 'JSDraw2';
|
|
6
|
+
|
|
7
|
+
import {helmTypeToPolymerType} from '@datagrok-libraries/bio/src/monomer-works/monomer-works';
|
|
8
|
+
import {PolymerType} from '@datagrok-libraries/bio/src/types';
|
|
9
|
+
|
|
10
|
+
import {HelmMonomerPlacer, ISeqMonomer} from '../helm-monomer-placer';
|
|
11
|
+
|
|
12
|
+
export function getHoveredMonomerFromEditorMol(
|
|
13
|
+
argsX: number, argsY: number, gridCell: DG.GridCell, mol: JSDraw2.IEditorMol
|
|
14
|
+
): ISeqMonomer | null {
|
|
15
|
+
let hoveredSeqMonomer: ISeqMonomer | null = null;
|
|
16
|
+
|
|
17
|
+
/** @return {[number, number]} [atom, distance] */
|
|
18
|
+
function getNearest(excluded: (number | undefined)[]): [number | undefined, number | undefined] {
|
|
19
|
+
let atom: number | undefined = undefined;
|
|
20
|
+
let distance: number | undefined = undefined;
|
|
21
|
+
for (let atomI = 0; atomI < mol.atoms.length; ++atomI) {
|
|
22
|
+
if (!excluded.includes(atomI)) {
|
|
23
|
+
const aX: number = mol.atoms[atomI].p.x;
|
|
24
|
+
const aY: number = mol.atoms[atomI].p.y;
|
|
25
|
+
const distanceToAtomI: number = Math.sqrt((argsX - aX) ** 2 + (argsY - aY) ** 2);
|
|
26
|
+
if (distance === undefined || distance > distanceToAtomI) {
|
|
27
|
+
atom = atomI;
|
|
28
|
+
distance = distanceToAtomI;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return [atom, distance];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const [firstAtomI, firstDistance] = getNearest([]);
|
|
36
|
+
const [secondAtomI, secondDistance] = getNearest([firstAtomI]);
|
|
37
|
+
|
|
38
|
+
if (firstAtomI !== undefined && firstDistance !== undefined) {
|
|
39
|
+
const firstAtom = mol.atoms[firstAtomI];
|
|
40
|
+
const firstSeqMonomer = getSeqMonomerFromHelmAtom(firstAtom);
|
|
41
|
+
if (secondAtomI !== undefined && secondDistance !== undefined) {
|
|
42
|
+
if (firstDistance < secondDistance * 0.45)
|
|
43
|
+
hoveredSeqMonomer = firstSeqMonomer;
|
|
44
|
+
} else {
|
|
45
|
+
if (firstDistance < 0.35 * gridCell.bounds.height)
|
|
46
|
+
hoveredSeqMonomer = firstSeqMonomer;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return hoveredSeqMonomer;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getHoveredMonomerFallback(
|
|
53
|
+
argsX: number, _argsY: number, gridCell: DG.GridCell, helmPlacer: HelmMonomerPlacer
|
|
54
|
+
): ISeqMonomer | null {
|
|
55
|
+
let hoveredSeqMonomer: ISeqMonomer | null = null;
|
|
56
|
+
const [allParts, lengths, sumLengths] = helmPlacer.getCellAllPartsLengths(gridCell.tableRowIndex!);
|
|
57
|
+
const maxIndex = Object.values(lengths).length - 1;
|
|
58
|
+
let left = 0;
|
|
59
|
+
let right = maxIndex;
|
|
60
|
+
let found = false;
|
|
61
|
+
let iterCount: number = 0;
|
|
62
|
+
|
|
63
|
+
let mid = 0;
|
|
64
|
+
if (argsX > sumLengths[0]) {
|
|
65
|
+
while (!found && iterCount < sumLengths.length) {
|
|
66
|
+
mid = Math.floor((right + left) / 2);
|
|
67
|
+
if (argsX >= sumLengths[mid] && argsX <= sumLengths[mid + 1]) {
|
|
68
|
+
left = mid;
|
|
69
|
+
found = true;
|
|
70
|
+
} else if (argsX < sumLengths[mid])
|
|
71
|
+
right = mid - 1;
|
|
72
|
+
else if (argsX > sumLengths[mid + 1])
|
|
73
|
+
left = mid + 1;
|
|
74
|
+
|
|
75
|
+
if (left == right)
|
|
76
|
+
found = true;
|
|
77
|
+
|
|
78
|
+
iterCount++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
left = (argsX >= sumLengths[left]) ? left : left - 1; // correct left to between sumLengths
|
|
82
|
+
if (left >= 0)
|
|
83
|
+
hoveredSeqMonomer = getSeqMonomerFromHelm(allParts[0], allParts[left]);
|
|
84
|
+
return hoveredSeqMonomer;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getSeqMonomerFromHelmAtom(atom: JSDraw2.IEditorMolAtom): ISeqMonomer {
|
|
88
|
+
const polymerType = helmTypeToPolymerType(atom.bio.type);
|
|
89
|
+
return {symbol: atom.elem, polymerType: polymerType};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getSeqMonomerFromHelm(
|
|
93
|
+
helmPrefix: string, symbol: string
|
|
94
|
+
): ISeqMonomer {
|
|
95
|
+
let resSeqMonomer: ISeqMonomer | undefined = undefined;
|
|
96
|
+
const polymerTypeList: PolymerType[] = ['RNA', 'PEPTIDE', 'CHEM', 'BLOB', 'G'];
|
|
97
|
+
for (const polymerType of polymerTypeList) {
|
|
98
|
+
if (helmPrefix.startsWith(polymerType))
|
|
99
|
+
resSeqMonomer = {symbol: symbol, polymerType: polymerType};
|
|
100
|
+
}
|
|
101
|
+
if (!resSeqMonomer)
|
|
102
|
+
throw new Error(`Monomer not found for symbol = '${symbol}' and helmPrefix = '${helmPrefix}'.`);
|
|
103
|
+
return resSeqMonomer;
|
|
104
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
import * as DG from 'datagrok-api/dg';
|
|
4
|
+
|
|
5
|
+
import * as org from 'org';
|
|
6
|
+
import * as scil from 'scil';
|
|
7
|
+
import * as JSDraw2 from 'JSDraw2';
|
|
8
|
+
import Atom = JSDraw2.Atom;
|
|
9
|
+
|
|
10
|
+
import {ILogger} from '@datagrok-libraries/bio/src/utils/logger';
|
|
11
|
+
import {HelmType, IMonomerLib, Monomer, WebEditorMonomer} from '@datagrok-libraries/bio/src/types';
|
|
12
|
+
import {helmTypeToPolymerType} from '@datagrok-libraries/bio/src/monomer-works/monomer-works';
|
|
13
|
+
import {PolymerTypes, HelmTypes} from '@datagrok-libraries/bio/src/utils/const';
|
|
14
|
+
import {
|
|
15
|
+
HELM_REQUIRED_FIELD as REQ
|
|
16
|
+
} from '@datagrok-libraries/bio/src/utils/const';
|
|
17
|
+
|
|
18
|
+
import {AmbiguousWebEditorMonomer, GapWebEditorMonomer, LibraryWebEditorMonomer} from './dummy-monomer';
|
|
19
|
+
|
|
20
|
+
const monomerRe = /[\w()]+/;
|
|
21
|
+
//** Do not mess with monomer symbol with parenthesis enclosed in square brackets */
|
|
22
|
+
const ambMonomerRe = RegExp(String.raw`\(${monomerRe}(,${monomerRe})+\)`);
|
|
23
|
+
|
|
24
|
+
export type GetMonomerResType = WebEditorMonomer | null;
|
|
25
|
+
|
|
26
|
+
export type GetMonomerFunc = (a: Atom<HelmType> | HelmType, name: string | undefined) => GetMonomerResType;
|
|
27
|
+
type GetMonomerOverridingFunc = (
|
|
28
|
+
a: Atom<HelmType> | HelmType, name: string, monomerLib: IMonomerLib,
|
|
29
|
+
originalGetMonomer: (a: Atom<HelmType> | HelmType, name: string) => GetMonomerResType) => GetMonomerResType;
|
|
30
|
+
|
|
31
|
+
export function getMonomerOverrideAndLogAlert(
|
|
32
|
+
monomerLib: IMonomerLib, getMonomerOverriding: GetMonomerOverridingFunc, trigger: () => void, logger: ILogger,
|
|
33
|
+
): void {
|
|
34
|
+
const logPrefix = 'Helm: overrideGetMonomerAndAlert()';
|
|
35
|
+
const monomers = org.helm.webeditor.Monomers;
|
|
36
|
+
const getMonomerOriginal = monomers.getMonomer.bind(monomers);
|
|
37
|
+
const alertOriginal = scil.Utils.alert;
|
|
38
|
+
try {
|
|
39
|
+
org.helm.webeditor.Monomers.getMonomer = (
|
|
40
|
+
a: Atom<HelmType> | HelmType, name: string
|
|
41
|
+
): GetMonomerResType => {
|
|
42
|
+
return getMonomerOverriding(a, name, monomerLib, getMonomerOriginal);
|
|
43
|
+
};
|
|
44
|
+
// Preventing alert message box for missing monomers with compressed Scilligence.JSDraw2.Lite.js
|
|
45
|
+
scil.Utils.alert = (s: string): void => {
|
|
46
|
+
logger.warning(`${logPrefix}, scil.Utils.alert() s = 's'.`);
|
|
47
|
+
};
|
|
48
|
+
trigger();
|
|
49
|
+
} finally {
|
|
50
|
+
monomers.getMonomer = getMonomerOriginal;
|
|
51
|
+
scil.Utils.alert = alertOriginal;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Inputs logic */
|
|
56
|
+
export function getMonomerHandleArgs(
|
|
57
|
+
a: Atom<HelmType> | HelmType, name?: string
|
|
58
|
+
): [/** biotype */ HelmType, /** elem */ string] {
|
|
59
|
+
let biotype: HelmType;
|
|
60
|
+
let elem: string;
|
|
61
|
+
if ((a as Atom<HelmType>).T === 'ATOM') {
|
|
62
|
+
biotype = (a as Atom<HelmType>).biotype();
|
|
63
|
+
elem = (a as Atom<HelmType>).elem;
|
|
64
|
+
} else {
|
|
65
|
+
biotype = a as HelmType;
|
|
66
|
+
elem = org.helm.webeditor.IO.trimBracket(name!);
|
|
67
|
+
}
|
|
68
|
+
return [biotype, elem];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
/** Substitutes {@link org.helm.webeditor.Monomers.getMonomer()} */
|
|
73
|
+
export function getWebEditorMonomer(
|
|
74
|
+
monomerLib: IMonomerLib,
|
|
75
|
+
a: Atom<HelmType> | HelmType, argName: string,
|
|
76
|
+
): WebEditorMonomer | null {
|
|
77
|
+
const [biotype, elem] = getMonomerHandleArgs(a, argName);
|
|
78
|
+
const pt = helmTypeToPolymerType(biotype);
|
|
79
|
+
|
|
80
|
+
/** Get or create {@link Monomer} object (in case it is missing in monomer library current config) */
|
|
81
|
+
let m: Monomer | null = monomerLib.getMonomer(pt, elem);
|
|
82
|
+
if (m && biotype == HelmTypes.LINKER && m[REQ.RGROUPS].length != 2) {
|
|
83
|
+
// Web Editor expects null
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (m && biotype == HelmTypes.SUGAR && m[REQ.RGROUPS].length != 3) {
|
|
87
|
+
// Web Editor expects null
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (!m /* && biotype != HelmTypes.LINKER*/)
|
|
91
|
+
m = monomerLib.addMissingMonomer(pt, elem);
|
|
92
|
+
|
|
93
|
+
/** Get or create {@link org,helm.WebEditorMonomer} */
|
|
94
|
+
let resWem: WebEditorMonomer | undefined = m.wem;
|
|
95
|
+
if (!resWem) {
|
|
96
|
+
if (elem === '*')
|
|
97
|
+
resWem = m.wem = new GapWebEditorMonomer(biotype, elem);
|
|
98
|
+
else if (
|
|
99
|
+
(biotype === 'HELM_NUCLETIDE' && elem === 'N') ||
|
|
100
|
+
(biotype === 'HELM_AA' && elem === 'X') ||
|
|
101
|
+
(biotype === 'HELM_CHEM' && false) || // TODO: Ambiguous monomer for CHEM
|
|
102
|
+
ambMonomerRe.test(elem) // e.g. (A,R,_)
|
|
103
|
+
)
|
|
104
|
+
resWem = m.wem = new AmbiguousWebEditorMonomer(biotype, elem);
|
|
105
|
+
|
|
106
|
+
if (!resWem)
|
|
107
|
+
resWem = m.wem = LibraryWebEditorMonomer.fromMonomer(biotype, m);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return resWem;
|
|
111
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as DG from 'datagrok-api/dg';
|
|
3
|
+
import * as ui from 'datagrok-api/ui';
|
|
4
|
+
|
|
5
|
+
import * as JSDraw2 from 'JSDraw2';
|
|
6
|
+
import wu from 'wu';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
CellRendererBackAsyncBase, RenderServiceBase
|
|
10
|
+
} from '@datagrok-libraries/bio/src/utils/cell-renderer-async-base';
|
|
11
|
+
import {HelmAux, HelmProps} from '@datagrok-libraries/bio/src/viewers/helm-service';
|
|
12
|
+
|
|
13
|
+
import {ISeqMonomer} from '../helm-monomer-placer';
|
|
14
|
+
import {getHoveredMonomerFromEditorMol} from './get-hovered';
|
|
15
|
+
import {_getHelmService} from '../package-utils';
|
|
16
|
+
import {errInfo} from '@datagrok-libraries/bio/src/utils/err-info';
|
|
17
|
+
import {ILogger} from '@datagrok-libraries/bio/src/utils/logger';
|
|
18
|
+
import {getGridCellRendererBack} from '@datagrok-libraries/bio/src/utils/cell-renderer-back-base';
|
|
19
|
+
|
|
20
|
+
import {_package, getMonomerLib} from '../package';
|
|
21
|
+
|
|
22
|
+
class WrapLogger implements ILogger {
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly base: ILogger
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
error(message: any, params?: object | undefined, stackTrace?: string | undefined): void {
|
|
28
|
+
this.base.error(message, params, stackTrace);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
warning(message: string, params?: object | undefined): void {
|
|
32
|
+
this.base.warning(message, params);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
info(message: string, params?: object | undefined): void {
|
|
36
|
+
// this.base.info(message, params);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
debug(message: string, params?: object | undefined): void {
|
|
40
|
+
// this.base.debug(message, params);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class HelmGridCellRendererBack extends CellRendererBackAsyncBase<HelmProps, HelmAux> {
|
|
45
|
+
private _auxList: (HelmAux | null)[];
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
gridCol: DG.GridColumn | null,
|
|
49
|
+
tableCol: DG.Column<string>,
|
|
50
|
+
) {
|
|
51
|
+
super(gridCol, tableCol, new WrapLogger(_package.logger) /* _package.logger */, true);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected override reset(): void {
|
|
55
|
+
super.reset();
|
|
56
|
+
this._auxList = new Array<HelmAux | null>(this.tableCol.length).fill(null);
|
|
57
|
+
if (this.gridCol && this.gridCol.dart) this.gridCol.grid?.invalidate();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
protected override getRenderService(): RenderServiceBase<HelmProps, HelmAux> {
|
|
61
|
+
return _getHelmService();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected override getRenderTaskProps(
|
|
65
|
+
gridCell: DG.GridCell, backColor: number, width: number, height: number,
|
|
66
|
+
): HelmProps {
|
|
67
|
+
return new HelmProps(gridCell.cell.value, backColor, width, height);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected override storeAux(gridCell: DG.GridCell, aux: HelmAux): void {
|
|
71
|
+
if (gridCell.tableRowIndex !== null)
|
|
72
|
+
this._auxList[gridCell.tableRowIndex] = aux;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Renders cell from image data (cache), returns true to update the cell by service.
|
|
76
|
+
* @param gridCellBounds {DG.Rect} Grid cell bounds
|
|
77
|
+
*/
|
|
78
|
+
protected override renderCellImageData(
|
|
79
|
+
gridCtx: CanvasRenderingContext2D, gridCellBounds: DG.Rect, gridCell: DG.GridCell, cellImageData: ImageData,
|
|
80
|
+
): boolean {
|
|
81
|
+
const dpr = window.devicePixelRatio;
|
|
82
|
+
if (gridCell.tableRowIndex === null) return false;
|
|
83
|
+
const aux = this._auxList[gridCell.tableRowIndex];
|
|
84
|
+
if (!aux) return true;
|
|
85
|
+
|
|
86
|
+
// Draw cell image data to scale it with drawImage() while transform()
|
|
87
|
+
const cellCanvas = ui.canvas(cellImageData.width, cellImageData.height);
|
|
88
|
+
const cellCtx = cellCanvas.getContext('2d')!;
|
|
89
|
+
cellCtx.putImageData(cellImageData, 0, 0);
|
|
90
|
+
|
|
91
|
+
// Get bbox canvas
|
|
92
|
+
const bBoxImageData = cellCtx.getImageData(aux.bBox.x, aux.bBox.y, aux.bBox.width, aux.bBox.height);
|
|
93
|
+
const bBoxCanvas = ui.canvas(aux.bBox.width, aux.bBox.height);
|
|
94
|
+
const bBoxCtx = bBoxCanvas.getContext('2d')!;
|
|
95
|
+
bBoxCtx.putImageData(bBoxImageData, 0, 0);
|
|
96
|
+
|
|
97
|
+
const fitCanvasWidth = gridCellBounds.width * dpr - 2;
|
|
98
|
+
const fitCanvasHeight = gridCellBounds.height * dpr - 2;
|
|
99
|
+
|
|
100
|
+
const bBoxRatio = Math.max(aux.bBox.width / aux.cBox.width, aux.bBox.height / aux.cBox.height);
|
|
101
|
+
const bBoxFullWidth = aux.bBox.width / bBoxRatio;
|
|
102
|
+
const bBoxFullHeight = aux.bBox.height / bBoxRatio;
|
|
103
|
+
|
|
104
|
+
const fitScale = Math.min(fitCanvasWidth / bBoxFullWidth, fitCanvasHeight / bBoxFullHeight);
|
|
105
|
+
|
|
106
|
+
// Relative shift of bbox center to cbox center
|
|
107
|
+
const bBoxShiftHR = (aux.bBox.left + aux.bBox.width / 2 - aux.cBox.width / 2) / aux.cBox.width;
|
|
108
|
+
const bBoxShiftVR = (aux.bBox.top + aux.bBox.height / 2 - aux.cBox.height / 2) / aux.cBox.height;
|
|
109
|
+
|
|
110
|
+
const bBoxFitLeft = fitCanvasWidth / 2 - bBoxCanvas.width * fitScale * (1 - 2 * bBoxShiftHR) / 2;
|
|
111
|
+
const bBoxFitTop = fitCanvasHeight / 2 - bBoxCanvas.height * fitScale * (1 - 2 * bBoxShiftVR) / 2;
|
|
112
|
+
|
|
113
|
+
const fitCanvas = ui.canvas(fitCanvasWidth, fitCanvasHeight);
|
|
114
|
+
const fitCtx = fitCanvas.getContext('2d')!;
|
|
115
|
+
// fitCtx.fillStyle = '#FFFFA0';
|
|
116
|
+
// fitCtx.fillRect(0, 0, fitCanvasWidth, fitCanvasHeight);
|
|
117
|
+
fitCtx.transform(fitScale, 0, 0, fitScale, bBoxFitLeft, bBoxFitTop);
|
|
118
|
+
fitCtx.drawImage(bBoxCanvas, 0, 0); // draw with scale transform
|
|
119
|
+
|
|
120
|
+
const fitCanvasData = fitCtx.getImageData(0, 0, fitCanvasWidth, fitCanvasHeight);
|
|
121
|
+
this.renderOnGrid(gridCtx, gridCellBounds, gridCell, fitCanvasData);
|
|
122
|
+
return true; // request rendering
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onMouseMove(gridCell: DG.GridCell, e: MouseEvent): void {
|
|
126
|
+
if (gridCell.tableRowIndex === null) return;
|
|
127
|
+
const aux = this._auxList[gridCell.tableRowIndex];
|
|
128
|
+
if (!aux) return;
|
|
129
|
+
|
|
130
|
+
const logPrefix = `${this.toLog()}.onMouseMove()`;
|
|
131
|
+
const dpr = window.devicePixelRatio;
|
|
132
|
+
|
|
133
|
+
/** {@link gridCell}.bounds */ const gcb = gridCell.bounds;
|
|
134
|
+
const argsX = (e.offsetX - gcb.x) * dpr;
|
|
135
|
+
const argsY = (e.offsetY - gcb.y) * dpr;
|
|
136
|
+
|
|
137
|
+
const editorMol: JSDraw2.IEditorMol | null = aux.mol;
|
|
138
|
+
if (!editorMol) {
|
|
139
|
+
this.logger.warning(`${logPrefix}, editorMol of the cell not found.`);
|
|
140
|
+
return; // The gridCell is not rendered yet
|
|
141
|
+
}
|
|
142
|
+
const seqMonomer: ISeqMonomer | null = getHoveredMonomerFromEditorMol(argsX, argsY, gridCell, editorMol);
|
|
143
|
+
|
|
144
|
+
if (seqMonomer) {
|
|
145
|
+
const monomerLib = getMonomerLib();
|
|
146
|
+
const tooltipEl = monomerLib ? monomerLib.getTooltip(seqMonomer.polymerType, seqMonomer.symbol) :
|
|
147
|
+
ui.divText('Monomer library is not available');
|
|
148
|
+
ui.tooltip.show(tooltipEl, e.x + 16, e.y + 16);
|
|
149
|
+
} else {
|
|
150
|
+
// Tooltip for missing monomers
|
|
151
|
+
ui.tooltip.hide();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// -- Handle events --
|
|
156
|
+
private monomerLibOnChanged(_value: any): void {
|
|
157
|
+
this.reset();
|
|
158
|
+
this.invalidateGrid();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static getOrCreate(gridCell: DG.GridCell): HelmGridCellRendererBack {
|
|
162
|
+
const [gridCol, tableCol, temp] =
|
|
163
|
+
getGridCellRendererBack<string, HelmGridCellRendererBack>(gridCell);
|
|
164
|
+
|
|
165
|
+
let res: HelmGridCellRendererBack = temp['rendererBack'];
|
|
166
|
+
if (!res) res = temp['rendererBack'] = new HelmGridCellRendererBack(gridCol, tableCol);
|
|
167
|
+
return res;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export class HelmGridCellRenderer extends DG.GridCellRenderer {
|
|
172
|
+
get name() { return 'helm'; }
|
|
173
|
+
|
|
174
|
+
get cellType() { return 'helm'; }
|
|
175
|
+
|
|
176
|
+
get defaultWidth(): number | null { return 400; }
|
|
177
|
+
|
|
178
|
+
get defaultHeight(): number | null { return 100; }
|
|
179
|
+
|
|
180
|
+
override onMouseMove(gridCell: DG.GridCell, e: MouseEvent) {
|
|
181
|
+
const logPrefix = `Helm: HelmGridCellRenderer.onMouseMove()`;
|
|
182
|
+
try {
|
|
183
|
+
HelmGridCellRendererBack.getOrCreate(gridCell).onMouseMove(gridCell, e);
|
|
184
|
+
} catch (err: any) {
|
|
185
|
+
const [errMsg, errStack] = errInfo(err);
|
|
186
|
+
_package.logger.error(errMsg, undefined, errStack);
|
|
187
|
+
} finally {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
e.stopPropagation();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
override render(
|
|
194
|
+
g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number,
|
|
195
|
+
gridCell: DG.GridCell, cellStyle: DG.GridCellStyle
|
|
196
|
+
): void {
|
|
197
|
+
HelmGridCellRendererBack.getOrCreate(gridCell).render(g, x, y, w, h, gridCell, cellStyle);
|
|
198
|
+
}
|
|
199
|
+
}
|