Coverage Report

Created: 2025-06-09 07:43

/src/gdal/frmts/raw/ntv2dataset.cpp
Line
Count
Source (jump to first uncovered line)
1
/******************************************************************************
2
 *
3
 * Project:  Horizontal Datum Formats
4
 * Purpose:  Implementation of NTv2 datum shift format used in Canada, France,
5
 *           Australia and elsewhere.
6
 * Author:   Frank Warmerdam, warmerdam@pobox.com
7
 * Financial Support: i-cubed (http://www.i-cubed.com)
8
 *
9
 ******************************************************************************
10
 * Copyright (c) 2010, Frank Warmerdam
11
 * Copyright (c) 2010-2012, Even Rouault <even dot rouault at spatialys.com>
12
 *
13
 * SPDX-License-Identifier: MIT
14
 ****************************************************************************/
15
16
// TODO(schwehr): There are a lot of magic numbers in this driver that should
17
// be changed to constants and documented.
18
19
#include "cpl_string.h"
20
#include "gdal_frmts.h"
21
#include "ogr_srs_api.h"
22
#include "rawdataset.h"
23
24
#include <algorithm>
25
26
// Format documentation: https://github.com/Esri/ntv2-file-routines
27
// Original archived specification:
28
// https://web.archive.org/web/20091227232322/http://www.mgs.gov.on.ca/stdprodconsume/groups/content/@mgs/@iandit/documents/resourcelist/stel02_047447.pdf
29
30
/**
31
 * The header for the file, and each grid consists of 11 16byte records.
32
 * The first half is an ASCII label, and the second half is the value
33
 * often in a little endian int or float.
34
 *
35
 * Example:
36
37
00000000  4e 55 4d 5f 4f 52 45 43  0b 00 00 00 00 00 00 00  |NUM_OREC........|
38
00000010  4e 55 4d 5f 53 52 45 43  0b 00 00 00 00 00 00 00  |NUM_SREC........|
39
00000020  4e 55 4d 5f 46 49 4c 45  01 00 00 00 00 00 00 00  |NUM_FILE........|
40
00000030  47 53 5f 54 59 50 45 20  53 45 43 4f 4e 44 53 20  |GS_TYPE SECONDS |
41
00000040  56 45 52 53 49 4f 4e 20  49 47 4e 30 37 5f 30 31  |VERSION IGN07_01|
42
00000050  53 59 53 54 45 4d 5f 46  4e 54 46 20 20 20 20 20  |SYSTEM_FNTF     |
43
00000060  53 59 53 54 45 4d 5f 54  52 47 46 39 33 20 20 20  |SYSTEM_TRGF93   |
44
00000070  4d 41 4a 4f 52 5f 46 20  cd cc cc 4c c2 54 58 41  |MAJOR_F ...L.TXA|
45
00000080  4d 49 4e 4f 52 5f 46 20  00 00 00 c0 88 3f 58 41  |MINOR_F .....?XA|
46
00000090  4d 41 4a 4f 52 5f 54 20  00 00 00 40 a6 54 58 41  |MAJOR_T ...@.TXA|
47
000000a0  4d 49 4e 4f 52 5f 54 20  27 e0 1a 14 c4 3f 58 41  |MINOR_T '....?XA|
48
000000b0  53 55 42 5f 4e 41 4d 45  46 52 41 4e 43 45 20 20  |SUB_NAMEFRANCE  |
49
000000c0  50 41 52 45 4e 54 20 20  4e 4f 4e 45 20 20 20 20  |PARENT  NONE    |
50
000000d0  43 52 45 41 54 45 44 20  33 31 2f 31 30 2f 30 37  |CREATED 31/10/07|
51
000000e0  55 50 44 41 54 45 44 20  20 20 20 20 20 20 20 20  |UPDATED         |
52
000000f0  53 5f 4c 41 54 20 20 20  00 00 00 00 80 04 02 41  |S_LAT   .......A|
53
00000100  4e 5f 4c 41 54 20 20 20  00 00 00 00 00 da 06 41  |N_LAT   .......A|
54
00000110  45 5f 4c 4f 4e 47 20 20  00 00 00 00 00 94 e1 c0  |E_LONG  ........|
55
00000120  57 5f 4c 4f 4e 47 20 20  00 00 00 00 00 56 d3 40  |W_LONG  .....V.@|
56
00000130  4c 41 54 5f 49 4e 43 20  00 00 00 00 00 80 76 40  |LAT_INC ......v@|
57
00000140  4c 4f 4e 47 5f 49 4e 43  00 00 00 00 00 80 76 40  |LONG_INC......v@|
58
00000150  47 53 5f 43 4f 55 4e 54  a4 43 00 00 00 00 00 00  |GS_COUNT.C......|
59
00000160  94 f7 c1 3e 70 ee a3 3f  2a c7 84 3d ff 42 af 3d  |...>p..?*..=.B.=|
60
61
the actual grid data is a raster with 4 float32 bands (lat offset, long
62
offset, lat error, long error).  The offset values are in arc seconds.
63
The grid is flipped in the x and y axis from our usual GDAL orientation.
64
That is, the first pixel is the south east corner with scanlines going
65
east to west, and rows from south to north.  As a GDAL dataset we represent
66
these both in the more conventional orientation.
67
 */
