@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,396 @@
|
|
|
1
|
+
import {SVGElementFactory} from './svg-element-factory';
|
|
2
|
+
import {isOverhangNucleotide} from '../../model/utils';
|
|
3
|
+
import {STRAND, STRANDS, STRAND_END, STRAND_ENDS, TERMINUS, TERMINI} from '../../model/const';
|
|
4
|
+
import {PatternConfiguration, StrandType, TerminalType} from '../../model/types';
|
|
5
|
+
import {SVG_CIRCLE_SIZES, SVG_TEXT_FONT_SIZES, SVG_ELEMENT_COLORS, STRAND_END_LABEL_TEXT} from './const';
|
|
6
|
+
import {StrandToNumberMap, StrandEndToSVGElementsMap, TerminusToSVGElementMap} from '../types';
|
|
7
|
+
import {
|
|
8
|
+
getNucleobaseLabelForCircle,
|
|
9
|
+
computeTextColorForNucleobaseLabel,
|
|
10
|
+
getNucleobaseColorFromStyleMap,
|
|
11
|
+
} from './utils';
|
|
12
|
+
import {PatternSVGDimensionsCalculator} from './dimensions-calculator';
|
|
13
|
+
|
|
14
|
+
export class NucleotidePatternSVGRenderer {
|
|
15
|
+
private config: PatternConfiguration;
|
|
16
|
+
private patternDimensionsCalculator: PatternSVGDimensionsCalculator;
|
|
17
|
+
private svgFactory: SVGElementFactoryWrapper;
|
|
18
|
+
private strandElementManager: StrandElementBuilder;
|
|
19
|
+
private legendBuilder: LegendBuilder;
|
|
20
|
+
|
|
21
|
+
constructor(patternConfig: PatternConfiguration) {
|
|
22
|
+
this.setupPatternConfig(patternConfig);
|
|
23
|
+
|
|
24
|
+
// todo: prefer dependency injection for all these properties
|
|
25
|
+
this.patternDimensionsCalculator = new PatternSVGDimensionsCalculator(this.config);
|
|
26
|
+
|
|
27
|
+
this.svgFactory = new SVGElementFactoryWrapper(new SVGElementFactory(), this.config, this.patternDimensionsCalculator);
|
|
28
|
+
|
|
29
|
+
this.strandElementManager = new StrandElementBuilder(this.svgFactory, this.config);
|
|
30
|
+
this.legendBuilder = new LegendBuilder(this.svgFactory, this.config);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
renderPattern(): SVGElement {
|
|
34
|
+
const labelElements = this.createLabelElements();
|
|
35
|
+
|
|
36
|
+
const countOfNucleotidesExcludingOverhangs = this.countNucleotidesExcludingOverhangs();
|
|
37
|
+
const strandElements = this.strandElementManager.createStrandElements(countOfNucleotidesExcludingOverhangs);
|
|
38
|
+
const titleElement = this.svgFactory.createTitleElement(this.config.patternName, countOfNucleotidesExcludingOverhangs, this.config.isAntisenseStrandIncluded);
|
|
39
|
+
|
|
40
|
+
const legend = this.legendBuilder.getLegendItems();
|
|
41
|
+
|
|
42
|
+
const patternSVGCanvas = this.svgFactory.createCanvas();
|
|
43
|
+
patternSVGCanvas.append(...labelElements, ...strandElements, titleElement, ...legend);
|
|
44
|
+
|
|
45
|
+
return patternSVGCanvas;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private countNucleotidesExcludingOverhangs(): StrandToNumberMap {
|
|
49
|
+
return STRANDS.reduce((acc, strand) => {
|
|
50
|
+
acc[strand] = this.config.nucleotideSequences[strand].filter((value) => !isOverhangNucleotide(value)).length;
|
|
51
|
+
return acc;
|
|
52
|
+
}, {} as StrandToNumberMap);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private createLabelElements(): SVGElement[] {
|
|
56
|
+
const strandEndLabels = this.getStrandEndLabels();
|
|
57
|
+
const labelsTerminusModification = this.getTerminusModificationLabels();
|
|
58
|
+
|
|
59
|
+
const labelElements = [
|
|
60
|
+
...STRANDS.flatMap((strand) => [
|
|
61
|
+
...Object.values(strandEndLabels[strand]),
|
|
62
|
+
...Object.values(labelsTerminusModification[strand])
|
|
63
|
+
]),
|
|
64
|
+
].filter((element) => element !== null) as SVGElement[];
|
|
65
|
+
|
|
66
|
+
return labelElements;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private setupPatternConfig(patternConfig: PatternConfiguration): void {
|
|
70
|
+
// WARNING: to ensure immutability, we need to deep copy the config object
|
|
71
|
+
this.config = JSON.parse(JSON.stringify(patternConfig)) as PatternConfiguration;
|
|
72
|
+
|
|
73
|
+
this.config.nucleotideSequences[STRAND.SENSE].reverse();
|
|
74
|
+
this.config.phosphorothioateLinkageFlags[STRAND.SENSE].reverse();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private getStrandEndLabels() {
|
|
78
|
+
const strandEndLabels = STRANDS.reduce((acc, strand) => {
|
|
79
|
+
acc[strand] = STRAND_ENDS.reduce((endAcc, end) => {
|
|
80
|
+
endAcc[end] = this.svgFactory.createLabelForStrandEnd(strand, end);
|
|
81
|
+
return endAcc;
|
|
82
|
+
}, {} as StrandEndToSVGElementsMap);
|
|
83
|
+
return acc;
|
|
84
|
+
}, {} as Record<typeof STRANDS[number], StrandEndToSVGElementsMap>);
|
|
85
|
+
|
|
86
|
+
return strandEndLabels;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private getTerminusModificationLabels() {
|
|
90
|
+
const labelsTerminusModification = STRANDS.reduce((acc, strand) => {
|
|
91
|
+
acc[strand] = TERMINI.reduce((terminiAcc, terminus) => {
|
|
92
|
+
terminiAcc[terminus] = this.svgFactory.createTerminusModificationLabel(strand, terminus);
|
|
93
|
+
return terminiAcc;
|
|
94
|
+
}, {} as TerminusToSVGElementMap);
|
|
95
|
+
return acc;
|
|
96
|
+
}, {} as Record<typeof STRANDS[number], TerminusToSVGElementMap>);
|
|
97
|
+
|
|
98
|
+
return labelsTerminusModification;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class LegendBuilder {
|
|
103
|
+
private containsPhosphorothioateLinkages: boolean;
|
|
104
|
+
constructor(
|
|
105
|
+
private svgFactory: SVGElementFactoryWrapper,
|
|
106
|
+
private config: PatternConfiguration
|
|
107
|
+
) {
|
|
108
|
+
this.containsPhosphorothioateLinkages = this.checkAnyPhosphorothioateLinkages();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getLegendItems(): SVGElement[] {
|
|
112
|
+
const commentLabel = this.svgFactory.createCommentLabel();
|
|
113
|
+
const nucleotideLegendItems = this.createLegendItemsForNucleotideTypes();
|
|
114
|
+
const phosphorothioateLinkageLegendItem = this.createLegendItemForPhosphorothioateLinkage(this.containsPhosphorothioateLinkages);
|
|
115
|
+
|
|
116
|
+
return [commentLabel, ...nucleotideLegendItems, ...phosphorothioateLinkageLegendItem];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private createLegendItemsForNucleotideTypes(): SVGElement[] {
|
|
120
|
+
const distinctNucleobaseTypes = this.extractNucleotideTypes();
|
|
121
|
+
const svgElements = [] as SVGElement[];
|
|
122
|
+
distinctNucleobaseTypes.forEach((nucleobaseType, index) => {
|
|
123
|
+
const legendCircle = this.svgFactory.createLegendCircle(nucleobaseType, index, distinctNucleobaseTypes, this.containsPhosphorothioateLinkages);
|
|
124
|
+
const legendText = this.svgFactory.createLegendText(nucleobaseType, index, distinctNucleobaseTypes, this.containsPhosphorothioateLinkages);
|
|
125
|
+
svgElements.push(legendCircle, legendText);
|
|
126
|
+
});
|
|
127
|
+
return svgElements;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private createLegendItemForPhosphorothioateLinkage(containsPhosphorothioateLinkages: boolean): SVGElement[] {
|
|
131
|
+
const starLinkageLegendLabel = this.svgFactory.createLinkageStarLegendLabel(containsPhosphorothioateLinkages);
|
|
132
|
+
const phosphorothioateLinkageLabel = this.svgFactory.createPhosphorothioateLinkageLabel(containsPhosphorothioateLinkages);
|
|
133
|
+
|
|
134
|
+
return [starLinkageLegendLabel, phosphorothioateLinkageLabel].filter((element) => element !== null) as SVGElement[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private extractNucleotideTypes(): string[] {
|
|
138
|
+
const distinctNucleotides = [...new Set(
|
|
139
|
+
this.config.nucleotideSequences[STRAND.SENSE].concat(
|
|
140
|
+
this.config.isAntisenseStrandIncluded ? this.config.nucleotideSequences[STRAND.ANTISENSE] : []
|
|
141
|
+
)
|
|
142
|
+
)];
|
|
143
|
+
|
|
144
|
+
return distinctNucleotides;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private checkAnyPhosphorothioateLinkages(): boolean {
|
|
148
|
+
return [...this.config.phosphorothioateLinkageFlags[STRAND.SENSE],
|
|
149
|
+
...(this.config.isAntisenseStrandIncluded ? this.config.phosphorothioateLinkageFlags[STRAND.ANTISENSE] : [])
|
|
150
|
+
].some((linkage) => linkage);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class NucleotideCountTracker {
|
|
155
|
+
private nucleotideCounts: StrandToNumberMap;
|
|
156
|
+
constructor(initialNucleotideCounts: StrandToNumberMap) {
|
|
157
|
+
// WARNING: to ensure immutability, we need to deep copy the object
|
|
158
|
+
this.nucleotideCounts = JSON.parse(JSON.stringify(initialNucleotideCounts)) as StrandToNumberMap;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
decrementIfNotOverhang(isOverhang: boolean, strand: STRAND): void {
|
|
162
|
+
if (!isOverhang)
|
|
163
|
+
this.nucleotideCounts[strand]--;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getCurrentCount(strand: STRAND): number {
|
|
167
|
+
return this.nucleotideCounts[strand];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
class SVGElementFactoryWrapper {
|
|
172
|
+
constructor(
|
|
173
|
+
private svgElementFactory: SVGElementFactory,
|
|
174
|
+
private config: PatternConfiguration,
|
|
175
|
+
private dimensionsCalculator: PatternSVGDimensionsCalculator
|
|
176
|
+
) { }
|
|
177
|
+
|
|
178
|
+
createCanvas(): SVGElement {
|
|
179
|
+
const canvasWidth = this.dimensionsCalculator.getCanvasWidth();
|
|
180
|
+
const canvasHeight = this.dimensionsCalculator.getCanvasHeight();
|
|
181
|
+
const svgCanvas = this.svgElementFactory.createCanvas(canvasWidth, canvasHeight);
|
|
182
|
+
|
|
183
|
+
return svgCanvas;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
createTitleElement(patternName: string,
|
|
187
|
+
numberOfNucleotides: StrandToNumberMap,
|
|
188
|
+
isAntisenseStrandActive: boolean
|
|
189
|
+
) {
|
|
190
|
+
const titleText = this.getTitleText(patternName, numberOfNucleotides, isAntisenseStrandActive);
|
|
191
|
+
const titleTextPosition = this.dimensionsCalculator.getTitleTextPosition();
|
|
192
|
+
return this.svgElementFactory.createTextElement(
|
|
193
|
+
titleText,
|
|
194
|
+
titleTextPosition,
|
|
195
|
+
SVG_TEXT_FONT_SIZES.NUCLEOBASE,
|
|
196
|
+
SVG_ELEMENT_COLORS.TITLE_TEXT
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private getTitleText(
|
|
201
|
+
patternName: string,
|
|
202
|
+
numberOfNucleotides: StrandToNumberMap,
|
|
203
|
+
isAntisenseStrandActive: boolean
|
|
204
|
+
): string {
|
|
205
|
+
const senseStrandLength = `${numberOfNucleotides[STRAND.SENSE]}`;
|
|
206
|
+
const antisenseStrandLength = isAntisenseStrandActive ? `/${numberOfNucleotides[STRAND.ANTISENSE]}` : '';
|
|
207
|
+
const titleText = `${patternName} for ${senseStrandLength}${antisenseStrandLength}-mer`;
|
|
208
|
+
|
|
209
|
+
return titleText;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
createPhosphorothioateLinkageStar(strand: StrandType, index: number): SVGElement | null {
|
|
213
|
+
const isActive = this.config.phosphorothioateLinkageFlags[strand][index];
|
|
214
|
+
if (!isActive)
|
|
215
|
+
return null;
|
|
216
|
+
|
|
217
|
+
const centerPosition = this.dimensionsCalculator.getCenterPositionOfLinkageStar(index, strand);
|
|
218
|
+
const color = SVG_ELEMENT_COLORS.LINKAGE_STAR;
|
|
219
|
+
const starElement = this.svgElementFactory.createStarElement(centerPosition, color);
|
|
220
|
+
|
|
221
|
+
return starElement;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
createNucleotideNumericLabel(
|
|
225
|
+
indexOfNucleotide: number,
|
|
226
|
+
strand: STRAND,
|
|
227
|
+
displayedNucleotideNumber: number
|
|
228
|
+
): SVGElement {
|
|
229
|
+
const nucleotide = this.config.nucleotideSequences[strand][indexOfNucleotide];
|
|
230
|
+
const isOverhang = isOverhangNucleotide(nucleotide);
|
|
231
|
+
const labelPosition = this.dimensionsCalculator.getNumericLabelPosition(indexOfNucleotide, strand, displayedNucleotideNumber);
|
|
232
|
+
|
|
233
|
+
const labelText = (!isOverhang && this.config.nucleotidesWithNumericLabels.includes(nucleotide)) ?
|
|
234
|
+
String(displayedNucleotideNumber) :
|
|
235
|
+
'';
|
|
236
|
+
|
|
237
|
+
const nucleotideNumericLabel = this.svgElementFactory.createTextElement(
|
|
238
|
+
labelText, labelPosition, SVG_TEXT_FONT_SIZES.COMMENT, SVG_ELEMENT_COLORS.TEXT
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return nucleotideNumericLabel;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
createNucleotideCircle(indexOfNucleotide: number, strand: STRAND): SVGCircleElement {
|
|
245
|
+
const nucleotide = this.config.nucleotideSequences[strand][indexOfNucleotide];
|
|
246
|
+
const nucleotideCirclePosition = this.dimensionsCalculator.getNucleotideCirclePosition(indexOfNucleotide, strand);
|
|
247
|
+
const nucleotideCircle = this.svgElementFactory.createCircleElement(
|
|
248
|
+
nucleotideCirclePosition, SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS, getNucleobaseColorFromStyleMap(nucleotide)
|
|
249
|
+
);
|
|
250
|
+
return nucleotideCircle;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
createNucleotideNameLabel(indexOfNucleotide: number, strand: STRAND): SVGTextElement {
|
|
254
|
+
const nucleotide = this.config.nucleotideSequences[strand][indexOfNucleotide];
|
|
255
|
+
const nucleobaseLabelTextPosition = this.dimensionsCalculator.getNucleotideLabelTextPosition(indexOfNucleotide, strand);
|
|
256
|
+
const nucleotideLabelText = this.svgElementFactory.createTextElement(
|
|
257
|
+
getNucleobaseLabelForCircle(nucleotide), nucleobaseLabelTextPosition, SVG_TEXT_FONT_SIZES.NUCLEOBASE, computeTextColorForNucleobaseLabel(nucleotide)
|
|
258
|
+
);
|
|
259
|
+
return nucleotideLabelText;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
createCommentLabel(): SVGElement {
|
|
263
|
+
const commentLabelPosition = this.dimensionsCalculator.getCommentLabelPosition();
|
|
264
|
+
|
|
265
|
+
const commentLabel = this.svgElementFactory.createTextElement(
|
|
266
|
+
this.config.patternComment,
|
|
267
|
+
commentLabelPosition,
|
|
268
|
+
SVG_TEXT_FONT_SIZES.COMMENT,
|
|
269
|
+
SVG_ELEMENT_COLORS.TEXT
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
return commentLabel;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
createLinkageStarLegendLabel(isPhosphorothioateLinkageActive: boolean): SVGElement | null {
|
|
276
|
+
const starLabelPosition = this.dimensionsCalculator.getStarLabelPosition();
|
|
277
|
+
const starLabel = isPhosphorothioateLinkageActive ?
|
|
278
|
+
this.svgElementFactory.createStarElement(starLabelPosition, SVG_ELEMENT_COLORS.LINKAGE_STAR) :
|
|
279
|
+
null;
|
|
280
|
+
|
|
281
|
+
return starLabel;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
createPhosphorothioateLinkageLabel(isPhosphorothioateLinkageActive: boolean): SVGElement | null {
|
|
285
|
+
const position = this.dimensionsCalculator.getPhosphorothioateLinkageLabelPosition();
|
|
286
|
+
const phosphorothioateLinkageLabel = isPhosphorothioateLinkageActive ?
|
|
287
|
+
this.svgElementFactory.createTextElement(
|
|
288
|
+
'ps linkage',
|
|
289
|
+
position,
|
|
290
|
+
SVG_TEXT_FONT_SIZES.COMMENT,
|
|
291
|
+
SVG_ELEMENT_COLORS.TEXT) :
|
|
292
|
+
null;
|
|
293
|
+
|
|
294
|
+
return phosphorothioateLinkageLabel;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
createLabelForStrandEnd(strand: StrandType, end: STRAND_END): SVGElement | null {
|
|
298
|
+
const isLabelActive = (strand === STRAND.SENSE) || (strand === STRAND.ANTISENSE && this.config.isAntisenseStrandIncluded);
|
|
299
|
+
if (!isLabelActive)
|
|
300
|
+
return null;
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
const labelText = STRAND_END_LABEL_TEXT[end][strand];
|
|
304
|
+
const labelPosition = this.dimensionsCalculator.getStrandEndLabelPosition(strand, end);
|
|
305
|
+
|
|
306
|
+
return this.svgElementFactory.createTextElement(labelText, labelPosition, SVG_TEXT_FONT_SIZES.NUCLEOBASE, SVG_ELEMENT_COLORS.TEXT);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
createTerminusModificationLabel(strand: StrandType, terminus: TerminalType): SVGElement | null {
|
|
310
|
+
if (strand === STRAND.ANTISENSE && !this.config.isAntisenseStrandIncluded)
|
|
311
|
+
return null;
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
const end = (strand === STRAND.SENSE && terminus === TERMINUS.FIVE_PRIME) ||
|
|
315
|
+
(strand === STRAND.ANTISENSE && terminus === TERMINUS.THREE_PRIME) ? STRAND_END.LEFT : STRAND_END.RIGHT;
|
|
316
|
+
|
|
317
|
+
const labelText = this.config.strandTerminusModifications[strand][terminus];
|
|
318
|
+
const labelPosition = this.dimensionsCalculator.getTerminusLabelPosition(strand, end);
|
|
319
|
+
return this.svgElementFactory.createTextElement(labelText, labelPosition, SVG_TEXT_FONT_SIZES.NUCLEOBASE, SVG_ELEMENT_COLORS.MODIFICATION_TEXT);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
createLegendCircle(nucleobaseType: string, index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): SVGCircleElement {
|
|
323
|
+
const radius = SVG_CIRCLE_SIZES.LEGEND_RADIUS;
|
|
324
|
+
const color = getNucleobaseColorFromStyleMap(nucleobaseType);
|
|
325
|
+
|
|
326
|
+
const centerPosition = this.dimensionsCalculator.getLegendCirclePosition(index, distinctNucleobaseTypes, containsPhosphorothioateLinkages);
|
|
327
|
+
return this.svgElementFactory.createCircleElement(centerPosition, radius, color);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
createLegendText(nucleobaseType: string, index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): SVGTextElement {
|
|
331
|
+
const legendPosition = this.dimensionsCalculator.getLegendTextPosition(index, distinctNucleobaseTypes, containsPhosphorothioateLinkages);
|
|
332
|
+
return this.svgElementFactory.createTextElement(nucleobaseType, legendPosition, SVG_TEXT_FONT_SIZES.COMMENT, SVG_ELEMENT_COLORS.TEXT);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
class StrandElementBuilder {
|
|
337
|
+
constructor(
|
|
338
|
+
private svgFactory: SVGElementFactoryWrapper,
|
|
339
|
+
private config: PatternConfiguration
|
|
340
|
+
) { }
|
|
341
|
+
|
|
342
|
+
createStrandElements(countOfNucleotidesExcludingOverhangs: StrandToNumberMap): SVGElement[] {
|
|
343
|
+
const svgElements = [] as SVGElement[];
|
|
344
|
+
|
|
345
|
+
const nucleotideCounter = new NucleotideCountTracker(countOfNucleotidesExcludingOverhangs);
|
|
346
|
+
|
|
347
|
+
STRANDS.forEach((strand) => {
|
|
348
|
+
const criterion = strand === STRAND.SENSE || (strand === STRAND.ANTISENSE && this.config.isAntisenseStrandIncluded);
|
|
349
|
+
if (!criterion)
|
|
350
|
+
return;
|
|
351
|
+
|
|
352
|
+
this.config.nucleotideSequences[strand].forEach((_, index) => {
|
|
353
|
+
const elements = this.createElementsForNucleotide(index, strand, nucleotideCounter, countOfNucleotidesExcludingOverhangs);
|
|
354
|
+
svgElements.push(...elements);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return svgElements;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// todo reduce the # of args
|
|
362
|
+
private createElementsForNucleotide(
|
|
363
|
+
indexOfNucleotide: number,
|
|
364
|
+
strand: STRAND,
|
|
365
|
+
counter: NucleotideCountTracker,
|
|
366
|
+
countOfNucleotidesExcludingOverhangs: StrandToNumberMap
|
|
367
|
+
): SVGElement[] {
|
|
368
|
+
const nucleotide = this.config.nucleotideSequences[strand][indexOfNucleotide];
|
|
369
|
+
const isOverhang = isOverhangNucleotide(nucleotide);
|
|
370
|
+
|
|
371
|
+
counter.decrementIfNotOverhang(isOverhang, strand);
|
|
372
|
+
const displayedNucleotideNumber = strand === STRAND.SENSE ?
|
|
373
|
+
counter.getCurrentCount(strand) + 1 :
|
|
374
|
+
countOfNucleotidesExcludingOverhangs[strand] - counter.getCurrentCount(strand);
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
const nucleotideNumericLabel = this.svgFactory.createNucleotideNumericLabel(indexOfNucleotide, strand, displayedNucleotideNumber);
|
|
378
|
+
const nucleotideCircle = this.svgFactory. createNucleotideCircle(indexOfNucleotide, strand);
|
|
379
|
+
const nucleotideNameLabel = this.svgFactory. createNucleotideNameLabel(indexOfNucleotide, strand);
|
|
380
|
+
|
|
381
|
+
const phosphorothioateLinkageStar = this.svgFactory.createPhosphorothioateLinkageStar(strand, indexOfNucleotide);
|
|
382
|
+
|
|
383
|
+
const lastNucleotideIndex = this.config.nucleotideSequences[strand].length;
|
|
384
|
+
const lastStar = this.svgFactory.createPhosphorothioateLinkageStar(strand, lastNucleotideIndex);
|
|
385
|
+
|
|
386
|
+
const nucleotideSvgElements = [
|
|
387
|
+
nucleotideNumericLabel,
|
|
388
|
+
nucleotideCircle,
|
|
389
|
+
nucleotideNameLabel,
|
|
390
|
+
phosphorothioateLinkageStar,
|
|
391
|
+
lastStar,
|
|
392
|
+
].filter((element) => element !== null) as SVGElement[];
|
|
393
|
+
|
|
394
|
+
return nucleotideSvgElements;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as DG from 'datagrok-api/dg';
|
|
2
|
+
|
|
3
|
+
import {STRANDS, STRAND_ENDS, TERMINI} from '../model/const';
|
|
4
|
+
|
|
5
|
+
export type BooleanInput = DG.InputBase<boolean | null>;
|
|
6
|
+
export type StringInput = DG.InputBase<string | null>;
|
|
7
|
+
export type NumberInput = DG.InputBase<number | null>;
|
|
8
|
+
|
|
9
|
+
export type Position = { x: number, y: number };
|
|
10
|
+
|
|
11
|
+
export type StrandEndToNumberMap = Record<typeof STRAND_ENDS[number], number>;
|
|
12
|
+
export type StrandToNumberMap = Record<typeof STRANDS[number], number>;
|
|
13
|
+
export type StrandEndToSVGElementsMap = Record<typeof STRAND_ENDS[number], SVGElement | null>;
|
|
14
|
+
export type TerminusToSVGElementMap = Record<typeof TERMINI[number], SVGElement | null>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as ui from 'datagrok-api/ui';
|
|
2
|
+
|
|
3
|
+
import {APP_NAME} from '../../common/view/const';
|
|
4
|
+
import {IsolatedAppUIBase} from '../../common/view/isolated-app-ui';
|
|
5
|
+
import {DataManager} from '../model/data-manager';
|
|
6
|
+
import {EventBus} from '../model/event-bus';
|
|
7
|
+
import {URLRouter} from '../model/router';
|
|
8
|
+
import {PatternAppLeftSection} from './components/left-section';
|
|
9
|
+
import {PatternAppRightSection} from './components/right-section';
|
|
10
|
+
import {PatternConfigRecord} from '../model/types';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export class OligoPatternUI extends IsolatedAppUIBase {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(APP_NAME.PATTERN);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
protected getContent(): Promise<HTMLDivElement> {
|
|
19
|
+
return getContent();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async function getContent(): Promise<HTMLDivElement> {
|
|
25
|
+
const dataManager = await DataManager.getInstance();
|
|
26
|
+
const urlRouter = new URLRouter();
|
|
27
|
+
|
|
28
|
+
const initialPatternRecord = await getInitialPatternRecord(dataManager, urlRouter);
|
|
29
|
+
const eventBus = new EventBus(dataManager, initialPatternRecord);
|
|
30
|
+
urlRouter.subscribeToObservables(eventBus);
|
|
31
|
+
|
|
32
|
+
const leftSection = new PatternAppLeftSection(eventBus, dataManager).getLayout();
|
|
33
|
+
const rightSection = new PatternAppRightSection(eventBus, dataManager).getLayout();
|
|
34
|
+
|
|
35
|
+
const isResizeable = true;
|
|
36
|
+
|
|
37
|
+
const layout = ui.splitH([
|
|
38
|
+
leftSection,
|
|
39
|
+
rightSection,
|
|
40
|
+
], {}, isResizeable);
|
|
41
|
+
|
|
42
|
+
return layout;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function getInitialPatternRecord(
|
|
46
|
+
dataManager: DataManager,
|
|
47
|
+
urlRouter: URLRouter
|
|
48
|
+
): Promise<PatternConfigRecord> {
|
|
49
|
+
const patternHash = urlRouter.getPatternHash();
|
|
50
|
+
if (!patternHash) {
|
|
51
|
+
urlRouter.clearPatternURL();
|
|
52
|
+
return dataManager.getDefaultPatternRecord();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let initialPatternRecord = await dataManager.getPatternRecord(patternHash);
|
|
56
|
+
if (!initialPatternRecord) {
|
|
57
|
+
urlRouter.clearPatternURL();
|
|
58
|
+
initialPatternRecord = dataManager.getDefaultPatternRecord();
|
|
59
|
+
}
|
|
60
|
+
return initialPatternRecord;
|
|
61
|
+
}
|
|
@@ -36,7 +36,7 @@ export function linkStrandsV3000(
|
|
|
36
36
|
|
|
37
37
|
let inverted = false;
|
|
38
38
|
const molBlocks = strands.senseStrands.concat(strands.antiStrands);
|
|
39
|
-
/** Minimal value of
|
|
39
|
+
/** Minimal value of ANTISENSE_STRAND and AS2 shift */
|
|
40
40
|
let ssYShift = 0;
|
|
41
41
|
|
|
42
42
|
for (let i = 0; i < molBlocks.length; i++) {
|
|
@@ -47,12 +47,12 @@ export function linkStrandsV3000(
|
|
|
47
47
|
|
|
48
48
|
if (i >= strands.senseStrands.length) {
|
|
49
49
|
if (inverted === false) {
|
|
50
|
-
//
|
|
50
|
+
// ANTISENSE_STRAND strand
|
|
51
51
|
inverted = true;
|
|
52
52
|
xShift = 0;
|
|
53
53
|
}
|
|
54
54
|
} else {
|
|
55
|
-
//
|
|
55
|
+
// SENSE_STRAND strands
|
|
56
56
|
ssYShift = Math.min(ssYShift, Math.min(
|
|
57
57
|
...coordinates.y.filter((item) => item < 0)
|
|
58
58
|
));
|
|
@@ -4,15 +4,15 @@ import * as ui from 'datagrok-api/ui';
|
|
|
4
4
|
import * as DG from 'datagrok-api/dg';
|
|
5
5
|
|
|
6
6
|
import {PHOSPHATE_SYMBOL} from './const';
|
|
7
|
-
import {sortByReverseLength} from '
|
|
8
|
-
import {MonomerLibWrapper} from '
|
|
9
|
-
import {
|
|
7
|
+
import {sortByReverseLength} from '../../common/model/helpers';
|
|
8
|
+
import {MonomerLibWrapper} from '../../common/model/monomer-lib/lib-wrapper';
|
|
9
|
+
import {MONOMERS_WITH_PHOSPHATE} from '../../common/model/data-loader/json-loader';
|
|
10
10
|
|
|
11
11
|
/** Wrapper for parsing a strand and getting a sequence of monomer IDs (with
|
|
12
12
|
* omitted linkers, if needed) */
|
|
13
13
|
export class MonomerSequenceParser {
|
|
14
14
|
constructor(
|
|
15
|
-
private sequence: string,
|
|
15
|
+
private sequence: string,
|
|
16
16
|
// todo: remove from the list of parameters
|
|
17
17
|
private codeMap: Map<string, string>
|
|
18
18
|
) {
|
|
@@ -39,9 +39,8 @@ export class MonomerSequenceParser {
|
|
|
39
39
|
const nextMonomerIsPhosphate = (i + 1 < parsedRawCodes.length && monomerIsPhosphateLinker(this.getSymbolForCode(parsedRawCodes[i + 1])));
|
|
40
40
|
|
|
41
41
|
// todo: refactor as molfile-specific
|
|
42
|
-
if (!isPhosphate && !monomerHasRightPhosphateLinker(monomerSymbol) && !nextMonomerIsPhosphate && !lastMonomer)
|
|
42
|
+
if (!isPhosphate && !monomerHasRightPhosphateLinker(monomerSymbol) && !nextMonomerIsPhosphate && !lastMonomer)
|
|
43
43
|
monomerSymbolSequence.push(PHOSPHATE_SYMBOL);
|
|
44
|
-
}
|
|
45
44
|
});
|
|
46
45
|
return monomerSymbolSequence;
|
|
47
46
|
}
|
|
@@ -70,20 +69,20 @@ export class MonomerSequenceParser {
|
|
|
70
69
|
|
|
71
70
|
// todo: port to monomer handler
|
|
72
71
|
private getAllCodesOfFormat(): string[] {
|
|
73
|
-
|
|
72
|
+
const allCodesInTheFormat = Array.from(this.codeMap.keys());
|
|
74
73
|
return sortByReverseLength(allCodesInTheFormat);
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
// todo: to be eliminated after full helm support
|
|
79
78
|
function monomerHasLeftPhosphateLinker(monomerSymbol: string): boolean {
|
|
80
|
-
return
|
|
79
|
+
return MONOMERS_WITH_PHOSPHATE['left'].includes(monomerSymbol);
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
function monomerHasRightPhosphateLinker(monomerSymbol: string): boolean {
|
|
84
|
-
return
|
|
83
|
+
return MONOMERS_WITH_PHOSPHATE['right'].includes(monomerSymbol);
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
function monomerIsPhosphateLinker(monomerSymbol: string): boolean {
|
|
88
|
-
return
|
|
87
|
+
return MONOMERS_WITH_PHOSPHATE['phosphate'].includes(monomerSymbol);
|
|
89
88
|
}
|
|
@@ -4,11 +4,11 @@ import * as DG from 'datagrok-api/dg';
|
|
|
4
4
|
|
|
5
5
|
import {errorToConsole} from '@datagrok-libraries/utils/src/to-console';
|
|
6
6
|
|
|
7
|
-
import {download} from '
|
|
7
|
+
import {download} from '../../common/model/helpers';
|
|
8
8
|
import {SequenceToMolfileConverter} from './sequence-to-molfile';
|
|
9
9
|
import {linkStrandsV3000} from './mol-transformations';
|
|
10
|
-
import {DEFAULT_FORMATS} from '
|
|
11
|
-
import {FormatDetector} from '
|
|
10
|
+
import {DEFAULT_FORMATS} from '../../common/model/const';
|
|
11
|
+
import {FormatDetector} from '../../common/model/parsing-validation/format-detector';
|
|
12
12
|
|
|
13
13
|
export type StrandData = {
|
|
14
14
|
strand: string,
|
|
@@ -62,7 +62,7 @@ export function saveSdf(
|
|
|
62
62
|
nonEmptyStrands.length === 0 ||
|
|
63
63
|
nonEmptyStrands.length === 1 && ss.strand === ''
|
|
64
64
|
) {
|
|
65
|
-
grok.shell.warning('Enter
|
|
65
|
+
grok.shell.warning('Enter SENSE_STRAND and optionally ANTISENSE_STRAND/AS2 to save SDF');
|
|
66
66
|
} else {
|
|
67
67
|
let result: string;
|
|
68
68
|
if (oneEntity) {
|
|
@@ -4,7 +4,7 @@ import * as ui from 'datagrok-api/ui';
|
|
|
4
4
|
import * as DG from 'datagrok-api/dg';
|
|
5
5
|
|
|
6
6
|
import {MonomerSequenceParser} from './monomer-code-parser';
|
|
7
|
-
import {MonomerLibWrapper} from '
|
|
7
|
+
import {MonomerLibWrapper} from '../../common/model/monomer-lib/lib-wrapper';
|
|
8
8
|
|
|
9
9
|
export class SequenceToMolfileConverter {
|
|
10
10
|
constructor(
|
|
@@ -24,7 +24,7 @@ export class SequenceToMolfileConverter {
|
|
|
24
24
|
parsedSequence.forEach((monomerSymbol, idx) => {
|
|
25
25
|
const monomerMolfile = this.getMonomerMolfile(monomerSymbol, idx);
|
|
26
26
|
monomerMolfiles.push(monomerMolfile);
|
|
27
|
-
})
|
|
27
|
+
});
|
|
28
28
|
let molfile = this.getPolymerMolfile(monomerMolfiles);
|
|
29
29
|
if (this.invert) {
|
|
30
30
|
molfile = this.reflect(molfile);
|