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

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,40 @@
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;
31
- }
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
- }
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
+ //
26
+ // The id the tree entry carries is minted here, beside the feature
27
+ // rather than stamped onto it, so the parsed feature is the library's
28
+ // shape and Gff3Feature serializes it identically for both adapters.
29
+ (lines, refName) => parseLinesLazy(lines).map((feature, i) => ({
30
+ start: feature.start,
31
+ end: feature.end,
32
+ feature,
33
+ uniqueId: `${this.id}-${refName}-${i}`,
34
+ })), 'Parsing GFF data');
35
+ return { header: headerLines.join('\n'), intervalTreeMap };
36
+ },
37
+ });
49
38
  async getRefNames(opts = {}) {
50
39
  const { intervalTreeMap } = await this.loadData(opts);
51
40
  return Object.keys(intervalTreeMap);
@@ -55,21 +44,21 @@ export default class Gff3Adapter extends BaseFeatureDataAdapter {
55
44
  return header;
56
45
  }
57
46
  getFeatures(query, opts = {}) {
47
+ // no try/catch: ObservableCreate forwards a rejected callback to
48
+ // observer.error itself
58
49
  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([
50
+ const { start, end, refName } = query;
51
+ const { intervalTreeMap } = await this.loadData(opts);
52
+ const tree = intervalTreeMap[refName];
53
+ if (tree) {
54
+ for (const { feature, uniqueId } of tree(opts.statusCallback).search([
63
55
  start,
64
56
  end,
65
- ]) || []) {
66
- observer.next(f);
57
+ ])) {
58
+ observer.next(new Gff3Feature(feature, uniqueId));
67
59
  }
68
- observer.complete();
69
- }
70
- catch (e) {
71
- observer.error(e);
72
60
  }
61
+ observer.complete();
73
62
  }, opts.stopToken);
74
63
  }
75
64
  }
@@ -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,51 @@
1
+ import type { Feature, SimpleFeatureSerialized } from '@jbrowse/core/util/simpleFeature';
2
+ import type { LazyGffFeature } from 'gff-nostream';
3
+ /**
4
+ * A {@link Feature} over a GFF3 feature whose attributes are still raw column-9
5
+ * text, resolving each one only when something asks for it.
6
+ *
7
+ * This exists because the two things that read a GFF3 feature want opposite
8
+ * amounts of it. Rendering reads a fixed handful of keys — `start`, `end`,
9
+ * `strand`, `type`, `phase`, `subfeatures`, plus `name`/`id` for labels and
10
+ * `gbkey` for the stock NCBI filter — and then packs the result into typed
11
+ * arrays, so the feature objects are dropped and never cross the RPC boundary.
12
+ * The feature details panel wants everything, and re-fetches the region from
13
+ * scratch to get it (`GetCanvasFeatureDetails`). Parsing every attribute during
14
+ * a render therefore did work that rendering did not use and that the details
15
+ * path did not reuse.
16
+ *
17
+ * `toJSON` still materializes the lot, which is what the details path
18
+ * serializes, so nothing downstream sees a smaller feature than it used to.
19
+ */
20
+ export declare class Gff3Feature implements Feature {
21
+ private data;
22
+ private uniqueId;
23
+ private parentFeature?;
24
+ private inflated?;
25
+ constructor(data: LazyGffFeature, uniqueId: string, parentFeature?: Feature | undefined);
26
+ /**
27
+ * A switch rather than a lookup table or a `data[name]` fallthrough: the
28
+ * fixed columns are read tens of times per feature by the layout pass, and
29
+ * they must not pay for an attribute scan. Anything not named here is an
30
+ * attribute, resolved on demand.
31
+ */
32
+ get(name: 'refName'): string;
33
+ get(name: 'name' | 'type' | 'id' | 'source'): string | undefined;
34
+ get(name: 'start' | 'end'): number;
35
+ get(name: 'phase'): 0 | 1 | 2 | undefined;
36
+ get(name: 'strand'): -1 | 0 | 1 | undefined;
37
+ get(name: 'score'): number | undefined;
38
+ get(name: 'subfeatures'): Feature[] | undefined;
39
+ get(name: string): unknown;
40
+ id(): string;
41
+ parent(): Feature | undefined;
42
+ /**
43
+ * Wrapped lazily rather than in the constructor: a feature the layout pass
44
+ * never reaches never allocates a wrapper per descendant. (SimpleFeature
45
+ * inflates its whole subtree eagerly on construction.)
46
+ */
47
+ children(): Feature[];
48
+ tags(): string[];
49
+ private columns;
50
+ toJSON(): SimpleFeatureSerialized;
51
+ }
@@ -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,85 @@
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 { hasIdAttribute, 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
+ // The bound exists to reach a record's children, so what disqualifies a
51
+ // record is having none. `ID` answers that exactly — nothing can name a
52
+ // parent that has no name — and gff-nostream decides it with the scan
53
+ // its linker uses, so a record the tree would attach children to is
54
+ // never one the bound leaves out. `dontRedispatch` can only approximate
55
+ // the same question by type: every NCBI `GCF_*_genomic.gff.gz` opens
56
+ // each reference with a chromosome-long `match` record no list named,
57
+ // and none of them has an `ID`. The list covers what `ID` cannot see: a
58
+ // record carrying an `ID` that nothing references, which hosted hg19
59
+ // RefSeq's chromosome-long `region` is.
60
+ const canHaveChildren = (line) => hasIdAttribute(line.line) && !dontRedispatchSet.has(line.type);
61
+ const lines = await readTabixLinesRedispatched(gff, query,
62
+ // A caller reading only top-level features is owed nothing by the
63
+ // flanks and pays for them anyway: they complete SUBFEATURE lists, and
64
+ // a top-level feature overlapping the query is in the query's own read
65
+ // already, since tabix returns every overlapping line and a child is
66
+ // contained in its parent. On an NCBI `GCF_*_genomic.gff.gz`, whose
67
+ // chromosome-long `match` widens the bound at every window, that is
68
+ // 193,008 lines parsed to keep 3 features — 2734 ms against 8 ms.
69
+ opts.topLevelOnly ? () => false : canHaveChildren, opts);
70
+ // emit only top-level features intersecting the original query. the
71
+ // byte offset stays on our own record and is used purely to mint a
72
+ // stable id, so it never pollutes the feature's data
73
+ for (const { feature, record } of parseRecordsLazy(lines)) {
74
+ if (doesIntersect2(feature.start, feature.end, query.start, query.end)) {
75
+ observer.next(new Gff3Feature(feature, `${this.id}-offset-${record.offset}`));
87
76
  }
88
77
  }
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
- }
78
+ observer.complete();
93
79
  }
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
- }
80
+ catch (e) {
81
+ observer.error(e);
101
82
  }
