/** * 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=. 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; }