Coverage Report

Created: 2026-06-30 08:33

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/frmts/raw/nsidcbindataset.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  Implementation for NSIDC binary format.
5
 * Author:   Michael Sumner, mdsumner@gmail.com
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2022, Michael Sumner
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "cpl_string.h"
14
#include "gdal_frmts.h"
15
#include "gdal_priv.h"
16
#include "ogr_spatialref.h"
17
#include "rawdataset.h"
18
19
#include <cmath>
20
#include <algorithm>
21
22
/***********************************************************************/
23
/* ====================================================================*/
24
/*                              NSIDCbinDataset                        */
25
/* ====================================================================*/
26
/***********************************************************************/
27
28
class NSIDCbinDataset final : public GDALPamDataset
29
{
30
    friend class NSIDCbinRasterBand;
31
32
    struct NSIDCbinHeader
33
    {
34
35
        // page 7, User Guide https://nsidc.org/data/nsidc-0051
36
        // 1.3.2 File Contents
37
        // The file format consists of a 300-byte descriptive header followed by
38
        // a two-dimensional array of one-byte values containing the data. The
39
        // file header is composed of:
40
        // - a 21-element array of 6-byte character strings that contain
41
        // information such as polar stereographic grid characteristics
42
        // - a 24-byte character string containing the file name
43
        // - a 80-character string containing an optional image title
44
        // - a 70-byte character string containing ancillary information such as
45
        // data origin, data set creation date, etc. For compatibility with ANSI
46
        // C, IDL, and other languages, character strings are terminated with a
47
        // NULL byte.
48
        // Example file:
49
        // ftp://sidads.colorado.edu/pub/DATASETS/nsidc0051_gsfc_nasateam_seaice/final-gsfc/south/daily/2010/nt_20100918_f17_v1.1_s.bin
50
51
        char missing_int[6] = {0};       // "00255"
52
        char columns[6] = {0};           // "  316"
53
        char rows[6] = {0};              // "  332"
54
        char internal1[6] = {0};         // "1.799"
55
        char latitude[6] = {0};          // "-51.3"
56
        char greenwich[6] = {0};         // "270.0"
57
        char internal2[6] = {0};         // "558.4"
58
        char jpole[6] = {0};             // "158.0"
59
        char ipole[6] = {0};             // "174.0"
60
        char instrument[6] = {0};        // "SSMIS"
61
        char data_descriptors[6] = {0};  // "17 cn"
62
        char julian_start[6] = {0};      // "  261"
63
        char hour_start[6] = {0};        // "-9999"
64
        char minute_start[6] = {0};      // "-9999"
65
        char julian_end[6] = {0};        // "  261"
66
        char hour_end[6] = {0};          // "-9999"
67
        char minute_end[6] = {0};        // "-9999"
68
        char year[6] = {0};              // " 2010"
69
        char julian[6] = {0};            // "  261"
70
        char channel[6] = {0};           // "  000"
71
        char scaling[6] = {0};           // "00250"
72
73
        // 121-126 Integer scaling factor
74
        // 127-150 24-character file name (without file-name extension)
75
        // 151-230 80-character image title
76
        // 231-300 70-character data information (creation date, data source,
77
        // etc.)
78
        char filename[24] = {0};    // "  nt_20100918_f17_v1.1_s"
79
        char imagetitle[80] = {0};  // "ANTARCTIC  SMMR  TOTAL ICE CONCENTRATION
80
                                    // NIMBUSN07     DAY 299 10/26/1978"
81
        char data_information[70] = {0};  // "ANTARCTIC  SMMR ONSSMIGRID CON
82
                                          // Coast253Pole251Land254 06/27/1996"
83
    };
84
85
    VSILFILE *fp = nullptr;
86
    CPLString osSRS{};
87
    NSIDCbinHeader sHeader{};
88
89
    GDALGeoTransform m_gt{};
90
    CPL_DISALLOW_COPY_ASSIGN(NSIDCbinDataset)
91
    OGRSpatialReference m_oSRS{};
92
93
  public:
94
    NSIDCbinDataset();
95
    ~NSIDCbinDataset() override;
96
    CPLErr GetGeoTransform(GDALGeoTransform &gt) const override;
97
98
    const OGRSpatialReference *GetSpatialRef() const override;
99
    static GDALDataset *Open(GDALOpenInfo *);
100
    static int Identify(GDALOpenInfo *);
101
};
102
103
static const char *stripLeadingSpaces_nsidc(const char *buf)
104
12
{
105
12
    const char *ptr = buf;
106
    /* Go until we run out of characters  or hit something non-zero */
107
27
    while (*ptr == ' ')
108
15
    {
109
15
        ptr++;
110
15
    }
111
12
    return ptr;
112
12
}
113
114
/************************************************************************/
115
/* ==================================================================== */
116
/*                           NSIDCbinRasterBand                         */
117
/* ==================================================================== */
118
/************************************************************************/
119
120
class NSIDCbinRasterBand final : public RawRasterBand
121
{
122
    friend class NSIDCbinDataset;
123
124
    CPL_DISALLOW_COPY_ASSIGN(NSIDCbinRasterBand)
125
126
  public:
127
    NSIDCbinRasterBand(GDALDataset *poDS, int nBand, VSILFILE *fpRaw,
128
                       vsi_l_offset nImgOffset, int nPixelOffset,
129
                       int nLineOffset, GDALDataType eDataType);
130
    ~NSIDCbinRasterBand() override;
131
132
    double GetNoDataValue(int *pbSuccess = nullptr) override;
133
    double GetScale(int *pbSuccess = nullptr) override;
134
    const char *GetUnitType() override;
135
};
136
137
/************************************************************************/
138
/*                         NSIDCbinRasterBand()                         */
139
/************************************************************************/
140
141
NSIDCbinRasterBand::NSIDCbinRasterBand(GDALDataset *poDSIn, int nBandIn,
142
                                       VSILFILE *fpRawIn,
143
                                       vsi_l_offset nImgOffsetIn,
144
                                       int nPixelOffsetIn, int nLineOffsetIn,
145
                                       GDALDataType eDataTypeIn)
