Coverage Report

Created: 2026-02-14 09:00

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