@gmod/tabix 1.5.0 → 1.5.3
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/CHANGELOG.md +20 -0
- package/README.md +3 -2
- package/dist/chunk.js +34 -62
- package/dist/chunk.js.map +1 -0
- package/dist/csi.d.ts +0 -1
- package/dist/csi.js +329 -533
- package/dist/csi.js.map +1 -0
- package/dist/index.js +12 -36
- package/dist/index.js.map +1 -0
- package/dist/indexFile.d.ts +1 -1
- package/dist/indexFile.js +113 -174
- package/dist/indexFile.js.map +1 -0
- package/dist/tabixIndexedFile.d.ts +8 -4
- package/dist/tabixIndexedFile.js +477 -774
- package/dist/tabixIndexedFile.js.map +1 -0
- package/dist/tbi.js +313 -485
- package/dist/tbi.js.map +1 -0
- package/dist/util.js +133 -148
- package/dist/util.js.map +1 -0
- package/dist/virtualOffset.js +45 -68
- package/dist/virtualOffset.js.map +1 -0
- package/esm/chunk.d.ts +18 -0
- package/esm/chunk.js +33 -0
- package/esm/chunk.js.map +1 -0
- package/esm/csi.d.ts +71 -0
- package/esm/csi.js +230 -0
- package/esm/csi.js.map +1 -0
- package/esm/index.d.ts +4 -0
- package/esm/index.js +5 -0
- package/esm/index.js.map +1 -0
- package/esm/indexFile.d.ts +32 -0
- package/esm/indexFile.js +40 -0
- package/esm/indexFile.js.map +1 -0
- package/esm/tabixIndexedFile.d.ts +118 -0
- package/esm/tabixIndexedFile.js +398 -0
- package/esm/tabixIndexedFile.js.map +1 -0
- package/esm/tbi.d.ts +45 -0
- package/esm/tbi.js +216 -0
- package/esm/tbi.js.map +1 -0
- package/esm/util.d.ts +25 -0
- package/esm/util.js +91 -0
- package/esm/util.js.map +1 -0
- package/esm/virtualOffset.d.ts +10 -0
- package/esm/virtualOffset.js +37 -0
- package/esm/virtualOffset.js.map +1 -0
- package/package.json +27 -44
- package/src/chunk.ts +52 -0
- package/src/csi.ts +277 -0
- package/src/declare.d.ts +2 -0
- package/src/index.ts +5 -0
- package/src/indexFile.ts +79 -0
- package/src/tabixIndexedFile.ts +519 -0
- package/src/tbi.ts +250 -0
- package/src/util.ts +103 -0
- package/src/virtualOffset.ts +47 -0
- package/dist/declare.d.js +0 -2
package/src/indexFile.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import AbortablePromiseCache from 'abortable-promise-cache'
|
|
2
|
+
import QuickLRU from 'quick-lru'
|
|
3
|
+
import { GenericFilehandle } from 'generic-filehandle'
|
|
4
|
+
import VirtualOffset from './virtualOffset'
|
|
5
|
+
import Chunk from './chunk'
|
|
6
|
+
|
|
7
|
+
export interface Options {
|
|
8
|
+
// support having some unknown parts of the options
|
|
9
|
+
[key: string]: unknown
|
|
10
|
+
signal?: AbortSignal
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default abstract class IndexFile {
|
|
14
|
+
public filehandle: GenericFilehandle
|
|
15
|
+
public renameRefSeq: (arg0: string) => string
|
|
16
|
+
private _parseCache: any
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {filehandle} filehandle
|
|
20
|
+
* @param {function} [renameRefSeqs]
|
|
21
|
+
*/
|
|
22
|
+
constructor({
|
|
23
|
+
filehandle,
|
|
24
|
+
renameRefSeqs = (n: string) => n,
|
|
25
|
+
}: {
|
|
26
|
+
filehandle: GenericFilehandle
|
|
27
|
+
renameRefSeqs?: (a: string) => string
|
|
28
|
+
}) {
|
|
29
|
+
this.filehandle = filehandle
|
|
30
|
+
this.renameRefSeq = renameRefSeqs
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public abstract lineCount(refName: string, args: Options): Promise<number>
|
|
34
|
+
|
|
35
|
+
protected abstract _parse(opts: Options): Promise<{
|
|
36
|
+
refNameToId: { [key: string]: number }
|
|
37
|
+
refIdToName: string[]
|
|
38
|
+
}>
|
|
39
|
+
|
|
40
|
+
public async getMetadata(opts: Options = {}) {
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
42
|
+
const { indices, ...rest } = await this.parse(opts)
|
|
43
|
+
return rest
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public abstract blocksForRange(
|
|
47
|
+
refName: string,
|
|
48
|
+
start: number,
|
|
49
|
+
end: number,
|
|
50
|
+
opts: Options,
|
|
51
|
+
): Promise<Chunk[]>
|
|
52
|
+
|
|
53
|
+
_findFirstData(
|
|
54
|
+
currentFdl: VirtualOffset | undefined,
|
|
55
|
+
virtualOffset: VirtualOffset,
|
|
56
|
+
) {
|
|
57
|
+
if (currentFdl) {
|
|
58
|
+
return currentFdl.compareTo(virtualOffset) > 0
|
|
59
|
+
? virtualOffset
|
|
60
|
+
: currentFdl
|
|
61
|
+
} else {
|
|
62
|
+
return virtualOffset
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async parse(opts: Options = {}) {
|
|
67
|
+
if (!this._parseCache) {
|
|
68
|
+
this._parseCache = new AbortablePromiseCache({
|
|
69
|
+
cache: new QuickLRU({ maxSize: 1 }),
|
|
70
|
+
fill: () => this._parse(opts),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
return this._parseCache.get('index', null, undefined)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async hasRefSeq(seqId: number, opts: Options = {}) {
|
|
77
|
+
return !!((await this.parse(opts)).indices[seqId] || {}).binIndex
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import AbortablePromiseCache from 'abortable-promise-cache'
|
|
2
|
+
import LRU from 'quick-lru'
|
|
3
|
+
import { GenericFilehandle, LocalFile } from 'generic-filehandle'
|
|
4
|
+
import { unzip, unzipChunkSlice } from '@gmod/bgzf-filehandle'
|
|
5
|
+
import { checkAbortSignal } from './util'
|
|
6
|
+
import IndexFile, { Options } from './indexFile'
|
|
7
|
+
|
|
8
|
+
import Chunk from './chunk'
|
|
9
|
+
import TBI from './tbi'
|
|
10
|
+
import CSI from './csi'
|
|
11
|
+
|
|
12
|
+
type GetLinesCallback = (line: string, fileOffset: number) => void
|
|
13
|
+
|
|
14
|
+
interface GetLinesOpts {
|
|
15
|
+
[key: string]: unknown
|
|
16
|
+
signal?: AbortSignal
|
|
17
|
+
lineCallback: GetLinesCallback
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function timeout(time: number) {
|
|
21
|
+
return new Promise(resolve => {
|
|
22
|
+
setTimeout(resolve, time)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
export default class TabixIndexedFile {
|
|
26
|
+
private filehandle: GenericFilehandle
|
|
27
|
+
private index: IndexFile
|
|
28
|
+
private chunkSizeLimit: number
|
|
29
|
+
private renameRefSeq: (n: string) => string
|
|
30
|
+
private chunkCache: any
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} args
|
|
33
|
+
* @param {string} [args.path]
|
|
34
|
+
* @param {filehandle} [args.filehandle]
|
|
35
|
+
* @param {string} [args.tbiPath]
|
|
36
|
+
* @param {filehandle} [args.tbiFilehandle]
|
|
37
|
+
* @param {string} [args.csiPath]
|
|
38
|
+
* @param {filehandle} [args.csiFilehandle]
|
|
39
|
+
* @param {chunkSizeLimit} default 50MiB
|
|
40
|
+
* @param {function} [args.renameRefSeqs] optional function with sig `string => string` to transform
|
|
41
|
+
* reference sequence names for the purpose of indexing and querying. note that the data that is returned is
|
|
42
|
+
* not altered, just the names of the reference sequences that are used for querying.
|
|
43
|
+
* @param {number} [args.chunkCacheSize] maximum size in bytes of the chunk cache. default 5MB
|
|
44
|
+
* @param {number} [args.blockCacheSize] maximum size in bytes of the block cache. default 5MB
|
|
45
|
+
*/
|
|
46
|
+
constructor({
|
|
47
|
+
path,
|
|
48
|
+
filehandle,
|
|
49
|
+
tbiPath,
|
|
50
|
+
tbiFilehandle,
|
|
51
|
+
csiPath,
|
|
52
|
+
csiFilehandle,
|
|
53
|
+
chunkSizeLimit = 50000000,
|
|
54
|
+
renameRefSeqs = n => n,
|
|
55
|
+
chunkCacheSize = 5 * 2 ** 20,
|
|
56
|
+
}: {
|
|
57
|
+
path?: string
|
|
58
|
+
filehandle?: GenericFilehandle
|
|
59
|
+
tbiPath?: string
|
|
60
|
+
tbiFilehandle?: GenericFilehandle
|
|
61
|
+
csiPath?: string
|
|
62
|
+
csiFilehandle?: GenericFilehandle
|
|
63
|
+
chunkSizeLimit?: number
|
|
64
|
+
renameRefSeqs?: (n: string) => string
|
|
65
|
+
chunkCacheSize?: number
|
|
66
|
+
}) {
|
|
67
|
+
if (filehandle) {
|
|
68
|
+
this.filehandle = filehandle
|
|
69
|
+
} else if (path) {
|
|
70
|
+
this.filehandle = new LocalFile(path)
|
|
71
|
+
} else {
|
|
72
|
+
throw new TypeError('must provide either filehandle or path')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (tbiFilehandle) {
|
|
76
|
+
this.index = new TBI({
|
|
77
|
+
filehandle: tbiFilehandle,
|
|
78
|
+
renameRefSeqs,
|
|
79
|
+
})
|
|
80
|
+
} else if (csiFilehandle) {
|
|
81
|
+
this.index = new CSI({
|
|
82
|
+
filehandle: csiFilehandle,
|
|
83
|
+
renameRefSeqs,
|
|
84
|
+
})
|
|
85
|
+
} else if (tbiPath) {
|
|
86
|
+
this.index = new TBI({
|
|
87
|
+
filehandle: new LocalFile(tbiPath),
|
|
88
|
+
renameRefSeqs,
|
|
89
|
+
})
|
|
90
|
+
} else if (csiPath) {
|
|
91
|
+
this.index = new CSI({
|
|
92
|
+
filehandle: new LocalFile(csiPath),
|
|
93
|
+
renameRefSeqs,
|
|
94
|
+
})
|
|
95
|
+
} else if (path) {
|
|
96
|
+
this.index = new TBI({
|
|
97
|
+
filehandle: new LocalFile(`${path}.tbi`),
|
|
98
|
+
renameRefSeqs,
|
|
99
|
+
})
|
|
100
|
+
} else {
|
|
101
|
+
throw new TypeError(
|
|
102
|
+
'must provide one of tbiFilehandle, tbiPath, csiFilehandle, or csiPath',
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.chunkSizeLimit = chunkSizeLimit
|
|
107
|
+
this.renameRefSeq = renameRefSeqs
|
|
108
|
+
this.chunkCache = new AbortablePromiseCache({
|
|
109
|
+
cache: new LRU({
|
|
110
|
+
maxSize: Math.floor(chunkCacheSize / (1 << 16)),
|
|
111
|
+
}),
|
|
112
|
+
|
|
113
|
+
fill: this.readChunk.bind(this),
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {string} refName name of the reference sequence
|
|
119
|
+
* @param {number} start start of the region (in 0-based half-open coordinates)
|
|
120
|
+
* @param {number} end end of the region (in 0-based half-open coordinates)
|
|
121
|
+
* @param {function|object} lineCallback callback called for each line in the region. can also pass a object param containing obj.lineCallback, obj.signal, etc
|
|
122
|
+
* @returns {Promise} resolved when the whole read is finished, rejected on error
|
|
123
|
+
*/
|
|
124
|
+
async getLines(
|
|
125
|
+
refName: string,
|
|
126
|
+
start: number,
|
|
127
|
+
end: number,
|
|
128
|
+
opts: GetLinesOpts | GetLinesCallback,
|
|
129
|
+
) {
|
|
130
|
+
let signal: AbortSignal | undefined
|
|
131
|
+
let options: Options = {}
|
|
132
|
+
let callback: (line: string, lineOffset: number) => void
|
|
133
|
+
if (typeof opts === 'undefined') {
|
|
134
|
+
throw new TypeError('line callback must be provided')
|
|
135
|
+
}
|
|
136
|
+
if (typeof opts === 'function') {
|
|
137
|
+
callback = opts
|
|
138
|
+
} else {
|
|
139
|
+
options = opts
|
|
140
|
+
callback = opts.lineCallback
|
|
141
|
+
}
|
|
142
|
+
if (refName === undefined) {
|
|
143
|
+
throw new TypeError('must provide a reference sequence name')
|
|
144
|
+
}
|
|
145
|
+
if (!callback) {
|
|
146
|
+
throw new TypeError('line callback must be provided')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const metadata = await this.index.getMetadata(options)
|
|
150
|
+
checkAbortSignal(signal)
|
|
151
|
+
if (!start) {
|
|
152
|
+
start = 0
|
|
153
|
+
}
|
|
154
|
+
if (!end) {
|
|
155
|
+
end = metadata.maxRefLength
|
|
156
|
+
}
|
|
157
|
+
if (!(start <= end)) {
|
|
158
|
+
throw new TypeError(
|
|
159
|
+
'invalid start and end coordinates. start must be less than or equal to end',
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
if (start === end) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const chunks = await this.index.blocksForRange(refName, start, end, options)
|
|
167
|
+
checkAbortSignal(signal)
|
|
168
|
+
|
|
169
|
+
// check the chunks for any that are over the size limit. if
|
|
170
|
+
// any are, don't fetch any of them
|
|
171
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
172
|
+
const size = chunks[i].fetchedSize()
|
|
173
|
+
if (size > this.chunkSizeLimit) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Too much data. Chunk size ${size.toLocaleString()} bytes exceeds chunkSizeLimit of ${this.chunkSizeLimit.toLocaleString()}.`,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// now go through each chunk and parse and filter the lines out of it
|
|
181
|
+
let last = Date.now()
|
|
182
|
+
for (let chunkNum = 0; chunkNum < chunks.length; chunkNum += 1) {
|
|
183
|
+
let previousStartCoordinate: number | undefined
|
|
184
|
+
const c = chunks[chunkNum]
|
|
185
|
+
const { buffer, cpositions, dpositions } = await this.chunkCache.get(
|
|
186
|
+
c.toString(),
|
|
187
|
+
c,
|
|
188
|
+
signal,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const lines = (
|
|
192
|
+
typeof TextDecoder !== 'undefined'
|
|
193
|
+
? new TextDecoder('utf-8').decode(buffer)
|
|
194
|
+
: buffer.toString()
|
|
195
|
+
).split('\n')
|
|
196
|
+
lines.pop()
|
|
197
|
+
|
|
198
|
+
checkAbortSignal(signal)
|
|
199
|
+
let blockStart = c.minv.dataPosition
|
|
200
|
+
let pos
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
203
|
+
const line = lines[i]
|
|
204
|
+
|
|
205
|
+
for (pos = 0; blockStart >= dpositions[pos]; pos += 1) {}
|
|
206
|
+
|
|
207
|
+
// filter the line for whether it is within the requested range
|
|
208
|
+
const { startCoordinate, overlaps } = this.checkLine(
|
|
209
|
+
metadata,
|
|
210
|
+
refName,
|
|
211
|
+
start,
|
|
212
|
+
end,
|
|
213
|
+
line,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
// do a small check just to make sure that the lines are really sorted by start coordinate
|
|
217
|
+
if (
|
|
218
|
+
previousStartCoordinate !== undefined &&
|
|
219
|
+
startCoordinate !== undefined &&
|
|
220
|
+
previousStartCoordinate > startCoordinate
|
|
221
|
+
) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Lines not sorted by start coordinate (${previousStartCoordinate} > ${startCoordinate}), this file is not usable with Tabix.`,
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
previousStartCoordinate = startCoordinate
|
|
227
|
+
|
|
228
|
+
if (overlaps) {
|
|
229
|
+
callback(
|
|
230
|
+
line.trim(),
|
|
231
|
+
// cpositions[pos] refers to actual file offset of a bgzip block boundaries
|
|
232
|
+
//
|
|
233
|
+
// we multiply by (1 <<8) in order to make sure each block has a "unique"
|
|
234
|
+
// address space so that data in that block could never overlap
|
|
235
|
+
//
|
|
236
|
+
// then the blockStart-dpositions is an uncompressed file offset from
|
|
237
|
+
// that bgzip block boundary, and since the cpositions are multiplied by
|
|
238
|
+
// (1 << 8) these uncompressed offsets get a unique space
|
|
239
|
+
cpositions[pos] * (1 << 8) + (blockStart - dpositions[pos]),
|
|
240
|
+
)
|
|
241
|
+
} else if (startCoordinate !== undefined && startCoordinate >= end) {
|
|
242
|
+
// the lines were overlapping the region, but now have stopped, so
|
|
243
|
+
// we must be at the end of the relevant data and we can stop
|
|
244
|
+
// processing data now
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
blockStart += line.length + 1
|
|
248
|
+
|
|
249
|
+
// yield if we have emitted beyond the yield limit
|
|
250
|
+
if (last - Date.now() > 500) {
|
|
251
|
+
last = Date.now()
|
|
252
|
+
checkAbortSignal(signal)
|
|
253
|
+
await timeout(1)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async getMetadata(opts: Options = {}) {
|
|
260
|
+
return this.index.getMetadata(opts)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* get a buffer containing the "header" region of
|
|
265
|
+
* the file, which are the bytes up to the first
|
|
266
|
+
* non-meta line
|
|
267
|
+
*
|
|
268
|
+
* @returns {Promise} for a buffer
|
|
269
|
+
*/
|
|
270
|
+
async getHeaderBuffer(opts: Options = {}) {
|
|
271
|
+
const { firstDataLine, metaChar, maxBlockSize } = await this.getMetadata(
|
|
272
|
+
opts,
|
|
273
|
+
)
|
|
274
|
+
checkAbortSignal(opts.signal)
|
|
275
|
+
const maxFetch =
|
|
276
|
+
firstDataLine && firstDataLine.blockPosition
|
|
277
|
+
? firstDataLine.blockPosition + maxBlockSize
|
|
278
|
+
: maxBlockSize
|
|
279
|
+
// TODO: what if we don't have a firstDataLine, and the header
|
|
280
|
+
// actually takes up more than one block? this case is not covered here
|
|
281
|
+
|
|
282
|
+
let bytes = await this._readRegion(0, maxFetch, opts)
|
|
283
|
+
checkAbortSignal(opts.signal)
|
|
284
|
+
try {
|
|
285
|
+
bytes = await unzip(bytes)
|
|
286
|
+
} catch (e) {
|
|
287
|
+
console.error(e)
|
|
288
|
+
throw new Error(
|
|
289
|
+
//@ts-ignore
|
|
290
|
+
`error decompressing block ${e.code} at 0 (length ${maxFetch}) ${e}`,
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// trim off lines after the last non-meta line
|
|
295
|
+
if (metaChar) {
|
|
296
|
+
// trim backward from the end
|
|
297
|
+
let lastNewline = -1
|
|
298
|
+
const newlineByte = '\n'.charCodeAt(0)
|
|
299
|
+
const metaByte = metaChar.charCodeAt(0)
|
|
300
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
301
|
+
if (i === lastNewline + 1 && bytes[i] !== metaByte) {
|
|
302
|
+
break
|
|
303
|
+
}
|
|
304
|
+
if (bytes[i] === newlineByte) {
|
|
305
|
+
lastNewline = i
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
bytes = bytes.slice(0, lastNewline + 1)
|
|
309
|
+
}
|
|
310
|
+
return bytes
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* get a string containing the "header" region of the
|
|
315
|
+
* file, is the portion up to the first non-meta line
|
|
316
|
+
*
|
|
317
|
+
* @returns {Promise} for a string
|
|
318
|
+
*/
|
|
319
|
+
async getHeader(opts: Options = {}) {
|
|
320
|
+
const bytes = await this.getHeaderBuffer(opts)
|
|
321
|
+
checkAbortSignal(opts.signal)
|
|
322
|
+
return bytes.toString('utf8')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* get an array of reference sequence names, in the order in which
|
|
327
|
+
* they occur in the file.
|
|
328
|
+
*
|
|
329
|
+
* reference sequence renaming is not applied to these names.
|
|
330
|
+
*
|
|
331
|
+
* @returns {Promise} for an array of string sequence names
|
|
332
|
+
*/
|
|
333
|
+
async getReferenceSequenceNames(opts: Options = {}) {
|
|
334
|
+
const metadata = await this.getMetadata(opts)
|
|
335
|
+
return metadata.refIdToName
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* @param {object} metadata metadata object from the parsed index,
|
|
340
|
+
* containing columnNumbers, metaChar, and format
|
|
341
|
+
* @param {string} regionRefName
|
|
342
|
+
* @param {number} regionStart region start coordinate (0-based-half-open)
|
|
343
|
+
* @param {number} regionEnd region end coordinate (0-based-half-open)
|
|
344
|
+
* @param {array[string]} line
|
|
345
|
+
* @returns {object} like `{startCoordinate, overlaps}`. overlaps is boolean,
|
|
346
|
+
* true if line is a data line that overlaps the given region
|
|
347
|
+
*/
|
|
348
|
+
checkLine(
|
|
349
|
+
{
|
|
350
|
+
columnNumbers,
|
|
351
|
+
metaChar,
|
|
352
|
+
coordinateType,
|
|
353
|
+
format,
|
|
354
|
+
}: {
|
|
355
|
+
columnNumbers: { ref: number; start: number; end: number }
|
|
356
|
+
metaChar: string
|
|
357
|
+
coordinateType: string
|
|
358
|
+
format: string
|
|
359
|
+
},
|
|
360
|
+
regionRefName: string,
|
|
361
|
+
regionStart: number,
|
|
362
|
+
regionEnd: number,
|
|
363
|
+
line: string,
|
|
364
|
+
) {
|
|
365
|
+
// skip meta lines
|
|
366
|
+
if (line.charAt(0) === metaChar) {
|
|
367
|
+
return { overlaps: false }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// check ref/start/end using column metadata from index
|
|
371
|
+
let { ref, start, end } = columnNumbers
|
|
372
|
+
if (!ref) {
|
|
373
|
+
ref = 0
|
|
374
|
+
}
|
|
375
|
+
if (!start) {
|
|
376
|
+
start = 0
|
|
377
|
+
}
|
|
378
|
+
if (!end) {
|
|
379
|
+
end = 0
|
|
380
|
+
}
|
|
381
|
+
if (format === 'VCF') {
|
|
382
|
+
end = 8
|
|
383
|
+
}
|
|
384
|
+
const maxColumn = Math.max(ref, start, end)
|
|
385
|
+
|
|
386
|
+
// this code is kind of complex, but it is fairly fast.
|
|
387
|
+
// basically, we want to avoid doing a split, because if the lines are really long
|
|
388
|
+
// that could lead to us allocating a bunch of extra memory, which is slow
|
|
389
|
+
|
|
390
|
+
let currentColumnNumber = 1 // cols are numbered starting at 1 in the index metadata
|
|
391
|
+
let currentColumnStart = 0
|
|
392
|
+
let refSeq = ''
|
|
393
|
+
let startCoordinate = -Infinity
|
|
394
|
+
for (let i = 0; i < line.length + 1; i += 1) {
|
|
395
|
+
if (line[i] === '\t' || i === line.length) {
|
|
396
|
+
if (currentColumnNumber === ref) {
|
|
397
|
+
if (
|
|
398
|
+
this.renameRefSeq(line.slice(currentColumnStart, i)) !==
|
|
399
|
+
regionRefName
|
|
400
|
+
) {
|
|
401
|
+
return { overlaps: false }
|
|
402
|
+
}
|
|
403
|
+
} else if (currentColumnNumber === start) {
|
|
404
|
+
startCoordinate = parseInt(line.slice(currentColumnStart, i), 10)
|
|
405
|
+
// we convert to 0-based-half-open
|
|
406
|
+
if (coordinateType === '1-based-closed') {
|
|
407
|
+
startCoordinate -= 1
|
|
408
|
+
}
|
|
409
|
+
if (startCoordinate >= regionEnd) {
|
|
410
|
+
return { startCoordinate, overlaps: false }
|
|
411
|
+
}
|
|
412
|
+
if (end === 0 || end === start) {
|
|
413
|
+
// if we have no end, we assume the feature is 1 bp long
|
|
414
|
+
if (startCoordinate + 1 <= regionStart) {
|
|
415
|
+
return { startCoordinate, overlaps: false }
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} else if (format === 'VCF' && currentColumnNumber === 4) {
|
|
419
|
+
refSeq = line.slice(currentColumnStart, i)
|
|
420
|
+
} else if (currentColumnNumber === end) {
|
|
421
|
+
let endCoordinate
|
|
422
|
+
// this will never match if there is no end column
|
|
423
|
+
if (format === 'VCF') {
|
|
424
|
+
endCoordinate = this._getVcfEnd(
|
|
425
|
+
startCoordinate,
|
|
426
|
+
refSeq,
|
|
427
|
+
line.slice(currentColumnStart, i),
|
|
428
|
+
)
|
|
429
|
+
} else {
|
|
430
|
+
endCoordinate = parseInt(line.slice(currentColumnStart, i), 10)
|
|
431
|
+
}
|
|
432
|
+
if (endCoordinate <= regionStart) {
|
|
433
|
+
return { overlaps: false }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
currentColumnStart = i + 1
|
|
437
|
+
currentColumnNumber += 1
|
|
438
|
+
if (currentColumnNumber > maxColumn) {
|
|
439
|
+
break
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return { startCoordinate, overlaps: true }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
_getVcfEnd(startCoordinate: number, refSeq: string, info: any) {
|
|
447
|
+
let endCoordinate = startCoordinate + refSeq.length
|
|
448
|
+
// ignore TRA features as they specify CHR2 and END
|
|
449
|
+
// as being on a different chromosome
|
|
450
|
+
// if CHR2 is on the same chromosome, still ignore it
|
|
451
|
+
// because there should be another pairwise feature
|
|
452
|
+
// at the end of this one
|
|
453
|
+
const isTRA = info.indexOf('SVTYPE=TRA') !== -1
|
|
454
|
+
if (info[0] !== '.' && !isTRA) {
|
|
455
|
+
let prevChar = ';'
|
|
456
|
+
for (let j = 0; j < info.length; j += 1) {
|
|
457
|
+
if (prevChar === ';' && info.slice(j, j + 4) === 'END=') {
|
|
458
|
+
let valueEnd = info.indexOf(';', j)
|
|
459
|
+
if (valueEnd === -1) {
|
|
460
|
+
valueEnd = info.length
|
|
461
|
+
}
|
|
462
|
+
endCoordinate = parseInt(info.slice(j + 4, valueEnd), 10)
|
|
463
|
+
break
|
|
464
|
+
}
|
|
465
|
+
prevChar = info[j]
|
|
466
|
+
}
|
|
467
|
+
} else if (isTRA) {
|
|
468
|
+
return startCoordinate + 1
|
|
469
|
+
}
|
|
470
|
+
return endCoordinate
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* return the approximate number of data lines in the given reference sequence
|
|
475
|
+
* @param {string} refSeq reference sequence name
|
|
476
|
+
* @returns {Promise} for number of data lines present on that reference sequence
|
|
477
|
+
*/
|
|
478
|
+
async lineCount(refName: string, opts: Options = {}) {
|
|
479
|
+
return this.index.lineCount(refName, opts)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async _readRegion(
|
|
483
|
+
position: number,
|
|
484
|
+
compressedSize: number,
|
|
485
|
+
opts: Options = {},
|
|
486
|
+
) {
|
|
487
|
+
const { bytesRead, buffer } = await this.filehandle.read(
|
|
488
|
+
Buffer.alloc(compressedSize),
|
|
489
|
+
0,
|
|
490
|
+
compressedSize,
|
|
491
|
+
position,
|
|
492
|
+
opts,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
return bytesRead < compressedSize ? buffer.slice(0, bytesRead) : buffer
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* read and uncompress the data in a chunk (composed of one or more
|
|
500
|
+
* contiguous bgzip blocks) of the file
|
|
501
|
+
* @param {Chunk} chunk
|
|
502
|
+
* @returns {Promise} for a string chunk of the file
|
|
503
|
+
*/
|
|
504
|
+
async readChunk(chunk: Chunk, opts: Options = {}) {
|
|
505
|
+
// fetch the uncompressed data, uncompress carefully a block at a time,
|
|
506
|
+
// and stop when done
|
|
507
|
+
|
|
508
|
+
const compressedData = await this._readRegion(
|
|
509
|
+
chunk.minv.blockPosition,
|
|
510
|
+
chunk.fetchedSize(),
|
|
511
|
+
opts,
|
|
512
|
+
)
|
|
513
|
+
try {
|
|
514
|
+
return unzipChunkSlice(compressedData, chunk)
|
|
515
|
+
} catch (e) {
|
|
516
|
+
throw new Error(`error decompressing chunk ${chunk.toString()} ${e}`)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|