s2-tools
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
405 lines • 14.1 kB
JavaScript
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