Coverage Report

Created: 2025-06-09 08:44

/src/gdal/frmts/raw/noaabdataset.cpp
Line
Count
Source (jump to first uncovered line)
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  Implementation of NOAA .b format used for GEOCON / NADCON5 grids
5
 * Author:   Even Rouault <even dot rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2022, Even Rouault <even dot rouault at spatialys.com>
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "cpl_conv.h"
14
#include "cpl_string.h"
15
#include "gdal_frmts.h"
16
#include "rawdataset.h"
17
#include "ogr_srs_api.h"
18
19
#include <limits>
20
21
// Specification of the format is at "paragraph 10.2 ".b" grids (GEOCON and
22
// NADCON 5.0)" of "NOAA Technical Report NOS NGS 63" at
23
// https://geodesy.noaa.gov/library/pdfs/NOAA_TR_NOS_NGS_0063.pdf
24
25
constexpr int HEADER_SIZE = 52;
26
constexpr int FORTRAN_HEADER_SIZE = 4;
27
constexpr int FORTRAN_TRAILER_SIZE = 4;
28
29
/************************************************************************/
30
/* ==================================================================== */
31
/*                          NOAA_B_Dataset                              */
32
/* ==================================================================== */
33
/************************************************************************/
34
35
class NOAA_B_Dataset final : public RawDataset
36
{
37
    OGRSpatialReference m_oSRS{};
38
    double m_adfGeoTransform[6];
39
40
    CPL_DISALLOW_COPY_ASSIGN(NOAA_B_Dataset)
41
42
    static int IdentifyEx(GDALOpenInfo *poOpenInfo, bool &bBigEndianOut);
43
44
    CPLErr Close() override
45
509
    {
46
509
        return GDALPamDataset::Close();
47
509
    }
48
49
  public:
50
    NOAA_B_Dataset()
51
11.7k
    {
52
11.7k
        m_oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
53
54
11.7k
        m_adfGeoTransform[0] = 0.0;
55
11.7k
        m_adfGeoTransform[1] = 1.0;
56
11.7k
        m_adfGeoTransform[2] = 0.0;
57
11.7k
        m_adfGeoTransform[3] = 0.0;
58
11.7k
        m_adfGeoTransform[4] = 0.0;
59
11.7k
        m_adfGeoTransform[5] = 1.0;
60
11.7k
    }
61
62
    CPLErr GetGeoTransform(double *padfTransform) override;
63
64
    const OGRSpatialReference *GetSpatialRef() const override
65
326
    {
66
326
        return &m_oSRS;
67
326
    }
68
69
    static GDALDataset *Open(GDALOpenInfo *);
70
    static int Identify(GDALOpenInfo *);
71
};
72
73
/************************************************************************/
74
/* ==================================================================== */
75
/*                          NOAA_B_Dataset                              */
76
/* ==================================================================== */
77
/************************************************************************/
78
79
/************************************************************************/
80
/*                           GetHeaderValues()                          */
81
/************************************************************************/
82
83
static void GetHeaderValues(const GDALOpenInfo *poOpenInfo, double &dfSWLat,
84
                            double &dfSWLon, double &dfDeltaLat,
85
                            double &dfDeltaLon, int32_t &nRows, int32_t &nCols,
86
                            int32_t &iKind, bool bBigEndian)
