@jbrowse/plugin-alignments 1.7.7 → 1.7.10

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 (39) hide show
  1. package/dist/AlignmentsFeatureDetail/AlignmentsFeatureDetail.js +13 -3
  2. package/dist/AlignmentsFeatureDetail/index.d.ts +28 -3
  3. package/dist/AlignmentsFeatureDetail/index.js +6 -17
  4. package/dist/BamAdapter/BamAdapter.d.ts +1 -1
  5. package/dist/BamAdapter/BamAdapter.js +3 -3
  6. package/dist/BamAdapter/MismatchParser.d.ts +2 -5
  7. package/dist/BamAdapter/MismatchParser.js +108 -46
  8. package/dist/BamAdapter/MismatchParser.test.js +6 -14
  9. package/dist/CramAdapter/CramAdapter.d.ts +10 -9
  10. package/dist/CramAdapter/CramAdapter.js +6 -6
  11. package/dist/CramAdapter/CramSlightlyLazyFeature.js +35 -30
  12. package/dist/LinearPileupDisplay/model.d.ts +6 -3
  13. package/dist/LinearPileupDisplay/model.js +132 -51
  14. package/dist/LinearSNPCoverageDisplay/components/Tooltip.js +37 -17
  15. package/dist/LinearSNPCoverageDisplay/models/model.d.ts +2 -2
  16. package/dist/LinearSNPCoverageDisplay/models/model.js +1 -1
  17. package/dist/PileupRenderer/PileupLayoutSession.d.ts +3 -0
  18. package/dist/PileupRenderer/PileupLayoutSession.js +3 -1
  19. package/dist/PileupRenderer/PileupRenderer.d.ts +66 -9
  20. package/dist/PileupRenderer/PileupRenderer.js +296 -258
  21. package/dist/PileupRenderer/configSchema.js +2 -2
  22. package/dist/SNPCoverageAdapter/SNPCoverageAdapter.d.ts +6 -6
  23. package/dist/SNPCoverageAdapter/SNPCoverageAdapter.js +95 -96
  24. package/dist/SNPCoverageRenderer/configSchema.d.ts +1 -1
  25. package/package.json +3 -3
  26. package/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx +14 -3
  27. package/src/AlignmentsFeatureDetail/index.ts +7 -17
  28. package/src/BamAdapter/BamAdapter.ts +3 -3
  29. package/src/BamAdapter/MismatchParser.test.ts +5 -7
  30. package/src/BamAdapter/MismatchParser.ts +72 -59
  31. package/src/CramAdapter/CramAdapter.ts +14 -10
  32. package/src/CramAdapter/CramSlightlyLazyFeature.ts +84 -91
  33. package/src/LinearPileupDisplay/model.ts +76 -24
  34. package/src/LinearSNPCoverageDisplay/components/Tooltip.tsx +41 -20
  35. package/src/LinearSNPCoverageDisplay/models/model.ts +1 -1
  36. package/src/PileupRenderer/PileupLayoutSession.ts +6 -1
  37. package/src/PileupRenderer/PileupRenderer.tsx +413 -225
  38. package/src/PileupRenderer/configSchema.ts +2 -2
  39. package/src/SNPCoverageAdapter/SNPCoverageAdapter.ts +89 -76
