mirror of
https://github.com/proj4js/proj4js.git
synced 2026-01-25 16:26:11 +00:00
309 lines
11 KiB
JavaScript
309 lines
11 KiB
JavaScript
/**
|
|
* Resources for details of NTv2 file formats:
|
|
* - 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
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} NadgridInfo
|
|
* @property {string} name The name of the NAD grid or 'null' if not specified.
|
|
* @property {boolean} mandatory Indicates if the grid is mandatory (true) or optional (false).
|
|
* @property {*} grid The loaded NAD grid object, or null if not loaded or not applicable.
|
|
* @property {boolean} isNull True if the grid is explicitly 'null', otherwise false.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} NTV2GridOptions
|
|
* @property {boolean} [includeErrorFields=true] Whether to include error fields in the subgrids.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} NadgridHeader
|
|
* @property {number} [nFields] Number of fields in the header.
|
|
* @property {number} [nSubgridFields] Number of fields in each subgrid header.
|
|
* @property {number} nSubgrids Number of subgrids in the file.
|
|
* @property {string} [shiftType] Type of shift (e.g., "SECONDS").
|
|
* @property {number} [fromSemiMajorAxis] Source ellipsoid semi-major axis.
|
|
* @property {number} [fromSemiMinorAxis] Source ellipsoid semi-minor axis.
|
|
* @property {number} [toSemiMajorAxis] Target ellipsoid semi-major axis.
|
|
* @property {number} [toSemiMinorAxis] Target ellipsoid semi-minor axis.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} Subgrid
|
|
* @property {Array<number>} ll Lower left corner of the grid in radians [longitude, latitude].
|
|
* @property {Array<number>} del Grid spacing in radians [longitude interval, latitude interval].
|
|
* @property {Array<number>} lim Number of columns in the grid [longitude columns, latitude columns].
|
|
* @property {number} [count] Total number of grid nodes.
|
|
* @property {Array} cvs Mapped node values for the grid.
|
|
*/
|
|
|
|
/** @typedef {{header: NadgridHeader, subgrids: Array<Subgrid>}} NADGrid */
|
|
|
|
/**
|
|
* @typedef {Object} GeoTIFF
|
|
* @property {() => Promise<number>} getImageCount - Returns the number of images in the GeoTIFF.
|
|
* @property {(index: number) => Promise<GeoTIFFImage>} getImage - Returns a GeoTIFFImage for the given index.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} GeoTIFFImage
|
|
* @property {() => number} getWidth - Returns the width of the image.
|
|
* @property {() => number} getHeight - Returns the height of the image.
|
|
* @property {() => number[]} getBoundingBox - Returns the bounding box as [minX, minY, maxX, maxY] in degrees.
|
|
* @property {() => Promise<ArrayLike<ArrayLike<number>>>} readRasters - Returns the raster data as an array of bands.
|
|
* @property {Object} fileDirectory - The file directory object containing metadata.
|
|
* @property {Object} fileDirectory.ModelPixelScale - The pixel scale array [scaleX, scaleY, scaleZ] in degrees.
|
|
*/
|
|
|
|
var loadedNadgrids = {};
|
|
|
|
/**
|
|
* @overload
|
|
* @param {string} key - The key to associate with the loaded grid.
|
|
* @param {ArrayBuffer} data - The NTv2 grid data as an ArrayBuffer.
|
|
* @param {NTV2GridOptions} [options] - Optional parameters for loading the grid.
|
|
* @returns {NADGrid} - The loaded NAD grid information.
|
|
*/
|
|
/**
|
|
* @overload
|
|
* @param {string} key - The key to associate with the loaded grid.
|
|
* @param {GeoTIFF} data - The GeoTIFF instance to read the grid from.
|
|
* @returns {{ready: Promise<NADGrid>}} - A promise that resolves to the loaded grid information.
|
|
*/
|
|
/**
|
|
* Load either a NTv2 file (.gsb) or a Geotiff (.tif) to a key that can be used in a proj string like +nadgrids=<key>. Pass the NTv2 file
|
|
* as an ArrayBuffer. Pass Geotiff as a GeoTIFF instance from the geotiff.js library.
|
|
* @param {string} key - The key to associate with the loaded grid.
|
|
* @param {ArrayBuffer|GeoTIFF} data The data to load, either an ArrayBuffer for NTv2 or a GeoTIFF instance.
|
|
* @param {NTV2GridOptions} [options] Optional parameters.
|
|
* @returns {{ready: Promise<NADGrid>}|NADGrid} - A promise that resolves to the loaded grid information.
|
|
*/
|
|
export default function nadgrid(key, data, options) {
|
|
if (data instanceof ArrayBuffer) {
|
|
return readNTV2Grid(key, data, options);
|
|
}
|
|
return { ready: readGeotiffGrid(key, data) };
|
|
}
|
|
|
|
/**
|
|
* @param {string} key The key to associate with the loaded grid.
|
|
* @param {ArrayBuffer} data The NTv2 grid data as an ArrayBuffer.
|
|
* @param {NTV2GridOptions} [options] Optional parameters for loading the grid.
|
|
* @returns {NADGrid} The loaded NAD grid information.
|
|
*/
|
|
function readNTV2Grid(key, data, options) {
|
|
var includeErrorFields = true;
|
|
if (options !== undefined && options.includeErrorFields === false) {
|
|
includeErrorFields = false;
|
|
}
|
|
var view = new DataView(data);
|
|
var isLittleEndian = detectLittleEndian(view);
|
|
var header = readHeader(view, isLittleEndian);
|
|
var subgrids = readSubgrids(view, header, isLittleEndian, includeErrorFields);
|
|
var nadgrid = { header: header, subgrids: subgrids };
|
|
loadedNadgrids[key] = nadgrid;
|
|
return nadgrid;
|
|
}
|
|
|
|
/**
|
|
* @param {string} key The key to associate with the loaded grid.
|
|
* @param {GeoTIFF} tiff The GeoTIFF instance to read the grid from.
|
|
* @returns {Promise<NADGrid>} A promise that resolves to the loaded NAD grid information.
|
|
*/
|
|
async function readGeotiffGrid(key, tiff) {
|
|
var subgrids = [];
|
|
var subGridCount = await tiff.getImageCount();
|
|
// proj produced tiff grid shift files appear to organize lower res subgrids first, higher res/ child subgrids last.
|
|
for (var subgridIndex = subGridCount - 1; subgridIndex >= 0; subgridIndex--) {
|
|
var image = await tiff.getImage(subgridIndex);
|
|
|
|
var rasters = await image.readRasters();
|
|
var data = rasters;
|
|
var lim = [image.getWidth(), image.getHeight()];
|
|
var imageBBoxRadians = image.getBoundingBox().map(degreesToRadians);
|
|
var del = [image.fileDirectory.ModelPixelScale[0], image.fileDirectory.ModelPixelScale[1]].map(degreesToRadians);
|
|
|
|
var maxX = imageBBoxRadians[0] + (lim[0] - 1) * del[0];
|
|
var minY = imageBBoxRadians[3] - (lim[1] - 1) * del[1];
|
|
|
|
var latitudeOffsetBand = data[0];
|
|
var longitudeOffsetBand = data[1];
|
|
var nodes = [];
|
|
|
|
for (let i = lim[1] - 1; i >= 0; i--) {
|
|
for (let j = lim[0] - 1; j >= 0; j--) {
|
|
var index = i * lim[0] + j;
|
|
nodes.push([-secondsToRadians(longitudeOffsetBand[index]), secondsToRadians(latitudeOffsetBand[index])]);
|
|
}
|
|
}
|
|
|
|
subgrids.push({
|
|
del: del,
|
|
lim: lim,
|
|
ll: [-maxX, minY],
|
|
cvs: nodes
|
|
});
|
|
}
|
|
|
|
var tifGrid = {
|
|
header: {
|
|
nSubgrids: subGridCount
|
|
},
|
|
subgrids: subgrids
|
|
};
|
|
loadedNadgrids[key] = tifGrid;
|
|
return tifGrid;
|
|
};
|
|
|
|
/**
|
|
* Given a proj4 value for nadgrids, return an array of loaded grids
|
|
* @param {string} nadgrids A comma-separated list of grid names, optionally prefixed with '@' to indicate optional grids.
|
|
* @returns
|
|
*/
|
|
export function getNadgrids(nadgrids) {
|
|
// Format details: http://proj.maptools.org/gen_parms.html
|
|
if (nadgrids === undefined) {
|
|
return null;
|
|
}
|
|
var grids = nadgrids.split(',');
|
|
return grids.map(parseNadgridString);
|
|
}
|
|
|
|
/**
|
|
* @param {string} value The nadgrid string to get information for.
|
|
* @returns {NadgridInfo|null} An object with grid information, or null if the input is empty.
|
|
*/
|
|
function parseNadgridString(value) {
|
|
if (value.length === 0) {
|
|
return null;
|
|
}
|
|
var optional = value[0] === '@';
|
|
if (optional) {
|
|
value = value.slice(1);
|
|
}
|
|
if (value === 'null') {
|
|
return { name: 'null', mandatory: !optional, grid: null, isNull: true };
|
|
}
|
|
return {
|
|
name: value,
|
|
mandatory: !optional,
|
|
grid: loadedNadgrids[value] || null,
|
|
isNull: false
|
|
};
|
|
}
|
|
|
|
function degreesToRadians(degrees) {
|
|
return (degrees) * Math.PI / 180;
|
|
}
|
|
|
|
function secondsToRadians(seconds) {
|
|
return (seconds / 3600) * Math.PI / 180;
|
|
}
|
|
|
|
function detectLittleEndian(view) {
|
|
var nFields = view.getInt32(8, false);
|
|
if (nFields === 11) {
|
|
return false;
|
|
}
|
|
nFields = view.getInt32(8, true);
|
|
if (nFields !== 11) {
|
|
console.warn('Failed to detect nadgrid endian-ness, defaulting to little-endian');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function readHeader(view, isLittleEndian) {
|
|
return {
|
|
nFields: view.getInt32(8, isLittleEndian),
|
|
nSubgridFields: view.getInt32(24, isLittleEndian),
|
|
nSubgrids: view.getInt32(40, isLittleEndian),
|
|
shiftType: decodeString(view, 56, 56 + 8).trim(),
|
|
fromSemiMajorAxis: view.getFloat64(120, isLittleEndian),
|
|
fromSemiMinorAxis: view.getFloat64(136, isLittleEndian),
|
|
toSemiMajorAxis: view.getFloat64(152, isLittleEndian),
|
|
toSemiMinorAxis: view.getFloat64(168, isLittleEndian)
|
|
};
|
|
}
|
|
|
|
function decodeString(view, start, end) {
|
|
return String.fromCharCode.apply(null, new Uint8Array(view.buffer.slice(start, end)));
|
|
}
|
|
|
|
function readSubgrids(view, header, isLittleEndian, includeErrorFields) {
|
|
var gridOffset = 176;
|
|
var grids = [];
|
|
for (var i = 0; i < header.nSubgrids; i++) {
|
|
var subHeader = readGridHeader(view, gridOffset, isLittleEndian);
|
|
var nodes = readGridNodes(view, gridOffset, subHeader, isLittleEndian, includeErrorFields);
|
|
var lngColumnCount = Math.round(
|
|
1 + (subHeader.upperLongitude - subHeader.lowerLongitude) / subHeader.longitudeInterval);
|
|
var latColumnCount = Math.round(
|
|
1 + (subHeader.upperLatitude - subHeader.lowerLatitude) / subHeader.latitudeInterval);
|
|
// Proj4 operates on radians whereas the coordinates are in seconds in the grid
|
|
grids.push({
|
|
ll: [secondsToRadians(subHeader.lowerLongitude), secondsToRadians(subHeader.lowerLatitude)],
|
|
del: [secondsToRadians(subHeader.longitudeInterval), secondsToRadians(subHeader.latitudeInterval)],
|
|
lim: [lngColumnCount, latColumnCount],
|
|
count: subHeader.gridNodeCount,
|
|
cvs: mapNodes(nodes)
|
|
});
|
|
var rowSize = 16;
|
|
if (includeErrorFields === false) {
|
|
rowSize = 8;
|
|
}
|
|
gridOffset += 176 + subHeader.gridNodeCount * rowSize;
|
|
}
|
|
return grids;
|
|
}
|
|
|
|
/**
|
|
* @param {*} nodes
|
|
* @returns Array<Array<number>>
|
|
*/
|
|
function mapNodes(nodes) {
|
|
return nodes.map(function (r) {
|
|
return [secondsToRadians(r.longitudeShift), secondsToRadians(r.latitudeShift)];
|
|
});
|
|
}
|
|
|
|
function readGridHeader(view, offset, isLittleEndian) {
|
|
return {
|
|
name: decodeString(view, offset + 8, offset + 16).trim(),
|
|
parent: decodeString(view, offset + 24, offset + 24 + 8).trim(),
|
|
lowerLatitude: view.getFloat64(offset + 72, isLittleEndian),
|
|
upperLatitude: view.getFloat64(offset + 88, isLittleEndian),
|
|
lowerLongitude: view.getFloat64(offset + 104, isLittleEndian),
|
|
upperLongitude: view.getFloat64(offset + 120, isLittleEndian),
|
|
latitudeInterval: view.getFloat64(offset + 136, isLittleEndian),
|
|
longitudeInterval: view.getFloat64(offset + 152, isLittleEndian),
|
|
gridNodeCount: view.getInt32(offset + 168, isLittleEndian)
|
|
};
|
|
}
|
|
|
|
function readGridNodes(view, offset, gridHeader, isLittleEndian, includeErrorFields) {
|
|
var nodesOffset = offset + 176;
|
|
var gridRecordLength = 16;
|
|
|
|
if (includeErrorFields === false) {
|
|
gridRecordLength = 8;
|
|
}
|
|
|
|
var gridShiftRecords = [];
|
|
for (var i = 0; i < gridHeader.gridNodeCount; i++) {
|
|
var record = {
|
|
latitudeShift: view.getFloat32(nodesOffset + i * gridRecordLength, isLittleEndian),
|
|
longitudeShift: view.getFloat32(nodesOffset + i * gridRecordLength + 4, isLittleEndian)
|
|
|
|
};
|
|
|
|
if (includeErrorFields !== false) {
|
|
record.latitudeAccuracy = view.getFloat32(nodesOffset + i * gridRecordLength + 8, isLittleEndian);
|
|
record.longitudeAccuracy = view.getFloat32(nodesOffset + i * gridRecordLength + 12, isLittleEndian);
|
|
}
|
|
|
|
gridShiftRecords.push(record);
|
|
}
|
|
return gridShiftRecords;
|
|
}
|