@apollo-annotation/shared 0.2.0 → 0.2.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.
package/dist/util.d.ts CHANGED
@@ -1,4 +1 @@
1
- import { AnnotationFeatureSnapshot } from '@apollo-annotation/mst';
2
- import { GFF3Feature } from '@gmod/gff';
3
- export declare function makeGFF3Feature(feature: AnnotationFeatureSnapshot, parentId?: string, refSeqNames?: Record<string, string | undefined>): GFF3Feature;
4
1
  export declare function splitStringIntoChunks(input: string, chunkSize: number): string[];
package/dist/util.js CHANGED
@@ -1,96 +1,6 @@
1
1
  "use strict";
2
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
2
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.makeGFF3Feature = makeGFF3Feature;
5
3
  exports.splitStringIntoChunks = splitStringIntoChunks;
6
- function makeGFF3Feature(feature, parentId, refSeqNames) {
7
- const locations = [{ start: feature.min, end: feature.max }];
8
- // const locations = feature.discontinuousLocations?.length
9
- // ? feature.discontinuousLocations
10
- // : [{ start: feature.start, end: feature.end, phase: feature.phase }]
11
- const attributes = JSON.parse(JSON.stringify(feature.attributes));
12
- const ontologyTerms = [];
13
- const source = feature.attributes?.source?.[0] ?? null;
14
- delete attributes.source;
15
- if (parentId) {
16
- attributes.Parent = [parentId];
17
- }
18
- if (attributes._id) {
19
- attributes.ID = attributes._id;
20
- delete attributes._id;
21
- }
22
- if (attributes.gff_name) {
23
- attributes.Name = attributes.gff_name;
24
- delete attributes.gff_name;
25
- }
26
- if (attributes.gff_alias) {
27
- attributes.Alias = attributes.gff_alias;
28
- delete attributes.gff_alias;
29
- }
30
- if (attributes.gff_target) {
31
- attributes.Target = attributes.gff_target;
32
- delete attributes.gff_target;
33
- }
34
- if (attributes.gff_gap) {
35
- attributes.Gap = attributes.gff_gap;
36
- delete attributes.gff_gap;
37
- }
38
- if (attributes.gff_derives_from) {
39
- attributes.Derives_from = attributes.gff_derives_from;
40
- delete attributes.gff_derives_from;
41
- }
42
- if (attributes.gff_note) {
43
- attributes.Note = attributes.gff_note;
44
- delete attributes.gff_note;
45
- }
46
- if (attributes.gff_dbxref) {
47
- attributes.Dbxref = attributes.gff_dbxref;
48
- delete attributes.gff_dbxref;
49
- }
50
- if (attributes.gff_is_circular) {
51
- attributes.Is_circular = attributes.gff_is_circular;
52
- delete attributes.gff_is_circular;
53
- }
54
- if (attributes.gff_ontology_term) {
55
- ontologyTerms.push(...attributes.gff_ontology_term);
56
- delete attributes.gff_ontology_term;
57
- }
58
- if (attributes['Gene Ontology']) {
59
- ontologyTerms.push(...attributes['Gene Ontology']);
60
- delete attributes['Gene Ontology'];
61
- }
62
- if (attributes['Sequence Ontology']) {
63
- ontologyTerms.push(...attributes['Sequence Ontology']);
64
- delete attributes['Sequence Ontology'];
65
- }
66
- if (ontologyTerms.length > 0) {
67
- attributes.Ontology_term = ontologyTerms;
68
- }
69
- return locations.map((location) => ({
70
- start: location.start + 1,
71
- end: location.end,
72
- seq_id: refSeqNames ? refSeqNames[feature.refSeq] ?? null : feature.refSeq,
73
- source,
74
- type: feature.type,
75
- score: null,
76
- // score: feature.score ?? null,
77
- strand: feature.strand ? (feature.strand === 1 ? '+' : '-') : null,
78
- phase: null,
79
- // phase:
80
- // location.phase === 0
81
- // ? '0'
82
- // : location.phase === 1
83
- // ? '1'
84
- // : location.phase === 2
85
- // ? '2'
86
- // : null,
87
- attributes: Object.keys(attributes).length > 0 ? attributes : null,
88
- derived_features: [],
89
- child_features: feature.children
90
- ? Object.values(feature.children).map((child) => makeGFF3Feature(child, attributes.ID?.[0], refSeqNames))
91
- : [],
92
- }));
93
- }
94
4
  function splitStringIntoChunks(input, chunkSize) {
95
5
  const chunks = [];
96
6
  for (let i = 0; i < input.length; i += chunkSize) {
package/dist/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;AAAA,sDAUC;AAVD,SAAgB,qBAAqB,CACnC,KAAa,EACb,SAAiB;IAEjB,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,SAAS,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAAA;QAC3C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACpB,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollo-annotation/shared",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "main": "./dist/index.js",
5
5
  "scripts": {
6
6
  "build": "yarn clean && tsc --build",
@@ -10,9 +10,9 @@
10
10
  "test:ci": "NODE_V8_COVERAGE=./coverage glob -c \"tsx --test --test-reporter spec --experimental-test-coverage \" \"**/*.test.ts\""
11
11
  },
12
12
  "dependencies": {
13
- "@apollo-annotation/common": "^0.2.0",
14
- "@apollo-annotation/mst": "^0.2.0",
15
- "@apollo-annotation/schemas": "^0.2.0",
13
+ "@apollo-annotation/common": "^0.2.2",
14
+ "@apollo-annotation/mst": "^0.2.2",
15
+ "@apollo-annotation/schemas": "^0.2.2",
16
16
  "@gmod/gff": "1.2.0",
17
17
  "@gmod/indexedfasta": "^2.0.4",
18
18
  "@jbrowse/core": "^2.13.1",
@@ -30,7 +30,7 @@
30
30
  "chai-exclude": "^3.0.0",
31
31
  "glob": "^11.0.0",
32
32
  "mobx": "^6.6.1",
33
- "mobx-state-tree": "^5.1.7",
33
+ "mobx-state-tree": "^5.4.0",
34
34
  "mongoose": "^6.12.0",
35
35
  "react-dom": "^18.2.0",
36
36
  "rimraf": "^3.0.2",
@@ -43,7 +43,7 @@
43
43
  "@mui/x-data-grid": "^7.0.0",
44
44
  "mobx": "^6.6.1",
45
45
  "mobx-react": "^7.2.1",
46
- "mobx-state-tree": "^5.1.7",
46
+ "mobx-state-tree": "^5.4.0",
47
47
  "prop-types": "^15.8.1",
48
48
  "react": "^18.2.0",
49
49
  "react-dom": "^18.2.0",
@@ -124,7 +124,7 @@ async function checkMRNA(
124
124
  refSeq: refSeq.toString(),
125
125
  start: max,
126
126
  end: max,
127
- message: `Feature "${_id}" is missing a stop codon`,
127
+ message: `Missing stop codon`,
128
128
  })
129
129
  }
130
130
  } else {
@@ -0,0 +1,170 @@
1
+ /* eslint-disable prefer-destructuring */
2
+ /* eslint-disable @typescript-eslint/no-floating-promises */
3
+ import { describe, it } from 'node:test'
4
+ import { assert } from 'chai'
5
+ import { readAnnotationFeatureSnapshot } from './gff3ToAnnotationFeature.test'
6
+ import { annotationFeatureToGFF3 } from './annotationFeatureToGFF3'
7
+ import { AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
8
+
9
+ describe('annotationFeatureToGFF3', () => {
10
+ it('Test mandatory columns', () => {
11
+ const annotationFeature = readAnnotationFeatureSnapshot(
12
+ 'test_data/gene.json',
13
+ )
14
+ const [gff3Feature] = annotationFeatureToGFF3(annotationFeature)
15
+
16
+ assert.deepEqual(gff3Feature.seq_id, 'chr1')
17
+ assert.deepEqual(gff3Feature.type, 'gene')
18
+ assert.deepEqual(gff3Feature.start, 1000)
19
+ assert.deepEqual(gff3Feature.end, 9000)
20
+ assert.deepEqual(gff3Feature.strand, '+')
21
+ assert.deepEqual(gff3Feature.score, 123)
22
+ assert.deepEqual(gff3Feature.source, 'test_data')
23
+ })
24
+ it('Feature with no children and no gff_id has no ID attribute', () => {
25
+ const annotationFeature = JSON.parse(`{
26
+ "_id": "66d70e4ccc30b55b65e5f619",
27
+ "refSeq": "chr1",
28
+ "type": "gene",
29
+ "min": 999,
30
+ "max": 9000,
31
+ "strand": 1,
32
+ "attributes": {}
33
+ }`) as AnnotationFeatureSnapshot
34
+ const [gff3Feature] = annotationFeatureToGFF3(annotationFeature)
35
+ assert.isUndefined(gff3Feature.attributes?.ID)
36
+ })
37
+ it('Feature with children and no gff_id has internal _id as ID', () => {
38
+ const annotationFeature = JSON.parse(`{
39
+ "_id": "66d70e4ccc30b55b65e5f619",
40
+ "refSeq": "chr1",
41
+ "type": "gene",
42
+ "min": 999,
43
+ "max": 9000,
44
+ "strand": 1,
45
+ "attributes": {},
46
+ "children": {
47
+ "66d70e4ccc30b55b65e5f618": {
48
+ "_id": "66d70e4ccc30b55b65e5f618",
49
+ "refSeq": "chr1",
50
+ "type": "gene_segment",
51
+ "min": 1049,
52
+ "max": 9000,
53
+ "strand": 1,
54
+ "attributes": {}
55
+ }
56
+ }
57
+ }`) as AnnotationFeatureSnapshot
58
+ const [gff3Feature] = annotationFeatureToGFF3(annotationFeature)
59
+ assert.deepEqual(gff3Feature.attributes?.ID, ['66d70e4ccc30b55b65e5f619'])
60
+ })
61
+ it('Convert multiple scores', () => {
62
+ const annotationFeature = JSON.parse(`{
63
+ "_id": "66d70e4ccc30b55b65e5f619",
64
+ "refSeq": "chr1",
65
+ "type": "gene",
66
+ "min": 999,
67
+ "max": 9000,
68
+ "strand": 1,
69
+ "attributes": {
70
+ "gff_id": ["gene10001"],
71
+ "gff_score": ["123", "345"]
72
+ }
73
+ }`) as AnnotationFeatureSnapshot
74
+ const [gff3Feature] = annotationFeatureToGFF3(annotationFeature)
75
+ assert.deepEqual(gff3Feature.score, 123)
76
+ })
77
+ it('Convert invalid score', () => {
78
+ const annotationFeature = JSON.parse(`{
79
+ "_id": "66d70e4ccc30b55b65e5f619",
80
+ "refSeq": "chr1",
81
+ "type": "gene",
82
+ "min": 999,
83
+ "max": 9000,
84
+ "strand": 1,
85
+ "attributes": {
86
+ "gff_id": ["gene10001"],
87
+ "gff_score": ["xyz"]
88
+ }
89
+ }`) as AnnotationFeatureSnapshot
90
+ const [gff3Feature] = annotationFeatureToGFF3(annotationFeature)
91
+ assert.deepEqual(gff3Feature.score, null)
92
+ })
93
+ it('Convert one gene test attributes', () => {
94
+ const annotationFeature = readAnnotationFeatureSnapshot(
95
+ 'test_data/gene.json',
96
+ )
97
+ const [gff3Feature] = annotationFeatureToGFF3(annotationFeature)
98
+
99
+ assert.deepEqual(gff3Feature.attributes?.Name, ['EDEN'])
100
+ assert.deepEqual(gff3Feature.attributes?.testid, ['t001', 't003'])
101
+ assert.deepEqual(gff3Feature.attributes?.ID, ['gene10001'])
102
+ assert.deepEqual(gff3Feature.attributes?.Ontology_term, [
103
+ 'GO1234',
104
+ 'GO4567',
105
+ 'SO1234',
106
+ ])
107
+ assert.deepEqual(gff3Feature.attributes?.Alias, ['myalias'])
108
+ assert.deepEqual(gff3Feature.attributes?.Target, ['mytarget'])
109
+ assert.deepEqual(gff3Feature.attributes?.Gap, ['mygap'])
110
+ assert.deepEqual(gff3Feature.attributes?.Derives_from, ['myderives'])
111
+ assert.deepEqual(gff3Feature.attributes?.Note, ['mynote'])
112
+ assert.deepEqual(gff3Feature.attributes?.Dbxref, ['mydbxref'])
113
+ assert.deepEqual(gff3Feature.attributes?.Is_circular, ['true'])
114
+ })
115
+ it('Convert one gene test children', () => {
116
+ const annotationFeature = readAnnotationFeatureSnapshot(
117
+ 'test_data/gene.json',
118
+ )
119
+ const [gff3Feature] = annotationFeatureToGFF3(annotationFeature)
120
+ const [children] = gff3Feature.child_features
121
+ const [mrna] = children
122
+ assert.deepEqual(mrna.type, 'mRNA')
123
+ assert.deepEqual(mrna.attributes?.Parent, ['gene10001'])
124
+
125
+ const [cds] = mrna.child_features[2]
126
+ assert.deepEqual(cds.type, 'CDS')
127
+ assert.deepEqual(cds.attributes?.ID, ['cds10001'])
128
+ assert.deepEqual(cds.attributes?.Parent, ['mRNA10001'])
129
+ })
130
+ it('Convert CDSs', () => {
131
+ const annotationFeature = readAnnotationFeatureSnapshot(
132
+ 'test_data/gene.json',
133
+ )
134
+ const [gff3Feature] = annotationFeatureToGFF3(annotationFeature)
135
+ const [children] = gff3Feature.child_features
136
+ const [mrna] = children
137
+ const cds10001 = mrna.child_features.filter((child) => {
138
+ const id = child[0].attributes?.ID
139
+ return id !== undefined && id[0] === 'cds10001'
140
+ })
141
+ assert.deepEqual(cds10001.length, 2)
142
+
143
+ const cds1_1 = cds10001[0][0]
144
+ assert.deepEqual(cds1_1.attributes?.ID, ['cds10001'])
145
+ assert.deepEqual(cds1_1.start, 1201)
146
+ assert.deepEqual(cds1_1.end, 1500)
147
+ assert.deepEqual(cds1_1.phase, '0')
148
+
149
+ const cds1_2 = cds10001[1][0]
150
+ assert.deepEqual(cds1_2.attributes?.ID, ['cds10001'])
151
+ assert.deepEqual(cds1_2.start, 5000)
152
+ assert.deepEqual(cds1_2.end, 5100)
153
+ assert.deepEqual(cds1_2.phase, '0')
154
+
155
+ assert.deepEqual(cds1_1.child_features[0][0].attributes?.ID, [
156
+ 'cds_region10001',
157
+ ])
158
+ assert.deepEqual(cds1_1.child_features[0][0].start, 1351)
159
+ assert.deepEqual(cds1_1.child_features[0][0].end, 1400)
160
+ assert.deepEqual(cds1_1.child_features[0][0].phase, null)
161
+
162
+ const cds10004 = mrna.child_features.filter((child) => {
163
+ const id = child[0].attributes?.ID
164
+ return id !== undefined && id[0] === 'cds10004'
165
+ })
166
+ assert.deepEqual(cds10004.length, 2)
167
+ const cds4_1 = cds10004[0][0]
168
+ assert.deepEqual(cds4_1.attributes?.ID, ['cds10004'])
169
+ })
170
+ })
@@ -0,0 +1,257 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+
3
+ import {
4
+ AnnotationFeatureSnapshot,
5
+ TranscriptPartLocation,
6
+ TranscriptPartNonCoding,
7
+ } from '@apollo-annotation/mst'
8
+ import { GFF3Feature } from '@gmod/gff'
9
+ import { intersection2 } from '@jbrowse/core/util'
10
+
11
+ export function annotationFeatureToGFF3(
12
+ feature: AnnotationFeatureSnapshot,
13
+ parentId?: string,
14
+ refSeqNames?: Record<string, string | undefined>,
15
+ ): GFF3Feature {
16
+ const attributes: Record<string, string[] | undefined> = JSON.parse(
17
+ JSON.stringify(feature.attributes),
18
+ )
19
+ const ontologyTerms: string[] = []
20
+ const source = feature.attributes?.gff_source?.[0] ?? null
21
+
22
+ delete attributes.gff_source
23
+ if (parentId) {
24
+ attributes.Parent = [parentId]
25
+ }
26
+ if (attributes.gff_id) {
27
+ attributes.ID = attributes.gff_id
28
+ delete attributes.gff_id
29
+ } else if (feature.children) {
30
+ attributes.ID = [feature._id]
31
+ }
32
+ if (attributes.gff_name) {
33
+ attributes.Name = attributes.gff_name
34
+ delete attributes.gff_name
35
+ }
36
+ if (attributes.gff_alias) {
37
+ attributes.Alias = attributes.gff_alias
38
+ delete attributes.gff_alias
39
+ }
40
+ if (attributes.gff_target) {
41
+ attributes.Target = attributes.gff_target
42
+ delete attributes.gff_target
43
+ }
44
+ if (attributes.gff_gap) {
45
+ attributes.Gap = attributes.gff_gap
46
+ delete attributes.gff_gap
47
+ }
48
+ if (attributes.gff_derives_from) {
49
+ attributes.Derives_from = attributes.gff_derives_from
50
+ delete attributes.gff_derives_from
51
+ }
52
+ if (attributes.gff_note) {
53
+ attributes.Note = attributes.gff_note
54
+ delete attributes.gff_note
55
+ }
56
+ if (attributes.gff_dbxref) {
57
+ attributes.Dbxref = attributes.gff_dbxref
58
+ delete attributes.gff_dbxref
59
+ }
60
+ if (attributes.gff_is_circular) {
61
+ attributes.Is_circular = attributes.gff_is_circular
62
+ delete attributes.gff_is_circular
63
+ }
64
+ if (attributes.gff_ontology_term) {
65
+ ontologyTerms.push(...attributes.gff_ontology_term)
66
+ delete attributes.gff_ontology_term
67
+ }
68
+ if (attributes['Gene Ontology']) {
69
+ ontologyTerms.push(...attributes['Gene Ontology'])
70
+ delete attributes['Gene Ontology']
71
+ }
72
+ if (attributes['Sequence Ontology']) {
73
+ ontologyTerms.push(...attributes['Sequence Ontology'])
74
+ delete attributes['Sequence Ontology']
75
+ }
76
+ if (ontologyTerms.length > 0) {
77
+ attributes.Ontology_term = ontologyTerms
78
+ }
79
+
80
+ const gff_score = feature.attributes?.gff_score
81
+ let score: number | null = null
82
+ if (gff_score && gff_score.length > 0) {
83
+ if (gff_score[0]) {
84
+ score = Number(gff_score[0])
85
+ if (Number.isNaN(score)) {
86
+ score = null
87
+ }
88
+ }
89
+ delete attributes.gff_score
90
+ }
91
+
92
+ const locations = [{ start: feature.min, end: feature.max }]
93
+
94
+ return locations.map((location) => ({
95
+ start: Number(location.start) + 1,
96
+ end: Number(location.end),
97
+ seq_id: refSeqNames ? refSeqNames[feature.refSeq] ?? null : feature.refSeq,
98
+ source,
99
+ type: feature.type,
100
+ score,
101
+ strand: feature.strand ? (feature.strand === 1 ? '+' : '-') : null,
102
+ phase: null,
103
+ attributes: Object.keys(attributes).length > 0 ? attributes : null,
104
+ derived_features: [],
105
+ child_features: prepareChildFeatures(
106
+ feature,
107
+ attributes.ID?.[0],
108
+ refSeqNames,
109
+ ),
110
+ }))
111
+ }
112
+
113
+ function prepareChildFeatures(
114
+ feature: AnnotationFeatureSnapshot,
115
+ parentID?: string,
116
+ refSeqNames?: Record<string, string | undefined>,
117
+ ): GFF3Feature[] {
118
+ if (!feature.children) {
119
+ return []
120
+ }
121
+ if (feature.type === 'mRNA') {
122
+ const child_features: GFF3Feature[] = []
123
+ const cdsLocations = getCdsLocations(feature)
124
+ let cds_idx = 0
125
+ for (const child of Object.values(feature.children)) {
126
+ const gffChild = annotationFeatureToGFF3(child, parentID, refSeqNames)
127
+ if (child.type === 'CDS') {
128
+ for (const loc of cdsLocations[cds_idx]) {
129
+ const gffCds = JSON.parse(JSON.stringify(gffChild)) as GFF3Feature
130
+ if (gffCds.length != 1) {
131
+ // Do we need this check?
132
+ throw new Error(
133
+ `Unexpected CDS: ${JSON.stringify(gffCds, null, 2)}`,
134
+ )
135
+ }
136
+ gffCds[0].start = loc.min + 1
137
+ gffCds[0].end = loc.max
138
+ gffCds[0].phase = loc.phase.toString()
139
+ gffCds[0].type = loc.type // Do we need this?
140
+ child_features.push(gffCds)
141
+ }
142
+ cds_idx++
143
+ } else {
144
+ child_features.push(gffChild)
145
+ }
146
+ }
147
+ return child_features
148
+ }
149
+ return Object.values(feature.children).map((child) =>
150
+ annotationFeatureToGFF3(child, parentID, refSeqNames),
151
+ )
152
+ }
153
+
154
+ interface TranscriptPartCoding extends TranscriptPartLocation {
155
+ type: 'CDS'
156
+ phase: 0 | 1 | 2
157
+ }
158
+ type TranscriptPart = TranscriptPartCoding | TranscriptPartNonCoding
159
+ type TranscriptParts = TranscriptPart[]
160
+
161
+ function getTranscriptParts(
162
+ feature: AnnotationFeatureSnapshot,
163
+ ): TranscriptParts[] {
164
+ if (feature.type !== 'mRNA') {
165
+ throw new Error(
166
+ 'Only features of type "mRNA" or equivalent can calculate CDS locations',
167
+ )
168
+ }
169
+ if (!feature.children) {
170
+ throw new Error('no CDS or exons in mRNA')
171
+ }
172
+ // In AnnotationFeatureModel we have `children.values()`
173
+ const children = Object.values(feature.children)
174
+ const cdsChildren = children.filter((child) => child.type === 'CDS')
175
+ if (cdsChildren.length === 0) {
176
+ throw new Error('no CDS in mRNA')
177
+ }
178
+ const transcriptParts: TranscriptParts[] = []
179
+ for (const cds of cdsChildren) {
180
+ const { max: cdsMax, min: cdsMin } = cds
181
+ const parts: TranscriptParts = []
182
+ let hasIntersected = false
183
+ const exonLocations: TranscriptPartLocation[] = []
184
+ for (const child of children) {
185
+ if (child.type === 'exon') {
186
+ exonLocations.push({ min: child.min, max: child.max })
187
+ }
188
+ }
189
+ exonLocations.sort(({ min: a }, { min: b }) => a - b)
190
+ for (const child of exonLocations) {
191
+ const lastPart = parts.at(-1)
192
+ if (lastPart) {
193
+ parts.push({ min: lastPart.max, max: child.min, type: 'intron' })
194
+ }
195
+ const [start, end] = intersection2(cdsMin, cdsMax, child.min, child.max)
196
+ let utrType: 'fivePrimeUTR' | 'threePrimeUTR'
197
+ if (hasIntersected) {
198
+ utrType = feature.strand === 1 ? 'threePrimeUTR' : 'fivePrimeUTR'
199
+ } else {
200
+ utrType = feature.strand === 1 ? 'fivePrimeUTR' : 'threePrimeUTR'
201
+ }
202
+ if (start !== undefined && end !== undefined) {
203
+ hasIntersected = true
204
+ if (start === child.min && end === child.max) {
205
+ parts.push({ min: start, max: end, phase: 0, type: 'CDS' })
206
+ } else if (start === child.min) {
207
+ parts.push(
208
+ { min: start, max: end, phase: 0, type: 'CDS' },
209
+ { min: end, max: child.max, type: utrType },
210
+ )
211
+ } else if (end === child.max) {
212
+ parts.push(
213
+ { min: child.min, max: start, type: utrType },
214
+ { min: start, max: end, phase: 0, type: 'CDS' },
215
+ )
216
+ } else {
217
+ parts.push(
218
+ { min: child.min, max: start, type: utrType },
219
+ { min: start, max: end, phase: 0, type: 'CDS' },
220
+ {
221
+ min: end,
222
+ max: child.max,
223
+ type:
224
+ utrType === 'fivePrimeUTR' ? 'threePrimeUTR' : 'fivePrimeUTR',
225
+ },
226
+ )
227
+ }
228
+ } else {
229
+ parts.push({ min: child.min, max: child.max, type: utrType })
230
+ }
231
+ }
232
+ parts.sort(({ min: a }, { min: b }) => a - b)
233
+ if (feature.strand === -1) {
234
+ parts.reverse()
235
+ }
236
+ let nextPhase: 0 | 1 | 2 = 0
237
+ const phasedParts = parts.map((loc) => {
238
+ if (loc.type !== 'CDS') {
239
+ return loc
240
+ }
241
+ const phase = nextPhase
242
+ nextPhase = ((3 - ((loc.max - loc.min - phase + 3) % 3)) % 3) as 0 | 1 | 2
243
+ return { ...loc, phase }
244
+ })
245
+ transcriptParts.push(phasedParts)
246
+ }
247
+ return transcriptParts
248
+ }
249
+
250
+ function getCdsLocations(
251
+ feature: AnnotationFeatureSnapshot,
252
+ ): TranscriptPartCoding[][] {
253
+ const transcriptParts = getTranscriptParts(feature)
254
+ return transcriptParts.map((transcript) =>
255
+ transcript.filter((transcriptPart) => transcriptPart.type === 'CDS'),
256
+ )
257
+ }
@@ -120,7 +120,9 @@ function readFeatureFile(fn: string): GFF3Feature[] {
120
120
  return inGff
121
121
  }
122
122
 
123
- function readAnnotationFeatureSnapshot(fn: string): AnnotationFeatureSnapshot {
123
+ export function readAnnotationFeatureSnapshot(
124
+ fn: string,
125
+ ): AnnotationFeatureSnapshot {
124
126
  const lines = readFileSync(fn).toString()
125
127
  return JSON.parse(lines) as AnnotationFeatureSnapshot
126
128
  }
package/src/GFF3/index.ts CHANGED
@@ -1,2 +1,3 @@
1
+ export * from './annotationFeatureToGFF3'
1
2
  export * from './gffReservedKeys'
2
3
  export * from './gff3ToAnnotationFeature'