@@ -31,8 +31,8 @@ var _default = (0, _configuration.ConfigurationSchema)('PileupRenderer', {
31
31
  },
32
32
  minSubfeatureWidth: {
33
33
  type: 'number',
34
- description: 'the minimum width in px for a pileup mismatch feature. use for increasing mismatch marker widths when zoomed out to e.g. 1px or 0.5px',
35
- defaultValue: 0
34
+ description: 'the minimum width in px for a pileup mismatch feature. use for increasing/decreasing mismatch marker widths when zoomed out, e.g. 0 or 1',
35
+ defaultValue: 0.7
36
36
  },
37
37
  maxHeight: {
38
38
  type: 'integer',
@@ -19,7 +19,13 @@ export default class SNPCoverageAdapter extends BaseFeatureDataAdapter {
19
19
  };
20
20
  }): Promise<{
21
21
  bins: {
22
+ refbase?: string | undefined;
22
23
  total: number;
24
+ all: number;
25
+ ref: number;
26
+ '-1': 0;
27
+ '0': 0;
28
+ '1': 0;
23
29
  lowqual: {
24
30
  total: number;
25
31
  strands: {
@@ -44,12 +50,6 @@ export default class SNPCoverageAdapter extends BaseFeatureDataAdapter {
44
50
  [key: string]: number;
45
51
  };
46
52
  };
47
- ref: {
48
- total: number;
49
- strands: {
50
- [key: string]: number;
51
- };
52
- };
53
53
  }[];
54
54
  skipmap: {
55
55
  [key: string]: {
@@ -57,36 +57,17 @@ function isInterbase(type) {
57
57
  function inc(bin, strand, type, field) {
58
58
  var thisBin = bin[type][field];
59
59
 
60
- if (!thisBin) {
60
+ if (thisBin === undefined) {
61
61
  thisBin = bin[type][field] = {
62
62
  total: 0,
63
- strands: {
64
- '-1': 0,
65
- '0': 0,
66
- '1': 0
67
- }
63
+ '-1': 0,
64
+ '0': 0,
65
+ '1': 0
68
66
  };
69
67
  }
70
68
 
71
69
  thisBin.total++;
72
- thisBin.strands[strand]++;
73
- } // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
-
75
-
76
- function dec(bin, strand, type, field) {
77
- if (!bin[type][field]) {
78
- bin[type][field] = {
79
- total: 0,
80
- strands: {
81
- '-1': 0,
82
- '0': 0,
83
- '1': 0
84
- }
85
- };
86
- }
87
-
88
- bin[type][field].total--;
89
- bin[type][field].strands[strand]--;
70
+ thisBin[strand]++;
90
71
  }
91
72
 
92
73
  var SNPCoverageAdapter = /*#__PURE__*/function (_BaseFeatureDataAdapt) {
@@ -396,36 +377,45 @@ var SNPCoverageAdapter = /*#__PURE__*/function (_BaseFeatureDataAdapt) {
396
377
 
397
378
  _loop = function _loop(i) {
398
379
  var feature = features[i];
399
- var ops = (0, _MismatchParser.parseCigar)(feature.get('CIGAR'));
400
380
  var fstart = feature.get('start');
401
381
  var fend = feature.get('end');
402
382
  var fstrand = feature.get('strand');
403
383
 
404
- for (var j = fstart; j < fend; j++) {
384
+ for (var j = fstart; j < fend + 1; j++) {
405
385
  var _i = j - region.start;
406
386
 
407
387
  if (_i >= 0 && _i < binMax) {
408
- var bin = bins[_i] || {
409
- total: 0,
410
- lowqual: {},
411
- cov: {},
412
- delskips: {},
413
- noncov: {},
414
- ref: {}
415
- };
388
+ if (bins[_i] === undefined) {
389
+ bins[_i] = {
390
+ total: 0,
391
+ all: 0,
392
+ ref: 0,
393
+ '-1': 0,
394
+ '0': 0,
395
+ '1': 0,
396
+ lowqual: {},
397
+ cov: {},
398
+ delskips: {},
399
+ noncov: {}
400
+ };
401
+ }
416
402
 
417
403
  if (j !== fend) {
418
- bin.total++;
419
- inc(bin, fstrand, 'ref', 'ref');
404
+ bins[_i].total++;
405
+ bins[_i].all++;
406
+ bins[_i].ref++;
407
+ bins[_i][fstrand]++;
420
408
  }
421
-
422
- bins[_i] = bin;
423
409
  }
424
410
  }
425
411
 
426
412
  if ((colorBy === null || colorBy === void 0 ? void 0 : colorBy.type) === 'modifications') {
427
413
  var seq = feature.get('seq');
428
414
  var mm = (0, _util.getTagAlt)(feature, 'MM', 'Mm') || '';
415
+ var ops = (0, _MismatchParser.parseCigar)(feature.get('CIGAR'));
416
+
417
+ var _fend = feature.get('end');
418
+
429
419
  (0, _MismatchParser.getModificationPositions)(mm, seq, fstrand).forEach(function (_ref4) {
430
420
  var type = _ref4.type,
431
421
  positions = _ref4.positions;
@@ -439,9 +429,14 @@ var SNPCoverageAdapter = /*#__PURE__*/function (_BaseFeatureDataAdapt) {
439
429
  var pos = _step.value;
440
430
  var epos = pos + fstart - region.start;
441
431
 
442
- if (epos >= 0 && epos < bins.length && pos + fstart < fend) {
443
- var _bin = bins[epos];
444
- inc(_bin, fstrand, 'cov', mod);
432
+ if (epos >= 0 && epos < bins.length && pos + fstart < _fend) {
433
+ var bin = bins[epos];
434
+
435
+ if (bin) {
436
+ inc(bin, fstrand, 'cov', mod);
437
+ } else {
438
+ console.warn('Undefined position in modifications snpcoverage encountered');
439
+ }
445
440
  }
446
441
  }
447
442
  } catch (err) {
@@ -462,13 +457,16 @@ var SNPCoverageAdapter = /*#__PURE__*/function (_BaseFeatureDataAdapt) {
462
457
  var _mm = (0, _util.getTagAlt)(feature, 'MM', 'Mm') || '';
463
458
 
464
459
  var methBins = new Array(region.end - region.start).fill(0);
460
+
461
+ var _ops = (0, _MismatchParser.parseCigar)(feature.get('CIGAR'));
462
+
465
463
  (0, _MismatchParser.getModificationPositions)(_mm, _seq, fstrand).forEach(function (_ref5) {
466
464
  var type = _ref5.type,
467
465
  positions = _ref5.positions;
468
466
 
469
467
  // we are processing methylation
470
468
  if (type === 'm') {
471
- var _iterator2 = _createForOfIteratorHelper((0, _MismatchParser.getNextRefPos)(ops, positions)),
469
+ var _iterator2 = _createForOfIteratorHelper((0, _MismatchParser.getNextRefPos)(_ops, positions)),
472
470
  _step2;
473
471
 
474
472
  try {
@@ -496,80 +494,81 @@ var SNPCoverageAdapter = /*#__PURE__*/function (_BaseFeatureDataAdapt) {
496
494
 
497
495
  var l2 = regionSeq[_i2 + 1].toLowerCase();
498
496
 
499
- var _bin2 = bins[_i2];
497
+ var bin = bins[_i2];
500
498
  var bin1 = bins[_i2 + 1]; // color
501
499
 
502
500
  if (l1 === 'c' && l2 === 'g') {
503
501
  if (methBins[_i2] || methBins[_i2 + 1]) {
504
- inc(_bin2, fstrand, 'cov', 'meth');
502
+ inc(bin, fstrand, 'cov', 'meth');
505
503
  inc(bin1, fstrand, 'cov', 'meth');
506
- dec(_bin2, fstrand, 'ref', 'ref');
507
- dec(bin1, fstrand, 'ref', 'ref');
504
+ bins[_i2].ref--;
505
+ bins[_i2][fstrand]--;
506
+ bins[_i2 + 1].ref--;
507
+ bins[_i2 + 1][fstrand]--;
508
508
  } else {
509
- inc(_bin2, fstrand, 'cov', 'unmeth');
509
+ inc(bin, fstrand, 'cov', 'unmeth');
510
510
  inc(bin1, fstrand, 'cov', 'unmeth');
511
- dec(_bin2, fstrand, 'ref', 'ref');
512
- dec(bin1, fstrand, 'ref', 'ref');
511
+ bins[_i2].ref--;
512
+ bins[_i2][fstrand]--;
513
+ bins[_i2 + 1].ref--;
514
+ bins[_i2 + 1][fstrand]--;
513
515
  }
514
516
  }
515
517
  }
516
518
  }
517
519
  } // normal SNP based coloring
518
- else {
519
- var mismatches = feature.get('mismatches');
520
520
 
521
- if (mismatches) {
522
- for (var _i3 = 0; _i3 < mismatches.length; _i3++) {
523
- var mismatch = mismatches[_i3];
524
- var mstart = fstart + mismatch.start;
525
521
 
526
- for (var _j2 = mstart; _j2 < mstart + mismatchLen(mismatch); _j2++) {
527
- var epos = _j2 - region.start;
522
+ var mismatches = feature.get('mismatches') || [];
523
+ var colorSNPs = (colorBy === null || colorBy === void 0 ? void 0 : colorBy.type) !== 'modifications' && (colorBy === null || colorBy === void 0 ? void 0 : colorBy.type) !== 'methylation';
528
524
 
529
- if (epos >= 0 && epos < bins.length) {
530
- var _bin3 = bins[epos];
531
- var base = mismatch.base,
532
- type = mismatch.type;
533
- var interbase = isInterbase(type);
525
+ for (var _i3 = 0; _i3 < mismatches.length; _i3++) {
526
+ var mismatch = mismatches[_i3];
527
+ var mstart = fstart + mismatch.start;
528
+ var mlen = mismatchLen(mismatch);
529
+ var mend = mstart + mlen;
534
530
 
535
- if (!interbase) {
536
- dec(_bin3, fstrand, 'ref', 'ref');
537
- } else {
538
- inc(_bin3, fstrand, 'noncov', type);
539
- }
531
+ for (var _j2 = mstart; _j2 < mstart + mlen; _j2++) {
532
+ var epos = _j2 - region.start;
540
533
 
541
- if (type === 'deletion' || type === 'skip') {
542
- inc(_bin3, fstrand, 'delskips', type);
543
- _bin3.total--;
544
- } else if (!interbase) {
545
- inc(_bin3, fstrand, 'cov', base);
546
- }
547
- }
548
- }
549
- }
534
+ if (epos >= 0 && epos < bins.length) {
535
+ var _bin = bins[epos];
536
+ var base = mismatch.base,
537
+ type = mismatch.type;
538
+ var interbase = isInterbase(type);
550
539
 
551
- mismatches.filter(function (mismatch) {
552
- return mismatch.type === 'skip';
553
- }).forEach(function (mismatch) {
554
- var mstart = feature.get('start') + mismatch.start;
555
- var start = mstart;
556
- var end = mstart + mismatch.length;
557
- var strand = feature.get('strand');
558
- var hash = "".concat(start, "_").concat(end, "_").concat(strand);
559
-
560
- if (!skipmap[hash]) {
561
- skipmap[hash] = {
562
- feature: feature,
563
- start: start,
564
- end: end,
565
- strand: strand,
566
- xs: (0, _util.getTag)(feature, 'XS') || (0, _util.getTag)(feature, 'TS'),
567
- score: 1
568
- };
540
+ if (!interbase) {
541
+ _bin.ref--;
542
+ _bin[fstrand]--;
569
543
  } else {
570
- skipmap[hash].score++;
544
+ inc(_bin, fstrand, 'noncov', type);
571
545
  }
572
- });
546
+
547
+ if (type === 'deletion' || type === 'skip') {
548
+ inc(_bin, fstrand, 'delskips', type);
549
+ _bin.total--;
550
+ } else if (!interbase && colorSNPs) {
551
+ inc(_bin, fstrand, 'cov', base);
552
+ _bin.refbase = mismatch.altbase;
553
+ }
554
+ }
555
+ }
556
+
557
+ if (mismatch.type === 'skip') {
558
+ var hash = "".concat(mstart, "_").concat(mend, "_").concat(fstrand);
559
+
560
+ if (skipmap[hash] === undefined) {
561
+ skipmap[hash] = {
562
+ feature: feature,
563
+ start: mstart,
564
+ end: mend,
565
+ strand: fstrand,
566
+ xs: (0, _util.getTag)(feature, 'XS') || (0, _util.getTag)(feature, 'TS'),
567
+ score: 0
568
+ };
569
+ }
570
+
571
+ skipmap[hash].score++;
573
572
  }
574
573
  }
575
574
  };
@@ -1,2 +1,2 @@
1
- declare var _default: import("@jbrowse/core/configuration").AnyConfigurationSchemaType;
1
+ declare const _default: import("@jbrowse/core/configuration").AnyConfigurationSchemaType;
2
2
  export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/plugin-alignments",
3
- "version": "1.7.7",
3
+ "version": "1.7.10",
4
4
  "description": "JBrowse 2 alignments adapters, tracks, etc.",
5
5
  "keywords": [
6
6
  "jbrowse",
@@ -35,7 +35,7 @@
35
35
  "dependencies": {
36
36
  "@babel/runtime": "^7.17.9",
37
37
  "@gmod/bam": "^1.1.15",
38
- "@gmod/cram": "^1.6.1",
38
+ "@gmod/cram": "^1.6.4",
39
39
  "@material-ui/icons": "^4.9.1",
40
40
  "color": "^3.1.2",
41
41
  "copy-to-clipboard": "^3.3.1",
@@ -57,5 +57,5 @@
57
57
  "publishConfig": {
58
58
  "access": "public"
59
59
  },
60
- "gitHead": "2c26e04ae942c380bf2f5b79ef7a49cc32b7bfed"
60
+ "gitHead": "02d8c1e88e5603ea5855faed4ccb814e28071b32"
61
61
  }
@@ -77,12 +77,22 @@ function AlignmentFlags(props: { feature: any }) {
77
77
 
78
78
  function Formatter({ value }: { value: unknown }) {
79
79
  const [show, setShow] = useState(false)
80
+ const [copied, setCopied] = useState(false)
80
81
  const display = String(value)
81
82
  if (display.length > 100) {
82
83
  return (
83
84
  <>
84
- <button type="button" onClick={() => copy(display)}>
85
- Copy
85
+ <button
86
+ type="button"
87
+ onClick={() => {
88
+ copy(display)
89
+ setCopied(true)
90
+ setTimeout(() => {
91
+ setCopied(false)
92
+ }, 700)
93
+ }}
94
+ >
95
+ {copied ? 'Copied to clipboard' : 'Copy'}
86
96
  </button>
87
97
  <button type="button" onClick={() => setShow(val => !val)}>
88
98
  {show ? 'Show less' : 'Show more'}
@@ -134,7 +144,8 @@ function SupplementaryAlignments(props: { tag: string; model: any }) {
134
144
  return (
135
145
  <li key={`${locString}-${index}`}>
136
146
  <Link
137
- onClick={() => {
147
+ onClick={event => {
148
+ event.preventDefault()
138
149
  const { view } = model
139
150
  try {
140
151
  if (view) {
@@ -1,30 +1,20 @@
1
1
  import { lazy } from 'react'
2
2
  import PluginManager from '@jbrowse/core/PluginManager'
3
3
  import { ConfigurationSchema } from '@jbrowse/core/configuration'
4
- import { ElementId } from '@jbrowse/core/util/types/mst'
5
4
  import { types } from 'mobx-state-tree'
6
5
  import WidgetType from '@jbrowse/core/pluggableElementTypes/WidgetType'
6
+ import { stateModelFactory as baseModelFactory } from '@jbrowse/core/BaseFeatureWidget'
7
7
 
8
8
  const configSchema = ConfigurationSchema('AlignmentsFeatureWidget', {})
9
9
 
10
10
  export function stateModelFactory(pluginManager: PluginManager) {
11
- return types
12
- .model('AlignmentsFeatureWidget', {
13
- id: ElementId,
11
+ const baseModel = baseModelFactory(pluginManager)
12
+ return types.compose(
13
+ baseModel,
14
+ types.model('AlignmentsFeatureWidget', {
14
15
  type: types.literal('AlignmentsFeatureWidget'),
15
- featureData: types.frozen(),
16
- view: types.safeReference(
17
- pluginManager.pluggableMstType('view', 'stateModel'),
18
- ),
19
- })
20
- .actions(self => ({
21
- setFeatureData(data: unknown) {
22
- self.featureData = data
23
- },
24
- clearFeatureData() {
25
- self.featureData = undefined
26
- },
27
- }))
16
+ }),
17
+ )
28
18
  }
29
19
 
30
20
  export default function register(pluginManager: PluginManager) {
@@ -171,7 +171,7 @@ export default class BamAdapter extends BaseFeatureDataAdapter {
171
171
  flagInclude: number
172
172
  flagExclude: number
173
173
  tagFilter: { tag: string; value: unknown }
174
- name: string
174
+ readName: string
175
175
  }
176
176
  },
177
177
  ) {
@@ -187,7 +187,7 @@ export default class BamAdapter extends BaseFeatureDataAdapter {
187
187
  flagInclude = 0,
188
188
  flagExclude = 0,
189
189
  tagFilter,
190
- name,
190
+ readName,
191
191
  } = filterBy || {}
192
192
 
193
193
  for (const record of records) {
@@ -214,7 +214,7 @@ export default class BamAdapter extends BaseFeatureDataAdapter {
214
214
  }
215
215
  }
216
216
 
217
- if (name && record.get('name') !== name) {
217
+ if (readName && record.get('name') !== readName) {
218
218
  continue
219
219
  }
220
220
 
@@ -233,17 +233,15 @@ test('clipping', () => {
233
233
  ])
234
234
  })
235
235
 
236
- test('getNextRefPos basic', () => {
236
+ test('getNextRefPos test 1', () => {
237
237
  const cigar = parseCigar('10S10M1I4M1D15M')
238
238
  const iter = getNextRefPos(cigar, [5, 10, 15, 20, 25, 30, 35])
239
- const [...vals] = iter
240
- expect(vals).toEqual([-5, 0, 5, 10, 14, 20, 25])
239
+ expect([...iter]).toEqual([0, 5, 15, 20, 25])
241
240
  })
242
- test('getNextRefPos with many indels', () => {
243
- const cigar = parseCigar('10S4M1D1IM10')
241
+ test('getNextRefPos test 2', () => {
242
+ const cigar = parseCigar('10S15M')
244
243
  const iter = getNextRefPos(cigar, [5, 10, 15])
245
- const [...vals] = iter
246
- expect(vals).toEqual([-5, 0, 5])
244
+ expect([...iter]).toEqual([0, 5])
247
245
  })
248
246
 
249
247
  test('getModificationPositions', () => {
@@ -10,6 +10,7 @@ export interface Mismatch {
10
10
  cliplen?: number
11
11
  }
12
12
  const mdRegex = new RegExp(/(\d+|\^[a-z]+|[a-z])/gi)
13
+ const modificationRegex = new RegExp(/([A-Z])([-+])([^,.?]+)([.?])?/)
13
14
  export function parseCigar(cigar: string) {
14
15
  return (cigar || '').split(/([MIDNSHPX=])/)
15
16
  }
@@ -38,6 +39,7 @@ export function cigarToMismatches(
38
39
  start: roffset + j,
39
40
  type: 'mismatch',
40
41
  base: seq[soffset + j],
42
+ altbase: ref[roffset + j],
41
43
  length: 1,
42
44
  })
43
45
  }
@@ -232,26 +234,32 @@ export function getMismatches(
232
234
  // get relative reference sequence positions for positions given relative to
233
235
  // the read sequence
234
236
  export function* getNextRefPos(cigarOps: string[], positions: number[]) {
235
- let cigarIdx = 0
236
237
  let readPos = 0
237
238
  let refPos = 0
239
+ let currPos = 0
238
240
 
239
- for (let i = 0; i < positions.length; i++) {
240
- const pos = positions[i]
241
- for (; cigarIdx < cigarOps.length && readPos < pos; cigarIdx += 2) {
242
- const len = +cigarOps[cigarIdx]
243
- const op = cigarOps[cigarIdx + 1]
244
- if (op === 'S' || op === 'I') {
245
- readPos += len
246
- } else if (op === 'D' || op === 'N') {
247
- refPos += len
248
- } else if (op === 'M' || op === 'X' || op === '=') {
249
- readPos += len
250
- refPos += len
241
+ for (let i = 0; i < cigarOps.length && currPos < positions.length; i += 2) {
242
+ const len = +cigarOps[i]
243
+ const op = cigarOps[i + 1]
244
+ if (op === 'S' || op === 'I') {
245
+ for (let i = 0; i < len && currPos < positions.length; i++) {
246
+ if (positions[currPos] === readPos + i) {
247
+ currPos++
248
+ }
249
+ }
250
+ readPos += len
251
+ } else if (op === 'D' || op === 'N') {
252
+ refPos += len
253
+ } else if (op === 'M' || op === 'X' || op === '=') {
254
+ for (let i = 0; i < len && currPos < positions.length; i++) {
255
+ if (positions[currPos] === readPos + i) {
256
+ yield refPos + i
257
+ currPos++
258
+ }
251
259
  }
260
+ readPos += len
261
+ refPos += len
252
262
  }
253
-
254
- yield positions[i] - readPos + refPos
255
263
  }
256
264
  }
257
265
  export function getModificationPositions(
@@ -260,54 +268,59 @@ export function getModificationPositions(
260
268
  fstrand: number,
261
269
  ) {
262
270
  const seq = fstrand === -1 ? revcom(fseq) : fseq
263
- return mm
264
- .split(';')
265
- .filter(mod => !!mod)
266
- .map(mod => {
267
- const [basemod, ...skips] = mod.split(',')
271
+ const mods = mm.split(';').filter(mod => !!mod)
272
+ const result = []
273
+ for (let i = 0; i < mods.length; i++) {
274
+ const mod = mods[i]
275
+ const [basemod, ...skips] = mod.split(',')
268
276
 
269
- // regexes based on parse_mm.pl from hts-specs
270
- const matches = basemod.match(/([A-Z])([-+])([^,.?]+)([.?])?/)
271
- if (!matches) {
272
- throw new Error('bad format for MM tag')
273
- }
274
- const [, base, strand, typestr] = matches
277
+ // regexes based on parse_mm.pl from hts-specs
278
+ const matches = basemod.match(modificationRegex)
279
+ if (!matches) {
280
+ throw new Error('bad format for MM tag')
281
+ }
282
+ const [, base, strand, typestr] = matches
275
283
 
276
- // can be a multi e.g. C+mh for both meth (m) and hydroxymeth (h) so
277
- // split, and they can also be chemical codes (ChEBI) e.g. C+16061
278
- const types = typestr.split(/(\d+|.)/).filter(f => !!f)
284
+ // can be a multi e.g. C+mh for both meth (m) and hydroxymeth (h) so
285
+ // split, and they can also be chemical codes (ChEBI) e.g. C+16061
286
+ const types = typestr.split(/(\d+|.)/).filter(f => !!f)
279
287
 
280
- if (strand === '-') {
281
- console.warn('unsupported negative strand modifications')
282
- // make sure to return a somewhat matching type even in this case
283
- return { type: 'unsupported', positions: [] }
284
- }
288
+ if (strand === '-') {
289
+ console.warn('unsupported negative strand modifications')
290
+ // make sure to return a somewhat matching type even in this case
291
+ result.push({ type: 'unsupported', positions: [] as number[] })
292
+ }
285
293
 
286
- // this logic also based on parse_mm.pl from hts-specs is that in the
287
- // sequence of the read, if we have a modification type e.g. C+m;2 and a
288
- // sequence ACGTACGTAC we skip the two instances of C and go to the last
289
- // C
290
- return types.map(type => {
291
- let i = 0
292
- return {
293
- type,
294
- positions: skips
295
- .map(score => +score)
296
- .map(delta => {
297
- do {
298
- if (base === 'N' || base === seq[i]) {
299
- delta--
300
- }
301
- i++
302
- } while (delta >= 0 && i < seq.length)
303
- const temp = i - 1
304
- return fstrand === -1 ? seq.length - 1 - temp : temp
305
- })
306
- .sort((a, b) => a - b),
307
- }
294
+ // this logic also based on parse_mm.pl from hts-specs is that in the
295
+ // sequence of the read, if we have a modification type e.g. C+m;2 and a
296
+ // sequence ACGTACGTAC we skip the two instances of C and go to the last
297
+ // C
298
+ for (let j = 0; j < types.length; j++) {
299
+ const type = types[j]
300
+ let i = 0
301
+ const positions = []
302
+ for (let k = 0; k < skips.length; k++) {
303
+ let delta = +skips[k]
304
+ do {
305
+ if (base === 'N' || base === seq[i]) {
306
+ delta--
307
+ }
308
+ i++
309
+ } while (delta >= 0 && i < seq.length)
310
+
311
+ const temp = i - 1
312
+ positions.push(fstrand === -1 ? seq.length - 1 - temp : temp)
313
+ }
314
+ if (fstrand === -1) {
315
+ positions.sort((a, b) => a - b)
316
+ }
317
+ result.push({
318
+ type,
319
+ positions,
308
320
  })
309
- })
310
- .flat()
321
+ }
322
+ }
323
+ return result
311
324
  }
312
325
 
313
326
  export function getModificationTypes(mm: string) {
@@ -317,7 +330,7 @@ export function getModificationTypes(mm: string) {
317
330
  .map(mod => {
318
331
  const [basemod] = mod.split(',')
319
332
 
320
- const matches = basemod.match(/([A-Z])([-+])([^,]+)/)
333
+ const matches = basemod.match(modificationRegex)
321
334
  if (!matches) {
322
335
  throw new Error('bad format for MM tag')
323
336
  }