@jbrowse/cli 4.2.1 → 5.0.0-beta.1
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/README.md +334 -60
- package/bin/run +1 -1
- package/bundle/index.js +493 -2555
- package/dist/base.js +0 -3
- package/dist/bin.js +2 -1
- package/dist/cliFetch.js +3 -0
- package/dist/commands/add-assembly/index.js +46 -19
- package/dist/commands/add-assembly/utils.js +204 -290
- package/dist/commands/add-connection.js +73 -79
- package/dist/commands/add-track-json.js +24 -12
- package/dist/commands/add-track-utils/adapter-utils.js +173 -283
- package/dist/commands/add-track-utils/file-operations.js +27 -11
- package/dist/commands/add-track-utils/multiwig.js +47 -0
- package/dist/commands/add-track-utils/track-config.js +68 -25
- package/dist/commands/add-track-utils/validators.js +36 -20
- package/dist/commands/add-track.js +178 -62
- package/dist/commands/admin-server/index.js +30 -49
- package/dist/commands/admin-server/utils.js +235 -125
- package/dist/commands/create.js +18 -34
- package/dist/commands/make-pif/cigar-utils.js +62 -19
- package/dist/commands/make-pif/index.js +98 -11
- package/dist/commands/make-pif/pif-generator.js +276 -42
- package/dist/commands/process-utils.js +13 -2
- package/dist/commands/remove-track.js +20 -13
- package/dist/commands/set-default-session.js +35 -31
- package/dist/commands/shared/config-operations.js +3 -10
- package/dist/commands/shared/sort-utils.js +67 -10
- package/dist/commands/shared/validators.js +38 -3
- package/dist/commands/sort-bed.js +2 -33
- package/dist/commands/sort-gff.js +2 -33
- package/dist/commands/text-index/adapter-utils.js +6 -23
- package/dist/commands/text-index/aggregate.js +40 -34
- package/dist/commands/text-index/command.js +35 -7
- package/dist/commands/text-index/config-utils.js +60 -83
- package/dist/commands/text-index/file-list.js +32 -18
- package/dist/commands/text-index/index.js +4 -4
- package/dist/commands/text-index/indexing-utils.js +49 -73
- package/dist/commands/text-index/per-track.js +44 -31
- package/dist/commands/text-index/validators.js +1 -1
- package/dist/commands/upgrade.js +20 -32
- package/dist/commands/validate/configManifest.generated.js +5394 -0
- package/dist/commands/validate/displayDefaultKeys.js +28 -0
- package/dist/commands/validate/index.js +97 -0
- package/dist/commands/validate/suggest.js +35 -0
- package/dist/commands/validate/types.js +6 -0
- package/dist/commands/validate/validateConfig.js +457 -0
- package/dist/index.js +99 -50
- package/dist/types/common.js +3 -27
- package/dist/utils.js +309 -128
- package/dist/version.js +1 -1
- package/package.json +8 -10
- package/dist/commands/make-pif/file-utils.js +0 -30
- package/dist/commands/track-utils.js +0 -74
- package/dist/util.js +0 -35
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { parseArgs } from 'util';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { validateFileArgument, validateRequiredCommands, } from
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { printHelp } from '../../utils.js';
|
|
3
|
+
import { describeExit, waitForProcessClose } from '../process-utils.js';
|
|
4
|
+
import { validateFileArgument, validateRequiredCommands, } from '../shared/validators.js';
|
|
5
|
+
import { DEFAULT_COARSE_SPLIT_GAP, createPIF, getOutputFilename, missingPairs, spawnSortProcess, } from './pif-generator.js';
|
|
5
6
|
export async function run(args) {
|
|
6
7
|
const options = {
|
|
7
8
|
help: {
|
|
@@ -16,6 +17,14 @@ export async function run(args) {
|
|
|
16
17
|
type: 'boolean',
|
|
17
18
|
description: 'Create a CSI index for the PIF file instead of TBI',
|
|
18
19
|
},
|
|
20
|
+
coarse: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Minimum insertion/deletion length (bp) at which a coarse-tier row is split into multiple pieces so each row stays tight — 0 emits an unsplit coarse tier. Defaults to 10000. The no-CIGAR coarse tier (prefix T/Q) is emitted alongside the per-row CIGAR fine tier by default so whole-genome synteny views can auto-switch to it; pass --no-coarse to omit it.',
|
|
23
|
+
},
|
|
24
|
+
'no-coarse': {
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
description: 'Do not emit the coarse no-CIGAR tier; write only the per-row CIGAR fine tier.',
|
|
27
|
+
},
|
|
19
28
|
};
|
|
20
29
|
const { values: flags, positionals } = parseArgs({
|
|
21
30
|
args,
|
|
@@ -24,14 +33,30 @@ export async function run(args) {
|
|
|
24
33
|
});
|
|
25
34
|
const description = 'creates pairwise indexed PAF (PIF), with bgzip and tabix';
|
|
26
35
|
const examples = [
|
|
27
|
-
'
|
|
36
|
+
'# creates input.pif.gz and input.pif.gz.tbi in the same directory',
|
|
37
|
+
'$ jbrowse make-pif input.paf',
|
|
38
|
+
'',
|
|
39
|
+
'# specify the output file, also creates output.pif.gz.tbi',
|
|
40
|
+
'$ jbrowse make-pif input.paf --out output.pif.gz',
|
|
41
|
+
'',
|
|
42
|
+
'# use a CSI index for assemblies with chromosomes longer than ~512 Mb',
|
|
43
|
+
'$ jbrowse make-pif input.paf --csi',
|
|
44
|
+
'',
|
|
45
|
+
'# emit an unsplit coarse tier (alongside the fine tier)',
|
|
46
|
+
'$ jbrowse make-pif input.paf --coarse 0',
|
|
28
47
|
'',
|
|
29
|
-
'
|
|
48
|
+
'# emit only the per-row CIGAR fine tier, skipping the coarse tier',
|
|
49
|
+
'$ jbrowse make-pif input.paf --no-coarse',
|
|
30
50
|
];
|
|
51
|
+
const notes = 'Use --csi for assemblies containing sequences longer than ~512 Mb. The ' +
|
|
52
|
+
'default TBI index cannot address coordinates beyond 2^29 (~536 Mb), so a ' +
|
|
53
|
+
'CSI index is required for large chromosomes (e.g. some plant and ' +
|
|
54
|
+
'amphibian genomes). Requires sh, sort, bgzip, and tabix on the PATH.';
|
|
31
55
|
if (flags.help) {
|
|
32
56
|
printHelp({
|
|
33
57
|
description,
|
|
34
58
|
examples,
|
|
59
|
+
notes,
|
|
35
60
|
usage: 'jbrowse make-pif <file> [options]',
|
|
36
61
|
options,
|
|
37
62
|
});
|
|
@@ -39,11 +64,73 @@ export async function run(args) {
|
|
|
39
64
|
}
|
|
40
65
|
const file = positionals[0];
|
|
41
66
|
validateFileArgument(file, 'make-pif', 'paf');
|
|
42
|
-
validateRequiredCommands(['sh', 'sort', '
|
|
43
|
-
const { out, csi = false } = flags;
|
|
67
|
+
validateRequiredCommands(['sh', 'sort', 'tabix', 'bgzip']);
|
|
68
|
+
const { out, csi = false, coarse, 'no-coarse': noCoarse = false } = flags;
|
|
44
69
|
const outputFile = getOutputFilename(file, out);
|
|
70
|
+
// --no-coarse used to silently win over an explicit --coarse, so a run asking
|
|
71
|
+
// for both wrote a file with no coarse tier and said nothing about it
|
|
72
|
+
if (noCoarse && coarse !== undefined) {
|
|
73
|
+
throw new Error('--coarse and --no-coarse are mutually exclusive');
|
|
74
|
+
}
|
|
75
|
+
const coarseSplitGap = noCoarse
|
|
76
|
+
? undefined
|
|
77
|
+
: coarse === undefined
|
|
78
|
+
? DEFAULT_COARSE_SPLIT_GAP
|
|
79
|
+
: +coarse;
|
|
80
|
+
if (coarseSplitGap !== undefined &&
|
|
81
|
+
(!Number.isFinite(coarseSplitGap) || coarseSplitGap < 0)) {
|
|
82
|
+
throw new Error(`Invalid --coarse value: ${coarse}`);
|
|
83
|
+
}
|
|
45
84
|
const child = spawnSortProcess(outputFile, csi);
|
|
46
|
-
|
|
47
|
-
child
|
|
48
|
-
|
|
85
|
+
const stdin = child.stdin;
|
|
86
|
+
// end stdin even if createPIF throws, otherwise the spawned sort/index child
|
|
87
|
+
// is left running with an open stdin
|
|
88
|
+
const stats = await createPIF(file, stdin, coarseSplitGap).finally(() => {
|
|
89
|
+
stdin.end();
|
|
90
|
+
});
|
|
91
|
+
const { samples, rows, skipped } = stats;
|
|
92
|
+
// no SIGPIPE exemption here (unlike sort-gff/sort-bed): this pipeline's output
|
|
93
|
+
// is the .pif.gz file, so anything that killed it early left that file broken
|
|
94
|
+
const exit = await waitForProcessClose(child);
|
|
95
|
+
if (exit.code !== 0) {
|
|
96
|
+
throw new Error(`PIF sort/index pipeline exited with ${describeExit(exit)}`);
|
|
97
|
+
}
|
|
98
|
+
// A file that yielded nothing is almost always the wrong file, not an empty
|
|
99
|
+
// alignment: without this the command wrote a valid, indexed, empty PIF and
|
|
100
|
+
// printed the add-track suggestion for it
|
|
101
|
+
if (rows === 0) {
|
|
102
|
+
throw new Error(`No valid PAF rows found in ${file ?? 'stdin'} (${skipped} line(s) had fewer than the 12 mandatory PAF columns). Is this a PAF file?`);
|
|
103
|
+
}
|
|
104
|
+
if (skipped > 0) {
|
|
105
|
+
console.warn(`Warning: skipped ${skipped} of ${rows + skipped} line(s) with fewer than the 12 mandatory PAF columns`);
|
|
106
|
+
}
|
|
107
|
+
// An all-vs-all PAF that does not state every pair indexes perfectly well and
|
|
108
|
+
// then draws an empty synteny band for each pair the aligner never emitted,
|
|
109
|
+
// which reads as "no homology here" rather than as "never computed". This is
|
|
110
|
+
// the moment someone is looking at the file, and the census it needs was
|
|
111
|
+
// already taken on the pass above.
|
|
112
|
+
const missing = missingPairs(stats);
|
|
113
|
+
if (missing.length > 0) {
|
|
114
|
+
const shown = missing
|
|
115
|
+
.slice(0, 5)
|
|
116
|
+
.map(([a, b]) => `${a}<->${b}`)
|
|
117
|
+
.join(', ');
|
|
118
|
+
const rest = missing.length - Math.min(missing.length, 5);
|
|
119
|
+
console.warn(`Warning: this file has ${samples.size} samples but states only ` +
|
|
120
|
+
`${(samples.size * (samples.size - 1)) / 2 - missing.length} of their ` +
|
|
121
|
+
`${(samples.size * (samples.size - 1)) / 2} pairs. Synteny bands for the ` +
|
|
122
|
+
`missing pairs (${shown}${rest > 0 ? `, and ${rest} more` : ''}) will draw ` +
|
|
123
|
+
`empty. That is what a reference-anchored alignment looks like — only ` +
|
|
124
|
+
`pairs involving the reference are stated. Order the synteny rows so the ` +
|
|
125
|
+
`reference sits between the others, use a complete all-vs-all if the ` +
|
|
126
|
+
`project publishes one, or view a larger cohort as a multiple alignment ` +
|
|
127
|
+
`(MAF) rather than a stack of pairwise bands.`);
|
|
128
|
+
}
|
|
129
|
+
const indexFile = `${outputFile}.${csi ? 'csi' : 'tbi'}`;
|
|
130
|
+
const nextCommand = samples.size > 0
|
|
131
|
+
? 'Next, add it as an all-vs-all synteny track (PanSN names detected):\n' +
|
|
132
|
+
` jbrowse add-track ${outputFile} --adapterType AllVsAllIndexedPAFAdapter -a ${[...samples].sort().join(',')} --load copy`
|
|
133
|
+
: 'Next, add it as a synteny track (set -a to your query,target assembly names):\n' +
|
|
134
|
+
` jbrowse add-track ${outputFile} -a query,target --load copy`;
|
|
135
|
+
console.log(`Created ${outputFile} and ${indexFile}\n\n${nextCommand}`);
|
|
49
136
|
}
|
|
@@ -1,55 +1,289 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Transform } from 'node:stream';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
import { createGunzip } from 'node:zlib';
|
|
7
|
+
import { csToCigar, flipCigar, pafIdentity, swapIndelCigar, } from '@jbrowse/cigar-utils';
|
|
8
|
+
import { splitCigarOnLargeGaps } from './cigar-utils.js';
|
|
9
|
+
// Default split gap (bp) for the coarse tier. A row is broken into multiple
|
|
10
|
+
// coarse pieces wherever a CIGAR insertion/deletion is at least this long, so
|
|
11
|
+
// each coarse row's bounding box stays tight. 10kb matches the adapter's
|
|
12
|
+
// default coarseBpPerPxThreshold (~1px at the zoom where the coarse tier is
|
|
13
|
+
// served), so smaller gaps that would be sub-pixel there don't fragment rows.
|
|
14
|
+
export const DEFAULT_COARSE_SPLIT_GAP = 10_000;
|
|
15
|
+
function panSNSample(refName) {
|
|
16
|
+
const i = refName.indexOf('#');
|
|
17
|
+
return i === -1 ? undefined : refName.slice(0, i);
|
|
18
|
+
}
|
|
19
|
+
function addPanSNPair(stats, c1, c2) {
|
|
20
|
+
const a = panSNSample(c1);
|
|
21
|
+
const b = panSNSample(c2);
|
|
22
|
+
if (a !== undefined) {
|
|
23
|
+
stats.samples.add(a);
|
|
24
|
+
}
|
|
25
|
+
if (b !== undefined) {
|
|
26
|
+
stats.samples.add(b);
|
|
27
|
+
}
|
|
28
|
+
if (a !== undefined && b !== undefined && a !== b) {
|
|
29
|
+
stats.pairs.add(a < b ? `${a}\u0000${b}` : `${b}\u0000${a}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The sample pairs a complete all-vs-all would state but this file does not.
|
|
34
|
+
* Empty for a pairwise PAF (no PanSN names) and for a genuinely complete file.
|
|
35
|
+
*/
|
|
36
|
+
export function missingPairs({ samples, pairs }) {
|
|
37
|
+
const sorted = [...samples].sort();
|
|
38
|
+
const out = [];
|
|
39
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
40
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
41
|
+
const a = sorted[i];
|
|
42
|
+
const b = sorted[j];
|
|
43
|
+
if (!pairs.has(`${a}\u0000${b}`)) {
|
|
44
|
+
out.push([a, b]);
|
|
20
45
|
}
|
|
21
|
-
// Write the second line and handle backpressure
|
|
22
|
-
await writeWithBackpressure(`${[`q${c1}`, l1, s1, e1, strand, c2, l2, s2, e2, ...rest].join('\t')}\n`);
|
|
23
46
|
}
|
|
24
47
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function coarsePieces({ cigar, strand, tstart, tend, qstart, qend, numMatches, blockLen, splitGap, }) {
|
|
51
|
+
const whole = { tstart, tend, qstart, qend, numMatches, blockLen };
|
|
52
|
+
const split = cigar && splitGap > 0
|
|
53
|
+
? splitCigarOnLargeGaps({
|
|
54
|
+
cigar,
|
|
55
|
+
strand,
|
|
56
|
+
tstart,
|
|
57
|
+
tend,
|
|
58
|
+
qstart,
|
|
59
|
+
qend,
|
|
60
|
+
splitGap,
|
|
61
|
+
})
|
|
62
|
+
: undefined;
|
|
63
|
+
// The walk's reconstruction is taken only when it CLOSED on the row's own far
|
|
64
|
+
// corner (see `CigarSplit.closed`) — otherwise the columns are what the fine
|
|
65
|
+
// tier draws and the coarse row must not drift off them.
|
|
66
|
+
//
|
|
67
|
+
// Closure rather than a piece count: `segments.length < 2` read as "nothing to
|
|
68
|
+
// split on", but a large gap at either END also yields a single piece, a
|
|
69
|
+
// genuine tightening that was being thrown away for the loose box. It is the
|
|
70
|
+
// ordinary case below the default `--coarse`. The count also never noticed a
|
|
71
|
+
// MULTI-piece walk that failed to close, which is the case it was written for.
|
|
72
|
+
return split?.closed && split.segments.length > 0
|
|
73
|
+
? split.segments.map(seg => ({
|
|
74
|
+
...seg,
|
|
75
|
+
numMatches: blockLen > 0 ? Math.round((numMatches * seg.blockLen) / blockLen) : 0,
|
|
76
|
+
}))
|
|
77
|
+
: [whole];
|
|
78
|
+
}
|
|
79
|
+
// A PIF row carries exactly ONE alignment string — `cg:Z:`, in the orientation
|
|
80
|
+
// of the perspective it is indexed under. A minimap2 `cs:Z:` is folded into that
|
|
81
|
+
// CIGAR and never emitted, for two reasons:
|
|
82
|
+
//
|
|
83
|
+
// - `cs` has its own orientation, and reorienting it means reversing op order
|
|
84
|
+
// AND reverse-complementing the spelled-out bases. Keeping an unflipped `cs`
|
|
85
|
+
// beside a flipped `cg` is silently wrong rather than merely lossy:
|
|
86
|
+
// `SyntenyFeature.forEachMismatch` prefers `cs`, so a row from
|
|
87
|
+
// `minimap2 -c --cs` (which emits both tags) drew every indel with reversed
|
|
88
|
+
// sense from the query perspective.
|
|
89
|
+
// - `csToCigar` yields `=`/`X`, so folding a `cs` in is strictly MORE
|
|
90
|
+
// informative than minimap2's own M-style `cg` — which is why `cs` wins when
|
|
91
|
+
// a row carries both. The substituted base letters are the only thing lost;
|
|
92
|
+
// mismatch positions survive as `X`.
|
|
93
|
+
//
|
|
94
|
+
// Returns the rewritten optional-tag list and where the surviving alignment
|
|
95
|
+
// string sits in it (-1 when the row carries none).
|
|
96
|
+
function foldCsIntoCg(tags) {
|
|
97
|
+
const csIdx = tags.findIndex(f => f.startsWith('cs:Z'));
|
|
98
|
+
if (csIdx === -1) {
|
|
99
|
+
return { tags, cigarIdx: tags.findIndex(f => f.startsWith('cg:Z')) };
|
|
100
|
+
}
|
|
101
|
+
const folded = `cg:Z:${csToCigar(tags[csIdx].slice(5))}`;
|
|
102
|
+
const rewritten = tags.flatMap((f, i) => i === csIdx ? [folded] : f.startsWith('cg:Z') ? [] : [f]);
|
|
103
|
+
return { tags: rewritten, cigarIdx: rewritten.indexOf(folded) };
|
|
104
|
+
}
|
|
105
|
+
// Coarse identity must match the fine tier exactly, or coloring jumps at the
|
|
106
|
+
// zoom where the view switches tiers. So it is written off the SAME function the
|
|
107
|
+
// adapters read the fine tier with (de:f: tag, then odgi's id:f:, then
|
|
108
|
+
// num_matches/block_len) rather than a private copy of that chain — a copy is
|
|
109
|
+
// what previously skipped the id:f: rung. A CIGAR recompute is deliberately not
|
|
110
|
+
// in the chain at all: a cg (M-style) CIGAR folds mismatches into M, so it would
|
|
111
|
+
// report ~0 divergence (spurious 100% identity) for a divergent alignment.
|
|
112
|
+
//
|
|
113
|
+
// When the row's own de:f: is what the reader lands on, that string is passed
|
|
114
|
+
// through byte-for-byte rather than recomputed: a round trip through toFixed(6)
|
|
115
|
+
// would silently truncate a 7-decimal tag and drift the two tiers apart by the
|
|
116
|
+
// exact mechanism this is guarding against.
|
|
117
|
+
function coarseDivergence(tags, numMatches, blockLen) {
|
|
118
|
+
const rawDe = tags.find(f => f.startsWith('de:f:'))?.slice(5);
|
|
119
|
+
const identity = pafIdentity({
|
|
120
|
+
de: rawDe,
|
|
121
|
+
id: tags.find(f => f.startsWith('id:f:'))?.slice(5),
|
|
122
|
+
numMatches,
|
|
123
|
+
blockLen,
|
|
124
|
+
});
|
|
125
|
+
return rawDe !== undefined && 1 - +rawDe === identity
|
|
126
|
+
? rawDe
|
|
127
|
+
: (1 - identity).toFixed(6);
|
|
128
|
+
}
|
|
129
|
+
function pifRow(fields) {
|
|
130
|
+
return `${fields.join('\t')}\n`;
|
|
131
|
+
}
|
|
132
|
+
function processLine(line, coarseSplitGap, stats) {
|
|
133
|
+
if (line.startsWith('#')) {
|
|
134
|
+
return '';
|
|
28
135
|
}
|
|
29
|
-
|
|
30
|
-
|
|
136
|
+
const parts = line.split('\t');
|
|
137
|
+
// A valid PAF row has 12 mandatory columns; anything shorter (blank, or
|
|
138
|
+
// truncated) would produce NaN coords, so skip it rather than emit garbage.
|
|
139
|
+
if (parts.length < 12) {
|
|
140
|
+
stats.skipped++;
|
|
141
|
+
return '';
|
|
31
142
|
}
|
|
143
|
+
stats.rows++;
|
|
144
|
+
// rest[0]=num_matches, rest[1]=block_len, rest[2]=mapq, rest[3+]=optional tags
|
|
145
|
+
const [c1, l1, s1, e1, strand, c2, l2, s2, e2, ...rest] = parts;
|
|
146
|
+
addPanSNPair(stats, c1, c2);
|
|
147
|
+
const { tags, cigarIdx } = foldCsIntoCg(rest);
|
|
148
|
+
const cigar = cigarIdx === -1 ? undefined : tags[cigarIdx].slice(5);
|
|
149
|
+
// the t-row keeps the CIGAR as PAF spelled it (target perspective); the q-row
|
|
150
|
+
// re-orients it for the query perspective it is indexed under
|
|
151
|
+
const queryTags = cigar === undefined
|
|
152
|
+
? tags
|
|
153
|
+
: tags.map((f, i) => i === cigarIdx
|
|
154
|
+
? `cg:Z:${strand === '-' ? flipCigar(cigar) : swapIndelCigar(cigar)}`
|
|
155
|
+
: f);
|
|
156
|
+
const fineRows = pifRow([`t${c2}`, l2, s2, e2, strand, c1, l1, s1, e1, ...tags]) +
|
|
157
|
+
pifRow([`q${c1}`, l1, s1, e1, strand, c2, l2, s2, e2, ...queryTags]);
|
|
158
|
+
if (coarseSplitGap === undefined) {
|
|
159
|
+
return fineRows;
|
|
160
|
+
}
|
|
161
|
+
const numMatches = +tags[0];
|
|
162
|
+
const blockLen = +tags[1];
|
|
163
|
+
const mapq = tags[2];
|
|
164
|
+
// Every optional tag except the alignment string itself rides along, so a
|
|
165
|
+
// click on a coarse ribbon shows the same attributes as the same alignment
|
|
166
|
+
// does zoomed in (rustybam's tags, minimap2's tp/cm/s1, odgi's id). Only the
|
|
167
|
+
// CIGAR is dropped — dropping it is the entire point of the tier — and de:f:,
|
|
168
|
+
// which is rewritten below. No `cs:Z:` can reach here: it was folded into the
|
|
169
|
+
// cg above, since a PIF row carries one alignment string.
|
|
170
|
+
const passthrough = tags
|
|
171
|
+
.slice(3)
|
|
172
|
+
.filter(f => !f.startsWith('cg:Z:') && !f.startsWith('de:f:'));
|
|
173
|
+
const de = coarseDivergence(tags, numMatches, blockLen);
|
|
174
|
+
return (fineRows +
|
|
175
|
+
coarsePieces({
|
|
176
|
+
cigar,
|
|
177
|
+
strand: strand,
|
|
178
|
+
tstart: +s2,
|
|
179
|
+
tend: +e2,
|
|
180
|
+
qstart: +s1,
|
|
181
|
+
qend: +e1,
|
|
182
|
+
numMatches,
|
|
183
|
+
blockLen,
|
|
184
|
+
splitGap: coarseSplitGap,
|
|
185
|
+
})
|
|
186
|
+
.flatMap(piece => {
|
|
187
|
+
const tail = [
|
|
188
|
+
piece.numMatches,
|
|
189
|
+
piece.blockLen,
|
|
190
|
+
mapq,
|
|
191
|
+
...passthrough,
|
|
192
|
+
`de:f:${de}`,
|
|
193
|
+
];
|
|
194
|
+
const { tstart, tend, qstart, qend } = piece;
|
|
195
|
+
return [
|
|
196
|
+
pifRow([
|
|
197
|
+
`T${c2}`,
|
|
198
|
+
l2,
|
|
199
|
+
tstart,
|
|
200
|
+
tend,
|
|
201
|
+
strand,
|
|
202
|
+
c1,
|
|
203
|
+
l1,
|
|
204
|
+
qstart,
|
|
205
|
+
qend,
|
|
206
|
+
...tail,
|
|
207
|
+
]),
|
|
208
|
+
pifRow([
|
|
209
|
+
`Q${c1}`,
|
|
210
|
+
l1,
|
|
211
|
+
qstart,
|
|
212
|
+
qend,
|
|
213
|
+
strand,
|
|
214
|
+
c2,
|
|
215
|
+
l2,
|
|
216
|
+
tstart,
|
|
217
|
+
tend,
|
|
218
|
+
...tail,
|
|
219
|
+
]),
|
|
220
|
+
];
|
|
221
|
+
})
|
|
222
|
+
.join(''));
|
|
223
|
+
}
|
|
224
|
+
function makePifTransform(coarseSplitGap, stats) {
|
|
225
|
+
let tail = '';
|
|
226
|
+
return new Transform({
|
|
227
|
+
transform(chunk, _enc, callback) {
|
|
228
|
+
const data = tail + chunk.toString('utf8');
|
|
229
|
+
const lastNl = data.lastIndexOf('\n');
|
|
230
|
+
if (lastNl === -1) {
|
|
231
|
+
tail = data;
|
|
232
|
+
callback();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
tail = data.slice(lastNl + 1);
|
|
236
|
+
callback(null, data
|
|
237
|
+
.slice(0, lastNl)
|
|
238
|
+
.split('\n')
|
|
239
|
+
.filter(Boolean)
|
|
240
|
+
.map(l => processLine(l, coarseSplitGap, stats))
|
|
241
|
+
.join(''));
|
|
242
|
+
},
|
|
243
|
+
flush(callback) {
|
|
244
|
+
callback(null, tail ? processLine(tail, coarseSplitGap, stats) : '');
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
// resolves to what the pass observed: the PanSN samples (empty for a pairwise
|
|
249
|
+
// PAF), so the caller can suggest the right adapter and assembly names, and the
|
|
250
|
+
// row/skip counts so it can warn on a file that was not PAF at all
|
|
251
|
+
export async function createPIF(filename, stream, coarseSplitGap) {
|
|
252
|
+
const stats = {
|
|
253
|
+
samples: new Set(),
|
|
254
|
+
pairs: new Set(),
|
|
255
|
+
rows: 0,
|
|
256
|
+
skipped: 0,
|
|
257
|
+
};
|
|
258
|
+
const transform = makePifTransform(coarseSplitGap, stats);
|
|
259
|
+
if (filename) {
|
|
260
|
+
const source = createReadStream(filename);
|
|
261
|
+
await (/\.b?gz$/i.test(filename)
|
|
262
|
+
? pipeline(source, createGunzip(), transform, stream)
|
|
263
|
+
: pipeline(source, transform, stream));
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
await pipeline(process.stdin, transform, stream);
|
|
267
|
+
}
|
|
268
|
+
return stats;
|
|
32
269
|
}
|
|
33
270
|
export function spawnSortProcess(outputFile, useCsi) {
|
|
34
|
-
// Use a more portable approach to avoid E2BIG errors
|
|
35
271
|
const sortCmd = `sort -t"\`printf '\\t'\`" -k1,1 -k3,3n`;
|
|
36
|
-
const bgzipCommand = `bgzip > "$
|
|
37
|
-
const tabixCommand = `tabix ${useCsi ? '-C ' : ''}-s1 -b3 -e4 -0 "$
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
272
|
+
const bgzipCommand = `bgzip > "$1"`;
|
|
273
|
+
const tabixCommand = `tabix ${useCsi ? '-C ' : ''}-s1 -b3 -e4 -0 "$1"`;
|
|
274
|
+
// `&&` (not `;`) so a bgzip failure aborts before tabix runs and propagates
|
|
275
|
+
// as the pipeline's exit code. The output path is passed as the shell's "$1"
|
|
276
|
+
// positional rather than interpolated into the command string, so a path
|
|
277
|
+
// with shell metacharacters (`"`, `$(...)`, backticks) can't break out and
|
|
278
|
+
// execute — same technique as sort-utils.ts. useCsi is a fixed literal.
|
|
279
|
+
const fullCommand = `${sortCmd} | ${bgzipCommand} && ${tabixCommand}`;
|
|
280
|
+
return spawn('sh', ['-c', fullCommand, 'sh', outputFile], {
|
|
281
|
+
env: { ...process.env, LC_ALL: 'C' },
|
|
45
282
|
stdio: ['pipe', process.stdout, process.stderr],
|
|
46
283
|
});
|
|
47
284
|
}
|
|
48
285
|
export function getOutputFilename(file, out) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return new Promise(resolve => {
|
|
53
|
-
child.on('close', resolve);
|
|
54
|
-
});
|
|
286
|
+
// strip .paf or .paf.gz so a gzipped input doesn't yield input.paf.gz.pif.gz
|
|
287
|
+
const base = path.basename(file || 'output').replace(/\.paf(\.gz)?$/i, '');
|
|
288
|
+
return out || `${base}.pif.gz`;
|
|
55
289
|
}
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
export async function waitForProcessClose(child) {
|
|
2
2
|
return new Promise((resolve, reject) => {
|
|
3
|
-
child.on('close', code => {
|
|
4
|
-
resolve(code);
|
|
3
|
+
child.on('close', (code, signal) => {
|
|
4
|
+
resolve({ code, signal });
|
|
5
5
|
});
|
|
6
6
|
child.on('error', err => {
|
|
7
7
|
reject(err);
|
|
8
8
|
});
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
|
+
// A downstream consumer closing the pipe early — `jbrowse sort-bed big.bed |
|
|
12
|
+
// head` — is that consumer's choice, not a failure of ours. `sh` reports a
|
|
13
|
+
// pipeline member's death by SIGPIPE as 128+13 rather than propagating the
|
|
14
|
+
// signal, so both spellings have to be recognized.
|
|
15
|
+
export function diedOfSigpipe({ code, signal }) {
|
|
16
|
+
return signal === 'SIGPIPE' || code === 128 + 13;
|
|
17
|
+
}
|
|
18
|
+
// for an error message: the exit code, or the signal when the child was killed
|
|
19
|
+
export function describeExit({ code, signal }) {
|
|
20
|
+
return code === null ? `signal ${signal}` : `code ${code}`;
|
|
21
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { parseArgs } from 'util';
|
|
2
|
-
import { printHelp,
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { printHelp, readConfigFile, requirePositional, resolveConfigPath, writeJsonFile, } from '../utils.js';
|
|
3
3
|
export async function run(args) {
|
|
4
4
|
const options = {
|
|
5
5
|
help: {
|
|
@@ -21,30 +21,37 @@ export async function run(args) {
|
|
|
21
21
|
allowPositionals: true,
|
|
22
22
|
});
|
|
23
23
|
const description = 'Remove a track configuration from a JBrowse 2 configuration. Be aware that this can cause crashes in saved sessions that refer to this track!';
|
|
24
|
-
const
|
|
24
|
+
const usage = 'jbrowse remove-track <trackId> [options]';
|
|
25
|
+
const examples = [
|
|
26
|
+
'# remove a track from the config.json in the current directory',
|
|
27
|
+
'$ jbrowse remove-track my_track_id',
|
|
28
|
+
'',
|
|
29
|
+
'# remove a track from a config.json in a specific installation directory',
|
|
30
|
+
'$ jbrowse remove-track my_track_id --out /path/to/jb2/',
|
|
31
|
+
'',
|
|
32
|
+
'# remove a track from a specific config file',
|
|
33
|
+
'$ jbrowse remove-track my_track_id --target /path/to/jb2/config.json',
|
|
34
|
+
];
|
|
25
35
|
if (flags.help) {
|
|
26
36
|
printHelp({
|
|
27
37
|
description,
|
|
28
38
|
examples,
|
|
29
|
-
usage
|
|
39
|
+
usage,
|
|
30
40
|
options,
|
|
31
41
|
});
|
|
32
42
|
return;
|
|
33
43
|
}
|
|
34
44
|
const trackId = positionals[0];
|
|
35
|
-
|
|
36
|
-
throw new Error('Missing required argument: trackId\nUsage: jbrowse remove-track <trackId> [options]');
|
|
37
|
-
}
|
|
45
|
+
requirePositional(trackId, 'trackId', usage);
|
|
38
46
|
const target = await resolveConfigPath(flags.target, flags.out);
|
|
39
|
-
const config = await
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (originalLength === newLength) {
|
|
47
|
+
const config = await readConfigFile(target);
|
|
48
|
+
const tracks = config.tracks ?? [];
|
|
49
|
+
const remaining = tracks.filter(track => track.trackId !== trackId);
|
|
50
|
+
if (remaining.length === tracks.length) {
|
|
44
51
|
console.log(`No track found with trackId: ${trackId}`);
|
|
45
52
|
}
|
|
46
53
|
else {
|
|
47
|
-
await writeJsonFile(target, config);
|
|
54
|
+
await writeJsonFile(target, { ...config, tracks: remaining });
|
|
48
55
|
console.log(`Removed track with trackId: ${trackId} from ${target}`);
|
|
49
56
|
}
|
|
50
57
|
}
|