nadgrid.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. /**
  2. * Resources for details of NTv2 file formats:
  3. * - https://web.archive.org/web/20140127204822if_/http://www.mgs.gov.on.ca:80/stdprodconsume/groups/content/@mgs/@iandit/documents/resourcelist/stel02_047447.pdf
  4. * - http://mimaka.com/help/gs/html/004_NTV2%20Data%20Format.htm
  5. */
  6. var loadedNadgrids = {};
  7. /**
  8. * Load a binary NTv2 file (.gsb) to a key that can be used in a proj string like +nadgrids=<key>. Pass the NTv2 file
  9. * as an ArrayBuffer.
  10. */
  11. export default function nadgrid(key, data) {
  12. var view = new DataView(data);
  13. var isLittleEndian = detectLittleEndian(view);
  14. var header = readHeader(view, isLittleEndian);
  15. if (header.nSubgrids > 1) {
  16. console.log('Only single NTv2 subgrids are currently supported, subsequent sub grids are ignored');
  17. }
  18. var subgrids = readSubgrids(view, header, isLittleEndian);
  19. var nadgrid = {header: header, subgrids: subgrids};
  20. loadedNadgrids[key] = nadgrid;
  21. return nadgrid;
  22. }
  23. /**
  24. * Given a proj4 value for nadgrids, return an array of loaded grids
  25. */
  26. export function getNadgrids(nadgrids) {
  27. // Format details: http://proj.maptools.org/gen_parms.html
  28. if (nadgrids === undefined) { return null; }
  29. var grids = nadgrids.split(',');
  30. return grids.map(parseNadgridString);
  31. }
  32. function parseNadgridString(value) {
  33. if (value.length === 0) {
  34. return null;
  35. }
  36. var optional = value[0] === '@';
  37. if (optional) {
  38. value = value.slice(1);
  39. }
  40. if (value === 'null') {
  41. return {name: 'null', mandatory: !optional, grid: null, isNull: true};
  42. }
  43. return {
  44. name: value,
  45. mandatory: !optional,
  46. grid: loadedNadgrids[value] || null,
  47. isNull: false
  48. };
  49. }
  50. function secondsToRadians(seconds) {
  51. return (seconds / 3600) * Math.PI / 180;
  52. }
  53. function detectLittleEndian(view) {
  54. var nFields = view.getInt32(8, false);
  55. if (nFields === 11) {
  56. return false;
  57. }
  58. nFields = view.getInt32(8, true);
  59. if (nFields !== 11) {
  60. console.warn('Failed to detect nadgrid endian-ness, defaulting to little-endian');
  61. }
  62. return true;
  63. }
  64. function readHeader(view, isLittleEndian) {
  65. return {
  66. nFields: view.getInt32(8, isLittleEndian),
  67. nSubgridFields: view.getInt32(24, isLittleEndian),
  68. nSubgrids: view.getInt32(40, isLittleEndian),
  69. shiftType: decodeString(view, 56, 56 + 8).trim(),
  70. fromSemiMajorAxis: view.getFloat64(120, isLittleEndian),
  71. fromSemiMinorAxis: view.getFloat64(136, isLittleEndian),
  72. toSemiMajorAxis: view.getFloat64(152, isLittleEndian),
  73. toSemiMinorAxis: view.getFloat64(168, isLittleEndian),
  74. };
  75. }
  76. function decodeString(view, start, end) {
  77. return String.fromCharCode.apply(null, new Uint8Array(view.buffer.slice(start, end)));
  78. }
  79. function readSubgrids(view, header, isLittleEndian) {
  80. var gridOffset = 176;
  81. var grids = [];
  82. for (var i = 0; i < header.nSubgrids; i++) {
  83. var subHeader = readGridHeader(view, gridOffset, isLittleEndian);
  84. var nodes = readGridNodes(view, gridOffset, subHeader, isLittleEndian);
  85. var lngColumnCount = Math.round(
  86. 1 + (subHeader.upperLongitude - subHeader.lowerLongitude) / subHeader.longitudeInterval);
  87. var latColumnCount = Math.round(
  88. 1 + (subHeader.upperLatitude - subHeader.lowerLatitude) / subHeader.latitudeInterval);
  89. // Proj4 operates on radians whereas the coordinates are in seconds in the grid
  90. grids.push({
  91. ll: [secondsToRadians(subHeader.lowerLongitude), secondsToRadians(subHeader.lowerLatitude)],
  92. del: [secondsToRadians(subHeader.longitudeInterval), secondsToRadians(subHeader.latitudeInterval)],
  93. lim: [lngColumnCount, latColumnCount],
  94. count: subHeader.gridNodeCount,
  95. cvs: mapNodes(nodes)
  96. });
  97. }
  98. return grids;
  99. }
  100. function mapNodes(nodes) {
  101. return nodes.map(function (r) {return [secondsToRadians(r.longitudeShift), secondsToRadians(r.latitudeShift)];});
  102. }
  103. function readGridHeader(view, offset, isLittleEndian) {
  104. return {
  105. name: decodeString(view, offset + 8, offset + 16).trim(),
  106. parent: decodeString(view, offset + 24, offset + 24 + 8).trim(),
  107. lowerLatitude: view.getFloat64(offset + 72, isLittleEndian),
  108. upperLatitude: view.getFloat64(offset + 88, isLittleEndian),
  109. lowerLongitude: view.getFloat64(offset + 104, isLittleEndian),
  110. upperLongitude: view.getFloat64(offset + 120, isLittleEndian),
  111. latitudeInterval: view.getFloat64(offset + 136, isLittleEndian),
  112. longitudeInterval: view.getFloat64(offset + 152, isLittleEndian),
  113. gridNodeCount: view.getInt32(offset + 168, isLittleEndian)
  114. };
  115. }
  116. function readGridNodes(view, offset, gridHeader, isLittleEndian) {
  117. var nodesOffset = offset + 176;
  118. var gridRecordLength = 16;
  119. var gridShiftRecords = [];
  120. for (var i = 0; i < gridHeader.gridNodeCount; i++) {
  121. var record = {
  122. latitudeShift: view.getFloat32(nodesOffset + i * gridRecordLength, isLittleEndian),
  123. longitudeShift: view.getFloat32(nodesOffset + i * gridRecordLength + 4, isLittleEndian),
  124. latitudeAccuracy: view.getFloat32(nodesOffset + i * gridRecordLength + 8, isLittleEndian),
  125. longitudeAccuracy: view.getFloat32(nodesOffset + i * gridRecordLength + 12, isLittleEndian),
  126. };
  127. gridShiftRecords.push(record);
  128. }
  129. return gridShiftRecords;
  130. }