@gmod/nclist
Version:
Read features from JBrowse 1 format nested containment list JSON
152 lines (136 loc) • 4.09 kB
text/typescript
//@ts-nocheck
import QuickLRU from 'quick-lru'
import AbortablePromiseCache from '@gmod/abortable-promise-cache'
import { newURL, readJSON } from './util.ts'
export default class NCList {
constructor({ readFile, cacheSize = 100 }) {
this.topList = []
this.chunkCache = new AbortablePromiseCache({
cache: new QuickLRU({ maxSize: cacheSize }),
fill: this.readChunkItems.bind(this),
})
this.readFile = readFile
if (!this.readFile) {
throw new Error(`must provide a "readFile" function`)
}
}
importExisting(nclist, attrs, baseURL, lazyUrlTemplate, lazyClass) {
this.topList = nclist
this.attrs = attrs
this.start = attrs.makeFastGetter('Start')
this.end = attrs.makeFastGetter('End')
this.lazyClass = lazyClass
this.baseURL = baseURL
this.lazyUrlTemplate = lazyUrlTemplate
}
binarySearch(arr, item, getter) {
let low = -1
let high = arr.length
let mid
while (high - low > 1) {
mid = (low + high) >>> 1
if (getter(arr[mid]) >= item) {
high = mid
} else {
low = mid
}
}
// if we're iterating rightward, return the high index;
// if leftward, the low index
if (getter === this.end) {
return high
}
return low
}
readChunkItems(chunkNum) {
const url = newURL(
this.lazyUrlTemplate.replaceAll(/\{Chunk\}/gi, chunkNum),
this.baseURL,
)
return readJSON(url, this.readFile, { defaultContent: [] })
}
async *iterateSublist(arr, from, to, inc, searchGet, testGet, path) {
const getChunk = this.attrs.makeGetter('Chunk')
const getSublist = this.attrs.makeGetter('Sublist')
const pendingPromises = []
for (
let i = this.binarySearch(arr, from, searchGet);
i < arr.length && i >= 0 && inc * testGet(arr[i]) < inc * to;
i += inc
) {
if (arr[i][0] === this.lazyClass) {
// this is a lazily-loaded chunk of the nclist
const chunkNum = getChunk(arr[i])
const chunkItemsP = this.chunkCache
.get(chunkNum, chunkNum)
.then(item => [item, chunkNum])
pendingPromises.push(chunkItemsP)
} else {
// this is just a regular feature
yield [arr[i], path.concat(i)]
}
// if this node has a contained sublist, process that too
const sublist = getSublist(arr[i])
if (sublist) {
yield* this.iterateSublist(
sublist,
from,
to,
inc,
searchGet,
testGet,
path.concat(i),
)
}
}
for (const p of pendingPromises) {
const [item, chunkNum] = await p
if (item) {
yield* this.iterateSublist(item, from, to, inc, searchGet, testGet, [
...path,
chunkNum,
])
}
}
}
async *iterate(from, to) {
// calls the given function once for each of the
// intervals that overlap the given interval
// if from <= to, iterates left-to-right, otherwise iterates right-to-left
// inc: iterate leftward or rightward
const inc = from > to ? -1 : 1
// searchGet: search on start or end
const searchGet = from > to ? this.start : this.end
// testGet: test on start or end
const testGet = from > to ? this.end : this.start
if (this.topList.length > 0) {
yield* this.iterateSublist(
this.topList,
from,
to,
inc,
searchGet,
testGet,
[0],
)
}
}
async histogram(from, to, numBins) {
// calls callback with a histogram of the feature density
// in the given interval
const result = new Array(numBins)
result.fill(0)
const binWidth = (to - from) / numBins
for await (const feat of this.iterate(from, to)) {
const firstBin = Math.max(0, ((this.start(feat) - from) / binWidth) | 0)
const lastBin = Math.min(
numBins,
((this.end(feat) - from) / binWidth) | 0,
)
for (let bin = firstBin; bin <= lastBin; bin += 1) {
result[bin] += 1
}
}
return result
}
}