87
181k
{
88
181k
    const auto ReadFloat64 = [bBigEndian](const GByte *&ptr)
89
725k
    {
90
725k
        double v;
91
725k
        memcpy(&v, ptr, sizeof(v));
92
725k
        ptr += sizeof(v);
93
725k
        if (bBigEndian)
94
725k
            CPL_MSBPTR64(&v);
95
384k
        else
96
725k
            CPL_LSBPTR64(&v);
97
725k
        return v;
98
725k
    };
99
100
181k
    const auto ReadInt32 = [bBigEndian](const GByte *&ptr)
101
543k
    {
102
543k
        int32_t v;
103
543k
        memcpy(&v, ptr, sizeof(v));
104
543k
        ptr += sizeof(v);
105
543k
        if (bBigEndian)
106
543k
            CPL_MSBPTR32(&v);
107
288k
        else
108
543k
            CPL_LSBPTR32(&v);
109
543k
        return v;
110
543k
    };
111
112
181k
    const GByte *ptr = poOpenInfo->pabyHeader + FORTRAN_HEADER_SIZE;
113
114
181k
    dfSWLat = ReadFloat64(ptr);
115
181k
    dfSWLon = ReadFloat64(ptr);
116
181k
    dfDeltaLat = ReadFloat64(ptr);
117
181k
    dfDeltaLon = ReadFloat64(ptr);
118
119
181k
    nRows = ReadInt32(ptr);
120
181k
    nCols = ReadInt32(ptr);
121
181k
    iKind = ReadInt32(ptr);
122
181k
}
123
124
/************************************************************************/
125
/*                              Identify()                              */
126
/************************************************************************/
127
128
int NOAA_B_Dataset::IdentifyEx(GDALOpenInfo *poOpenInfo, bool &bBigEndianOut)
129
130
871k
{
131
871k
    if (poOpenInfo->nHeaderBytes < HEADER_SIZE)
132
786k
        return FALSE;
133
134
#if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
135
    if (!poOpenInfo->IsExtensionEqualToCI("b"))
136
        return FALSE;
137
#endif
138
139
    // Sanity checks on header
140
84.9k
    double dfSWLat;
141
84.9k
    double dfSWLon;
142
84.9k
    double dfDeltaLat;
143
84.9k
    double dfDeltaLon;
144
84.9k
    int32_t nRows;
145
84.9k
    int32_t nCols;
146
84.9k
    int32_t iKind;
147
148
    // Fun... nadcon5 files are encoded in big-endian, but vertcon3 files...
149
    // in little-endian. We could probably figure that out directly from the
150
    // 4 bytes which are 0x00 0x00 0x00 0x2C for nadcon5, and the reverse for
151
    // vertcon3, but the semantics of those 4 bytes is undocumented.
152
    // So try both possibilities and rely on sanity checks.
153
230k
    for (int i = 0; i < 2; ++i)
154
169k
    {
155
169k
        const bool bBigEndian = i == 0 ? true : false;
156
169k
        GetHeaderValues(poOpenInfo, dfSWLat, dfSWLon, dfDeltaLat, dfDeltaLon,
157
169k
                        nRows, nCols, iKind, bBigEndian);
158
169k
        if (!(fabs(dfSWLat) <= 90))
159
99.9k
            continue;
160
69.4k
        if (!(fabs(dfSWLon) <=
161
69.4k
              360))  // NADCON5 grids typically have SWLon > 180
162
18.4k
            continue;
163
50.9k
        if (!(dfDeltaLat > 0 && dfDeltaLat <= 1))
164
13.0k
            continue;
165
37.9k
        if (!(dfDeltaLon > 0 && dfDeltaLon <= 1))
166
4.57k
            continue;
167
33.3k
        if (!(nRows > 0 && dfSWLat + (nRows - 1) * dfDeltaLat <= 90))
168
1.09k
            continue;
169
32.2k
        if (!(nCols > 0 && (nCols - 1) * dfDeltaLon <= 360))
170
1.27k
            continue;
171
31.0k
        if (!(iKind >= -1 && iKind <= 2))
172
7.15k
            continue;
173
174
23.8k
        bBigEndianOut = bBigEndian;
175
23.8k
        return TRUE;
176
31.0k
    }
177
61.0k
    return FALSE;
178
84.9k
}
179
180
int NOAA_B_Dataset::Identify(GDALOpenInfo *poOpenInfo)
181
182
859k
{
183
859k
    bool bBigEndian = false;
184
859k
    return IdentifyEx(poOpenInfo, bBigEndian);
185
859k
}
186
187
/************************************************************************/
188
/*                          GetGeoTransform()                           */
189
/************************************************************************/
190
191
CPLErr NOAA_B_Dataset::GetGeoTransform(double *padfTransform)
192
193
412
{
194
412
    memcpy(padfTransform, m_adfGeoTransform, sizeof(double) * 6);
195
412
    return CE_None;
196
412
}
197
198
/************************************************************************/
199
/*                                Open()                                */
200
/************************************************************************/
201
202
GDALDataset *NOAA_B_Dataset::Open(GDALOpenInfo *poOpenInfo)
203
204
11.9k
{
205
11.9k
    bool bBigEndian = false;
206
11.9k
    if (!IdentifyEx(poOpenInfo, bBigEndian) || poOpenInfo->fpL == nullptr ||
207
11.9k
        poOpenInfo->eAccess == GA_Update)
208
0
    {
209
0
        return nullptr;
210
0
    }
211
212
    /* -------------------------------------------------------------------- */
213
    /*      Read the header.                                                */
214
    /* -------------------------------------------------------------------- */
215
11.9k
    double dfSWLat;
216
11.9k
    double dfSWLon;
217
11.9k
    double dfDeltaLat;
218
11.9k
    double dfDeltaLon;
219
11.9k
    int32_t nRows;
220
11.9k
    int32_t nCols;
221
11.9k
    int32_t iKind;
222
11.9k
    GetHeaderValues(poOpenInfo, dfSWLat, dfSWLon, dfDeltaLat, dfDeltaLon, nRows,
223
11.9k
                    nCols, iKind, bBigEndian);
224
225
11.9k
    if (iKind == -1)
226
53
    {
227
53
        CPLError(CE_Failure, CPLE_NotSupported,
228
53
                 "KIND = -1 in NOAA .b dataset not supported");
229
53
        return nullptr;
230
53
    }
231
232
11.8k
    const GDALDataType eDT =
233
        // iKind == -1 ? GDT_Int16 :
234
11.8k
        iKind == 0   ? GDT_Int32
235
11.8k
        : iKind == 1 ? GDT_Float32
236
270
                     : GDT_Int16;
237
11.8k
    const int nDTSize = GDALGetDataTypeSizeBytes(eDT);
238
11.8k
    if (!GDALCheckDatasetDimensions(nCols, nRows) ||
239
11.8k
        (nDTSize > 0 && static_cast<vsi_l_offset>(nCols) * nRows >
240
11.8k
                            std::numeric_limits<vsi_l_offset>::max() / nDTSize))
241
0
    {
242
0
        return nullptr;
243
0
    }
244
11.8k
    if (nDTSize > 0 && nCols > (std::numeric_limits<int>::max() -
245
11.8k
                                FORTRAN_HEADER_SIZE - FORTRAN_TRAILER_SIZE) /
246
11.8k
                                   nDTSize)
247
142
    {
248
142
        return nullptr;
249
142
    }
250
11.7k
    const int nLineSize =
251
11.7k
        FORTRAN_HEADER_SIZE + nCols * nDTSize + FORTRAN_TRAILER_SIZE;
252
253
    /* -------------------------------------------------------------------- */
254
    /*      Create a corresponding GDALDataset.                             */
255
    /* -------------------------------------------------------------------- */
256
11.7k
    auto poDS = std::make_unique<NOAA_B_Dataset>();
257
258
11.7k
    poDS->nRasterXSize = nCols;
259
11.7k
    poDS->nRasterYSize = nRows;
260
261
    // Adjust longitude > 180 to [-180, 180] range
262
11.7k
    if (dfSWLon > 180)
263
0
        dfSWLon -= 360;
264
265
    // Convert from south-west center-of-pixel convention to
266
    // north-east pixel-corner convention
267
11.7k
    poDS->m_adfGeoTransform[0] = dfSWLon - dfDeltaLon / 2;
268
11.7k
    poDS->m_adfGeoTransform[1] = dfDeltaLon;
269
11.7k
    poDS->m_adfGeoTransform[2] = 0.0;
270
11.7k
    poDS->m_adfGeoTransform[3] =
271
11.7k
        dfSWLat + (nRows - 1) * dfDeltaLat + dfDeltaLat / 2;
272
11.7k
    poDS->m_adfGeoTransform[4] = 0.0;
273
11.7k
    poDS->m_adfGeoTransform[5] = -dfDeltaLat;
274
275
    /* -------------------------------------------------------------------- */
276
    /*      Create band information object.                                 */
277
    /* -------------------------------------------------------------------- */
278
279
    // Borrow file handle
280
11.7k
    VSILFILE *fpImage = poOpenInfo->fpL;
281
11.7k
    poOpenInfo->fpL = nullptr;
282
283
    // Records are presented from the southern-most to the northern-most
284
11.7k
    auto poBand = RawRasterBand::Create(
285
11.7k
        poDS.get(), 1, fpImage,
286
        // skip to beginning of northern-most line
287
11.7k
        HEADER_SIZE +
288
11.7k
            static_cast<vsi_l_offset>(poDS->nRasterYSize - 1) * nLineSize +
289
11.7k
            FORTRAN_HEADER_SIZE,
290
11.7k
        nDTSize, -nLineSize, eDT,
291
11.7k
        bBigEndian ? RawRasterBand::ByteOrder::ORDER_BIG_ENDIAN
292
11.7k
                   : RawRasterBand::ByteOrder::ORDER_LITTLE_ENDIAN,
293
11.7k
        RawRasterBand::OwnFP::YES);
294
11.7k
    if (!poBand)
295
0
        return nullptr;
296
11.7k
    poDS->SetBand(1, std::move(poBand));
297
298
    /* -------------------------------------------------------------------- */
299
    /*      Guess CRS from filename.                                        */
300
    /* -------------------------------------------------------------------- */
301
11.7k
    const std::string osFilename(CPLGetFilename(poOpenInfo->pszFilename));
302
303
11.7k
    static const struct
304
11.7k
    {
305
11.7k
        const char *pszPrefix;
306
11.7k
        int nEPSGCode;
307
11.7k
    }
308
    // Cf https://geodesy.noaa.gov/pub/nadcon5/20160901release/Builds/
309
11.7k
    asFilenameToCRS[] = {
310
11.7k
        {"nadcon5.nad27.", 4267},       // NAD27
311
11.7k
        {"nadcon5.pr40.", 4139},        // Puerto Rico (1940)
312
11.7k
        {"nadcon5.ohd.", 4135},         // Old Hawaian
313
11.7k
        {"nadcon5.sl1952.", 4136},      // Saint Lawrence Island (1952)
314
11.7k
        {"nadcon5.sp1952.", 4137},      // Saint Paul Island (1952)
315
11.7k
        {"nadcon5.sg1952.", 4138},      // Saint George Island (1952)
316
11.7k
        {"nadcon5.as62.", 4169},        // American Samoa 1962
317
11.7k
        {"nadcon5.gu63.", 4675},        // Guam 1963
318
11.7k
        {"nadcon5.nad83_1986.", 4269},  // NAD83
319
11.7k
        {"nadcon5.nad83_harn.", 4152},  // NAD83(HARN)
320
11.7k
        {"nadcon5.nad83_1992.",
321
11.7k
         4152},  // NAD83(1992) for Alaska is NAD83(HARN) in EPSG
322
11.7k
        {"nadcon5.nad83_1993.",
323
11.7k
         4152},  // NAD83(1993) for American Samoa, PRVI, Guam and Hawaii is
324
                 // NAD83(HARN) in EPSG
325
11.7k
        {"nadcon5.nad83_1997.", 8545},  // NAD83(HARN Corrected)
326
11.7k
        {"nadcon5.nad83_fbn.", 8860},   // NAD83(FBN)
327
11.7k
        {"nadcon5.nad83_2002.",
328
11.7k
         8860},  // NAD83(2002) for Alaska, PRVI and Guam is NAD83(FBN) in EPSG
329
11.7k
        {"nadcon5.nad83_2007.", 4759},  // NAD83(NSRS2007)
330
11.7k
    };
331
332
11.7k
    for (const auto &sPair : asFilenameToCRS)
333
187k
    {
334
187k
        if (STARTS_WITH_CI(osFilename.c_str(), sPair.pszPrefix))
335
14
        {
336
14
            poDS->m_oSRS.importFromEPSG(sPair.nEPSGCode);
337
14
            break;
338
14
        }
339
187k
    }
340
11.7k
    if (poDS->m_oSRS.IsEmpty())
341
11.7k
    {
342
11.7k
        poDS->m_oSRS.importFromWkt(
343
11.7k
            "GEOGCRS[\"Unspecified geographic CRS\",DATUM[\"Unspecified datum "
344
11.7k
            "based on GRS80 ellipsoid\",ELLIPSOID[\"GRS "
345
11.7k
            "1980\",6378137,298.257222101]],CS[ellipsoidal,2],AXIS[\"geodetic "
346
11.7k
            "latitude (Lat)\",north,ANGLEUNIT[\"degree\",0.0174532925199433]], "
347
11.7k
            "       AXIS[\"geodetic longitude "
348
11.7k
            "(Lon)\",east,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433]]]");
349
11.7k
    }
350
351
    /* -------------------------------------------------------------------- */
352
    /*      Initialize any PAM information.                                 */
353
    /* -------------------------------------------------------------------- */
354
11.7k
    poDS->SetDescription(poOpenInfo->pszFilename);
355
11.7k
    poDS->TryLoadXML();
356
357
    /* -------------------------------------------------------------------- */
358
    /*      Check for overviews.                                            */
359
    /* -------------------------------------------------------------------- */
360
11.7k
    poDS->oOvManager.Initialize(poDS.get(), poOpenInfo->pszFilename);
361
362
11.7k
    return poDS.release();
363
11.7k
}
364
365
/************************************************************************/
366
/*                        GDALRegister_NOAA_B()                         */
367
/************************************************************************/
368
369
void GDALRegister_NOAA_B()
370
24
{
371
24
    if (GDALGetDriverByName("NOAA_B") != nullptr)
372
0
        return;
373
374
24
    GDALDriver *poDriver = new GDALDriver();
375
376
24
    poDriver->SetDescription("NOAA_B");
377
24
    poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
378
24
    poDriver->SetMetadataItem(GDAL_DMD_LONGNAME,
379
24
                              "NOAA GEOCON/NADCON5 .b format");
380
24
    poDriver->SetMetadataItem(GDAL_DMD_EXTENSION, "b");
381
24
    poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
382
24
    poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/raster/noaa_b.html");
383
384
24
    poDriver->pfnIdentify = NOAA_B_Dataset::Identify;
385
24
    poDriver->pfnOpen = NOAA_B_Dataset::Open;
386
387
24
    GetGDALDriverManager()->RegisterDriver(poDriver);
388
24
}