146
3
    : RawRasterBand(poDSIn, nBandIn, fpRawIn, nImgOffsetIn, nPixelOffsetIn,
147
3
                    nLineOffsetIn, eDataTypeIn,
148
3
                    RawRasterBand::ByteOrder::ORDER_LITTLE_ENDIAN,
149
3
                    RawRasterBand::OwnFP::NO)
150
3
{
151
3
}
152
153
/************************************************************************/
154
/*                        ~NSIDCbinRasterBand()                         */
155
/************************************************************************/
156
157
NSIDCbinRasterBand::~NSIDCbinRasterBand()
158
3
{
159
3
}
160
161
/************************************************************************/
162
/*                           GetNoDataValue()                           */
163
/************************************************************************/
164
165
double NSIDCbinRasterBand::GetNoDataValue(int *pbSuccess)
166
167
12
{
168
12
    if (pbSuccess != nullptr)
169
9
        *pbSuccess = TRUE;
170
    // we might check this if other format variants can be different
171
    // or if we change the Band type, or if we generalize to choosing Byte vs.
172
    // Float type but for now it's constant
173
    // https://lists.osgeo.org/pipermail/gdal-dev/2022-August/056144.html
174
    // const char  *pszLine = poPDS->sHeader.missing_int;
175
12
    return 255.0;  // CPLAtof(pszLine);
176
12
}
177
178
/************************************************************************/
179
/*                              GetScale()                              */
180
/************************************************************************/
181
182
double NSIDCbinRasterBand::GetScale(int *pbSuccess)
183
3
{
184
3
    if (pbSuccess != nullptr)
185
3
        *pbSuccess = TRUE;
186
    // again just use a constant unless we see other file variants
187
    // also, this might be fraction rather than percentage
188
    // atof(cpl::down_cast<NSIDCbinDataset*>(poDS)->sHeader.scaling)/100;
189
3
    return 0.4;
190
3
}
191
192
/************************************************************************/
193
/*                          NSIDCbinDataset()                           */
194
/************************************************************************/
195
196
3
NSIDCbinDataset::NSIDCbinDataset() : fp(nullptr), m_oSRS(OGRSpatialReference())
197
3
{
198
3
}
199
200
/************************************************************************/
201
/*                          ~NSIDCbinDataset()                          */
202
/************************************************************************/
203
204
NSIDCbinDataset::~NSIDCbinDataset()
205
206
3
{
207
3
    if (fp)
208
3
        CPL_IGNORE_RET_VAL(VSIFCloseL(fp));
209
3
    fp = nullptr;
210
3
}
211
212
/************************************************************************/
213
/*                                Open()                                */
214
/************************************************************************/
215
216
GDALDataset *NSIDCbinDataset::Open(GDALOpenInfo *poOpenInfo)
217
218
451k
{
219
220
    // Confirm that the header is compatible with a NSIDC dataset.
221
451k
    if (!Identify(poOpenInfo))
222
451k
        return nullptr;
223
224
    // Confirm the requested access is supported.
225
3
    if (poOpenInfo->eAccess == GA_Update)
226
0
    {
227
0
        ReportUpdateNotSupportedByDriver("NSIDCbin");
228
0
        return nullptr;
229
0
    }
230
231
    // Check that the file pointer from GDALOpenInfo* is available
232
3
    if (poOpenInfo->fpL == nullptr)
233
0
    {
234
0
        return nullptr;
235
0
    }
236
237
    /* -------------------------------------------------------------------- */
238
    /*      Create a corresponding GDALDataset.                             */
239
    /* -------------------------------------------------------------------- */
240
3
    auto poDS = std::make_unique<NSIDCbinDataset>();
241
242
3
    poDS->eAccess = poOpenInfo->eAccess;
243
3
    std::swap(poDS->fp, poOpenInfo->fpL);
244
245
    /* -------------------------------------------------------------------- */
246
    /*      Read the header information.                                    */
247
    /* -------------------------------------------------------------------- */
248
3
    if (VSIFReadL(&(poDS->sHeader), 300, 1, poDS->fp) != 1)
249
0
    {
250
0
        CPLError(CE_Failure, CPLE_FileIO,
251
0
                 "Attempt to read 300 byte header filed on file %s\n",
252
0
                 poOpenInfo->pszFilename);
253
0
        return nullptr;
254
0
    }
255
256
    // avoid unused warnings
257
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.missing_int);
258
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.internal1);
259
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.latitude);
260
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.greenwich);
261
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.internal2);
262
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.jpole);
263
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.ipole);
264
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.julian_start);
265
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.hour_start);
266
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.minute_start);
267
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.julian_end);
268
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.hour_end);
269
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.minute_end);
270
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.channel);
271
3
    CPL_IGNORE_RET_VAL(poDS->sHeader.scaling);
