@datagrok/sequence-translator 1.2.6 → 1.2.9
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 +12 -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 +21 -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 +470 -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 +68 -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 +69 -0
- package/src/apps/pattern/view/components/bulk-convert/table-controls.ts +37 -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 +198 -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 +63 -0
- package/src/apps/pattern/view/svg-utils/dimensions-calculator.ts +498 -0
- package/src/apps/pattern/view/svg-utils/svg-display-manager.ts +45 -0
- package/src/apps/pattern/view/svg-utils/svg-element-factory.ts +82 -0
- package/src/apps/pattern/view/svg-utils/svg-renderer.ts +396 -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} +2 -0
- package/src/apps/translator/view/ui.ts +547 -0
- package/src/demo/demo-st-ui.ts +12 -32
- package/src/package.ts +76 -56
- package/src/plugins/mermade.ts +9 -9
- package/src/polytool/const.ts +40 -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/transformation.ts +326 -0
- package/src/polytool/ui.ts +59 -0
- package/src/polytool/utils.ts +20 -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 -5
- package/tsconfig.json +4 -10
- 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/view/apps/oligo-translator.ts +0 -184
- /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,498 @@
|
|
|
1
|
+
import {NUCLEOTIDES} from '../../../common/model/const';
|
|
2
|
+
import {STRAND, STRANDS, STRAND_END, STRAND_TO_END_TERMINUS_MAP, STRAND_ENDS} from '../../model/const';
|
|
3
|
+
import {PatternConfiguration} from '../../model/types';
|
|
4
|
+
import {SVG_CIRCLE_SIZES, SVG_TEXT_FONT_SIZES, NUMERIC_LABEL_POSITION_OFFSET, DEFAULT_FONT_FAMILY, Y_POSITIONS_FOR_STRAND_ELEMENTS, STRAND_END_LABEL_TEXT} from './const';
|
|
5
|
+
import {Position, StrandToNumberMap, StrandEndToNumberMap} from '../types';
|
|
6
|
+
import {isOverhangNucleotide} from '../../model/utils';
|
|
7
|
+
|
|
8
|
+
export class PatternSVGDimensionsCalculator {
|
|
9
|
+
private canvasDimensions: CanvasDimensionCalculator;
|
|
10
|
+
private nucleotidePositionCalculator: NucleotidePositionCalculator;
|
|
11
|
+
private legendPositionCalculator: LegendPositionCalculator;
|
|
12
|
+
private labelPositionCalculator: LabelPositionCalculator;
|
|
13
|
+
private linkageStarPositionCalculator: LinkageStarPositionCalculator;
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
private config: PatternConfiguration
|
|
17
|
+
) {
|
|
18
|
+
const rightOverhangNucleotideCounts = this.computeRightOverhangNucleotideCounts();
|
|
19
|
+
const maxEffectiveStrandLength = this.computeMaxEffectiveStrandLength(rightOverhangNucleotideCounts);
|
|
20
|
+
const maxWidthOfRightOverhangs = this.computeMaxWidthOfRightOverhangs(rightOverhangNucleotideCounts);
|
|
21
|
+
const strandLabelWidth = this.computeStrandLabelWidth();
|
|
22
|
+
const maxTerminusLabelWidthByEnd = this.computeMaxWidthOfTerminusLabels();
|
|
23
|
+
|
|
24
|
+
this.initializeCalculators(
|
|
25
|
+
rightOverhangNucleotideCounts,
|
|
26
|
+
maxEffectiveStrandLength,
|
|
27
|
+
maxWidthOfRightOverhangs,
|
|
28
|
+
strandLabelWidth,
|
|
29
|
+
maxTerminusLabelWidthByEnd
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private initializeCalculators(
|
|
34
|
+
rightOverhangNucleotideCounts: StrandToNumberMap,
|
|
35
|
+
maxEffectiveStrandLength: number,
|
|
36
|
+
maxWidthOfRightOverhangs: number,
|
|
37
|
+
strandLabelWidth: StrandEndToNumberMap,
|
|
38
|
+
maxTerminusLabelWidthByEnd: StrandEndToNumberMap
|
|
39
|
+
): void {
|
|
40
|
+
this.canvasDimensions = new CanvasDimensionCalculator(
|
|
41
|
+
this.config,
|
|
42
|
+
maxEffectiveStrandLength,
|
|
43
|
+
maxWidthOfRightOverhangs,
|
|
44
|
+
strandLabelWidth,
|
|
45
|
+
maxTerminusLabelWidthByEnd
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
this.nucleotidePositionCalculator = new NucleotidePositionCalculator(
|
|
49
|
+
this.config,
|
|
50
|
+
maxEffectiveStrandLength,
|
|
51
|
+
rightOverhangNucleotideCounts,
|
|
52
|
+
maxTerminusLabelWidthByEnd
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
this.legendPositionCalculator = new LegendPositionCalculator(
|
|
56
|
+
this.config,
|
|
57
|
+
this.canvasDimensions
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
this.labelPositionCalculator = new LabelPositionCalculator(
|
|
61
|
+
this.config,
|
|
62
|
+
maxWidthOfRightOverhangs,
|
|
63
|
+
maxTerminusLabelWidthByEnd,
|
|
64
|
+
strandLabelWidth,
|
|
65
|
+
rightOverhangNucleotideCounts,
|
|
66
|
+
this.nucleotidePositionCalculator
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
this.linkageStarPositionCalculator = new LinkageStarPositionCalculator(
|
|
70
|
+
this.nucleotidePositionCalculator,
|
|
71
|
+
rightOverhangNucleotideCounts
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private computeRightOverhangNucleotideCounts(): StrandToNumberMap {
|
|
76
|
+
return STRANDS.reduce((overhangCounts, strand) => {
|
|
77
|
+
overhangCounts[strand] = this.countOverhangNucleotidesAtStartOfStrand(strand);
|
|
78
|
+
return overhangCounts;
|
|
79
|
+
}, {} as StrandToNumberMap);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private computeMaxEffectiveStrandLength(rightOverhangNucleotideCounts: StrandToNumberMap): number {
|
|
83
|
+
return Math.max(...STRANDS.map((strand) => this.config.nucleotideSequences[strand].length - rightOverhangNucleotideCounts[strand]));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private computeMaxWidthOfRightOverhangs(rightOverhangNucleotideCounts: StrandToNumberMap): number {
|
|
87
|
+
return Math.max(rightOverhangNucleotideCounts[STRAND.SENSE], rightOverhangNucleotideCounts[STRAND.ANTISENSE]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getCanvasWidth(): number {
|
|
91
|
+
return this.canvasDimensions.getCanvasWidth();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getCanvasHeight(): number {
|
|
95
|
+
return this.canvasDimensions.getCanvasHeight();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getNucleotideCirclePosition(indexOfNucleotide: number, strand: STRAND): Position {
|
|
99
|
+
return this.nucleotidePositionCalculator.getNucleotideCirclePosition(indexOfNucleotide, strand);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
getNucleotideLabelTextPosition(indexOfNucleotide: number, strand: STRAND): Position {
|
|
103
|
+
return this.nucleotidePositionCalculator.getNucleotideLabelTextPosition(indexOfNucleotide, strand);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getNumericLabelPosition(
|
|
107
|
+
indexOfNucleotide: number,
|
|
108
|
+
strand: STRAND,
|
|
109
|
+
displayedNucleotideNumber: number
|
|
110
|
+
): Position {
|
|
111
|
+
return this.nucleotidePositionCalculator.getNumericLabelPosition(indexOfNucleotide, strand, displayedNucleotideNumber);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
getLegendCirclePosition(index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): Position {
|
|
116
|
+
return this.legendPositionCalculator.getLegendCirclePosition(index, distinctNucleobaseTypes, containsPhosphorothioateLinkages);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getLegendTextPosition(index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): Position {
|
|
120
|
+
return this.legendPositionCalculator.getLegendTextPosition(index, distinctNucleobaseTypes, containsPhosphorothioateLinkages);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getStarLabelPosition(): Position {
|
|
124
|
+
return this.legendPositionCalculator.getStarLabelPosition();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getPhosphorothioateLinkageLabelPosition(): Position {
|
|
128
|
+
return this.legendPositionCalculator.getPhosphorothioateLinkageLabelPosition();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getTerminusLabelPosition(strand: STRAND, end: STRAND_END): Position {
|
|
132
|
+
return this.labelPositionCalculator.getTerminusLabelPosition(strand, end);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getStrandEndLabelPosition(strand: STRAND, end: STRAND_END): Position {
|
|
136
|
+
return this.labelPositionCalculator.getStrandEndLabelPosition(strand, end);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getTitleTextPosition(): Position {
|
|
140
|
+
return this.labelPositionCalculator.getTitleTextPosition();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getCommentLabelPosition(): Position {
|
|
144
|
+
return this.labelPositionCalculator.getCommentLabelPosition();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getCenterPositionOfLinkageStar(index: number, strand: STRAND): Position {
|
|
148
|
+
return this.linkageStarPositionCalculator.getCenterPositionOfLinkageStar(index, strand);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private computeStrandLabelWidth(): StrandEndToNumberMap {
|
|
152
|
+
const widthOfStrandLabel = Object.fromEntries(
|
|
153
|
+
STRAND_ENDS.map(
|
|
154
|
+
(strandEnd) => [strandEnd, this.getMaxWidthStrandEndLabelsByEnd(strandEnd)]
|
|
155
|
+
)
|
|
156
|
+
) as StrandEndToNumberMap;
|
|
157
|
+
|
|
158
|
+
return widthOfStrandLabel;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private computeMaxWidthOfTerminusLabels(): StrandEndToNumberMap {
|
|
162
|
+
const maxWidthOfTerminusLabelsByEnd = STRAND_ENDS.reduce((maxWidthMap, end) => {
|
|
163
|
+
maxWidthMap[end] = this.getMaxWidthOfTerminusLabelsByEnd(end);
|
|
164
|
+
return maxWidthMap;
|
|
165
|
+
}, {} as StrandEndToNumberMap);
|
|
166
|
+
|
|
167
|
+
return maxWidthOfTerminusLabelsByEnd;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private getMaxWidthOfTerminusLabelsByEnd(end: STRAND_END): number {
|
|
171
|
+
return this.calculateMaxWidthOfStrandEndLabel(
|
|
172
|
+
(strand) => this.config.strandTerminusModifications[strand][STRAND_TO_END_TERMINUS_MAP[strand][end]]
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private getMaxWidthStrandEndLabelsByEnd(strandEnd: STRAND_END): number {
|
|
177
|
+
return this.calculateMaxWidthOfStrandEndLabel(
|
|
178
|
+
(strand) => STRAND_END_LABEL_TEXT[strandEnd][strand]
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private calculateMaxWidthOfStrandEndLabel(getLabelText: (strand: STRAND) => string): number {
|
|
183
|
+
const textWidthCalculator = TextWidthCalculator.getInstance();
|
|
184
|
+
return Math.max(
|
|
185
|
+
...STRANDS.map((strand) =>
|
|
186
|
+
textWidthCalculator.computeTextWidth(
|
|
187
|
+
getLabelText(strand),
|
|
188
|
+
SVG_TEXT_FONT_SIZES.NUCLEOBASE,
|
|
189
|
+
DEFAULT_FONT_FAMILY
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private countOverhangNucleotidesAtStartOfStrand(strand: STRAND): number {
|
|
196
|
+
const nucleotides = this.config.nucleotideSequences[strand];
|
|
197
|
+
|
|
198
|
+
let overhangNucleotidesCount = 0;
|
|
199
|
+
for (const nucleotide of nucleotides) {
|
|
200
|
+
if (!isOverhangNucleotide(nucleotide))
|
|
201
|
+
break;
|
|
202
|
+
overhangNucleotidesCount++;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return overhangNucleotidesCount;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
class TextWidthCalculator {
|
|
210
|
+
private static instance: TextWidthCalculator;
|
|
211
|
+
private canvas: HTMLCanvasElement;
|
|
212
|
+
private context: CanvasRenderingContext2D | null;
|
|
213
|
+
private pixelRatio: number;
|
|
214
|
+
|
|
215
|
+
// WARNING: singleton used to avoid creating canvas element on every call
|
|
216
|
+
private constructor() {
|
|
217
|
+
this.canvas = document.createElement('canvas');
|
|
218
|
+
this.context = this.canvas.getContext('2d');
|
|
219
|
+
this.pixelRatio = window.devicePixelRatio || 1;
|
|
220
|
+
|
|
221
|
+
this.canvas.width *= this.pixelRatio;
|
|
222
|
+
this.canvas.height *= this.pixelRatio;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
public static getInstance(): TextWidthCalculator {
|
|
226
|
+
if (!TextWidthCalculator.instance)
|
|
227
|
+
TextWidthCalculator.instance = new TextWidthCalculator();
|
|
228
|
+
|
|
229
|
+
return TextWidthCalculator.instance;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public computeTextWidth(text: string, fontSize: number, fontFamily: string): number {
|
|
233
|
+
if (this.context) {
|
|
234
|
+
this.context.font = `${fontSize * this.pixelRatio}px ${fontFamily}`;
|
|
235
|
+
const metrics = this.context.measureText(text);
|
|
236
|
+
return metrics.width / this.pixelRatio;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
class CanvasDimensionCalculator {
|
|
244
|
+
constructor(
|
|
245
|
+
private config: PatternConfiguration,
|
|
246
|
+
private maxEffectiveStrandLength: number,
|
|
247
|
+
private maxWidthOfRightOverhangs: number,
|
|
248
|
+
private strandLabelWidth: StrandEndToNumberMap,
|
|
249
|
+
private maxTerminusWidthByEnd: StrandEndToNumberMap
|
|
250
|
+
) {}
|
|
251
|
+
|
|
252
|
+
getCanvasWidth(): number {
|
|
253
|
+
const widthOfNucleobases = SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER * (this.maxEffectiveStrandLength + this.maxWidthOfRightOverhangs);
|
|
254
|
+
|
|
255
|
+
const canvasWidth = STRAND_ENDS.reduce((totalWidth, end) => {
|
|
256
|
+
totalWidth += this.strandLabelWidth[end] + this.maxTerminusWidthByEnd[end];
|
|
257
|
+
return totalWidth;
|
|
258
|
+
}, 0) + widthOfNucleobases + SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER;
|
|
259
|
+
|
|
260
|
+
return canvasWidth;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
getCanvasHeight(): number {
|
|
264
|
+
return (this.config.isAntisenseStrandIncluded ? 11 : 9) * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
class NucleotidePositionCalculator {
|
|
269
|
+
constructor(
|
|
270
|
+
private config: PatternConfiguration,
|
|
271
|
+
private maxEffectiveStrandLength: number,
|
|
272
|
+
private rightOverhangNucleotideCounts: StrandToNumberMap,
|
|
273
|
+
private maxTerminusWidthByEnd: StrandEndToNumberMap
|
|
274
|
+
) {}
|
|
275
|
+
|
|
276
|
+
getNucleotideCirclePosition(indexOfNucleotide: number, strand: STRAND): Position {
|
|
277
|
+
return {
|
|
278
|
+
x: this.computeNucleotideCircleXPositionByStrand(indexOfNucleotide, strand),
|
|
279
|
+
y: Y_POSITIONS_FOR_STRAND_ELEMENTS[strand].NUCLEOBASE_CIRCLE,
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
getNucleotideLabelTextPosition(indexOfNucleotide: number, strand: STRAND): Position {
|
|
284
|
+
return {
|
|
285
|
+
// todo: refactor legacy dependency on one-digit parameter
|
|
286
|
+
x: this.computeNucleotideCircleXPositionByStrand(indexOfNucleotide, strand) + NUMERIC_LABEL_POSITION_OFFSET.ONE_DIGIT,
|
|
287
|
+
y: Y_POSITIONS_FOR_STRAND_ELEMENTS[strand].NUCLEOBASE_LABEL,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// todo: cleanup legacy logic
|
|
292
|
+
private computeNucleotideCircleXPositionByStrand(index: number, strand: STRAND): number {
|
|
293
|
+
const rightOverhangCount = this.rightOverhangNucleotideCounts[strand];
|
|
294
|
+
return this.computeNucleobaseCircleXPosition(index, rightOverhangCount);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
computeNucleobaseCircleXPosition(index: number, rightOverhangCount: number): number {
|
|
298
|
+
const rightModificationOffset = this.maxTerminusWidthByEnd[STRAND_END.RIGHT];
|
|
299
|
+
const positionalIndex = this.maxEffectiveStrandLength - index + rightOverhangCount + 1;
|
|
300
|
+
const xPosition = positionalIndex * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER;
|
|
301
|
+
const finalPosition = rightModificationOffset + xPosition;
|
|
302
|
+
return finalPosition;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
getNumericLabelPosition(
|
|
306
|
+
indexOfNucleotide: number,
|
|
307
|
+
strand: STRAND,
|
|
308
|
+
displayedNucleotideNumber: number
|
|
309
|
+
): Position {
|
|
310
|
+
const indexForVisualStrand = this.getVisualStrandIndex(indexOfNucleotide, strand);
|
|
311
|
+
|
|
312
|
+
const numericLabelOffset = this.computeNumericLabelXOffset(
|
|
313
|
+
this.config.nucleotideSequences[strand],
|
|
314
|
+
indexForVisualStrand,
|
|
315
|
+
displayedNucleotideNumber
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const numericLabelXPosition = this.computeNucleotideCircleXPositionByStrand(indexOfNucleotide, strand) + numericLabelOffset;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
x: numericLabelXPosition,
|
|
322
|
+
y: Y_POSITIONS_FOR_STRAND_ELEMENTS[strand].NUMERIC_LABEL,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private computeNumericLabelXOffset(bases: string[], nucleobaseIndex: number, nucleotideNumericLabel: number): number {
|
|
327
|
+
const isSingleDigitLabel = nucleotideNumericLabel >= 0 && nucleotideNumericLabel < 10;
|
|
328
|
+
|
|
329
|
+
const criterion = isSingleDigitLabel || NUCLEOTIDES.includes(bases[nucleobaseIndex]);
|
|
330
|
+
|
|
331
|
+
return criterion ?
|
|
332
|
+
NUMERIC_LABEL_POSITION_OFFSET.ONE_DIGIT :
|
|
333
|
+
NUMERIC_LABEL_POSITION_OFFSET.TWO_DIGIT;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Inverses index for antisense strand image */
|
|
337
|
+
private getVisualStrandIndex(indexOfNucleotide: number, strand: STRAND): number {
|
|
338
|
+
return strand === STRAND.SENSE ?
|
|
339
|
+
indexOfNucleotide :
|
|
340
|
+
this.config.nucleotideSequences[strand].length - indexOfNucleotide;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
class LegendPositionCalculator {
|
|
345
|
+
constructor(
|
|
346
|
+
private config: PatternConfiguration,
|
|
347
|
+
private canvasDimensionCalculator: CanvasDimensionCalculator
|
|
348
|
+
) {}
|
|
349
|
+
|
|
350
|
+
getLegendCirclePosition(index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): Position {
|
|
351
|
+
const centerPosition = {
|
|
352
|
+
x: this.computeLegendCircleXPosition(index, distinctNucleobaseTypes, containsPhosphorothioateLinkages),
|
|
353
|
+
y: this.getLegendVerticalPosition(),
|
|
354
|
+
};
|
|
355
|
+
return centerPosition;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
getLegendTextPosition(index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): Position {
|
|
359
|
+
const legendPosition = {
|
|
360
|
+
x: this.computeLegendCircleXPosition(index, distinctNucleobaseTypes, containsPhosphorothioateLinkages) + SVG_CIRCLE_SIZES.LEGEND_RADIUS + 4,
|
|
361
|
+
y: this.getLegendTextVerticalPosition(),
|
|
362
|
+
};
|
|
363
|
+
return legendPosition;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private computeLegendCircleXPosition(index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): number {
|
|
367
|
+
const legendStartIndex = containsPhosphorothioateLinkages ? 1 : 0;
|
|
368
|
+
const totalPositions = distinctNucleobaseTypes.length + legendStartIndex;
|
|
369
|
+
const width = this.canvasDimensionCalculator.getCanvasWidth();
|
|
370
|
+
const spacingUnit = width / totalPositions;
|
|
371
|
+
const position = (index + legendStartIndex) * spacingUnit;
|
|
372
|
+
const adjustedPosition = position + SVG_CIRCLE_SIZES.LEGEND_RADIUS;
|
|
373
|
+
return Math.round(adjustedPosition);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
getStarLabelPosition(): Position {
|
|
377
|
+
return {
|
|
378
|
+
x: SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS,
|
|
379
|
+
y: this.getLegendVerticalPosition(),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
getPhosphorothioateLinkageLabelPosition(): Position {
|
|
384
|
+
return {
|
|
385
|
+
x: 2 * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS - 8,
|
|
386
|
+
y: this.getLegendTextVerticalPosition(),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private getLegendVerticalPosition(): number {
|
|
391
|
+
return (this.config.isAntisenseStrandIncluded ? 9.5 : 6) * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private getLegendTextVerticalPosition(): number {
|
|
395
|
+
const position = this.config.isAntisenseStrandIncluded ? 10 * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS : Y_POSITIONS_FOR_STRAND_ELEMENTS[STRAND.ANTISENSE].NUCLEOBASE_CIRCLE;
|
|
396
|
+
return position - 3;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
class LabelPositionCalculator {
|
|
401
|
+
private xPositionOfTerminusModifications: Record<typeof STRAND_ENDS[number], StrandToNumberMap>;
|
|
402
|
+
constructor(
|
|
403
|
+
private config: PatternConfiguration,
|
|
404
|
+
private maxWidthOfRightOverhangs: number,
|
|
405
|
+
private maxTerminusWidthByEnd: StrandEndToNumberMap,
|
|
406
|
+
private strandLabelWidth: StrandEndToNumberMap,
|
|
407
|
+
private rightOverhangNucleotideCounts: StrandToNumberMap,
|
|
408
|
+
private nucleotidePositionCalculator: NucleotidePositionCalculator
|
|
409
|
+
) {
|
|
410
|
+
this.xPositionOfTerminusModifications = this.computeXPositionOfTerminusModifications();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
getTerminusLabelPosition(strand: STRAND, end: STRAND_END): Position {
|
|
414
|
+
return {
|
|
415
|
+
x: this.xPositionOfTerminusModifications[end][strand],
|
|
416
|
+
y: Y_POSITIONS_FOR_STRAND_ELEMENTS[strand].NUCLEOBASE_LABEL,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
getStrandEndLabelPosition(strand: STRAND, end: STRAND_END): Position {
|
|
421
|
+
const xPosition = this.getXPositionOfStrandLabels()[end];
|
|
422
|
+
return {
|
|
423
|
+
x: xPosition,
|
|
424
|
+
// todo: remove legacy grouping by y positions
|
|
425
|
+
y: Y_POSITIONS_FOR_STRAND_ELEMENTS[strand].NUCLEOBASE_LABEL,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
getTitleTextPosition(): Position {
|
|
430
|
+
return {
|
|
431
|
+
x: SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS,
|
|
432
|
+
y: SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
getCommentLabelPosition(): Position {
|
|
437
|
+
const y = (this.config.isAntisenseStrandIncluded ? 11 : 8.5) * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS;
|
|
438
|
+
|
|
439
|
+
const x = this.getXPositionOfStrandLabels()[STRAND_END.LEFT];
|
|
440
|
+
return {x, y};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private getXPositionOfStrandLabels(): StrandEndToNumberMap {
|
|
444
|
+
const maxRightTerminusModificationShift = Math.max(
|
|
445
|
+
...STRANDS.map((strand) => this.xPositionOfTerminusModifications[STRAND_END.RIGHT][strand])
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const rightEndXPosition = maxRightTerminusModificationShift +
|
|
449
|
+
this.maxTerminusWidthByEnd[STRAND_END.LEFT] +
|
|
450
|
+
SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER * this.maxWidthOfRightOverhangs;
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
[STRAND_END.LEFT]: 0,
|
|
454
|
+
[STRAND_END.RIGHT]: rightEndXPosition,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private computeXPositionOfTerminusModifications(): Record<typeof STRAND_ENDS[number], StrandToNumberMap> {
|
|
459
|
+
const xPositionOfTerminusModifications = Object.fromEntries(
|
|
460
|
+
STRAND_ENDS.map((end) => [
|
|
461
|
+
end,
|
|
462
|
+
Object.fromEntries(
|
|
463
|
+
STRANDS.map((strand) => [
|
|
464
|
+
strand,
|
|
465
|
+
end === STRAND_END.LEFT ?
|
|
466
|
+
this.strandLabelWidth[STRAND_END.LEFT] - 5 :
|
|
467
|
+
this.rightOverhangNucleotideCounts[strand] * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER + this.nucleotidePositionCalculator.computeNucleobaseCircleXPosition(-0.5, 0)
|
|
468
|
+
])
|
|
469
|
+
)
|
|
470
|
+
])
|
|
471
|
+
) as Record<typeof STRAND_ENDS[number], StrandToNumberMap>;
|
|
472
|
+
|
|
473
|
+
return xPositionOfTerminusModifications;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
class LinkageStarPositionCalculator {
|
|
478
|
+
constructor(
|
|
479
|
+
private nucleotidePositionCalculator: NucleotidePositionCalculator,
|
|
480
|
+
private rightOverhangNucleotideCounts: StrandToNumberMap
|
|
481
|
+
) {}
|
|
482
|
+
|
|
483
|
+
getCenterPositionOfLinkageStar(index: number, strand: STRAND): Position {
|
|
484
|
+
return {
|
|
485
|
+
x: this.getXPositionOfLinkageStar(index, strand),
|
|
486
|
+
y: this.getYPositionOfLinkageStar(strand),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// todo: remove legacy division of x-y coordinates throughout the code
|
|
491
|
+
private getXPositionOfLinkageStar(index: number, strand: STRAND): number {
|
|
492
|
+
return this.nucleotidePositionCalculator.computeNucleobaseCircleXPosition(index, this.rightOverhangNucleotideCounts[strand]) + SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private getYPositionOfLinkageStar(strand: STRAND): number {
|
|
496
|
+
return Y_POSITIONS_FOR_STRAND_ELEMENTS[strand].NUCLEOBASE_LABEL + SVG_CIRCLE_SIZES.LINKAGE_STAR_RADIUS;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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 {PatternConfiguration, StrandType} from '../../model/types';
|
|
7
|
+
import {EventBus} from '../../model/event-bus';
|
|
8
|
+
import {NucleotidePatternSVGRenderer} from './svg-renderer';
|
|
9
|
+
//@ts-ignore
|
|
10
|
+
import * as svgExport from 'save-svg-as-png';
|
|
11
|
+
|
|
12
|
+
export class SvgDisplayManager {
|
|
13
|
+
private svgDisplayDiv = ui.div([]);
|
|
14
|
+
private svgElement: SVGElement;
|
|
15
|
+
|
|
16
|
+
private constructor(
|
|
17
|
+
private eventBus: EventBus
|
|
18
|
+
) {
|
|
19
|
+
eventBus.patternStateChanged$.subscribe(() => this.updateSvgContainer());
|
|
20
|
+
eventBus.svgSaveRequested$.subscribe(() => this.saveSvgAsPng());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static createSvgDiv(eventBus: EventBus): HTMLDivElement {
|
|
24
|
+
const displayManager = new SvgDisplayManager(eventBus);
|
|
25
|
+
return displayManager.svgDisplayDiv;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private updateSvgContainer(): void {
|
|
29
|
+
$(this.svgDisplayDiv).empty();
|
|
30
|
+
const patternConfig = this.eventBus.getPatternConfig();
|
|
31
|
+
this.svgElement = this.createSvg(patternConfig);
|
|
32
|
+
this.svgDisplayDiv.append(this.svgElement);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private createSvg(patternConfig: PatternConfiguration) {
|
|
36
|
+
const renderer = new NucleotidePatternSVGRenderer(patternConfig);
|
|
37
|
+
const svg = renderer.renderPattern();
|
|
38
|
+
return svg;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private saveSvgAsPng(): void {
|
|
42
|
+
const patternName = this.eventBus.getPatternName();
|
|
43
|
+
svgExport.saveSvgAsPng(this.svgElement, patternName, {backgroundColor: 'white'});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
public 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
|
+
public 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
|
+
public 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
|
+
public 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
|
+
}
|