@jbrowse/cli 5.0.0-beta.1 → 5.0.0-beta.3
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 +115 -26
- package/dist/commands/add-track-utils/adapter-utils.js +49 -0
- package/dist/commands/add-track.js +29 -6
- 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 +55 -25
- package/dist/commands/make-pif/pif-generator.js +105 -133
- package/dist/commands/shared/density.js +12 -0
- package/dist/commands/text-index/adapter-utils.js +0 -4
- package/dist/commands/validate/configManifest.generated.js +408 -357
- package/dist/commands/validate/index.js +5 -5
- package/dist/commands/validate/validateConfig.js +97 -32
- package/dist/index.js +5 -0
- package/dist/version.js +1 -1
- package/package.json +5 -9
- package/bin/run +0 -4
- package/bundle/index.js +0 -1569
- package/dist/commands/make-pif/cigar-utils.js +0 -67
package/README.md
CHANGED
|
@@ -59,6 +59,7 @@ COMMANDS
|
|
|
59
59
|
admin-server Start up a small admin server for JBrowse configuration
|
|
60
60
|
upgrade Upgrades JBrowse 2 to latest version
|
|
61
61
|
make-pif Creates pairwise indexed PAF (PIF), with bgzip and tabix
|
|
62
|
+
make-density Counts feature starts per bin into a bigWig density sidecar, with bedGraphToBigWig
|
|
62
63
|
sort-gff Sort a GFF/GTF for tabix: sort -k1,1 -k4,4n, header kept on top
|
|
63
64
|
sort-bed Sort a BED for tabix: sort -k1,1 -k2,2n, header kept on top
|
|
64
65
|
add-connection Add a connection to a JBrowse 2 configuration
|
|
@@ -263,9 +264,9 @@ Options:
|
|
|
263
264
|
-a, --assemblyNames Assembly name or names for track as comma separated
|
|
264
265
|
string. For pairwise synteny tracks the order is
|
|
265
266
|
query,target (reverse of minimap2/nucmer argument
|
|
266
|
-
order); for
|
|
267
|
-
(
|
|
268
|
-
every assembly the file covers, in any order
|
|
267
|
+
order); for the multi-genome PAF adapters
|
|
268
|
+
(MultiGenomePAFAdapter/MultiGenomeIndexedPAFAdapter)
|
|
269
|
+
list every assembly the file covers, in any order
|
|
269
270
|
|
|
270
271
|
--category Optional comma separated string of categories to
|
|
271
272
|
group tracks
|
|
@@ -310,6 +311,11 @@ Options:
|
|
|
310
311
|
|
|
311
312
|
--bed2 Used only for mcscan anchors/simpleAnchors types
|
|
312
313
|
|
|
314
|
+
--density Path or URL of a features-per-bin bigWig (jbrowse
|
|
315
|
+
make-density) to attach as the adapter's
|
|
316
|
+
densityAdapter. A local <file>.density.bw beside
|
|
317
|
+
the track file is attached without this flag
|
|
318
|
+
|
|
313
319
|
Notes:
|
|
314
320
|
|
|
315
321
|
--load controls how the data file is placed relative to config.json: copy, move,
|
|
@@ -337,11 +343,19 @@ or a .json file with an array of BigWig locations or subadapter objects (each
|
|
|
337
343
|
carrying its own name/color/group). With --load, local list entries are copied
|
|
338
344
|
like any other track file.
|
|
339
345
|
|
|
346
|
+
--density attaches a features-per-bin bigWig (jbrowse make-density) as the
|
|
347
|
+
adapter's densityAdapter, the band a display draws where the region is too large
|
|
348
|
+
to fetch features. A local <file>.density.bw beside the track file is attached
|
|
349
|
+
without the flag; a URL cannot be probed, so a remote sidecar needs it. The
|
|
350
|
+
adapters carrying the slot are BamAdapter, CramAdapter, HtsgetBamAdapter,
|
|
351
|
+
Gff3TabixAdapter, GtfTabixAdapter, BedTabixAdapter, BigBedAdapter,
|
|
352
|
+
VcfTabixAdapter and SplitVcfTabixAdapter.
|
|
353
|
+
|
|
340
354
|
For pairwise synteny adapters (PAF/Delta/Chain) --assemblyNames is query,target
|
|
341
|
-
— the reverse of the minimap2/nucmer input order. For the
|
|
342
|
-
(
|
|
343
|
-
assemblies the file covers, in any order, since one
|
|
344
|
-
pair.
|
|
355
|
+
— the reverse of the minimap2/nucmer input order. For the multi-genome PAF
|
|
356
|
+
adapters (MultiGenomePAFAdapter, MultiGenomeIndexedPAFAdapter) it is instead the
|
|
357
|
+
full list of assemblies the file covers, in any order, since one multi-genome
|
|
358
|
+
file backs every pair.
|
|
345
359
|
|
|
346
360
|
Examples:
|
|
347
361
|
|
|
@@ -371,6 +385,9 @@ $ jbrowse add-track --multiwig a.bw,b.bw,c.bw --load copy --name "Coverage"
|
|
|
371
385
|
|
|
372
386
|
# ...or from a sources.json carrying per-row name/color for each BigWig
|
|
373
387
|
$ jbrowse add-track --multiwig sources.json --name "CATlas ATAC"
|
|
388
|
+
|
|
389
|
+
# attach a remote density sidecar (a local genes.gff3.density.bw needs no flag)
|
|
390
|
+
$ jbrowse add-track https://mywebsite.com/genes.gff3.gz --density https://mywebsite.com/genes.gff3.density.bw
|
|
374
391
|
```
|
|
375
392
|
|
|
376
393
|
|
|
@@ -398,14 +415,14 @@ nothing. That is what this command is mainly for.
|
|
|
398
415
|
Two levels are reported:
|
|
399
416
|
|
|
400
417
|
error JBrowse accepts it and silently does the wrong thing — an unknown
|
|
401
|
-
slot, a
|
|
402
|
-
|
|
403
|
-
trackId. Exits 1.
|
|
418
|
+
slot, a key a defaultSession view or display does not declare, a
|
|
419
|
+
track naming an assembly the config never defines, a defaultSession
|
|
420
|
+
naming a trackId that does not exist, a duplicate trackId. Exits 1.
|
|
404
421
|
|
|
405
422
|
warning JBrowse will complain by itself on load, or handles it — a type name
|
|
406
423
|
the core plugins do not register (expected if one of your plugins
|
|
407
|
-
registers it), or a legacy key a migration rewrites
|
|
408
|
-
the run.
|
|
424
|
+
registers it), or a legacy key a migration rewrites, such as a view
|
|
425
|
+
nesting its settings under "init". Never fails the run.
|
|
409
426
|
|
|
410
427
|
Types registered by plugins are not known to this command, so they come through
|
|
411
428
|
as warnings rather than errors.
|
|
@@ -547,6 +564,9 @@ Options:
|
|
|
547
564
|
-p, --port Specified port to start the server on (default:
|
|
548
565
|
9090)
|
|
549
566
|
|
|
567
|
+
--host Interface to bind to (default: 127.0.0.1). Use
|
|
568
|
+
0.0.0.0 to accept connections from other machines
|
|
569
|
+
|
|
550
570
|
--root Path to the root of the JB2 installation
|
|
551
571
|
|
|
552
572
|
--bodySizeLimit Size limit of the update message (default: 25mb)
|
|
@@ -554,9 +574,10 @@ Options:
|
|
|
554
574
|
Notes:
|
|
555
575
|
|
|
556
576
|
The admin-server lets a browser session write changes back to config.json on
|
|
557
|
-
disk, authorized by a one-time key printed in the startup URL. It
|
|
558
|
-
|
|
559
|
-
|
|
577
|
+
disk, authorized by a one-time key printed in the startup URL. It binds to
|
|
578
|
+
127.0.0.1 so only this machine can reach it. The key gates the config routes,
|
|
579
|
+
but the rest of the JBrowse directory is served unauthenticated, so widen --host
|
|
580
|
+
only on a trusted network.
|
|
560
581
|
|
|
561
582
|
Examples:
|
|
562
583
|
|
|
@@ -571,6 +592,9 @@ $ jbrowse admin-server --root /path/to/jb2/
|
|
|
571
592
|
|
|
572
593
|
# raise the body size limit for very large config updates
|
|
573
594
|
$ jbrowse admin-server --bodySizeLimit 100mb
|
|
595
|
+
|
|
596
|
+
# accept connections from other machines on the network
|
|
597
|
+
$ jbrowse admin-server --host 0.0.0.0
|
|
574
598
|
```
|
|
575
599
|
|
|
576
600
|
|
|
@@ -637,16 +661,22 @@ Options:
|
|
|
637
661
|
|
|
638
662
|
--csi Create a CSI index for the PIF file instead of TBI
|
|
639
663
|
|
|
640
|
-
--coarse
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
664
|
+
--coarse Accuracy bound (bp) of the coarse tier. A coarse
|
|
665
|
+
row replaces its CIGAR with a coarse CIGAR (cr:Z:)
|
|
666
|
+
that keeps every indel longer than half this length
|
|
667
|
+
and folds the rest into runs, each run staying
|
|
668
|
+
within this many bp of the real alignment. Defaults
|
|
669
|
+
to 10000. The coarse tier (prefix T/Q) is emitted
|
|
670
|
+
alongside the per-row CIGAR fine tier by default so
|
|
671
|
+
whole-genome synteny views can auto-switch to it;
|
|
672
|
+
pass --no-coarse to omit it.
|
|
673
|
+
|
|
674
|
+
--no-coarse Do not emit the coarse tier; write only the per-row
|
|
675
|
+
CIGAR fine tier.
|
|
647
676
|
|
|
648
|
-
--
|
|
649
|
-
|
|
677
|
+
--threads Compression threads for bgzip. Defaults to 4. Raise
|
|
678
|
+
it on a machine with cores to spare, or set 1 to
|
|
679
|
+
leave the rest of the machine alone.
|
|
650
680
|
|
|
651
681
|
Notes:
|
|
652
682
|
|
|
@@ -666,11 +696,70 @@ $ jbrowse make-pif input.paf --out output.pif.gz
|
|
|
666
696
|
# use a CSI index for assemblies with chromosomes longer than ~512 Mb
|
|
667
697
|
$ jbrowse make-pif input.paf --csi
|
|
668
698
|
|
|
669
|
-
#
|
|
670
|
-
$ jbrowse make-pif input.paf --coarse
|
|
699
|
+
# a looser coarse tier: runs within 50kb of the alignment, indels over 25kb kept
|
|
700
|
+
$ jbrowse make-pif input.paf --coarse 50000
|
|
671
701
|
|
|
672
702
|
# emit only the per-row CIGAR fine tier, skipping the coarse tier
|
|
673
703
|
$ jbrowse make-pif input.paf --no-coarse
|
|
704
|
+
|
|
705
|
+
# give bgzip more (or fewer) compression threads than the default 4
|
|
706
|
+
$ jbrowse make-pif input.paf --threads 8
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
## jbrowse make-density
|
|
711
|
+
|
|
712
|
+
```
|
|
713
|
+
Counts feature starts per bin into a bigWig, the density sidecar a track draws
|
|
714
|
+
where the region is too large to fetch its features
|
|
715
|
+
|
|
716
|
+
Usage: jbrowse make-density <file> [options]
|
|
717
|
+
|
|
718
|
+
Options:
|
|
719
|
+
-h, --help Show help
|
|
720
|
+
|
|
721
|
+
--bin Width of each bin, in bp [default: 1000]
|
|
722
|
+
|
|
723
|
+
--chrom-sizes Two-column reference name and length table for the
|
|
724
|
+
assembly the file is on
|
|
725
|
+
|
|
726
|
+
--assembly FASTA of the assembly the file is on; its .fai
|
|
727
|
+
supplies the reference lengths, in place of
|
|
728
|
+
--chrom-sizes
|
|
729
|
+
|
|
730
|
+
--out Where to write the bigWig. Defaults to
|
|
731
|
+
<file>.density.bw beside the input, which is where
|
|
732
|
+
add-track looks for it
|
|
733
|
+
|
|
734
|
+
Notes:
|
|
735
|
+
|
|
736
|
+
The sidecar is attached to a track through its adapter's densityAdapter slot,
|
|
737
|
+
which BamAdapter, CramAdapter, HtsgetBamAdapter, Gff3TabixAdapter,
|
|
738
|
+
GtfTabixAdapter, BedTabixAdapter, BigBedAdapter, VcfTabixAdapter and
|
|
739
|
+
SplitVcfTabixAdapter declare. add-track attaches it on its own when the default
|
|
740
|
+
<file>.density.bw sits beside the track file, so building it before adding the
|
|
741
|
+
track is all it takes; --density attaches one by path or URL.
|
|
742
|
+
|
|
743
|
+
GFF3 counts only top-level features — a line whose attributes carry no Parent= —
|
|
744
|
+
so a gene is one count rather than one per exon. GTF, BED and VCF have no parent
|
|
745
|
+
link to follow, so every record counts.
|
|
746
|
+
|
|
747
|
+
BAM and CRAM are out of scope: read depth is not feature density, and the tools
|
|
748
|
+
that compute it already write bigWig. Point add-track --density at what they
|
|
749
|
+
produce.
|
|
750
|
+
|
|
751
|
+
Requires bedGraphToBigWig (UCSC) on the PATH.
|
|
752
|
+
|
|
753
|
+
Examples:
|
|
754
|
+
|
|
755
|
+
# writes genes.gff3.density.bw beside the input, in 1kb bins
|
|
756
|
+
$ jbrowse make-density genes.gff3.gz --chrom-sizes hg38.chrom.sizes
|
|
757
|
+
|
|
758
|
+
# take the reference lengths from the assembly FASTA index instead
|
|
759
|
+
$ jbrowse make-density variants.vcf.gz --assembly hg38.fa
|
|
760
|
+
|
|
761
|
+
# coarser bins, and a name of your own
|
|
762
|
+
$ jbrowse make-density genes.gff3.gz --assembly hg38.fa --bin 10000 --out genes.10kb.bw
|
|
674
763
|
```
|
|
675
764
|
|
|
676
765
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { adapterTypesToTrackTypeMap, formats, matchFormat, resolveIndexType, trackTypeForAdapter, } from '@jbrowse/add-track-core';
|
|
3
|
+
import { isURL } from '../../types/common.js';
|
|
4
|
+
import { densitySidecarPath } from '../shared/density.js';
|
|
3
5
|
function hasLocField(spec) {
|
|
4
6
|
return 'locField' in spec;
|
|
5
7
|
}
|
|
@@ -51,6 +53,53 @@ export function siblingSidecar(location, suffix) {
|
|
|
51
53
|
];
|
|
52
54
|
return candidates.find(c => fs.existsSync(c)) ?? candidates[0];
|
|
53
55
|
}
|
|
56
|
+
// The feature adapters whose schema spreads
|
|
57
|
+
// `densityAdapterConfigSchemaFields`, so a `densityAdapter` written onto one
|
|
58
|
+
// resolves to a slot rather than being dropped. Change one, change the other.
|
|
59
|
+
const densityAdapterTypes = new Set([
|
|
60
|
+
'BamAdapter',
|
|
61
|
+
'CramAdapter',
|
|
62
|
+
'HtsgetBamAdapter',
|
|
63
|
+
'Gff3TabixAdapter',
|
|
64
|
+
'GtfTabixAdapter',
|
|
65
|
+
'BedTabixAdapter',
|
|
66
|
+
'BigBedAdapter',
|
|
67
|
+
'VcfTabixAdapter',
|
|
68
|
+
'SplitVcfTabixAdapter',
|
|
69
|
+
]);
|
|
70
|
+
/**
|
|
71
|
+
* The adapter with its density sidecar attached, and the local file to load
|
|
72
|
+
* beside the config when there is one.
|
|
73
|
+
*
|
|
74
|
+
* An explicit `--density` is taken as given, since a URL cannot be probed;
|
|
75
|
+
* otherwise the conventional `<file>.density.bw` counts only when it is
|
|
76
|
+
* actually there, the way `siblingSidecar` probes an index.
|
|
77
|
+
*/
|
|
78
|
+
export function withDensityAdapter({ adapter, location, density, makeLocation, }) {
|
|
79
|
+
const supported = densityAdapterTypes.has(adapter.type);
|
|
80
|
+
if (density !== undefined && !supported) {
|
|
81
|
+
throw new Error(`--density has no slot on ${adapter.type}. The adapters carrying a density sidecar are: ${[...densityAdapterTypes].join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
const probed = location !== undefined && fs.existsSync(densitySidecarPath(location))
|
|
84
|
+
? densitySidecarPath(location)
|
|
85
|
+
: undefined;
|
|
86
|
+
if (probed !== undefined && !supported) {
|
|
87
|
+
console.warn(`Warning: ${probed} sits beside the track file but ${adapter.type} has no densityAdapter slot, so it is not attached. The adapters carrying one are indexed: bgzip and tabix the file, then add it.`);
|
|
88
|
+
}
|
|
89
|
+
const file = density === undefined ? (supported ? probed : undefined) : density;
|
|
90
|
+
return file === undefined
|
|
91
|
+
? { adapter }
|
|
92
|
+
: {
|
|
93
|
+
adapter: {
|
|
94
|
+
...adapter,
|
|
95
|
+
densityAdapter: {
|
|
96
|
+
type: 'BigWigAdapter',
|
|
97
|
+
bigWigLocation: makeLocation(file),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
file: isURL(file) ? undefined : file,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
54
103
|
// builds the adapter object and the list of source files for a spec in one
|
|
55
104
|
// place, so the config it writes and the files add-track copies can never drift
|
|
56
105
|
function buildFromSpec(spec, { location, index, bed1, bed2, makeLocation }) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { printHelp, readConfigFile, resolveConfigPath } from '../utils.js';
|
|
4
|
-
import { guessTrack, guessTrackType, makeLocationProtocol, } from './add-track-utils/adapter-utils.js';
|
|
4
|
+
import { guessTrack, guessTrackType, makeLocationProtocol, withDensityAdapter, } from './add-track-utils/adapter-utils.js';
|
|
5
5
|
import { loadFiles } from './add-track-utils/file-operations.js';
|
|
6
6
|
import { buildMultiWiggle } from './add-track-utils/multiwig.js';
|
|
7
7
|
import { addSyntenyAssemblyNames, buildTrackConfig, mapLocationForFiles, mergeDisplayDefaults, } from './add-track-utils/track-config.js';
|
|
@@ -39,7 +39,7 @@ export async function run(args) {
|
|
|
39
39
|
assemblyNames: {
|
|
40
40
|
type: 'string',
|
|
41
41
|
short: 'a',
|
|
42
|
-
description: 'Assembly name or names for track as comma separated string. For pairwise synteny tracks the order is query,target (reverse of minimap2/nucmer argument order); for
|
|
42
|
+
description: 'Assembly name or names for track as comma separated string. For pairwise synteny tracks the order is query,target (reverse of minimap2/nucmer argument order); for the multi-genome PAF adapters (MultiGenomePAFAdapter/MultiGenomeIndexedPAFAdapter) list every assembly the file covers, in any order',
|
|
43
43
|
},
|
|
44
44
|
category: {
|
|
45
45
|
type: 'string',
|
|
@@ -104,6 +104,10 @@ export async function run(args) {
|
|
|
104
104
|
type: 'string',
|
|
105
105
|
description: 'Used only for mcscan anchors/simpleAnchors types',
|
|
106
106
|
},
|
|
107
|
+
density: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: "Path or URL of a features-per-bin bigWig (jbrowse make-density) to attach as the adapter's densityAdapter. A local <file>.density.bw beside the track file is attached without this flag",
|
|
110
|
+
},
|
|
107
111
|
};
|
|
108
112
|
const { values: flags, positionals } = parseArgs({
|
|
109
113
|
args,
|
|
@@ -134,11 +138,18 @@ export async function run(args) {
|
|
|
134
138
|
'BigWig files/URLs, or a .json file with an array of BigWig locations or ' +
|
|
135
139
|
'subadapter objects (each carrying its own name/color/group). With --load, ' +
|
|
136
140
|
'local list entries are copied like any other track file.\n\n' +
|
|
141
|
+
'--density attaches a features-per-bin bigWig (jbrowse make-density) as the ' +
|
|
142
|
+
"adapter's densityAdapter, the band a display draws where the region is too " +
|
|
143
|
+
'large to fetch features. A local <file>.density.bw beside the track file is ' +
|
|
144
|
+
'attached without the flag; a URL cannot be probed, so a remote sidecar needs ' +
|
|
145
|
+
'it. The adapters carrying the slot are BamAdapter, CramAdapter, ' +
|
|
146
|
+
'HtsgetBamAdapter, Gff3TabixAdapter, GtfTabixAdapter, BedTabixAdapter, ' +
|
|
147
|
+
'BigBedAdapter, VcfTabixAdapter and SplitVcfTabixAdapter.\n\n' +
|
|
137
148
|
'For pairwise synteny adapters (PAF/Delta/Chain) --assemblyNames is ' +
|
|
138
149
|
'query,target — the reverse of the minimap2/nucmer input order. For the ' +
|
|
139
|
-
'
|
|
150
|
+
'multi-genome PAF adapters (MultiGenomePAFAdapter, MultiGenomeIndexedPAFAdapter) it is ' +
|
|
140
151
|
'instead the full list of assemblies the file covers, in any order, since ' +
|
|
141
|
-
'one
|
|
152
|
+
'one multi-genome file backs every pair.';
|
|
142
153
|
const examples = [
|
|
143
154
|
'# copy /path/to/my.bam and /path/to/my.bam.bai to current directory and adds track to config.json',
|
|
144
155
|
'$ jbrowse add-track /path/to/my.bam --load copy',
|
|
@@ -166,6 +177,9 @@ export async function run(args) {
|
|
|
166
177
|
'',
|
|
167
178
|
'# ...or from a sources.json carrying per-row name/color for each BigWig',
|
|
168
179
|
'$ jbrowse add-track --multiwig sources.json --name "CATlas ATAC"',
|
|
180
|
+
'',
|
|
181
|
+
'# attach a remote density sidecar (a local genes.gff3.density.bw needs no flag)',
|
|
182
|
+
'$ jbrowse add-track https://mywebsite.com/genes.gff3.gz --density https://mywebsite.com/genes.gff3.density.bw',
|
|
169
183
|
];
|
|
170
184
|
if (flags.help) {
|
|
171
185
|
printHelp({
|
|
@@ -187,7 +201,7 @@ export async function run(args) {
|
|
|
187
201
|
const mapLocation = (p) => wrap(mapLocationForFiles(p, load, subDir));
|
|
188
202
|
// build the adapter (and the set of files to load) up front, so the track-arg
|
|
189
203
|
// and load validation runs before we touch the config on disk
|
|
190
|
-
const { adapter, files, trackType, trackId } = multiwig
|
|
204
|
+
const { adapter: builtAdapter, files, trackType, trackId, } = multiwig
|
|
191
205
|
? buildMultiWiggleTrack({
|
|
192
206
|
sources: multiwig,
|
|
193
207
|
mapLocation,
|
|
@@ -207,6 +221,15 @@ export async function run(args) {
|
|
|
207
221
|
trackType: flags.trackType,
|
|
208
222
|
trackId: flags.trackId,
|
|
209
223
|
});
|
|
224
|
+
const { adapter, file: densityFile } = withDensityAdapter({
|
|
225
|
+
adapter: builtAdapter,
|
|
226
|
+
location,
|
|
227
|
+
density: flags.density,
|
|
228
|
+
makeLocation: mapLocation,
|
|
229
|
+
});
|
|
230
|
+
if (densityFile !== undefined && !load) {
|
|
231
|
+
throw new Error(`--density names a local file (${densityFile}); pass --load to say how it is placed beside the config`);
|
|
232
|
+
}
|
|
210
233
|
const baseConfigObj = config ? parseConfigFlag(config) : undefined;
|
|
211
234
|
const displayDefaults = mergeDisplayDefaults({
|
|
212
235
|
configObj: baseConfigObj,
|
|
@@ -245,7 +268,7 @@ export async function run(args) {
|
|
|
245
268
|
});
|
|
246
269
|
const updatedConfig = { ...configContents, tracks };
|
|
247
270
|
await loadFiles({
|
|
248
|
-
files,
|
|
271
|
+
files: [...files, densityFile],
|
|
249
272
|
destDir: configDir,
|
|
250
273
|
mode: load,
|
|
251
274
|
subDir,
|
|
@@ -13,6 +13,11 @@ export async function run(args) {
|
|
|
13
13
|
short: 'p',
|
|
14
14
|
description: 'Specified port to start the server on (default: 9090)',
|
|
15
15
|
},
|
|
16
|
+
host: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Interface to bind to (default: 127.0.0.1). Use 0.0.0.0 to accept ' +
|
|
19
|
+
'connections from other machines',
|
|
20
|
+
},
|
|
16
21
|
root: {
|
|
17
22
|
type: 'string',
|
|
18
23
|
description: 'Path to the root of the JB2 installation',
|
|
@@ -40,12 +45,15 @@ export async function run(args) {
|
|
|
40
45
|
'',
|
|
41
46
|
'# raise the body size limit for very large config updates',
|
|
42
47
|
'$ jbrowse admin-server --bodySizeLimit 100mb',
|
|
48
|
+
'',
|
|
49
|
+
'# accept connections from other machines on the network',
|
|
50
|
+
'$ jbrowse admin-server --host 0.0.0.0',
|
|
43
51
|
];
|
|
44
52
|
const notes = 'The admin-server lets a browser session write changes back to ' +
|
|
45
53
|
'config.json on disk, authorized by a one-time key printed in the ' +
|
|
46
|
-
'startup URL. It
|
|
47
|
-
'
|
|
48
|
-
'
|
|
54
|
+
'startup URL. It binds to 127.0.0.1 so only this machine can reach it. ' +
|
|
55
|
+
'The key gates the config routes, but the rest of the JBrowse directory ' +
|
|
56
|
+
'is served unauthenticated, so widen --host only on a trusted network.';
|
|
49
57
|
if (flags.help) {
|
|
50
58
|
printHelp({
|
|
51
59
|
description,
|
|
@@ -56,7 +64,7 @@ export async function run(args) {
|
|
|
56
64
|
});
|
|
57
65
|
return;
|
|
58
66
|
}
|
|
59
|
-
const { root, bodySizeLimit = '25mb' } = flags;
|
|
67
|
+
const { root, host, bodySizeLimit = '25mb' } = flags;
|
|
60
68
|
const { outFile, baseDir } = await setupConfigFile({ root });
|
|
61
69
|
const port = parsePort({ portStr: flags.port });
|
|
62
70
|
// the key lives only in this process and in the startup URL printed below. It
|
|
@@ -71,5 +79,5 @@ export async function run(args) {
|
|
|
71
79
|
serverRef,
|
|
72
80
|
bodySizeLimit: parseByteLimit(bodySizeLimit),
|
|
73
81
|
}));
|
|
74
|
-
startServer({ server, port, key, outFile, serverRef });
|
|
82
|
+
startServer({ server, port, host, key, outFile, serverRef });
|
|
75
83
|
}
|
|
@@ -4,6 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
import handler from 'serve-handler';
|
|
5
5
|
import { debug, isRecord, resolveConfigPath, writeJsonFile, } from '../../utils.js';
|
|
6
6
|
import { createDefaultConfig } from '../add-assembly/utils.js';
|
|
7
|
+
// Loopback, so the unauthenticated static handler below does not serve the
|
|
8
|
+
// data directory to every host on the network. `--host` opts out.
|
|
9
|
+
export const DEFAULT_HOST = '127.0.0.1';
|
|
7
10
|
export function parsePort({ portStr, defaultPort = 9090, }) {
|
|
8
11
|
if (!portStr) {
|
|
9
12
|
return defaultPort;
|
|
@@ -122,9 +125,8 @@ function sendText(res, status, body) {
|
|
|
122
125
|
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
123
126
|
res.end(body);
|
|
124
127
|
}
|
|
125
|
-
// What `cors()` sent. The
|
|
126
|
-
//
|
|
127
|
-
// origin never was.
|
|
128
|
+
// What `cors()` sent. The admin key is what authorizes a write; the origin
|
|
129
|
+
// never was.
|
|
128
130
|
function applyCorsHeaders(res) {
|
|
129
131
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
130
132
|
res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, POST, OPTIONS');
|
|
@@ -278,9 +280,9 @@ export function createRequestHandler({ baseDir, outFile, key, serverRef, bodySiz
|
|
|
278
280
|
}
|
|
279
281
|
};
|
|
280
282
|
}
|
|
281
|
-
export function startServer({ server, port, key, outFile, serverRef, }) {
|
|
282
|
-
server.listen(port, () => {
|
|
283
|
-
console.log(`Admin server started on
|
|
283
|
+
export function startServer({ server, port, host = DEFAULT_HOST, key, outFile, serverRef, }) {
|
|
284
|
+
server.listen(port, host, () => {
|
|
285
|
+
console.log(`Admin server started on ${host}:${port}\n\n` +
|
|
284
286
|
`To access the admin interface, open your browser to:\n` +
|
|
285
287
|
`http://localhost:${port}?adminKey=${key}\n\n` +
|
|
286
288
|
`Admin key: ${key}\n` +
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createReadStream, existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { createGunzip } from 'node:zlib';
|
|
4
|
+
// Which parser a filename selects. BAM/CRAM/SAM are named so the refusal can
|
|
5
|
+
// say what to do instead, rather than falling into the unknown-extension case.
|
|
6
|
+
const formats = [
|
|
7
|
+
{ regex: /\.gff3?(\.b?gz)?$/i, format: 'gff3' },
|
|
8
|
+
{ regex: /\.gtf(\.b?gz)?$/i, format: 'gtf' },
|
|
9
|
+
{ regex: /\.vcf(\.b?gz)?$/i, format: 'vcf' },
|
|
10
|
+
{ regex: /\.bed(\.b?gz)?$/i, format: 'bed' },
|
|
11
|
+
];
|
|
12
|
+
const alignmentFormat = /\.(bam|cram|sam)$/i;
|
|
13
|
+
export function densityFormat(file) {
|
|
14
|
+
const hit = formats.find(f => f.regex.test(file));
|
|
15
|
+
if (!hit) {
|
|
16
|
+
throw new Error(alignmentFormat.test(file)
|
|
17
|
+
? `${file} is an alignment file, which make-density cannot read. Count coverage with a tool that reads BAM/CRAM (e.g. "bamCoverage" or "samtools depth" piped to bedGraphToBigWig) and pass the result with add-track --density.`
|
|
18
|
+
: `Cannot tell what kind of file ${file} is. make-density reads GFF3, GTF, BED and VCF, plain or bgzipped.`);
|
|
19
|
+
}
|
|
20
|
+
return hit.format;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The reference name and 0-based start of one record, or undefined for a line
|
|
24
|
+
* that contributes nothing: a header, a directive, a short line, or — in GFF3 —
|
|
25
|
+
* a child feature, whose start would count its parent's locus a second time.
|
|
26
|
+
* A GTF row says whether it is a `transcript`, the record its track draws;
|
|
27
|
+
* BED and VCF have no parent link to follow, so every record counts.
|
|
28
|
+
*/
|
|
29
|
+
function parseRecord(line, format) {
|
|
30
|
+
const columns = line.split('\t');
|
|
31
|
+
const refName = columns[0];
|
|
32
|
+
const startsHeader = line.startsWith('#') ||
|
|
33
|
+
line.startsWith('track ') ||
|
|
34
|
+
line.startsWith('browser ');
|
|
35
|
+
if (startsHeader || refName === undefined || refName === '') {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
if (format === 'bed') {
|
|
39
|
+
const start = columns[1];
|
|
40
|
+
return start === undefined
|
|
41
|
+
? undefined
|
|
42
|
+
: { refName, start: +start, transcript: false };
|
|
43
|
+
}
|
|
44
|
+
const start = columns[format === 'vcf' ? 1 : 3];
|
|
45
|
+
const attributes = columns[8];
|
|
46
|
+
const isChild = format === 'gff3' && attributes?.includes('Parent=');
|
|
47
|
+
return start === undefined || isChild
|
|
48
|
+
? undefined
|
|
49
|
+
: {
|
|
50
|
+
refName,
|
|
51
|
+
start: +start - 1,
|
|
52
|
+
transcript: format === 'gtf' && columns[2] === 'transcript',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function tally(bins, key, bin) {
|
|
56
|
+
const existing = bins.get(key);
|
|
57
|
+
const perRef = existing === undefined ? new Map() : existing;
|
|
58
|
+
if (existing === undefined) {
|
|
59
|
+
bins.set(key, perRef);
|
|
60
|
+
}
|
|
61
|
+
const count = perRef.get(bin);
|
|
62
|
+
perRef.set(bin, count === undefined ? 1 : count + 1);
|
|
63
|
+
}
|
|
64
|
+
export async function countFeatureStarts({ file, format, binSize, chromSizes, }) {
|
|
65
|
+
// A GTF's transcripts are tallied apart from its exons and CDS rows: where a
|
|
66
|
+
// file has transcript rows they are the count, and a file with none (an
|
|
67
|
+
// exon-only GTF) counts every row.
|
|
68
|
+
const all = new Map();
|
|
69
|
+
const transcripts = new Map();
|
|
70
|
+
const unknownRefNames = new Set();
|
|
71
|
+
let records = 0;
|
|
72
|
+
let transcriptRecords = 0;
|
|
73
|
+
const source = createReadStream(file);
|
|
74
|
+
const lines = createInterface({
|
|
75
|
+
input: /\.b?gz$/i.test(file) ? source.pipe(createGunzip()) : source,
|
|
76
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
77
|
+
});
|
|
78
|
+
for await (const line of lines) {
|
|
79
|
+
// everything after a GFF3 ##FASTA directive is sequence, not features
|
|
80
|
+
if (line.trimEnd() === '##FASTA') {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const record = parseRecord(line, format);
|
|
84
|
+
if (record && Number.isFinite(record.start)) {
|
|
85
|
+
records++;
|
|
86
|
+
transcriptRecords += record.transcript ? 1 : 0;
|
|
87
|
+
const length = chromSizes.get(record.refName);
|
|
88
|
+
if (length === undefined) {
|
|
89
|
+
unknownRefNames.add(record.refName);
|
|
90
|
+
}
|
|
91
|
+
else if (record.start >= 0 && record.start < length) {
|
|
92
|
+
const bin = Math.floor(record.start / binSize);
|
|
93
|
+
tally(all, record.refName, bin);
|
|
94
|
+
if (record.transcript) {
|
|
95
|
+
tally(transcripts, record.refName, bin);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
lines.close();
|
|
101
|
+
source.destroy();
|
|
102
|
+
return transcriptRecords > 0
|
|
103
|
+
? { bins: transcripts, records: transcriptRecords, unknownRefNames }
|
|
104
|
+
: { bins: all, records, unknownRefNames };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* bedGraph rows covering every base of every reference, in the order
|
|
108
|
+
* bedGraphToBigWig demands (`sort -k1,1 -k2,2n`): one row per counted bin,
|
|
109
|
+
* clipped to the reference's length, and one per run of empty bins between
|
|
110
|
+
* them. The empty runs are load-bearing: a bigWig's zoom levels average over
|
|
111
|
+
* the bases its rows cover, so a file that skipped them would read, zoomed out,
|
|
112
|
+
* as the mean over the bins that held something rather than over the span, and
|
|
113
|
+
* a gene desert beside a cluster would score the same.
|
|
114
|
+
*/
|
|
115
|
+
export function* bedGraphLines({ bins, chromSizes, binSize, }) {
|
|
116
|
+
const byName = [...chromSizes.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
117
|
+
for (const [refName, length] of byName) {
|
|
118
|
+
const perRef = bins.get(refName);
|
|
119
|
+
const counted = perRef
|
|
120
|
+
? [...perRef.entries()].sort(([a], [b]) => a - b)
|
|
121
|
+
: [];
|
|
122
|
+
let cursor = 0;
|
|
123
|
+
for (const [bin, count] of counted) {
|
|
124
|
+
const start = bin * binSize;
|
|
125
|
+
const end = Math.min(start + binSize, length);
|
|
126
|
+
if (start > cursor) {
|
|
127
|
+
yield `${refName}\t${cursor}\t${start}\t0\n`;
|
|
128
|
+
}
|
|
129
|
+
yield `${refName}\t${start}\t${end}\t${count}\n`;
|
|
130
|
+
cursor = end;
|
|
131
|
+
}
|
|
132
|
+
if (cursor < length) {
|
|
133
|
+
yield `${refName}\t${cursor}\t${length}\t0\n`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function chromSizesLines(chromSizes) {
|
|
138
|
+
return [...chromSizes.entries()]
|
|
139
|
+
.map(([refName, length]) => `${refName}\t${length}\n`)
|
|
140
|
+
.join('');
|
|
141
|
+
}
|
|
142
|
+
// a `.chrom.sizes` and a `.fai` both put the name in column 1 and the length in
|
|
143
|
+
// column 2, so one parser reads either
|
|
144
|
+
function parseChromSizes(text, source) {
|
|
145
|
+
const sizes = new Map();
|
|
146
|
+
for (const line of text.split('\n')) {
|
|
147
|
+
const [refName, length] = line.split('\t');
|
|
148
|
+
if (refName && length !== undefined && Number.isFinite(+length)) {
|
|
149
|
+
sizes.set(refName, +length);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (sizes.size === 0) {
|
|
153
|
+
throw new Error(`No reference sequences found in ${source}`);
|
|
154
|
+
}
|
|
155
|
+
return sizes;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The reference lengths the bigWig header needs, from an explicit
|
|
159
|
+
* `--chrom-sizes` or from the `.fai` beside an `--assembly` FASTA.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveChromSizes({ chromSizes, assembly, }) {
|
|
162
|
+
if (chromSizes !== undefined) {
|
|
163
|
+
return parseChromSizes(readFileSync(chromSizes, 'utf8'), chromSizes);
|
|
164
|
+
}
|
|
165
|
+
if (assembly !== undefined) {
|
|
166
|
+
const fai = `${assembly}.fai`;
|
|
167
|
+
if (!existsSync(fai)) {
|
|
168
|
+
throw new Error(`No index beside ${assembly}: expected ${fai}. Make one with "samtools faidx ${assembly}", or pass --chrom-sizes instead.`);
|
|
169
|
+
}
|
|
170
|
+
return parseChromSizes(readFileSync(fai, 'utf8'), fai);
|
|
171
|
+
}
|
|
172
|
+
throw new Error('Reference lengths are required: pass --chrom-sizes <file> or --assembly <fasta> (whose .fai is read)');
|
|
173
|
+
}
|
|
174
|
+
export function parseBinSize(bin) {
|
|
175
|
+
const binSize = +bin;
|
|
176
|
+
if (!Number.isInteger(binSize) || binSize < 1) {
|
|
177
|
+
throw new Error(`--bin must be a positive whole number of bp, got ${bin}`);
|
|
178
|
+
}
|
|
179
|
+
return binSize;
|
|
180
|
+
}
|