s2-tools
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
227 lines • 9.19 kB
JavaScript
import { splitSectionChunks } from './sections';
import { BufferReader, toReader } from '..';
export * from './sections';
/**
* Fetch GFS data.
* You can find some data to reference what's available [here](https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod/).
* An example of what variable data means can be found [here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2.0p50.f000.shtml).
* @param source - The source of the data, `aws` | `ftpprd` | `nomads` | `google` | `azure` | or a user provided url
* @param product - which product to fetch
* @param year - The year to fetch given a 4 digit year
* @param month - The month to fetch given a 2 digit month 01 is January and 12 is December
* @param day - The day to fetch given a 2 digit day, e.g. '01' or '31'
* @param hour - The forecast hour with 2 digits often in increments of 6 up to 18, e.g. '00' or '12'
* @param forecast - The forecast hour with 3 digits often in increments of 3 up to 384, e.g. '000' or '003'
* @param filters - The filters to apply by filtering lines in the .idx file
* @returns - A GRIB2Reader of the specific sections
*/
export async function fetchGFSAtmos(source, product, year, month, day, hour, forecast = '000', filters) {
// If year is not 4 chars, month not 2, day not 2, or forecast is not 3 chars, return error
if (year.length !== 4 || month.length !== 2 || day.length !== 2 || forecast.length !== 3) {
throw new Error('Year, month, day, and forecast must be 4, 2, 2, and 3 characters, respectively.');
}
const link = getGFSLink(source, product, year, month, day, hour, forecast);
// pull .idx file FIRST
const idxs = await parsedIDXFromURL(`${link}.idx`, filters ?? []);
return await GRIB2Reader.fromIDX(link, idxs);
}
/**
* Get the link to download GFS data
* @param source - The source of the data, `aws` | `ftpprd` | `nomads` | `google` | `azure` | or a user provided url
* @param product - which product to fetch
* @param year - The year to fetch given a 4 digit year
* @param month - The month to fetch given a 2 digit month 01 is January and 12 is December
* @param day - The day to fetch given a 2 digit day, e.g. '01' or '31'
* @param hour - The forecast hour with 2 digits often in increments of 6 up to 18, e.g. '00' or '12'
* @param forecast - The forecast hour with 3 digits often in increments of 3 up to 384, e.g. '000' or '003'
* @returns - A GRIB2Reader of the specific sections
*/
function getGFSLink(source, product, year, month, day, hour, forecast) {
let link = '';
if (source === 'aws') {
link = `https://noaa-gfs-bdp-pds.s3.amazonaws.com/`;
}
else if (source === 'ftpprd') {
link = 'https://ftpprd.ncep.noaa.gov/data/nccf/com/gfs/prod/';
}
else if (source === 'nomads') {
link = 'https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod/';
}
else if (source === 'google') {
link = 'https://storage.googleapis.com/global-forecast-system/';
}
else if (source === 'azure') {
link = 'https://noaagfs.blob.core.windows.net/gfs/';
}
else {
link = source;
}
link += `gfs.${year}${month}${day}/${hour}/atmos/gfs.t${hour}z.${product}.f${forecast}`;
return link;
}
/**
* Parse the .idx file for GRIB2 section details using a URL
* @param url - The URL of the .idx file
* @param filters - The filters to apply
* @param offsetPosition - The position of the offset in the ":" sequence
* @returns - An array of SectionLocations
*/
export async function parsedIDXFromURL(url, filters, offsetPosition = 1) {
const data = await fetch(url).then(async (res) => await res.text());
return parseIDX(data, filters, offsetPosition);
}
/**
* Parse the .idx file for GRIB2 section details
* @param data - The contents of the .idx file
* @param filters - The filters to apply
* @param offsetPosition - The position of the offset in the ":" sequence
* @returns - An array of SectionLocations
*/
export function parseIDX(data, filters, offsetPosition = 1) {
let res = [];
// split lines, parse information, and add to array
const lines = data.split('\n');
for (const line of lines) {
if (line.length === 0)
continue;
const pieces = line.split(':');
const offset = parseInt(pieces[offsetPosition], 10);
res.push({ start: offset, line, name: line });
}
// now add the "end"s
for (let i = 0; i < res.length - 1; i++)
res[i].end = res[i + 1].start;
// lastly add the filters
if (filters.length > 0) {
res = res.filter((sL) => filters.some((f) => sL.line.includes(f)));
}
// set names to filter names
for (let i = 0; i < res.length; i++)
res[i].name = filters[i];
return res;
}
/**
* # GRIB2 Reader
*
* ## Description
*
* This class reads a GRIB2 file and returns a list of GRIB2 products.
* Implements the {@link FeatureIterator} interface.
*
* ## Usage
*
* ### The recommended way to parse grib files is to filter out what you want:
* ```ts
* // pull .idx file FIRST and filter the ones you want
* const filters = [':DZDT:0.01 mb:', ':TMP:0.4 mb:', ':ABSV:0.4 mb:anl:'];
* const idxs = await parsedIDXFromURL(`${link}.idx`, filters);
* // now bulid the reader
* const gribReader = await GRIB2Reader.fromIDX(link, idxs);
*
* for await (const feature of gribReader) {
* console.log(feature);
* }
* ```
*
* ### Parsing the entire grib file:
* ```
* const gribReader = new GRIB2Reader(link);
* ```
*/
export class GRIB2Reader {
idxs;
packets = [];
/**
* @param readers - Reader(s) for entire GRIB file. If array, its grib chunks, otherwise it will be the entire file
* @param idxs - The list of section locations we will be parsing
*/
constructor(readers, idxs) {
this.idxs = idxs;
const gribChunks = Array.isArray(readers) ? readers : splitGribChunks(toReader(readers));
for (const gribChunk of gribChunks)
this.packets.push(splitSectionChunks(gribChunk));
}
/**
* Create a GRIB2Reader from a .idx file
* @param source - Either the http path to the .idx file or the entire GRIB file
* @param idxs - The parsed .idx file with the locations of each section
* @returns A GRIB2Reader of the specific sections
*/
static async fromIDX(source, idxs) {
const readers = [];
if (typeof source === 'string') {
for (const { start, end } of idxs) {
const chunk = await fetch(source, {
headers: { Range: `bytes=${start}-${end === undefined ? '' : end}` },
}).then(async (res) => await res.arrayBuffer());
readers.push(new BufferReader(chunk));
}
}
else {
for (const idx of idxs) {
readers.push(new BufferReader(source.slice(idx.start, idx.end).buffer));
}
}
return new GRIB2Reader(readers, idxs);
}
/**
* Iterate through each packet and add their products to the geometry
* @yields {VectorFeature}
*/
async *[Symbol.asyncIterator]() {
// setup metadata
const productMetadata = this.packets
.map((packet) => packet.productDefinition?.values)
.filter((p) => p !== undefined);
// setup geometry
const geometry = this.packets[0].gridDefinition?.values.buildGrid();
if (geometry === undefined)
return;
// add M-Values from each packet
// for (const packet of this.packets) {
for (let i = 0; i < this.packets.length; i++) {
const packet = this.packets[i];
const name = this.idxs?.[i]?.name ?? String(i);
const data = packet.data?.getData();
if (data === undefined)
continue;
for (let i = 0; i < data.length; i++) {
const mValue = data[i];
if (mValue === undefined)
continue;
const geo = geometry[i];
if (geo.m === undefined)
geo.m = {};
geo.m[name] = mValue;
}
}
yield {
type: 'VectorFeature',
geometry: {
type: 'MultiPoint',
coordinates: geometry,
is3D: false,
},
properties: {},
metadata: productMetadata,
};
}
}
/**
* Split the bytes of the GRIB file into individual GRIB chunks that represent sections
* @param reader - Reader for entire GRIB file
* @returns Array of GRIB Chunk Buffers containing individual GRIB definitions in file
*/
function splitGribChunks(reader) {
if (reader.byteLength === 0)
return [];
const length = Number(reader.slice(8, 16).getBigUint64(0));
const gribData = new BufferReader(reader.slice(0, length).buffer);
const chunks = [gribData];
if (length === reader.byteLength)
return chunks;
const rest = new BufferReader(reader.slice(length).buffer);
chunks.push(...splitGribChunks(rest));
return chunks;
}
//# sourceMappingURL=index.js.map