@jbrowse/plugin-blat 4.3.0

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.
Files changed (42) hide show
  1. package/LICENSE +201 -0
  2. package/esm/BlatDialog.d.ts +6 -0
  3. package/esm/BlatDialog.js +93 -0
  4. package/esm/IsPcrDialog.d.ts +6 -0
  5. package/esm/IsPcrDialog.js +101 -0
  6. package/esm/MonospaceTextField.d.ts +2 -0
  7. package/esm/MonospaceTextField.js +8 -0
  8. package/esm/UcscQueryActions.d.ts +8 -0
  9. package/esm/UcscQueryActions.js +19 -0
  10. package/esm/UcscQueryFields.d.ts +8 -0
  11. package/esm/UcscQueryFields.js +31 -0
  12. package/esm/UcscQueryStatus.d.ts +5 -0
  13. package/esm/UcscQueryStatus.js +15 -0
  14. package/esm/UcscResultsWidget/UcscResultsWidget.d.ts +5 -0
  15. package/esm/UcscResultsWidget/UcscResultsWidget.js +23 -0
  16. package/esm/UcscResultsWidget/configSchema.d.ts +1 -0
  17. package/esm/UcscResultsWidget/configSchema.js +2 -0
  18. package/esm/UcscResultsWidget/hitSummary.d.ts +9 -0
  19. package/esm/UcscResultsWidget/hitSummary.js +31 -0
  20. package/esm/UcscResultsWidget/index.d.ts +2 -0
  21. package/esm/UcscResultsWidget/index.js +13 -0
  22. package/esm/UcscResultsWidget/stateModel.d.ts +42 -0
  23. package/esm/UcscResultsWidget/stateModel.js +41 -0
  24. package/esm/blatQuery.d.ts +75 -0
  25. package/esm/blatQuery.js +245 -0
  26. package/esm/desktopBlat.d.ts +9 -0
  27. package/esm/desktopBlat.js +22 -0
  28. package/esm/index.d.ts +7 -0
  29. package/esm/index.js +37 -0
  30. package/esm/ispcrQuery.d.ts +59 -0
  31. package/esm/ispcrQuery.js +160 -0
  32. package/esm/ispcrToSam.d.ts +9 -0
  33. package/esm/ispcrToSam.js +86 -0
  34. package/esm/pslToSam.d.ts +54 -0
  35. package/esm/pslToSam.js +153 -0
  36. package/esm/ucscDbMap.d.ts +2 -0
  37. package/esm/ucscDbMap.js +25 -0
  38. package/esm/ucscShared.d.ts +40 -0
  39. package/esm/ucscShared.js +116 -0
  40. package/esm/useUcscQuery.d.ts +40 -0
  41. package/esm/useUcscQuery.js +199 -0
  42. package/package.json +54 -0