68
69
constexpr size_t knREGULAR_RECORD_SIZE = 16;
70
// This one is for velocity grids such as the NAD83(CRSR)v7 / NAD83v70VG.gvb
71
// which is the only example I know actually of that format variant.
72
constexpr size_t knMAX_RECORD_SIZE = 24;
73
74
/************************************************************************/
75
/* ==================================================================== */
76
/*                              NTv2Dataset                             */
77
/* ==================================================================== */
78
/************************************************************************/
79
80
class NTv2Dataset final : public RawDataset
81
{
82
  public:
83
    RawRasterBand::ByteOrder m_eByteOrder = RawRasterBand::NATIVE_BYTE_ORDER;
84
    bool m_bMustSwap = false;
85
    VSILFILE *fpImage = nullptr;  // image data file.
86
87
    size_t nRecordSize = 0;
88
    vsi_l_offset nGridOffset = 0;
89
90
    OGRSpatialReference m_oSRS{};
91
    double adfGeoTransform[6];
92
93
    void CaptureMetadataItem(const char *pszItem);
94
95
    bool OpenGrid(const char *pachGridHeader, vsi_l_offset nDataStart);
96
97
    CPL_DISALLOW_COPY_ASSIGN(NTv2Dataset)
98
99
    CPLErr Close() override;
100
101
  public:
102
    NTv2Dataset();
103
    ~NTv2Dataset() override;
104
105
    CPLErr GetGeoTransform(double *padfTransform) override;
106
107
    const OGRSpatialReference *GetSpatialRef() const override
108
0
    {
109
0
        return &m_oSRS;
110
0
    }
111
112
    static GDALDataset *Open(GDALOpenInfo *);
113
    static int Identify(GDALOpenInfo *);
114
};
115
116
/************************************************************************/
117
/* ==================================================================== */
118
/*                              NTv2Dataset                             */
119
/* ==================================================================== */
120
/************************************************************************/
121
122
/************************************************************************/
123
/*                             NTv2Dataset()                          */
124
/************************************************************************/
125
126
NTv2Dataset::NTv2Dataset()
127
0
{
128
0
    m_oSRS.SetFromUserInput(SRS_WKT_WGS84_LAT_LONG);
129
0
    m_oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
130
131
0
    adfGeoTransform[0] = 0.0;
132
0
    adfGeoTransform[1] = 0.0;  // TODO(schwehr): Should this be 1.0?
133
0
    adfGeoTransform[2] = 0.0;
134
0
    adfGeoTransform[3] = 0.0;
135
0
    adfGeoTransform[4] = 0.0;
136
0
    adfGeoTransform[5] = 0.0;  // TODO(schwehr): Should this be 1.0?
137
0
}
138
139
/************************************************************************/
140
/*                            ~NTv2Dataset()                            */
141
/************************************************************************/
142
143
NTv2Dataset::~NTv2Dataset()
144
145
0
{
146
0
    NTv2Dataset::Close();
147
0
}
148
149
/************************************************************************/
150
/*                              Close()                                 */
151
/************************************************************************/
152
153
CPLErr NTv2Dataset::Close()
154
0
{
155
0
    CPLErr eErr = CE_None;
156
0
    if (nOpenFlags != OPEN_FLAGS_CLOSED)
157
0
    {
158
0
        if (fpImage)
159
0
        {
160
0
            if (VSIFCloseL(fpImage) != 0)
161
0
            {
162
0
                CPLError(CE_Failure, CPLE_FileIO, "I/O error");
163
0
                eErr = CE_Failure;
164
0
            }
165
0
        }
166
167
0
        if (GDALPamDataset::Close() != CE_None)
168
0
            eErr = CE_Failure;
169
0
    }
170
0
    return eErr;
171
0
}
172
173
/************************************************************************/
174
/*                        SwapPtr32IfNecessary()                        */
175
/************************************************************************/
176
177
static void SwapPtr32IfNecessary(bool bMustSwap, void *ptr)
178
0
{
179
0
    if (bMustSwap)
180
0
    {
181
0
        CPL_SWAP32PTR(static_cast<GByte *>(ptr));
182
0
    }
183
0
}
184
185
/************************************************************************/
186
/*                        SwapPtr64IfNecessary()                        */
187
/************************************************************************/
188
189
static void SwapPtr64IfNecessary(bool bMustSwap, void *ptr)
190
0
{
191
0
    if (bMustSwap)
192
0
    {
193
0
        CPL_SWAP64PTR(static_cast<GByte *>(ptr));
194
0
    }
195
0
}
196
197
/************************************************************************/
198
/*                              Identify()                              */
199
/************************************************************************/
200
201
int NTv2Dataset::Identify(GDALOpenInfo *poOpenInfo)
202
203
177k
{
204
177k
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "NTv2:"))
205
0
        return TRUE;
