@jbrowse/core
Version:
JBrowse 2 core libraries used by plugins
877 lines (876 loc) • 26.9 kB
JavaScript
import { useEffect, useRef, useState } from 'react';
import { unzip } from '@gmod/bgzf-filehandle';
import useMeasure from '@jbrowse/core/util/useMeasure';
import { getEnv as getEnvMST, getParent, getSnapshot, hasParent, isAlive, isStateTreeNode, } from '@jbrowse/mobx-state-tree';
import { flushSync } from 'react-dom';
import { createRoot } from 'react-dom/client';
import { coarseStripHTML } from "./coarseStripHTML.js";
import { colord } from "./colord.js";
import { parseLocString } from "./locString.js";
import { checkStopToken } from "./stopToken.js";
import { isDisplayModel, isSessionModel, isTrackModel, isUriLocation, isViewModel, } from "./types/index.js";
export * from "./types/index.js";
export * from "./when.js";
export * from "./range.js";
export * from "./dedupe.js";
export * from "./coarseStripHTML.js";
export * from "./offscreenCanvasPonyfill.js";
export * from "./offscreenCanvasUtils.js";
export * from "./rpc.js";
export * from "./crypto.js";
const containingDisplayCache = new WeakMap();
const containingTrackCache = new WeakMap();
const containingViewCache = new WeakMap();
const sessionCache = new WeakMap();
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handle = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handle);
};
}, [value, delay]);
return debouncedValue;
}
export function useWidthSetter(view, padding) {
const [ref, { width }] = useMeasure();
useEffect(() => {
let token;
if (width && isAlive(view)) {
token = requestAnimationFrame(() => {
view.setWidth(width);
});
}
return () => {
if (token) {
cancelAnimationFrame(token);
}
};
}, [padding, view, width]);
return ref;
}
export function useDebouncedCallback(callback, wait = 400) {
const argsRef = useRef(null);
const timeout = useRef(null);
useEffect(() => {
if (timeout.current) {
clearTimeout(timeout.current);
}
}, []);
return function debouncedCallback(...args) {
argsRef.current = args;
if (timeout.current) {
clearTimeout(timeout.current);
}
timeout.current = setTimeout(() => {
if (argsRef.current) {
callback(...argsRef.current);
}
}, wait);
};
}
export function findParentThat(node, predicate) {
if (!hasParent(node)) {
throw new Error('node does not have parent');
}
let currentNode = getParent(node);
while (currentNode && isAlive(currentNode)) {
if (predicate(currentNode)) {
return currentNode;
}
if (hasParent(currentNode)) {
currentNode = getParent(currentNode);
}
else {
break;
}
}
throw new Error('no matching node found');
}
export function springAnimate(fromValue, toValue, setValue, onFinish = () => { }, precision = 0, tension = 400, friction = 20, clamp = true) {
const mass = 1;
if (!precision) {
precision = Math.abs(toValue - fromValue) / 1000;
}
let animationFrameId;
function update(animation) {
const time = performance.now();
let position = animation.lastPosition;
let lastTime = animation.lastTime || time;
let velocity = animation.lastVelocity || 0;
if (time > lastTime + 64) {
lastTime = time;
}
const numSteps = Math.floor(time - lastTime);
for (let i = 0; i < numSteps; ++i) {
const force = -tension * (position - toValue);
const damping = -friction * velocity;
const acceleration = (force + damping) / mass;
velocity += (acceleration * 1) / 1000;
position += (velocity * 1) / 1000;
}
const isVelocity = Math.abs(velocity) <= precision;
const isDisplacement = tension !== 0 ? Math.abs(toValue - position) <= precision : true;
const isOvershooting = clamp && tension !== 0
? fromValue < toValue
? position > toValue
: position < toValue
: false;
const endOfAnimation = isOvershooting || (isVelocity && isDisplacement);
if (endOfAnimation) {
setValue(toValue);
onFinish();
}
else {
setValue(position);
animationFrameId = requestAnimationFrame(() => {
update({
lastPosition: position,
lastTime: time,
lastVelocity: velocity,
});
});
}
}
return [
() => {
update({ lastPosition: fromValue });
},
() => {
cancelAnimationFrame(animationFrameId);
},
];
}
export function findParentThatIs(node, predicate) {
return findParentThat(node, predicate);
}
export function getSession(node) {
const cached = sessionCache.get(node);
if (cached && isAlive(cached)) {
return cached;
}
try {
const result = findParentThatIs(node, isSessionModel);
sessionCache.set(node, result);
return result;
}
catch (e) {
throw new Error('no session model found!');
}
}
export function getContainingView(node) {
const cached = containingViewCache.get(node);
if (cached && isAlive(cached)) {
return cached;
}
try {
const result = findParentThatIs(node, isViewModel);
containingViewCache.set(node, result);
return result;
}
catch (e) {
throw new Error('no containing view found');
}
}
export function getContainingTrack(node) {
const cached = containingTrackCache.get(node);
if (cached && isAlive(cached)) {
return cached;
}
try {
const result = findParentThatIs(node, isTrackModel);
containingTrackCache.set(node, result);
return result;
}
catch (e) {
throw new Error('no containing track found');
}
}
export function getContainingDisplay(node) {
const cached = containingDisplayCache.get(node);
if (cached && isAlive(cached)) {
return cached;
}
try {
const result = findParentThatIs(node, isDisplayModel);
containingDisplayCache.set(node, result);
return result;
}
catch (e) {
throw new Error('no containing display found');
}
}
export function assembleLocString(region) {
return assembleLocStringFast(region, toLocale);
}
export function assembleLocStringFast(region, cb = (n) => n) {
const { assemblyName, refName, start, end, reversed } = region;
const assemblyNameString = assemblyName ? `{${assemblyName}}` : '';
let startString;
if (start !== undefined) {
startString = `:${cb(start + 1)}`;
}
else if (end !== undefined) {
startString = ':1';
}
else {
startString = '';
}
let endString;
if (end !== undefined) {
endString = start !== undefined && start + 1 === end ? '' : `..${cb(end)}`;
}
else {
endString = start !== undefined ? '..' : '';
}
let rev = '';
if (reversed) {
rev = '[rev]';
}
return `${assemblyNameString}${refName}${startString}${endString}${rev}`;
}
export function compareLocs(locA, locB) {
const assemblyComp = locA.assemblyName || locB.assemblyName
? (locA.assemblyName || '').localeCompare(locB.assemblyName || '')
: 0;
if (assemblyComp) {
return assemblyComp;
}
const refComp = locA.refName || locB.refName
? (locA.refName || '').localeCompare(locB.refName || '')
: 0;
if (refComp) {
return refComp;
}
if (locA.start !== undefined && locB.start !== undefined) {
const startComp = locA.start - locB.start;
if (startComp) {
return startComp;
}
}
if (locA.end !== undefined && locB.end !== undefined) {
const endComp = locA.end - locB.end;
if (endComp) {
return endComp;
}
}
return 0;
}
export function compareLocStrings(a, b, isValidRefName) {
const locA = parseLocString(a, isValidRefName);
const locB = parseLocString(b, isValidRefName);
return compareLocs(locA, locB);
}
export function clamp(num, min, max) {
if (num < min) {
return min;
}
if (num > max) {
return max;
}
return num;
}
function roundToNearestPointOne(num) {
return Math.round(num * 10) / 10;
}
export function bpToPx(bp, { reversed, end = 0, start = 0, }, bpPerPx) {
return roundToNearestPointOne((reversed ? end - bp : bp - start) / bpPerPx);
}
const oneEightyOverPi = 180 / Math.PI;
const piOverOneEighty = Math.PI / 180;
export function radToDeg(radians) {
return (radians * oneEightyOverPi) % 360;
}
export function degToRad(degrees) {
return (degrees * piOverOneEighty) % (2 * Math.PI);
}
export function polarToCartesian(rho, theta) {
return [rho * Math.cos(theta), rho * Math.sin(theta)];
}
export function cartesianToPolar(x, y) {
const rho = Math.sqrt(x * x + y * y);
const theta = Math.atan(y / x);
return [rho, theta];
}
export function featureSpanPx(feature, region, bpPerPx) {
return bpSpanPx(feature.get('start'), feature.get('end'), region, bpPerPx);
}
export function bpSpanPx(leftBp, rightBp, region, bpPerPx) {
const start = bpToPx(leftBp, region, bpPerPx);
const end = bpToPx(rightBp, region, bpPerPx);
return region.reversed ? [end, start] : [start, end];
}
export function calculateLayoutBounds(featureStart, featureEnd, layoutWidthBp, reversed) {
const featureWidthBp = featureEnd - featureStart;
const labelOverhangBp = Math.max(0, layoutWidthBp - featureWidthBp);
return reversed
? [featureStart - labelOverhangBp, featureEnd]
: [featureStart, featureStart + layoutWidthBp];
}
export function iterMap(iter, func, sizeHint) {
const results = Array.from({ length: sizeHint || 0 });
let counter = 0;
for (const item of iter) {
results[counter] = func(item);
counter += 1;
}
return results;
}
export function findLastIndex(array, predicate) {
let l = array.length;
while (l--) {
if (predicate(array[l], l, array)) {
return l;
}
}
return -1;
}
export function findLast(array, predicate) {
let l = array.length;
while (l--) {
if (predicate(array[l], l, array)) {
return array[l];
}
}
return undefined;
}
export function renameRegionIfNeeded(refNameMap, region, getSeqAdapterRefName) {
if (isStateTreeNode(region) && !isAlive(region)) {
return region;
}
const newRef = refNameMap?.[region.refName];
if (newRef) {
return {
...(isStateTreeNode(region) ? getSnapshot(region) : region),
refName: newRef,
originalRefName: getSeqAdapterRefName?.(region.refName) ?? region.refName,
};
}
return region;
}
export async function renameRegionsIfNeeded(assemblyManager, args) {
const { regions = [], adapterConfig } = args;
if (!args.sessionId) {
throw new Error('sessionId is required');
}
const assemblyNames = regions.map(r => r.assemblyName);
const uniqueAssemblyNames = [...new Set(assemblyNames)];
const assemblyData = Object.fromEntries(await Promise.all(uniqueAssemblyNames.map(async (name) => [
name,
{
refNameMap: await assemblyManager.getRefNameMapForAdapter(adapterConfig, name, args),
assembly: assemblyManager.get(name),
},
])));
return {
...args,
regions: regions.map((region, i) => {
const { refNameMap, assembly } = assemblyData[assemblyNames[i]];
return renameRegionIfNeeded(refNameMap, region, assembly ? r => assembly.getSeqAdapterRefName(r) : undefined);
}),
};
}
export function minmax(a, b) {
return [Math.min(a, b), Math.max(a, b)];
}
export function shorten(name, max = 70, short = 30) {
return name.length > max
? `${name.slice(0, short)}...${name.slice(-short)}`
: name;
}
export function shorten2(name, max = 70) {
return name.length > max ? `${name.slice(0, max)}...` : name;
}
export function stringify({ refName, coord, assemblyName, oob, }, useAssemblyName) {
return [
assemblyName && useAssemblyName ? `{${assemblyName}}` : '',
refName
? `${shorten(refName)}:${toLocale(coord)}${oob ? ' (out of bounds)' : ''}`
: '',
].join('');
}
export const isElectron = /electron/i.test(typeof navigator !== 'undefined' ? navigator.userAgent : '');
export const complementTable = {
S: 'S',
w: 'w',
T: 'A',
r: 'y',
a: 't',
N: 'N',
K: 'M',
x: 'x',
d: 'h',
Y: 'R',
V: 'B',
y: 'r',
M: 'K',
h: 'd',
k: 'm',
C: 'G',
g: 'c',
t: 'a',
A: 'T',
n: 'n',
W: 'W',
X: 'X',
m: 'k',
v: 'b',
B: 'V',
s: 's',
H: 'D',
c: 'g',
D: 'H',
b: 'v',
R: 'Y',
G: 'C',
};
export function revcom(str) {
const revcomped = [];
for (let i = str.length - 1; i >= 0; i--) {
revcomped.push(complementTable[str[i]] ?? str[i]);
}
return revcomped.join('');
}
export function reverse(str) {
const reversed = [];
for (let i = str.length - 1; i >= 0; i--) {
reversed.push(str[i]);
}
return reversed.join('');
}
export function complement(str) {
const comp = [];
for (let i = 0, l = str.length; i < l; i++) {
comp.push(complementTable[str[i]] ?? str[i]);
}
return comp.join('');
}
export const rIC = typeof jest === 'undefined'
?
typeof window !== 'undefined' && window.requestIdleCallback
? window.requestIdleCallback
: (cb) => setTimeout(() => {
cb();
}, 1)
: (cb) => {
cb();
};
const widths = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2796875, 0.2765625, 0.3546875, 0.5546875, 0.5546875, 0.8890625, 0.665625, 0.190625, 0.3328125, 0.3328125, 0.3890625, 0.5828125, 0.2765625, 0.3328125, 0.2765625, 0.3015625, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.2765625, 0.2765625, 0.584375, 0.5828125, 0.584375, 0.5546875, 1.0140625, 0.665625, 0.665625, 0.721875, 0.721875, 0.665625, 0.609375, 0.7765625, 0.721875, 0.2765625, 0.5, 0.665625, 0.5546875, 0.8328125, 0.721875, 0.7765625, 0.665625, 0.7765625, 0.721875, 0.665625, 0.609375, 0.721875, 0.665625, 0.94375, 0.665625, 0.665625, 0.609375, 0.2765625, 0.3546875, 0.2765625, 0.4765625, 0.5546875, 0.3328125, 0.5546875, 0.5546875, 0.5, 0.5546875, 0.5546875, 0.2765625, 0.5546875, 0.5546875, 0.221875, 0.240625, 0.5, 0.221875, 0.8328125, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.3328125, 0.5, 0.2765625, 0.5546875, 0.5, 0.721875, 0.5, 0.5, 0.5, 0.3546875, 0.259375, 0.353125, 0.5890625];
const avgWidth = 0.5279276315789471;
export function measureText(str, fontSize = 10) {
const s = String(str);
let total = 0;
for (let i = 0, l = s.length; i < l; i++) {
total += widths[s.charCodeAt(i)] ?? avgWidth;
}
return total * fontSize;
}
export function getFrame(start, end, strand, phase) {
return strand === 1
? (((start + phase) % 3) + 1)
: (-1 * ((end - phase) % 3) - 1);
}
export const defaultStarts = ['ATG'];
export const defaultStops = ['TAA', 'TAG', 'TGA'];
export const defaultCodonTable = {
TCA: 'S',
TCC: 'S',
TCG: 'S',
TCT: 'S',
TTC: 'F',
TTT: 'F',
TTA: 'L',
TTG: 'L',
TAC: 'Y',
TAT: 'Y',
TAA: '*',
TAG: '*',
TGC: 'C',
TGT: 'C',
TGA: '*',
TGG: 'W',
CTA: 'L',
CTC: 'L',
CTG: 'L',
CTT: 'L',
CCA: 'P',
CCC: 'P',
CCG: 'P',
CCT: 'P',
CAC: 'H',
CAT: 'H',
CAA: 'Q',
CAG: 'Q',
CGA: 'R',
CGC: 'R',
CGG: 'R',
CGT: 'R',
ATA: 'I',
ATC: 'I',
ATT: 'I',
ATG: 'M',
ACA: 'T',
ACC: 'T',
ACG: 'T',
ACT: 'T',
AAC: 'N',
AAT: 'N',
AAA: 'K',
AAG: 'K',
AGC: 'S',
AGT: 'S',
AGA: 'R',
AGG: 'R',
GTA: 'V',
GTC: 'V',
GTG: 'V',
GTT: 'V',
GCA: 'A',
GCC: 'A',
GCG: 'A',
GCT: 'A',
GAC: 'D',
GAT: 'D',
GAA: 'E',
GAG: 'E',
GGA: 'G',
GGC: 'G',
GGG: 'G',
GGT: 'G',
};
export function generateCodonTable(table) {
const tempCodonTable = {};
for (const codon of Object.keys(table)) {
const aa = table[codon];
const nucs = [];
for (let i = 0; i < 3; i++) {
const nuc = codon.charAt(i);
nucs[i] = [];
nucs[i][0] = nuc.toUpperCase();
nucs[i][1] = nuc.toLowerCase();
}
for (let i = 0; i < 2; i++) {
const n0 = nucs[0][i];
for (let j = 0; j < 2; j++) {
const n1 = nucs[1][j];
for (let k = 0; k < 2; k++) {
const n2 = nucs[2][k];
const triplet = n0 + n1 + n2;
tempCodonTable[triplet] = aa;
}
}
}
}
return tempCodonTable;
}
export async function updateStatus(msg, cb, fn) {
cb?.(msg);
const res = await fn();
cb?.('');
return res;
}
export async function updateStatus2(msg, cb, stopToken, fn) {
cb(msg);
const res = await fn();
checkStopToken(stopToken);
cb('');
return res;
}
export function hashCode(str) {
let hash = 0;
if (str.length === 0) {
return hash;
}
for (let i = 0; i < str.length; i++) {
const chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0;
}
return hash;
}
export function objectHash(obj) {
return `${hashCode(JSON.stringify(obj))}`;
}
export async function bytesForRegions(regions, index) {
const blockResults = await Promise.all(regions.map(r => index.blocksForRange(r.refName, r.start, r.end)));
return sum(blockResults
.flat()
.map(block => block.maxv.blockPosition + 65535 - block.minv.blockPosition));
}
export function isSupportedIndexingAdapter(type = '') {
return [
'Gff3TabixAdapter',
'VcfTabixAdapter',
'Gff3Adapter',
'VcfAdapter',
].includes(type);
}
export function getBpDisplayStr(total) {
if (Math.floor(total / 1_000_000) > 0) {
return `${reducePrecision(total / 1_000_000)}Mbp`;
}
else if (Math.floor(total / 1_000) > 0) {
return `${reducePrecision(total / 1_000)}Kbp`;
}
else {
return `${Math.floor(total)}bp`;
}
}
export function reducePrecision(s, n = 3) {
return toLocale(Number.parseFloat(s.toPrecision(n)));
}
export function getProgressDisplayStr(current, total) {
if (Math.floor(total / 1_000_000) > 0) {
return `${reducePrecision(current / 1_000_000)}/${reducePrecision(total / 1_000_000)}Mb`;
}
else if (Math.floor(total / 1_000) > 0) {
return `${reducePrecision(current / 1_000)}/${reducePrecision(total / 1_000)}Kb`;
}
else {
return `${reducePrecision(current)}/${reducePrecision(total)} bytes`;
}
}
export function toLocale(n) {
if (n < 1000) {
return String(n);
}
const str = String(n);
const len = str.length;
let result = '';
for (let i = 0; i < len; i++) {
if (i > 0 && (len - i) % 3 === 0) {
result += ',';
}
result += str[i];
}
return result;
}
export function getTickDisplayStr(totalBp, bpPerPx) {
return Math.floor(bpPerPx / 1_000) > 0
? `${toLocale(Number.parseFloat((totalBp / 1_000_000).toFixed(2)))}M`
: toLocale(Math.floor(totalBp));
}
export function getLayoutId({ sessionId, trackInstanceId, }) {
return `${sessionId}-${trackInstanceId}`;
}
export function getStatsId({ sessionId, trackInstanceId, }) {
return `${sessionId}-${trackInstanceId}`;
}
export function useLocalStorage(key, initialValue, enabled = true) {
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === 'undefined' || !enabled) {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
}
catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== 'undefined' && enabled) {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
}
catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
export function getUriLink(value) {
const { uri, baseUri = '' } = value;
let href;
try {
href = new URL(uri, baseUri).href;
}
catch (e) {
href = uri;
}
return href;
}
export function getStr(obj) {
return isObject(obj)
? isUriLocation(obj)
? getUriLink(obj)
: JSON.stringify(obj)
: String(obj);
}
export function linkify(s) {
const pattern = /(^|[\s\n]|<[A-Za-z]*\/?>)((?:https?|ftp):\/\/[-A-Z0-9+\u0026\u2019@#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026@#/%=~()_|])/gi;
return s.replaceAll(pattern, '$1<a href=\'$2\' target="_blank">$2</a>');
}
export function measureGridWidth(elements, args) {
const { padding = 30, minWidth = 80, fontSize = 12, maxWidth = 1000, stripHTML = false, } = args || {};
return max(elements
.map(element => getStr(element))
.map(str => (stripHTML ? coarseStripHTML(str) : str))
.map(str => measureText(str, fontSize))
.map(n => Math.min(Math.max(n + padding, minWidth), maxWidth)));
}
export function getEnv(obj) {
return getEnvMST(obj);
}
export function localStorageGetItem(item) {
return typeof localStorage !== 'undefined'
? localStorage.getItem(item)
: undefined;
}
export function localStorageSetItem(str, item) {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(str, item);
}
}
export function max(arr, init = Number.NEGATIVE_INFINITY) {
let max = init;
for (const entry of arr) {
max = Math.max(entry, max);
}
return max;
}
export function min(arr, init = Number.POSITIVE_INFINITY) {
let min = init;
for (const entry of arr) {
min = Math.min(entry, min);
}
return min;
}
export function sum(arr) {
let sum = 0;
for (const entry of arr) {
sum += entry;
}
return sum;
}
export function avg(arr) {
return sum(arr) / arr.length;
}
export function groupBy(array, predicate) {
const result = {};
for (const value of array) {
const t = predicate(value);
if (!result[t]) {
result[t] = [];
}
result[t].push(value);
}
return result;
}
export function notEmpty(value) {
return value !== null && value !== undefined;
}
export function mergeIntervals(intervals, w = 5000) {
if (intervals.length <= 1) {
return intervals;
}
const stack = [];
let top = null;
intervals = intervals.sort((a, b) => a.start - b.start);
stack.push(intervals[0]);
for (let i = 1; i < intervals.length; i++) {
top = stack.at(-1);
if (top.end + w < intervals[i].start - w) {
stack.push(intervals[i]);
}
else if (top.end < intervals[i].end) {
top.end = Math.max(top.end, intervals[i].end);
stack.pop();
stack.push(top);
}
}
return stack;
}
export function gatherOverlaps(regions, w = 5000) {
const memo = {};
for (const x of regions) {
if (!memo[x.refName]) {
memo[x.refName] = [];
}
memo[x.refName].push(x);
}
return Object.values(memo).flatMap(group => mergeIntervals(group.sort((a, b) => a.start - b.start), w));
}
export function stripAlpha(str) {
return colord(str).alpha(1).toHex();
}
export function getStrokeProps(str) {
if (str) {
const c = colord(str);
return {
strokeOpacity: c.alpha(),
stroke: c.alpha(1).toHex(),
};
}
else {
return {};
}
}
export function getFillProps(str) {
if (str) {
const c = colord(str);
return {
fillOpacity: c.alpha(),
fill: c.alpha(1).toHex(),
};
}
else {
return {};
}
}
export function renderToStaticMarkup(node) {
const div = document.createElement('div');
flushSync(() => {
createRoot(div).render(node);
});
return div.innerHTML.replaceAll(/\brgba\((.+?),[^,]+?\)/g, 'rgb($1)');
}
export function isGzip(buf) {
return buf[0] === 31 && buf[1] === 139 && buf[2] === 8;
}
export async function fetchAndMaybeUnzip(loc, opts = {}) {
const { statusCallback = () => { } } = opts;
const buf = await updateStatus('Downloading file', statusCallback, () => loc.readFile(opts));
return isGzip(buf)
? await updateStatus('Unzipping', statusCallback, () => unzip(buf))
: buf;
}
export async function fetchAndMaybeUnzipText(loc, opts) {
const buffer = await fetchAndMaybeUnzip(loc, opts);
if (buffer.length > 536_870_888) {
throw new Error('Data exceeds maximum string length (512MB)');
}
return new TextDecoder('utf8', { fatal: true }).decode(buffer);
}
export function isObject(x) {
return typeof x === 'object' && x !== null;
}
export function localStorageGetNumber(key, defaultVal) {
return +(localStorageGetItem(key) ?? defaultVal);
}
export function localStorageGetBoolean(key, defaultVal) {
return Boolean(JSON.parse(localStorageGetItem(key) || JSON.stringify(defaultVal)));
}
export function localStorageSetBoolean(key, value) {
localStorageSetItem(key, JSON.stringify(value));
}
export function testAdapter(fileName, regex, adapterHint, expected) {
return (regex.test(fileName) && !adapterHint) || adapterHint === expected;
}
export { default as SimpleFeature, isFeature, } from "./simpleFeature.js";
export { blobToDataURL } from "./blobToDataURL.js";
export { makeAbortableReaction } from "./makeAbortableReaction.js";
export * from "./aborting.js";
export * from "./linkify.js";
export * from "./locString.js";
export * from "./stopToken.js";
export * from "./tracks.js";
export * from "./fileHandleStore.js";
export { IntervalTree } from "./IntervalTree.js";