@jbrowse/plugin-gff3 4.3.0 → 5.0.0-beta.1

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,19 +1,11 @@
1
1
  import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
2
- import { IntervalTree } from '@jbrowse/core/util';
2
+ import type { Gff3AdapterConfig } from './configSchema.ts';
3
3
  import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter';
4
4
  import type { Feature } from '@jbrowse/core/util/simpleFeature';
5
5
  import type { NoAssemblyRegion } from '@jbrowse/core/util/types';
6
- type StatusCallback = (arg: string) => void;
7
- export default class Gff3Adapter extends BaseFeatureDataAdapter {
8
- calculatedIntervalTreeMap: Record<string, IntervalTree<Feature>>;
9
- gffFeatures?: Promise<{
10
- header: string;
11
- intervalTreeMap: Record<string, (sc?: StatusCallback) => IntervalTree<Feature>>;
12
- }>;
13
- private loadDataP;
6
+ export default class Gff3Adapter extends BaseFeatureDataAdapter<Gff3AdapterConfig> {
14
7
  private loadData;
15
8
  getRefNames(opts?: BaseOptions): Promise<string[]>;
16
9
  getHeader(opts?: BaseOptions): Promise<string>;
17
10
  getFeatures(query: NoAssemblyRegion, opts?: BaseOptions): import("rxjs").Observable<Feature>;
18
11
  }
19
- export {};
@@ -1,51 +1,41 @@
1
- import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
2
- import { IntervalTree, fetchAndMaybeUnzip } from '@jbrowse/core/util';
1
+ import { BaseFeatureDataAdapter, cachedSetup, } from '@jbrowse/core/data_adapters/BaseAdapter';
2
+ import { fetchAndMaybeUnzip } from '@jbrowse/core/util';
3
3
  import { openLocation } from '@jbrowse/core/util/io';
4
+ import { groupLinesByRef, makeFeatureIntervalTreeMap, } from '@jbrowse/core/util/parseLineByLine';
4
5
  import { ObservableCreate } from '@jbrowse/core/util/rxjs';
5
- import SimpleFeature from '@jbrowse/core/util/simpleFeature';
6
- import { parseStringSyncJBrowse } from 'gff-nostream';
7
- import { parseGffBuffer } from "./gffParser.js";
6
+ import { parseLinesLazy } from 'gff-nostream';
7
+ import { Gff3Feature } from '../Gff3Feature.js';
8
8
  export default class Gff3Adapter extends BaseFeatureDataAdapter {
9
- calculatedIntervalTreeMap = {};
10
- gffFeatures;
11
- async loadDataP(opts) {
12
- const { statusCallback = () => { } } = opts || {};
13
- const buffer = await fetchAndMaybeUnzip(openLocation(this.getConf('gffLocation'), this.pluginManager), opts);
14
- const { header, featureMap } = parseGffBuffer(buffer, statusCallback);
15
- const intervalTreeMap = Object.fromEntries(Object.entries(featureMap).map(([refName, lines]) => [
16
- refName,
17
- (sc) => {
18
- if (!this.calculatedIntervalTreeMap[refName]) {
19
- sc?.('Parsing GFF data');
20
- const intervalTree = new IntervalTree();
21
- const features = parseStringSyncJBrowse(lines);
22
- for (let i = 0, l = features.length; i < l; i++) {
23
- const f = features[i];
24
- const obj = new SimpleFeature({
25
- data: f,
26
- id: `${this.id}-${refName}-${i}`,
27
- });
28
- intervalTree.insert([f.start, f.end], obj);
29
- }
30
- this.calculatedIntervalTreeMap[refName] = intervalTree;
9
+ // the whole file is resident after one load, so the fetch/parse status comes
10
+ // from inside the load itself rather than a label wrapped around it
11
+ loadData = cachedSetup({
12
+ setup: async (opts) => {
13
+ const buffer = await fetchAndMaybeUnzip(openLocation(this.getConf('gffLocation'), this.pluginManager), opts);
14
+ const { headerLines, linesByRef } = groupLinesByRef(buffer, opts.statusCallback);
15
+ const intervalTreeMap = makeFeatureIntervalTreeMap(linesByRef,
16
+ // lines are already split and comment/FASTA-filtered by
17
+ // groupLinesByRef, so feed them straight to parseLinesLazy rather than
18
+ // re-joining and re-splitting through parseStringSync.
19
+ //
20
+ // Lazy because the whole file stays resident for the session: leaving
21
+ // column 9 as text rather than an object per attribute is what keeps
22
+ // that resident set small (8.5x on GENCODE-shaped input), and the
23
+ // render path reads only a handful of attributes anyway — see
24
+ // Gff3Feature.
25
+ (lines, refName) => {
26
+ const features = parseLinesLazy(lines);
27
+ // stamped in place rather than through `{...feature, uniqueId}`:
28
+ // these are freshly parsed objects nobody else holds, and the spread
29
+ // copied every attribute of every top-level feature in the file to
30
+ // add one key
31
+ for (let i = 0; i < features.length; i++) {
32
+ features[i].uniqueId = `${this.id}-${refName}-${i}`;
31
33
  }
32
- return this.calculatedIntervalTreeMap[refName];
33
- },
34
- ]));
35
- return {
36
- header,
37
- intervalTreeMap,
38
- };
39
- }
40
- async loadData(opts) {
41
- if (!this.gffFeatures) {
42
- this.gffFeatures = this.loadDataP(opts).catch((e) => {
43
- this.gffFeatures = undefined;
44
- throw e;
45
- });
46
- }
47
- return this.gffFeatures;
48
- }
34
+ return features;
35
+ }, 'Parsing GFF data');
36
+ return { header: headerLines.join('\n'), intervalTreeMap };
37
+ },
38
+ });
49
39
  async getRefNames(opts = {}) {
50
40
  const { intervalTreeMap } = await this.loadData(opts);
51
41
  return Object.keys(intervalTreeMap);
@@ -55,21 +45,18 @@ export default class Gff3Adapter extends BaseFeatureDataAdapter {
55
45
  return header;
56
46
  }
57
47
  getFeatures(query, opts = {}) {
48
+ // no try/catch: ObservableCreate forwards a rejected callback to
49
+ // observer.error itself
58
50
  return ObservableCreate(async (observer) => {
59
- try {
60
- const { start, end, refName } = query;
61
- const { intervalTreeMap } = await this.loadData(opts);
62
- for (const f of intervalTreeMap[refName]?.(opts.statusCallback).search([
63
- start,
64
- end,
65
- ]) || []) {
66
- observer.next(f);
51
+ const { start, end, refName } = query;
52
+ const { intervalTreeMap } = await this.loadData(opts);
53
+ const tree = intervalTreeMap[refName];
54
+ if (tree) {
55
+ for (const f of tree(opts.statusCallback).search([start, end])) {
56
+ observer.next(new Gff3Feature(f, f.uniqueId));
67
57
  }
68
- observer.complete();
69
- }
70
- catch (e) {
71
- observer.error(e);
72
58
  }
59
+ observer.complete();
73
60
  }, opts.stopToken);
74
61
  }
75
62
  }
@@ -1,10 +1,34 @@
1
+ import type { Instance } from '@jbrowse/mobx-state-tree';
2
+ export declare function normalizeSnapshot(snap: Record<string, unknown>): Record<string, unknown>;
3
+ /**
4
+ * #config Gff3Adapter
5
+ * #category adapter
6
+ * #trackType FeatureTrack
7
+ * #fileFormat feature | GFF3 (plain) | Loaded entirely into memory; for small files
8
+ * used to load plain-text GFF3 files. Loads the whole file into memory, so
9
+ * prefer the Gff3TabixAdapter for large files.
10
+ *
11
+ * #example
12
+ * ```js
13
+ * {
14
+ * type: 'Gff3Adapter',
15
+ * uri: 'https://example.com/genes.gff3',
16
+ * }
17
+ * ```
18
+ */
1
19
  declare const Gff3Adapter: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
2
- gffLocation: {
3
- type: string;
4
- defaultValue: {
5
- uri: string;
6
- locationType: string;
20
+ /**
21
+ * #slot
22
+ * location of the GFF3 file. May be gzipped; it is decompressed and parsed
23
+ * in full on first use, so the file has to fit in memory.
24
+ */
25
+ readonly gffLocation: {
26
+ readonly type: "fileLocation";
27
+ readonly defaultValue: {
28
+ readonly uri: "/path/to/my.gff";
29
+ readonly locationType: "UriLocation";
7
30
  };
8
31
  };
9
32
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
33
+ export type Gff3AdapterConfig = Instance<typeof Gff3Adapter>;
10
34
  export default Gff3Adapter;
@@ -1,6 +1,37 @@
1
1
  import { ConfigurationSchema } from '@jbrowse/core/configuration';
2
- function x() { }
2
+ export function normalizeSnapshot(snap) {
3
+ return snap.uri
4
+ ? {
5
+ ...snap,
6
+ gffLocation: {
7
+ uri: snap.uri,
8
+ baseUri: snap.baseUri,
9
+ },
10
+ }
11
+ : snap;
12
+ }
13
+ /**
14
+ * #config Gff3Adapter
15
+ * #category adapter
16
+ * #trackType FeatureTrack
17
+ * #fileFormat feature | GFF3 (plain) | Loaded entirely into memory; for small files
18
+ * used to load plain-text GFF3 files. Loads the whole file into memory, so
19
+ * prefer the Gff3TabixAdapter for large files.
20
+ *
21
+ * #example
22
+ * ```js
23
+ * {
24
+ * type: 'Gff3Adapter',
25
+ * uri: 'https://example.com/genes.gff3',
26
+ * }
27
+ * ```
28
+ */
3
29
  const Gff3Adapter = ConfigurationSchema('Gff3Adapter', {
30
+ /**
31
+ * #slot
32
+ * location of the GFF3 file. May be gzipped; it is decompressed and parsed
33
+ * in full on first use, so the file has to fit in memory.
34
+ */
4
35
  gffLocation: {
5
36
  type: 'fileLocation',
6
37
  defaultValue: {
@@ -10,16 +41,19 @@ const Gff3Adapter = ConfigurationSchema('Gff3Adapter', {
10
41
  },
11
42
  }, {
12
43
  explicitlyTyped: true,
13
- preProcessSnapshot: snap => {
14
- return snap.uri
15
- ? {
16
- ...snap,
17
- gffLocation: {
18
- uri: snap.uri,
19
- baseUri: snap.baseUri,
20
- },
21
- }
22
- : snap;
23
- },
44
+ /**
45
+ * #preProcessSnapshot
46
+ *
47
+ *
48
+ * preprocessor to allow minimal config:
49
+ *
50
+ * ```json
51
+ * {
52
+ * "type": "Gff3Adapter",
53
+ * "uri": "yourfile.gff3",
54
+ * }
55
+ * ```
56
+ */
57
+ preProcessSnapshot: normalizeSnapshot,
24
58
  });
25
59
  export default Gff3Adapter;
@@ -1,10 +1,11 @@
1
1
  import { AdapterType } from '@jbrowse/core/pluggableElementTypes';
2
- import configSchema from "./configSchema.js";
2
+ import configSchema, { normalizeSnapshot } from './configSchema.js';
3
3
  export default function Gff3AdapterF(pluginManager) {
4
4
  pluginManager.addAdapterType(() => new AdapterType({
5
5
  name: 'Gff3Adapter',
6
6
  displayName: 'GFF3 adapter',
7
+ normalizeSnapshot,
7
8
  configSchema,
8
- getAdapterClass: () => import("./Gff3Adapter.js").then(r => r.default),
9
+ getAdapterClass: () => import('./Gff3Adapter.js').then(r => r.default),
9
10
  }));
10
11
  }
@@ -0,0 +1,55 @@
1
+ import type { Feature, SimpleFeatureSerialized } from '@jbrowse/core/util/simpleFeature';
2
+ import type { LazyGffFeature } from 'gff-nostream';
3
+ /** A parsed GFF3 feature carrying the id the adapter minted for it. */
4
+ export type IdentifiedGffFeature = LazyGffFeature & {
5
+ uniqueId: string;
6
+ };
7
+ /**
8
+ * A {@link Feature} over a GFF3 feature whose attributes are still raw column-9
9
+ * text, resolving each one only when something asks for it.
10
+ *
11
+ * This exists because the two things that read a GFF3 feature want opposite
12
+ * amounts of it. Rendering reads a fixed handful of keys — `start`, `end`,
13
+ * `strand`, `type`, `phase`, `subfeatures`, plus `name`/`id` for labels and
14
+ * `gbkey` for the stock NCBI filter — and then packs the result into typed
15
+ * arrays, so the feature objects are dropped and never cross the RPC boundary.
16
+ * The feature details panel wants everything, and re-fetches the region from
17
+ * scratch to get it (`GetCanvasFeatureDetails`). Parsing every attribute during
18
+ * a render therefore did work that rendering did not use and that the details
19
+ * path did not reuse.
20
+ *
21
+ * `toJSON` still materializes the lot, which is what the details path
22
+ * serializes, so nothing downstream sees a smaller feature than it used to.
23
+ */
24
+ export declare class Gff3Feature implements Feature {
25
+ private data;
26
+ private uniqueId;
27
+ private parentFeature?;
28
+ private inflated?;
29
+ constructor(data: IdentifiedGffFeature | LazyGffFeature, uniqueId: string, parentFeature?: Feature | undefined);
30
+ /**
31
+ * A switch rather than a lookup table or a `data[name]` fallthrough: the
32
+ * fixed columns are read tens of times per feature by the layout pass, and
33
+ * they must not pay for an attribute scan. Anything not named here is an
34
+ * attribute, resolved on demand.
35
+ */
36
+ get(name: 'refName'): string;
37
+ get(name: 'name' | 'type' | 'id' | 'source'): string | undefined;
38
+ get(name: 'start' | 'end'): number;
39
+ get(name: 'phase'): 0 | 1 | 2 | undefined;
40
+ get(name: 'strand'): -1 | 0 | 1 | undefined;
41
+ get(name: 'score'): number | undefined;
42
+ get(name: 'subfeatures'): Feature[] | undefined;
43
+ get(name: string): unknown;
44
+ id(): string;
45
+ parent(): Feature | undefined;
46
+ /**
47
+ * Wrapped lazily rather than in the constructor: a feature the layout pass
48
+ * never reaches never allocates a wrapper per descendant. (SimpleFeature
49
+ * inflates its whole subtree eagerly on construction.)
50
+ */
51
+ children(): Feature[];
52
+ tags(): string[];
53
+ private columns;
54
+ toJSON(): SimpleFeatureSerialized;
55
+ }
@@ -0,0 +1,110 @@
1
+ import { getAttribute, getAttributes } from 'gff-nostream';
2
+ /**
3
+ * A {@link Feature} over a GFF3 feature whose attributes are still raw column-9
4
+ * text, resolving each one only when something asks for it.
5
+ *
6
+ * This exists because the two things that read a GFF3 feature want opposite
7
+ * amounts of it. Rendering reads a fixed handful of keys — `start`, `end`,
8
+ * `strand`, `type`, `phase`, `subfeatures`, plus `name`/`id` for labels and
9
+ * `gbkey` for the stock NCBI filter — and then packs the result into typed
10
+ * arrays, so the feature objects are dropped and never cross the RPC boundary.
11
+ * The feature details panel wants everything, and re-fetches the region from
12
+ * scratch to get it (`GetCanvasFeatureDetails`). Parsing every attribute during
13
+ * a render therefore did work that rendering did not use and that the details
14
+ * path did not reuse.
15
+ *
16
+ * `toJSON` still materializes the lot, which is what the details path
17
+ * serializes, so nothing downstream sees a smaller feature than it used to.
18
+ */
19
+ export class Gff3Feature {
20
+ data;
21
+ uniqueId;
22
+ parentFeature;
23
+ inflated;
24
+ constructor(data, uniqueId, parentFeature) {
25
+ this.data = data;
26
+ this.uniqueId = uniqueId;
27
+ this.parentFeature = parentFeature;
28
+ }
29
+ get(name) {
30
+ switch (name) {
31
+ case 'start': {
32
+ return this.data.start;
33
+ }
34
+ case 'end': {
35
+ return this.data.end;
36
+ }
37
+ case 'type': {
38
+ return this.data.type;
39
+ }
40
+ case 'refName': {
41
+ return this.data.refName;
42
+ }
43
+ case 'source': {
44
+ return this.data.source;
45
+ }
46
+ case 'score': {
47
+ return this.data.score;
48
+ }
49
+ case 'phase': {
50
+ return this.data.phase;
51
+ }
52
+ case 'strand': {
53
+ // a subfeature with no strand column of its own inherits its parent's,
54
+ // which is what SimpleFeature does too — it resolves the same way here,
55
+ // through the parent handle rather than by copying the field down
56
+ return this.data.strand ?? this.parentFeature?.get('strand');
57
+ }
58
+ case 'subfeatures': {
59
+ return this.children();
60
+ }
61
+ case 'parent': {
62
+ return this.parentFeature;
63
+ }
64
+ default: {
65
+ return getAttribute(this.data, name);
66
+ }
67
+ }
68
+ }
69
+ id() {
70
+ return this.uniqueId;
71
+ }
72
+ parent() {
73
+ return this.parentFeature;
74
+ }
75
+ /**
76
+ * Wrapped lazily rather than in the constructor: a feature the layout pass
77
+ * never reaches never allocates a wrapper per descendant. (SimpleFeature
78
+ * inflates its whole subtree eagerly on construction.)
79
+ */
80
+ children() {
81
+ this.inflated ??= this.data.subfeatures.map((f, i) => new Gff3Feature(f, `${this.uniqueId}-${i}`, this));
82
+ return this.inflated;
83
+ }
84
+ tags() {
85
+ return [
86
+ ...Object.keys(this.columns()),
87
+ 'subfeatures',
88
+ ...Object.keys(getAttributes(this.data)),
89
+ ];
90
+ }
91
+ // the eight parsed columns, without the raw attribute text or the nested
92
+ // features, both of which the serialized form carries differently
93
+ columns() {
94
+ const { attributeString, subfeatures, ...rest } = this.data;
95
+ return rest;
96
+ }
97
+ toJSON() {
98
+ const d = {
99
+ ...this.columns(),
100
+ ...getAttributes(this.data),
101
+ strand: this.get('strand'),
102
+ uniqueId: this.uniqueId,
103
+ subfeatures: this.children().map(f => f.toJSON()),
104
+ };
105
+ if (this.parentFeature) {
106
+ d.parentId = this.parentFeature.id();
107
+ }
108
+ return d;
109
+ }
110
+ }
@@ -1,21 +1,12 @@
1
- import { TabixIndexedFile } from '@gmod/tabix';
2
1
  import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
2
+ import type { Gff3TabixAdapterConfig } from './configSchema.ts';
3
3
  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
- export default class Gff3TabixAdapter extends BaseFeatureDataAdapter {
7
- private configured?;
8
- private configurePre;
9
- protected configurePre2(): Promise<{
10
- gff: TabixIndexedFile;
11
- dontRedispatchSet: Set<string>;
12
- }>;
13
- configure(opts?: BaseOptions): Promise<{
14
- gff: TabixIndexedFile;
15
- dontRedispatchSet: Set<string>;
16
- }>;
6
+ export default class Gff3TabixAdapter extends BaseFeatureDataAdapter<Gff3TabixAdapterConfig> {
7
+ private configure;
17
8
  getRefNames(opts?: BaseOptions): Promise<string[]>;
18
9
  getHeader(opts?: BaseOptions): Promise<string>;
10
+ getRegionByteSize(regions: Region[], opts?: BaseOptions): Promise<number>;
19
11
  getFeatures(query: Region, opts?: BaseOptions): import("rxjs").Observable<Feature>;
20
- private getFeaturesHelper;
21
12
  }
@@ -1,108 +1,66 @@
1
1
  import { TabixIndexedFile } from '@gmod/tabix';
2
- import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
3
- import { updateStatus } from '@jbrowse/core/util';
4
- import { openLocation } from '@jbrowse/core/util/io';
2
+ import { BaseFeatureDataAdapter, cachedSetup, } from '@jbrowse/core/data_adapters/BaseAdapter';
3
+ import { downloadStatus } from '@jbrowse/core/util';
4
+ import { sharedBgzfWorkerPool } from '@jbrowse/core/util/bgzfWorkerPool';
5
+ import { decompressedBytesBudget } from '@jbrowse/core/util/cacheBudgets';
6
+ import { openLocation, openTabixIndexFilehandle } from '@jbrowse/core/util/io';
5
7
  import { doesIntersect2 } from '@jbrowse/core/util/range';
6
8
  import { ObservableCreate } from '@jbrowse/core/util/rxjs';
7
- import SimpleFeature from '@jbrowse/core/util/simpleFeature';
8
- import { parseRecordsJBrowse } from 'gff-nostream';
9
+ import { readTabixLinesRedispatched } from '@jbrowse/core/util/tabix';
10
+ import { parseRecordsLazy } from 'gff-nostream';
11
+ import { Gff3Feature } from '../Gff3Feature.js';
9
12
  export default class Gff3TabixAdapter extends BaseFeatureDataAdapter {
10
- configured;
11
- async configurePre(_opts) {
12
- const gffGzLocation = this.getConf('gffGzLocation');
13
- const indexType = this.getConf(['index', 'indexType']);
14
- const loc = this.getConf(['index', 'location']);
15
- const dontRedispatch = this.getConf('dontRedispatch');
16
- const gff = new TabixIndexedFile({
17
- filehandle: openLocation(gffGzLocation, this.pluginManager),
18
- csiFilehandle: indexType === 'CSI' ? openLocation(loc, this.pluginManager) : undefined,
19
- tbiFilehandle: indexType !== 'CSI' ? openLocation(loc, this.pluginManager) : undefined,
20
- chunkCacheSize: 50 * 2 ** 20,
21
- });
22
- return {
23
- gff,
24
- dontRedispatchSet: new Set(dontRedispatch),
25
- header: await gff.getHeader(),
26
- };
27
- }
28
- async configurePre2() {
29
- if (!this.configured) {
30
- this.configured = this.configurePre().catch((e) => {
31
- this.configured = undefined;
32
- throw e;
13
+ configure = cachedSetup({
14
+ label: 'Downloading index',
15
+ setup: async (_opts, onProgress) => {
16
+ const gff = new TabixIndexedFile({
17
+ filehandle: openLocation(this.getConf('gffGzLocation'), this.pluginManager),
18
+ ...openTabixIndexFilehandle(this.getConf(['index', 'location']), this.getConf(['index', 'indexType']), this.pluginManager),
19
+ chunkCacheBudget: decompressedBytesBudget,
20
+ bgzfWorkerPool: sharedBgzfWorkerPool(),
33
21
  });
34
- }
35
- return this.configured;
36
- }
37
- async configure(opts) {
38
- const { statusCallback = () => { } } = opts || {};
39
- return updateStatus('Downloading index', statusCallback, () => this.configurePre2());
40
- }
22
+ return {
23
+ gff,
24
+ dontRedispatchSet: new Set(this.getConf('dontRedispatch')),
25
+ // the index is a whole-file read, so its byte ticks turn the
26
+ // "Downloading index" label into a determinate bar
27
+ header: await gff.getHeader({ onProgress }),
28
+ };
29
+ },
30
+ });
41
31
  async getRefNames(opts = {}) {
42
32
  const { gff } = await this.configure(opts);
43
- return gff.getReferenceSequenceNames(opts);
33
+ return downloadStatus('Downloading index', opts.statusCallback, onProgress => gff.getReferenceSequenceNames({ ...opts, onProgress }));
44
34
  }
45
35
  async getHeader(opts = {}) {
36
+ const { header } = await this.configure(opts);
37
+ return header;
38
+ }
39
+ // Index-only compressed-byte estimate (no feature download), used by the
40
+ // feature-fetch RPC to short-circuit an over-budget region before pulling
41
+ // every line — see executeRenderFeatureData.
42
+ async getRegionByteSize(regions, opts = {}) {
46
43
  const { gff } = await this.configure(opts);
47
- return gff.getHeader();
44
+ return gff.bytesForRegions(regions, opts);
48
45
  }
49
46
  getFeatures(query, opts = {}) {
50
47
  return ObservableCreate(async (observer) => {
51
- const { gff } = await this.configure(opts);
52
- const metadata = await gff.getMetadata();
53
- await this.getFeaturesHelper(query, opts, metadata, observer, true);
54
- }, opts.stopToken);
55
- }
56
- async getFeaturesHelper(query, opts, metadata, observer, allowRedispatch, originalQuery = query) {
57
- const { statusCallback = () => { } } = opts;
58
- try {
59
- const lines = [];
60
- const { dontRedispatchSet, gff } = await this.configure(opts);
61
- await updateStatus('Downloading features', statusCallback, () => gff.getLines(query.refName, query.start, query.end, (line, fileOffset, start, end) => {
62
- const t1 = line.indexOf('\t');
63
- const t2 = line.indexOf('\t', t1 + 1);
64
- const t3 = line.indexOf('\t', t2 + 1);
65
- const type = line.slice(t2 + 1, t3);
66
- lines.push({
67
- line,
68
- lineHash: fileOffset,
69
- start,
70
- end,
71
- hasEscapes: line.includes('%'),
72
- type,
73
- });
74
- }));
75
- if (allowRedispatch && lines.length) {
76
- let minStart = Number.POSITIVE_INFINITY;
77
- let maxEnd = Number.NEGATIVE_INFINITY;
78
- for (const rec of lines) {
79
- if (!dontRedispatchSet.has(rec.type)) {
80
- const start = rec.start - 1;
81
- if (start < minStart) {
82
- minStart = start;
83
- }
84
- if (rec.end > maxEnd) {
85
- maxEnd = rec.end;
86
- }
48
+ try {
49
+ const { gff, dontRedispatchSet } = await this.configure(opts);
50
+ const lines = await readTabixLinesRedispatched(gff, query, dontRedispatchSet, opts);
51
+ // emit only top-level features intersecting the original query. the
52
+ // byte offset stays on our own record and is used purely to mint a
53
+ // stable id, so it never pollutes the feature's data
54
+ for (const { feature, record } of parseRecordsLazy(lines)) {
55
+ if (doesIntersect2(feature.start, feature.end, query.start, query.end)) {
56
+ observer.next(new Gff3Feature(feature, `${this.id}-offset-${record.offset}`));
87
57
  }
88
58
  }
89
- if (maxEnd > query.end || minStart < query.start) {
90
- await this.getFeaturesHelper({ ...query, start: minStart, end: maxEnd }, opts, metadata, observer, false, query);
91
- return;
92
- }
59
+ observer.complete();
93
60
  }
94
- for (const feature of parseRecordsJBrowse(lines)) {
95
- if (doesIntersect2(feature.start, feature.end, originalQuery.start, originalQuery.end)) {
96
- observer.next(new SimpleFeature({
97
- data: feature,
98
- id: `${this.id}-offset-${feature._lineHash}`,
99
- }));
100
- }
61
+ catch (e) {
62
+ observer.error(e);
101
63
  }
102
- observer.complete();
103
- }
104
- catch (e) {
105
- observer.error(e);
106
- }
64
+ }, opts.stopToken);
107
65
  }
108
66
  }
@@ -1,28 +1,68 @@
1
+ import type { Instance } from '@jbrowse/mobx-state-tree';
2
+ export declare function normalizeSnapshot(snap: Record<string, unknown>): Record<string, unknown>;
3
+ /**
4
+ * #config Gff3TabixAdapter
5
+ * #category adapter
6
+ * #trackType FeatureTrack
7
+ * #fileFormat feature | GFF3 (tabix)
8
+ * used to load bgzip-compressed, tabix-indexed GFF3 files
9
+ *
10
+ * #example
11
+ * The `uri` shorthand auto-resolves the `.tbi` index; add `csi: true` for a
12
+ * `.csi` index instead:
13
+ * ```js
14
+ * {
15
+ * type: 'Gff3TabixAdapter',
16
+ * uri: 'https://example.com/genes.gff3.gz',
17
+ * }
18
+ * ```
19
+ */
1
20
  declare const Gff3TabixAdapter: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
2
- gffGzLocation: {
3
- type: string;
4
- defaultValue: {
5
- uri: string;
6
- locationType: string;
21
+ /**
22
+ * #slot
23
+ * location of the bgzip-compressed GFF3, sorted by position. Must be bgzip
24
+ * rather than plain gzip, which tabix cannot index.
25
+ */
26
+ readonly gffGzLocation: {
27
+ readonly type: "fileLocation";
28
+ readonly defaultValue: {
29
+ readonly uri: "/path/to/my.gff.gz";
30
+ readonly locationType: "UriLocation";
7
31
  };
8
32
  };
9
- index: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
10
- indexType: {
11
- model: import("@jbrowse/mobx-state-tree").ISimpleType<"TBI" | "CSI">;
12
- type: string;
13
- defaultValue: string;
33
+ readonly index: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
34
+ readonly indexType: {
35
+ readonly model: import("@jbrowse/mobx-state-tree").ISimpleType<"CSI" | "TBI">;
36
+ readonly type: 'stringEnum';
37
+ readonly defaultValue: 'TBI';
38
+ readonly description: '`TBI` is the usual `tabix` output. `CSI` is required for a reference longer than 512 Mb, which TBI cannot address.';
14
39
  };
15
- location: {
16
- type: string;
17
- defaultValue: {
18
- uri: string;
19
- locationType: string;
40
+ readonly location: {
41
+ readonly type: 'fileLocation';
42
+ readonly defaultValue: {
43
+ readonly uri: '/path/to/my.gz.tbi';
44
+ readonly locationType: 'UriLocation';
20
45
  };
46
+ readonly description: 'location of the tabix index. Only needed when it is not named `<file>.tbi` (or `.csi`), which is what the `uri` shorthand assumes.';
21
47
  };
22
48
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
23
- dontRedispatch: {
24
- type: string;
25
- defaultValue: string[];
49
+ /**
50
+ * #slot
51
+ * the Gff3TabixAdapter has to "redispatch" if it fetches a region and
52
+ * features it finds inside that region extend outside the region we requested.
53
+ * you can disable this for certain feature types to avoid fetching e.g. the
54
+ * entire chromosome
55
+ *
56
+ * the defaults are the whole-sequence records the common GFF3 sources emit:
57
+ * `region` (NCBI), `supercontig`/`scaffold` (Ensembl, for non-chromosomal
58
+ * sequences), plus `chromosome` and `contig`. They span an entire reference
59
+ * and have no children, so letting one expand the fetch would pull a whole
60
+ * chromosome to gain nothing
61
+ */
62
+ readonly dontRedispatch: {
63
+ readonly type: "stringArray";
64
+ readonly defaultValue: readonly ["chromosome", "region", "contig", "supercontig", "scaffold"];
26
65
  };
27
66
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
67
+ export type Gff3TabixAdapterConfig = Instance<typeof Gff3TabixAdapter>;
28
68
  export default Gff3TabixAdapter;
@@ -1,7 +1,30 @@
1
- import { ConfigurationSchema } from '@jbrowse/core/configuration';
2
- import { types } from '@jbrowse/mobx-state-tree';
3
- function x() { }
1
+ import { ConfigurationSchema, expandTabixShorthand, tabixIndexFields, } from '@jbrowse/core/configuration';
2
+ export function normalizeSnapshot(snap) {
3
+ return expandTabixShorthand(snap, 'gffGzLocation');
4
+ }
5
+ /**
6
+ * #config Gff3TabixAdapter
7
+ * #category adapter
8
+ * #trackType FeatureTrack
9
+ * #fileFormat feature | GFF3 (tabix)
10
+ * used to load bgzip-compressed, tabix-indexed GFF3 files
11
+ *
12
+ * #example
13
+ * The `uri` shorthand auto-resolves the `.tbi` index; add `csi: true` for a
14
+ * `.csi` index instead:
15
+ * ```js
16
+ * {
17
+ * type: 'Gff3TabixAdapter',
18
+ * uri: 'https://example.com/genes.gff3.gz',
19
+ * }
20
+ * ```
21
+ */
4
22
  const Gff3TabixAdapter = ConfigurationSchema('Gff3TabixAdapter', {
23
+ /**
24
+ * #slot
25
+ * location of the bgzip-compressed GFF3, sorted by position. Must be bgzip
26
+ * rather than plain gzip, which tabix cannot index.
27
+ */
5
28
  gffGzLocation: {
6
29
  type: 'fileLocation',
7
30
  defaultValue: {
@@ -9,42 +32,47 @@ const Gff3TabixAdapter = ConfigurationSchema('Gff3TabixAdapter', {
9
32
  locationType: 'UriLocation',
10
33
  },
11
34
  },
12
- index: ConfigurationSchema('Gff3TabixIndex', {
13
- indexType: {
14
- model: types.enumeration('IndexType', ['TBI', 'CSI']),
15
- type: 'stringEnum',
16
- defaultValue: 'TBI',
17
- },
18
- location: {
19
- type: 'fileLocation',
20
- defaultValue: {
21
- uri: '/path/to/my.gff.gz.tbi',
22
- locationType: 'UriLocation',
23
- },
24
- },
25
- }),
35
+ index: ConfigurationSchema('TabixIndex', { ...tabixIndexFields }),
36
+ /**
37
+ * #slot
38
+ * the Gff3TabixAdapter has to "redispatch" if it fetches a region and
39
+ * features it finds inside that region extend outside the region we requested.
40
+ * you can disable this for certain feature types to avoid fetching e.g. the
41
+ * entire chromosome
42
+ *
43
+ * the defaults are the whole-sequence records the common GFF3 sources emit:
44
+ * `region` (NCBI), `supercontig`/`scaffold` (Ensembl, for non-chromosomal
45
+ * sequences), plus `chromosome` and `contig`. They span an entire reference
46
+ * and have no children, so letting one expand the fetch would pull a whole
47
+ * chromosome to gain nothing
48
+ */
26
49
  dontRedispatch: {
27
50
  type: 'stringArray',
28
- defaultValue: ['chromosome', 'region', 'contig'],
51
+ defaultValue: [
52
+ 'chromosome',
53
+ 'region',
54
+ 'contig',
55
+ 'supercontig',
56
+ 'scaffold',
57
+ ],
29
58
  },
30
59
  }, {
31
60
  explicitlyTyped: true,
32
- preProcessSnapshot: snap => {
33
- return snap.uri
34
- ? {
35
- ...snap,
36
- gffGzLocation: {
37
- uri: snap.uri,
38
- baseUri: snap.baseUri,
39
- },
40
- index: {
41
- location: {
42
- uri: `${snap.uri}.tbi`,
43
- baseUri: snap.baseUri,
44
- },
45
- },
46
- }
47
- : snap;
48
- },
61
+ /**
62
+ * #preProcessSnapshot
63
+ *
64
+ *
65
+ * preprocessor to allow minimal config, assumes tbi index at
66
+ * yourfile.gff3.gz.tbi (or .csi if csi:true):
67
+ *
68
+ * ```json
69
+ * {
70
+ * "type": "Gff3TabixAdapter",
71
+ * "uri": "yourfile.gff3.gz",
72
+ * "csi": true
73
+ * }
74
+ * ```
75
+ */
76
+ preProcessSnapshot: normalizeSnapshot,
49
77
  });
50
78
  export default Gff3TabixAdapter;
@@ -1,10 +1,11 @@
1
1
  import { AdapterType } from '@jbrowse/core/pluggableElementTypes';
2
- import configSchema from "./configSchema.js";
2
+ import configSchema, { normalizeSnapshot } from './configSchema.js';
3
3
  export default function Gff3TabixAdapterF(pluginManager) {
4
4
  pluginManager.addAdapterType(() => new AdapterType({
5
5
  name: 'Gff3TabixAdapter',
6
6
  displayName: 'GFF3 tabix adapter',
7
+ normalizeSnapshot,
7
8
  configSchema,
8
- getAdapterClass: () => import("./Gff3TabixAdapter.js").then(r => r.default),
9
+ getAdapterClass: () => import('./Gff3TabixAdapter.js').then(r => r.default),
9
10
  }));
10
11
  }
package/esm/index.js CHANGED
@@ -1,12 +1,10 @@
1
1
  import Plugin from '@jbrowse/core/Plugin';
2
- import Gff3AdapterF from "./Gff3Adapter/index.js";
3
- import Gff3TabixAdapterF from "./Gff3TabixAdapter/index.js";
4
- import GuessGff3F from "./GuessGff3/index.js";
2
+ import Gff3AdapterF from './Gff3Adapter/index.js';
3
+ import Gff3TabixAdapterF from './Gff3TabixAdapter/index.js';
5
4
  export default class GFF3Plugin extends Plugin {
6
5
  name = 'GFF3Plugin';
7
6
  install(pluginManager) {
8
7
  Gff3TabixAdapterF(pluginManager);
9
8
  Gff3AdapterF(pluginManager);
10
- GuessGff3F(pluginManager);
11
9
  }
12
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/plugin-gff3",
3
- "version": "4.3.0",
3
+ "version": "5.0.0-beta.1",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 gff3.",
6
6
  "keywords": [
@@ -21,13 +21,12 @@
21
21
  "esm"
22
22
  ],
23
23
  "dependencies": {
24
- "@flatten-js/interval-tree": "^2.0.3",
25
- "@gmod/tabix": "^3.3.3",
26
- "@jbrowse/mobx-state-tree": "^5.6.0",
27
- "gff-nostream": "^3.0.5",
28
- "mobx": "^6.15.3",
24
+ "@gmod/tabix": "^3.8.2",
25
+ "@jbrowse/mobx-state-tree": "^6.5.1",
26
+ "gff-nostream": "^5.3.0",
27
+ "mobx": "^7.0.3",
29
28
  "rxjs": "^7.8.2",
30
- "@jbrowse/core": "^4.3.0"
29
+ "@jbrowse/core": "^5.0.0-beta.1"
31
30
  },
32
31
  "publishConfig": {
33
32
  "access": "public"
@@ -35,16 +34,13 @@
35
34
  "sideEffects": false,
36
35
  "scripts": {
37
36
  "build": "pnpm run /^build:/",
38
- "test": "cd ../..; jest --passWithNoTests plugins/gff3 --passWithNoTests",
37
+ "test": "cd ../..; jest --passWithNoTests plugins/gff3",
39
38
  "prebuild": "pnpm clean",
40
- "build:esm": "tsc -p tsconfig.build.esm.json",
39
+ "build:esm": "node ../../node_modules/typescript7/bin/tsc --build tsconfig.build.esm.json",
41
40
  "clean": "rimraf esm *.tsbuildinfo"
42
41
  },
43
42
  "types": "esm/index.d.ts",
44
43
  "exports": {
45
- ".": {
46
- "types": "./esm/index.d.ts",
47
- "import": "./esm/index.js"
48
- }
44
+ ".": "./esm/index.js"
49
45
  }
50
46
  }
@@ -1,7 +0,0 @@
1
- import type { StatusCallback } from '@jbrowse/core/util/parseLineByLine';
2
- interface GffParseResult {
3
- header: string;
4
- featureMap: Record<string, string>;
5
- }
6
- export declare function parseGffBuffer(buffer: Uint8Array, statusCallback?: StatusCallback): GffParseResult;
7
- export {};
@@ -1,26 +0,0 @@
1
- import { parseLineByLine } from '@jbrowse/core/util/parseLineByLine';
2
- export function parseGffBuffer(buffer, statusCallback = () => { }) {
3
- const headerLines = [];
4
- const featureMap = {};
5
- parseLineByLine(buffer, line => {
6
- if (line.startsWith('#')) {
7
- headerLines.push(line);
8
- }
9
- else if (line.startsWith('>')) {
10
- return false;
11
- }
12
- else {
13
- const ret = line.indexOf('\t');
14
- const refName = line.slice(0, ret);
15
- if (!featureMap[refName]) {
16
- featureMap[refName] = '';
17
- }
18
- featureMap[refName] += `${line}\n`;
19
- }
20
- return true;
21
- }, statusCallback);
22
- return {
23
- header: headerLines.join('\n'),
24
- featureMap,
25
- };
26
- }
@@ -1,2 +0,0 @@
1
- import type PluginManager from '@jbrowse/core/PluginManager';
2
- export default function GuessGff3F(pluginManager: PluginManager): void;
@@ -1,30 +0,0 @@
1
- import { testAdapter } from '@jbrowse/core/util';
2
- import { getFileName, makeIndex, makeIndexType, } from '@jbrowse/core/util/tracks';
3
- export default function GuessGff3F(pluginManager) {
4
- pluginManager.addToExtensionPoint('Core-guessAdapterForLocation', (adapterGuesser) => {
5
- return (file, index, adapterHint) => {
6
- const fileName = getFileName(file);
7
- const indexName = index && getFileName(index);
8
- if (testAdapter(fileName, /\.gff3?\.b?gz$/i, adapterHint, 'Gff3TabixAdapter')) {
9
- return {
10
- type: 'Gff3TabixAdapter',
11
- bamLocation: file,
12
- gffGzLocation: file,
13
- index: {
14
- location: index || makeIndex(file, '.tbi'),
15
- indexType: makeIndexType(indexName, 'CSI', 'TBI'),
16
- },
17
- };
18
- }
19
- else if (testAdapter(fileName, /\.gff3?$/i, adapterHint, 'Gff3Adapter')) {
20
- return {
21
- type: 'Gff3Adapter',
22
- gffLocation: file,
23
- };
24
- }
25
- else {
26
- return adapterGuesser(file, index, adapterHint);
27
- }
28
- };
29
- });
30
- }
@@ -1,16 +0,0 @@
1
- import type { GFF3FeatureLineWithRefs } from 'gff-nostream';
2
- interface GFF3Feature {
3
- start: number;
4
- end: number;
5
- strand?: number;
6
- type: string | null;
7
- source: string | null;
8
- refName: string;
9
- derived_features: unknown[] | null;
10
- phase?: number;
11
- score?: number;
12
- subfeatures: GFF3Feature[] | undefined;
13
- [key: string]: unknown;
14
- }
15
- export declare function featureData(data: GFF3FeatureLineWithRefs): GFF3Feature;
16
- export {};
@@ -1,52 +0,0 @@
1
- const DEFAULT_FIELDS = new Set([
2
- 'start',
3
- 'end',
4
- 'seq_id',
5
- 'score',
6
- 'type',
7
- 'source',
8
- 'phase',
9
- 'strand',
10
- ]);
11
- export function featureData(data) {
12
- const { end, start, child_features, derived_features, attributes, type, source, phase, seq_id, score, strand, } = data;
13
- let strand2;
14
- if (strand === '+') {
15
- strand2 = 1;
16
- }
17
- else if (strand === '-') {
18
- strand2 = -1;
19
- }
20
- else if (strand === '.') {
21
- strand2 = 0;
22
- }
23
- const dataAttributes = attributes || {};
24
- const resultAttributes = {};
25
- for (const a of Object.keys(dataAttributes)) {
26
- let b = a.toLowerCase();
27
- if (DEFAULT_FIELDS.has(b)) {
28
- b += '2';
29
- }
30
- if (dataAttributes[a] && a !== '_lineHash') {
31
- let attr = dataAttributes[a];
32
- if (Array.isArray(attr) && attr.length === 1) {
33
- ;
34
- [attr] = attr;
35
- }
36
- resultAttributes[b] = attr;
37
- }
38
- }
39
- return {
40
- ...resultAttributes,
41
- start: start - 1,
42
- end: end,
43
- strand: strand2,
44
- type,
45
- source,
46
- refName: seq_id,
47
- derived_features,
48
- phase: phase === null ? undefined : Number(phase),
49
- score: score === null ? undefined : score,
50
- subfeatures: child_features.flatMap(childLocs => childLocs.map(childLoc => featureData(childLoc))),
51
- };
52
- }