206
207
177k
    if (poOpenInfo->nHeaderBytes < 64)
208
173k
        return FALSE;
209
210
3.49k
    const char *pszHeader =
211
3.49k
        reinterpret_cast<const char *>(poOpenInfo->pabyHeader);
212
3.49k
    if (!STARTS_WITH_CI(pszHeader + 0, "NUM_OREC"))
213
3.49k
        return FALSE;
214
215
1
    if (!STARTS_WITH_CI(pszHeader + knREGULAR_RECORD_SIZE, "NUM_SREC") &&
216
1
        !STARTS_WITH_CI(pszHeader + knMAX_RECORD_SIZE, "NUM_SREC"))
217
1
        return FALSE;
218
219
0
    return TRUE;
220
1
}
221
222
/************************************************************************/
223
/*                                Open()                                */
224
/************************************************************************/
225
226
GDALDataset *NTv2Dataset::Open(GDALOpenInfo *poOpenInfo)
227
228
0
{
229
0
    if (!Identify(poOpenInfo) || poOpenInfo->eAccess == GA_Update)
230
0
        return nullptr;
231
232
    /* -------------------------------------------------------------------- */
233
    /*      Are we targeting a particular grid?                             */
234
    /* -------------------------------------------------------------------- */
235
0
    CPLString osFilename;
236
0
    int iTargetGrid = -1;
237
238
0
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "NTv2:"))
239
0
    {
240
0
        const char *pszRest = poOpenInfo->pszFilename + 5;
241
242
0
        iTargetGrid = atoi(pszRest);
243
0
        while (*pszRest != '\0' && *pszRest != ':')
244
0
            pszRest++;
245
246
0
        if (*pszRest == ':')
247
0
            pszRest++;
248
249
0
        osFilename = pszRest;
250
0
    }
251
0
    else
252
0
    {
253
0
        osFilename = poOpenInfo->pszFilename;
254
0
    }
255
256
    /* -------------------------------------------------------------------- */
257
    /*      Create a corresponding GDALDataset.                             */
258
    /* -------------------------------------------------------------------- */
259
0
    auto poDS = std::make_unique<NTv2Dataset>();
260
0
    poDS->eAccess = poOpenInfo->eAccess;
261
262
    /* -------------------------------------------------------------------- */
