@datagrok/sequence-translator 1.2.7 → 1.3.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/.eslintrc.json +5 -5
- package/CHANGELOG.md +14 -0
- package/dist/package-test.js +2 -1
- package/dist/package-test.js.LICENSE.txt +8 -0
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +2 -1
- package/dist/package.js.LICENSE.txt +8 -0
- package/dist/package.js.map +1 -1
- package/files/pattern-app-data.json +80 -0
- package/package.json +22 -14
- package/src/{model → apps/common/model}/const.ts +1 -1
- package/src/{model/data-loading-utils → apps/common/model/data-loader}/const.ts +7 -2
- package/src/apps/common/model/data-loader/json-loader.ts +48 -0
- package/src/{model/data-loading-utils → apps/common/model/data-loader}/types.ts +13 -6
- package/src/{model → apps/common/model}/monomer-lib/lib-wrapper.ts +9 -12
- package/src/apps/common/model/oligo-toolkit-package.ts +30 -0
- package/src/{model → apps/common/model}/parsing-validation/format-detector.ts +5 -5
- package/src/{model → apps/common/model}/parsing-validation/format-handler.ts +18 -19
- package/src/{model → apps/common/model}/parsing-validation/sequence-validator.ts +1 -1
- package/src/apps/common/view/app-ui-base.ts +28 -0
- package/src/apps/common/view/combined-app-ui.ts +66 -0
- package/src/{view/utils → apps/common/view/components}/colored-input/colored-text-input.ts +1 -1
- package/src/{view/utils → apps/common/view/components}/draw-molecule.ts +1 -1
- package/src/{view/utils → apps/common/view/components}/molecule-img.ts +3 -3
- package/src/{view/const/ui.ts → apps/common/view/const.ts} +4 -4
- package/src/apps/common/view/isolated-app-ui.ts +43 -0
- package/src/{view/monomer-lib-viewer/viewer.ts → apps/common/view/monomer-lib-viewer.ts} +2 -2
- package/src/apps/common/view/utils.ts +29 -0
- package/src/apps/pattern/model/const.ts +121 -0
- package/src/apps/pattern/model/data-manager.ts +297 -0
- package/src/apps/pattern/model/event-bus.ts +487 -0
- package/src/apps/pattern/model/router.ts +46 -0
- package/src/apps/pattern/model/subscription-manager.ts +21 -0
- package/src/apps/pattern/model/translator.ts +94 -0
- package/src/apps/pattern/model/types.ts +52 -0
- package/src/apps/pattern/model/utils.ts +110 -0
- package/src/apps/pattern/view/components/bulk-convert/column-input.ts +79 -0
- package/src/apps/pattern/view/components/bulk-convert/table-controls.ts +38 -0
- package/src/apps/pattern/view/components/bulk-convert/table-input.ts +95 -0
- package/src/apps/pattern/view/components/edit-block-controls.ts +196 -0
- package/src/apps/pattern/view/components/left-section.ts +44 -0
- package/src/apps/pattern/view/components/load-block-controls.ts +200 -0
- package/src/apps/pattern/view/components/numeric-label-visibility-controls.ts +69 -0
- package/src/apps/pattern/view/components/right-section.ts +148 -0
- package/src/apps/pattern/view/components/strand-editor/dialog.ts +79 -0
- package/src/apps/pattern/view/components/strand-editor/header-controls.ts +105 -0
- package/src/apps/pattern/view/components/strand-editor/strand-controls.ts +159 -0
- package/src/apps/pattern/view/components/terminal-modification-editor.ts +127 -0
- package/src/apps/pattern/view/components/translation-examples-block.ts +139 -0
- package/src/{view/style/pattern-app.css → apps/pattern/view/style.css} +4 -0
- package/src/apps/pattern/view/svg-utils/const.ts +77 -0
- package/src/apps/pattern/view/svg-utils/legend-block.ts +92 -0
- package/src/apps/pattern/view/svg-utils/strands-block.ts +335 -0
- package/src/apps/pattern/view/svg-utils/svg-block-base.ts +37 -0
- package/src/apps/pattern/view/svg-utils/svg-display-manager.ts +44 -0
- package/src/apps/pattern/view/svg-utils/svg-element-factory.ts +94 -0
- package/src/apps/pattern/view/svg-utils/svg-renderer.ts +51 -0
- package/src/apps/pattern/view/svg-utils/text-dimensions-calculator.ts +29 -0
- package/src/apps/pattern/view/svg-utils/title-block.ts +53 -0
- package/src/apps/pattern/view/svg-utils/utils.ts +37 -0
- package/src/apps/pattern/view/types.ts +14 -0
- package/src/apps/pattern/view/ui.ts +61 -0
- package/src/{model/structure-app → apps/structure/model}/mol-transformations.ts +3 -3
- package/src/{model/structure-app → apps/structure/model}/monomer-code-parser.ts +9 -10
- package/src/{model/structure-app → apps/structure/model}/oligo-structure.ts +4 -4
- package/src/{model/structure-app → apps/structure/model}/sequence-to-molfile.ts +2 -2
- package/src/{view/apps/oligo-structure.ts → apps/structure/view/ui.ts} +31 -17
- package/src/{model/translator-app → apps/translator/model}/conversion-utils.ts +25 -7
- package/src/{model/translator-app → apps/translator/model}/format-converter.ts +7 -12
- package/src/{view/const/oligo-translator.ts → apps/translator/view/const.ts} +1 -1
- package/src/{view/apps/oligo-translator.ts → apps/translator/view/ui.ts} +88 -42
- package/src/demo/demo-st-ui.ts +12 -32
- package/src/package.ts +91 -55
- package/src/plugins/mermade.ts +9 -9
- package/src/polytool/const.ts +28 -0
- package/src/polytool/csv-to-json-monomer-lib-converter.ts +40 -0
- package/src/polytool/cyclized.ts +56 -0
- package/src/polytool/monomer-lib-handler.ts +115 -0
- package/src/polytool/pt-conversion.ts +307 -0
- package/src/polytool/pt-dialog.ts +115 -0
- package/src/polytool/pt-enumeration.ts +127 -0
- package/src/polytool/pt-rules.ts +73 -0
- package/src/polytool/utils.ts +27 -0
- package/src/tests/const.ts +5 -5
- package/src/tests/formats-support.ts +6 -6
- package/src/tests/formats-to-helm.ts +5 -5
- package/src/tests/helm-to-nucleotides.ts +5 -10
- package/tsconfig.json +3 -9
- package/webpack.config.js +3 -0
- package/files/axolabs-style.json +0 -97
- package/src/model/data-loading-utils/json-loader.ts +0 -38
- package/src/model/pattern-app/const.ts +0 -33
- package/src/model/pattern-app/draw-svg.ts +0 -193
- package/src/model/pattern-app/helpers.ts +0 -96
- package/src/model/pattern-app/oligo-pattern.ts +0 -111
- package/src/view/app-ui.ts +0 -193
- package/src/view/apps/oligo-pattern.ts +0 -759
- /package/src/{model → apps/common/model}/helpers.ts +0 -0
- /package/src/{model → apps/common/model}/monomer-lib/const.ts +0 -0
- /package/src/{view/utils → apps/common/view/components}/app-info-dialog.ts +0 -0
- /package/src/{view/utils → apps/common/view/components}/colored-input/input-painters.ts +0 -0
- /package/src/{view/style/colored-text-input.css → apps/common/view/components/colored-input/style.css} +0 -0
- /package/src/{view/utils → apps/common/view/components}/router.ts +0 -0
- /package/src/{model/structure-app → apps/structure/model}/const.ts +0 -0
- /package/src/{view/style/structure-app.css → apps/structure/view/style.css} +0 -0
- /package/src/{model/translator-app → apps/translator/model}/const.ts +0 -0
- /package/src/{view/style/translator-app.css → apps/translator/view/style.css} +0 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import {NUCLEOTIDES} from '../../../common/model/const';
|
|
2
|
+
import {STRAND, STRANDS, TERMINUS} from '../../model/const';
|
|
3
|
+
import {PatternConfiguration} from '../../model/types';
|
|
4
|
+
import {isOverhangNucleotide} from '../../model/utils';
|
|
5
|
+
import {SVG_CIRCLE_SIZES, SVG_ELEMENT_COLORS, SVG_TEXT_FONT_SIZES} from './const';
|
|
6
|
+
import {SVGBlockBase} from './svg-block-base';
|
|
7
|
+
import {SVGElementFactory} from './svg-element-factory';
|
|
8
|
+
import {TextDimensionsCalculator} from './text-dimensions-calculator';
|
|
9
|
+
import {computeTextColorForNucleobaseLabel, getNucleobaseColorFromStyleMap, getNucleobaseLabelForCircle} from './utils';
|
|
10
|
+
|
|
11
|
+
const NUMERIC_LABEL_PADDING = 5;
|
|
12
|
+
const SENSE_STRAND_HEIGHT = SVG_TEXT_FONT_SIZES.NUCLEOBASE +
|
|
13
|
+
NUMERIC_LABEL_PADDING + SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER;
|
|
14
|
+
const SENSE_STRAND_PADDING = 10;
|
|
15
|
+
const LEFT_LABEL_WIDTH = 55;
|
|
16
|
+
const SENSE_STRAND_HORIZONTAL_SHIFT = SENSE_STRAND_PADDING + LEFT_LABEL_WIDTH;
|
|
17
|
+
const RIGHT_LABEL_WIDTH = 20;
|
|
18
|
+
|
|
19
|
+
export class StrandsBlock extends SVGBlockBase {
|
|
20
|
+
private strands: SVGBlockBase[];
|
|
21
|
+
private labels: SVGBlockBase[];
|
|
22
|
+
constructor(
|
|
23
|
+
svgElementFactory: SVGElementFactory,
|
|
24
|
+
config: PatternConfiguration,
|
|
25
|
+
yShift: number
|
|
26
|
+
) {
|
|
27
|
+
super(svgElementFactory, config, yShift);
|
|
28
|
+
const strandTypes = STRANDS.filter((strandType) => config.nucleotideSequences[strandType].length > 0);
|
|
29
|
+
|
|
30
|
+
this.strands = strandTypes
|
|
31
|
+
.map((strand) => new SingleStrandBlock(this.svgElementFactory, config, yShift, strand));
|
|
32
|
+
|
|
33
|
+
this.labels = strandTypes.map(
|
|
34
|
+
(strandType, idx) =>
|
|
35
|
+
new StrandLabel(this.svgElementFactory, config, yShift, strandType, this.strands[idx] as SingleStrandBlock)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get svgElements(): SVGElement[] {
|
|
40
|
+
const elements = [
|
|
41
|
+
...this.strands,
|
|
42
|
+
...this.labels
|
|
43
|
+
].map((block) => block.svgElements).flat();
|
|
44
|
+
return elements;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getContentWidth(): number {
|
|
48
|
+
return Math.max(...this.labels.map((labelBlock) => labelBlock.getContentWidth()));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getContentHeight(): number {
|
|
52
|
+
const result = this.strands
|
|
53
|
+
.reduce((acc, strand) => acc + strand.getContentHeight(), 0);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class SingleStrandBlock extends SVGBlockBase {
|
|
59
|
+
private _svgElements: SVGElement[];
|
|
60
|
+
private nucleotideNumericLabels: (number | null)[];
|
|
61
|
+
constructor(
|
|
62
|
+
protected svgElementFactory: SVGElementFactory,
|
|
63
|
+
protected config: PatternConfiguration,
|
|
64
|
+
protected yShift: number,
|
|
65
|
+
private strand: STRAND
|
|
66
|
+
) {
|
|
67
|
+
super(svgElementFactory, config, yShift);
|
|
68
|
+
|
|
69
|
+
// WARNING: should be computed before creating circles
|
|
70
|
+
this.nucleotideNumericLabels = this.computeNucleotideNumericLabels();
|
|
71
|
+
|
|
72
|
+
if (this.strand === STRAND.ANTISENSE) {
|
|
73
|
+
this.config.phosphorothioateLinkageFlags[this.strand].reverse();
|
|
74
|
+
this.config.nucleotideSequences[this.strand].reverse();
|
|
75
|
+
this.nucleotideNumericLabels.reverse();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this._svgElements = [
|
|
79
|
+
this.createStrandCircles(),
|
|
80
|
+
this.createPTOLinkageStars(),
|
|
81
|
+
].flat();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private computeNucleotideNumericLabels(): (number | null)[] {
|
|
85
|
+
let index = 0;
|
|
86
|
+
const nucleotides = this.config.nucleotideSequences[this.strand];
|
|
87
|
+
const indices = nucleotides.map((nucleotide) => {
|
|
88
|
+
if (isOverhangNucleotide(nucleotide)) return null;
|
|
89
|
+
index++;
|
|
90
|
+
return index;
|
|
91
|
+
});
|
|
92
|
+
return indices;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get svgElements(): SVGElement[] {
|
|
96
|
+
return this._svgElements;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private getStrandCircleYShift(): number {
|
|
100
|
+
return getStrandCircleYShift(this.strand, this.yShift);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private createStrandCircles(): SVGElement[] {
|
|
104
|
+
const defaultShift = {
|
|
105
|
+
x: SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS + SENSE_STRAND_HORIZONTAL_SHIFT,
|
|
106
|
+
y: this.getStrandCircleYShift()
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const nucleotides = this.config.nucleotideSequences[this.strand];
|
|
110
|
+
|
|
111
|
+
const elements = nucleotides
|
|
112
|
+
.map((nucleotide, index) => this.createNucleotideElementGroup(nucleotide, index, defaultShift)).flat();
|
|
113
|
+
|
|
114
|
+
return elements;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private createNucleotideElementGroup(
|
|
118
|
+
nucleotide: string,
|
|
119
|
+
index: number,
|
|
120
|
+
defaultShift: {x: number, y: number}
|
|
121
|
+
): SVGElement[] {
|
|
122
|
+
const circleElements = this.createNucleotideCircleElements(nucleotide, index, defaultShift);
|
|
123
|
+
const numericLabel = this.config.nucleotidesWithNumericLabels.includes(nucleotide) ?
|
|
124
|
+
this.createNucleotideNumericLabel(index, defaultShift) : null;
|
|
125
|
+
|
|
126
|
+
return [...circleElements, numericLabel].filter((element) => element !== null) as SVGElement[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private createNucleotideCircleElements(
|
|
130
|
+
nucleotide: string,
|
|
131
|
+
index: number,
|
|
132
|
+
defaultShift: {x: number, y: number}
|
|
133
|
+
): (SVGElement | null)[] {
|
|
134
|
+
const color = getNucleobaseColorFromStyleMap(nucleotide);
|
|
135
|
+
const centerPosition = {...defaultShift, x: defaultShift.x + index * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER};
|
|
136
|
+
|
|
137
|
+
const circle = this.svgElementFactory
|
|
138
|
+
.createCircleElement(centerPosition, SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS, color);
|
|
139
|
+
|
|
140
|
+
const nonModifiedNucleotideLetterLabel = this.createNucleotideLetterLabel(index, defaultShift, nucleotide);
|
|
141
|
+
|
|
142
|
+
return [circle, nonModifiedNucleotideLetterLabel];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private getNucleotideCircleCenterPosition(
|
|
146
|
+
index: number,
|
|
147
|
+
defaultShift: {x: number, y: number}
|
|
148
|
+
): {x: number, y: number} {
|
|
149
|
+
return {
|
|
150
|
+
x: defaultShift.x + index * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER,
|
|
151
|
+
y: defaultShift.y
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private createNucleotideLetterLabel(
|
|
156
|
+
index: number,
|
|
157
|
+
defaultShift: {x: number, y: number},
|
|
158
|
+
nucleobase: string
|
|
159
|
+
): SVGElement | null {
|
|
160
|
+
if (!NUCLEOTIDES.includes(nucleobase))
|
|
161
|
+
return null;
|
|
162
|
+
|
|
163
|
+
const text = getNucleobaseLabelForCircle(nucleobase);
|
|
164
|
+
const color = computeTextColorForNucleobaseLabel(nucleobase);
|
|
165
|
+
// position at the very center of the circle
|
|
166
|
+
const position = this.getPositionForNucleotideLabel(index, defaultShift);
|
|
167
|
+
return this.svgElementFactory.createTextElement(
|
|
168
|
+
text,
|
|
169
|
+
position,
|
|
170
|
+
SVG_TEXT_FONT_SIZES.NUCLEOBASE,
|
|
171
|
+
color
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Returns the position for the letter with its center being at the center of the circle */
|
|
176
|
+
private getPositionForNucleotideLabel(
|
|
177
|
+
index: number,
|
|
178
|
+
defaultShift: {x: number, y: number}
|
|
179
|
+
): {x: number, y: number} {
|
|
180
|
+
const circleCenter = this.getNucleotideCircleCenterPosition(index, defaultShift);
|
|
181
|
+
const textDimensions = TextDimensionsCalculator.getTextDimensions('A', SVG_TEXT_FONT_SIZES.NUCLEOBASE);
|
|
182
|
+
return {
|
|
183
|
+
x: circleCenter.x - textDimensions.width / 2,
|
|
184
|
+
// the coefficient 1/3 is fine-tuned to make the text look centered
|
|
185
|
+
y: circleCenter.y + textDimensions.height / 3
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private getNumericLabelYShift(
|
|
190
|
+
defaultShift: {x: number, y: number}
|
|
191
|
+
): number {
|
|
192
|
+
return this.strand === STRAND.SENSE ?
|
|
193
|
+
defaultShift.y - (SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS + NUMERIC_LABEL_PADDING) :
|
|
194
|
+
defaultShift.y + SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS + NUMERIC_LABEL_PADDING + SVG_TEXT_FONT_SIZES.COMMENT;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private createNucleotideNumericLabel(
|
|
198
|
+
index: number,
|
|
199
|
+
defaultShift: {x: number, y: number}
|
|
200
|
+
): SVGElement | null {
|
|
201
|
+
const label = this.nucleotideNumericLabels[index];
|
|
202
|
+
if (label === null) return null;
|
|
203
|
+
|
|
204
|
+
const width = TextDimensionsCalculator.getTextDimensions(label.toString(), SVG_TEXT_FONT_SIZES.COMMENT).width;
|
|
205
|
+
|
|
206
|
+
const position = {
|
|
207
|
+
x: defaultShift.x + index * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER - width / 2,
|
|
208
|
+
y: this.getNumericLabelYShift(defaultShift)
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return this.svgElementFactory.createTextElement(
|
|
212
|
+
label.toString(),
|
|
213
|
+
position,
|
|
214
|
+
SVG_TEXT_FONT_SIZES.COMMENT,
|
|
215
|
+
SVG_ELEMENT_COLORS.TEXT
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private createPTOLinkageStars(): SVGElement[] {
|
|
220
|
+
const ptoFlags = this.config.phosphorothioateLinkageFlags[this.strand];
|
|
221
|
+
|
|
222
|
+
const yShift = this.getStrandCircleYShift() + SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS * 0.8;
|
|
223
|
+
|
|
224
|
+
const elements = ptoFlags
|
|
225
|
+
.map((ptoFlag, index) => {
|
|
226
|
+
if (!ptoFlag) return null;
|
|
227
|
+
|
|
228
|
+
const centerPosition = {
|
|
229
|
+
x: SENSE_STRAND_HORIZONTAL_SHIFT + index * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER,
|
|
230
|
+
y: yShift
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const color = SVG_ELEMENT_COLORS.LINKAGE_STAR;
|
|
234
|
+
return this.svgElementFactory.createStarElement(centerPosition, color);
|
|
235
|
+
})
|
|
236
|
+
.filter((element) => element !== null) as SVGElement[];
|
|
237
|
+
|
|
238
|
+
return elements;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getContentWidth(): number {
|
|
242
|
+
const numberOfMonomers = this.config.nucleotideSequences[this.strand].length;
|
|
243
|
+
return numberOfMonomers * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
getContentHeight(): number {
|
|
247
|
+
return SENSE_STRAND_HEIGHT + SENSE_STRAND_PADDING;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
class StrandLabel extends SVGBlockBase {
|
|
252
|
+
private _svgElements: SVGElement[];
|
|
253
|
+
constructor(
|
|
254
|
+
protected svgElementFactory: SVGElementFactory,
|
|
255
|
+
protected config: PatternConfiguration,
|
|
256
|
+
protected yShift: number,
|
|
257
|
+
private strand: STRAND,
|
|
258
|
+
private strandSvgWrapper: SingleStrandBlock
|
|
259
|
+
) {
|
|
260
|
+
super(svgElementFactory, config, yShift);
|
|
261
|
+
this._svgElements = this.createSVGElements();
|
|
262
|
+
// this.strandSvgWrapper.shiftElements({x: this.getLeftLabelWidth(), y: 0});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private createSVGElements(): SVGElement[] {
|
|
266
|
+
const elements = [
|
|
267
|
+
this.createLeftLabel(),
|
|
268
|
+
this.createRightLabel()
|
|
269
|
+
];
|
|
270
|
+
return elements;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private getLeftLabelWidth(): number {
|
|
274
|
+
return LEFT_LABEL_WIDTH;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private getRightLabelWidth(): number {
|
|
278
|
+
return RIGHT_LABEL_WIDTH;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private createLeftLabel(): SVGTextElement {
|
|
282
|
+
const terminus = this.strand === STRAND.SENSE ? TERMINUS.FIVE_PRIME : TERMINUS.THREE_PRIME;
|
|
283
|
+
const text = `${this.strand}: ${terminus} `;
|
|
284
|
+
const textDimensions = TextDimensionsCalculator.getTextDimensions(text, SVG_TEXT_FONT_SIZES.NUCLEOBASE);
|
|
285
|
+
const position = {
|
|
286
|
+
x: SENSE_STRAND_PADDING,
|
|
287
|
+
y: getStrandCircleYShift(this.strand, this.yShift) + textDimensions.height / 3
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return this.svgElementFactory.createTextElement(
|
|
291
|
+
text,
|
|
292
|
+
position,
|
|
293
|
+
SVG_TEXT_FONT_SIZES.NUCLEOBASE,
|
|
294
|
+
SVG_ELEMENT_COLORS.TEXT
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private createRightLabel(): SVGTextElement {
|
|
299
|
+
const terminus = this.strand === STRAND.SENSE ? TERMINUS.THREE_PRIME : TERMINUS.FIVE_PRIME;
|
|
300
|
+
const text = ` ${terminus}`;
|
|
301
|
+
const textDimensions = TextDimensionsCalculator.getTextDimensions(text, SVG_TEXT_FONT_SIZES.NUCLEOBASE);
|
|
302
|
+
const position = {
|
|
303
|
+
x: SENSE_STRAND_HORIZONTAL_SHIFT + this.strandSvgWrapper.getContentWidth() + 5,
|
|
304
|
+
y: getStrandCircleYShift(this.strand, this.yShift) + textDimensions.height / 3
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
return this.svgElementFactory.createTextElement(
|
|
308
|
+
text,
|
|
309
|
+
position,
|
|
310
|
+
SVG_TEXT_FONT_SIZES.NUCLEOBASE,
|
|
311
|
+
SVG_ELEMENT_COLORS.TEXT
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
get svgElements(): SVGElement[] {
|
|
316
|
+
return this._svgElements;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
getContentWidth(): number {
|
|
320
|
+
return this.strandSvgWrapper.getContentWidth() + this.getLeftLabelWidth() + this.getRightLabelWidth() + SENSE_STRAND_PADDING;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
getContentHeight(): number {
|
|
324
|
+
return this.strandSvgWrapper.getContentHeight();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function getStrandCircleYShift(
|
|
329
|
+
strand: STRAND,
|
|
330
|
+
defaultYShift: number
|
|
331
|
+
): number {
|
|
332
|
+
return strand === STRAND.SENSE ?
|
|
333
|
+
defaultYShift + NUMERIC_LABEL_PADDING + SVG_TEXT_FONT_SIZES.COMMENT + SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS :
|
|
334
|
+
defaultYShift + SENSE_STRAND_HEIGHT + SENSE_STRAND_PADDING + SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS;
|
|
335
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {PatternConfiguration} from '../../model/types';
|
|
2
|
+
import {SVGElementFactory} from './svg-element-factory';
|
|
3
|
+
|
|
4
|
+
/** Horizontal block within SVG: title, strands, legend */
|
|
5
|
+
export abstract class SVGBlockBase {
|
|
6
|
+
// protected svgElements: SVGElement[] = [];
|
|
7
|
+
constructor(
|
|
8
|
+
protected svgElementFactory: SVGElementFactory,
|
|
9
|
+
protected config: PatternConfiguration,
|
|
10
|
+
protected yShift: number
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
abstract get svgElements(): SVGElement[];
|
|
14
|
+
|
|
15
|
+
abstract getContentHeight(): number;
|
|
16
|
+
|
|
17
|
+
shiftElements(shift: {x: number, y: number}): void {
|
|
18
|
+
this.svgElements.forEach((element) => {
|
|
19
|
+
const transform = element.getAttribute('transform') || '';
|
|
20
|
+
const match = transform.match(/translate\(([^,]+),([^,]+)\)/);
|
|
21
|
+
const x = match ? parseFloat(match[1]) : 0;
|
|
22
|
+
const y = match ? parseFloat(match[2]) : 0;
|
|
23
|
+
const newTransform = `translate(${x + shift.x},${y + shift.y})`;
|
|
24
|
+
element.setAttribute('transform', `${transform} ${newTransform}`);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
adjustContentWithinGlobalContainer(globalWidth: number): void {
|
|
29
|
+
const contentWidth = this.getContentWidth();
|
|
30
|
+
if (contentWidth < globalWidth) {
|
|
31
|
+
const shift = (globalWidth - contentWidth) / 2;
|
|
32
|
+
this.shiftElements({x: shift, y: 0});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
abstract getContentWidth(): number;
|
|
37
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* Do not change these import lines to match external modules in webpack configuration */
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
|
|
4
|
+
import {EventBus} from '../../model/event-bus';
|
|
5
|
+
import {PatternConfiguration} from '../../model/types';
|
|
6
|
+
//@ts-ignore
|
|
7
|
+
import * as svgExport from 'save-svg-as-png';
|
|
8
|
+
import {NucleotidePatternSVGRenderer} from './svg-renderer';
|
|
9
|
+
|
|
10
|
+
export class SvgDisplayManager {
|
|
11
|
+
private svgDisplayDiv = ui.div([]);
|
|
12
|
+
private svgElement: SVGElement;
|
|
13
|
+
|
|
14
|
+
private constructor(
|
|
15
|
+
private eventBus: EventBus
|
|
16
|
+
) {
|
|
17
|
+
eventBus.updateSvgContainer$.subscribe(() => this.updateSvgContainer());
|
|
18
|
+
eventBus.svgSaveRequested$.subscribe(() => this.saveSvgAsPng());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static createSvgDiv(eventBus: EventBus): HTMLDivElement {
|
|
22
|
+
const displayManager = new SvgDisplayManager(eventBus);
|
|
23
|
+
return displayManager.svgDisplayDiv;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private updateSvgContainer(): void {
|
|
27
|
+
$(this.svgDisplayDiv).empty();
|
|
28
|
+
const patternConfig = this.eventBus.getPatternConfig();
|
|
29
|
+
this.svgElement = this.createSvg(patternConfig);
|
|
30
|
+
this.svgDisplayDiv.append(this.svgElement);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private createSvg(patternConfig: PatternConfiguration) {
|
|
34
|
+
// const renderer = new NucleotidePatternSVGRenderer(patternConfig);
|
|
35
|
+
const renderer = new NucleotidePatternSVGRenderer(patternConfig);
|
|
36
|
+
const svg = renderer.renderPattern();
|
|
37
|
+
return svg;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private saveSvgAsPng(): void {
|
|
41
|
+
const patternName = this.eventBus.getPatternName();
|
|
42
|
+
svgExport.saveSvgAsPng(this.svgElement, patternName, {backgroundColor: 'white'});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {Position} from '../types';
|
|
2
|
+
|
|
3
|
+
export class SVGElementFactory {
|
|
4
|
+
private readonly xmlNamespace = 'http://www.w3.org/2000/svg';
|
|
5
|
+
|
|
6
|
+
private createElement(elementType: string): Element {
|
|
7
|
+
return document.createElementNS(this.xmlNamespace, elementType);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
private setAttributes(targetElement: Element, attributes: { [key: string]: any }): void {
|
|
11
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
|
12
|
+
targetElement.setAttribute(key, String(value));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
createCanvas(width: number, height: number): SVGElement {
|
|
17
|
+
const svgElement = this.createElement('svg') as SVGElement;
|
|
18
|
+
this.setAttributes(svgElement, {
|
|
19
|
+
id: 'mySvg',
|
|
20
|
+
width,
|
|
21
|
+
height,
|
|
22
|
+
});
|
|
23
|
+
return svgElement;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
createCircleElement(centerPosition: Position, radius: number, color: string): SVGCircleElement {
|
|
27
|
+
const circle = this.createElement('circle') as SVGCircleElement;
|
|
28
|
+
this.setAttributes(circle, {
|
|
29
|
+
cx: centerPosition.x,
|
|
30
|
+
cy: centerPosition.y,
|
|
31
|
+
r: radius,
|
|
32
|
+
fill: color,
|
|
33
|
+
});
|
|
34
|
+
return circle;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
createTextElement(textContent: string, position: Position, fontSize: number, color: string): SVGTextElement {
|
|
38
|
+
const textElement = this.createElement('text') as SVGTextElement;
|
|
39
|
+
this.setAttributes(textElement, {
|
|
40
|
+
'x': position.x,
|
|
41
|
+
'y': position.y,
|
|
42
|
+
'font-size': fontSize,
|
|
43
|
+
'font-weight': 'normal',
|
|
44
|
+
'font-family': 'Arial',
|
|
45
|
+
'fill': color,
|
|
46
|
+
});
|
|
47
|
+
textElement.textContent = textContent;
|
|
48
|
+
return textElement;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
createStarElement(centerPosition: Position, color: string): SVGPolygonElement {
|
|
52
|
+
const star = this.createElement('polygon') as SVGPolygonElement;
|
|
53
|
+
const points = this.computeStarVertexCoordinates(centerPosition);
|
|
54
|
+
const pointsAttribute = points.map((point) => point.join(',')).join(' ');
|
|
55
|
+
|
|
56
|
+
this.setAttributes(star, {
|
|
57
|
+
points: pointsAttribute,
|
|
58
|
+
fill: color,
|
|
59
|
+
});
|
|
60
|
+
return star;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private computeStarVertexCoordinates(centerPosition: Position): [number, number][] {
|
|
64
|
+
const outerVerticesPerStar = 5;
|
|
65
|
+
const innerVertexRadius = 3;
|
|
66
|
+
const outerVertexRadius = innerVertexRadius * 2;
|
|
67
|
+
const radiansPerVertex = Math.PI / outerVerticesPerStar;
|
|
68
|
+
const radiansOffset = - radiansPerVertex / 2;
|
|
69
|
+
const totalNumberOfVertices = outerVerticesPerStar * 2;
|
|
70
|
+
|
|
71
|
+
const points: [number, number][] = Array.from({length: totalNumberOfVertices}, (_, i) => {
|
|
72
|
+
const isOuterVertex = i % 2 === 0;
|
|
73
|
+
const radius = isOuterVertex ? outerVertexRadius : innerVertexRadius;
|
|
74
|
+
const angle = i * radiansPerVertex + radiansOffset;
|
|
75
|
+
const x = centerPosition.x + Math.cos(angle) * radius;
|
|
76
|
+
const y = centerPosition.y + Math.sin(angle) * radius;
|
|
77
|
+
return [x, y];
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return points;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
createRectangleElement(topLeftCorner: Position, width: number, height: number, color: string): SVGRectElement {
|
|
84
|
+
const rectangle = this.createElement('rect') as SVGRectElement;
|
|
85
|
+
this.setAttributes(rectangle, {
|
|
86
|
+
x: topLeftCorner.x,
|
|
87
|
+
y: topLeftCorner.y,
|
|
88
|
+
width,
|
|
89
|
+
height,
|
|
90
|
+
fill: color,
|
|
91
|
+
});
|
|
92
|
+
return rectangle;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import {PatternConfiguration} from '../../model/types';
|
|
3
|
+
import {StrandsBlock} from './strands-block';
|
|
4
|
+
import {SVGBlockBase} from './svg-block-base';
|
|
5
|
+
import {SVGElementFactory} from './svg-element-factory';
|
|
6
|
+
import {TitleBlock} from './title-block';
|
|
7
|
+
import {LegendBlock} from './legend-block';
|
|
8
|
+
import {LEGEND_PADDING, TITLE_SHIFT} from './const';
|
|
9
|
+
|
|
10
|
+
export class NucleotidePatternSVGRenderer {
|
|
11
|
+
private title: TitleBlock;
|
|
12
|
+
private strands: StrandsBlock;
|
|
13
|
+
private legend: LegendBlock;
|
|
14
|
+
private svgElementFactory = new SVGElementFactory();
|
|
15
|
+
|
|
16
|
+
constructor(patternConfig: PatternConfiguration) {
|
|
17
|
+
const config = _.cloneDeep(patternConfig);
|
|
18
|
+
|
|
19
|
+
let heightShift = TITLE_SHIFT;
|
|
20
|
+
|
|
21
|
+
this.title = new TitleBlock(this.svgElementFactory, config, heightShift);
|
|
22
|
+
heightShift += this.title.getContentHeight();
|
|
23
|
+
|
|
24
|
+
this.strands = new StrandsBlock(this.svgElementFactory, config, heightShift);
|
|
25
|
+
heightShift += this.strands.getContentHeight() + LEGEND_PADDING;
|
|
26
|
+
|
|
27
|
+
this.legend = new LegendBlock(this.svgElementFactory, config, heightShift);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
renderPattern(): SVGElement {
|
|
31
|
+
const width = this.getGlobalWidth();
|
|
32
|
+
const height = this.getGlobalHeight();
|
|
33
|
+
const canvas = this.svgElementFactory.createCanvas(width, height);
|
|
34
|
+
|
|
35
|
+
const elements = [this.title, this.strands, this.legend].map((block) => block.svgElements).flat();
|
|
36
|
+
canvas.append(...elements);
|
|
37
|
+
|
|
38
|
+
return canvas;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private getGlobalWidth(): number {
|
|
42
|
+
const blocks = [this.title, this.strands, this.legend] as SVGBlockBase[];
|
|
43
|
+
return Math.max(...blocks.map((block) => block.getContentWidth()));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private getGlobalHeight(): number {
|
|
47
|
+
const blocks = [this.title, this.strands, this.legend];
|
|
48
|
+
const height = blocks.reduce((acc, block) => acc + block.getContentHeight(), TITLE_SHIFT);
|
|
49
|
+
return height;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export class TextDimensionsCalculator {
|
|
2
|
+
private static instance: TextDimensionsCalculator;
|
|
3
|
+
private canvas: HTMLCanvasElement;
|
|
4
|
+
private constructor() { }
|
|
5
|
+
|
|
6
|
+
private static getInstance(): TextDimensionsCalculator {
|
|
7
|
+
if (!TextDimensionsCalculator.instance) {
|
|
8
|
+
TextDimensionsCalculator.instance = new TextDimensionsCalculator();
|
|
9
|
+
TextDimensionsCalculator.instance.canvas = document.createElement('canvas');
|
|
10
|
+
}
|
|
11
|
+
return TextDimensionsCalculator.instance;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static getTextDimensions(text: string, fontSize: number): {width: number, height: number} {
|
|
15
|
+
const canvas = TextDimensionsCalculator.getInstance().canvas;
|
|
16
|
+
const context = canvas.getContext('2d');
|
|
17
|
+
if (!context)
|
|
18
|
+
throw new Error('Canvas 2D context is not available');
|
|
19
|
+
|
|
20
|
+
context.font = `${fontSize}px Arial`;
|
|
21
|
+
const scaleFactor = 1.1;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
width: context.measureText(text).width * scaleFactor,
|
|
25
|
+
height: fontSize * scaleFactor
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {STRAND} from '../../model/const';
|
|
2
|
+
import {PatternConfiguration} from '../../model/types';
|
|
3
|
+
import {FONT_SIZE} from './const';
|
|
4
|
+
import {SVGBlockBase} from './svg-block-base';
|
|
5
|
+
import {SVGElementFactory} from './svg-element-factory';
|
|
6
|
+
import {TextDimensionsCalculator} from './text-dimensions-calculator';
|
|
7
|
+
|
|
8
|
+
const TITLE_LEFT_PADDING = 15;
|
|
9
|
+
|
|
10
|
+
export class TitleBlock extends SVGBlockBase {
|
|
11
|
+
private titleText: string;
|
|
12
|
+
private _svgElements: SVGElement[] = [];
|
|
13
|
+
constructor(
|
|
14
|
+
protected svgElementFactory: SVGElementFactory,
|
|
15
|
+
protected config: PatternConfiguration,
|
|
16
|
+
protected yShift: number
|
|
17
|
+
) {
|
|
18
|
+
super(svgElementFactory, config, yShift);
|
|
19
|
+
this.titleText = this.getTitleText();
|
|
20
|
+
this._svgElements = [this.getTitle()];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get svgElements(): SVGElement[] {
|
|
24
|
+
return this._svgElements;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private getTitle(): SVGElement {
|
|
28
|
+
return this.svgElementFactory.createTextElement(
|
|
29
|
+
this.titleText,
|
|
30
|
+
{x: TITLE_LEFT_PADDING, y: this.yShift + FONT_SIZE.TITLE},
|
|
31
|
+
FONT_SIZE.TITLE,
|
|
32
|
+
'black'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getContentWidth(): number {
|
|
37
|
+
return TextDimensionsCalculator.getTextDimensions(this.titleText, FONT_SIZE.TITLE).width;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getContentHeight(): number {
|
|
41
|
+
return TextDimensionsCalculator.getTextDimensions(this.titleText, FONT_SIZE.TITLE).height;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private getTitleText(): string {
|
|
45
|
+
const senseStrandLength = `${this.config.nucleotideSequences[STRAND.SENSE].length}`;
|
|
46
|
+
const antisenseStrandLength = this.config.isAntisenseStrandIncluded ?
|
|
47
|
+
`/${this.config.nucleotideSequences[STRAND.ANTISENSE].length}` : '';
|
|
48
|
+
const titleText = `${this.config.patternName} for ${senseStrandLength}${antisenseStrandLength}-mer`;
|
|
49
|
+
|
|
50
|
+
return titleText;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {NUCLEOTIDES} from '../../../common/model/const';
|
|
2
|
+
import {PATTERN_APP_DATA} from '../../../common/model/data-loader/json-loader';
|
|
3
|
+
import {LUMINANCE_COEFFICIENTS, TEXT_COLOR, SVG_CIRCLE_SIZES} from './const';
|
|
4
|
+
import {isOverhangNucleotide} from '../../model/utils';
|
|
5
|
+
|
|
6
|
+
export function computeLegendCircleYPosition(isAntisenseStrandActive: boolean): number {
|
|
7
|
+
return (isAntisenseStrandActive ? 9.5 : 6) * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getNucleobaseLabelForCircle(nucleobase: string): string {
|
|
11
|
+
const criterion = !isOverhangNucleotide(nucleobase) && NUCLEOTIDES.includes(nucleobase);
|
|
12
|
+
|
|
13
|
+
return criterion ? nucleobase : '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function computeTextColorForNucleobaseLabel(nucleobase: string): string {
|
|
17
|
+
const nucleobaseColor = getNucleobaseColorFromStyleMap(nucleobase);
|
|
18
|
+
|
|
19
|
+
const rgbValues = nucleobaseColor.match(/\d+/g)?.map(Number);
|
|
20
|
+
if (!rgbValues || rgbValues.length < 3)
|
|
21
|
+
return TEXT_COLOR.LIGHT;
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const [r, g, b] = rgbValues;
|
|
25
|
+
const luminance = r * LUMINANCE_COEFFICIENTS.RED + g * LUMINANCE_COEFFICIENTS.GREEN + b * LUMINANCE_COEFFICIENTS.BLUE;
|
|
26
|
+
return luminance > LUMINANCE_COEFFICIENTS.THRESHOLD ? TEXT_COLOR.DARK : TEXT_COLOR.LIGHT;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getNucleobaseColorFromStyleMap(nucleobase: string): string {
|
|
30
|
+
// todo: optimize
|
|
31
|
+
const format = Object.keys(PATTERN_APP_DATA)[0];
|
|
32
|
+
if (!format)
|
|
33
|
+
throw new Error('No format found in PATTERN_APP_DATA');
|
|
34
|
+
|
|
35
|
+
const styleMap = PATTERN_APP_DATA[format];
|
|
36
|
+
return styleMap[nucleobase].color || '';
|
|
37
|
+
}
|