@datagrok/sequence-translator 1.2.9 → 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/CHANGELOG.md +7 -0
- package/dist/package-test.js +1 -1
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/package.json +4 -3
- package/src/apps/pattern/model/event-bus.ts +23 -6
- package/src/apps/pattern/model/translator.ts +27 -1
- package/src/apps/pattern/view/components/bulk-convert/column-input.ts +13 -3
- package/src/apps/pattern/view/components/bulk-convert/table-controls.ts +4 -3
- package/src/apps/pattern/view/components/load-block-controls.ts +4 -2
- package/src/apps/pattern/view/svg-utils/const.ts +15 -1
- 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 +4 -5
- package/src/apps/pattern/view/svg-utils/svg-element-factory.ts +16 -4
- package/src/apps/pattern/view/svg-utils/svg-renderer.ts +32 -377
- 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/translator/view/ui.ts +1 -1
- package/src/package.ts +22 -6
- package/src/polytool/const.ts +3 -15
- 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 +7 -0
- package/src/tests/helm-to-nucleotides.ts +0 -5
- package/tsconfig.json +1 -1
- package/src/apps/pattern/view/svg-utils/dimensions-calculator.ts +0 -498
- package/src/polytool/transformation.ts +0 -326
- package/src/polytool/ui.ts +0 -59
|
@@ -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
|
+
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/* Do not change these import lines to match external modules in webpack configuration */
|
|
2
|
-
import * as grok from 'datagrok-api/grok';
|
|
3
2
|
import * as ui from 'datagrok-api/ui';
|
|
4
|
-
import * as DG from 'datagrok-api/dg';
|
|
5
3
|
|
|
6
|
-
import {PatternConfiguration, StrandType} from '../../model/types';
|
|
7
4
|
import {EventBus} from '../../model/event-bus';
|
|
8
|
-
import {
|
|
5
|
+
import {PatternConfiguration} from '../../model/types';
|
|
9
6
|
//@ts-ignore
|
|
10
7
|
import * as svgExport from 'save-svg-as-png';
|
|
8
|
+
import {NucleotidePatternSVGRenderer} from './svg-renderer';
|
|
11
9
|
|
|
12
10
|
export class SvgDisplayManager {
|
|
13
11
|
private svgDisplayDiv = ui.div([]);
|
|
@@ -16,7 +14,7 @@ export class SvgDisplayManager {
|
|
|
16
14
|
private constructor(
|
|
17
15
|
private eventBus: EventBus
|
|
18
16
|
) {
|
|
19
|
-
eventBus.
|
|
17
|
+
eventBus.updateSvgContainer$.subscribe(() => this.updateSvgContainer());
|
|
20
18
|
eventBus.svgSaveRequested$.subscribe(() => this.saveSvgAsPng());
|
|
21
19
|
}
|
|
22
20
|
|
|
@@ -33,6 +31,7 @@ export class SvgDisplayManager {
|
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
private createSvg(patternConfig: PatternConfiguration) {
|
|
34
|
+
// const renderer = new NucleotidePatternSVGRenderer(patternConfig);
|
|
36
35
|
const renderer = new NucleotidePatternSVGRenderer(patternConfig);
|
|
37
36
|
const svg = renderer.renderPattern();
|
|
38
37
|
return svg;
|
|
@@ -13,7 +13,7 @@ export class SVGElementFactory {
|
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
createCanvas(width: number, height: number): SVGElement {
|
|
17
17
|
const svgElement = this.createElement('svg') as SVGElement;
|
|
18
18
|
this.setAttributes(svgElement, {
|
|
19
19
|
id: 'mySvg',
|
|
@@ -23,7 +23,7 @@ export class SVGElementFactory {
|
|
|
23
23
|
return svgElement;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
createCircleElement(centerPosition: Position, radius: number, color: string): SVGCircleElement {
|
|
27
27
|
const circle = this.createElement('circle') as SVGCircleElement;
|
|
28
28
|
this.setAttributes(circle, {
|
|
29
29
|
cx: centerPosition.x,
|
|
@@ -34,7 +34,7 @@ export class SVGElementFactory {
|
|
|
34
34
|
return circle;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
createTextElement(textContent: string, position: Position, fontSize: number, color: string): SVGTextElement {
|
|
38
38
|
const textElement = this.createElement('text') as SVGTextElement;
|
|
39
39
|
this.setAttributes(textElement, {
|
|
40
40
|
'x': position.x,
|
|
@@ -48,7 +48,7 @@ export class SVGElementFactory {
|
|
|
48
48
|
return textElement;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
createStarElement(centerPosition: Position, color: string): SVGPolygonElement {
|
|
52
52
|
const star = this.createElement('polygon') as SVGPolygonElement;
|
|
53
53
|
const points = this.computeStarVertexCoordinates(centerPosition);
|
|
54
54
|
const pointsAttribute = points.map((point) => point.join(',')).join(' ');
|
|
@@ -79,4 +79,16 @@ export class SVGElementFactory {
|
|
|
79
79
|
|
|
80
80
|
return points;
|
|
81
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
|
+
}
|
|
82
94
|
}
|
|
@@ -1,396 +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';
|
|
1
5
|
import {SVGElementFactory} from './svg-element-factory';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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';
|
|
6
|
+
import {TitleBlock} from './title-block';
|
|
7
|
+
import {LegendBlock} from './legend-block';
|
|
8
|
+
import {LEGEND_PADDING, TITLE_SHIFT} from './const';
|
|
13
9
|
|
|
14
10
|
export class NucleotidePatternSVGRenderer {
|
|
15
|
-
private
|
|
16
|
-
private
|
|
17
|
-
private
|
|
18
|
-
private
|
|
19
|
-
private legendBuilder: LegendBuilder;
|
|
11
|
+
private title: TitleBlock;
|
|
12
|
+
private strands: StrandsBlock;
|
|
13
|
+
private legend: LegendBlock;
|
|
14
|
+
private svgElementFactory = new SVGElementFactory();
|
|
20
15
|
|
|
21
16
|
constructor(patternConfig: PatternConfiguration) {
|
|
22
|
-
|
|
17
|
+
const config = _.cloneDeep(patternConfig);
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
this.patternDimensionsCalculator = new PatternSVGDimensionsCalculator(this.config);
|
|
19
|
+
let heightShift = TITLE_SHIFT;
|
|
26
20
|
|
|
27
|
-
this.
|
|
21
|
+
this.title = new TitleBlock(this.svgElementFactory, config, heightShift);
|
|
22
|
+
heightShift += this.title.getContentHeight();
|
|
28
23
|
|
|
29
|
-
this.
|
|
30
|
-
|
|
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
|
-
}
|
|
24
|
+
this.strands = new StrandsBlock(this.svgElementFactory, config, heightShift);
|
|
25
|
+
heightShift += this.strands.getContentHeight() + LEGEND_PADDING;
|
|
185
26
|
|
|
186
|
-
|
|
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
|
-
);
|
|
27
|
+
this.legend = new LegendBlock(this.svgElementFactory, config, heightShift);
|
|
198
28
|
}
|
|
199
29
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
}
|
|
30
|
+
renderPattern(): SVGElement {
|
|
31
|
+
const width = this.getGlobalWidth();
|
|
32
|
+
const height = this.getGlobalHeight();
|
|
33
|
+
const canvas = this.svgElementFactory.createCanvas(width, height);
|
|
321
34
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const color = getNucleobaseColorFromStyleMap(nucleobaseType);
|
|
35
|
+
const elements = [this.title, this.strands, this.legend].map((block) => block.svgElements).flat();
|
|
36
|
+
canvas.append(...elements);
|
|
325
37
|
|
|
326
|
-
|
|
327
|
-
return this.svgElementFactory.createCircleElement(centerPosition, radius, color);
|
|
38
|
+
return canvas;
|
|
328
39
|
}
|
|
329
40
|
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
return
|
|
41
|
+
private getGlobalWidth(): number {
|
|
42
|
+
const blocks = [this.title, this.strands, this.legend] as SVGBlockBase[];
|
|
43
|
+
return Math.max(...blocks.map((block) => block.getContentWidth()));
|
|
333
44
|
}
|
|
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
45
|
|
|
394
|
-
|
|
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;
|
|
395
50
|
}
|
|
396
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
|
+
|
|
@@ -165,7 +165,7 @@ class TranslatorAppLayout {
|
|
|
165
165
|
translatedColumn.semType = DG.SEMTYPE.MACROMOLECULE;
|
|
166
166
|
const units = outputFormat == NUCLEOTIDES_FORMAT ? NOTATION.FASTA : NOTATION.HELM;
|
|
167
167
|
translatedColumn.setTag(DG.TAGS.UNITS, units);
|
|
168
|
-
const seqHandler = SeqHandler.forColumn(translatedColumn);
|
|
168
|
+
const seqHandler = SeqHandler.forColumn(translatedColumn as DG.Column<string>);
|
|
169
169
|
const setUnits = outputFormat == NUCLEOTIDES_FORMAT ? SeqHandler.setUnitsToFastaColumn :
|
|
170
170
|
SeqHandler.setUnitsToHelmColumn;
|
|
171
171
|
setUnits(seqHandler);
|