263
    /*      Open the file.                                                  */
264
    /* -------------------------------------------------------------------- */
265
0
    if (poOpenInfo->eAccess == GA_ReadOnly)
266
0
        poDS->fpImage = VSIFOpenL(osFilename, "rb");
267
0
    else
268
0
        poDS->fpImage = VSIFOpenL(osFilename, "rb+");
269
270
0
    if (poDS->fpImage == nullptr)
271
0
    {
272
0
        return nullptr;
273
0
    }
274
275
    /* -------------------------------------------------------------------- */
276
    /*      Read the file header.                                           */
277
    /* -------------------------------------------------------------------- */
278
0
    char achHeader[11 * knMAX_RECORD_SIZE] = {0};
279
280
0
    if (VSIFSeekL(poDS->fpImage, 0, SEEK_SET) != 0 ||
281
0
        VSIFReadL(achHeader, 1, 64, poDS->fpImage) != 64)
282
0
    {
283
0
        return nullptr;
284
0
    }
285
286
0
    poDS->nRecordSize =
287
0
        STARTS_WITH_CI(achHeader + knMAX_RECORD_SIZE, "NUM_SREC")
288
0
            ? knMAX_RECORD_SIZE
289
0
            : knREGULAR_RECORD_SIZE;
290
0
    if (VSIFReadL(achHeader + 64, 1, 11 * poDS->nRecordSize - 64,
291
0
                  poDS->fpImage) != 11 * poDS->nRecordSize - 64)
292
0
    {
293
0
        return nullptr;
294
0
    }
295
296
0
    const bool bIsLE = achHeader[8] == 11 && achHeader[9] == 0 &&
297
0
                       achHeader[10] == 0 && achHeader[11] == 0;
298
0
    const bool bIsBE = achHeader[8] == 0 && achHeader[9] == 0 &&
299
0
                       achHeader[10] == 0 && achHeader[11] == 11;
300
0
    if (!bIsLE && !bIsBE)
301
0
    {
302
0
        return nullptr;
303
0
    }
304
0
    poDS->m_eByteOrder = bIsLE ? RawRasterBand::ByteOrder::ORDER_LITTLE_ENDIAN
305
0
                               : RawRasterBand::ByteOrder::ORDER_BIG_ENDIAN;
306
0
    poDS->m_bMustSwap = poDS->m_eByteOrder != RawRasterBand::NATIVE_BYTE_ORDER;
307
308
0
    SwapPtr32IfNecessary(poDS->m_bMustSwap,
309
0
                         achHeader + 2 * poDS->nRecordSize + 8);
310
0
    GInt32 nSubFileCount = 0;
311
0
    memcpy(&nSubFileCount, achHeader + 2 * poDS->nRecordSize + 8, 4);
312
0
    if (nSubFileCount <= 0 || nSubFileCount >= 1024)
313
0
    {
314
0
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid value for NUM_FILE : %d",
315
0
                 nSubFileCount);
316
0
        return nullptr;
317
0
    }
318
319
0
    poDS->CaptureMetadataItem(achHeader + 3 * poDS->nRecordSize);
320
0
    poDS->CaptureMetadataItem(achHeader + 4 * poDS->nRecordSize);
321
0
    poDS->CaptureMetadataItem(achHeader + 5 * poDS->nRecordSize);
322
0
    poDS->CaptureMetadataItem(achHeader + 6 * poDS->nRecordSize);
323
324
0
    double dfValue = 0.0;
325
0
    memcpy(&dfValue, achHeader + 7 * poDS->nRecordSize + 8, 8);
326
0
    SwapPtr64IfNecessary(poDS->m_bMustSwap, &dfValue);
327
0
    CPLString osFValue;
328
0
    osFValue.Printf("%.15g", dfValue);
329
0
    poDS->SetMetadataItem("MAJOR_F", osFValue);
330
331
0
    memcpy(&dfValue, achHeader + 8 * poDS->nRecordSize + 8, 8);
332
0
    SwapPtr64IfNecessary(poDS->m_bMustSwap, &dfValue);
333
0
    osFValue.Printf("%.15g", dfValue);
