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