272
273
    /* -------------------------------------------------------------------- */
274
    /*      Extract information of interest from the header.                */
275
    /* -------------------------------------------------------------------- */
276
277
3
    poDS->nRasterXSize = atoi(poDS->sHeader.columns);
278
3
    poDS->nRasterYSize = atoi(poDS->sHeader.rows);
279
280
3
    const char *psHeader = reinterpret_cast<char *>(poOpenInfo->pabyHeader);
281
3
    bool south = STARTS_WITH(psHeader + 230, "ANTARCTIC");
282
283
3
    if (!GDALCheckDatasetDimensions(poDS->nRasterXSize, poDS->nRasterYSize))
284
0
    {
285
0
        return nullptr;
286
0
    }
287
288
    /* -------------------------------------------------------------------- */
289
    /*      Extract metadata from the header.                               */
290
    /* -------------------------------------------------------------------- */
291
292
3
    poDS->SetMetadataItem("INSTRUMENT", poDS->sHeader.instrument);
293
3
    poDS->SetMetadataItem("YEAR", stripLeadingSpaces_nsidc(poDS->sHeader.year));
294
3
    poDS->SetMetadataItem("JULIAN_DAY",
295
3
                          stripLeadingSpaces_nsidc(poDS->sHeader.julian));
296
3
    poDS->SetMetadataItem(
297
3
        "DATA_DESCRIPTORS",
298
3
        stripLeadingSpaces_nsidc(poDS->sHeader.data_descriptors));
