@jbrowse/cli 5.0.0-beta.1 → 5.0.0-beta.11
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 +149 -44
- package/dist/commands/add-track-utils/adapter-utils.js +47 -39
- package/dist/commands/add-track-utils/track-config.js +5 -7
- package/dist/commands/add-track.js +31 -9
- package/dist/commands/admin-server/index.js +13 -5
- package/dist/commands/admin-server/utils.js +8 -6
- package/dist/commands/make-density/density-generator.js +180 -0
- package/dist/commands/make-density/index.js +139 -0
- package/dist/commands/make-pif/index.js +62 -27
- package/dist/commands/make-pif/pif-generator.js +98 -132
- package/dist/commands/shared/density.js +12 -0
- package/dist/commands/text-index/adapter-utils.js +17 -7
- package/dist/commands/text-index/aggregate.js +5 -5
- package/dist/commands/text-index/command.js +3 -1
- package/dist/commands/text-index/config-utils.js +7 -7
- package/dist/commands/text-index/indexing-utils.js +21 -49
- package/dist/commands/validate/configManifest.generated.js +4071 -1845
- package/dist/commands/validate/configSchema.generated.js +20482 -0
- package/dist/commands/validate/index.js +21 -5
- package/dist/commands/validate/liftConfig.js +34 -0
- package/dist/commands/validate/markRules/aggregateFieldName.js +9 -0
- package/dist/commands/validate/markRules/autoBin.js +27 -0
- package/dist/commands/validate/markRules/colorScale.js +244 -0
- package/dist/commands/validate/markRules/cssColorsLevel4.js +162 -0
- package/dist/commands/validate/markRules/jexlStrings.js +23 -0
- package/dist/commands/validate/markRules/markProblems.js +573 -0
- package/dist/commands/validate/markRules/markRuleFacts.js +6 -0
- package/dist/commands/validate/markRules/markSpecs.js +84 -0
- package/dist/commands/validate/markRules/markVocabulary.js +59 -0
- package/dist/commands/validate/markRules/shapeNameList.js +7 -0
- package/dist/commands/validate/markRules/stepChannels.js +55 -0
- package/dist/commands/validate/schemaValidate.js +425 -0
- package/dist/commands/validate/validateConfig.js +317 -258
- package/dist/index.js +5 -0
- package/dist/version.js +1 -1
- package/package.json +6 -10
- package/bin/run +0 -4
- package/bundle/index.js +0 -1569
- package/dist/commands/make-pif/cigar-utils.js +0 -67
|
@@ -4,14 +4,33 @@ import path from 'node:path';
|
|
|
4
4
|
import { Transform } from 'node:stream';
|
|
5
5
|
import { pipeline } from 'node:stream/promises';
|
|
6
6
|
import { createGunzip } from 'node:zlib';
|
|
7
|
-
import { csToCigar, flipCigar,
|
|
8
|
-
import {
|
|
9
|
-
// Default
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// default coarseBpPerPxThreshold
|
|
13
|
-
// served
|
|
14
|
-
export const
|
|
7
|
+
import { coarsenCigar, csToCigar, flipCigar, flipCoarseCigar, swapCoarseCigar, swapIndelCigar, } from '@jbrowse/cigar-utils';
|
|
8
|
+
import { version } from '../../version.js';
|
|
9
|
+
// Default gap (bp) of the coarse tier's coarse CIGAR: how far a straight line
|
|
10
|
+
// across one of its runs may be from the alignment's real path. Indels longer
|
|
11
|
+
// than half of it keep their letter; shorter ones fold into the runs. 10kb
|
|
12
|
+
// matches the adapter's default coarseBpPerPxThreshold, so at the zoom where the
|
|
13
|
+
// tier is served the bound is ~1px and a folded indel was sub-pixel anyway.
|
|
14
|
+
export const DEFAULT_COARSE_GAP = 10_000;
|
|
15
|
+
// Generation 2 (2026-09): every row carries `pi:i:`, the input row's index,
|
|
16
|
+
// and the header names the writer.
|
|
17
|
+
export const PIF_FORMAT_VERSION = 2;
|
|
18
|
+
/**
|
|
19
|
+
* The one meta line a PIF carries, sorted first by the C-locale sort and kept
|
|
20
|
+
* by tabix as a header. It states the format generation, the writer, the
|
|
21
|
+
* tiers written, the coarse tier's accuracy bound (`--coarse`), and whether
|
|
22
|
+
* every input row had a CIGAR — the facts a reader cannot recover from the rows.
|
|
23
|
+
*/
|
|
24
|
+
export function pifHeader(coarseGap, stats) {
|
|
25
|
+
const cigars = stats.cigarRows === 0
|
|
26
|
+
? 'none'
|
|
27
|
+
: stats.cigarRows === stats.rows
|
|
28
|
+
? 'all'
|
|
29
|
+
: 'some';
|
|
30
|
+
const tiers = coarseGap === undefined ? 'fine' : 'fine,coarse';
|
|
31
|
+
const bound = coarseGap === undefined ? '' : `\tcoarse:i:${coarseGap}`;
|
|
32
|
+
return `#pif\tversion:i:${PIF_FORMAT_VERSION}\twriter:Z:jbrowse-cli/${version}\ttiers:Z:${tiers}${bound}\tcigars:Z:${cigars}\n`;
|
|
33
|
+
}
|
|
15
34
|
function panSNSample(refName) {
|
|
16
35
|
const i = refName.indexOf('#');
|
|
17
36
|
return i === -1 ? undefined : refName.slice(0, i);
|
|
@@ -47,34 +66,23 @@ export function missingPairs({ samples, pairs }) {
|
|
|
47
66
|
}
|
|
48
67
|
return out;
|
|
49
68
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
// The coarse row's `cr:Z:` value, or nothing when the row has no CIGAR or the
|
|
70
|
+
// fold is a single run that closes on the coordinate columns, which then say it
|
|
71
|
+
// all. A fold of several runs with no kept indel is still written: a lopsided
|
|
72
|
+
// cluster of small indels bends the path by their sum. So is a fold whose walk
|
|
73
|
+
// misses the row's own far corner (clipping ops, a hand-made cg, a cs whose
|
|
74
|
+
// spans don't add up): the fine tier walks that CIGAR from the start corner
|
|
75
|
+
// without checking, and the coarse tier draws the same path only if it walks
|
|
76
|
+
// the same fold.
|
|
77
|
+
function coarseFold({ cigar, coarseGap, ownLen, mateLen, }) {
|
|
78
|
+
if (cigar === undefined) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const coarse = coarsenCigar(cigar, coarseGap);
|
|
82
|
+
const closed = coarse.ownLen === ownLen && coarse.mateLen === mateLen;
|
|
83
|
+
return !closed || coarse.gapCount > 0 || coarse.opCount > 1
|
|
84
|
+
? coarse.ops
|
|
62
85
|
: 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
86
|
}
|
|
79
87
|
// A PIF row carries exactly ONE alignment string — `cg:Z:`, in the orientation
|
|
80
88
|
// of the perspective it is indexed under. A minimap2 `cs:Z:` is folded into that
|
|
@@ -102,34 +110,11 @@ function foldCsIntoCg(tags) {
|
|
|
102
110
|
const rewritten = tags.flatMap((f, i) => i === csIdx ? [folded] : f.startsWith('cg:Z') ? [] : [f]);
|
|
103
111
|
return { tags: rewritten, cigarIdx: rewritten.indexOf(folded) };
|
|
104
112
|
}
|
|
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
113
|
function pifRow(fields) {
|
|
130
114
|
return `${fields.join('\t')}\n`;
|
|
131
115
|
}
|
|
132
|
-
function processLine(
|
|
116
|
+
function processLine(rawLine, coarseGap, stats) {
|
|
117
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
133
118
|
if (line.startsWith('#')) {
|
|
134
119
|
return '';
|
|
135
120
|
}
|
|
@@ -144,8 +129,18 @@ function processLine(line, coarseSplitGap, stats) {
|
|
|
144
129
|
// rest[0]=num_matches, rest[1]=block_len, rest[2]=mapq, rest[3+]=optional tags
|
|
145
130
|
const [c1, l1, s1, e1, strand, c2, l2, s2, e2, ...rest] = parts;
|
|
146
131
|
addPanSNPair(stats, c1, c2);
|
|
147
|
-
|
|
132
|
+
// an incoming cr:Z: or pi:i: (a PIF turned back into PAF, or a tool that
|
|
133
|
+
// adopted the tags) is dropped: the coarse tier writes its own fold below,
|
|
134
|
+
// and every row of this alignment gets this pass's row index as `pi`, the
|
|
135
|
+
// one id its four rows share across perspectives and tiers
|
|
136
|
+
const { tags, cigarIdx } = foldCsIntoCg([
|
|
137
|
+
...rest.filter(f => !f.startsWith('cr:') && !f.startsWith('pi:')),
|
|
138
|
+
`pi:i:${stats.rows - 1}`,
|
|
139
|
+
]);
|
|
148
140
|
const cigar = cigarIdx === -1 ? undefined : tags[cigarIdx].slice(5);
|
|
141
|
+
if (cigar !== undefined) {
|
|
142
|
+
stats.cigarRows++;
|
|
143
|
+
}
|
|
149
144
|
// the t-row keeps the CIGAR as PAF spelled it (target perspective); the q-row
|
|
150
145
|
// re-orients it for the query perspective it is indexed under
|
|
151
146
|
const queryTags = cigar === undefined
|
|
@@ -155,73 +150,30 @@ function processLine(line, coarseSplitGap, stats) {
|
|
|
155
150
|
: f);
|
|
156
151
|
const fineRows = pifRow([`t${c2}`, l2, s2, e2, strand, c1, l1, s1, e1, ...tags]) +
|
|
157
152
|
pifRow([`q${c1}`, l1, s1, e1, strand, c2, l2, s2, e2, ...queryTags]);
|
|
158
|
-
if (
|
|
153
|
+
if (coarseGap === undefined) {
|
|
159
154
|
return fineRows;
|
|
160
155
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
156
|
+
// the coarse row is the fine row with its CIGAR replaced by the fold and
|
|
157
|
+
// every other column and tag verbatim, so a click and the identity coloring
|
|
158
|
+
// read the same on both tiers
|
|
159
|
+
const coarse = cigarIdx === -1 ? tags : tags.filter((_, i) => i !== cigarIdx);
|
|
160
|
+
// the T row's own axis is the target, as the PAF CIGAR is written; the Q row
|
|
161
|
+
// re-orients it for the query, the way the fine tier's cg is
|
|
162
|
+
const cr = coarseFold({
|
|
163
|
+
cigar,
|
|
164
|
+
coarseGap,
|
|
165
|
+
ownLen: +e2 - +s2,
|
|
166
|
+
mateLen: +e1 - +s1,
|
|
167
|
+
});
|
|
168
|
+
const tCr = cr === undefined ? [] : [`cr:Z:${cr}`];
|
|
169
|
+
const qCr = cr === undefined
|
|
170
|
+
? []
|
|
171
|
+
: [`cr:Z:${strand === '-' ? flipCoarseCigar(cr) : swapCoarseCigar(cr)}`];
|
|
174
172
|
return (fineRows +
|
|
175
|
-
|
|
176
|
-
|
|
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(''));
|
|
173
|
+
pifRow([`T${c2}`, l2, s2, e2, strand, c1, l1, s1, e1, ...coarse, ...tCr]) +
|
|
174
|
+
pifRow([`Q${c1}`, l1, s1, e1, strand, c2, l2, s2, e2, ...coarse, ...qCr]));
|
|
223
175
|
}
|
|
224
|
-
function makePifTransform(
|
|
176
|
+
function makePifTransform(coarseGap, stats) {
|
|
225
177
|
let tail = '';
|
|
226
178
|
return new Transform({
|
|
227
179
|
transform(chunk, _enc, callback) {
|
|
@@ -237,25 +189,30 @@ function makePifTransform(coarseSplitGap, stats) {
|
|
|
237
189
|
.slice(0, lastNl)
|
|
238
190
|
.split('\n')
|
|
239
191
|
.filter(Boolean)
|
|
240
|
-
.map(l => processLine(l,
|
|
192
|
+
.map(l => processLine(l, coarseGap, stats))
|
|
241
193
|
.join(''));
|
|
242
194
|
},
|
|
243
195
|
flush(callback) {
|
|
244
|
-
|
|
196
|
+
// the header goes out last and sorts first: the counts it states are
|
|
197
|
+
// only known once every row has been seen, and `#` precedes every tier
|
|
198
|
+
// letter in the C locale
|
|
199
|
+
callback(null, (tail ? processLine(tail, coarseGap, stats) : '') +
|
|
200
|
+
pifHeader(coarseGap, stats));
|
|
245
201
|
},
|
|
246
202
|
});
|
|
247
203
|
}
|
|
248
204
|
// resolves to what the pass observed: the PanSN samples (empty for a pairwise
|
|
249
205
|
// PAF), so the caller can suggest the right adapter and assembly names, and the
|
|
250
206
|
// row/skip counts so it can warn on a file that was not PAF at all
|
|
251
|
-
export async function createPIF(filename, stream,
|
|
207
|
+
export async function createPIF(filename, stream, coarseGap) {
|
|
252
208
|
const stats = {
|
|
253
209
|
samples: new Set(),
|
|
254
210
|
pairs: new Set(),
|
|
255
211
|
rows: 0,
|
|
212
|
+
cigarRows: 0,
|
|
256
213
|
skipped: 0,
|
|
257
214
|
};
|
|
258
|
-
const transform = makePifTransform(
|
|
215
|
+
const transform = makePifTransform(coarseGap, stats);
|
|
259
216
|
if (filename) {
|
|
260
217
|
const source = createReadStream(filename);
|
|
261
218
|
await (/\.b?gz$/i.test(filename)
|
|
@@ -267,15 +224,24 @@ export async function createPIF(filename, stream, coarseSplitGap) {
|
|
|
267
224
|
}
|
|
268
225
|
return stats;
|
|
269
226
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
227
|
+
// bgzip is the only stage of this pipeline that cannot use more than one core on
|
|
228
|
+
// its own: sort parallelizes itself and tabix is I/O bound. 4 is enough to take
|
|
229
|
+
// compression off the critical path on the files this command is pointed at
|
|
230
|
+
// without claiming a whole machine.
|
|
231
|
+
export const DEFAULT_BGZIP_THREADS = 4;
|
|
232
|
+
export function spawnSortProcess(outputFile, useCsi, threads = DEFAULT_BGZIP_THREADS) {
|
|
233
|
+
// -s: tabix wants the rows ordered by (refName, start) and by nothing else,
|
|
234
|
+
// so the whole-line comparison a non-stable sort falls back to on a tie is
|
|
235
|
+
// comparing multi-kb CIGARs to pick an order no reader can observe
|
|
236
|
+
const sortCmd = `sort -s -t"\`printf '\\t'\`" -k1,1 -k3,3n`;
|
|
237
|
+
const bgzipCommand = `bgzip -@ ${threads} > "$1"`;
|
|
273
238
|
const tabixCommand = `tabix ${useCsi ? '-C ' : ''}-s1 -b3 -e4 -0 "$1"`;
|
|
274
239
|
// `&&` (not `;`) so a bgzip failure aborts before tabix runs and propagates
|
|
275
240
|
// as the pipeline's exit code. The output path is passed as the shell's "$1"
|
|
276
241
|
// positional rather than interpolated into the command string, so a path
|
|
277
242
|
// with shell metacharacters (`"`, `$(...)`, backticks) can't break out and
|
|
278
|
-
// execute — same technique as sort-utils.ts. useCsi is a fixed literal
|
|
243
|
+
// execute — same technique as sort-utils.ts. useCsi is a fixed literal, and
|
|
244
|
+
// the caller has already checked threads is a positive integer.
|
|
279
245
|
const fullCommand = `${sortCmd} | ${bgzipCommand} && ${tabixCommand}`;
|
|
280
246
|
return spawn('sh', ['-c', fullCommand, 'sh', outputFile], {
|
|
281
247
|
env: { ...process.env, LC_ALL: 'C' },
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the density sidecar for a data file lives: `<file>.density.bw`, with a
|
|
3
|
+
* `.gz`/`.bgz` stripped first so `genes.gff3.gz` yields `genes.gff3.density.bw`
|
|
4
|
+
* rather than a name carrying the compression of a file it is not compressed
|
|
5
|
+
* like.
|
|
6
|
+
*
|
|
7
|
+
* One rule, two commands: `make-density` writes here by default and `add-track`
|
|
8
|
+
* probes here, so a sidecar built with no `--out` is the one the track picks up.
|
|
9
|
+
*/
|
|
10
|
+
export function densitySidecarPath(file) {
|
|
11
|
+
return `${file.replace(/\.b?gz$/i, '')}.density.bw`;
|
|
12
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export function createTrixAdapter(name, assemblyNames
|
|
1
|
+
import { trixFileUris } from '@jbrowse/text-indexing-core';
|
|
2
|
+
export function createTrixAdapter(name, assemblyNames) {
|
|
3
3
|
const uris = trixFileUris(name);
|
|
4
4
|
return {
|
|
5
5
|
type: 'TrixTextSearchAdapter',
|
|
6
|
-
textSearchAdapterId: `${sanitizeForFilename(name)}-${idSuffix}`,
|
|
7
6
|
ixFilePath: {
|
|
8
7
|
uri: uris.ix,
|
|
9
8
|
locationType: 'UriLocation',
|
|
@@ -12,10 +11,21 @@ export function createTrixAdapter(name, assemblyNames, idSuffix = 'index') {
|
|
|
12
11
|
uri: uris.ixx,
|
|
13
12
|
locationType: 'UriLocation',
|
|
14
13
|
},
|
|
15
|
-
metaFilePath: {
|
|
16
|
-
uri: uris.meta,
|
|
17
|
-
locationType: 'UriLocation',
|
|
18
|
-
},
|
|
19
14
|
assemblyNames,
|
|
20
15
|
};
|
|
21
16
|
}
|
|
17
|
+
// an aggregate entry is the index its files are, so a re-index of the same
|
|
18
|
+
// assembly finds the entry pointing at them however it was written, `.ix`
|
|
19
|
+
// string and `{ uri }` included; an entry with no trix file, a JBrowse 1
|
|
20
|
+
// index, is only ever itself
|
|
21
|
+
export function indexFileOf(entry) {
|
|
22
|
+
if (typeof entry === 'string') {
|
|
23
|
+
return entry;
|
|
24
|
+
}
|
|
25
|
+
const { ixFilePath, uri } = entry;
|
|
26
|
+
return ixFilePath
|
|
27
|
+
? 'uri' in ixFilePath
|
|
28
|
+
? ixFilePath.uri
|
|
29
|
+
: ixFilePath.localPath
|
|
30
|
+
: (uri ?? entry);
|
|
31
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseCommaSeparatedString, writeJsonFile } from '../../utils.js';
|
|
2
|
-
import { createTrixAdapter } from './adapter-utils.js';
|
|
2
|
+
import { createTrixAdapter, indexFileOf } from './adapter-utils.js';
|
|
3
3
|
import { formatDryRun, getAssemblyNames, getTrackConfigs, loadConfigForIndexing, prepareIndexDriverFlags, } from './config-utils.js';
|
|
4
4
|
import { indexDriver } from './indexing-utils.js';
|
|
5
5
|
export async function aggregateIndex(flags) {
|
|
@@ -22,7 +22,7 @@ export async function aggregateIndex(flags) {
|
|
|
22
22
|
}
|
|
23
23
|
const trixConf = createTrixAdapter(asm, [asm]);
|
|
24
24
|
if (!force &&
|
|
25
|
-
existing.some(x => x
|
|
25
|
+
existing.some(x => indexFileOf(x) === indexFileOf(trixConf))) {
|
|
26
26
|
console.log(`Note: ${asm} has already been indexed with this configuration, use --force to overwrite this assembly. Skipping for now`);
|
|
27
27
|
continue;
|
|
28
28
|
}
|
|
@@ -44,11 +44,11 @@ export async function aggregateIndex(flags) {
|
|
|
44
44
|
if (!dryrun) {
|
|
45
45
|
await writeJsonFile(configPath, {
|
|
46
46
|
...config,
|
|
47
|
-
// upsert by
|
|
47
|
+
// upsert by index file: a Map keyed on it keeps the first occurrence's
|
|
48
48
|
// position while the last value wins, so re-indexing an assembly replaces
|
|
49
|
-
// its
|
|
49
|
+
// its entry in place and a new one lands at the end
|
|
50
50
|
aggregateTextSearchAdapters: [
|
|
51
|
-
...new Map([...existing, ...written].map(a => [a
|
|
51
|
+
...new Map([...existing, ...written].map(a => [indexFileOf(a), a])).values(),
|
|
52
52
|
],
|
|
53
53
|
});
|
|
54
54
|
}
|
|
@@ -90,7 +90,9 @@ export async function run(args) {
|
|
|
90
90
|
`Only tracks with an indexable adapter type (${Object.keys(indexableAdapters).sort().join(', ')}) are indexed; ` +
|
|
91
91
|
'tracks with other adapter types are skipped automatically.\n\n' +
|
|
92
92
|
'GTF has no Name/ID attributes, so the default --attributes also match ' +
|
|
93
|
-
'their GTF spellings (gene_name, transcript_name, gene_id, transcript_id).' +
|
|
93
|
+
'their GTF spellings (gene_name, transcript_name, gene_id, transcript_id). ' +
|
|
94
|
+
'A GFF3 row with no Name, as in GENCODE, is named by its own gene_name or ' +
|
|
95
|
+
'transcript_name.' +
|
|
94
96
|
'\n\n' +
|
|
95
97
|
'--exclude names types not to index; --include names the only types to ' +
|
|
96
98
|
'index. Reach for --include when the file draws from a vocabulary you do ' +
|
|
@@ -13,13 +13,13 @@ function validatePrefixSize(value) {
|
|
|
13
13
|
}
|
|
14
14
|
export function prepareIndexDriverFlags(flags) {
|
|
15
15
|
return {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
policy: {
|
|
17
|
+
attributes: parseCommaSeparatedString(flags.attributes),
|
|
18
|
+
exclude: parseCommaSeparatedString(flags.exclude),
|
|
19
|
+
include: flags.include
|
|
20
|
+
? parseCommaSeparatedString(flags.include)
|
|
21
|
+
: undefined,
|
|
22
|
+
},
|
|
23
23
|
quiet: flags.quiet ?? false,
|
|
24
24
|
prefixSize: validatePrefixSize(flags.prefixSize),
|
|
25
25
|
};
|
|
@@ -1,44 +1,30 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { Readable } from 'node:stream';
|
|
4
|
-
import { TRIX_DIR, generateMeta, guessAdapterFromFileName, indexFiles, trixFilePaths, } from '@jbrowse/text-indexing-core';
|
|
1
|
+
import { guessAdapterFromFileName, writeTrixIndex, } from '@jbrowse/text-indexing-core';
|
|
5
2
|
import { Presets, SingleBar } from 'cli-progress';
|
|
6
|
-
import { ixIxxStream } from 'ixixx';
|
|
7
3
|
import { supported } from '../../types/common.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// no longer leaves an empty trix/ behind
|
|
11
|
-
function ensureTrixDir(outLocation) {
|
|
12
|
-
fs.mkdirSync(path.join(outLocation, TRIX_DIR), { recursive: true });
|
|
4
|
+
function bar(format) {
|
|
5
|
+
return new SingleBar({ format, etaBuffer: 2000 }, Presets.shades_classic);
|
|
13
6
|
}
|
|
14
|
-
async function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
progressBar.start(1, 0);
|
|
18
|
-
}
|
|
19
|
-
const { ix, ixx } = trixFilePaths(outLocation, name);
|
|
20
|
-
await ixIxxStream(readStream, ix, ixx, prefixSize);
|
|
21
|
-
if (!quiet) {
|
|
22
|
-
progressBar.update(1);
|
|
23
|
-
progressBar.stop();
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
export async function indexDriver({ trackConfigs, attributes, outLocation, name, quiet, typesToExclude, typesToInclude, assemblyNames, prefixSize, }) {
|
|
27
|
-
ensureTrixDir(outLocation);
|
|
28
|
-
const readStream = Readable.from(indexFiles({
|
|
7
|
+
export async function indexDriver({ trackConfigs, policy, outLocation, name, quiet, assemblyNames, prefixSize, }) {
|
|
8
|
+
let sortBar;
|
|
9
|
+
await writeTrixIndex({
|
|
29
10
|
tracks: trackConfigs,
|
|
30
|
-
attributesToIndex: attributes,
|
|
31
11
|
outDir: outLocation,
|
|
32
|
-
|
|
33
|
-
|
|
12
|
+
name,
|
|
13
|
+
policy,
|
|
14
|
+
assemblyNames,
|
|
15
|
+
prefixSize,
|
|
16
|
+
onSort: quiet
|
|
17
|
+
? undefined
|
|
18
|
+
: () => {
|
|
19
|
+
sortBar = bar('{bar} Sorting and writing index...');
|
|
20
|
+
sortBar.start(1, 0);
|
|
21
|
+
},
|
|
34
22
|
makeProgress: quiet
|
|
35
23
|
? undefined
|
|
36
24
|
: trackId => {
|
|
37
|
-
const progressBar =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
etaBuffer: 2000,
|
|
41
|
-
}, Presets.shades_classic);
|
|
25
|
+
const progressBar = bar(
|
|
26
|
+
// eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {bar}/{percentage}/{eta} are cli-progress format tokens, not JS interpolation
|
|
27
|
+
`{bar} ${trackId} {percentage}% | ETA: {eta}s`);
|
|
42
28
|
return {
|
|
43
29
|
onStart: totalBytes => {
|
|
44
30
|
progressBar.start(totalBytes, 0);
|
|
@@ -51,23 +37,9 @@ export async function indexDriver({ trackConfigs, attributes, outLocation, name,
|
|
|
51
37
|
},
|
|
52
38
|
};
|
|
53
39
|
},
|
|
54
|
-
}));
|
|
55
|
-
await runIxIxx({
|
|
56
|
-
readStream,
|
|
57
|
-
outLocation,
|
|
58
|
-
name,
|
|
59
|
-
prefixSize,
|
|
60
|
-
quiet,
|
|
61
|
-
});
|
|
62
|
-
generateMeta({
|
|
63
|
-
configs: trackConfigs,
|
|
64
|
-
attributesToIndex: attributes,
|
|
65
|
-
outDir: outLocation,
|
|
66
|
-
name,
|
|
67
|
-
featureTypesToExclude: typesToExclude,
|
|
68
|
-
featureTypesToInclude: typesToInclude,
|
|
69
|
-
assemblyNames,
|
|
70
40
|
});
|
|
41
|
+
sortBar?.update(1);
|
|
42
|
+
sortBar?.stop();
|
|
71
43
|
}
|
|
72
44
|
export function prepareFileTrackConfigs(files, fileIds) {
|
|
73
45
|
return files.map((file, i) => {
|