334
0
    poDS->SetMetadataItem("MINOR_F", osFValue);
335
336
0
    memcpy(&dfValue, achHeader + 9 * poDS->nRecordSize + 8, 8);
337
0
    SwapPtr64IfNecessary(poDS->m_bMustSwap, &dfValue);
338
0
    osFValue.Printf("%.15g", dfValue);
339
0
    poDS->SetMetadataItem("MAJOR_T", osFValue);
340
341
0
    memcpy(&dfValue, achHeader + 10 * poDS->nRecordSize + 8, 8);
342
0
    SwapPtr64IfNecessary(poDS->m_bMustSwap, &dfValue);
343
0
    osFValue.Printf("%.15g", dfValue);
344
0
    poDS->SetMetadataItem("MINOR_T", osFValue);
345
346
    /* ==================================================================== */
347
    /*      Loop over grids.                                                */
348
    /* ==================================================================== */
349
0
    vsi_l_offset nGridOffset = 11 * poDS->nRecordSize;
350
351
0
    for (int iGrid = 0; iGrid < nSubFileCount; iGrid++)
352
0
    {
353
0
        if (VSIFSeekL(poDS->fpImage, nGridOffset, SEEK_SET) < 0 ||
354
0
            VSIFReadL(achHeader, 11, poDS->nRecordSize, poDS->fpImage) !=
355
0
                poDS->nRecordSize)
356
0
        {
357
0
            CPLError(CE_Failure, CPLE_AppDefined,
358
0
                     "Cannot read header for subfile %d", iGrid);
359
0
            return nullptr;
360
0
        }
361
362
0
        for (int i = 4; i <= 9; i++)
363
0
            SwapPtr64IfNecessary(poDS->m_bMustSwap,
364
0
                                 achHeader + i * poDS->nRecordSize + 8);
365
366
0
        SwapPtr32IfNecessary(poDS->m_bMustSwap,
367
0
                             achHeader + 10 * poDS->nRecordSize + 8);
368
369
0
        GUInt32 nGSCount = 0;
370
0
        memcpy(&nGSCount, achHeader + 10 * poDS->nRecordSize + 8, 4);
371
372
0
        CPLString osSubName;
373
0
        osSubName.assign(achHeader + 8, 8);
374
0
        osSubName.Trim();
375
376
        // If this is our target grid, open it as a dataset.
377
0
        if (iTargetGrid == iGrid || (iTargetGrid == -1 && iGrid == 0))
378
0
        {
379
0
            if (!poDS->OpenGrid(achHeader, nGridOffset))
380
0
            {
381
0
                return nullptr;
382
0
            }
383
0
        }
384
385
        // If we are opening the file as a whole, list subdatasets.
386
0
        if (iTargetGrid == -1)
387
0
        {
388
0
            CPLString osKey;
389
0
            CPLString osValue;
390
0
            osKey.Printf("SUBDATASET_%d_NAME", iGrid);
391
0
            osValue.Printf("NTv2:%d:%s", iGrid, osFilename.c_str());
392
0
            poDS->SetMetadataItem(osKey, osValue, "SUBDATASETS");
393
394
0
            osKey.Printf("SUBDATASET_%d_DESC", iGrid);
395
0
            osValue.Printf("%s", osSubName.c_str());
396
0
            poDS->SetMetadataItem(osKey, osValue, "SUBDATASETS");
397
0
        }
398
399
0
        nGridOffset +=
400
0
            (11 + static_cast<vsi_l_offset>(nGSCount)) * poDS->nRecordSize;
401
0
    }
402
403
    /* -------------------------------------------------------------------- */
404
    /*      Initialize any PAM information.                                 */
405
    /* -------------------------------------------------------------------- */
406
0
    poDS->SetDescription(poOpenInfo->pszFilename);
407
0
    poDS->TryLoadXML();
408
409
    /* -------------------------------------------------------------------- */
410
    /*      Check for overviews.                                            */
411
    /* -------------------------------------------------------------------- */
412
0
    poDS->oOvManager.Initialize(poDS.get(), poOpenInfo->pszFilename);
413
414
0
    return poDS.release();
