@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,42 @@
1
+ import type { SimpleFeatureSerialized } from '@jbrowse/core/util';
2
+ import type { Instance } from '@jbrowse/mobx-state-tree';
3
+ /**
4
+ * #stateModel UcscResultsWidget
5
+ * The hit table for one UCSC BLAT or in-silico PCR query. The features are the
6
+ * same ones the result track was built from, so the list and the track can't
7
+ * disagree.
8
+ */
9
+ export declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelType<{
10
+ /**
11
+ * #property
12
+ */
13
+ id: import("@jbrowse/mobx-state-tree").ISimpleType<string>;
14
+ /**
15
+ * #property
16
+ */
17
+ type: import("@jbrowse/mobx-state-tree").ILiteralType<"UcscResultsWidget">;
18
+ /**
19
+ * #property
20
+ * hits, best first (BLAT sorts by score; hgPcr returns products in order)
21
+ */
22
+ features: import("@jbrowse/mobx-state-tree").IType<SimpleFeatureSerialized[] | null | undefined, SimpleFeatureSerialized[], SimpleFeatureSerialized[]>;
23
+ /**
24
+ * #property
25
+ * assembly the hits are on, for navigating and for the header line
26
+ */
27
+ assembly: import("@jbrowse/mobx-state-tree").ISimpleType<string>;
28
+ /**
29
+ * #property
30
+ * name of the on-the-fly track these hits were added as
31
+ */
32
+ trackName: import("@jbrowse/mobx-state-tree").ISimpleType<string>;
33
+ /**
34
+ * #property
35
+ * what one result is called. A BLAT result is a hit; an hgPcr result is a
36
+ * product, which is a band on a gel — the word a bench scientist reasons in,
37
+ * and "2 hits" understates what two of them mean for a PCR run
38
+ */
39
+ resultNoun: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
40
+ }, {}, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree")._NotCustomized>;
41
+ export type UcscResultsWidgetStateModel = ReturnType<typeof stateModelFactory>;
42
+ export type UcscResultsWidgetModel = Instance<UcscResultsWidgetStateModel>;
@@ -0,0 +1,41 @@
1
+ import { types } from '@jbrowse/mobx-state-tree';
2
+ /**
3
+ * #stateModel UcscResultsWidget
4
+ * The hit table for one UCSC BLAT or in-silico PCR query. The features are the
5
+ * same ones the result track was built from, so the list and the track can't
6
+ * disagree.
7
+ */
8
+ export function stateModelFactory() {
9
+ return types.model('UcscResultsWidget', {
10
+ /**
11
+ * #property
12
+ */
13
+ id: types.identifier,
14
+ /**
15
+ * #property
16
+ */
17
+ type: types.literal('UcscResultsWidget'),
18
+ /**
19
+ * #property
20
+ * hits, best first (BLAT sorts by score; hgPcr returns products in order)
21
+ */
22
+ features: types.frozen([]),
23
+ /**
24
+ * #property
25
+ * assembly the hits are on, for navigating and for the header line
26
+ */
27
+ assembly: types.string,
28
+ /**
29
+ * #property
30
+ * name of the on-the-fly track these hits were added as
31
+ */
32
+ trackName: types.string,
33
+ /**
34
+ * #property
35
+ * what one result is called. A BLAT result is a hit; an hgPcr result is a
36
+ * product, which is a band on a gel — the word a bench scientist reasons in,
37
+ * and "2 hits" understates what two of them mean for a PCR run
38
+ */
39
+ resultNoun: types.optional(types.enumeration(['hit', 'product']), 'hit'),
40
+ });
41
+ }
@@ -0,0 +1,75 @@
1
+ import type { SimpleFeatureSerialized } from '@jbrowse/core/util';
2
+ interface BlatJsonResponse {
3
+ fields: string[];
4
+ blat: (string | number)[][];
5
+ genome?: string;
6
+ }
7
+ /**
8
+ * One PSL hit, with the column-name indirection resolved and the derived
9
+ * kent numbers (score, identity, coverage) computed once. Both consumers read
10
+ * this: the hit-list features, and the SAM conversion that turns the blocks into
11
+ * a CIGAR alignment.
12
+ */
13
+ export interface PslRow {
14
+ matches: number;
15
+ misMatches: number;
16
+ repMatches: number;
17
+ qNumInsert: number;
18
+ qBaseInsert: number;
19
+ tNumInsert: number;
20
+ tBaseInsert: number;
21
+ strand: 1 | -1;
22
+ qName: string;
23
+ qSize: number;
24
+ qStart: number;
25
+ qEnd: number;
26
+ tName: string;
27
+ tSize: number;
28
+ tStart: number;
29
+ tEnd: number;
30
+ blockSizes: number[];
31
+ qStarts: number[];
32
+ tStarts: number[];
33
+ score: number;
34
+ identity: number;
35
+ coverage: number;
36
+ }
37
+ export declare function parsePslRows(data: BlatJsonResponse): PslRow[];
38
+ export declare function pslToFeatures(rows: PslRow[]): SimpleFeatureSerialized[];
39
+ export declare const MINIMUM_BLAT_LENGTH = 20;
40
+ export declare const MAXIMUM_BLAT_LENGTH = 25000;
41
+ export declare const MAXIMUM_BLAT_QUERIES = 25;
42
+ export declare function stripFasta(seq: string): string;
43
+ export declare function fastaRecordCount(seq: string): number;
44
+ export declare function queryLabel(seq: string): string;
45
+ export declare function buildBlatBody({ db, seq, apiKey, }: {
46
+ db: string;
47
+ seq: string;
48
+ apiKey?: string;
49
+ }): string;
50
+ export declare class BlatChallengeError extends Error {
51
+ name: string;
52
+ }
53
+ export declare function isChallengePage(text: string): boolean;
54
+ export declare function challengeError(): BlatChallengeError;
55
+ export declare function parseBlatResponse(text: string): PslRow[];
56
+ export declare const UCSC_BLAT_URL = "https://genome.ucsc.edu/cgi-bin/hgBlat";
57
+ /**
58
+ * Where the dialog points when the user has not typed a server of their own:
59
+ * the jbrowse.org proxy, which injects a UCSC apiKey server-side and meters the
60
+ * budget everyone using it shares (`products/aws/blat-proxy`). That is what makes
61
+ * a first BLAT work with no key and no CAPTCHA.
62
+ *
63
+ * A browser has nowhere else to go — it cannot call genome.ucsc.edu at all (no
64
+ * CORS headers) and must not carry a key in a public bundle. Desktop can reach
65
+ * UCSC itself, since `blatFetch` runs in the main process, so it keeps
66
+ * {@link UCSC_BLAT_URL} in reserve two ways: as the fallback when the proxy is
67
+ * out of budget or down, and as where the server field moves outright once the
68
+ * user supplies an apiKey of their own. The proxy overwrites a client key with
69
+ * its own, so a key only ever spends against UCSC direct.
70
+ *
71
+ * Either way this is only a default — the dialog's server field overrides it,
72
+ * which is how someone runs their own proxy or their own gfServer.
73
+ */
74
+ export declare const DEFAULT_BLAT_URL = "https://api.jbrowse.org/ucsc/v1/blat";
75
+ export {};
@@ -0,0 +1,245 @@
1
+ // PSL packs the per-block coordinates as comma-separated lists with a trailing
2
+ // comma, e.g. "31,101,13,"
3
+ function parseCommaList(value) {
4
+ return value.split(',').filter(Boolean).map(Number);
5
+ }
6
+ // kent's pslCalcMilliBad with isMrna=true, which is what hgBlat's own hit table
7
+ // reports: a target span longer than the query is an intron rather than a
8
+ // penalty, and target inserts aren't charged. Integer-divided like the C
9
+ // original so the percentage matches the number UCSC prints.
10
+ function pslMilliBad({ matches, misMatches, repMatches, qNumInsert, qAliSize, tAliSize, }) {
11
+ const total = matches + repMatches + misMatches;
12
+ const sizeDif = Math.max(0, qAliSize - tAliSize);
13
+ return Math.min(qAliSize, tAliSize) <= 0 || total === 0
14
+ ? 0
15
+ : Math.trunc((1000 *
16
+ (misMatches + qNumInsert + Math.round(3 * Math.log(1 + sizeDif)))) /
17
+ total);
18
+ }
19
+ function pslRow(row, col) {
20
+ const str = (name) => String(row[col[name]] ?? '');
21
+ const num = (name) => Number(row[col[name]] ?? 0);
22
+ const matches = num('matches');
23
+ const misMatches = num('misMatches');
24
+ const repMatches = num('repMatches');
25
+ const qNumInsert = num('qNumInsert');
26
+ const tNumInsert = num('tNumInsert');
27
+ const qStart = num('qStart');
28
+ const qEnd = num('qEnd');
29
+ const qSize = num('qSize');
30
+ const tStart = num('tStart');
31
+ const tEnd = num('tEnd');
32
+ return {
33
+ matches,
34
+ misMatches,
35
+ repMatches,
36
+ qNumInsert,
37
+ qBaseInsert: num('qBaseInsert'),
38
+ tNumInsert,
39
+ tBaseInsert: num('tBaseInsert'),
40
+ strand: str('strand').startsWith('-') ? -1 : 1,
41
+ qName: str('qName'),
42
+ qSize,
43
+ qStart,
44
+ qEnd,
45
+ tName: str('tName'),
46
+ tSize: num('tSize'),
47
+ tStart,
48
+ tEnd,
49
+ blockSizes: parseCommaList(str('blockSizes')),
50
+ qStarts: parseCommaList(str('qStarts')),
51
+ tStarts: parseCommaList(str('tStarts')),
52
+ // kent's pslScore, which ranks the hit table: repeat matches count half
53
+ score: matches + (repMatches >> 1) - misMatches - qNumInsert - tNumInsert,
54
+ identity: 100 -
55
+ pslMilliBad({
56
+ matches,
57
+ misMatches,
58
+ repMatches,
59
+ qNumInsert,
60
+ qAliSize: qEnd - qStart,
61
+ tAliSize: tEnd - tStart,
62
+ }) /
63
+ 10,
64
+ // how much of the submitted sequence this hit accounts for — the other half
65
+ // of "did my sequence map here", since a high-identity hit over 10% of the
66
+ // query is not the locus you were looking for
67
+ coverage: qSize > 0 ? (100 * (qEnd - qStart)) / qSize : 0,
68
+ };
69
+ }
70
+ // Sorted by score descending, matching the order hgBlat lists its hit table in,
71
+ // so consumers can treat the first row as the best placement of the query.
72
+ export function parsePslRows(data) {
73
+ // JSON that is not a PSL table at all — a proxy or mirror relaying its own
74
+ // envelope, a kent JSON error — otherwise reached `.map` on undefined and
75
+ // surfaced a TypeError where the server's own words belong
76
+ if (!Array.isArray(data.fields) || !Array.isArray(data.blat)) {
77
+ throw new Error('BLAT server returned JSON without the expected fields/blat columns');
78
+ }
79
+ const col = Object.fromEntries(data.fields.map((f, i) => [f, i]));
80
+ return data.blat
81
+ .map(row => pslRow(row, col))
82
+ .sort((a, b) => b.score - a.score);
83
+ }
84
+ function pslRowToFeature(row, featureIndex) {
85
+ const { tName: refName, strand, identity, blockSizes, tStarts } = row;
86
+ const uniqueId = `blat-${featureIndex}`;
87
+ return {
88
+ uniqueId,
89
+ refName,
90
+ start: row.tStart,
91
+ end: row.tEnd,
92
+ strand,
93
+ type: 'match',
94
+ name: `${row.qName} ${identity.toFixed(1)}%`,
95
+ score: row.score,
96
+ identity: Number(identity.toFixed(1)),
97
+ coverage: Number(row.coverage.toFixed(1)),
98
+ matches: row.matches,
99
+ misMatches: row.misMatches,
100
+ queryName: row.qName,
101
+ queryStart: row.qStart,
102
+ queryEnd: row.qEnd,
103
+ querySize: row.qSize,
104
+ blockCount: blockSizes.length,
105
+ subfeatures: blockSizes.map((size, i) => ({
106
+ uniqueId: `${uniqueId}-block-${i}`,
107
+ refName,
108
+ start: tStarts[i],
109
+ end: tStarts[i] + size,
110
+ strand,
111
+ type: 'match_part',
112
+ })),
113
+ };
114
+ }
115
+ export function pslToFeatures(rows) {
116
+ return rows.map((row, i) => pslRowToFeature(row, i));
117
+ }
118
+ export const MINIMUM_BLAT_LENGTH = 20;
119
+ // UCSC rejects queries over 25kb server-side; enforce locally for a clear message
120
+ export const MAXIMUM_BLAT_LENGTH = 25000;
121
+ // hgBlat's per-submission cap on FASTA records
122
+ export const MAXIMUM_BLAT_QUERIES = 25;
123
+ // residues only, for the length limits. The query itself goes to the server
124
+ // verbatim: hgBlat parses FASTA and labels each hit with its record's name, so
125
+ // stripping headers here would fuse a multi-record paste into one chimeric
126
+ // query and throw away the names that tell the hits apart.
127
+ //
128
+ // Letters only, which is what kent's FASTA reader counts and therefore what the
129
+ // limits are stated in — a sequence pasted with line numbers or alignment-gap
130
+ // dashes is fewer bases to BLAT than it has characters. Counting whitespace out
131
+ // but digits in measured a 25kb-limited query against a number the server does
132
+ // not use. Same rule as parseQuerySequences, which has to agree with `qSize`.
133
+ export function stripFasta(seq) {
134
+ return seq
135
+ .split('\n')
136
+ .filter(line => !line.startsWith('>'))
137
+ .join('')
138
+ .replaceAll(/[^A-Za-z]/g, '');
139
+ }
140
+ // hgBlat places each FASTA record separately, so records are queries
141
+ export function fastaRecordCount(seq) {
142
+ const headers = seq.match(/^>/gm);
143
+ return headers ? headers.length : 1;
144
+ }
145
+ // names the result track after what was searched: the first FASTA header, or
146
+ // the leading bases of a bare sequence
147
+ export function queryLabel(seq) {
148
+ const header = /^>\s*(\S+)/m.exec(seq);
149
+ const residues = stripFasta(seq);
150
+ return header
151
+ ? header[1]
152
+ : residues.slice(0, 12) + (residues.length > 12 ? '…' : '');
153
+ }
154
+ export function buildBlatBody({ db, seq, apiKey, }) {
155
+ const params = new URLSearchParams({
156
+ userSeq: seq,
157
+ type: 'DNA',
158
+ db,
159
+ output: 'json',
160
+ });
161
+ // UCSC removed open programmatic BLAT access in 2025; an account apiKey
162
+ // (Genome Browser account → Hub Development → API key) bypasses the
163
+ // Cloudflare Turnstile that otherwise fronts hgBlat
164
+ if (apiKey) {
165
+ params.set('apiKey', apiKey);
166
+ }
167
+ return params.toString();
168
+ }
169
+ // thrown when the server returns its CAPTCHA challenge page instead of results
170
+ // — the caller surfaces a "solve challenge" affordance for this case (and, when
171
+ // available, an apiKey avoids the challenge entirely)
172
+ export class BlatChallengeError extends Error {
173
+ name = 'BlatChallengeError';
174
+ }
175
+ // Markers on a Cloudflare Turnstile challenge page. Every marker has to be
176
+ // specific to the challenge itself: a bare `cf-`, `captcha` or `challenge` also
177
+ // matches an ordinary UCSC page, whose standard nav links to
178
+ // FAQdownloads.html#CAPTCHA and whose scripts load from challenges.cloudflare.com.
179
+ // Those two words made a failed parse of a perfectly good result page report
180
+ // itself as a CAPTCHA, sending the user off to solve one that wasn't there.
181
+ //
182
+ // Duplicated in `products/aws/blat-proxy/src/routes.ts`, which decides the same thing
183
+ // about the same pages server-side. The proxy is a standalone package and cannot
184
+ // import this one, so the copies are deliberate: narrow one and narrow both.
185
+ const CHALLENGE_MARKERS = /turnstile|cf[-_]chl/i;
186
+ export function isChallengePage(text) {
187
+ return CHALLENGE_MARKERS.test(text);
188
+ }
189
+ export function challengeError() {
190
+ return new BlatChallengeError('The UCSC server returned a CAPTCHA challenge instead of results. ' +
191
+ 'Solve it in the window, or supply a UCSC apiKey (Genome Browser ' +
192
+ 'account → Hub Development → API key) to avoid it.');
193
+ }
194
+ // kent CGIs report a bad db ("Can't find database …") or a server-side failure
195
+ // by errAbort-ing into an HTML page, usually inside a <pre>. Pull that text out
196
+ // so an assembly UCSC doesn't host says why, rather than "unexpected HTML".
197
+ function htmlErrorMessage(text) {
198
+ const pre = /<pre[^>]*>([\S\s]*?)<\/pre>/i.exec(text);
199
+ return (pre ? pre[1] : text)
200
+ .replaceAll(/<(script|style)[\S\s]*?<\/\1>/gi, ' ')
201
+ .replaceAll(/<[^>]*>/g, ' ')
202
+ .replaceAll('&nbsp;', ' ')
203
+ .replaceAll('&quot;', '"')
204
+ .replaceAll('&lt;', '<')
205
+ .replaceAll('&gt;', '>')
206
+ .replaceAll('&amp;', '&')
207
+ .replaceAll(/\s+/g, ' ')
208
+ .trim()
209
+ .slice(0, 300);
210
+ }
211
+ // hgBlat returns text/html content-type with a JSON body, and both non-match
212
+ // errors and the Cloudflare Turnstile challenge come back as HTML pages, so we
213
+ // parse the text ourselves and give a readable error if it isn't JSON
214
+ export function parseBlatResponse(text) {
215
+ if (text.trimStart().startsWith('<')) {
216
+ if (isChallengePage(text)) {
217
+ throw challengeError();
218
+ }
219
+ const message = htmlErrorMessage(text);
220
+ throw new Error(message
221
+ ? `BLAT server error: ${message}`
222
+ : 'BLAT server returned an unexpected HTML response instead of JSON');
223
+ }
224
+ const data = JSON.parse(text);
225
+ return parsePslRows(data);
226
+ }
227
+ export const UCSC_BLAT_URL = 'https://genome.ucsc.edu/cgi-bin/hgBlat';
228
+ /**
229
+ * Where the dialog points when the user has not typed a server of their own:
230
+ * the jbrowse.org proxy, which injects a UCSC apiKey server-side and meters the
231
+ * budget everyone using it shares (`products/aws/blat-proxy`). That is what makes
232
+ * a first BLAT work with no key and no CAPTCHA.
233
+ *
234
+ * A browser has nowhere else to go — it cannot call genome.ucsc.edu at all (no
235
+ * CORS headers) and must not carry a key in a public bundle. Desktop can reach
236
+ * UCSC itself, since `blatFetch` runs in the main process, so it keeps
237
+ * {@link UCSC_BLAT_URL} in reserve two ways: as the fallback when the proxy is
238
+ * out of budget or down, and as where the server field moves outright once the
239
+ * user supplies an apiKey of their own. The proxy overwrites a client key with
240
+ * its own, so a key only ever spends against UCSC direct.
241
+ *
242
+ * Either way this is only a default — the dialog's server field overrides it,
243
+ * which is how someone runs their own proxy or their own gfServer.
244
+ */
245
+ export const DEFAULT_BLAT_URL = 'https://api.jbrowse.org/ucsc/v1/blat';
@@ -0,0 +1,9 @@
1
+ export declare function desktopBlatFetch({ url, body, }: {
2
+ url: string;
3
+ body: string;
4
+ }): Promise<{
5
+ ok: boolean;
6
+ status: number;
7
+ text: string;
8
+ }>;
9
+ export declare function openBlatChallenge(url: string): Promise<boolean>;
@@ -0,0 +1,22 @@
1
+ // Bridge to the jbrowse-desktop main process. BLAT requests are routed through
2
+ // the main process for two reasons: the renderer is CORS-bound like a browser
3
+ // (so a direct cross-origin hgBlat call fails), and the cf_clearance cookie
4
+ // from a solved Cloudflare Turnstile challenge (held in the default session)
5
+ // attaches first-party there. A user-supplied apiKey avoids the challenge; the
6
+ // solve-window path is the fallback when there's no key.
7
+ // window.require('electron') is the desktop preload bridge; typed narrowly here
8
+ // rather than polluting the global Window type from a plugin
9
+ function getIpcRenderer() {
10
+ const req = window
11
+ .require;
12
+ if (!req) {
13
+ throw new Error('electron ipcRenderer unavailable');
14
+ }
15
+ return req('electron').ipcRenderer;
16
+ }
17
+ export async function desktopBlatFetch({ url, body, }) {
18
+ return (await getIpcRenderer().invoke('blatFetch', url, body));
19
+ }
20
+ export async function openBlatChallenge(url) {
21
+ return (await getIpcRenderer().invoke('openBlatChallenge', url));
22
+ }
package/esm/index.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import Plugin from '@jbrowse/core/Plugin';
2
+ import type PluginManager from '@jbrowse/core/PluginManager';
3
+ export default class BlatPlugin extends Plugin {
4
+ name: string;
5
+ install(pluginManager: PluginManager): void;
6
+ configure(pluginManager: PluginManager): void;
7
+ }
package/esm/index.js ADDED
@@ -0,0 +1,37 @@
1
+ import Plugin from '@jbrowse/core/Plugin';
2
+ import { isAbstractMenuManager } from '@jbrowse/core/util';
3
+ import BiotechIcon from '@mui/icons-material/Biotech';
4
+ import SearchIcon from '@mui/icons-material/Search';
5
+ import BlatDialog from './BlatDialog.js';
6
+ import IsPcrDialog from './IsPcrDialog.js';
7
+ import UcscResultsWidgetF from './UcscResultsWidget/index.js';
8
+ export default class BlatPlugin extends Plugin {
9
+ name = 'BlatPlugin';
10
+ install(pluginManager) {
11
+ UcscResultsWidgetF(pluginManager);
12
+ }
13
+ configure(pluginManager) {
14
+ if (isAbstractMenuManager(pluginManager.rootModel)) {
15
+ pluginManager.rootModel.appendToMenu('Tools', {
16
+ label: 'BLAT search…',
17
+ icon: SearchIcon,
18
+ onClick: (session) => {
19
+ session.queueDialog(handleClose => [
20
+ BlatDialog,
21
+ { session, handleClose },
22
+ ]);
23
+ },
24
+ });
25
+ pluginManager.rootModel.appendToMenu('Tools', {
26
+ label: 'In-silico PCR…',
27
+ icon: BiotechIcon,
28
+ onClick: (session) => {
29
+ session.queueDialog(handleClose => [
30
+ IsPcrDialog,
31
+ { session, handleClose },
32
+ ]);
33
+ },
34
+ });
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,59 @@
1
+ import type { SimpleFeatureSerialized } from '@jbrowse/core/util';
2
+ export declare const UCSC_ISPCR_URL = "https://genome.ucsc.edu/cgi-bin/hgPcr";
3
+ export declare const DEFAULT_ISPCR_URL = "https://api.jbrowse.org/ucsc/v1/ispcr";
4
+ export declare const DEFAULT_MAX_PRODUCT_SIZE = 4000;
5
+ export declare const MINIMUM_PRIMER_LENGTH = 15;
6
+ /** One amplicon, in the terms the header states it. */
7
+ export interface PcrProduct {
8
+ refName: string;
9
+ /** interbase, half-open */
10
+ start: number;
11
+ end: number;
12
+ /** the strand hgPcr reports the product on, i.e. which primer was forward */
13
+ strand: 1 | -1;
14
+ size: number;
15
+ /** the primers as submitted, 5'->3' */
16
+ forwardPrimer: string;
17
+ reversePrimer: string;
18
+ }
19
+ /**
20
+ * The two primer footprints, low coordinate first.
21
+ *
22
+ * A primer pair converges: the footprint at the low coordinate reads rightward
23
+ * and the one at the high coordinate reads leftward, whichever of the two the
24
+ * submitter called forward. Only *which* is which follows the product strand —
25
+ * on a plus product the forward primer sits at the low end, on a minus product
26
+ * it sits at the high end.
27
+ *
28
+ * Each footprint carries the primer's own bases, which is what lets a mismatch
29
+ * against the template be drawn rather than a perfect anneal assumed. They are
30
+ * as submitted, i.e. 5'->3' on the strand that primer anneals to, so a footprint
31
+ * at the low end reads on the reference's plus strand and one at the high end
32
+ * has to be reverse-complemented to sit on it ({@link ispcrToSam} does that).
33
+ */
34
+ export declare function productFootprints(product: PcrProduct): {
35
+ low: {
36
+ name: string;
37
+ role: "fwd" | "rev";
38
+ start: number;
39
+ end: number;
40
+ primer: string;
41
+ };
42
+ high: {
43
+ name: string;
44
+ role: "fwd" | "rev";
45
+ start: number;
46
+ end: number;
47
+ primer: string;
48
+ };
49
+ };
50
+ export declare function parseIsPcrProducts(text: string): PcrProduct[];
51
+ export declare function pcrProductsToFeatures(products: PcrProduct[]): SimpleFeatureSerialized[];
52
+ export declare function parseIsPcrResponse(text: string): SimpleFeatureSerialized[];
53
+ export declare function buildIsPcrBody({ db, forwardPrimer, reversePrimer, maxProductSize, apiKey, }: {
54
+ db: string;
55
+ forwardPrimer: string;
56
+ reversePrimer: string;
57
+ maxProductSize?: number;
58
+ apiKey?: string;
59
+ }): string;