@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,73 @@
|
|
|
1
|
+
import * as DG from 'datagrok-api/dg';
|
|
2
|
+
import * as grok from 'datagrok-api/grok';
|
|
3
|
+
import {ActiveFiles} from '@datagrok-libraries/utils/src/settings/active-files-base';
|
|
4
|
+
|
|
5
|
+
export const RULES_PATH = 'System:AppData/Bio/polytool-rules/';
|
|
6
|
+
export const RULES_STORAGE_NAME = 'Polytool';
|
|
7
|
+
export const RULES_TYPE_LINK = 'link';
|
|
8
|
+
export const RULES_TYPE_HOMODIMER = 'fragmentDuplication';
|
|
9
|
+
export const RULES_TYPE_HETERODIMER = 'differentFragments';
|
|
10
|
+
|
|
11
|
+
export class RuleInputs extends ActiveFiles {
|
|
12
|
+
constructor(path: string, userStorageName: string, ext: string ) {
|
|
13
|
+
super(path, userStorageName, ext);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type Rules = {
|
|
18
|
+
homodimerCode: string | null,
|
|
19
|
+
heterodimerCode: string | null,
|
|
20
|
+
linkRules: RuleLink[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type RuleLink = {
|
|
24
|
+
code: number,
|
|
25
|
+
firstMonomer: string,
|
|
26
|
+
secondMonomer: string,
|
|
27
|
+
firstSubstitution: string,
|
|
28
|
+
secondSubstitution: string,
|
|
29
|
+
firstLinkingGroup: number,
|
|
30
|
+
secondLinkingGroup: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function getRules(ruleFiles: string[]): Promise<Rules> {
|
|
34
|
+
const fileSource = new DG.FileSource(RULES_PATH);
|
|
35
|
+
const linkRules: RuleLink[] = [];
|
|
36
|
+
const rules: Rules = {homodimerCode: null, heterodimerCode: null, linkRules: linkRules};
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < ruleFiles.length; i++) {
|
|
39
|
+
const rulesRaw = await fileSource.readAsText(ruleFiles[i].replace(RULES_PATH, ''));
|
|
40
|
+
const ruleSingle = JSON.parse(rulesRaw);
|
|
41
|
+
for (let j = 0; j < ruleSingle.length; j++) {
|
|
42
|
+
if (ruleSingle[j].type !== undefined && ruleSingle[j].code !== undefined) {
|
|
43
|
+
switch (ruleSingle[j].type) {
|
|
44
|
+
case RULES_TYPE_LINK: {
|
|
45
|
+
const rule = ruleSingle[j].monomericSubstitution;
|
|
46
|
+
rule['code'] = ruleSingle[j].code;
|
|
47
|
+
linkRules.push(rule);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case RULES_TYPE_HOMODIMER: {
|
|
51
|
+
if (rules.homodimerCode)
|
|
52
|
+
grok.shell.warning(`PolyTool: homodimer code is duplicated in rules.`);
|
|
53
|
+
rules.homodimerCode = ruleSingle[j].code;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case RULES_TYPE_HETERODIMER: {
|
|
57
|
+
if (rules.heterodimerCode)
|
|
58
|
+
grok.shell.warning(`PolyTool: heterodimer code is duplicated in rules.`);
|
|
59
|
+
rules.heterodimerCode = ruleSingle[j].code;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
default:
|
|
63
|
+
grok.shell.warning(`PolyTool: Unexpected type - '${ruleSingle[j]}'.`);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
grok.shell.warning('Polytool: rules contain invalid rule');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return rules;
|
|
73
|
+
}
|
package/src/polytool/utils.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as grok from 'datagrok-api/grok';
|
|
3
3
|
import * as ui from 'datagrok-api/ui';
|
|
4
4
|
import * as DG from 'datagrok-api/dg';
|
|
5
|
+
import {_package} from '../package';
|
|
5
6
|
|
|
6
7
|
import {ALPHABET, ALIGNMENT, NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
7
8
|
|
|
@@ -18,3 +19,9 @@ function addCommonTags(col: DG.Column<any>) {
|
|
|
18
19
|
col.setTag('alphabet', ALPHABET.PT);
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
export function handleError(err: any): void {
|
|
23
|
+
const errMsg: string = err instanceof Error ? err.message : err.toString();
|
|
24
|
+
const stack: string | undefined = err instanceof Error ? err.stack : undefined;
|
|
25
|
+
grok.shell.error(errMsg);
|
|
26
|
+
_package.logger.error(err.message, undefined, stack);
|
|
27
|
+
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/* Do not change these import lines to match external modules in webpack configuration */
|
|
2
|
-
import * as grok from 'datagrok-api/grok';
|
|
3
|
-
import * as ui from 'datagrok-api/ui';
|
|
4
|
-
import * as DG from 'datagrok-api/dg';
|
|
5
|
-
|
|
6
1
|
import {before, category, expect, test} from '@datagrok-libraries/utils/src/test';
|
|
7
2
|
import {getNucleotidesSequence} from '../apps/translator/model/conversion-utils';
|
|
8
3
|
import {loadJsonData} from '../apps/common/model/data-loader/json-loader';
|
package/tsconfig.json
CHANGED
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
|
66
66
|
|
|
67
67
|
/* Advanced Options */
|
|
68
|
-
"skipLibCheck":
|
|
68
|
+
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
|
69
69
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -1,498 +0,0 @@
|
|
|
1
|
-
import {NUCLEOTIDES} from '../../../common/model/const';
|
|
2
|
-
import {STRAND, STRANDS, STRAND_END, STRAND_TO_END_TERMINUS_MAP, STRAND_ENDS} from '../../model/const';
|
|
3
|
-
import {PatternConfiguration} from '../../model/types';
|
|
4
|
-
import {SVG_CIRCLE_SIZES, SVG_TEXT_FONT_SIZES, NUMERIC_LABEL_POSITION_OFFSET, DEFAULT_FONT_FAMILY, Y_POSITIONS_FOR_STRAND_ELEMENTS, STRAND_END_LABEL_TEXT} from './const';
|
|
5
|
-
import {Position, StrandToNumberMap, StrandEndToNumberMap} from '../types';
|
|
6
|
-
import {isOverhangNucleotide} from '../../model/utils';
|
|
7
|
-
|
|
8
|
-
export class PatternSVGDimensionsCalculator {
|
|
9
|
-
private canvasDimensions: CanvasDimensionCalculator;
|
|
10
|
-
private nucleotidePositionCalculator: NucleotidePositionCalculator;
|
|
11
|
-
private legendPositionCalculator: LegendPositionCalculator;
|
|
12
|
-
private labelPositionCalculator: LabelPositionCalculator;
|
|
13
|
-
private linkageStarPositionCalculator: LinkageStarPositionCalculator;
|
|
14
|
-
|
|
15
|
-
constructor(
|
|
16
|
-
private config: PatternConfiguration
|
|
17
|
-
) {
|
|
18
|
-
const rightOverhangNucleotideCounts = this.computeRightOverhangNucleotideCounts();
|
|
19
|
-
const maxEffectiveStrandLength = this.computeMaxEffectiveStrandLength(rightOverhangNucleotideCounts);
|
|
20
|
-
const maxWidthOfRightOverhangs = this.computeMaxWidthOfRightOverhangs(rightOverhangNucleotideCounts);
|
|
21
|
-
const strandLabelWidth = this.computeStrandLabelWidth();
|
|
22
|
-
const maxTerminusLabelWidthByEnd = this.computeMaxWidthOfTerminusLabels();
|
|
23
|
-
|
|
24
|
-
this.initializeCalculators(
|
|
25
|
-
rightOverhangNucleotideCounts,
|
|
26
|
-
maxEffectiveStrandLength,
|
|
27
|
-
maxWidthOfRightOverhangs,
|
|
28
|
-
strandLabelWidth,
|
|
29
|
-
maxTerminusLabelWidthByEnd
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private initializeCalculators(
|
|
34
|
-
rightOverhangNucleotideCounts: StrandToNumberMap,
|
|
35
|
-
maxEffectiveStrandLength: number,
|
|
36
|
-
maxWidthOfRightOverhangs: number,
|
|
37
|
-
strandLabelWidth: StrandEndToNumberMap,
|
|
38
|
-
maxTerminusLabelWidthByEnd: StrandEndToNumberMap
|
|
39
|
-
): void {
|
|
40
|
-
this.canvasDimensions = new CanvasDimensionCalculator(
|
|
41
|
-
this.config,
|
|
42
|
-
maxEffectiveStrandLength,
|
|
43
|
-
maxWidthOfRightOverhangs,
|
|
44
|
-
strandLabelWidth,
|
|
45
|
-
maxTerminusLabelWidthByEnd
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
this.nucleotidePositionCalculator = new NucleotidePositionCalculator(
|
|
49
|
-
this.config,
|
|
50
|
-
maxEffectiveStrandLength,
|
|
51
|
-
rightOverhangNucleotideCounts,
|
|
52
|
-
maxTerminusLabelWidthByEnd
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
this.legendPositionCalculator = new LegendPositionCalculator(
|
|
56
|
-
this.config,
|
|
57
|
-
this.canvasDimensions
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
this.labelPositionCalculator = new LabelPositionCalculator(
|
|
61
|
-
this.config,
|
|
62
|
-
maxWidthOfRightOverhangs,
|
|
63
|
-
maxTerminusLabelWidthByEnd,
|
|
64
|
-
strandLabelWidth,
|
|
65
|
-
rightOverhangNucleotideCounts,
|
|
66
|
-
this.nucleotidePositionCalculator
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
this.linkageStarPositionCalculator = new LinkageStarPositionCalculator(
|
|
70
|
-
this.nucleotidePositionCalculator,
|
|
71
|
-
rightOverhangNucleotideCounts
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
private computeRightOverhangNucleotideCounts(): StrandToNumberMap {
|
|
76
|
-
return STRANDS.reduce((overhangCounts, strand) => {
|
|
77
|
-
overhangCounts[strand] = this.countOverhangNucleotidesAtStartOfStrand(strand);
|
|
78
|
-
return overhangCounts;
|
|
79
|
-
}, {} as StrandToNumberMap);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private computeMaxEffectiveStrandLength(rightOverhangNucleotideCounts: StrandToNumberMap): number {
|
|
83
|
-
return Math.max(...STRANDS.map((strand) => this.config.nucleotideSequences[strand].length - rightOverhangNucleotideCounts[strand]));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private computeMaxWidthOfRightOverhangs(rightOverhangNucleotideCounts: StrandToNumberMap): number {
|
|
87
|
-
return Math.max(rightOverhangNucleotideCounts[STRAND.SENSE], rightOverhangNucleotideCounts[STRAND.ANTISENSE]);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
getCanvasWidth(): number {
|
|
91
|
-
return this.canvasDimensions.getCanvasWidth();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
getCanvasHeight(): number {
|
|
95
|
-
return this.canvasDimensions.getCanvasHeight();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
getNucleotideCirclePosition(indexOfNucleotide: number, strand: STRAND): Position {
|
|
99
|
-
return this.nucleotidePositionCalculator.getNucleotideCirclePosition(indexOfNucleotide, strand);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
getNucleotideLabelTextPosition(indexOfNucleotide: number, strand: STRAND): Position {
|
|
103
|
-
return this.nucleotidePositionCalculator.getNucleotideLabelTextPosition(indexOfNucleotide, strand);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
getNumericLabelPosition(
|
|
107
|
-
indexOfNucleotide: number,
|
|
108
|
-
strand: STRAND,
|
|
109
|
-
displayedNucleotideNumber: number
|
|
110
|
-
): Position {
|
|
111
|
-
return this.nucleotidePositionCalculator.getNumericLabelPosition(indexOfNucleotide, strand, displayedNucleotideNumber);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
getLegendCirclePosition(index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): Position {
|
|
116
|
-
return this.legendPositionCalculator.getLegendCirclePosition(index, distinctNucleobaseTypes, containsPhosphorothioateLinkages);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
getLegendTextPosition(index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): Position {
|
|
120
|
-
return this.legendPositionCalculator.getLegendTextPosition(index, distinctNucleobaseTypes, containsPhosphorothioateLinkages);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
getStarLabelPosition(): Position {
|
|
124
|
-
return this.legendPositionCalculator.getStarLabelPosition();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
getPhosphorothioateLinkageLabelPosition(): Position {
|
|
128
|
-
return this.legendPositionCalculator.getPhosphorothioateLinkageLabelPosition();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
getTerminusLabelPosition(strand: STRAND, end: STRAND_END): Position {
|
|
132
|
-
return this.labelPositionCalculator.getTerminusLabelPosition(strand, end);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
getStrandEndLabelPosition(strand: STRAND, end: STRAND_END): Position {
|
|
136
|
-
return this.labelPositionCalculator.getStrandEndLabelPosition(strand, end);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
getTitleTextPosition(): Position {
|
|
140
|
-
return this.labelPositionCalculator.getTitleTextPosition();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
getCommentLabelPosition(): Position {
|
|
144
|
-
return this.labelPositionCalculator.getCommentLabelPosition();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
getCenterPositionOfLinkageStar(index: number, strand: STRAND): Position {
|
|
148
|
-
return this.linkageStarPositionCalculator.getCenterPositionOfLinkageStar(index, strand);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
private computeStrandLabelWidth(): StrandEndToNumberMap {
|
|
152
|
-
const widthOfStrandLabel = Object.fromEntries(
|
|
153
|
-
STRAND_ENDS.map(
|
|
154
|
-
(strandEnd) => [strandEnd, this.getMaxWidthStrandEndLabelsByEnd(strandEnd)]
|
|
155
|
-
)
|
|
156
|
-
) as StrandEndToNumberMap;
|
|
157
|
-
|
|
158
|
-
return widthOfStrandLabel;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private computeMaxWidthOfTerminusLabels(): StrandEndToNumberMap {
|
|
162
|
-
const maxWidthOfTerminusLabelsByEnd = STRAND_ENDS.reduce((maxWidthMap, end) => {
|
|
163
|
-
maxWidthMap[end] = this.getMaxWidthOfTerminusLabelsByEnd(end);
|
|
164
|
-
return maxWidthMap;
|
|
165
|
-
}, {} as StrandEndToNumberMap);
|
|
166
|
-
|
|
167
|
-
return maxWidthOfTerminusLabelsByEnd;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private getMaxWidthOfTerminusLabelsByEnd(end: STRAND_END): number {
|
|
171
|
-
return this.calculateMaxWidthOfStrandEndLabel(
|
|
172
|
-
(strand) => this.config.strandTerminusModifications[strand][STRAND_TO_END_TERMINUS_MAP[strand][end]]
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
private getMaxWidthStrandEndLabelsByEnd(strandEnd: STRAND_END): number {
|
|
177
|
-
return this.calculateMaxWidthOfStrandEndLabel(
|
|
178
|
-
(strand) => STRAND_END_LABEL_TEXT[strandEnd][strand]
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
private calculateMaxWidthOfStrandEndLabel(getLabelText: (strand: STRAND) => string): number {
|
|
183
|
-
const textWidthCalculator = TextWidthCalculator.getInstance();
|
|
184
|
-
return Math.max(
|
|
185
|
-
...STRANDS.map((strand) =>
|
|
186
|
-
textWidthCalculator.computeTextWidth(
|
|
187
|
-
getLabelText(strand),
|
|
188
|
-
SVG_TEXT_FONT_SIZES.NUCLEOBASE,
|
|
189
|
-
DEFAULT_FONT_FAMILY
|
|
190
|
-
)
|
|
191
|
-
)
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
private countOverhangNucleotidesAtStartOfStrand(strand: STRAND): number {
|
|
196
|
-
const nucleotides = this.config.nucleotideSequences[strand];
|
|
197
|
-
|
|
198
|
-
let overhangNucleotidesCount = 0;
|
|
199
|
-
for (const nucleotide of nucleotides) {
|
|
200
|
-
if (!isOverhangNucleotide(nucleotide))
|
|
201
|
-
break;
|
|
202
|
-
overhangNucleotidesCount++;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return overhangNucleotidesCount;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
class TextWidthCalculator {
|
|
210
|
-
private static instance: TextWidthCalculator;
|
|
211
|
-
private canvas: HTMLCanvasElement;
|
|
212
|
-
private context: CanvasRenderingContext2D | null;
|
|
213
|
-
private pixelRatio: number;
|
|
214
|
-
|
|
215
|
-
// WARNING: singleton used to avoid creating canvas element on every call
|
|
216
|
-
private constructor() {
|
|
217
|
-
this.canvas = document.createElement('canvas');
|
|
218
|
-
this.context = this.canvas.getContext('2d');
|
|
219
|
-
this.pixelRatio = window.devicePixelRatio || 1;
|
|
220
|
-
|
|
221
|
-
this.canvas.width *= this.pixelRatio;
|
|
222
|
-
this.canvas.height *= this.pixelRatio;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
public static getInstance(): TextWidthCalculator {
|
|
226
|
-
if (!TextWidthCalculator.instance)
|
|
227
|
-
TextWidthCalculator.instance = new TextWidthCalculator();
|
|
228
|
-
|
|
229
|
-
return TextWidthCalculator.instance;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
public computeTextWidth(text: string, fontSize: number, fontFamily: string): number {
|
|
233
|
-
if (this.context) {
|
|
234
|
-
this.context.font = `${fontSize * this.pixelRatio}px ${fontFamily}`;
|
|
235
|
-
const metrics = this.context.measureText(text);
|
|
236
|
-
return metrics.width / this.pixelRatio;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return 0;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
class CanvasDimensionCalculator {
|
|
244
|
-
constructor(
|
|
245
|
-
private config: PatternConfiguration,
|
|
246
|
-
private maxEffectiveStrandLength: number,
|
|
247
|
-
private maxWidthOfRightOverhangs: number,
|
|
248
|
-
private strandLabelWidth: StrandEndToNumberMap,
|
|
249
|
-
private maxTerminusWidthByEnd: StrandEndToNumberMap
|
|
250
|
-
) {}
|
|
251
|
-
|
|
252
|
-
getCanvasWidth(): number {
|
|
253
|
-
const widthOfNucleobases = SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER * (this.maxEffectiveStrandLength + this.maxWidthOfRightOverhangs);
|
|
254
|
-
|
|
255
|
-
const canvasWidth = STRAND_ENDS.reduce((totalWidth, end) => {
|
|
256
|
-
totalWidth += this.strandLabelWidth[end] + this.maxTerminusWidthByEnd[end];
|
|
257
|
-
return totalWidth;
|
|
258
|
-
}, 0) + widthOfNucleobases + SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER;
|
|
259
|
-
|
|
260
|
-
return canvasWidth;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
getCanvasHeight(): number {
|
|
264
|
-
return (this.config.isAntisenseStrandIncluded ? 11 : 9) * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
class NucleotidePositionCalculator {
|
|
269
|
-
constructor(
|
|
270
|
-
private config: PatternConfiguration,
|
|
271
|
-
private maxEffectiveStrandLength: number,
|
|
272
|
-
private rightOverhangNucleotideCounts: StrandToNumberMap,
|
|
273
|
-
private maxTerminusWidthByEnd: StrandEndToNumberMap
|
|
274
|
-
) {}
|
|
275
|
-
|
|
276
|
-
getNucleotideCirclePosition(indexOfNucleotide: number, strand: STRAND): Position {
|
|
277
|
-
return {
|
|
278
|
-
x: this.computeNucleotideCircleXPositionByStrand(indexOfNucleotide, strand),
|
|
279
|
-
y: Y_POSITIONS_FOR_STRAND_ELEMENTS[strand].NUCLEOBASE_CIRCLE,
|
|
280
|
-
};
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
getNucleotideLabelTextPosition(indexOfNucleotide: number, strand: STRAND): Position {
|
|
284
|
-
return {
|
|
285
|
-
// todo: refactor legacy dependency on one-digit parameter
|
|
286
|
-
x: this.computeNucleotideCircleXPositionByStrand(indexOfNucleotide, strand) + NUMERIC_LABEL_POSITION_OFFSET.ONE_DIGIT,
|
|
287
|
-
y: Y_POSITIONS_FOR_STRAND_ELEMENTS[strand].NUCLEOBASE_LABEL,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// todo: cleanup legacy logic
|
|
292
|
-
private computeNucleotideCircleXPositionByStrand(index: number, strand: STRAND): number {
|
|
293
|
-
const rightOverhangCount = this.rightOverhangNucleotideCounts[strand];
|
|
294
|
-
return this.computeNucleobaseCircleXPosition(index, rightOverhangCount);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
computeNucleobaseCircleXPosition(index: number, rightOverhangCount: number): number {
|
|
298
|
-
const rightModificationOffset = this.maxTerminusWidthByEnd[STRAND_END.RIGHT];
|
|
299
|
-
const positionalIndex = this.maxEffectiveStrandLength - index + rightOverhangCount + 1;
|
|
300
|
-
const xPosition = positionalIndex * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER;
|
|
301
|
-
const finalPosition = rightModificationOffset + xPosition;
|
|
302
|
-
return finalPosition;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
getNumericLabelPosition(
|
|
306
|
-
indexOfNucleotide: number,
|
|
307
|
-
strand: STRAND,
|
|
308
|
-
displayedNucleotideNumber: number
|
|
309
|
-
): Position {
|
|
310
|
-
const indexForVisualStrand = this.getVisualStrandIndex(indexOfNucleotide, strand);
|
|
311
|
-
|
|
312
|
-
const numericLabelOffset = this.computeNumericLabelXOffset(
|
|
313
|
-
this.config.nucleotideSequences[strand],
|
|
314
|
-
indexForVisualStrand,
|
|
315
|
-
displayedNucleotideNumber
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
const numericLabelXPosition = this.computeNucleotideCircleXPositionByStrand(indexOfNucleotide, strand) + numericLabelOffset;
|
|
319
|
-
|
|
320
|
-
return {
|
|
321
|
-
x: numericLabelXPosition,
|
|
322
|
-
y: Y_POSITIONS_FOR_STRAND_ELEMENTS[strand].NUMERIC_LABEL,
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
private computeNumericLabelXOffset(bases: string[], nucleobaseIndex: number, nucleotideNumericLabel: number): number {
|
|
327
|
-
const isSingleDigitLabel = nucleotideNumericLabel >= 0 && nucleotideNumericLabel < 10;
|
|
328
|
-
|
|
329
|
-
const criterion = isSingleDigitLabel || NUCLEOTIDES.includes(bases[nucleobaseIndex]);
|
|
330
|
-
|
|
331
|
-
return criterion ?
|
|
332
|
-
NUMERIC_LABEL_POSITION_OFFSET.ONE_DIGIT :
|
|
333
|
-
NUMERIC_LABEL_POSITION_OFFSET.TWO_DIGIT;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/** Inverses index for antisense strand image */
|
|
337
|
-
private getVisualStrandIndex(indexOfNucleotide: number, strand: STRAND): number {
|
|
338
|
-
return strand === STRAND.SENSE ?
|
|
339
|
-
indexOfNucleotide :
|
|
340
|
-
this.config.nucleotideSequences[strand].length - indexOfNucleotide;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
class LegendPositionCalculator {
|
|
345
|
-
constructor(
|
|
346
|
-
private config: PatternConfiguration,
|
|
347
|
-
private canvasDimensionCalculator: CanvasDimensionCalculator
|
|
348
|
-
) {}
|
|
349
|
-
|
|
350
|
-
getLegendCirclePosition(index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): Position {
|
|
351
|
-
const centerPosition = {
|
|
352
|
-
x: this.computeLegendCircleXPosition(index, distinctNucleobaseTypes, containsPhosphorothioateLinkages),
|
|
353
|
-
y: this.getLegendVerticalPosition(),
|
|
354
|
-
};
|
|
355
|
-
return centerPosition;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
getLegendTextPosition(index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): Position {
|
|
359
|
-
const legendPosition = {
|
|
360
|
-
x: this.computeLegendCircleXPosition(index, distinctNucleobaseTypes, containsPhosphorothioateLinkages) + SVG_CIRCLE_SIZES.LEGEND_RADIUS + 4,
|
|
361
|
-
y: this.getLegendTextVerticalPosition(),
|
|
362
|
-
};
|
|
363
|
-
return legendPosition;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
private computeLegendCircleXPosition(index: number, distinctNucleobaseTypes: string[], containsPhosphorothioateLinkages: boolean): number {
|
|
367
|
-
const legendStartIndex = containsPhosphorothioateLinkages ? 1 : 0;
|
|
368
|
-
const totalPositions = distinctNucleobaseTypes.length + legendStartIndex;
|
|
369
|
-
const width = this.canvasDimensionCalculator.getCanvasWidth();
|
|
370
|
-
const spacingUnit = width / totalPositions;
|
|
371
|
-
const position = (index + legendStartIndex) * spacingUnit;
|
|
372
|
-
const adjustedPosition = position + SVG_CIRCLE_SIZES.LEGEND_RADIUS;
|
|
373
|
-
return Math.round(adjustedPosition);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
getStarLabelPosition(): Position {
|
|
377
|
-
return {
|
|
378
|
-
x: SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS,
|
|
379
|
-
y: this.getLegendVerticalPosition(),
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
getPhosphorothioateLinkageLabelPosition(): Position {
|
|
384
|
-
return {
|
|
385
|
-
x: 2 * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS - 8,
|
|
386
|
-
y: this.getLegendTextVerticalPosition(),
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
private getLegendVerticalPosition(): number {
|
|
391
|
-
return (this.config.isAntisenseStrandIncluded ? 9.5 : 6) * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
private getLegendTextVerticalPosition(): number {
|
|
395
|
-
const position = this.config.isAntisenseStrandIncluded ? 10 * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS : Y_POSITIONS_FOR_STRAND_ELEMENTS[STRAND.ANTISENSE].NUCLEOBASE_CIRCLE;
|
|
396
|
-
return position - 3;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
class LabelPositionCalculator {
|
|
401
|
-
private xPositionOfTerminusModifications: Record<typeof STRAND_ENDS[number], StrandToNumberMap>;
|
|
402
|
-
constructor(
|
|
403
|
-
private config: PatternConfiguration,
|
|
404
|
-
private maxWidthOfRightOverhangs: number,
|
|
405
|
-
private maxTerminusWidthByEnd: StrandEndToNumberMap,
|
|
406
|
-
private strandLabelWidth: StrandEndToNumberMap,
|
|
407
|
-
private rightOverhangNucleotideCounts: StrandToNumberMap,
|
|
408
|
-
private nucleotidePositionCalculator: NucleotidePositionCalculator
|
|
409
|
-
) {
|
|
410
|
-
this.xPositionOfTerminusModifications = this.computeXPositionOfTerminusModifications();
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
getTerminusLabelPosition(strand: STRAND, end: STRAND_END): Position {
|
|
414
|
-
return {
|
|
415
|
-
x: this.xPositionOfTerminusModifications[end][strand],
|
|
416
|
-
y: Y_POSITIONS_FOR_STRAND_ELEMENTS[strand].NUCLEOBASE_LABEL,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
getStrandEndLabelPosition(strand: STRAND, end: STRAND_END): Position {
|
|
421
|
-
const xPosition = this.getXPositionOfStrandLabels()[end];
|
|
422
|
-
return {
|
|
423
|
-
x: xPosition,
|
|
424
|
-
// todo: remove legacy grouping by y positions
|
|
425
|
-
y: Y_POSITIONS_FOR_STRAND_ELEMENTS[strand].NUCLEOBASE_LABEL,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
getTitleTextPosition(): Position {
|
|
430
|
-
return {
|
|
431
|
-
x: SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS,
|
|
432
|
-
y: SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS,
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
getCommentLabelPosition(): Position {
|
|
437
|
-
const y = (this.config.isAntisenseStrandIncluded ? 11 : 8.5) * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS;
|
|
438
|
-
|
|
439
|
-
const x = this.getXPositionOfStrandLabels()[STRAND_END.LEFT];
|
|
440
|
-
return {x, y};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
private getXPositionOfStrandLabels(): StrandEndToNumberMap {
|
|
444
|
-
const maxRightTerminusModificationShift = Math.max(
|
|
445
|
-
...STRANDS.map((strand) => this.xPositionOfTerminusModifications[STRAND_END.RIGHT][strand])
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
const rightEndXPosition = maxRightTerminusModificationShift +
|
|
449
|
-
this.maxTerminusWidthByEnd[STRAND_END.LEFT] +
|
|
450
|
-
SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER * this.maxWidthOfRightOverhangs;
|
|
451
|
-
|
|
452
|
-
return {
|
|
453
|
-
[STRAND_END.LEFT]: 0,
|
|
454
|
-
[STRAND_END.RIGHT]: rightEndXPosition,
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
private computeXPositionOfTerminusModifications(): Record<typeof STRAND_ENDS[number], StrandToNumberMap> {
|
|
459
|
-
const xPositionOfTerminusModifications = Object.fromEntries(
|
|
460
|
-
STRAND_ENDS.map((end) => [
|
|
461
|
-
end,
|
|
462
|
-
Object.fromEntries(
|
|
463
|
-
STRANDS.map((strand) => [
|
|
464
|
-
strand,
|
|
465
|
-
end === STRAND_END.LEFT ?
|
|
466
|
-
this.strandLabelWidth[STRAND_END.LEFT] - 5 :
|
|
467
|
-
this.rightOverhangNucleotideCounts[strand] * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER + this.nucleotidePositionCalculator.computeNucleobaseCircleXPosition(-0.5, 0)
|
|
468
|
-
])
|
|
469
|
-
)
|
|
470
|
-
])
|
|
471
|
-
) as Record<typeof STRAND_ENDS[number], StrandToNumberMap>;
|
|
472
|
-
|
|
473
|
-
return xPositionOfTerminusModifications;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
class LinkageStarPositionCalculator {
|
|
478
|
-
constructor(
|
|
479
|
-
private nucleotidePositionCalculator: NucleotidePositionCalculator,
|
|
480
|
-
private rightOverhangNucleotideCounts: StrandToNumberMap
|
|
481
|
-
) {}
|
|
482
|
-
|
|
483
|
-
getCenterPositionOfLinkageStar(index: number, strand: STRAND): Position {
|
|
484
|
-
return {
|
|
485
|
-
x: this.getXPositionOfLinkageStar(index, strand),
|
|
486
|
-
y: this.getYPositionOfLinkageStar(strand),
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// todo: remove legacy division of x-y coordinates throughout the code
|
|
491
|
-
private getXPositionOfLinkageStar(index: number, strand: STRAND): number {
|
|
492
|
-
return this.nucleotidePositionCalculator.computeNucleobaseCircleXPosition(index, this.rightOverhangNucleotideCounts[strand]) + SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
private getYPositionOfLinkageStar(strand: STRAND): number {
|
|
496
|
-
return Y_POSITIONS_FOR_STRAND_ELEMENTS[strand].NUCLEOBASE_LABEL + SVG_CIRCLE_SIZES.LINKAGE_STAR_RADIUS;
|
|
497
|
-
}
|
|
498
|
-
}
|