143 lines
5.0 KiB
JavaScript
143 lines
5.0 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
|
||
|
*/
|
||
|
|
||
|
var loadedNadgrids = {};
|
||
|
|
||
|
/**
|
||
|
* Load a binary NTv2 file (.gsb) to a key that can be used in a proj string like +nadgrids=<key>. Pass the NTv2 file
|
||
|
* as an ArrayBuffer.
|
||
|
*/
|
||
|
export default function nadgrid(key, data) {
|
||
|
var view = new DataView(data);
|
||
|
var isLittleEndian = detectLittleEndian(view);
|
||
|
var header = readHeader(view, isLittleEndian);
|
||
|
if (header.nSubgrids > 1) {
|
||
|
console.log('Only single NTv2 subgrids are currently supported, subsequent sub grids are ignored');
|
||
|
}
|
||
|
var subgrids = readSubgrids(view, header, isLittleEndian);
|
||
|
var nadgrid = {header: header, subgrids: subgrids};
|
||
|
loadedNadgrids[key] = nadgrid;
|
||
|
return nadgrid;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Given a proj4 value for nadgrids, return an array of loaded grids
|
||
|
*/
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
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 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) {
|
||
|
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);
|
||
|
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)
|
||
|
});
|
||
|
}
|
||
|
return grids;
|
||
|
}
|
||
|
|
||
|
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) {
|
||
|
var nodesOffset = offset + 176;
|
||
|
var gridRecordLength = 16;
|
||
|
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),
|
||
|
latitudeAccuracy: view.getFloat32(nodesOffset + i * gridRecordLength + 8, isLittleEndian),
|
||
|
longitudeAccuracy: view.getFloat32(nodesOffset + i * gridRecordLength + 12, isLittleEndian),
|
||
|
};
|
||
|
gridShiftRecords.push(record);
|
||
|
}
|
||
|
return gridShiftRecords;
|
||
|
}
|