299
3
    poDS->SetMetadataItem("IMAGE_TITLE", poDS->sHeader.imagetitle);
300
3
    poDS->SetMetadataItem("FILENAME",
301
3
                          stripLeadingSpaces_nsidc(poDS->sHeader.filename));
302
3
    poDS->SetMetadataItem("DATA_INFORMATION", poDS->sHeader.data_information);
303
304
    /* -------------------------------------------------------------------- */
305
    /*      Create band information objects.                                */
306
    /* -------------------------------------------------------------------- */
307
3
    int nBytesPerSample = 1;
308
309
3
    auto poBand = std::make_unique<NSIDCbinRasterBand>(
310
3
        poDS.get(), 1, poDS->fp, 300, nBytesPerSample, poDS->nRasterXSize,
311
3
        GDT_UInt8);
312
3
    if (!poBand->IsValid())
313
0
        return nullptr;
314
3
    poDS->SetBand(1, std::move(poBand));
315
316
    /* -------------------------------------------------------------------- */
317
    /*      Geotransform, we simply know this from the documentation.       */
318
    /*       If we have similar binary files (at 12.5km for example) then   */
319
    /*        need more nuanced handling                                    */
320
    /*      Projection,  this is not technically enough, because the old    */
321
    /*       stuff is Hughes 1980.                                          */
322
    /*      FIXME: old or new epsg codes based on header info, or jul/year  */
323
    /* -------------------------------------------------------------------- */
324
325
3
    int epsg = -1;
326
3
    if (south)
327
3
    {
328
3
        poDS->m_gt.xorig = -3950000.0;
329
3
        poDS->m_gt.xscale = 25000;
330
3
        poDS->m_gt.xrot = 0.0;
331
3
        poDS->m_gt.yorig = 4350000.0;
332
3
        poDS->m_gt.yrot = 0.0;
333
3
        poDS->m_gt.yscale = -25000;
334
335
3
        epsg = 3976;
336
3
    }
337
0
    else
338
0
    {
339
0
        poDS->m_gt.xorig = -3837500;
340
0
        poDS->m_gt.xscale = 25000;
341
0
        poDS->m_gt.xrot = 0.0;
342
0
        poDS->m_gt.yorig = 5837500;
343
0
        poDS->m_gt.yrot = 0.0;
344
0
        poDS->m_gt.yscale = -25000;
345
346
0
        epsg = 3413;
347
0
    }
348
349
3
    if (poDS->m_oSRS.importFromEPSG(epsg) != OGRERR_NONE)
350
0
    {
351
0
        CPLError(CE_Failure, CPLE_AppDefined,
352
0
                 "Unknown error initializing SRS from ESPG code. ");
353
0
        return nullptr;
354
0
    }
355
356
    /* -------------------------------------------------------------------- */
357
    /*      Initialize any PAM information.                                 */
358
    /* -------------------------------------------------------------------- */
359
3
    poDS->SetDescription(poOpenInfo->pszFilename);
360
3
    poDS->TryLoadXML();
361
362
3
    return poDS.release();