@@ -0,0 +1,160 @@
1
+ import { challengeError, isChallengePage } from './blatQuery.js';
2
+ // UCSC hgPcr (In-Silico PCR). Unlike hgBlat there is no output=json mode; a
3
+ // successful query returns an HTML page whose <PRE> block holds FASTA amplicons
4
+ // with headers like:
5
+ // >chr9:132576352+132576623 272bp GTGACGTCG... CCTAGGTTG...
6
+ // where the sign between the two 1-based coordinates is the product strand and
7
+ // the two trailing tokens are the forward and reverse primers as submitted.
8
+ //
9
+ // Not anchored to the start of a line. The whole shape — a '>', a locus with a
10
+ // strand sign inside it, a bp count, two primers — is specific enough on its
11
+ // own, and the anchor was a standing bet that markup never precedes the '>' on
12
+ // its line. `<PRE>` opening on the same line as the first amplicon is all it
13
+ // would take to lose that product, and this parser has already shipped one
14
+ // silent zero-products bug of exactly that kind (see the anchored-header test).
15
+ // A '>' still may not reach across a newline to claim a header below it.
16
+ const AMPLICON_HEADER = />[^\S\n]*(\S+):(\d+)([+-])(\d+)\s+(\d+)bp\s+(\S+)\s+(\S+)/g;
17
+ export const UCSC_ISPCR_URL = 'https://genome.ucsc.edu/cgi-bin/hgPcr';
18
+ // The jbrowse.org proxy, for the same reasons as DEFAULT_BLAT_URL: it holds the
19
+ // apiKey, so a first query costs neither a key nor a CAPTCHA, and desktop keeps
20
+ // UCSC_ISPCR_URL in reserve for when the proxy cannot serve.
21
+ export const DEFAULT_ISPCR_URL = 'https://api.jbrowse.org/ucsc/v1/ispcr';
22
+ // hgPcr's default cap; also the server-side max the CGI accepts without error
23
+ export const DEFAULT_MAX_PRODUCT_SIZE = 4000;
24
+ // primers shorter than this are rejected by hgPcr as too unspecific
25
+ export const MINIMUM_PRIMER_LENGTH = 15;
26
+ /**
27
+ * The two primer footprints, low coordinate first.
28
+ *
29
+ * A primer pair converges: the footprint at the low coordinate reads rightward
30
+ * and the one at the high coordinate reads leftward, whichever of the two the
31
+ * submitter called forward. Only *which* is which follows the product strand —
32
+ * on a plus product the forward primer sits at the low end, on a minus product
33
+ * it sits at the high end.
34
+ *
35
+ * Each footprint carries the primer's own bases, which is what lets a mismatch
36
+ * against the template be drawn rather than a perfect anneal assumed. They are
37
+ * as submitted, i.e. 5'->3' on the strand that primer anneals to, so a footprint
38
+ * at the low end reads on the reference's plus strand and one at the high end
39
+ * has to be reverse-complemented to sit on it ({@link ispcrToSam} does that).
40
+ */
41
+ export function productFootprints(product) {
42
+ const { start, end, strand, forwardPrimer, reversePrimer } = product;
43
+ const lowIsForward = strand === 1;
44
+ const lowPrimer = lowIsForward ? forwardPrimer : reversePrimer;
45
+ const highPrimer = lowIsForward ? reversePrimer : forwardPrimer;
46
+ return {
47
+ low: {
48
+ name: lowIsForward ? 'forward primer' : 'reverse primer',
49
+ role: lowIsForward ? 'fwd' : 'rev',
50
+ start,
51
+ end: start + lowPrimer.length,
52
+ primer: lowPrimer,
53
+ },
54
+ high: {
55
+ name: lowIsForward ? 'reverse primer' : 'forward primer',
56
+ role: lowIsForward ? 'rev' : 'fwd',
57
+ start: end - highPrimer.length,
58
+ end,
59
+ primer: highPrimer,
60
+ },
61
+ };
62
+ }
63
+ export function parseIsPcrProducts(text) {
64
+ // a successful result is HTML (FASTA inside <PRE>), and so are the "no
65
+ // results" and Cloudflare Turnstile pages, so we can't reject on the leading
66
+ // '<' the way BLAT's JSON path does. Scan for amplicon headers first; only if
67
+ // none are found do we distinguish a challenge from a genuine empty result,
68
+ // which avoids mistaking incidental "cf-"/"challenge" markup on a real result
69
+ // page for a CAPTCHA
70
+ //
71
+ // hgPcr links each amplicon's position to the browser, so a real header is
72
+ // &gt;<A HREF="../cgi-bin/hgTracks?...">chr17:7676521+7676667</A> 147bp ...
73
+ // and the position never followed the '>' directly. Strip markup before
74
+ // decoding entities, in that order: tags first, so escaped text can't be read
75
+ // as a tag, then '&gt;' becomes the '>' the header starts with.
76
+ const decoded = text
77
+ .replaceAll(/<[^>]*>/g, '')
78
+ .replaceAll('&gt;', '>')
79
+ .replaceAll('&lt;', '<');
80
+ const products = [];
81
+ for (const match of decoded.matchAll(AMPLICON_HEADER)) {
82
+ const [, refName, startStr, sign, endStr, sizeStr, fwd, rev] = match;
83
+ products.push({
84
+ refName: refName,
85
+ // headers are 1-based inclusive; convert to interbase half-open
86
+ start: Number(startStr) - 1,
87
+ end: Number(endStr),
88
+ strand: sign === '-' ? -1 : 1,
89
+ size: Number(sizeStr),
90
+ forwardPrimer: fwd,
91
+ reversePrimer: rev,
92
+ });
93
+ }
94
+ // the raw text, not `decoded`: a challenge page IS markup, so stripping tags
95
+ // would throw away the very thing that identifies it
96
+ if (!products.length && isChallengePage(text)) {
97
+ throw challengeError();
98
+ }
99
+ return products;
100
+ }
101
+ export function pcrProductsToFeatures(products) {
102
+ return products.map(product => {
103
+ const { refName, start, end, strand, size } = product;
104
+ const uniqueId = `ispcr-${refName}-${start}-${end}-${strand}`;
105
+ const { low, high } = productFootprints(product);
106
+ return {
107
+ uniqueId,
108
+ refName,
109
+ start,
110
+ end,
111
+ strand,
112
+ type: 'PCR_product',
113
+ name: `${size} bp`,
114
+ forwardPrimer: product.forwardPrimer,
115
+ reversePrimer: product.reversePrimer,
116
+ // the footprints converge, so their strands are +1 and -1 whatever the
117
+ // product's is: a primer's direction is its own, not the amplicon's
118
+ subfeatures: [
119
+ {
120
+ uniqueId: `${uniqueId}-${low.role}`,
121
+ name: low.name,
122
+ refName,
123
+ start: low.start,
124
+ end: low.end,
125
+ strand: 1,
126
+ type: 'primer',
127
+ },
128
+ {
129
+ uniqueId: `${uniqueId}-${high.role}`,
130
+ name: high.name,
131
+ refName,
132
+ start: high.start,
133
+ end: high.end,
134
+ strand: -1,
135
+ type: 'primer',
136
+ },
137
+ ],
138
+ };
139
+ });
140
+ }
141
+ export function parseIsPcrResponse(text) {
142
+ return pcrProductsToFeatures(parseIsPcrProducts(text));
143
+ }
144
+ export function buildIsPcrBody({ db, forwardPrimer, reversePrimer, maxProductSize = DEFAULT_MAX_PRODUCT_SIZE, apiKey, }) {
145
+ const params = new URLSearchParams({
146
+ db,
147
+ wp_target: 'genome',
148
+ wp_f: forwardPrimer,
149
+ wp_r: reversePrimer,
150
+ wp_size: String(maxProductSize),
151
+ wp_perfect: '15',
152
+ wp_good: '15',
153
+ 'boolshad.wp_flipReverse': '0',
154
+ });
155
+ // as with hgBlat, an account apiKey bypasses the Cloudflare Turnstile
156
+ if (apiKey) {
157
+ params.set('apiKey', apiKey);
158
+ }
159
+ return params.toString();
160
+ }
@@ -0,0 +1,9 @@
1
+ import type { PcrProduct } from './ispcrQuery.ts';
2
+ /**
3
+ * A set of hgPcr products as SAM text, ready to hand a SamAdapter as `samText`.
4
+ *
5
+ * No `@SQ` lines: hgPcr's page states no chromosome sizes, and inventing one
6
+ * would be a lie the adapter then reports as the reference length. A headerless
7
+ * SAM takes its reference names from the records instead.
8
+ */
9
+ export declare function ispcrToSam(products: PcrProduct[]): string;
@@ -0,0 +1,86 @@
1
+ import { revcom } from '@jbrowse/core/util';
2
+ import { productFootprints } from './ispcrQuery.js';
3
+ /**
4
+ * A PCR product has the same shape as a paired-end read: two short oriented
5
+ * footprints pointing at each other, with an unsequenced insert between them.
6
+ * So each product becomes a read pair — the primers are the mates, the amplicon
7
+ * interior is the insert, and TLEN is the product size, which is the band the
8
+ * bench sees on a gel. Shown in an alignments track with view-as-pairs on, that
9
+ * draws the arrows-facing-inward glyph a primer pair is always drawn as, and it
10
+ * comes with the pairing machinery (insert-size sort and color) already attached.
11
+ *
12
+ * The other thing the pair buys is primer *quality*: hgPcr accepts imperfect
13
+ * matches, and a mismatch near a primer's 3' end is a real reason an
14
+ * amplification fails. SEQ carries the primer's own bases in reference
15
+ * orientation, so the pileup marks every base that disagrees with the template
16
+ * rather than leaving the reader to assume a perfect anneal.
17
+ */
18
+ // SAM FLAG bits
19
+ const PAIRED = 1;
20
+ const PROPER = 2;
21
+ const REVERSE = 16;
22
+ const MATE_REVERSE = 32;
23
+ const FIRST = 64;
24
+ const SECOND = 128;
25
+ // BLAT and hgPcr both report a match, not a mapping probability, so a MAPQ here
26
+ // would be invented; 255 is the spec's "unavailable".
27
+ const MAPQ = 255;
28
+ function samLine(fields) {
29
+ return fields.join('\t');
30
+ }
31
+ /**
32
+ * The two mates of one product. Names are per-product and unique: two products
33
+ * sharing a QNAME would be read as one pair spanning both of them.
34
+ */
35
+ function productToSamLines(product) {
36
+ const { refName, start, end, strand } = product;
37
+ const { low, high } = productFootprints(product);
38
+ const size = end - start;
39
+ const lowPos = low.start + 1;
40
+ const highPos = high.start + 1;
41
+ const qName = `${refName}:${lowPos}-${end}_${size}bp`;
42
+ // the forward primer is read 1, which is at the low end of a plus product and
43
+ // the high end of a minus one
44
+ const lowIsForward = strand === 1;
45
+ return [
46
+ samLine([
47
+ qName,
48
+ PAIRED | PROPER | MATE_REVERSE | (lowIsForward ? FIRST : SECOND),
49
+ refName,
50
+ lowPos,
51
+ MAPQ,
52
+ `${low.primer.length}M`,
53
+ '=',
54
+ highPos,
55
+ size,
56
+ // a footprint at the low end matches the plus strand as submitted
57
+ low.primer,
58
+ '*',
59
+ ]),
60
+ samLine([
61
+ qName,
62
+ PAIRED | PROPER | REVERSE | (lowIsForward ? SECOND : FIRST),
63
+ refName,
64
+ highPos,
65
+ MAPQ,
66
+ `${high.primer.length}M`,
67
+ '=',
68
+ lowPos,
69
+ -size,
70
+ // SEQ is always reference-forward, and the high footprint's primer anneals
71
+ // to the other strand, so it is stored reverse-complemented
72
+ revcom(high.primer),
73
+ '*',
74
+ ]),
75
+ ];
76
+ }
77
+ /**
78
+ * A set of hgPcr products as SAM text, ready to hand a SamAdapter as `samText`.
79
+ *
80
+ * No `@SQ` lines: hgPcr's page states no chromosome sizes, and inventing one
81
+ * would be a lie the adapter then reports as the reference length. A headerless
82
+ * SAM takes its reference names from the records instead.
83
+ */
84
+ export function ispcrToSam(products) {
85
+ return ['@HD\tVN:1.6', ...products.flatMap(productToSamLines), ''].join('\n');
86
+ }
@@ -0,0 +1,54 @@
1
+ import type { PslRow } from './blatQuery.ts';
2
+ /**
3
+ * PSL states a hit as a list of aligned blocks in both query and target
4
+ * coordinates, which is exactly the information a CIGAR encodes: each block is
5
+ * an M run, the target gap between two blocks is a D, the query gap an I, and
6
+ * the unaligned query ends are soft clips.
7
+ *
8
+ * D rather than N for every target gap: a DNA BLAT hit cannot tell an intron
9
+ * from a deletion (a spliced-mRNA query would want N, a diverged genomic query
10
+ * D), and psl2sam takes the same position. The gap is drawn either way.
11
+ *
12
+ * Coordinates are the ones PSL states, which for a `-` strand hit means
13
+ * `blockSizes`/`qStarts` are in reverse-complement query space — the same space
14
+ * as the SEQ a reverse-strand SAM record carries. That correspondence is what
15
+ * makes the soft clips and the per-base mismatches land in the right place, so
16
+ * {@link pslQuerySeq} reverse-complements to match.
17
+ */
18
+ export declare function pslToCigar({ blockSizes, qStarts, tStarts, qSize, }: {
19
+ blockSizes: number[];
20
+ qStarts: number[];
21
+ tStarts: number[];
22
+ qSize: number;
23
+ }): string;
24
+ /**
25
+ * The submitted query in the orientation the hit aligned it, which is what SAM's
26
+ * SEQ column holds. Answers "do I have the bases this row describes", so it
27
+ * checks the length the row states as well as the name: the CIGAR spans `qSize`
28
+ * query bases by construction, and bases of any other length would sit out of
29
+ * register against the reference, drawing mismatches the whole way along a hit
30
+ * BLAT scored as near-perfect. Returns undefined for both misses, and the record
31
+ * then carries `*` and renders its block structure without per-base mismatches.
32
+ */
33
+ export declare function pslQuerySeq(row: PslRow, querySequences: Map<string, string>): string | undefined;
34
+ /**
35
+ * A PSL result set as SAM text, ready to hand to a SamAdapter as `samText`.
36
+ * `@SQ` lines come from the target sizes PSL already states, so the track knows
37
+ * its reference names without consulting the assembly.
38
+ *
39
+ * Rows arrive best-first (pslToFeatures sorts by score), which is what decides
40
+ * each query's primary alignment.
41
+ */
42
+ export declare function pslToSam(rows: PslRow[], querySequences: Map<string, string>): string;
43
+ /**
44
+ * The submitted query text as name -> residues. hgBlat places each FASTA record
45
+ * separately and labels its hits with that record's name, so this is the map
46
+ * from a hit back to the bases it was made of.
47
+ *
48
+ * Keeps only letters, which is what Kent's FASTA reader counts: a pasted
49
+ * sequence carrying line numbers, or alignment-gap dashes, is those bases to
50
+ * BLAT and so `qSize` excludes them. Stripping only whitespace left our text
51
+ * longer than the hit it describes, which is exactly the off-by-N that puts
52
+ * every base out of register.
53
+ */
54
+ export declare function parseQuerySequences(text: string): Map<string, string>;
@@ -0,0 +1,153 @@
1
+ import { revcom } from '@jbrowse/core/util';
2
+ /**
3
+ * PSL states a hit as a list of aligned blocks in both query and target
4
+ * coordinates, which is exactly the information a CIGAR encodes: each block is
5
+ * an M run, the target gap between two blocks is a D, the query gap an I, and
6
+ * the unaligned query ends are soft clips.
7
+ *
8
+ * D rather than N for every target gap: a DNA BLAT hit cannot tell an intron
9
+ * from a deletion (a spliced-mRNA query would want N, a diverged genomic query
10
+ * D), and psl2sam takes the same position. The gap is drawn either way.
11
+ *
12
+ * Coordinates are the ones PSL states, which for a `-` strand hit means
13
+ * `blockSizes`/`qStarts` are in reverse-complement query space — the same space
14
+ * as the SEQ a reverse-strand SAM record carries. That correspondence is what
15
+ * makes the soft clips and the per-base mismatches land in the right place, so
16
+ * {@link pslQuerySeq} reverse-complements to match.
17
+ */
18
+ export function pslToCigar({ blockSizes, qStarts, tStarts, qSize, }) {
19
+ // a row stating no blocks describes no alignment; '*' is SAM's word for that,
20
+ // where the empty string the loop would otherwise build is an unparseable
21
+ // CIGAR column
22
+ if (!blockSizes.length) {
23
+ return '*';
24
+ }
25
+ const ops = [];
26
+ const leadingClip = qStarts[0] ?? 0;
27
+ if (leadingClip > 0) {
28
+ ops.push(`${leadingClip}S`);
29
+ }
30
+ for (let i = 0; i < blockSizes.length; i++) {
31
+ if (i > 0) {
32
+ const queryGap = qStarts[i] - (qStarts[i - 1] + blockSizes[i - 1]);
33
+ const targetGap = tStarts[i] - (tStarts[i - 1] + blockSizes[i - 1]);
34
+ if (queryGap > 0) {
35
+ ops.push(`${queryGap}I`);
36
+ }
37
+ if (targetGap > 0) {
38
+ ops.push(`${targetGap}D`);
39
+ }
40
+ }
41
+ ops.push(`${blockSizes[i]}M`);
42
+ }
43
+ const last = blockSizes.length - 1;
44
+ const trailingClip = qSize - (qStarts[last] + blockSizes[last]);
45
+ if (trailingClip > 0) {
46
+ ops.push(`${trailingClip}S`);
47
+ }
48
+ return ops.join('');
49
+ }
50
+ /**
51
+ * The submitted query in the orientation the hit aligned it, which is what SAM's
52
+ * SEQ column holds. Answers "do I have the bases this row describes", so it
53
+ * checks the length the row states as well as the name: the CIGAR spans `qSize`
54
+ * query bases by construction, and bases of any other length would sit out of
55
+ * register against the reference, drawing mismatches the whole way along a hit
56
+ * BLAT scored as near-perfect. Returns undefined for both misses, and the record
57
+ * then carries `*` and renders its block structure without per-base mismatches.
58
+ */
59
+ export function pslQuerySeq(row, querySequences) {
60
+ const seq = querySequences.get(row.qName) ??
61
+ // hgBlat labels a headerless query "YourSeq"; a single submitted record is
62
+ // unambiguously the one every hit came from whatever it ended up named
63
+ (querySequences.size === 1 ? [...querySequences.values()][0] : undefined);
64
+ return seq?.length === row.qSize
65
+ ? row.strand < 0
66
+ ? revcom(seq)
67
+ : seq
68
+ : undefined;
69
+ }
70
+ // Kent's psl2sam: the edit distance is the mismatched bases plus the bases
71
+ // inserted on either side.
72
+ function editDistance(row) {
73
+ return row.misMatches + row.qBaseInsert + row.tBaseInsert;
74
+ }
75
+ /**
76
+ * One SAM alignment line per PSL row. `bestOf` names the queries whose leading
77
+ * hit has already been emitted: BLAT returns every placement of a query, and
78
+ * all but its best are secondary alignments (0x100) — competing mappings of one
79
+ * sequence, which is what keeps them off the best hit's row in the pileup.
80
+ * MAPQ is 255, the spec's "unavailable": BLAT reports identity, not a mapping
81
+ * probability, so any number here would be invented.
82
+ */
83
+ function pslRowToSamLine(row, querySequences, bestOf) {
84
+ const secondary = bestOf.has(row.qName);
85
+ bestOf.add(row.qName);
86
+ const seq = pslQuerySeq(row, querySequences);
87
+ return [
88
+ row.qName,
89
+ (row.strand < 0 ? 16 : 0) | (secondary ? 256 : 0),
90
+ row.tName,
91
+ row.tStart + 1,
92
+ 255,
93
+ pslToCigar(row),
94
+ '*',
95
+ 0,
96
+ 0,
97
+ seq ?? '*',
98
+ '*',
99
+ `NM:i:${editDistance(row)}`,
100
+ `AS:i:${row.score}`,
101
+ ].join('\t');
102
+ }
103
+ /**
104
+ * A PSL result set as SAM text, ready to hand to a SamAdapter as `samText`.
105
+ * `@SQ` lines come from the target sizes PSL already states, so the track knows
106
+ * its reference names without consulting the assembly.
107
+ *
108
+ * Rows arrive best-first (pslToFeatures sorts by score), which is what decides
109
+ * each query's primary alignment.
110
+ */
111
+ export function pslToSam(rows, querySequences) {
112
+ const targetSizes = new Map(rows.map(row => [row.tName, row.tSize]));
113
+ const bestOf = new Set();
114
+ return [
115
+ '@HD\tVN:1.6',
116
+ ...[...targetSizes].map(([name, size]) => `@SQ\tSN:${name}\tLN:${size}`),
117
+ ...rows.map(row => pslRowToSamLine(row, querySequences, bestOf)),
118
+ '',
119
+ ].join('\n');
120
+ }
121
+ /**
122
+ * The submitted query text as name -> residues. hgBlat places each FASTA record
123
+ * separately and labels its hits with that record's name, so this is the map
124
+ * from a hit back to the bases it was made of.
125
+ *
126
+ * Keeps only letters, which is what Kent's FASTA reader counts: a pasted
127
+ * sequence carrying line numbers, or alignment-gap dashes, is those bases to
128
+ * BLAT and so `qSize` excludes them. Stripping only whitespace left our text
129
+ * longer than the hit it describes, which is exactly the off-by-N that puts
130
+ * every base out of register.
131
+ */
132
+ export function parseQuerySequences(text) {
133
+ const sequences = new Map();
134
+ let name = 'YourSeq';
135
+ let residues = [];
136
+ const flush = () => {
137
+ if (residues.length) {
138
+ sequences.set(name, residues.join(''));
139
+ }
140
+ };
141
+ for (const line of text.split('\n')) {
142
+ if (line.startsWith('>')) {
143
+ flush();
144
+ name = /^>\s*(\S+)/.exec(line)?.[1] ?? 'YourSeq';
145
+ residues = [];
146
+ }
147
+ else {
148
+ residues.push(line.replaceAll(/[^A-Za-z]/g, ''));
149
+ }
150
+ }
151
+ flush();
152
+ return sequences;
153
+ }
@@ -0,0 +1,2 @@
1
+ export declare function assemblyToUcscDb(name: string): string;
2
+ export declare function looksLikeUcscDb(db: string): boolean;
@@ -0,0 +1,25 @@
1
+ // jb2hubs assemblies are frequently already named with UCSC db ids (hg38, mm39,
2
+ // …) so identity is the default. This covers the common GRC/T2T aliases that
3
+ // aren't literal UCSC db names.
4
+ const aliases = {
5
+ GRCh38: 'hg38',
6
+ GRCh37: 'hg19',
7
+ GRCm39: 'mm39',
8
+ GRCm38: 'mm10',
9
+ 'T2T-CHM13v2.0': 'hs1',
10
+ 'T2T-CHM13': 'hs1',
11
+ chm13: 'hs1',
12
+ };
13
+ export function assemblyToUcscDb(name) {
14
+ return aliases[name] ?? name;
15
+ }
16
+ // UCSC db names are a lowercase organism prefix plus a version (hg38, mm39,
17
+ // danRer11, hs1), and GenArk accessions resolve on hgBlat directly. Neither
18
+ // shape proves UCSC hosts the db, but a name outside both — a genome opened
19
+ // from local files, "my_assembly" — certainly isn't one, and that is worth
20
+ // saying before a query goes out rather than after it comes back empty.
21
+ const UCSC_DB = /^[a-z][A-Za-z]*\d+$/;
22
+ const GENARK_ACCESSION = /^GC[AF]_\d+\.\d+$/;
23
+ export function looksLikeUcscDb(db) {
24
+ return UCSC_DB.test(db) || GENARK_ACCESSION.test(db);
25
+ }
@@ -0,0 +1,40 @@
1
+ import type { AbstractViewContainer, AssemblyHost, DialogHost, NotificationSink, SimpleFeatureSerialized } from '@jbrowse/core/util';
2
+ /**
3
+ * What this plugin asks of the host it is installed into: an assembly to
4
+ * resolve a UCSC db against, views to navigate, somewhere to report a hit it
5
+ * could not reach, and somewhere to put its two dialogs. Everything else it
6
+ * needs — adding the result track, opening the results panel — it asks for with
7
+ * a guard, because a host may not offer it.
8
+ */
9
+ export interface UcscHost extends AbstractViewContainer, AssemblyHost, NotificationSink, DialogHost {
10
+ }
11
+ export declare function ucscDbStamp(session: UcscHost, name: string): string | undefined;
12
+ export declare function resolveUcscDb(session: UcscHost, name: string): string;
13
+ /**
14
+ * Whether this host can draw a hit as an alignment rather than as plain blocks.
15
+ *
16
+ * Results are added as an AlignmentsTrack over a SamAdapter, which is what draws
17
+ * the blocks, the indels, the soft-clipped query ends and the per-base
18
+ * mismatches. SamAdapter arrived in JBrowse after v4.3.0, and an external plugin
19
+ * is loaded by whatever host a config points it at — including releases older
20
+ * than the plugin itself. Asking the host is the difference between quietly
21
+ * falling back to the block-structure track BLAT results always used to be, and
22
+ * adding a track whose adapter cannot resolve.
23
+ */
24
+ export declare function canRenderAlignments(session: UcscHost): boolean;
25
+ export declare function featureLocString(feature: SimpleFeatureSerialized): string;
26
+ export declare function navToFeature(session: UcscHost, assembly: string, feature: SimpleFeatureSerialized): Promise<void>;
27
+ export interface ResultTrackConf {
28
+ type: string;
29
+ adapter: Record<string, unknown>;
30
+ displayDefaults?: Record<string, unknown>;
31
+ }
32
+ export declare function addResultTrack({ session, assembly, features, trackIdPrefix, trackName, trackConf, resultNoun, }: {
33
+ session: UcscHost;
34
+ assembly: string;
35
+ features: SimpleFeatureSerialized[];
36
+ trackIdPrefix: string;
37
+ trackName: string;
38
+ trackConf?: ResultTrackConf;
39
+ resultNoun?: 'hit' | 'product';
40
+ }): Promise<void>;
@@ -0,0 +1,116 @@
1
+ import { getConf } from '@jbrowse/core/configuration';
2
+ import { getEnv, isSessionModelWithWidgets, isSessionWithAddSessionTrack, } from '@jbrowse/core/util';
3
+ import { assemblyToUcscDb } from './ucscDbMap.js';
4
+ function isNavigableView(view) {
5
+ return ('showTrack' in view &&
6
+ typeof view.showTrack === 'function' &&
7
+ 'navToLocString' in view &&
8
+ typeof view.navToLocString === 'function');
9
+ }
10
+ // jb2hubs stamps the UCSC db on the assembly's sequence.metadata. Its presence
11
+ // is also the one positive proof that UCSC can search the assembly, so the
12
+ // dialogs read it directly to decide whether to warn.
13
+ export function ucscDbStamp(session, name) {
14
+ const assembly = session.assemblyManager.get(name);
15
+ const stamped = assembly
16
+ ? getConf(assembly, ['sequence', 'metadata', 'blatDb'])
17
+ : undefined;
18
+ return stamped;
19
+ }
20
+ // Prefers the stamp, falling back to the static alias map for assemblies whose
21
+ // configs predate it. Shared by the BLAT and in-silico PCR dialogs.
22
+ export function resolveUcscDb(session, name) {
23
+ const stamped = ucscDbStamp(session, name);
24
+ return stamped ? stamped : assemblyToUcscDb(name);
25
+ }
26
+ /**
27
+ * Whether this host can draw a hit as an alignment rather than as plain blocks.
28
+ *
29
+ * Results are added as an AlignmentsTrack over a SamAdapter, which is what draws
30
+ * the blocks, the indels, the soft-clipped query ends and the per-base
31
+ * mismatches. SamAdapter arrived in JBrowse after v4.3.0, and an external plugin
32
+ * is loaded by whatever host a config points it at — including releases older
33
+ * than the plugin itself. Asking the host is the difference between quietly
34
+ * falling back to the block-structure track BLAT results always used to be, and
35
+ * adding a track whose adapter cannot resolve.
36
+ */
37
+ export function canRenderAlignments(session) {
38
+ const { pluginManager } = getEnv(session);
39
+ return (pluginManager.adapterTypes.has('SamAdapter') &&
40
+ pluginManager.trackTypes.has('AlignmentsTrack'));
41
+ }
42
+ // Builds a locstring from a feature. Feature coordinates are interbase, so the
43
+ // start needs +1 to become the 1-based coordinate a locstring names.
44
+ export function featureLocString(feature) {
45
+ return `${feature.refName}:${feature.start + 1}-${feature.end}`;
46
+ }
47
+ function findNavigableView(session, assembly) {
48
+ const view = session.views.find(v => v.type === 'LinearGenomeView' && !!v.assemblyNames?.includes(assembly));
49
+ return view && isNavigableView(view) ? view : undefined;
50
+ }
51
+ // Navigates an open view to a hit, growing the window so it lands with flanking
52
+ // context rather than filling the viewport edge to edge. Shared by the
53
+ // post-query navigation and the results widget's per-hit links.
54
+ export async function navToFeature(session, assembly, feature) {
55
+ const view = findNavigableView(session, assembly);
56
+ const loc = featureLocString(feature);
57
+ if (view) {
58
+ try {
59
+ await view.navToLocString(loc, assembly, 0.2);
60
+ }
61
+ catch (e) {
62
+ // a hit on a refName the loaded assembly doesn't know (the UCSC db and
63
+ // the loaded assembly aren't the same build, or lack an alias) still
64
+ // belongs in the track — report the coordinates rather than dropping them
65
+ session.notify(`Could not navigate to ${loc}: ${e}`, 'warning');
66
+ }
67
+ }
68
+ else {
69
+ session.notify(`No open view displays ${assembly}`, 'warning');
70
+ }
71
+ }
72
+ // Turns a UCSC query result into an on-the-fly track, shows it, and opens the
73
+ // result list in the drawer. Deliberately does NOT navigate: "first" is the
74
+ // server's ordering, not the user's intent, so jumping there picks a result for
75
+ // them — an off-target product can take the view — and it throws away wherever
76
+ // they were when they asked. The drawer lists every result as a link, so going to
77
+ // one is a click, and which one is theirs to choose. Callers pass `features`
78
+ // best-first (pslToFeatures sorts by score; hgPcr returns its products in order).
79
+ export async function addResultTrack({ session, assembly, features, trackIdPrefix, trackName, trackConf, resultNoun, }) {
80
+ if (!isSessionWithAddSessionTrack(session)) {
81
+ throw new Error("Can't add tracks to this session");
82
+ }
83
+ const trackId = `${trackIdPrefix}-${Date.now()}`;
84
+ const resultTrack = trackConf ?? {
85
+ type: 'FeatureTrack',
86
+ adapter: { type: 'FromConfigAdapter', features },
87
+ };
88
+ session.addSessionTrackConf({
89
+ ...resultTrack,
90
+ trackId,
91
+ name: trackName,
92
+ assemblyNames: [assembly],
93
+ });
94
+ const view = findNavigableView(session, assembly);
95
+ if (view) {
96
+ view.showTrack(trackId);
97
+ }
98
+ else {
99
+ session.notify(`Added track "${trackId}" but no open view displays ${assembly}`, 'warning');
100
+ }
101
+ // #region showWidget
102
+ // addWidget constructs the widget and returns it; showWidget is what puts it
103
+ // in the drawer. The third argument is the initial state, so its keys are the
104
+ // properties UcscResultsWidget's state model declares. Not every session has
105
+ // a drawer — an embedded component may be built without one — so the guard is
106
+ // not optional.
107
+ if (isSessionModelWithWidgets(session)) {
108
+ session.showWidget(session.addWidget('UcscResultsWidget', 'ucscResults', {
109
+ features,
110
+ assembly,
111
+ trackName,
112
+ resultNoun,
113
+ }));
114
+ }
115
+ // #endregion
116
+ }