@jbrowse/plugin-gff3 5.0.0-beta.11 → 5.0.0-beta.14
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/esm/Gff3Adapter/Gff3Adapter.js +16 -8
- package/esm/Gff3Adapter/configSchema.d.ts +2 -2
- package/esm/Gff3Adapter/configSchema.js +3 -2
- package/esm/Gff3Feature.d.ts +3 -1
- package/esm/Gff3Feature.js +6 -1
- package/esm/Gff3TabixAdapter/Gff3TabixAdapter.d.ts +3 -0
- package/esm/Gff3TabixAdapter/Gff3TabixAdapter.js +49 -17
- package/esm/Gff3TabixAdapter/configSchema.d.ts +1 -4
- package/esm/Gff3TabixAdapter/configSchema.js +3 -4
- package/esm/fileAttributes.d.ts +7 -0
- package/esm/fileAttributes.js +38 -0
- package/esm/originSpanning.d.ts +24 -0
- package/esm/originSpanning.js +59 -0
- package/package.json +6 -6
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { parseLinesLazy } from '@gmod/gff-nostream';
|
|
1
2
|
import { BaseFeatureDataAdapter, cachedSetup, } from '@jbrowse/core/data_adapters/BaseAdapter';
|
|
2
3
|
import { fetchAndMaybeUnzip } from '@jbrowse/core/util';
|
|
3
4
|
import { openLocation } from '@jbrowse/core/util/io';
|
|
4
5
|
import { groupLinesByRef, makeFeatureIntervalTreeMap, } from '@jbrowse/core/util/parseLineByLine';
|
|
5
6
|
import { ObservableCreate } from '@jbrowse/core/util/rxjs';
|
|
6
|
-
import { parseLinesLazy } from 'gff-nostream';
|
|
7
7
|
import { Gff3Feature } from '../Gff3Feature.js';
|
|
8
|
+
import { cutAtOrigin, sequenceLengths } from '../originSpanning.js';
|
|
8
9
|
export default class Gff3Adapter extends BaseFeatureDataAdapter {
|
|
9
10
|
// the whole file is resident after one load, so the fetch/parse status comes
|
|
10
11
|
// from inside the load itself rather than a label wrapped around it
|
|
@@ -32,7 +33,8 @@ export default class Gff3Adapter extends BaseFeatureDataAdapter {
|
|
|
32
33
|
feature,
|
|
33
34
|
uniqueId: `${this.id}-${refName}-${i}`,
|
|
34
35
|
})), 'Parsing GFF data');
|
|
35
|
-
|
|
36
|
+
const header = headerLines.join('\n');
|
|
37
|
+
return { header, intervalTreeMap, lengths: sequenceLengths(header) };
|
|
36
38
|
},
|
|
37
39
|
});
|
|
38
40
|
async getRefNames(opts = {}) {
|
|
@@ -48,13 +50,19 @@ export default class Gff3Adapter extends BaseFeatureDataAdapter {
|
|
|
48
50
|
// observer.error itself
|
|
49
51
|
return ObservableCreate(async (observer) => {
|
|
50
52
|
const { start, end, refName } = query;
|
|
51
|
-
const { intervalTreeMap } = await this.loadData(opts);
|
|
52
|
-
const tree = intervalTreeMap[refName];
|
|
53
|
+
const { intervalTreeMap, lengths } = await this.loadData(opts);
|
|
54
|
+
const tree = intervalTreeMap[refName]?.(opts.statusCallback);
|
|
53
55
|
if (tree) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// a feature crossing a circular sequence's origin is stored from its
|
|
57
|
+
// start near the end, so a query short of the end looks there too
|
|
58
|
+
const length = lengths.get(refName);
|
|
59
|
+
const candidates = [
|
|
60
|
+
...tree.search([start, end]),
|
|
61
|
+
...(length !== undefined && end < length
|
|
62
|
+
? tree.search([length - 1, length])
|
|
63
|
+
: []),
|
|
64
|
+
];
|
|
65
|
+
for (const { feature, uniqueId } of cutAtOrigin(candidates, query, length)) {
|
|
58
66
|
observer.next(new Gff3Feature(feature, uniqueId));
|
|
59
67
|
}
|
|
60
68
|
}
|
|
@@ -5,8 +5,8 @@ export declare function normalizeSnapshot(snap: Record<string, unknown>): Record
|
|
|
5
5
|
* #category adapter
|
|
6
6
|
* #trackType FeatureTrack
|
|
7
7
|
* #fileFormat feature | GFF3 (plain) | Loaded entirely into memory; for small files
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Reads plain-text GFF3 files. The adapter loads the whole file into memory,
|
|
9
|
+
* so use `Gff3TabixAdapter` for large files.
|
|
10
10
|
*
|
|
11
11
|
* #example
|
|
12
12
|
* ```js
|
|
@@ -7,8 +7,8 @@ export function normalizeSnapshot(snap) {
|
|
|
7
7
|
* #category adapter
|
|
8
8
|
* #trackType FeatureTrack
|
|
9
9
|
* #fileFormat feature | GFF3 (plain) | Loaded entirely into memory; for small files
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Reads plain-text GFF3 files. The adapter loads the whole file into memory,
|
|
11
|
+
* so use `Gff3TabixAdapter` for large files.
|
|
12
12
|
*
|
|
13
13
|
* #example
|
|
14
14
|
* ```js
|
|
@@ -33,6 +33,7 @@ const Gff3Adapter = ConfigurationSchema('Gff3Adapter', {
|
|
|
33
33
|
},
|
|
34
34
|
}, {
|
|
35
35
|
explicitlyTyped: true,
|
|
36
|
+
closed: true,
|
|
36
37
|
/**
|
|
37
38
|
* #preProcessSnapshot
|
|
38
39
|
*
|
package/esm/Gff3Feature.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { LazyGffFeature } from '@gmod/gff-nostream';
|
|
1
2
|
import type { Feature, SimpleFeatureSerialized } from '@jbrowse/core/util/simpleFeature';
|
|
2
|
-
import type { LazyGffFeature } from 'gff-nostream';
|
|
3
3
|
/**
|
|
4
4
|
* A {@link Feature} over a GFF3 feature whose attributes are still raw column-9
|
|
5
5
|
* text, resolving each one only when something asks for it.
|
|
@@ -38,6 +38,8 @@ export declare class Gff3Feature implements Feature {
|
|
|
38
38
|
get(name: 'subfeatures'): Feature[] | undefined;
|
|
39
39
|
get(name: string): unknown;
|
|
40
40
|
id(): string;
|
|
41
|
+
/** The feature's column 9 keyed as the file writes it (`fileAttributes`). */
|
|
42
|
+
fileAttributes(): Record<string, string[]>;
|
|
41
43
|
parent(): Feature | undefined;
|
|
42
44
|
/**
|
|
43
45
|
* Wrapped lazily rather than in the constructor: a feature the layout pass
|
package/esm/Gff3Feature.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getAttribute, getAttributes } from 'gff-nostream';
|
|
1
|
+
import { getAttribute, getAttributes } from '@gmod/gff-nostream';
|
|
2
|
+
import { fileAttributes } from './fileAttributes.js';
|
|
2
3
|
/**
|
|
3
4
|
* A {@link Feature} over a GFF3 feature whose attributes are still raw column-9
|
|
4
5
|
* text, resolving each one only when something asks for it.
|
|
@@ -69,6 +70,10 @@ export class Gff3Feature {
|
|
|
69
70
|
id() {
|
|
70
71
|
return this.uniqueId;
|
|
71
72
|
}
|
|
73
|
+
/** The feature's column 9 keyed as the file writes it (`fileAttributes`). */
|
|
74
|
+
fileAttributes() {
|
|
75
|
+
return fileAttributes(this.data.attributeString);
|
|
76
|
+
}
|
|
72
77
|
parent() {
|
|
73
78
|
return this.parentFeature;
|
|
74
79
|
}
|
|
@@ -4,7 +4,10 @@ import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter';
|
|
|
4
4
|
import type { Feature } from '@jbrowse/core/util/simpleFeature';
|
|
5
5
|
import type { Region } from '@jbrowse/core/util/types';
|
|
6
6
|
export default class Gff3TabixAdapter extends BaseFeatureDataAdapter<Gff3TabixAdapterConfig> {
|
|
7
|
+
private file?;
|
|
8
|
+
freeResources(): void;
|
|
7
9
|
private configure;
|
|
10
|
+
private lengths;
|
|
8
11
|
getRefNames(opts?: BaseOptions): Promise<string[]>;
|
|
9
12
|
getHeader(opts?: BaseOptions): Promise<string>;
|
|
10
13
|
getRegionByteSize(regions: Region[], opts?: BaseOptions): Promise<number>;
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import { hasIdAttribute, parseRecordsLazy } from '@gmod/gff-nostream';
|
|
1
2
|
import { TabixIndexedFile } from '@gmod/tabix';
|
|
2
3
|
import { BaseFeatureDataAdapter, cachedSetup, } from '@jbrowse/core/data_adapters/BaseAdapter';
|
|
3
4
|
import { downloadStatus } from '@jbrowse/core/util';
|
|
4
5
|
import { decompressedBytesBudget } from '@jbrowse/core/util/cacheBudgets';
|
|
5
6
|
import { openLocation, openTabixIndexFilehandle } from '@jbrowse/core/util/io';
|
|
6
|
-
import { doesIntersect2 } from '@jbrowse/core/util/range';
|
|
7
7
|
import { ObservableCreate } from '@jbrowse/core/util/rxjs';
|
|
8
8
|
import { readTabixLinesRedispatched } from '@jbrowse/core/util/tabix';
|
|
9
|
-
import { hasIdAttribute, parseRecordsLazy } from 'gff-nostream';
|
|
10
9
|
import { Gff3Feature } from '../Gff3Feature.js';
|
|
10
|
+
import { cutAtOrigin, sequenceLengths } from '../originSpanning.js';
|
|
11
11
|
export default class Gff3TabixAdapter extends BaseFeatureDataAdapter {
|
|
12
|
+
// set once the file is open, for dataAdapterCache's eviction to clear
|
|
13
|
+
file;
|
|
14
|
+
freeResources() {
|
|
15
|
+
this.file?.clearChunkCache();
|
|
16
|
+
}
|
|
12
17
|
configure = cachedSetup({
|
|
13
18
|
label: 'Downloading index',
|
|
14
19
|
setup: async (opts, onProgress) => {
|
|
@@ -17,6 +22,7 @@ export default class Gff3TabixAdapter extends BaseFeatureDataAdapter {
|
|
|
17
22
|
...openTabixIndexFilehandle(this.getConf(['index', 'location']), this.getConf(['index', 'indexType']), this.pluginManager),
|
|
18
23
|
chunkCacheBudget: decompressedBytesBudget,
|
|
19
24
|
});
|
|
25
|
+
this.file = gff;
|
|
20
26
|
// the index is a whole-file read, so its byte ticks turn the
|
|
21
27
|
// "Downloading index" label into a determinate bar
|
|
22
28
|
await gff.getReferenceSequenceNames({ onProgress, signal: opts.signal });
|
|
@@ -26,6 +32,15 @@ export default class Gff3TabixAdapter extends BaseFeatureDataAdapter {
|
|
|
26
32
|
};
|
|
27
33
|
},
|
|
28
34
|
});
|
|
35
|
+
// the header read is sized from the index, so it can start only once the
|
|
36
|
+
// index is in; the region read needs the same index and nothing else, so
|
|
37
|
+
// getFeatures runs the two together
|
|
38
|
+
lengths = cachedSetup({
|
|
39
|
+
setup: async (opts) => {
|
|
40
|
+
const { gff } = await this.configure(opts);
|
|
41
|
+
return sequenceLengths(await gff.getHeader(opts));
|
|
42
|
+
},
|
|
43
|
+
});
|
|
29
44
|
async getRefNames(opts = {}) {
|
|
30
45
|
const { gff } = await this.configure(opts);
|
|
31
46
|
return downloadStatus('Downloading index', opts.statusCallback, onProgress => gff.getReferenceSequenceNames({ ...opts, onProgress }));
|
|
@@ -47,7 +62,7 @@ export default class Gff3TabixAdapter extends BaseFeatureDataAdapter {
|
|
|
47
62
|
const { gff, dontRedispatchSet } = await this.configure(opts);
|
|
48
63
|
// The bound exists to reach a record's children, so what disqualifies a
|
|
49
64
|
// record is having none. `ID` answers that exactly — nothing can name a
|
|
50
|
-
// parent that has no name — and gff-nostream decides it with the scan
|
|
65
|
+
// parent that has no name — and @gmod/gff-nostream decides it with the scan
|
|
51
66
|
// its linker uses, so a record the tree would attach children to is
|
|
52
67
|
// never one the bound leaves out. `dontRedispatch` can only approximate
|
|
53
68
|
// the same question by type: every NCBI `GCF_*_genomic.gff.gz` opens
|
|
@@ -56,22 +71,39 @@ export default class Gff3TabixAdapter extends BaseFeatureDataAdapter {
|
|
|
56
71
|
// record carrying an `ID` that nothing references, which hosted hg19
|
|
57
72
|
// RefSeq's chromosome-long `region` is.
|
|
58
73
|
const canHaveChildren = (line) => hasIdAttribute(line.line) && !dontRedispatchSet.has(line.type);
|
|
59
|
-
const
|
|
74
|
+
const read = (region) => readTabixLinesRedispatched(gff, region,
|
|
60
75
|
// A caller reading only top-level features is owed nothing by the
|
|
61
|
-
// flanks and pays for them anyway: they complete SUBFEATURE lists,
|
|
62
|
-
// a top-level feature overlapping the query is in the query's
|
|
63
|
-
// already, since tabix returns every overlapping line and
|
|
64
|
-
// contained in its parent. On an NCBI
|
|
65
|
-
// chromosome-long `match` widens the
|
|
66
|
-
// 193,008 lines parsed to keep 3
|
|
76
|
+
// flanks and pays for them anyway: they complete SUBFEATURE lists,
|
|
77
|
+
// and a top-level feature overlapping the query is in the query's
|
|
78
|
+
// own read already, since tabix returns every overlapping line and
|
|
79
|
+
// a child is contained in its parent. On an NCBI
|
|
80
|
+
// `GCF_*_genomic.gff.gz`, whose chromosome-long `match` widens the
|
|
81
|
+
// bound at every window, that is 193,008 lines parsed to keep 3
|
|
82
|
+
// features — 2734 ms against 8 ms.
|
|
67
83
|
opts.topLevelOnly ? () => false : canHaveChildren, opts);
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
// a feature crossing a circular sequence's origin is indexed from its
|
|
85
|
+
// start near the end, so a query short of the end reads the last base
|
|
86
|
+
// too, for the part such a feature carries past the origin. That read
|
|
87
|
+
// needs the length from the header, so it starts when the header
|
|
88
|
+
// arrives and the query's own read does not wait for it.
|
|
89
|
+
const tailRead = this.lengths(opts).then(async (lengths) => {
|
|
90
|
+
const length = lengths.get(query.refName);
|
|
91
|
+
return {
|
|
92
|
+
length,
|
|
93
|
+
lines: length !== undefined && query.end < length
|
|
94
|
+
? await read({ ...query, start: length - 1, end: length })
|
|
95
|
+
: [],
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
// the byte offset stays on our own record and is used purely to mint
|
|
99
|
+
// a stable id, so it never pollutes the feature's data
|
|
100
|
+
const [lines, tail] = await Promise.all([read(query), tailRead]);
|
|
101
|
+
const candidates = [lines, tail.lines].flatMap(group => parseRecordsLazy(group).map(({ feature, record }) => ({
|
|
102
|
+
feature,
|
|
103
|
+
uniqueId: `${this.id}-offset-${record.offset}`,
|
|
104
|
+
})));
|
|
105
|
+
for (const { feature, uniqueId } of cutAtOrigin(candidates, query, tail.length)) {
|
|
106
|
+
observer.next(new Gff3Feature(feature, uniqueId));
|
|
75
107
|
}
|
|
76
108
|
observer.complete();
|
|
77
109
|
}
|
|
@@ -5,7 +5,7 @@ export declare function normalizeSnapshot(snap: Record<string, unknown>): Record
|
|
|
5
5
|
* #category adapter
|
|
6
6
|
* #trackType FeatureTrack
|
|
7
7
|
* #fileFormat feature | GFF3 (tabix)
|
|
8
|
-
*
|
|
8
|
+
* Reads bgzip-compressed, tabix-indexed GFF3 files.
|
|
9
9
|
*
|
|
10
10
|
* #example
|
|
11
11
|
* The `uri` shorthand auto-resolves the `.tbi` index; add `csi: true` for a
|
|
@@ -18,9 +18,6 @@ export declare function normalizeSnapshot(snap: Record<string, unknown>): Record
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
declare const Gff3TabixAdapter: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
|
|
21
|
-
readonly densityAdapter: {
|
|
22
|
-
type: "maybeFrozen";
|
|
23
|
-
};
|
|
24
21
|
type: {
|
|
25
22
|
type: "string";
|
|
26
23
|
} & {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ConfigurationSchema, expandTabixShorthand, tabixIndexSchema, } from '@jbrowse/core/configuration';
|
|
2
|
-
import { densityAdapterConfigSchemaFields } from '@jbrowse/core/data_adapters/BaseAdapter';
|
|
3
2
|
export function normalizeSnapshot(snap) {
|
|
4
3
|
return expandTabixShorthand(snap, 'gffGzLocation');
|
|
5
4
|
}
|
|
@@ -8,7 +7,7 @@ export function normalizeSnapshot(snap) {
|
|
|
8
7
|
* #category adapter
|
|
9
8
|
* #trackType FeatureTrack
|
|
10
9
|
* #fileFormat feature | GFF3 (tabix)
|
|
11
|
-
*
|
|
10
|
+
* Reads bgzip-compressed, tabix-indexed GFF3 files.
|
|
12
11
|
*
|
|
13
12
|
* #example
|
|
14
13
|
* The `uri` shorthand auto-resolves the `.tbi` index; add `csi: true` for a
|
|
@@ -35,7 +34,7 @@ const Gff3TabixAdapter = ConfigurationSchema('Gff3TabixAdapter', {
|
|
|
35
34
|
},
|
|
36
35
|
/**
|
|
37
36
|
* #slot
|
|
38
|
-
*
|
|
37
|
+
* The tabix index location and its kind. The `uri` shorthand derives
|
|
39
38
|
* both, so a config using it states neither.
|
|
40
39
|
*/
|
|
41
40
|
index: tabixIndexSchema(),
|
|
@@ -62,9 +61,9 @@ const Gff3TabixAdapter = ConfigurationSchema('Gff3TabixAdapter', {
|
|
|
62
61
|
'scaffold',
|
|
63
62
|
],
|
|
64
63
|
},
|
|
65
|
-
...densityAdapterConfigSchemaFields,
|
|
66
64
|
}, {
|
|
67
65
|
explicitlyTyped: true,
|
|
66
|
+
closed: true,
|
|
68
67
|
/**
|
|
69
68
|
* #preProcessSnapshot
|
|
70
69
|
*
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A GFF3 column 9 keyed as the file writes it (`ID`, `Name`, `gene_name`),
|
|
3
|
+
* each value the comma-separated list the column gives, %-escapes decoded.
|
|
4
|
+
* `@gmod/gff-nostream` lowercases the keys for JBrowse's own reads, which a
|
|
5
|
+
* host reading the file's fields should not see.
|
|
6
|
+
*/
|
|
7
|
+
export declare function fileAttributes(column: string): Record<string, string[]>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A GFF3 column 9 keyed as the file writes it (`ID`, `Name`, `gene_name`),
|
|
3
|
+
* each value the comma-separated list the column gives, %-escapes decoded.
|
|
4
|
+
* `@gmod/gff-nostream` lowercases the keys for JBrowse's own reads, which a
|
|
5
|
+
* host reading the file's fields should not see.
|
|
6
|
+
*/
|
|
7
|
+
export function fileAttributes(column) {
|
|
8
|
+
const out = {};
|
|
9
|
+
if (column === '.' || column === '') {
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
for (const entry of column.split(';')) {
|
|
13
|
+
const eq = entry.indexOf('=');
|
|
14
|
+
const key = unescape(entry.slice(0, eq).trim());
|
|
15
|
+
const values = entry
|
|
16
|
+
.slice(eq + 1)
|
|
17
|
+
.trimEnd()
|
|
18
|
+
.split(',')
|
|
19
|
+
.filter(v => v !== '')
|
|
20
|
+
.map(unescape);
|
|
21
|
+
if (eq > 0 && key && values.length) {
|
|
22
|
+
out[key] = values;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
function unescape(s) {
|
|
28
|
+
return s.includes('%')
|
|
29
|
+
? s.replaceAll(/(?:%[\dA-Fa-f]{2})+/g, run => {
|
|
30
|
+
try {
|
|
31
|
+
return decodeURIComponent(run);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return run;
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
: s;
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LazyGffFeature } from '@gmod/gff-nostream';
|
|
2
|
+
/** Each sequence's length, as the file's `##sequence-region` pragmas state it. */
|
|
3
|
+
export declare function sequenceLengths(header: string): Map<string, number>;
|
|
4
|
+
export interface Candidate {
|
|
5
|
+
feature: LazyGffFeature;
|
|
6
|
+
uniqueId: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The candidates `query` overlaps, a feature crossing the origin of a
|
|
10
|
+
* sequence `length` long cut there into the part before it and the part after.
|
|
11
|
+
*
|
|
12
|
+
* GFF3 writes such a feature on a circular sequence with its end past the
|
|
13
|
+
* sequence's length, in "virtual" coordinates, so one record covers both
|
|
14
|
+
* sides. Each half keeps the children that fall in it, clipped; the half
|
|
15
|
+
* after the origin, shifted back onto the sequence, takes `originTailId` of
|
|
16
|
+
* the id.
|
|
17
|
+
*/
|
|
18
|
+
export declare function cutAtOrigin(candidates: Iterable<Candidate>, query: {
|
|
19
|
+
start: number;
|
|
20
|
+
end: number;
|
|
21
|
+
}, length: number | undefined): Generator<{
|
|
22
|
+
feature: LazyGffFeature;
|
|
23
|
+
uniqueId: string;
|
|
24
|
+
}, void, unknown>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { originTailId } from '@jbrowse/core/util/originCut';
|
|
2
|
+
import { doesIntersect2 } from '@jbrowse/core/util/range';
|
|
3
|
+
/** Each sequence's length, as the file's `##sequence-region` pragmas state it. */
|
|
4
|
+
export function sequenceLengths(header) {
|
|
5
|
+
const lengths = new Map();
|
|
6
|
+
for (const line of header.split('\n')) {
|
|
7
|
+
const match = /^##sequence-region\s+(\S+)\s+\d+\s+(\d+)/.exec(line);
|
|
8
|
+
if (match) {
|
|
9
|
+
lengths.set(match[1], Number(match[2]));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return lengths;
|
|
13
|
+
}
|
|
14
|
+
function clip(feature, start, end, shift) {
|
|
15
|
+
const from = Math.max(feature.start, start);
|
|
16
|
+
const to = Math.min(feature.end, end);
|
|
17
|
+
return to > from
|
|
18
|
+
? {
|
|
19
|
+
...feature,
|
|
20
|
+
start: from - shift,
|
|
21
|
+
end: to - shift,
|
|
22
|
+
subfeatures: feature.subfeatures.flatMap(child => clip(child, start, end, shift) ?? []),
|
|
23
|
+
}
|
|
24
|
+
: undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The candidates `query` overlaps, a feature crossing the origin of a
|
|
28
|
+
* sequence `length` long cut there into the part before it and the part after.
|
|
29
|
+
*
|
|
30
|
+
* GFF3 writes such a feature on a circular sequence with its end past the
|
|
31
|
+
* sequence's length, in "virtual" coordinates, so one record covers both
|
|
32
|
+
* sides. Each half keeps the children that fall in it, clipped; the half
|
|
33
|
+
* after the origin, shifted back onto the sequence, takes `originTailId` of
|
|
34
|
+
* the id.
|
|
35
|
+
*/
|
|
36
|
+
export function* cutAtOrigin(candidates, query, length) {
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
for (const { feature, uniqueId } of candidates) {
|
|
39
|
+
if (seen.has(uniqueId)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
seen.add(uniqueId);
|
|
43
|
+
const halves = length !== undefined && feature.end > length
|
|
44
|
+
? [
|
|
45
|
+
{ half: clip(feature, feature.start, length, 0), uniqueId },
|
|
46
|
+
{
|
|
47
|
+
half: clip(feature, length, feature.end, length),
|
|
48
|
+
uniqueId: originTailId(uniqueId),
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
: [{ half: feature, uniqueId }];
|
|
52
|
+
for (const { half, uniqueId: id } of halves) {
|
|
53
|
+
if (half &&
|
|
54
|
+
doesIntersect2(half.start, half.end, query.start, query.end)) {
|
|
55
|
+
yield { feature: half, uniqueId: id };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/plugin-gff3",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "JBrowse 2 gff3.",
|
|
6
6
|
"keywords": [
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
"esm"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@gmod/
|
|
25
|
-
"@
|
|
26
|
-
"
|
|
27
|
-
"mobx": "^7.0.
|
|
24
|
+
"@gmod/gff-nostream": "^5.6.2",
|
|
25
|
+
"@gmod/tabix": "^3.11.0",
|
|
26
|
+
"@jbrowse/mobx-state-tree": "^6.9.0",
|
|
27
|
+
"mobx": "^7.0.6",
|
|
28
28
|
"rxjs": "^7.8.2",
|
|
29
|
-
"@jbrowse/core": "^5.0.0-beta.
|
|
29
|
+
"@jbrowse/core": "^5.0.0-beta.14"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"react": ">=19.0.0",
|