@jbrowse/bandage-core 4.0.24 → 4.0.26

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.
@@ -1,38 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- cutWindowGFA,
3
+ HPRC_GBZ,
4
+ LAYOUT_MODE_VALUES,
5
+ cutGbzRegion,
6
+ featuresOnBackbone,
4
7
  figureSvg,
5
8
  forceLayout,
6
- haplotypeWanted,
9
+ genesFromBed,
10
+ genesFromGff3Lines,
11
+ genesFromText,
12
+ graphBackbone,
7
13
  layoutModeByValue,
8
14
  loadBandage,
9
15
  loadGraph,
10
- referencePathQuery,
11
- referenceSamplesOf,
12
- resolveReferenceSample
13
- } from "../chunks/chunk-4AOHEU76.js";
16
+ openGbz,
17
+ parseRegion
18
+ } from "../chunks/chunk-75KVO2WV.js";
14
19
 
15
20
  // src/cli/figure.ts
16
- import { open, readFile, writeFile } from "node:fs/promises";
21
+ import { access, open, readFile, writeFile } from "node:fs/promises";
17
22
  import path from "node:path";
18
23
  import { parseArgs } from "node:util";
19
24
  import { gunzipSync } from "node:zlib";
20
- import { GBZBase } from "@gmod/gbz-base";
21
- var HPRC = {
22
- db: "https://s3-us-west-2.amazonaws.com/human-pangenomics/pangenomes/freeze/release2/minigraph-cactus/v2.1/hprc-v2.1-mc-grch38/hprc-v2.1-mc-grch38.gbz.db",
23
- index: "https://jbrowse.org/demos/hprc/hprc-v2.1-mc-grch38.haplotype-index.anchored.db"
24
- };
25
- var WALK_LIMIT = 1e5;
26
- function parseRegion(text) {
27
- const m = /^\s*([^:\s]+):([\d,]+)-([\d,]+)\s*$/.exec(text);
28
- if (!m) {
29
- throw new Error(
30
- `"${text}" is not a region like chr6:160,614,798-160,647,758`
31
- );
32
- }
33
- const n = (s) => Number(s.replaceAll(",", ""));
34
- return { refName: m[1], start: n(m[2]), end: n(m[3]) };
35
- }
25
+ import { TabixIndexedFile } from "@gmod/tabix";
36
26
  var isUrl = (s) => /^https?:\/\//.test(s);
37
27
  function byteSource(location) {
38
28
  if (isUrl(location)) {
@@ -47,7 +37,7 @@ function byteSource(location) {
47
37
  return new Uint8Array(await res.arrayBuffer());
48
38
  },
49
39
  async stat() {
50
- const res = await fetch(location, { method: "HEAD" });
40
+ const res = await fetchOk(location, { method: "HEAD" });
51
41
  return { size: Number(res.headers.get("content-length")) };
52
42
  }
53
43
  };
@@ -64,36 +54,47 @@ function byteSource(location) {
64
54
  }
65
55
  };
66
56
  }