102
- observer.complete();
103
- }
104
- catch (e) {
105
- observer.error(e);
106
- }
83
+ }, opts.stopToken);
107
84
  }
108
85
  }
@@ -1,28 +1,73 @@
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
+ readonly densityAdapter: {
22
+ readonly type: 'frozen';
23
+ readonly defaultValue: null;
24
+ readonly description: 'optional quantitative sub-adapter (e.g. a BigWigAdapter over a features-per-bin bigWig) drawn as a density band where the region is too large to fetch features; null disables it';
25
+ };
26
+ /**
27
+ * #slot
28
+ * location of the bgzip-compressed GFF3, sorted by position. Must be bgzip
29
+ * rather than plain gzip, which tabix cannot index.
30
+ */
31
+ readonly gffGzLocation: {
32
+ readonly type: "fileLocation";
33
+ readonly defaultValue: {
34
+ readonly uri: "/path/to/my.gff.gz";
35
+ readonly locationType: "UriLocation";
7
36
  };
8
37
  };
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;
38
+ readonly index: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
39
+ readonly indexType: {
40
+ readonly model: import("@jbrowse/mobx-state-tree").ISimpleType<"CSI" | "TBI">;
41
+ readonly type: 'stringEnum';
42
+ readonly defaultValue: 'TBI';
43
+ readonly description: '`TBI` is the usual `tabix` output. `CSI` is required for a reference longer than 512 Mb, which TBI cannot address.';
14
44
  };
