@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/package-test.js +1 -1
  3. package/dist/package-test.js.map +1 -1
  4. package/dist/package.js +1 -1
  5. package/dist/package.js.map +1 -1
  6. package/package.json +4 -3
  7. package/src/apps/pattern/model/event-bus.ts +23 -6
  8. package/src/apps/pattern/model/translator.ts +27 -1
  9. package/src/apps/pattern/view/components/bulk-convert/column-input.ts +13 -3
  10. package/src/apps/pattern/view/components/bulk-convert/table-controls.ts +4 -3
  11. package/src/apps/pattern/view/components/load-block-controls.ts +4 -2
  12. package/src/apps/pattern/view/svg-utils/const.ts +15 -1
  13. package/src/apps/pattern/view/svg-utils/legend-block.ts +92 -0
  14. package/src/apps/pattern/view/svg-utils/strands-block.ts +335 -0
  15. package/src/apps/pattern/view/svg-utils/svg-block-base.ts +37 -0
  16. package/src/apps/pattern/view/svg-utils/svg-display-manager.ts +4 -5
  17. package/src/apps/pattern/view/svg-utils/svg-element-factory.ts +16 -4
  18. package/src/apps/pattern/view/svg-utils/svg-renderer.ts +32 -377
  19. package/src/apps/pattern/view/svg-utils/text-dimensions-calculator.ts +29 -0
  20. package/src/apps/pattern/view/svg-utils/title-block.ts +53 -0
  21. package/src/apps/translator/view/ui.ts +1 -1
  22. package/src/package.ts +22 -6
  23. package/src/polytool/const.ts +3 -15
  24. package/src/polytool/pt-conversion.ts +307 -0
  25. package/src/polytool/pt-dialog.ts +115 -0
  26. package/src/polytool/pt-enumeration.ts +127 -0
  27. package/src/polytool/pt-rules.ts +73 -0
  28. package/src/polytool/utils.ts +7 -0
  29. package/src/tests/helm-to-nucleotides.ts +0 -5
  30. package/tsconfig.json +1 -1
  31. package/src/apps/pattern/view/svg-utils/dimensions-calculator.ts +0 -498
  32. package/src/polytool/transformation.ts +0 -326
  33. 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 {NucleotidePatternSVGRenderer} from './svg-renderer';
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.patternStateChanged$.subscribe(() => this.updateSvgContainer());
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
- public createCanvas(width: number, height: number): SVGElement {
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
- public createCircleElement(centerPosition: Position, radius: number, color: string): SVGCircleElement {
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
- public createTextElement(textContent: string, position: Position, fontSize: number, color: string): SVGTextElement {
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
- public createStarElement(centerPosition: Position, color: string): SVGPolygonElement {
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 {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';
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 config: PatternConfiguration;
16
- private patternDimensionsCalculator: PatternSVGDimensionsCalculator;
17
- private svgFactory: SVGElementFactoryWrapper;
18
- private strandElementManager: StrandElementBuilder;
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
- this.setupPatternConfig(patternConfig);
17
+ const config = _.cloneDeep(patternConfig);
23
18
 
24
- // todo: prefer dependency injection for all these properties
25
- this.patternDimensionsCalculator = new PatternSVGDimensionsCalculator(this.config);
19
+ let heightShift = TITLE_SHIFT;
26
20
 
27
- this.svgFactory = new SVGElementFactoryWrapper(new SVGElementFactory(), this.config, this.patternDimensionsCalculator);
21
+ this.title = new TitleBlock(this.svgElementFactory, config, heightShift);
22
+ heightShift += this.title.getContentHeight();
28
23
 
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
- }
24
+ this.strands = new StrandsBlock(this.svgElementFactory, config, heightShift);
25
+ heightShift += this.strands.getContentHeight() + LEGEND_PADDING;
185
26
 
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
- );
27
+ this.legend = new LegendBlock(this.svgElementFactory, config, heightShift);
198
28
  }
199
29
 
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
- }
30
+ renderPattern(): SVGElement {
31
+ const width = this.getGlobalWidth();
32
+ const height = this.getGlobalHeight();
33
+ const canvas = this.svgElementFactory.createCanvas(width, height);
321
34
 
322
- createLegendCircle(nucleobaseType: string, index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): SVGCircleElement {
323
- const radius = SVG_CIRCLE_SIZES.LEGEND_RADIUS;
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
- const centerPosition = this.dimensionsCalculator.getLegendCirclePosition(index, distinctNucleobaseTypes, containsPhosphorothioateLinkages);
327
- return this.svgElementFactory.createCircleElement(centerPosition, radius, color);
38
+ return canvas;
328
39
  }
329
40
 
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);
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
- return nucleotideSvgElements;
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);