@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 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 all-vs-all adapters
267
- (AllVsAllPAFAdapter/AllVsAllIndexedPAFAdapter) list
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 all-vs-all adapters
342
- (AllVsAllPAFAdapter, AllVsAllIndexedPAFAdapter) it is instead the full list of
343
- assemblies the file covers, in any order, since one all-vs-all file backs every
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 track naming an assembly the config never defines, a
402
- defaultSession naming a trackId that does not exist, a duplicate
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. Never fails
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 is meant for
558
- local configuration only: run it on a trusted machine and do not expose the port
559
- to untrusted networks or the public internet.
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 Minimum insertion/deletion length (bp) at which a
641
- coarse-tier row is split into multiple pieces so
642
- each row stays tight — 0 emits an unsplit coarse
643
- tier. Defaults to 10000. The no-CIGAR coarse tier
644
- (prefix T/Q) is emitted alongside the per-row CIGAR
645
- fine tier by default so whole-genome synteny views
646
- can auto-switch to it; pass --no-coarse to omit it.
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
- --no-coarse Do not emit the coarse no-CIGAR tier; write only
649
- the per-row CIGAR fine tier.
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
- # emit an unsplit coarse tier (alongside the fine tier)
670
- $ jbrowse make-pif input.paf --coarse 0
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 all-vs-all adapters (AllVsAllPAFAdapter/AllVsAllIndexedPAFAdapter) list every assembly the file covers, in any order',
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
- 'all-vs-all adapters (AllVsAllPAFAdapter, AllVsAllIndexedPAFAdapter) it is ' +
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 all-vs-all file backs every pair.';
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 is meant for local configuration only: run it on a ' +
47
- 'trusted machine and do not expose the port to untrusted networks or the ' +
48
- 'public internet.';
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 server binds to localhost for a browser tab on the
126
- // same machine to talk to, and the admin key is what authorizes a write — the
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 port ${port}\n\n` +
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
+ }