/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 >) 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 >) 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 | } |