15
- location: {
16
- type: string;
17
- defaultValue: {
18
- uri: string;
19
- locationType: string;
45
+ readonly location: {
46
+ readonly type: 'fileLocation';
47
+ readonly defaultValue: {
48
+ readonly uri: '/path/to/my.gz.tbi';
49
+ readonly locationType: 'UriLocation';
20
50
  };
51
+ 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
52
  };
22
53
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
23
- dontRedispatch: {
24
- type: string;
25
- defaultValue: string[];
54
+ /**
55
+ * #slot
56
+ * the Gff3TabixAdapter has to "redispatch" if it fetches a region and
57
+ * features it finds inside that region extend outside the region we requested.
58
+ * you can disable this for certain feature types to avoid fetching e.g. the
59
+ * entire chromosome
60
+ *
61
+ * the defaults are the whole-sequence records the common GFF3 sources emit:
62
+ * `region` (NCBI), `supercontig`/`scaffold` (Ensembl, for non-chromosomal
63
+ * sequences), plus `chromosome` and `contig`. They span an entire reference
64
+ * and have no children, so letting one expand the fetch would pull a whole
65
+ * chromosome to gain nothing
66
+ */
67
+ readonly dontRedispatch: {
68
+ readonly type: "stringArray";
69
+ readonly defaultValue: readonly ["chromosome", "region", "contig", "supercontig", "scaffold"];
26
70
  };
27
71
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
72
+ export type Gff3TabixAdapterConfig = Instance<typeof Gff3TabixAdapter>;
28
73
  export default Gff3TabixAdapter;
@@ -1,7 +1,31 @@
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
+ import { densityAdapterConfigSchemaFields } from '@jbrowse/core/data_adapters/BaseAdapter';
3
+ export function normalizeSnapshot(snap) {
4
+ return expandTabixShorthand(snap, 'gffGzLocation');
5
+ }
6
+ /**
7
+ * #config Gff3TabixAdapter
8
+ * #category adapter
9
+ * #trackType FeatureTrack
10
+ * #fileFormat feature | GFF3 (tabix)
11
+ * used to load bgzip-compressed, tabix-indexed GFF3 files
12
+ *
13
+ * #example
14
+ * The `uri` shorthand auto-resolves the `.tbi` index; add `csi: true` for a
15
+ * `.csi` index instead:
16
+ * ```js
17
+ * {
18
+ * type: 'Gff3TabixAdapter',
19
+ * uri: 'https://example.com/genes.gff3.gz',
20
+ * }
21
+ * ```
22
+ */
4
23
  const Gff3TabixAdapter = ConfigurationSchema('Gff3TabixAdapter', {
24
+ /**
25
+ * #slot
26
+ * location of the bgzip-compressed GFF3, sorted by position. Must be bgzip
27
+ * rather than plain gzip, which tabix cannot index.
28
+ */
5
29
  gffGzLocation: {
6
30
  type: 'fileLocation',
7
31
  defaultValue: {
@@ -9,42 +33,48 @@ const Gff3TabixAdapter = ConfigurationSchema('Gff3TabixAdapter', {
9
33
  locationType: 'UriLocation',
10
34
  },
11
35
  },
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
- }),
36
+ index: ConfigurationSchema('TabixIndex', { ...tabixIndexFields }),
37
+ /**
38
+ * #slot
39
+ * the Gff3TabixAdapter has to "redispatch" if it fetches a region and
40
+ * features it finds inside that region extend outside the region we requested.
41
+ * you can disable this for certain feature types to avoid fetching e.g. the
42
+ * entire chromosome
43
+ *
44
+ * the defaults are the whole-sequence records the common GFF3 sources emit:
45
+ * `region` (NCBI), `supercontig`/`scaffold` (Ensembl, for non-chromosomal
46
+ * sequences), plus `chromosome` and `contig`. They span an entire reference
47
+ * and have no children, so letting one expand the fetch would pull a whole
48
+ * chromosome to gain nothing
49
+ */
26
50
  dontRedispatch: {
27
51
  type: 'stringArray',
28
- defaultValue: ['chromosome', 'region', 'contig'],
52
+ defaultValue: [
53
+ 'chromosome',
54
+ 'region',
55
+ 'contig',
56
+ 'supercontig',
57
+ 'scaffold',
58
+ ],
29
59
  },
60
+ ...densityAdapterConfigSchemaFields,
30
61
  }, {
31
62
  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
- },
63
+ /**
64
+ * #preProcessSnapshot
65
+ *
66
+ *
67
+ * preprocessor to allow minimal config, assumes tbi index at
68
+ * yourfile.gff3.gz.tbi (or .csi if csi:true):
69
+ *
70
+ * ```json
71
+ * {
72
+ * "type": "Gff3TabixAdapter",
73
+ * "uri": "yourfile.gff3.gz",
74
+ * "csi": true
75
+ * }
76
+ * ```
77
+ */
78
+ preProcessSnapshot: normalizeSnapshot,
49
79
  });
50
80
  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.2",
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.4.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.2"
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
- }