UNPKG

s2-tools

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

405 lines 14.1 kB
import { toReader } from '..'; // Grammar constants const NC_UNLIMITED = 0; const NC_DIMENSION = 10; const NC_VARIABLE = 11; const NC_ATTRIBUTE = 12; /** Enum of the NetCDF data types available */ export var CDFDataType; (function (CDFDataType) { /** Byte size (1 byte) */ CDFDataType[CDFDataType["BYTE"] = 1] = "BYTE"; /** Char size (1 byte) */ CDFDataType[CDFDataType["CHAR"] = 2] = "CHAR"; /** Short size (2 bytes) */ CDFDataType[CDFDataType["SHORT"] = 3] = "SHORT"; /** Integer size (4 bytes) */ CDFDataType[CDFDataType["INT"] = 4] = "INT"; /** Float size (4 bytes) */ CDFDataType[CDFDataType["FLOAT"] = 5] = "FLOAT"; /** Double size (8 bytes) */ CDFDataType[CDFDataType["DOUBLE"] = 6] = "DOUBLE"; })(CDFDataType || (CDFDataType = {})); /** * @param type - the NetCDF data type * @returns the number of bytes for the data type */ function typeToBytes(type) { switch (type) { case CDFDataType.BYTE: case CDFDataType.CHAR: return 1; case CDFDataType.SHORT: return 2; case CDFDataType.INT: case CDFDataType.FLOAT: return 4; case CDFDataType.DOUBLE: return 8; default: return -1; } } /** * # NetCDF v3.x Reader * * ## Description * Read the NetCDF v3.x file format * [See specification](https://www.unidata.ucar.edu/software/netcdf/docs/file_format_specifications.html) * Implements the {@link FeatureIterator} interface * * ## Usage * ```ts * import { NetCDFReader } from 's2-tools'; * import { FileReader } from 's2-tools/file'; * * const reader = new NetCDFReader(new FileReader('./data.nc')); * for (const feature of reader) { * console.log(feature); * } * ``` */ export class NetCDFReader { reader; recordDimension = { size: 0 }; /** List of dimensions */ dimensions = []; /** List of global attributes */ globalAttributes = {}; /** List of variables */ variables = []; /** Describes if offsets are 32 or 64 bits */ is64; /** Track the cursor for parsing the header */ #cursor = 4; #lonKey = 'lon'; #latKey = 'lat'; #heightKey; #propFields; /** * @param input - The data as either a buffer or file reader * @param options - User defined options to apply when reading the NetCDF file */ constructor(input, options) { this.reader = toReader(input); // Validate that it's a NetCDF file const magic = this.reader.parseString(0, 3); if (magic !== 'CDF') throw new TypeError('Not a valid NetCDF file: should start with CDF'); // Check the NetCDF format this.is64 = this.reader.getUint8(3) === 1 ? false : true; // Read the header this.#parseHeader(); this.#lonKey = options?.lonKey ?? 'lon'; this.#latKey = options?.latKey ?? 'lat'; this.#heightKey = options?.heightKey; this.#propFields = options?.propFields ?? []; } /** * Retrieves the data for a given variable * @param variableName - Name of the variable to search or variable object * @returns The variable values */ getDataVariable(variableName) { const variable = this.variables.find((val) => { return val.name === variableName; }); // return nothing if not found if (variable === undefined) return undefined; // go to the offset position this.#cursor = variable.offset; // return the data if (variable.record) { return this.#getRecord(variable); } else { return this.#getNonRecord(variable); } } /** * Generator to iterate over each (Geo|S2)JSON object in the file * @yields {VectorFeature} */ async *[Symbol.asyncIterator]() { const lat = this.getDataVariable(this.#latKey)?.flat(); const lon = this.getDataVariable(this.#lonKey)?.flat(); const height = this.#heightKey !== undefined ? this.getDataVariable(this.#heightKey)?.flat() : undefined; const fieldProps = {}; for (const field of this.#propFields) fieldProps[field] = this.getDataVariable(field)?.flat() ?? []; if (lat === undefined || lon === undefined) return; for (let index = 0; index < lat.length; index++) { const point = { x: lon[index], y: lat[index], z: height?.[index] }; const properties = {}; for (const field of this.#propFields) properties[field] = fieldProps[field][index]; yield { type: 'VectorFeature', geometry: { type: 'Point', is3D: point.z !== undefined, coordinates: point, }, properties: properties, }; } } /** * Internal method to Parse the header */ #parseHeader() { // build dimension list this.recordDimension.size = this.#getU32(); this.#buildDimensionList(); // build global attributes this.globalAttributes = this.#buildAttributes(); // build the variable list this.#buildVariablesList(); } /** * Internal method to build the dimension list */ #buildDimensionList() { const dimListTag = this.#getU32(); if (dimListTag === 0) { const ensureEmpty = this.#getU32(); if (ensureEmpty !== 0) throw new TypeError('wrong empty tag for list of dimensions'); } else { if (dimListTag !== NC_DIMENSION) throw new TypeError('wrong tag for list of dimensions'); // Length of dimensions const dimensionSize = this.#getU32(); //populate `name` and `size` for each dimension for (let index = 0; index < dimensionSize; index++) { // Read name const name = this.#getName(); // Read dimension size const size = this.#getU32(); if (size === NC_UNLIMITED) { // in netcdf 3 one field can be of size unlimited this.recordDimension.id = index; this.recordDimension.name = name; } // store the dimension this.dimensions.push({ index, name, size, }); } } } /** * Internal method to build attributes including global attributes * @returns - attributes from a block of data at a given offset */ #buildAttributes() { const atrributes = {}; const gAttTag = this.#getU32(); if (gAttTag === 0) { const ensureEmpty = this.#getU32(); if (ensureEmpty !== 0) throw new TypeError('wrong empty tag for list of attributes'); } else { if (gAttTag !== NC_ATTRIBUTE) throw new TypeError('wrong tag for list of attributes'); // Length of attributes const attributeSize = this.#getU32(); // Populate `name`, `type` and `value` for each attribute for (let gaIdx = 0; gaIdx < attributeSize; gaIdx++) { // Read name, type, and size of data block const name = this.#getName(); const type = this.#getU32(); const size = this.#getU32(); // store the attribute key-value atrributes[name] = this.#getType(type, size); } } return atrributes; } /** * Internal method to build a variable list from a block of data at a given offset */ #buildVariablesList() { const varTag = this.#getU32(); let recordStep = 0; if (varTag === 0) { const ensureEmpty = this.#getU32(); if (ensureEmpty !== 0) throw new TypeError('wrong empty tag for list of variables'); } else { if (varTag !== NC_VARIABLE) throw new TypeError('wrong tag for list of variables'); // Length of variables const varSize = this.#getU32(); for (let vIdx = 0; vIdx < varSize; vIdx++) { // Read name, dimensionality, and index into the list of dimensions const name = this.#getName(); const dimensionality = this.#getU32(); const dimensionsIds = []; for (let dim = 0; dim < dimensionality; dim++) dimensionsIds.push(this.#getU32()); // Read variables size const attributes = this.#buildAttributes(); // Read type const type = this.#getU32(); // Read variable size // The 32-bit varSize field is not large enough to contain the size of variables that require // more than 2^32 - 4 bytes, so 2^32 - 1 is used in the varSize field for such variables. const varSize = this.#getU32(); // Read offset const offset = this.#getOffset(); let record = false; // Count amount of record variables if (dimensionsIds.length > 0 && dimensionsIds[0] === this.recordDimension.id) { recordStep += varSize; record = true; } this.variables.push({ name, dimensions: dimensionsIds.map((id) => this.dimensions[id]), attributes, type, size: varSize, offset, record, }); } } this.recordDimension.recordStep = recordStep; } /** * Internal method to get the current offset * @returns - the current offset */ #getOffset() { if (this.is64) return Number(this.#getU64()); return this.#getU32(); } /** * Internal method to get a 32 but value under the cursor * @returns - a 32 bit value */ #getU32() { const data = this.reader.getUint32(this.#cursor); this.#cursor += 4; return data; } /** * Internal method to get a 64 but value under the cursor * @returns - a 64 bit value */ #getU64() { const data = this.reader.getBigUint64(this.#cursor); this.#cursor += 8; return data; } /** * Internal method to read a string under the cursor * @returns - a string */ #getName() { const nameLength = this.#getU32(); const name = this.reader.parseString(this.#cursor, nameLength); this.#cursor += nameLength; this.#padding(); return name; } /** * @param type - the data type * @param size - the data size * @returns - the data */ #getType(type, size) { let res; if (type === CDFDataType.BYTE) { res = []; for (let i = 0; i < size; i++) { res.push(this.reader.getUint8(this.#cursor)); this.#cursor++; } } else if (type === CDFDataType.CHAR) { res = this.reader.parseString(this.#cursor, size); this.#cursor += size; } else if (type === CDFDataType.SHORT || type === CDFDataType.INT || type === CDFDataType.FLOAT || type === CDFDataType.DOUBLE) { const step = type === CDFDataType.DOUBLE ? 8 : type === CDFDataType.SHORT ? 2 : 4; const readNumber = type === CDFDataType.SHORT ? this.reader.getInt16.bind(this.reader) : type === CDFDataType.INT ? this.reader.getInt32.bind(this.reader) : type === CDFDataType.FLOAT ? this.reader.getFloat32.bind(this.reader) : this.reader.getFloat64.bind(this.reader); res = []; for (let i = 0; i < size; i++) { res.push(readNumber(this.#cursor)); this.#cursor += step; } if (res.length === 1) res = res[0]; } else { throw new Error(`non valid type ${type}`); } this.#padding(); return res; } /** * Read data for the given non-record variable * @param variable - Variable metadata * @returns - Data of the element */ #getNonRecord(variable) { // variable type const { size, type } = variable; // size of the data const totalSize = size / typeToBytes(type); // iterates over the data const data = []; for (let i = 0; i < totalSize; i++) data.push(this.#getType(type, 1)); return data; } /** * Read data for the given record variable * @param variable - Variable metadata * @returns - Data of the element */ #getRecord(variable) { // prep variables const { recordStep, size: totalSize } = this.recordDimension; const { size, type } = variable; const width = size !== 0 ? size / typeToBytes(type) : 1; // TODO streaming data if (recordStep === undefined) throw new Error('recordDimension.recordStep is undefined'); // iterates over the data const data = []; for (let i = 0; i < totalSize; i++) { const currentOffset = this.#cursor; data.push(this.#getType(type, width)); this.#cursor = currentOffset + recordStep; } return data; } /** Apply padding as data is mapped to 4-byte alignment */ #padding() { if (this.#cursor % 4 !== 0) this.#cursor += 4 - (this.#cursor % 4); } } //# sourceMappingURL=index.js.map