s2-tools
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
271 lines • 9.36 kB
JavaScript
import { toReader } from '.';
/** Seconds to degrees (S / 3_600) */
const SEC2DEG = 0.00000484813681109536;
/**
* # NAD Grid V2 Reader
*
* ## Description
* Store Grids from a NTv2 file (.gsb)
*
* ## Usage
* ```ts
* import { NadGridReader } from 's2-tools';
* import { MMapReader } from 's2-tools/mmap';
*
* const store = new NadGridStore();
*
* store.addGridFromReader('BETA2007.gsb', new MMapReader(`${__dirname}/fixtures/BETA2007.gsb`));
*
* const grid = store.getGrid('BETA2007.gsb');
* ```
*/
export class NadGridStore {
grids = new Map();
/**
* Insert a new NadGrid into the store
* @param grid - a nadgrid class to store
*/
addGrid(grid) {
this.grids.set(grid.key, grid);
}
/**
* Get a grid from the store given a key or name
* @param key - the key or name of the grid
* @returns - the grid
*/
getGrid(key) {
return this.grids.get(key);
}
/**
* Add a grid given a data input
* @param key - the key or name of the grid
* @param input - the input data to parse
*/
addGridFromReader(key, input) {
const grid = new NadGridReader(key, input);
this.addGrid(grid);
}
/**
* Get grid definitions from a string name
* @param keys - complex string of grid keys to test against
* @returns - an array of grid definitions
*/
getGridsFromString(keys) {
const res = [];
if (keys === undefined)
return res;
for (const grid of keys.split(',')) {
const g = this.getGridFromString(grid);
if (g !== undefined)
res.push(g);
}
return res;
}
/**
* Get a grid definition from a string
* @param name - a single grid name to test against
* @returns - a grid definition
*/
getGridFromString(name) {
if (name.length === 0)
return undefined;
const optional = name[0] === '@';
if (optional)
name = name.slice(1);
if (name === 'null') {
return { name: 'null', mandatory: !optional, grid: undefined, isNull: true };
}
return {
name: name,
mandatory: !optional,
grid: this.grids.get(name),
isNull: false,
};
}
}
/**
* # NAD Grid Reader
*
* ## Description
* Loads/reads a binary NTv2 file (.gsb) implementing the {@link FeatureIterator} interface
*
* It should be noted that a proj4 Transformer usually uses this class internally. But if you want
* to manually parse a .gsb file, you can use this class directly.
*
* ## Usage
*
* ```ts
* import { NadGridReader } from 's2-tools'
* // mmap is a Bun exclusive feature, consider using `s2-tools/file`'s `FileReader` instead.
* import { MMapReader } from 's2-tools/mmap';
*
* const reader = new NadGridReader('BETA2007.gsb', new MMapReader('./BETA2007.gsb'));
*
* // access all the vector features
* const data = await Array.fromAsync(reader);
* ```
*
* ## Links
* - https://web.archive.org/web/20140127204822if_/http://www.mgs.gov.on.ca:80/stdprodconsume/groups/content/@mgs/@iandit/documents/resourcelist/stel02_047447.pdf
* - http://mimaka.com/help/gs/html/004_NTV2%20Data%20Format.htm
*/
export class NadGridReader {
key;
reader;
#isLittleEndian = false;
#header;
subgrids = [];
/**
* @param key - the key or name of the grid
* @param input - the input data to parse
*/
constructor(key, input) {
this.key = key;
this.reader = toReader(input);
this.#isLittleEndian = this.#detectLittleEndian();
this.#readHeader();
this.#readSubGrids();
}
/** @returns - The header describing how to decode the feature */
get header() {
return { ...this.#header };
}
/**
* Return all the features in the shapefile
* @returns - a collection of VectorFeatures
*/
getFeatureCollection() {
const features = this.subgrids.map(this.#subGrideToVectorFeature);
return { type: 'FeatureCollection', features };
}
/**
* Iterate over all features in the shapefile
* @yields {VectorFeature<NadGridMetadata>}
*/
async *[Symbol.asyncIterator]() {
for (const subgrid of this.subgrids)
yield this.#subGrideToVectorFeature(subgrid);
}
/**
* @returns - true if the file is little-endian
*/
#detectLittleEndian() {
const { reader } = this;
let nFields = reader.getInt32(8, false);
if (nFields === 11)
return false;
nFields = reader.getInt32(8, true);
if (nFields !== 11) {
console.warn('Failed to detect nadgrid endian-ness, defaulting to little-endian');
}
return true;
}
/** Parse the main header data. Describes the subgrids to decode lon-lat */
#readHeader() {
const { reader } = this;
const le = this.#isLittleEndian;
this.#header = {
nFields: reader.getInt32(8, le),
nSubgridFields: reader.getInt32(24, le),
nSubgrids: reader.getInt32(40, le),
shiftType: reader.parseString(56, 8),
fromSemiMajorAxis: reader.getFloat64(120, le),
fromSemiMinorAxis: reader.getFloat64(136, le),
toSemiMajorAxis: reader.getFloat64(152, le),
toSemiMinorAxis: reader.getFloat64(168, le),
};
}
/** Build all grid data */
#readSubGrids() {
let gridOffset = 176;
for (let i = 0; i < this.#header.nSubgrids; i++) {
const subHeader = this.#readSubGridHeader(gridOffset);
const nodes = this.#readGridNodes(gridOffset, subHeader);
const lonColumnCount = Math.round(1 + (subHeader.upperLongitude - subHeader.lowerLongitude) / subHeader.longitudeInterval);
const latColumnCount = Math.round(1 + (subHeader.upperLatitude - subHeader.lowerLatitude) / subHeader.latitudeInterval);
const subGrid = {
cvs: nodes.map(({ longitudeShift, latitudeShift }) => {
return { x: longitudeShift * SEC2DEG, y: latitudeShift * SEC2DEG };
}),
ll: {
x: subHeader.lowerLongitude * SEC2DEG,
y: subHeader.lowerLatitude * SEC2DEG,
},
del: { x: subHeader.longitudeInterval * SEC2DEG, y: subHeader.latitudeInterval * SEC2DEG },
lim: { x: lonColumnCount, y: latColumnCount },
count: subHeader.gridNodeCount,
};
this.subgrids.push(subGrid);
gridOffset += 176 + subHeader.gridNodeCount * 16;
}
}
/**
* @param subGrid - the subgrid to convert to a vector feature
* @returns - the vector feature
*/
#subGrideToVectorFeature(subGrid) {
const { cvs, ll, del, lim, count } = subGrid;
return {
type: 'VectorFeature',
properties: {},
geometry: {
type: 'MultiPoint',
is3D: false,
// CVS => lonLat coords
coordinates: cvs,
},
metadata: {
// ll => lowerLonLat
lowerLonLat: ll,
// del => lonLatInterval
lonLatInterval: del,
// lim => lonLatColumnCount
lonLatColumnCount: lim,
// count => count
count,
},
};
}
/**
* @param offset - offset to read in the subgrid header
* @returns - the subgrid header
*/
#readSubGridHeader(offset) {
const { reader } = this;
const le = this.#isLittleEndian;
return {
name: reader.parseString(offset + 8, 8),
parent: reader.parseString(offset + 24, 8),
lowerLatitude: reader.getFloat64(offset + 72, le),
upperLatitude: reader.getFloat64(offset + 88, le),
lowerLongitude: reader.getFloat64(offset + 104, le),
upperLongitude: reader.getFloat64(offset + 120, le),
latitudeInterval: reader.getFloat64(offset + 136, le),
longitudeInterval: reader.getFloat64(offset + 152, le),
gridNodeCount: reader.getInt32(offset + 168, le),
};
}
/**
* @param offset - offset of the grid
* @param gridHeader - header of the grid
* @returns - an array of grid nodes
*/
#readGridNodes(offset, gridHeader) {
const { reader } = this;
const le = this.#isLittleEndian;
const nodesOffset = offset + 176;
const gridRecordLength = 16;
const gridShiftRecords = [];
for (let i = 0; i < gridHeader.gridNodeCount; i++) {
const record = {
latitudeShift: reader.getFloat32(nodesOffset + i * gridRecordLength, le),
longitudeShift: reader.getFloat32(nodesOffset + i * gridRecordLength + 4, le),
latitudeAccuracy: reader.getFloat32(nodesOffset + i * gridRecordLength + 8, le),
longitudeAccuracy: reader.getFloat32(nodesOffset + i * gridRecordLength + 12, le),
};
gridShiftRecords.push(record);
}
return gridShiftRecords;
}
}
//# sourceMappingURL=nadgrid.js.map