Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: Hierarchical Data Format Release 5 (HDF5)
4 : * Purpose: Read S100 bathymetric datasets.
5 : * Author: Even Rouault <even dot rouault at spatialys dot com>
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2023, Even Rouault <even dot rouault at spatialys dot com>
9 : *
10 : * Permission is hereby granted, free of charge, to any person obtaining a
11 : * copy of this software and associated documentation files (the "Software"),
12 : * to deal in the Software without restriction, including without limitation
13 : * the rights to use, copy, modify, merge, publish, distribute, sublicense,
14 : * and/or sell copies of the Software, and to permit persons to whom the
15 : * Software is furnished to do so, subject to the following conditions:
16 : *
17 : * The above copyright notice and this permission notice shall be included
18 : * in all copies or substantial portions of the Software.
19 : *
20 : * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 : * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 : * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23 : * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 : * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 : * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 : * DEALINGS IN THE SOFTWARE.
27 : ****************************************************************************/
28 :
29 : #include "s100.h"
30 : #include "hdf5dataset.h"
31 :
32 : #include <algorithm>
33 : #include <cmath>
34 :
35 : /************************************************************************/
36 : /* S100BaseDataset() */
37 : /************************************************************************/
38 :
39 19 : S100BaseDataset::S100BaseDataset(const std::string &osFilename)
40 19 : : m_osFilename(osFilename)
41 : {
42 19 : }
43 :
44 : /************************************************************************/
45 : /* Init() */
46 : /************************************************************************/
47 :
48 19 : bool S100BaseDataset::Init()
49 : {
50 : // Open the file as an HDF5 file.
51 19 : hid_t fapl = H5Pcreate(H5P_FILE_ACCESS);
52 19 : H5Pset_driver(fapl, HDF5GetFileDriver(), nullptr);
53 19 : hid_t hHDF5 = H5Fopen(m_osFilename.c_str(), H5F_ACC_RDONLY, fapl);
54 19 : H5Pclose(fapl);
55 19 : if (hHDF5 < 0)
56 0 : return false;
57 :
58 38 : auto poSharedResources = GDAL::HDF5SharedResources::Create(m_osFilename);
59 19 : poSharedResources->m_hHDF5 = hHDF5;
60 :
61 19 : m_poRootGroup = HDF5Dataset::OpenGroup(poSharedResources);
62 19 : if (m_poRootGroup == nullptr)
63 0 : return false;
64 :
65 19 : S100ReadSRS(m_poRootGroup.get(), m_oSRS);
66 :
67 19 : S100ReadVerticalDatum(this, m_poRootGroup.get());
68 :
69 : m_osMetadataFile =
70 19 : S100ReadMetadata(this, m_osFilename, m_poRootGroup.get());
71 :
72 19 : return true;
73 : }
74 :
75 : /************************************************************************/
76 : /* GetGeoTransform() */
77 : /************************************************************************/
78 :
79 10 : CPLErr S100BaseDataset::GetGeoTransform(double *padfGeoTransform)
80 :
81 : {
82 10 : if (m_bHasGT)
83 : {
84 10 : memcpy(padfGeoTransform, m_adfGeoTransform, sizeof(double) * 6);
85 10 : return CE_None;
86 : }
87 :
88 0 : return GDALPamDataset::GetGeoTransform(padfGeoTransform);
89 : }
90 :
91 : /************************************************************************/
92 : /* GetSpatialRef() */
93 : /************************************************************************/
94 :
95 9 : const OGRSpatialReference *S100BaseDataset::GetSpatialRef() const
96 : {
97 9 : if (!m_oSRS.IsEmpty())
98 9 : return &m_oSRS;
99 0 : return GDALPamDataset::GetSpatialRef();
100 : }
101 :
102 : /************************************************************************/
103 : /* GetFileList() */
104 : /************************************************************************/
105 :
106 4 : char **S100BaseDataset::GetFileList()
107 : {
108 4 : char **papszFileList = GDALPamDataset::GetFileList();
109 4 : if (!m_osMetadataFile.empty())
110 4 : papszFileList = CSLAddString(papszFileList, m_osMetadataFile.c_str());
111 4 : return papszFileList;
112 : }
113 :
114 : /************************************************************************/
115 : /* S100ReadSRS() */
116 : /************************************************************************/
117 :
118 37 : bool S100ReadSRS(const GDALGroup *poRootGroup, OGRSpatialReference &oSRS)
119 : {
120 : // Get SRS
121 74 : auto poHorizontalCRS = poRootGroup->GetAttribute("horizontalCRS");
122 63 : if (poHorizontalCRS &&
123 63 : poHorizontalCRS->GetDataType().GetClass() == GEDTC_NUMERIC)
124 : {
125 : // horizontalCRS is v2.2
126 26 : oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
127 26 : if (oSRS.importFromEPSG(poHorizontalCRS->ReadAsInt()) != OGRERR_NONE)
128 : {
129 0 : oSRS.Clear();
130 : }
131 : }
132 : else
133 : {
134 : auto poHorizontalDatumReference =
135 33 : poRootGroup->GetAttribute("horizontalDatumReference");
136 : auto poHorizontalDatumValue =
137 33 : poRootGroup->GetAttribute("horizontalDatumValue");
138 11 : if (poHorizontalDatumReference && poHorizontalDatumValue)
139 : {
140 : const char *pszAuthName =
141 11 : poHorizontalDatumReference->ReadAsString();
142 11 : const char *pszAuthCode = poHorizontalDatumValue->ReadAsString();
143 11 : if (pszAuthName && pszAuthCode)
144 : {
145 11 : oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
146 22 : if (oSRS.SetFromUserInput(
147 22 : (std::string(pszAuthName) + ':' + pszAuthCode).c_str(),
148 : OGRSpatialReference::
149 11 : SET_FROM_USER_INPUT_LIMITATIONS_get()) !=
150 : OGRERR_NONE)
151 : {
152 0 : oSRS.Clear();
153 : }
154 : }
155 : }
156 : }
157 74 : return !oSRS.IsEmpty();
158 : }
159 :
160 : /************************************************************************/
161 : /* S100GetNumPointsLongitudinalLatitudinal() */
162 : /************************************************************************/
163 :
164 43 : bool S100GetNumPointsLongitudinalLatitudinal(const GDALGroup *poGroup,
165 : int &nNumPointsLongitudinal,
166 : int &nNumPointsLatitudinal)
167 : {
168 129 : auto poSpacingX = poGroup->GetAttribute("gridSpacingLongitudinal");
169 129 : auto poSpacingY = poGroup->GetAttribute("gridSpacingLatitudinal");
170 : auto poNumPointsLongitudinal =
171 129 : poGroup->GetAttribute("numPointsLongitudinal");
172 129 : auto poNumPointsLatitudinal = poGroup->GetAttribute("numPointsLatitudinal");
173 43 : if (poSpacingX &&
174 129 : poSpacingX->GetDataType().GetNumericDataType() == GDT_Float64 &&
175 86 : poSpacingY &&
176 86 : poSpacingY->GetDataType().GetNumericDataType() == GDT_Float64 &&
177 43 : poNumPointsLongitudinal &&
178 43 : GDALDataTypeIsInteger(
179 86 : poNumPointsLongitudinal->GetDataType().GetNumericDataType()) &&
180 129 : poNumPointsLatitudinal &&
181 43 : GDALDataTypeIsInteger(
182 43 : poNumPointsLatitudinal->GetDataType().GetNumericDataType()))
183 : {
184 43 : nNumPointsLongitudinal = poNumPointsLongitudinal->ReadAsInt();
185 43 : nNumPointsLatitudinal = poNumPointsLatitudinal->ReadAsInt();
186 :
187 : // Those are optional, but use them when available, to detect
188 : // potential inconsistency
189 129 : auto poEastBoundLongitude = poGroup->GetAttribute("eastBoundLongitude");
190 129 : auto poWestBoundLongitude = poGroup->GetAttribute("westBoundLongitude");
191 : auto poSouthBoundLongitude =
192 129 : poGroup->GetAttribute("southBoundLatitude");
193 86 : auto poNorthBoundLatitude = poGroup->GetAttribute("northBoundLatitude");
194 43 : if (poEastBoundLongitude &&
195 0 : GDALDataTypeIsFloating(
196 43 : poEastBoundLongitude->GetDataType().GetNumericDataType()) &&
197 0 : poWestBoundLongitude &&
198 0 : GDALDataTypeIsFloating(
199 0 : poWestBoundLongitude->GetDataType().GetNumericDataType()) &&
200 0 : poSouthBoundLongitude &&
201 0 : GDALDataTypeIsFloating(
202 0 : poSouthBoundLongitude->GetDataType().GetNumericDataType()) &&
203 43 : poNorthBoundLatitude &&
204 0 : GDALDataTypeIsFloating(
205 0 : poNorthBoundLatitude->GetDataType().GetNumericDataType()))
206 : {
207 0 : const double dfSpacingX = poSpacingX->ReadAsDouble();
208 0 : const double dfSpacingY = poSpacingY->ReadAsDouble();
209 :
210 0 : const double dfEast = poEastBoundLongitude->ReadAsDouble();
211 0 : const double dfWest = poWestBoundLongitude->ReadAsDouble();
212 0 : const double dfSouth = poSouthBoundLongitude->ReadAsDouble();
213 0 : const double dfNorth = poNorthBoundLatitude->ReadAsDouble();
214 0 : if (std::fabs((dfWest + nNumPointsLongitudinal * dfSpacingX) -
215 0 : dfEast) < 5 * dfSpacingX &&
216 0 : std::fabs((dfSouth + nNumPointsLatitudinal * dfSpacingY) -
217 0 : dfNorth) < 5 * dfSpacingY)
218 : {
219 : // We need up to 5 spacings for product
220 : // S-111 Trial Data Set Release 1.1/111UK_20210401T000000Z_SolentAndAppr_dcf2.h5
221 : }
222 : else
223 : {
224 0 : CPLError(
225 : CE_Warning, CPLE_AppDefined,
226 : "Caution: "
227 : "eastBoundLongitude/westBoundLongitude/southBoundLatitude/"
228 : "northBoundLatitude/gridSpacingLongitudinal/"
229 : "gridSpacingLatitudinal/numPointsLongitudinal/"
230 : "numPointsLatitudinal do not seem to be consistent");
231 0 : CPLDebug("S100", "Computed east = %f. Actual = %f",
232 0 : dfWest + nNumPointsLongitudinal * dfSpacingX, dfEast);
233 0 : CPLDebug("S100", "Computed north = %f. Actual = %f",
234 0 : dfSouth + nNumPointsLatitudinal * dfSpacingY, dfNorth);
235 : }
236 : }
237 :
238 43 : return true;
239 : }
240 0 : return false;
241 : }
242 :
243 : /************************************************************************/
244 : /* S100GetGeoTransform() */
245 : /************************************************************************/
246 :
247 17 : bool S100GetGeoTransform(const GDALGroup *poGroup, double adfGeoTransform[6],
248 : bool bNorthUp)
249 : {
250 51 : auto poOriginX = poGroup->GetAttribute("gridOriginLongitude");
251 51 : auto poOriginY = poGroup->GetAttribute("gridOriginLatitude");
252 51 : auto poSpacingX = poGroup->GetAttribute("gridSpacingLongitudinal");
253 51 : auto poSpacingY = poGroup->GetAttribute("gridSpacingLatitudinal");
254 17 : if (poOriginX &&
255 51 : poOriginX->GetDataType().GetNumericDataType() == GDT_Float64 &&
256 34 : poOriginY &&
257 34 : poOriginY->GetDataType().GetNumericDataType() == GDT_Float64 &&
258 34 : poSpacingX &&
259 34 : poSpacingX->GetDataType().GetNumericDataType() == GDT_Float64 &&
260 51 : poSpacingY &&
261 17 : poSpacingY->GetDataType().GetNumericDataType() == GDT_Float64)
262 : {
263 17 : int nNumPointsLongitudinal = 0;
264 17 : int nNumPointsLatitudinal = 0;
265 17 : if (!S100GetNumPointsLongitudinalLatitudinal(
266 : poGroup, nNumPointsLongitudinal, nNumPointsLatitudinal))
267 0 : return false;
268 :
269 17 : const double dfSpacingX = poSpacingX->ReadAsDouble();
270 17 : const double dfSpacingY = poSpacingY->ReadAsDouble();
271 :
272 17 : adfGeoTransform[0] = poOriginX->ReadAsDouble();
273 17 : adfGeoTransform[3] =
274 17 : poOriginY->ReadAsDouble() +
275 17 : (bNorthUp ? dfSpacingY * (nNumPointsLatitudinal - 1) : 0);
276 17 : adfGeoTransform[1] = dfSpacingX;
277 17 : adfGeoTransform[5] = bNorthUp ? -dfSpacingY : dfSpacingY;
278 :
279 : // From pixel-center convention to pixel-corner convention
280 17 : adfGeoTransform[0] -= adfGeoTransform[1] / 2;
281 17 : adfGeoTransform[3] -= adfGeoTransform[5] / 2;
282 :
283 17 : return true;
284 : }
285 0 : return false;
286 : }
287 :
288 : /************************************************************************/
289 : /* S100GetDimensions() */
290 : /************************************************************************/
291 :
292 18 : bool S100GetDimensions(
293 : const GDALGroup *poGroup,
294 : std::vector<std::shared_ptr<GDALDimension>> &apoDims,
295 : std::vector<std::shared_ptr<GDALMDArray>> &apoIndexingVars)
296 : {
297 54 : auto poOriginX = poGroup->GetAttribute("gridOriginLongitude");
298 54 : auto poOriginY = poGroup->GetAttribute("gridOriginLatitude");
299 54 : auto poSpacingX = poGroup->GetAttribute("gridSpacingLongitudinal");
300 54 : auto poSpacingY = poGroup->GetAttribute("gridSpacingLatitudinal");
301 18 : if (poOriginX &&
302 54 : poOriginX->GetDataType().GetNumericDataType() == GDT_Float64 &&
303 36 : poOriginY &&
304 36 : poOriginY->GetDataType().GetNumericDataType() == GDT_Float64 &&
305 36 : poSpacingX &&
306 36 : poSpacingX->GetDataType().GetNumericDataType() == GDT_Float64 &&
307 54 : poSpacingY &&
308 18 : poSpacingY->GetDataType().GetNumericDataType() == GDT_Float64)
309 : {
310 18 : int nNumPointsLongitudinal = 0;
311 18 : int nNumPointsLatitudinal = 0;
312 18 : if (!S100GetNumPointsLongitudinalLatitudinal(
313 : poGroup, nNumPointsLongitudinal, nNumPointsLatitudinal))
314 0 : return false;
315 :
316 : {
317 : auto poDim = std::make_shared<GDALDimensionWeakIndexingVar>(
318 36 : std::string(), "Y", GDAL_DIM_TYPE_HORIZONTAL_Y, std::string(),
319 36 : nNumPointsLatitudinal);
320 : auto poIndexingVar = GDALMDArrayRegularlySpaced::Create(
321 54 : std::string(), poDim->GetName(), poDim,
322 72 : poOriginY->ReadAsDouble(), poSpacingY->ReadAsDouble(), 0);
323 18 : poDim->SetIndexingVariable(poIndexingVar);
324 18 : apoDims.emplace_back(poDim);
325 18 : apoIndexingVars.emplace_back(poIndexingVar);
326 : }
327 :
328 : {
329 : auto poDim = std::make_shared<GDALDimensionWeakIndexingVar>(
330 36 : std::string(), "X", GDAL_DIM_TYPE_HORIZONTAL_X, std::string(),
331 36 : nNumPointsLongitudinal);
332 : auto poIndexingVar = GDALMDArrayRegularlySpaced::Create(
333 54 : std::string(), poDim->GetName(), poDim,
334 72 : poOriginX->ReadAsDouble(), poSpacingX->ReadAsDouble(), 0);
335 18 : poDim->SetIndexingVariable(poIndexingVar);
336 18 : apoDims.emplace_back(poDim);
337 18 : apoIndexingVars.emplace_back(poIndexingVar);
338 : }
339 :
340 18 : return true;
341 : }
342 0 : return false;
343 : }
344 :
345 : /************************************************************************/
346 : /* S100ReadVerticalDatum() */
347 : /************************************************************************/
348 :
349 19 : void S100ReadVerticalDatum(GDALDataset *poDS, const GDALGroup *poRootGroup)
350 : {
351 : // https://iho.int/uploads/user/pubs/standards/s-100/S-100_5.0.0_Final_Clean_Web.pdf
352 : // Table S100_VerticalAndSoundingDatum page 20
353 : static const struct
354 : {
355 : int nCode;
356 : const char *pszMeaning;
357 : const char *pszAbbrev;
358 : } asVerticalDatums[] = {
359 : {1, "meanLowWaterSprings", "MLWS"},
360 : {2, "meanLowerLowWaterSprings", nullptr},
361 : {3, "meanSeaLevel", "MSL"},
362 : {4, "lowestLowWater", nullptr},
363 : {5, "meanLowWater", "MLW"},
364 : {6, "lowestLowWaterSprings", nullptr},
365 : {7, "approximateMeanLowWaterSprings", nullptr},
366 : {8, "indianSpringLowWater", nullptr},
367 : {9, "lowWaterSprings", nullptr},
368 : {10, "approximateLowestAstronomicalTide", nullptr},
369 : {11, "nearlyLowestLowWater", nullptr},
370 : {12, "meanLowerLowWater", "MLLW"},
371 : {13, "lowWater", "LW"},
372 : {14, "approximateMeanLowWater", nullptr},
373 : {15, "approximateMeanLowerLowWater", nullptr},
374 : {16, "meanHighWater", "MHW"},
375 : {17, "meanHighWaterSprings", "MHWS"},
376 : {18, "highWater", "HW"},
377 : {19, "approximateMeanSeaLevel", nullptr},
378 : {20, "highWaterSprings", nullptr},
379 : {21, "meanHigherHighWater", "MHHW"},
380 : {22, "equinoctialSpringLowWater", nullptr},
381 : {23, "lowestAstronomicalTide", "LAT"},
382 : {24, "localDatum", nullptr},
383 : {25, "internationalGreatLakesDatum1985", nullptr},
384 : {26, "meanWaterLevel", nullptr},
385 : {27, "lowerLowWaterLargeTide", nullptr},
386 : {28, "higherHighWaterLargeTide", nullptr},
387 : {29, "nearlyHighestHighWater", nullptr},
388 : {30, "highestAstronomicalTide", "HAT"},
389 : {44, "balticSeaChartDatum2000", nullptr},
390 : {46, "internationalGreatLakesDatum2020", nullptr},
391 : };
392 :
393 57 : auto poVerticalDatum = poRootGroup->GetAttribute("verticalDatum");
394 38 : if (poVerticalDatum &&
395 38 : poVerticalDatum->GetDataType().GetClass() == GEDTC_NUMERIC)
396 : {
397 19 : bool bFound = false;
398 19 : const auto nVal = poVerticalDatum->ReadAsInt();
399 228 : for (const auto &sVerticalDatum : asVerticalDatums)
400 : {
401 228 : if (sVerticalDatum.nCode == nVal)
402 : {
403 19 : bFound = true;
404 19 : poDS->GDALDataset::SetMetadataItem("VERTICAL_DATUM_MEANING",
405 19 : sVerticalDatum.pszMeaning);
406 19 : if (sVerticalDatum.pszAbbrev)
407 19 : poDS->GDALDataset::SetMetadataItem(
408 19 : "VERTICAL_DATUM_ABBREV", sVerticalDatum.pszAbbrev);
409 19 : break;
410 : }
411 : }
412 19 : if (!bFound)
413 : {
414 0 : poDS->GDALDataset::SetMetadataItem("verticalDatum",
415 : CPLSPrintf("%d", nVal));
416 : }
417 : }
418 19 : }
419 :
420 : /************************************************************************/
421 : /* S100ReadMetadata() */
422 : /************************************************************************/
423 :
424 19 : std::string S100ReadMetadata(GDALDataset *poDS, const std::string &osFilename,
425 : const GDALGroup *poRootGroup)
426 : {
427 19 : std::string osMetadataFile;
428 199 : for (const auto &poAttr : poRootGroup->GetAttributes())
429 : {
430 180 : const auto &osName = poAttr->GetName();
431 180 : if (osName == "metadata")
432 : {
433 19 : const char *pszVal = poAttr->ReadAsString();
434 19 : if (pszVal && pszVal[0])
435 : {
436 : osMetadataFile = CPLFormFilename(CPLGetPath(osFilename.c_str()),
437 19 : pszVal, nullptr);
438 : VSIStatBufL sStat;
439 19 : if (VSIStatL(osMetadataFile.c_str(), &sStat) != 0)
440 : {
441 : // Test products from https://data.admiralty.co.uk/portal/apps/sites/#/marine-data-portal/pages/s-100
442 : // advertise a metadata filename starting with "MD_", per the spec,
443 : // but the actual filename does not start with "MD_"...
444 0 : if (STARTS_WITH(pszVal, "MD_"))
445 : {
446 : osMetadataFile =
447 : CPLFormFilename(CPLGetPath(osFilename.c_str()),
448 0 : pszVal + strlen("MD_"), nullptr);
449 0 : if (VSIStatL(osMetadataFile.c_str(), &sStat) != 0)
450 : {
451 0 : osMetadataFile.clear();
452 : }
453 : }
454 : else
455 : {
456 0 : osMetadataFile.clear();
457 : }
458 : }
459 : }
460 : }
461 308 : else if (osName != "horizontalCRS" &&
462 289 : osName != "horizontalDatumReference" &&
463 279 : osName != "horizontalDatumValue" &&
464 255 : osName != "productSpecification" &&
465 236 : osName != "eastBoundLongitude" &&
466 236 : osName != "northBoundLatitude" &&
467 236 : osName != "southBoundLatitude" &&
468 354 : osName != "westBoundLongitude" && osName != "extentTypeCode" &&
469 312 : osName != "verticalCS" && osName != "verticalCoordinateBase" &&
470 398 : osName != "verticalDatumReference" &&
471 76 : osName != "verticalDatum")
472 : {
473 57 : const char *pszVal = poAttr->ReadAsString();
474 57 : if (pszVal && pszVal[0])
475 57 : poDS->GDALDataset::SetMetadataItem(osName.c_str(), pszVal);
476 : }
477 : }
478 19 : return osMetadataFile;
479 : }
|