67
- async function gfaText(location) {
68
- const bytes = isUrl(location) ? new Uint8Array(await (await fetch(location)).arrayBuffer()) : await readFile(location);
57
+ async function fetchOk(location, init) {
58
+ const res = await fetch(location, init);
59
+ if (!res.ok) {
60
+ throw new Error(`HTTP ${res.status} reading ${location}`);
61
+ }
62
+ return res;
63
+ }
64
+ async function text(location) {
65
+ const bytes = isUrl(location) ? new Uint8Array(await (await fetchOk(location)).arrayBuffer()) : await readFile(location);
69
66
  const gzipped = bytes[0] === 31 && bytes[1] === 139;
70
67
  return new TextDecoder().decode(gzipped ? gunzipSync(bytes) : bytes);
71
68
  }
72
- async function cutGbz(gbz) {
73
- const { db, index } = gbz.db === "hprc" ? HPRC : gbz;
74
- const base = await GBZBase.open(
75
- byteSource(db),
76
- index ? { haplotypeIndex: byteSource(index) } : {}
69
+ async function readGenes(genes, graph, resolve) {
70
+ const backbone = graphBackbone(graph);
71
+ if (!backbone) {
72
+ return [];
73
+ }
74
+ const file = resolve(genes.file);
75
+ const format = genes.format ?? (/\.bed(\.gz)?$/i.test(genes.file) ? "bed" : "gff3");
76
+ if (!genes.index) {
77
+ return featuresOnBackbone(genesFromText(await text(file)), backbone);
78
+ }
79
+ const index = resolve(genes.index);
80
+ const csi = index.endsWith(".csi");
81
+ const tabix = new TabixIndexedFile(
82
+ isUrl(file) ? { url: file, ...csi ? { csiUrl: index } : { tbiUrl: index } } : { path: file, ...csi ? { csiPath: index } : { tbiPath: index } }
77
83
  );
78
- const samples = await referenceSamplesOf(base);
79
- const sample = resolveReferenceSample({
80
- configured: "",
81
- anchorPrefix: gbz.referenceSample ?? samples[0] ?? "",
82
- referenceSamples: samples
83
- });
84
- const region = parseRegion(gbz.region);
85
- const query = await referencePathQuery(base, sample, region.refName);
86
- if (!query) {
87
- throw new Error(`${sample} has no indexed path named ${region.refName}`);
84
+ const indexed = new Set(await tabix.getReferenceSequenceNames());
85
+ const lines = [];
86
+ for (const contig of backbone.contigs) {
87
+ const refName = [contig.contig, contig.refName].find((n) => indexed.has(n));
88
+ if (refName !== void 0) {
89
+ await tabix.getLines(refName, contig.start, contig.end, {
90
+ lineCallback: (line) => lines.push(line)
91
+ });
92
+ }
88
93
  }
89
- const wanted = gbz.haplotypes?.length ? gbz.haplotypes : void 0;
90
- const text = await cutWindowGFA(base, query, region.start, region.end, {
91
- context: 1e3,
92
- snarls: "contained",
93
- limit: WALK_LIMIT,
94
- ...wanted ? { keep: (name) => haplotypeWanted(name, wanted) } : {}
95
- });
96
- return { text, region, name: `${sample} ${gbz.region}` };
94
+ return featuresOnBackbone(
95
+ format === "gff3" ? genesFromGff3Lines(lines) : genesFromBed(lines.join("\n")),
96
+ backbone
97
+ );
97
98
  }
98
99
  var engine = async (request) => {
99
100
  const bandage = await loadBandage();
@@ -101,15 +102,54 @@ var engine = async (request) => {
101
102
  const result = bandage.computeLayout(request.graph, request.options);
102
103
  return { result, duration: performance.now() - start };
103
104
  };
104
- async function renderSpec(spec, base = ".") {
105
- const source = spec.gbz ? await cutGbz(spec.gbz) : spec.gfa ? {
106
- text: await gfaText(
107
- isUrl(spec.gfa) ? spec.gfa : path.resolve(base, spec.gfa)
108
- ),
109
- region: spec.region ? parseRegion(spec.region) : void 0,
110
- name: path.basename(spec.gfa)
111
- } : void 0;
112
- if (!source) {
105
+ async function exists(location) {
106
+ try {
107
+ await (isUrl(location) ? fetchOk(location, { method: "HEAD" }) : access(location));
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+ async function siblingIndex(db) {
114
+ const sibling = db.replace(/\.gbz\.db$/i, ".haplotype-index.db");
115
+ return sibling !== db && await exists(sibling) ? sibling : void 0;
116
+ }
117
+ var UNDRAWABLE = /* @__PURE__ */ new Set(["walkrows", "tubemap", "tubemapref"]);
118
+ function checkLayout(layout) {
119
+ if (layout === void 0) {
120
+ return;
121
+ }
122
+ if (!LAYOUT_MODE_VALUES.includes(layout)) {
123
+ throw new Error(
124
+ `unknown layout "${layout}": one of ${LAYOUT_MODE_VALUES.filter((v) => !UNDRAWABLE.has(v)).join(", ")}`
125
+ );
126
+ }
127
+ if (UNDRAWABLE.has(layout)) {
128
+ throw new Error(`a figure cannot draw the "${layout}" layout yet`);
129
+ }
130
+ }
131
+ async function renderSpec(spec, base) {
132
+ checkLayout(spec.layout);
133
+ const resolve = (location) => isUrl(location) ? location : path.resolve(base, location);
134
+ let source;
135
+ if (spec.gbz) {
136
+ const { db, index } = spec.gbz.db === "hprc" ? HPRC_GBZ : spec.gbz;
137
+ const indexLocation = index ? resolve(index) : await siblingIndex(resolve(db));
138
+ const cut = await cutGbzRegion(
139
+ await openGbz(
140
+ byteSource(resolve(db)),
141
+ indexLocation ? byteSource(indexLocation) : void 0
142
+ ),
143
+ spec.gbz
144
+ );
145
+ source = { ...cut, name: `${cut.sample} ${spec.gbz.region}` };
146
+ } else if (spec.gfa) {
147
+ source = {
148
+ text: await text(resolve(spec.gfa)),
149
+ name: path.basename(spec.gfa),
150
+ region: spec.region ? parseRegion(spec.region) : void 0
151
+ };
152
+ } else {
113
153
  throw new Error("a spec names its graph as `gfa` or `gbz`");
114
154
  }
115
155
  const graph = loadGraph(source.text, source.name, {
@@ -128,17 +168,36 @@ async function renderSpec(spec, base = ".") {
128
168
  ...spec,
129
169
  walks: spec.walks?.map((w) => typeof w === "string" ? { walk: w } : w),
130
170
  region: source.region,
131
- metadata: JSON.stringify(spec)
171
+ genes: spec.genes ? await readGenes(spec.genes, graph, resolve) : void 0,
172
+ spec
132
173
  });
133
174
  }
175
+ var USAGE = "usage: bandage-figure <spec.json> [-o figure.svg]";
176
+ function args() {
177
+ try {
178
+ return parseArgs({
179
+ allowPositionals: true,
180
+ options: {
181
+ out: { type: "string", short: "o" },
182
+ help: { type: "boolean", short: "h" }
183
+ }
184
+ });
185
+ } catch (e) {
186
+ console.error(`${e instanceof Error ? e.message : String(e)}
187
+ ${USAGE}`);
188
+ process.exit(2);
189
+ }
190
+ }
134
191
  async function main() {
135
- const { positionals, values } = parseArgs({
136
- allowPositionals: true,
137
- options: { out: { type: "string", short: "o" } }
138
- });
192
+ const { positionals, values } = args();
193
+ if (values.help) {
194
+ process.stdout.write(`${USAGE}
195
+ `);
196
+ return;
197
+ }
139
198
  const file = positionals[0];
140
199
  if (!file) {
141
- console.error("usage: bandage-figure <spec.json> [-o figure.svg]");
200
+ console.error(USAGE);
142
201
  process.exit(2);
143
202
  }
144
203
  const spec = JSON.parse(await readFile(file, "utf8"));
@@ -149,4 +208,9 @@ async function main() {
149
208
  process.stdout.write(svg);
150
209
  }
151
210
  }
152
- await main();
211
+ try {
212
+ await main();
213
+ } catch (e) {
214
+ console.error(`bandage-figure: ${e instanceof Error ? e.message : String(e)}`);
215
+ process.exit(1);
216
+ }