Files
fourdst/electron/opatParser.js

375 lines
11 KiB
JavaScript

/**
* A helper class to read binary data from an ArrayBuffer.
* It keeps track of the current position and handles reading different data types.
*/
class DataReader {
/**
* @param {ArrayBuffer} arrayBuffer The file content.
*/
constructor(arrayBuffer) {
this.dataView = new DataView(arrayBuffer);
this.decoder = new TextDecoder();
this.offset = 0;
}
seek(newOffset) {
this.offset = newOffset;
}
readString(length) {
const strBytes = new Uint8Array(this.dataView.buffer, this.offset, length);
this.offset += length;
// Trim null characters from the end
const firstNull = strBytes.indexOf(0);
const nonNullSlice = firstNull === -1 ? strBytes : strBytes.slice(0, firstNull);
return this.decoder.decode(nonNullSlice);
}
readBytes(length) {
const bytes = new Uint8Array(this.dataView.buffer, this.offset, length);
this.offset += length;
return bytes;
}
readUint8() {
const value = this.dataView.getUint8(this.offset, true);
this.offset += 1;
return value;
}
readUint16() {
const value = this.dataView.getUint16(this.offset, true);
this.offset += 2;
return value;
}
readUint32() {
const value = this.dataView.getUint32(this.offset, true);
this.offset += 4;
return value;
}
readUint64() {
const value = this.dataView.getBigUint64(this.offset, true);
this.offset += 8;
return value;
}
readFloat64() {
const value = this.dataView.getFloat64(this.offset, true);
this.offset += 8;
return value;
}
readFloat64Array(length) {
const byteLength = length * 8;
const values = new Float64Array(this.dataView.buffer, this.offset, length);
this.offset += byteLength;
return values;
}
}
/**
* Corresponds to the Header struct in opatIO.h
*/
class Header {
constructor() {
this.magic = "";
this.version = 0;
this.numTables = 0;
this.headerSize = 0;
this.indexOffset = 0n; // BigInt
this.creationDate = "";
this.sourceInfo = "";
this.comment = "";
this.numIndex = 0;
this.hashPrecision = 0;
this.reserved = null;
}
static fromReader(reader) {
const h = new Header();
h.magic = reader.readString(4);
h.version = reader.readUint16();
h.numTables = reader.readUint32();
h.headerSize = reader.readUint32();
h.indexOffset = reader.readUint64();
h.creationDate = reader.readString(16);
h.sourceInfo = reader.readString(64);
h.comment = reader.readString(128);
h.numIndex = reader.readUint16();
h.hashPrecision = reader.readUint8();
h.reserved = reader.readBytes(23);
return h;
}
}
/**
* Corresponds to the CardHeader struct in opatIO.h
*/
class CardHeader {
constructor() {
this.magic = "";
this.numTables = 0;
this.headerSize = 0;
this.indexOffset = 0n;
this.cardSize = 0n;
this.comment = "";
this.reserved = null;
}
static fromReader(reader) {
const h = new CardHeader();
h.magic = reader.readString(4);
h.numTables = reader.readUint32();
h.headerSize = reader.readUint32();
h.indexOffset = reader.readUint64();
h.cardSize = reader.readUint64();
h.comment = reader.readString(128);
h.reserved = reader.readBytes(100);
return h;
}
}
/**
* Corresponds to the CardCatalogEntry struct in opatIO.h
*/
class CardCatalogEntry {
constructor() {
this.index = []; // This will be an array of numbers (FloatIndexVector)
this.byteStart = 0n;
this.byteEnd = 0n;
this.sha256 = null; // Uint8Array
}
/**
* In JS, we can use a stringified version of the index vector as a key for Maps.
*/
getKey() {
return JSON.stringify(this.index);
}
static fromReader(reader, numIndex) {
const entry = new CardCatalogEntry();
entry.index = Array.from(reader.readFloat64Array(numIndex));
entry.byteStart = reader.readUint64();
entry.byteEnd = reader.readUint64();
entry.sha256 = reader.readBytes(32);
return entry;
}
}
/**
* Corresponds to the TableIndexEntry struct in opatIO.h
*/
class TableIndexEntry {
constructor() {
this.tag = "";
this.byteStart = 0n;
this.byteEnd = 0n;
this.numColumns = 0;
this.numRows = 0;
this.columnName = "";
this.rowName = "";
this.size = 0n; // Vector size of each cell
this.reserved = null;
}
static fromReader(reader) {
const entry = new TableIndexEntry();
entry.tag = reader.readString(8);
entry.byteStart = reader.readUint64();
entry.byteEnd = reader.readUint64();
entry.numColumns = reader.readUint16();
entry.numRows = reader.readUint16();
entry.columnName = reader.readString(8);
entry.rowName = reader.readString(8);
entry.size = reader.readUint64();
entry.reserved = reader.readBytes(12);
return entry;
}
}
/**
* Corresponds to the OPATTable struct in opatIO.h
*/
class OPATTable {
constructor() {
this.rowValues = new Float64Array(0);
this.columnValues = new Float64Array(0);
this.data = new Float64Array(0);
this.N_R = 0;
this.N_C = 0;
this.m_vsize = 0;
}
/**
* Gets the vector at a specific cell.
* @param {number} row The row index.
* @param {number} column The column index.
* @returns {Float64Array} The vector data in the cell.
*/
getData(row, column) {
if (row >= this.N_R || column >= this.N_C) {
throw new Error("Index out of range");
}
const startIndex = (row * this.N_C + column) * this.m_vsize;
return this.data.subarray(startIndex, startIndex + this.m_vsize);
}
/**
* Gets a single primitive value from a cell.
* @param {number} row
* @param {number} column
* @param {number} zdepth The index within the cell's vector.
*/
getValue(row, column, zdepth = 0) {
if (row >= this.N_R || column >= this.N_C || zdepth >= this.m_vsize) {
throw new Error("Index out of range");
}
const index = (row * this.N_C + column) * this.m_vsize + zdepth;
return this.data[index];
}
static fromReader(reader, cardEntry, tableEntry) {
const table = new OPATTable();
table.N_R = tableEntry.numRows;
table.N_C = tableEntry.numColumns;
table.m_vsize = Number(tableEntry.size); // Convert BigInt to Number
reader.seek(Number(cardEntry.byteStart) + Number(tableEntry.byteStart));
table.rowValues = reader.readFloat64Array(table.N_R);
table.columnValues = reader.readFloat64Array(table.N_C);
const dataSize = table.N_R * table.N_C * table.m_vsize;
table.data = reader.readFloat64Array(dataSize);
return table;
}
}
/**
* Corresponds to the DataCard struct in opatIO.h
*/
class DataCard {
constructor() {
/** @type {CardHeader} */
this.header = null;
/** @type {Map<string, TableIndexEntry>} */
this.tableIndex = new Map();
/** @type {Map<string, OPATTable>} */
this.tableData = new Map();
}
/**
* Get a table by its tag name.
* @param {string} tag The tag of the table to retrieve.
* @returns {OPATTable | undefined}
*/
get(tag) {
return this.tableData.get(tag);
}
static fromReader(reader, cardCatalogEntry) {
const card = new DataCard();
reader.seek(Number(cardCatalogEntry.byteStart));
// 1. Read Card Header
card.header = CardHeader.fromReader(reader);
// 2. Read Table Index
reader.seek(Number(cardCatalogEntry.byteStart) + Number(card.header.indexOffset));
for (let i = 0; i < card.header.numTables; i++) {
const tableIndexEntry = TableIndexEntry.fromReader(reader);
card.tableIndex.set(tableIndexEntry.tag, tableIndexEntry);
}
// 3. Read all Table Data within this card
for (const [tag, tableEntry] of card.tableIndex.entries()) {
const table = OPATTable.fromReader(reader, cardCatalogEntry, tableEntry);
card.tableData.set(tag, table);
}
return card;
}
}
/**
* The main class representing an entire OPAT file.
*/
class OPAT {
constructor() {
/** @type {Header} */
this.header = null;
/** @type {Map<string, CardCatalogEntry>} */
this.cardCatalog = new Map();
/** @type {Map<string, DataCard>} */
this.cards = new Map();
}
/**
* Get a DataCard by its index vector.
* @param {number[]} index The index vector (e.g., [1.0, 2.5]).
* @returns {DataCard | undefined}
*/
get(index) {
const key = JSON.stringify(index);
return this.cards.get(key);
}
/**
* Calculates and returns the bounds for each dimension.
* @returns {Array<{min: number, max: number}>}
*/
getBounds() {
const bounds = Array(this.header.numIndex).fill(null).map(() => ({
min: Infinity,
max: -Infinity
}));
for (const entry of this.cardCatalog.values()) {
for (let i = 0; i < this.header.numIndex; i++) {
bounds[i].min = Math.min(bounds[i].min, entry.index[i]);
bounds[i].max = Math.max(bounds[i].max, entry.index[i]);
}
}
return bounds;
}
}
/**
* Main parsing function. Reads an ArrayBuffer and returns an OPAT object.
* @param {ArrayBuffer} arrayBuffer The content of the .opat file.
* @returns {OPAT} The parsed OPAT object.
*/
function parseOPAT(arrayBuffer) {
const reader = new DataReader(arrayBuffer);
const opat = new OPAT();
// 1. Read Header
opat.header = Header.fromReader(reader);
if (opat.header.magic !== "OPAT") {
throw new Error("File is not a valid OPAT file.");
}
// 2. Read Card Catalog
reader.seek(Number(opat.header.indexOffset)); // Seek to where the catalog starts
for (let i = 0; i < opat.header.numTables; i++) {
const entry = CardCatalogEntry.fromReader(reader, opat.header.numIndex);
opat.cardCatalog.set(entry.getKey(), entry);
}
// 3. Read all DataCards
for (const entry of opat.cardCatalog.values()) {
const dataCard = DataCard.fromReader(reader, entry);
opat.cards.set(entry.getKey(), dataCard);
}
return opat;
}