415
0
}
416
417
/************************************************************************/
418
/*                              OpenGrid()                              */
419
/*                                                                      */
420
/*      Note that the caller will already have byte swapped needed      */
421
/*      portions of the header.                                         */
422
/************************************************************************/
423
424
bool NTv2Dataset::OpenGrid(const char *pachHeader, vsi_l_offset nGridOffsetIn)
425
426
0
{
427
0
    nGridOffset = nGridOffsetIn;
428
429
    /* -------------------------------------------------------------------- */
430
    /*      Read the grid header.                                           */
431
    /* -------------------------------------------------------------------- */
432
0
    CaptureMetadataItem(pachHeader + 0 * nRecordSize);
433
0
    CaptureMetadataItem(pachHeader + 1 * nRecordSize);
434
0
    CaptureMetadataItem(pachHeader + 2 * nRecordSize);
435
0
    CaptureMetadataItem(pachHeader + 3 * nRecordSize);
436
437
0
    double s_lat, n_lat, e_long, w_long, lat_inc, long_inc;
438
0
    memcpy(&s_lat, pachHeader + 4 * nRecordSize + 8, 8);
439
0
    memcpy(&n_lat, pachHeader + 5 * nRecordSize + 8, 8);
440
0
    memcpy(&e_long, pachHeader + 6 * nRecordSize + 8, 8);
441
0
    memcpy(&w_long, pachHeader + 7 * nRecordSize + 8, 8);
442
0
    memcpy(&lat_inc, pachHeader + 8 * nRecordSize + 8, 8);
443
0
    memcpy(&long_inc, pachHeader + 9 * nRecordSize + 8, 8);
444
445
0
    e_long *= -1;
446
0
    w_long *= -1;
447
448
0
    if (long_inc == 0.0 || lat_inc == 0.0)
449
0
        return false;
450
0
    const double dfXSize = floor((e_long - w_long) / long_inc + 1.5);
451
0
    const double dfYSize = floor((n_lat - s_lat) / lat_inc + 1.5);
452
0
    if (!(dfXSize >= 0 && dfXSize < INT_MAX) ||
453
0
        !(dfYSize >= 0 && dfYSize < INT_MAX))
454
0
        return false;
455
0
    nRasterXSize = static_cast<int>(dfXSize);
456
0
    nRasterYSize = static_cast<int>(dfYSize);
457
458
0
    const int l_nBands = nRecordSize == knREGULAR_RECORD_SIZE ? 4 : 6;
459
0
    const int nPixelSize = l_nBands * 4;
460
461
0
    if (!GDALCheckDatasetDimensions(nRasterXSize, nRasterYSize))
462
0
        return false;
463
0
    if (nRasterXSize > INT_MAX / nPixelSize)
464
0
        return false;
465
466
    /* -------------------------------------------------------------------- */
467
    /*      Create band information object.                                 */
468
    /*                                                                      */
469
    /*      We use unusual offsets to remap from bottom to top, to top      */
470
    /*      to bottom orientation, and also to remap east to west, to       */
471
    /*      west to east.                                                   */
472
    /* -------------------------------------------------------------------- */
473
0
    for (int iBand = 0; iBand < l_nBands; iBand++)
474
0
    {
475
0
        auto poBand = RawRasterBand::Create(
476
0
            this, iBand + 1, fpImage,
477
0
            nGridOffset + 4 * iBand + 11 * nRecordSize +
478
0
                static_cast<vsi_l_offset>(nRasterXSize - 1) * nPixelSize +
479
0
                static_cast<vsi_l_offset>(nRasterYSize - 1) * nPixelSize *
480
0
                    nRasterXSize,
481
0
            -nPixelSize, -nPixelSize * nRasterXSize, GDT_Float32, m_eByteOrder,
482
0
            RawRasterBand::OwnFP::NO);
483
0
        if (!poBand)
484
0
            return false;
485
0
        SetBand(iBand + 1, std::move(poBand));
486
0
    }
487
488
0
    if (l_nBands == 4)
489
0
    {
490
0
        GetRasterBand(1)->SetDescription("Latitude Offset (arc seconds)");
491
0
        GetRasterBand(2)->SetDescription("Longitude Offset (arc seconds)");
492
0
        GetRasterBand(2)->SetMetadataItem("positive_value", "west");
493
0
        GetRasterBand(3)->SetDescription("Latitude Error");
494
0
        GetRasterBand(4)->SetDescription("Longitude Error");
495
0
    }
496
0
    else
497
0
    {
498
        // A bit surprising that the order is easting, northing here, contrary
499
        // to the classic NTv2 order.... Verified on NAD83v70VG.gvb
500
        // (https://webapp.geod.nrcan.gc.ca/geod/process/download-helper.php?file_id=NAD83v70VG)
501
        // against the TRX software
502
        // (https://webapp.geod.nrcan.gc.ca/geod/process/download-helper.php?file_id=trx)
503
        // https://webapp.geod.nrcan.gc.ca/geod/tools-outils/nad83-docs.php
504
        // Unfortunately I couldn't find an official documentation of the format
505
        // !
506
0
        GetRasterBand(1)->SetDescription("East velocity (mm/year)");
507
0
        GetRasterBand(2)->SetDescription("North velocity (mm/year)");
508
0
        GetRasterBand(3)->SetDescription("Up velocity (mm/year)");
509
0
        GetRasterBand(4)->SetDescription("East velocity Error (mm/year)");
510
0
        GetRasterBand(5)->SetDescription("North velocity Error (mm/year)");
511
0
        GetRasterBand(6)->SetDescription("Up velocity Error (mm/year)");
512
0
    }
513
514
    /* -------------------------------------------------------------------- */
515
    /*      Setup georeferencing.                                           */
516
    /* -------------------------------------------------------------------- */
517
0
    adfGeoTransform[0] = (w_long - long_inc * 0.5) / 3600.0;
518
0
    adfGeoTransform[1] = long_inc / 3600.0;
519
0
    adfGeoTransform[2] = 0.0;
520
0
    adfGeoTransform[3] = (n_lat + lat_inc * 0.5) / 3600.0;
521
0
    adfGeoTransform[4] = 0.0;
522
0
    adfGeoTransform[5] = (-1 * lat_inc) / 3600.0;
523
524
0
    return true;
525
0
}
526
527
/************************************************************************/
528
/*                        CaptureMetadataItem()                         */
529
/************************************************************************/
530
531
void NTv2Dataset::CaptureMetadataItem(const char *pszItem)
532
533
0
{
534
0
    CPLString osKey;
535
0
    CPLString osValue;
536
537
0
    osKey.assign(pszItem, 8);
538
0
    osValue.assign(pszItem + 8, 8);
539
540
0
    SetMetadataItem(osKey.Trim(), osValue.Trim());
541
0
}
542
543
/************************************************************************/
544
/*                          GetGeoTransform()                           */
545
/************************************************************************/
546
547
CPLErr NTv2Dataset::GetGeoTransform(double *padfTransform)
548
549
0
{
550
0
    memcpy(padfTransform, adfGeoTransform, sizeof(double) * 6);
551
0
    return CE_None;
552
0
}
553
554
/************************************************************************/
555
/*                         GDALRegister_NTv2()                          */
556
/************************************************************************/
557
558
void GDALRegister_NTv2()
559
560
2
{
561
2
    if (GDALGetDriverByName("NTv2") != nullptr)
562
0
        return;
563
564
2
    GDALDriver *poDriver = new GDALDriver();
565
566
2
    poDriver->SetDescription("NTv2");
567
2
    poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
568
2
    poDriver->SetMetadataItem(GDAL_DMD_LONGNAME, "NTv2 Datum Grid Shift");
569
2
    poDriver->SetMetadataItem(GDAL_DMD_EXTENSIONS, "gsb gvb");
570
2
    poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
571
2
    poDriver->SetMetadataItem(GDAL_DMD_SUBDATASETS, "YES");
572
573
2
    poDriver->pfnOpen = NTv2Dataset::Open;
574
2
    poDriver->pfnIdentify = NTv2Dataset::Identify;
575
576
2
    GetGDALDriverManager()->RegisterDriver(poDriver);
577
2
}