363
3
}
364
365
/************************************************************************/
366
/*                              Identify()                              */
367
/************************************************************************/
368
int NSIDCbinDataset::Identify(GDALOpenInfo *poOpenInfo)
369
451k
{
370
371
    // -------------------------------------------------------------------- /
372
    //      Works for daily and monthly, north and south NSIDC binary files /
373
    //      north and south are different dimensions, different extents but /
374
    //      both are 25000m resolution.
375
    //
376
    //      First we check to see if the file has the expected header       /
377
    //      bytes.                                                          /
378
    // -------------------------------------------------------------------- /
379
380
451k
    if (poOpenInfo->nHeaderBytes < 300 || poOpenInfo->fpL == nullptr)
381
384k
        return FALSE;
382
383
66.8k
    const char *psHeader = reinterpret_cast<char *>(poOpenInfo->pabyHeader);
384
    // Check if century values seem reasonable.
385
66.8k
    if (!(EQUALN(psHeader + 103, "20", 2) || EQUALN(psHeader + 103, "19", 2) ||
386
          // the first files 1978 don't have a space at the start
387
66.7k
          EQUALN(psHeader + 102, "20", 2) || EQUALN(psHeader + 102, "19", 2)))
388
66.6k
    {
389
66.6k
        return FALSE;
390
66.6k
    }
391
392
    // Check if descriptors reasonable.
393
249
    if (!(STARTS_WITH(psHeader + 230, "ANTARCTIC") ||
394
246
          STARTS_WITH(psHeader + 230, "ARCTIC")))
395
246
    {
396
397
246
        return FALSE;
398
246
    }
399
400
3
    return TRUE;
401
249
}
402
403
/************************************************************************/
404
/*                           GetSpatialRef()                            */
405
/************************************************************************/
406
407
const OGRSpatialReference *NSIDCbinDataset::GetSpatialRef() const
408
3
{
409
3
    return &m_oSRS;
410
3
}
411
412
/************************************************************************/
413
/*                          GetGeoTransform()                           */
414
/************************************************************************/
415
416
CPLErr NSIDCbinDataset::GetGeoTransform(GDALGeoTransform &gt) const
417
418
3
{
419
3
    gt = m_gt;
420
3
    return CE_None;
421
3
}
422
423
/************************************************************************/
424
/*                            GetUnitType()                             */
425
/************************************************************************/
426
427
const char *NSIDCbinRasterBand::GetUnitType()
428
3
{
429
    // undecided, atm stick with Byte but may switch to Float and lose values >
430
    // 250 or generalize to non-raw driver
431
    // https://lists.osgeo.org/pipermail/gdal-dev/2022-August/056144.html
432
    // if (eDataType == GDT_Float32)
433
    //     return "Percentage";  // or "Fraction [0,1]"
434
435
    // Byte values don't have a clear unit type
436
3
    return "";
437
3
}
438
439
/************************************************************************/
440
/*                       GDALRegister_NSIDCbin()                        */
441
/************************************************************************/
442
443
void GDALRegister_NSIDCbin()
444
445
22
{
446
22
    if (GDALGetDriverByName("NSIDCbin") != nullptr)
447
0
        return;
448
449
22
    GDALDriver *poDriver = new GDALDriver();
450
451
22
    poDriver->SetDescription("NSIDCbin");
452
22
    poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
453
22
    poDriver->SetMetadataItem(GDAL_DMD_LONGNAME,
454
22
                              "NSIDC Sea Ice Concentrations binary (.bin)");
455
22
    poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC,
456
22
                              "drivers/raster/nsidcbin.html");
457
22
    poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
458
22
    poDriver->SetMetadataItem(GDAL_DMD_EXTENSION, "bin");
459
460
22
    poDriver->pfnOpen = NSIDCbinDataset::Open;
461
462
22
    GetGDALDriverManager()->RegisterDriver(poDriver);
463
22
}