Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: OpenGIS Simple Features Reference Implementation
4 : * Purpose: OGRSpatialReference translation to/from netCDF CF-1 georeferencing.
5 : * Author: Even Rouault <even.rouault at spatialys.com>
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2004, Frank Warmerdam
9 : * Copyright (c) 2007-2024, Even Rouault <even.rouault at spatialys.com>
10 : *
11 : * Permission is hereby granted, free of charge, to any person obtaining a
12 : * copy of this software and associated documentation files (the "Software"),
13 : * to deal in the Software without restriction, including without limitation
14 : * the rights to use, copy, modify, merge, publish, distribute, sublicense,
15 : * and/or sell copies of the Software, and to permit persons to whom the
16 : * Software is furnished to do so, subject to the following conditions:
17 : *
18 : * The above copyright notice and this permission notice shall be included
19 : * in all copies or substantial portions of the Software.
20 : *
21 : * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22 : * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 : * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
24 : * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 : * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 : * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27 : * DEALINGS IN THE SOFTWARE.
28 : ****************************************************************************/
29 :
30 : #include "ogr_core.h"
31 : #include "ogrsf_frmts.h"
32 : #include "ogr_srs_cf1.h"
33 :
34 : /************************************************************************/
35 : /* OSRImportFromCF1() */
36 : /************************************************************************/
37 :
38 : /**
39 : * \brief Import a CRS from netCDF CF-1 definitions.
40 : *
41 : * This function is the same as OGRSpatialReference::importFromCF1().
42 : * @since 3.9
43 : */
44 :
45 4 : OGRErr OSRImportFromCF1(OGRSpatialReferenceH hSRS, CSLConstList papszKeyValues,
46 : const char *pszUnits)
47 :
48 : {
49 4 : VALIDATE_POINTER1(hSRS, "OSRImportFromCF1", OGRERR_FAILURE);
50 :
51 4 : return OGRSpatialReference::FromHandle(hSRS)->importFromCF1(papszKeyValues,
52 4 : pszUnits);
53 : }
54 :
55 : /************************************************************************/
56 : /* FetchDoubleParam() */
57 : /************************************************************************/
58 :
59 322 : static double FetchDoubleParam(CSLConstList papszKeyValues,
60 : const char *pszParam, double dfDefault)
61 :
62 : {
63 322 : const char *pszValue = CSLFetchNameValue(papszKeyValues, pszParam);
64 322 : if (pszValue)
65 : {
66 175 : return CPLAtofM(pszValue);
67 : }
68 :
69 147 : return dfDefault;
70 : }
71 :
72 : /************************************************************************/
73 : /* NCDFTokenizeArray() */
74 : /************************************************************************/
75 :
76 : // Parse a string, and return as a string list.
77 : // If it an array of the form {a,b}, then tokenize it.
78 : // Otherwise, return a copy.
79 13 : static char **NCDFTokenizeArray(const char *pszValue)
80 : {
81 13 : if (pszValue == nullptr || EQUAL(pszValue, ""))
82 0 : return nullptr;
83 :
84 13 : char **papszValues = nullptr;
85 13 : const int nLen = static_cast<int>(strlen(pszValue));
86 :
87 13 : if (pszValue[0] == '{' && nLen > 2 && pszValue[nLen - 1] == '}')
88 : {
89 5 : char *pszTemp = static_cast<char *>(CPLMalloc((nLen - 2) + 1));
90 5 : strncpy(pszTemp, pszValue + 1, nLen - 2);
91 5 : pszTemp[nLen - 2] = '\0';
92 5 : papszValues = CSLTokenizeString2(pszTemp, ",", CSLT_ALLOWEMPTYTOKENS);
93 5 : CPLFree(pszTemp);
94 : }
95 : else
96 : {
97 8 : papszValues = reinterpret_cast<char **>(CPLCalloc(2, sizeof(char *)));
98 8 : papszValues[0] = CPLStrdup(pszValue);
99 8 : papszValues[1] = nullptr;
100 : }
101 :
102 13 : return papszValues;
103 : }
104 :
105 : /************************************************************************/
106 : /* FetchStandardParallels() */
107 : /************************************************************************/
108 :
109 : static std::vector<std::string>
110 18 : FetchStandardParallels(CSLConstList papszKeyValues)
111 : {
112 : // cf-1.0 tags
113 : const char *pszValue =
114 18 : CSLFetchNameValue(papszKeyValues, CF_PP_STD_PARALLEL);
115 :
116 18 : std::vector<std::string> ret;
117 18 : if (pszValue != nullptr)
118 : {
119 30 : CPLStringList aosValues;
120 25 : if (pszValue[0] != '{' &&
121 18 : (strchr(pszValue, ',') != nullptr ||
122 23 : CPLString(pszValue).Trim().find(' ') != std::string::npos))
123 : {
124 : // Some files like
125 : // ftp://data.knmi.nl/download/KNW-NetCDF-3D/1.0/noversion/2013/11/14/KNW-1.0_H37-ERA_NL_20131114.nc
126 : // do not use standard formatting for arrays, but just space
127 : // separated syntax
128 2 : aosValues = CSLTokenizeString2(pszValue, ", ", 0);
129 : }
130 : else
131 : {
132 13 : aosValues = NCDFTokenizeArray(pszValue);
133 : }
134 37 : for (int i = 0; i < aosValues.size(); i++)
135 : {
136 22 : ret.push_back(aosValues[i]);
137 : }
138 : }
139 : // Try gdal tags.
140 : else
141 : {
142 3 : pszValue = CSLFetchNameValue(papszKeyValues, CF_PP_STD_PARALLEL_1);
143 :
144 3 : if (pszValue != nullptr)
145 0 : ret.push_back(pszValue);
146 :
147 3 : pszValue = CSLFetchNameValue(papszKeyValues, CF_PP_STD_PARALLEL_2);
148 :
149 3 : if (pszValue != nullptr)
150 0 : ret.push_back(pszValue);
151 : }
152 :
153 18 : return ret;
154 : }
155 :
156 : /************************************************************************/
157 : /* importFromCF1() */
158 : /************************************************************************/
159 :
160 : /**
161 : * \brief Import a CRS from netCDF CF-1 definitions.
162 : *
163 : * http://cfconventions.org/cf-conventions/cf-conventions.html#appendix-grid-mappings
164 : *
165 : * This function is the equivalent of the C function OSRImportFromCF1().
166 : *
167 : * @param papszKeyValues Key/value pairs from the grid mapping variable.
168 : * Multi-valued parameters (typically "standard_parallel") should be comma
169 : * separated.
170 : *
171 : * @param pszUnits Value of the "units" attribute of the X/Y arrays. May be nullptr
172 : *
173 : * @return OGRERR_NONE on success or an error code in case of failure.
174 : * @since 3.9
175 : */
176 :
177 38 : OGRErr OGRSpatialReference::importFromCF1(CSLConstList papszKeyValues,
178 : const char *pszUnits)
179 : {
180 : // Import from "spatial_ref" or "crs_wkt" attributes in priority
181 38 : const char *pszWKT = CSLFetchNameValue(papszKeyValues, NCDF_SPATIAL_REF);
182 38 : if (!pszWKT)
183 : {
184 37 : pszWKT = CSLFetchNameValue(papszKeyValues, NCDF_CRS_WKT);
185 : }
186 38 : if (pszWKT)
187 2 : return importFromWkt(pszWKT);
188 :
189 : const char *pszGridMappingName =
190 36 : CSLFetchNameValue(papszKeyValues, CF_GRD_MAPPING_NAME);
191 :
192 : // Some files such as
193 : // http://www.ecad.eu/download/ensembles/data/Grid_0.44deg_rot/tg_0.44deg_rot_v16.0.nc.gz
194 : // lack an explicit projection_var:grid_mapping_name attribute
195 40 : if (pszGridMappingName == nullptr &&
196 4 : CSLFetchNameValue(papszKeyValues, CF_PP_GRID_NORTH_POLE_LONGITUDE) !=
197 : nullptr)
198 : {
199 3 : pszGridMappingName = CF_PT_ROTATED_LATITUDE_LONGITUDE;
200 : }
201 :
202 36 : if (pszGridMappingName == nullptr)
203 1 : return OGRERR_FAILURE;
204 :
205 35 : bool bGotGeogCS = false;
206 :
207 : // Check for datum/spheroid information.
208 : double dfEarthRadius =
209 35 : FetchDoubleParam(papszKeyValues, CF_PP_EARTH_RADIUS, -1.0);
210 :
211 : const double dfLonPrimeMeridian =
212 35 : FetchDoubleParam(papszKeyValues, CF_PP_LONG_PRIME_MERIDIAN, 0.0);
213 :
214 : const char *pszPMName =
215 35 : CSLFetchNameValue(papszKeyValues, CF_PRIME_MERIDIAN_NAME);
216 :
217 : // Should try to find PM name from its value if not Greenwich.
218 35 : if (pszPMName == nullptr && !CPLIsEqual(dfLonPrimeMeridian, 0.0))
219 0 : pszPMName = "unknown";
220 :
221 : double dfInverseFlattening =
222 35 : FetchDoubleParam(papszKeyValues, CF_PP_INVERSE_FLATTENING, -1.0);
223 :
224 : double dfSemiMajorAxis =
225 35 : FetchDoubleParam(papszKeyValues, CF_PP_SEMI_MAJOR_AXIS, -1.0);
226 :
227 : const double dfSemiMinorAxis =
228 35 : FetchDoubleParam(papszKeyValues, CF_PP_SEMI_MINOR_AXIS, -1.0);
229 :
230 : // See if semi-major exists if radius doesn't.
231 35 : if (dfEarthRadius < 0.0)
232 34 : dfEarthRadius = dfSemiMajorAxis;
233 :
234 : // If still no radius, check old tag.
235 35 : if (dfEarthRadius < 0.0)
236 : dfEarthRadius =
237 14 : FetchDoubleParam(papszKeyValues, CF_PP_EARTH_RADIUS_OLD, -1.0);
238 :
239 : const char *pszEllipsoidName =
240 35 : CSLFetchNameValue(papszKeyValues, CF_REFERENCE_ELLIPSOID_NAME);
241 :
242 : const char *pszDatumName =
243 35 : CSLFetchNameValue(papszKeyValues, CF_HORIZONTAL_DATUM_NAME);
244 :
245 : const char *pszGeogName =
246 35 : CSLFetchNameValue(papszKeyValues, CF_GEOGRAPHIC_CRS_NAME);
247 35 : if (pszGeogName == nullptr)
248 34 : pszGeogName = "unknown";
249 :
250 35 : bool bRotatedPole = false;
251 :
252 : // Has radius value.
253 35 : if (dfEarthRadius > 0.0)
254 : {
255 : // Check for inv_flat tag.
256 25 : if (dfInverseFlattening < 0.0)
257 : {
258 : // No inv_flat tag, check for semi_minor.
259 7 : if (dfSemiMinorAxis < 0.0)
260 : {
261 : // No way to get inv_flat, use sphere.
262 4 : SetGeogCS(pszGeogName, pszDatumName,
263 : pszEllipsoidName ? pszEllipsoidName : "Sphere",
264 : dfEarthRadius, 0.0, pszPMName, dfLonPrimeMeridian);
265 4 : bGotGeogCS = true;
266 : }
267 : else
268 : {
269 3 : if (dfSemiMajorAxis < 0.0)
270 0 : dfSemiMajorAxis = dfEarthRadius;
271 : // set inv_flat using semi_minor/major
272 : dfInverseFlattening =
273 3 : OSRCalcInvFlattening(dfSemiMajorAxis, dfSemiMinorAxis);
274 :
275 3 : SetGeogCS(pszGeogName, pszDatumName,
276 : pszEllipsoidName ? pszEllipsoidName : "Spheroid",
277 : dfEarthRadius, dfInverseFlattening, pszPMName,
278 : dfLonPrimeMeridian);
279 3 : bGotGeogCS = true;
280 : }
281 : }
282 : else
283 : {
284 18 : SetGeogCS(pszGeogName, pszDatumName,
285 : pszEllipsoidName ? pszEllipsoidName : "Spheroid",
286 : dfEarthRadius, dfInverseFlattening, pszPMName,
287 : dfLonPrimeMeridian);
288 18 : bGotGeogCS = true;
289 : }
290 :
291 25 : if (bGotGeogCS)
292 25 : CPLDebug("GDAL_netCDF", "got spheroid from CF: (%f , %f)",
293 : dfEarthRadius, dfInverseFlattening);
294 : }
295 :
296 : // Transverse Mercator.
297 35 : if (EQUAL(pszGridMappingName, CF_PT_TM))
298 : {
299 : const double dfScale =
300 7 : FetchDoubleParam(papszKeyValues, CF_PP_SCALE_FACTOR_MERIDIAN, 1.0);
301 :
302 : const double dfCenterLon =
303 7 : FetchDoubleParam(papszKeyValues, CF_PP_LONG_CENTRAL_MERIDIAN, 0.0);
304 :
305 : const double dfCenterLat =
306 7 : FetchDoubleParam(papszKeyValues, CF_PP_LAT_PROJ_ORIGIN, 0.0);
307 :
308 : const double dfFalseEasting =
309 7 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_EASTING, 0.0);
310 :
311 : const double dfFalseNorthing =
312 7 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_NORTHING, 0.0);
313 :
314 7 : SetTM(dfCenterLat, dfCenterLon, dfScale, dfFalseEasting,
315 : dfFalseNorthing);
316 :
317 7 : if (!bGotGeogCS)
318 0 : SetWellKnownGeogCS("WGS84");
319 : }
320 :
321 : // Albers Equal Area.
322 35 : if (EQUAL(pszGridMappingName, CF_PT_AEA))
323 : {
324 : const double dfCenterLon =
325 2 : FetchDoubleParam(papszKeyValues, CF_PP_LONG_CENTRAL_MERIDIAN, 0.0);
326 :
327 : const double dfFalseEasting =
328 2 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_EASTING, 0.0);
329 :
330 : const double dfFalseNorthing =
331 2 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_NORTHING, 0.0);
332 :
333 4 : const auto aosStdParallels = FetchStandardParallels(papszKeyValues);
334 :
335 2 : double dfStdP1 = 0;
336 2 : double dfStdP2 = 0;
337 2 : if (aosStdParallels.size() == 1)
338 : {
339 : // TODO CF-1 standard says it allows AEA to be encoded
340 : // with only 1 standard parallel. How should this
341 : // actually map to a 2StdP OGC WKT version?
342 0 : CPLError(CE_Warning, CPLE_NotSupported,
343 : "NetCDF driver import of AEA-1SP is not tested, "
344 : "using identical std. parallels.");
345 0 : dfStdP1 = CPLAtofM(aosStdParallels[0].c_str());
346 0 : dfStdP2 = dfStdP1;
347 : }
348 2 : else if (aosStdParallels.size() == 2)
349 : {
350 2 : dfStdP1 = CPLAtofM(aosStdParallels[0].c_str());
351 2 : dfStdP2 = CPLAtofM(aosStdParallels[1].c_str());
352 : }
353 : // Old default.
354 : else
355 : {
356 : dfStdP1 =
357 0 : FetchDoubleParam(papszKeyValues, CF_PP_STD_PARALLEL_1, 0.0);
358 :
359 : dfStdP2 =
360 0 : FetchDoubleParam(papszKeyValues, CF_PP_STD_PARALLEL_2, 0.0);
361 : }
362 :
363 : const double dfCenterLat =
364 2 : FetchDoubleParam(papszKeyValues, CF_PP_LAT_PROJ_ORIGIN, 0.0);
365 :
366 2 : SetACEA(dfStdP1, dfStdP2, dfCenterLat, dfCenterLon, dfFalseEasting,
367 : dfFalseNorthing);
368 :
369 2 : if (!bGotGeogCS)
370 0 : SetWellKnownGeogCS("WGS84");
371 : }
372 :
373 : // Cylindrical Equal Area
374 33 : else if (EQUAL(pszGridMappingName, CF_PT_CEA) ||
375 33 : EQUAL(pszGridMappingName, CF_PT_LCEA))
376 : {
377 0 : const auto aosStdParallels = FetchStandardParallels(papszKeyValues);
378 :
379 0 : double dfStdP1 = 0;
380 0 : if (!aosStdParallels.empty())
381 : {
382 0 : dfStdP1 = CPLAtofM(aosStdParallels[0].c_str());
383 : }
384 : else
385 : {
386 : // TODO: Add support for 'scale_factor_at_projection_origin'
387 : // variant to standard parallel. Probably then need to calc
388 : // a std parallel equivalent.
389 0 : CPLError(CE_Failure, CPLE_NotSupported,
390 : "NetCDF driver does not support import of CF-1 LCEA "
391 : "'scale_factor_at_projection_origin' variant yet.");
392 : }
393 :
394 : const double dfCentralMeridian =
395 0 : FetchDoubleParam(papszKeyValues, CF_PP_LONG_CENTRAL_MERIDIAN, 0.0);
396 :
397 : const double dfFalseEasting =
398 0 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_EASTING, 0.0);
399 :
400 : const double dfFalseNorthing =
401 0 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_NORTHING, 0.0);
402 :
403 0 : SetCEA(dfStdP1, dfCentralMeridian, dfFalseEasting, dfFalseNorthing);
404 :
405 0 : if (!bGotGeogCS)
406 0 : SetWellKnownGeogCS("WGS84");
407 : }
408 :
409 : // lambert_azimuthal_equal_area.
410 33 : else if (EQUAL(pszGridMappingName, CF_PT_LAEA))
411 : {
412 : const double dfCenterLon =
413 1 : FetchDoubleParam(papszKeyValues, CF_PP_LON_PROJ_ORIGIN, 0.0);
414 :
415 : const double dfCenterLat =
416 1 : FetchDoubleParam(papszKeyValues, CF_PP_LAT_PROJ_ORIGIN, 0.0);
417 :
418 : const double dfFalseEasting =
419 1 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_EASTING, 0.0);
420 :
421 : const double dfFalseNorthing =
422 1 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_NORTHING, 0.0);
423 :
424 1 : SetLAEA(dfCenterLat, dfCenterLon, dfFalseEasting, dfFalseNorthing);
425 :
426 1 : if (!bGotGeogCS)
427 0 : SetWellKnownGeogCS("WGS84");
428 :
429 2 : if (GetAttrValue("DATUM") != nullptr &&
430 1 : EQUAL(GetAttrValue("DATUM"), "WGS_1984"))
431 : {
432 0 : SetProjCS("LAEA (WGS84)");
433 : }
434 : }
435 :
436 : // Azimuthal Equidistant.
437 32 : else if (EQUAL(pszGridMappingName, CF_PT_AE))
438 : {
439 : const double dfCenterLon =
440 0 : FetchDoubleParam(papszKeyValues, CF_PP_LON_PROJ_ORIGIN, 0.0);
441 :
442 : const double dfCenterLat =
443 0 : FetchDoubleParam(papszKeyValues, CF_PP_LAT_PROJ_ORIGIN, 0.0);
444 :
445 : const double dfFalseEasting =
446 0 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_EASTING, 0.0);
447 :
448 : const double dfFalseNorthing =
449 0 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_NORTHING, 0.0);
450 :
451 0 : SetAE(dfCenterLat, dfCenterLon, dfFalseEasting, dfFalseNorthing);
452 :
453 0 : if (!bGotGeogCS)
454 0 : SetWellKnownGeogCS("WGS84");
455 : }
456 :
457 : // Lambert conformal conic.
458 32 : else if (EQUAL(pszGridMappingName, CF_PT_LCC))
459 : {
460 : const double dfCenterLon =
461 8 : FetchDoubleParam(papszKeyValues, CF_PP_LONG_CENTRAL_MERIDIAN, 0.0);
462 :
463 : const double dfCenterLat =
464 8 : FetchDoubleParam(papszKeyValues, CF_PP_LAT_PROJ_ORIGIN, 0.0);
465 :
466 : const double dfFalseEasting =
467 8 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_EASTING, 0.0);
468 :
469 : const double dfFalseNorthing =
470 8 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_NORTHING, 0.0);
471 :
472 16 : const auto aosStdParallels = FetchStandardParallels(papszKeyValues);
473 :
474 : // 2SP variant.
475 8 : if (aosStdParallels.size() == 2)
476 : {
477 5 : const double dfStdP1 = CPLAtofM(aosStdParallels[0].c_str());
478 5 : const double dfStdP2 = CPLAtofM(aosStdParallels[1].c_str());
479 5 : SetLCC(dfStdP1, dfStdP2, dfCenterLat, dfCenterLon, dfFalseEasting,
480 : dfFalseNorthing);
481 : }
482 : // 1SP variant (with standard_parallel or center lon).
483 : // See comments in netcdfdataset.h for this projection.
484 : else
485 : {
486 3 : double dfScale = FetchDoubleParam(papszKeyValues,
487 : CF_PP_SCALE_FACTOR_ORIGIN, -1.0);
488 :
489 : // CF definition, without scale factor.
490 3 : if (CPLIsEqual(dfScale, -1.0))
491 : {
492 : double dfStdP1;
493 : // With standard_parallel.
494 3 : if (aosStdParallels.size() == 1)
495 3 : dfStdP1 = CPLAtofM(aosStdParallels[0].c_str());
496 : // With center lon instead.
497 : else
498 0 : dfStdP1 = dfCenterLat;
499 : // dfStdP2 = dfStdP1;
500 :
501 : // Test if we should actually compute scale factor.
502 3 : if (!CPLIsEqual(dfStdP1, dfCenterLat))
503 : {
504 0 : CPLError(CE_Warning, CPLE_NotSupported,
505 : "NetCDF driver import of LCC-1SP with "
506 : "standard_parallel1 != "
507 : "latitude_of_projection_origin "
508 : "(which forces a computation of scale_factor) "
509 : "is experimental (bug #3324)");
510 : // Use Snyder eq. 15-4 to compute dfScale from
511 : // dfStdP1 and dfCenterLat. Only tested for
512 : // dfStdP1=dfCenterLat and (25,26), needs more data
513 : // for testing. Other option: use the 2SP variant -
514 : // how to compute new standard parallels?
515 0 : dfScale =
516 0 : (cos(dfStdP1) *
517 0 : pow(tan(M_PI / 4 + dfStdP1 / 2), sin(dfStdP1))) /
518 0 : (cos(dfCenterLat) * pow(tan(M_PI / 4 + dfCenterLat / 2),
519 : sin(dfCenterLat)));
520 : }
521 : // Default is 1.0.
522 : else
523 : {
524 3 : dfScale = 1.0;
525 : }
526 :
527 3 : SetLCC1SP(dfCenterLat, dfCenterLon, dfScale, dfFalseEasting,
528 : dfFalseNorthing);
529 : // Store dfStdP1 so we can output it to CF later.
530 3 : SetNormProjParm(SRS_PP_STANDARD_PARALLEL_1, dfStdP1);
531 : }
532 : // OGC/PROJ.4 definition with scale factor.
533 : else
534 : {
535 0 : SetLCC1SP(dfCenterLat, dfCenterLon, dfScale, dfFalseEasting,
536 : dfFalseNorthing);
537 : }
538 : }
539 :
540 8 : if (!bGotGeogCS)
541 3 : SetWellKnownGeogCS("WGS84");
542 : }
543 :
544 : // Is this Latitude/Longitude Grid explicitly?
545 :
546 24 : else if (EQUAL(pszGridMappingName, CF_PT_LATITUDE_LONGITUDE))
547 : {
548 3 : if (!bGotGeogCS)
549 0 : SetWellKnownGeogCS("WGS84");
550 : }
551 :
552 : // Mercator.
553 21 : else if (EQUAL(pszGridMappingName, CF_PT_MERCATOR))
554 : {
555 :
556 : // If there is a standard_parallel, know it is Mercator 2SP.
557 0 : const auto aosStdParallels = FetchStandardParallels(papszKeyValues);
558 :
559 0 : if (!aosStdParallels.empty())
560 : {
561 : // CF-1 Mercator 2SP always has lat centered at equator.
562 0 : const double dfStdP1 = CPLAtofM(aosStdParallels[0].c_str());
563 :
564 0 : const double dfCenterLat = 0.0;
565 :
566 : const double dfCenterLon =
567 0 : FetchDoubleParam(papszKeyValues, CF_PP_LON_PROJ_ORIGIN, 0.0);
568 :
569 : const double dfFalseEasting =
570 0 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_EASTING, 0.0);
571 :
572 : const double dfFalseNorthing =
573 0 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_NORTHING, 0.0);
574 :
575 0 : SetMercator2SP(dfStdP1, dfCenterLat, dfCenterLon, dfFalseEasting,
576 : dfFalseNorthing);
577 : }
578 : else
579 : {
580 : const double dfCenterLon =
581 0 : FetchDoubleParam(papszKeyValues, CF_PP_LON_PROJ_ORIGIN, 0.0);
582 :
583 : const double dfCenterLat =
584 0 : FetchDoubleParam(papszKeyValues, CF_PP_LAT_PROJ_ORIGIN, 0.0);
585 :
586 0 : const double dfScale = FetchDoubleParam(
587 : papszKeyValues, CF_PP_SCALE_FACTOR_ORIGIN, 1.0);
588 :
589 : const double dfFalseEasting =
590 0 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_EASTING, 0.0);
591 :
592 : const double dfFalseNorthing =
593 0 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_NORTHING, 0.0);
594 :
595 0 : SetMercator(dfCenterLat, dfCenterLon, dfScale, dfFalseEasting,
596 : dfFalseNorthing);
597 : }
598 :
599 0 : if (!bGotGeogCS)
600 0 : SetWellKnownGeogCS("WGS84");
601 : }
602 :
603 : // Orthographic.
604 21 : else if (EQUAL(pszGridMappingName, CF_PT_ORTHOGRAPHIC))
605 : {
606 : const double dfCenterLon =
607 0 : FetchDoubleParam(papszKeyValues, CF_PP_LON_PROJ_ORIGIN, 0.0);
608 :
609 : const double dfCenterLat =
610 0 : FetchDoubleParam(papszKeyValues, CF_PP_LAT_PROJ_ORIGIN, 0.0);
611 :
612 : const double dfFalseEasting =
613 0 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_EASTING, 0.0);
614 :
615 : const double dfFalseNorthing =
616 0 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_NORTHING, 0.0);
617 :
618 0 : SetOrthographic(dfCenterLat, dfCenterLon, dfFalseEasting,
619 : dfFalseNorthing);
620 :
621 0 : if (!bGotGeogCS)
622 0 : SetWellKnownGeogCS("WGS84");
623 : }
624 :
625 : // Polar Stereographic.
626 21 : else if (EQUAL(pszGridMappingName, CF_PT_POLAR_STEREO))
627 : {
628 16 : const auto aosStdParallels = FetchStandardParallels(papszKeyValues);
629 :
630 : const double dfCenterLon =
631 8 : FetchDoubleParam(papszKeyValues, CF_PP_VERT_LONG_FROM_POLE, 0.0);
632 :
633 : const double dfFalseEasting =
634 8 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_EASTING, 0.0);
635 :
636 : const double dfFalseNorthing =
637 8 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_NORTHING, 0.0);
638 :
639 : // CF allows the use of standard_parallel (lat_ts) OR
640 : // scale_factor (k0), make sure we have standard_parallel, using
641 : // Snyder eq. 22-7 with k=1 and lat=standard_parallel.
642 8 : if (!aosStdParallels.empty())
643 : {
644 5 : const double dfStdP1 = CPLAtofM(aosStdParallels[0].c_str());
645 :
646 : // Polar Stereographic Variant B with latitude of standard
647 : // parallel
648 5 : SetPS(dfStdP1, dfCenterLon, 1.0, dfFalseEasting, dfFalseNorthing);
649 : }
650 : else
651 : {
652 : // Fetch latitude_of_projection_origin (+90/-90).
653 : double dfLatProjOrigin =
654 3 : FetchDoubleParam(papszKeyValues, CF_PP_LAT_PROJ_ORIGIN, 0.0);
655 3 : if (!CPLIsEqual(dfLatProjOrigin, 90.0) &&
656 0 : !CPLIsEqual(dfLatProjOrigin, -90.0))
657 : {
658 0 : CPLError(CE_Failure, CPLE_NotSupported,
659 : "Polar Stereographic must have a %s "
660 : "parameter equal to +90 or -90.",
661 : CF_PP_LAT_PROJ_ORIGIN);
662 0 : dfLatProjOrigin = 90.0;
663 : }
664 :
665 3 : const double dfScale = FetchDoubleParam(
666 : papszKeyValues, CF_PP_SCALE_FACTOR_ORIGIN, 1.0);
667 :
668 : // Polar Stereographic Variant A with scale factor at
669 : // natural origin and latitude of origin = +/- 90
670 3 : SetPS(dfLatProjOrigin, dfCenterLon, dfScale, dfFalseEasting,
671 : dfFalseNorthing);
672 : }
673 :
674 8 : if (!bGotGeogCS)
675 6 : SetWellKnownGeogCS("WGS84");
676 : }
677 :
678 : // Stereographic.
679 13 : else if (EQUAL(pszGridMappingName, CF_PT_STEREO))
680 : {
681 : const double dfCenterLon =
682 0 : FetchDoubleParam(papszKeyValues, CF_PP_LON_PROJ_ORIGIN, 0.0);
683 :
684 : const double dfCenterLat =
685 0 : FetchDoubleParam(papszKeyValues, CF_PP_LAT_PROJ_ORIGIN, 0.0);
686 :
687 : const double dfScale =
688 0 : FetchDoubleParam(papszKeyValues, CF_PP_SCALE_FACTOR_ORIGIN, 1.0);
689 :
690 : const double dfFalseEasting =
691 0 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_EASTING, 0.0);
692 :
693 : const double dfFalseNorthing =
694 0 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_NORTHING, 0.0);
695 :
696 0 : SetStereographic(dfCenterLat, dfCenterLon, dfScale, dfFalseEasting,
697 : dfFalseNorthing);
698 :
699 0 : if (!bGotGeogCS)
700 0 : SetWellKnownGeogCS("WGS84");
701 : }
702 :
703 : // Geostationary.
704 13 : else if (EQUAL(pszGridMappingName, CF_PT_GEOS))
705 : {
706 : const double dfCenterLon =
707 3 : FetchDoubleParam(papszKeyValues, CF_PP_LON_PROJ_ORIGIN, 0.0);
708 :
709 3 : const double dfSatelliteHeight = FetchDoubleParam(
710 : papszKeyValues, CF_PP_PERSPECTIVE_POINT_HEIGHT, 35785831.0);
711 :
712 : const char *pszSweepAxisAngle =
713 3 : CSLFetchNameValue(papszKeyValues, CF_PP_SWEEP_ANGLE_AXIS);
714 :
715 : const double dfFalseEasting =
716 3 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_EASTING, 0.0);
717 :
718 : const double dfFalseNorthing =
719 3 : FetchDoubleParam(papszKeyValues, CF_PP_FALSE_NORTHING, 0.0);
720 :
721 3 : SetGEOS(dfCenterLon, dfSatelliteHeight, dfFalseEasting,
722 : dfFalseNorthing);
723 :
724 3 : if (!bGotGeogCS)
725 1 : SetWellKnownGeogCS("WGS84");
726 :
727 3 : if (pszSweepAxisAngle != nullptr && EQUAL(pszSweepAxisAngle, "x"))
728 : {
729 2 : char *pszProj4 = nullptr;
730 2 : exportToProj4(&pszProj4);
731 4 : CPLString osProj4 = pszProj4;
732 2 : osProj4 += " +sweep=x";
733 2 : SetExtension(GetRoot()->GetValue(), "PROJ4", osProj4);
734 2 : CPLFree(pszProj4);
735 : }
736 : }
737 :
738 10 : else if (EQUAL(pszGridMappingName, CF_PT_ROTATED_LATITUDE_LONGITUDE))
739 : {
740 3 : const double dfGridNorthPoleLong = FetchDoubleParam(
741 : papszKeyValues, CF_PP_GRID_NORTH_POLE_LONGITUDE, 0.0);
742 3 : const double dfGridNorthPoleLat = FetchDoubleParam(
743 : papszKeyValues, CF_PP_GRID_NORTH_POLE_LATITUDE, 0.0);
744 3 : const double dfNorthPoleGridLong = FetchDoubleParam(
745 : papszKeyValues, CF_PP_NORTH_POLE_GRID_LONGITUDE, 0.0);
746 :
747 3 : bRotatedPole = true;
748 3 : SetDerivedGeogCRSWithPoleRotationNetCDFCFConvention(
749 : "Rotated_pole", dfGridNorthPoleLat, dfGridNorthPoleLong,
750 : dfNorthPoleGridLong);
751 : }
752 :
753 35 : if (IsProjected())
754 : {
755 : const char *pszProjectedCRSName =
756 32 : CSLFetchNameValue(papszKeyValues, CF_PROJECTED_CRS_NAME);
757 32 : if (pszProjectedCRSName)
758 0 : SetProjCS(pszProjectedCRSName);
759 : }
760 :
761 : // Add units to PROJCS.
762 35 : if (IsGeographic() && !bRotatedPole)
763 : {
764 3 : SetAngularUnits(SRS_UA_DEGREE, CPLAtof(SRS_UA_DEGREE_CONV));
765 3 : SetAuthority("GEOGCS|UNIT", "EPSG", 9122);
766 : }
767 32 : else if (pszUnits != nullptr && !EQUAL(pszUnits, ""))
768 : {
769 24 : if (EQUAL(pszUnits, "m") || EQUAL(pszUnits, "metre") ||
770 13 : EQUAL(pszUnits, "meter"))
771 : {
772 11 : SetLinearUnits("metre", 1.0);
773 11 : SetAuthority("PROJCS|UNIT", "EPSG", 9001);
774 : }
775 13 : else if (EQUAL(pszUnits, "km"))
776 : {
777 6 : SetLinearUnits("kilometre", 1000.0);
778 6 : SetAuthority("PROJCS|UNIT", "EPSG", 9036);
779 : }
780 7 : else if (EQUAL(pszUnits, "US_survey_foot") ||
781 7 : EQUAL(pszUnits, "US_survey_feet"))
782 : {
783 0 : SetLinearUnits("US survey foot", CPLAtof(SRS_UL_US_FOOT_CONV));
784 0 : SetAuthority("PROJCS|UNIT", "EPSG", 9003);
785 : }
786 : else
787 : {
788 7 : CPLError(CE_Warning, CPLE_AppDefined,
789 : "Unhandled X/Y axis unit %s. SRS will ignore "
790 : "axis unit and be likely wrong.",
791 : pszUnits);
792 : }
793 : }
794 :
795 35 : return OGRERR_NONE;
796 : }
797 :
798 : /* -------------------------------------------------------------------- */
799 : /* CF-1 to GDAL mappings */
800 : /* -------------------------------------------------------------------- */
801 :
802 : /* Following are a series of mappings from CF-1 convention parameters
803 : * for each projection, to the equivalent in OGC WKT used internally by GDAL.
804 : * See: http://cf-pcmdi.llnl.gov/documents/cf-conventions/1.5/apf.html
805 : */
806 :
807 : /* A struct allowing us to map between GDAL(OGC WKT) and CF-1 attributes */
808 : typedef struct
809 : {
810 : const char *CF_ATT;
811 : const char *WKT_ATT;
812 : // TODO: mappings may need default values, like scale factor?
813 : // double defval;
814 : } oNetcdfSRS_PP;
815 :
816 : // default mappings, for the generic case
817 : /* These 'generic' mappings are based on what was previously in the
818 : poNetCDFSRS struct. They will be used as a fallback in case none
819 : of the others match (i.e. you are exporting a projection that has
820 : no CF-1 equivalent).
821 : They are not used for known CF-1 projections since there is not a
822 : unique 2-way projection-independent
823 : mapping between OGC WKT params and CF-1 ones: it varies per-projection.
824 : */
825 :
826 : static const oNetcdfSRS_PP poGenericMappings[] = {
827 : /* scale_factor is handled as a special case, write 2 values */
828 : {CF_PP_STD_PARALLEL_1, SRS_PP_STANDARD_PARALLEL_1},
829 : {CF_PP_STD_PARALLEL_2, SRS_PP_STANDARD_PARALLEL_2},
830 : {CF_PP_LONG_CENTRAL_MERIDIAN, SRS_PP_CENTRAL_MERIDIAN},
831 : {CF_PP_LONG_CENTRAL_MERIDIAN, SRS_PP_LONGITUDE_OF_CENTER},
832 : {CF_PP_LON_PROJ_ORIGIN, SRS_PP_LONGITUDE_OF_ORIGIN},
833 : // Multiple mappings to LAT_PROJ_ORIGIN
834 : {CF_PP_LAT_PROJ_ORIGIN, SRS_PP_LATITUDE_OF_ORIGIN},
835 : {CF_PP_LAT_PROJ_ORIGIN, SRS_PP_LATITUDE_OF_CENTER},
836 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
837 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
838 : {nullptr, nullptr},
839 : };
840 :
841 : // Albers equal area
842 : //
843 : // grid_mapping_name = albers_conical_equal_area
844 : // WKT: Albers_Conic_Equal_Area
845 : // EPSG:9822
846 : //
847 : // Map parameters:
848 : //
849 : // * standard_parallel - There may be 1 or 2 values.
850 : // * longitude_of_central_meridian
851 : // * latitude_of_projection_origin
852 : // * false_easting
853 : // * false_northing
854 : //
855 : static const oNetcdfSRS_PP poAEAMappings[] = {
856 : {CF_PP_STD_PARALLEL_1, SRS_PP_STANDARD_PARALLEL_1},
857 : {CF_PP_STD_PARALLEL_2, SRS_PP_STANDARD_PARALLEL_2},
858 : {CF_PP_LAT_PROJ_ORIGIN, SRS_PP_LATITUDE_OF_CENTER},
859 : {CF_PP_LONG_CENTRAL_MERIDIAN, SRS_PP_LONGITUDE_OF_CENTER},
860 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
861 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
862 : {nullptr, nullptr}};
863 :
864 : // Azimuthal equidistant
865 : //
866 : // grid_mapping_name = azimuthal_equidistant
867 : // WKT: Azimuthal_Equidistant
868 : //
869 : // Map parameters:
870 : //
871 : // * longitude_of_projection_origin
872 : // * latitude_of_projection_origin
873 : // * false_easting
874 : // * false_northing
875 : //
876 : static const oNetcdfSRS_PP poAEMappings[] = {
877 : {CF_PP_LAT_PROJ_ORIGIN, SRS_PP_LATITUDE_OF_CENTER},
878 : {CF_PP_LON_PROJ_ORIGIN, SRS_PP_LONGITUDE_OF_CENTER},
879 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
880 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
881 : {nullptr, nullptr}};
882 :
883 : // Lambert azimuthal equal area
884 : //
885 : // grid_mapping_name = lambert_azimuthal_equal_area
886 : // WKT: Lambert_Azimuthal_Equal_Area
887 : //
888 : // Map parameters:
889 : //
890 : // * longitude_of_projection_origin
891 : // * latitude_of_projection_origin
892 : // * false_easting
893 : // * false_northing
894 : //
895 : static const oNetcdfSRS_PP poLAEAMappings[] = {
896 : {CF_PP_LAT_PROJ_ORIGIN, SRS_PP_LATITUDE_OF_CENTER},
897 : {CF_PP_LON_PROJ_ORIGIN, SRS_PP_LONGITUDE_OF_CENTER},
898 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
899 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
900 : {nullptr, nullptr}};
901 :
902 : // Lambert conformal
903 : //
904 : // grid_mapping_name = lambert_conformal_conic
905 : // WKT: Lambert_Conformal_Conic_1SP / Lambert_Conformal_Conic_2SP
906 : //
907 : // Map parameters:
908 : //
909 : // * standard_parallel - There may be 1 or 2 values.
910 : // * longitude_of_central_meridian
911 : // * latitude_of_projection_origin
912 : // * false_easting
913 : // * false_northing
914 : //
915 : // See
916 : // http://www.remotesensing.org/geotiff/proj_list/lambert_conic_conformal_1sp.html
917 :
918 : // Lambert conformal conic - 1SP
919 : /* See bug # 3324
920 : It seems that the missing scale factor can be computed from
921 : standard_parallel1 and latitude_of_projection_origin. If both are equal (the
922 : common case) then scale factor=1, else use Snyder eq. 15-4. We save in the
923 : WKT standard_parallel1 for export to CF, but do not export scale factor. If a
924 : WKT has a scale factor != 1 and no standard_parallel1 then export is not CF,
925 : but we output scale factor for compat. is there a formula for that?
926 : */
927 : static const oNetcdfSRS_PP poLCC1SPMappings[] = {
928 : {CF_PP_STD_PARALLEL_1, SRS_PP_STANDARD_PARALLEL_1},
929 : {CF_PP_LAT_PROJ_ORIGIN, SRS_PP_LATITUDE_OF_ORIGIN},
930 : {CF_PP_LONG_CENTRAL_MERIDIAN, SRS_PP_CENTRAL_MERIDIAN},
931 : {CF_PP_SCALE_FACTOR_ORIGIN, SRS_PP_SCALE_FACTOR}, /* special case */
932 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
933 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
934 : {nullptr, nullptr}};
935 :
936 : // Lambert conformal conic - 2SP
937 : static const oNetcdfSRS_PP poLCC2SPMappings[] = {
938 : {CF_PP_STD_PARALLEL_1, SRS_PP_STANDARD_PARALLEL_1},
939 : {CF_PP_STD_PARALLEL_2, SRS_PP_STANDARD_PARALLEL_2},
940 : {CF_PP_LAT_PROJ_ORIGIN, SRS_PP_LATITUDE_OF_ORIGIN},
941 : {CF_PP_LONG_CENTRAL_MERIDIAN, SRS_PP_CENTRAL_MERIDIAN},
942 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
943 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
944 : {nullptr, nullptr}};
945 :
946 : // Lambert cylindrical equal area
947 : //
948 : // grid_mapping_name = lambert_cylindrical_equal_area
949 : // WKT: Cylindrical_Equal_Area
950 : // EPSG:9834 (Spherical) and EPSG:9835
951 : //
952 : // Map parameters:
953 : //
954 : // * longitude_of_central_meridian
955 : // * either standard_parallel or scale_factor_at_projection_origin
956 : // * false_easting
957 : // * false_northing
958 : //
959 : // NB: CF-1 specifies a 'scale_factor_at_projection' alternative
960 : // to std_parallel ... but no reference to this in EPSG/remotesensing.org
961 : // ignore for now.
962 : //
963 : static const oNetcdfSRS_PP poLCEAMappings[] = {
964 : {CF_PP_STD_PARALLEL_1, SRS_PP_STANDARD_PARALLEL_1},
965 : {CF_PP_LONG_CENTRAL_MERIDIAN, SRS_PP_CENTRAL_MERIDIAN},
966 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
967 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
968 : {nullptr, nullptr}};
969 :
970 : // Latitude-Longitude
971 : //
972 : // grid_mapping_name = latitude_longitude
973 : //
974 : // Map parameters:
975 : //
976 : // * None
977 : //
978 : // NB: handled as a special case - !isProjected()
979 :
980 : // Mercator
981 : //
982 : // grid_mapping_name = mercator
983 : // WKT: Mercator_1SP / Mercator_2SP
984 : //
985 : // Map parameters:
986 : //
987 : // * longitude_of_projection_origin
988 : // * either standard_parallel or scale_factor_at_projection_origin
989 : // * false_easting
990 : // * false_northing
991 :
992 : // Mercator 1 Standard Parallel (EPSG:9804)
993 : static const oNetcdfSRS_PP poM1SPMappings[] = {
994 : {CF_PP_LON_PROJ_ORIGIN, SRS_PP_CENTRAL_MERIDIAN},
995 : // LAT_PROJ_ORIGIN is always equator (0) in CF-1
996 : {CF_PP_SCALE_FACTOR_ORIGIN, SRS_PP_SCALE_FACTOR},
997 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
998 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
999 : {nullptr, nullptr}};
1000 :
1001 : // Mercator 2 Standard Parallel
1002 : static const oNetcdfSRS_PP poM2SPMappings[] = {
1003 : {CF_PP_LON_PROJ_ORIGIN, SRS_PP_CENTRAL_MERIDIAN},
1004 : {CF_PP_STD_PARALLEL_1, SRS_PP_STANDARD_PARALLEL_1},
1005 : // From best understanding of this projection, only
1006 : // actually specify one SP - it is the same N/S of equator.
1007 : // {CF_PP_STD_PARALLEL_2, SRS_PP_LATITUDE_OF_ORIGIN},
1008 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
1009 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
1010 : {nullptr, nullptr}};
1011 :
1012 : // Orthographic
1013 : // grid_mapping_name = orthographic
1014 : // WKT: Orthographic
1015 : //
1016 : // Map parameters:
1017 : //
1018 : // * longitude_of_projection_origin
1019 : // * latitude_of_projection_origin
1020 : // * false_easting
1021 : // * false_northing
1022 : //
1023 : static const oNetcdfSRS_PP poOrthoMappings[] = {
1024 : {CF_PP_LAT_PROJ_ORIGIN, SRS_PP_LATITUDE_OF_ORIGIN},
1025 : {CF_PP_LON_PROJ_ORIGIN, SRS_PP_CENTRAL_MERIDIAN},
1026 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
1027 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
1028 : {nullptr, nullptr}};
1029 :
1030 : // Polar stereographic
1031 : //
1032 : // grid_mapping_name = polar_stereographic
1033 : // WKT: Polar_Stereographic
1034 : //
1035 : // Map parameters:
1036 : //
1037 : // * straight_vertical_longitude_from_pole
1038 : // * latitude_of_projection_origin - Either +90. or -90.
1039 : // * Either standard_parallel or scale_factor_at_projection_origin
1040 : // * false_easting
1041 : // * false_northing
1042 :
1043 : static const oNetcdfSRS_PP poPSmappings[] = {
1044 : /* {CF_PP_STD_PARALLEL_1, SRS_PP_LATITUDE_OF_ORIGIN}, */
1045 : /* {CF_PP_SCALE_FACTOR_ORIGIN, SRS_PP_SCALE_FACTOR}, */
1046 : {CF_PP_VERT_LONG_FROM_POLE, SRS_PP_CENTRAL_MERIDIAN},
1047 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
1048 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
1049 : {nullptr, nullptr}};
1050 :
1051 : // Rotated Pole
1052 : //
1053 : // grid_mapping_name = rotated_latitude_longitude
1054 : // WKT: N/A
1055 : //
1056 : // Map parameters:
1057 : //
1058 : // * grid_north_pole_latitude
1059 : // * grid_north_pole_longitude
1060 : // * north_pole_grid_longitude - This parameter is optional (default is 0.).
1061 :
1062 : // No WKT equivalent
1063 :
1064 : // Stereographic
1065 : //
1066 : // grid_mapping_name = stereographic
1067 : // WKT: Stereographic (and/or Oblique_Stereographic??)
1068 : //
1069 : // Map parameters:
1070 : //
1071 : // * longitude_of_projection_origin
1072 : // * latitude_of_projection_origin
1073 : // * scale_factor_at_projection_origin
1074 : // * false_easting
1075 : // * false_northing
1076 : //
1077 : // NB: see bug#4267 Stereographic vs. Oblique_Stereographic
1078 : //
1079 : static const oNetcdfSRS_PP poStMappings[] = {
1080 : {CF_PP_LAT_PROJ_ORIGIN, SRS_PP_LATITUDE_OF_ORIGIN},
1081 : {CF_PP_LON_PROJ_ORIGIN, SRS_PP_CENTRAL_MERIDIAN},
1082 : {CF_PP_SCALE_FACTOR_ORIGIN, SRS_PP_SCALE_FACTOR},
1083 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
1084 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
1085 : {nullptr, nullptr}};
1086 :
1087 : // Transverse Mercator
1088 : //
1089 : // grid_mapping_name = transverse_mercator
1090 : // WKT: Transverse_Mercator
1091 : //
1092 : // Map parameters:
1093 : //
1094 : // * scale_factor_at_central_meridian
1095 : // * longitude_of_central_meridian
1096 : // * latitude_of_projection_origin
1097 : // * false_easting
1098 : // * false_northing
1099 : //
1100 : static const oNetcdfSRS_PP poTMMappings[] = {
1101 : {CF_PP_SCALE_FACTOR_MERIDIAN, SRS_PP_SCALE_FACTOR},
1102 : {CF_PP_LONG_CENTRAL_MERIDIAN, SRS_PP_CENTRAL_MERIDIAN},
1103 : {CF_PP_LAT_PROJ_ORIGIN, SRS_PP_LATITUDE_OF_ORIGIN},
1104 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
1105 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
1106 : {nullptr, nullptr}};
1107 :
1108 : // Vertical perspective
1109 : //
1110 : // grid_mapping_name = vertical_perspective
1111 : // WKT: ???
1112 : //
1113 : // Map parameters:
1114 : //
1115 : // * latitude_of_projection_origin
1116 : // * longitude_of_projection_origin
1117 : // * perspective_point_height
1118 : // * false_easting
1119 : // * false_northing
1120 : //
1121 : // TODO: see how to map this to OGR
1122 :
1123 : static const oNetcdfSRS_PP poGEOSMappings[] = {
1124 : {CF_PP_LON_PROJ_ORIGIN, SRS_PP_CENTRAL_MERIDIAN},
1125 : {CF_PP_PERSPECTIVE_POINT_HEIGHT, SRS_PP_SATELLITE_HEIGHT},
1126 : {CF_PP_FALSE_EASTING, SRS_PP_FALSE_EASTING},
1127 : {CF_PP_FALSE_NORTHING, SRS_PP_FALSE_NORTHING},
1128 : /* { CF_PP_SWEEP_ANGLE_AXIS, .... } handled as a proj.4 extension */
1129 : {nullptr, nullptr}};
1130 :
1131 : /* Mappings for various projections, including netcdf and GDAL projection names
1132 : and corresponding oNetcdfSRS_PP mapping struct.
1133 : A NULL mappings value means that the projection is not included in the CF
1134 : standard and the generic mapping (poGenericMappings) will be used. */
1135 : typedef struct
1136 : {
1137 : const char *CF_SRS;
1138 : const char *WKT_SRS;
1139 : const oNetcdfSRS_PP *mappings;
1140 : } oNetcdfSRS_PT;
1141 :
1142 : static const oNetcdfSRS_PT poNetcdfSRS_PT[] = {
1143 : {CF_PT_AEA, SRS_PT_ALBERS_CONIC_EQUAL_AREA, poAEAMappings},
1144 : {CF_PT_AE, SRS_PT_AZIMUTHAL_EQUIDISTANT, poAEMappings},
1145 : {"cassini_soldner", SRS_PT_CASSINI_SOLDNER, nullptr},
1146 : {CF_PT_LCEA, SRS_PT_CYLINDRICAL_EQUAL_AREA, poLCEAMappings},
1147 : {"eckert_iv", SRS_PT_ECKERT_IV, nullptr},
1148 : {"eckert_vi", SRS_PT_ECKERT_VI, nullptr},
1149 : {"equidistant_conic", SRS_PT_EQUIDISTANT_CONIC, nullptr},
1150 : {"equirectangular", SRS_PT_EQUIRECTANGULAR, nullptr},
1151 : {"gall_stereographic", SRS_PT_GALL_STEREOGRAPHIC, nullptr},
1152 : {CF_PT_GEOS, SRS_PT_GEOSTATIONARY_SATELLITE, poGEOSMappings},
1153 : {"goode_homolosine", SRS_PT_GOODE_HOMOLOSINE, nullptr},
1154 : {"gnomonic", SRS_PT_GNOMONIC, nullptr},
1155 : {"hotine_oblique_mercator", SRS_PT_HOTINE_OBLIQUE_MERCATOR, nullptr},
1156 : {"hotine_oblique_mercator_2P",
1157 : SRS_PT_HOTINE_OBLIQUE_MERCATOR_TWO_POINT_NATURAL_ORIGIN, nullptr},
1158 : {"laborde_oblique_mercator", SRS_PT_LABORDE_OBLIQUE_MERCATOR, nullptr},
1159 : {CF_PT_LCC, SRS_PT_LAMBERT_CONFORMAL_CONIC_1SP, poLCC1SPMappings},
1160 : {CF_PT_LCC, SRS_PT_LAMBERT_CONFORMAL_CONIC_2SP, poLCC2SPMappings},
1161 : {CF_PT_LAEA, SRS_PT_LAMBERT_AZIMUTHAL_EQUAL_AREA, poLAEAMappings},
1162 : {CF_PT_MERCATOR, SRS_PT_MERCATOR_1SP, poM1SPMappings},
1163 : {CF_PT_MERCATOR, SRS_PT_MERCATOR_2SP, poM2SPMappings},
1164 : {"miller_cylindrical", SRS_PT_MILLER_CYLINDRICAL, nullptr},
1165 : {"mollweide", SRS_PT_MOLLWEIDE, nullptr},
1166 : {"new_zealand_map_grid", SRS_PT_NEW_ZEALAND_MAP_GRID, nullptr},
1167 : /* for now map to STEREO, see bug #4267 */
1168 : {"oblique_stereographic", SRS_PT_OBLIQUE_STEREOGRAPHIC, nullptr},
1169 : /* {STEREO, SRS_PT_OBLIQUE_STEREOGRAPHIC, poStMappings }, */
1170 : {CF_PT_ORTHOGRAPHIC, SRS_PT_ORTHOGRAPHIC, poOrthoMappings},
1171 : {CF_PT_POLAR_STEREO, SRS_PT_POLAR_STEREOGRAPHIC, poPSmappings},
1172 : {"polyconic", SRS_PT_POLYCONIC, nullptr},
1173 : {"robinson", SRS_PT_ROBINSON, nullptr},
1174 : {"sinusoidal", SRS_PT_SINUSOIDAL, nullptr},
1175 : {CF_PT_STEREO, SRS_PT_STEREOGRAPHIC, poStMappings},
1176 : {"swiss_oblique_cylindrical", SRS_PT_SWISS_OBLIQUE_CYLINDRICAL, nullptr},
1177 : {CF_PT_TM, SRS_PT_TRANSVERSE_MERCATOR, poTMMappings},
1178 : {"TM_south_oriented", SRS_PT_TRANSVERSE_MERCATOR_SOUTH_ORIENTED, nullptr},
1179 : {nullptr, nullptr, nullptr},
1180 : };
1181 :
1182 : /* Write any needed projection attributes *
1183 : * poPROJCS: ptr to proj crd system
1184 : * pszProjection: name of projection system in GDAL WKT
1185 : *
1186 : * The function first looks for the oNetcdfSRS_PP mapping object
1187 : * that corresponds to the input projection name. If none is found
1188 : * the generic mapping is used. In the case of specific mappings,
1189 : * the driver looks for each attribute listed in the mapping object
1190 : * and then looks up the value within the OGR_SRSNode. In the case
1191 : * of the generic mapping, the lookup is reversed (projection params,
1192 : * then mapping). For more generic code, GDAL->NETCDF
1193 : * mappings and the associated value are saved in std::map objects.
1194 : */
1195 :
1196 : // NOTE: modifications by ET to combine the specific and generic mappings.
1197 :
1198 : static std::vector<std::pair<std::string, double>>
1199 43 : NCDFGetProjAttribs(const OGR_SRSNode *poPROJCS, const char *pszProjection)
1200 : {
1201 43 : const oNetcdfSRS_PP *poMap = nullptr;
1202 43 : int nMapIndex = -1;
1203 :
1204 : // Find the appropriate mapping.
1205 1319 : for (int iMap = 0; poNetcdfSRS_PT[iMap].WKT_SRS != nullptr; iMap++)
1206 : {
1207 1319 : if (EQUAL(pszProjection, poNetcdfSRS_PT[iMap].WKT_SRS))
1208 : {
1209 43 : nMapIndex = iMap;
1210 43 : poMap = poNetcdfSRS_PT[iMap].mappings;
1211 43 : break;
1212 : }
1213 : }
1214 :
1215 : // ET TODO if projection name is not found, should we do something special?
1216 43 : if (nMapIndex == -1)
1217 : {
1218 0 : CPLError(CE_Warning, CPLE_AppDefined,
1219 : "projection name %s not found in the lookup tables!",
1220 : pszProjection);
1221 : }
1222 : // If no mapping was found or assigned, set the generic one.
1223 43 : if (!poMap)
1224 : {
1225 0 : CPLError(CE_Warning, CPLE_AppDefined,
1226 : "projection name %s in not part of the CF standard, "
1227 : "will not be supported by CF!",
1228 : pszProjection);
1229 0 : poMap = poGenericMappings;
1230 : }
1231 :
1232 : // Initialize local map objects.
1233 :
1234 : // Attribute <GDAL,NCDF> and Value <NCDF,value> mappings
1235 86 : std::map<std::string, std::string> oAttMap;
1236 257 : for (int iMap = 0; poMap[iMap].WKT_ATT != nullptr; iMap++)
1237 : {
1238 214 : oAttMap[poMap[iMap].WKT_ATT] = poMap[iMap].CF_ATT;
1239 : }
1240 :
1241 43 : const char *pszParamVal = nullptr;
1242 86 : std::map<std::string, double> oValMap;
1243 556 : for (int iChild = 0; iChild < poPROJCS->GetChildCount(); iChild++)
1244 : {
1245 513 : const OGR_SRSNode *poNode = poPROJCS->GetChild(iChild);
1246 730 : if (!EQUAL(poNode->GetValue(), "PARAMETER") ||
1247 217 : poNode->GetChildCount() != 2)
1248 296 : continue;
1249 217 : const char *pszParamStr = poNode->GetChild(0)->GetValue();
1250 217 : pszParamVal = poNode->GetChild(1)->GetValue();
1251 :
1252 217 : oValMap[pszParamStr] = CPLAtof(pszParamVal);
1253 : }
1254 :
1255 : // Results to write.
1256 43 : std::vector<std::pair<std::string, double>> oOutList;
1257 :
1258 : // Lookup mappings and fill output vector.
1259 43 : if (poMap != poGenericMappings)
1260 : {
1261 : // special case for PS (Polar Stereographic) grid.
1262 43 : if (EQUAL(pszProjection, SRS_PT_POLAR_STEREOGRAPHIC))
1263 : {
1264 2 : const double dfLat = oValMap[SRS_PP_LATITUDE_OF_ORIGIN];
1265 :
1266 2 : auto oScaleFactorIter = oValMap.find(SRS_PP_SCALE_FACTOR);
1267 2 : if (oScaleFactorIter != oValMap.end())
1268 : {
1269 : // Polar Stereographic (variant A)
1270 1 : const double dfScaleFactor = oScaleFactorIter->second;
1271 : // dfLat should be +/- 90
1272 1 : oOutList.push_back(
1273 2 : std::make_pair(std::string(CF_PP_LAT_PROJ_ORIGIN), dfLat));
1274 1 : oOutList.push_back(std::make_pair(
1275 2 : std::string(CF_PP_SCALE_FACTOR_ORIGIN), dfScaleFactor));
1276 : }
1277 : else
1278 : {
1279 : // Polar Stereographic (variant B)
1280 1 : const double dfLatPole = (dfLat > 0) ? 90.0 : -90.0;
1281 1 : oOutList.push_back(std::make_pair(
1282 2 : std::string(CF_PP_LAT_PROJ_ORIGIN), dfLatPole));
1283 1 : oOutList.push_back(
1284 2 : std::make_pair(std::string(CF_PP_STD_PARALLEL), dfLat));
1285 : }
1286 : }
1287 :
1288 : // Specific mapping, loop over mapping values.
1289 257 : for (const auto &oAttIter : oAttMap)
1290 : {
1291 214 : const std::string &osGDALAtt = oAttIter.first;
1292 214 : const std::string &osNCDFAtt = oAttIter.second;
1293 214 : const auto oValIter = oValMap.find(osGDALAtt);
1294 :
1295 214 : if (oValIter != oValMap.end())
1296 : {
1297 214 : double dfValue = oValIter->second;
1298 214 : bool bWriteVal = true;
1299 :
1300 : // special case for LCC-1SP
1301 : // See comments in netcdfdataset.h for this projection.
1302 252 : if (EQUAL(SRS_PP_SCALE_FACTOR, osGDALAtt.c_str()) &&
1303 38 : EQUAL(pszProjection, SRS_PT_LAMBERT_CONFORMAL_CONIC_1SP))
1304 : {
1305 : // Default is to not write as it is not CF-1.
1306 0 : bWriteVal = false;
1307 : // Test if there is no standard_parallel1.
1308 0 : if (oValMap.find(std::string(CF_PP_STD_PARALLEL_1)) ==
1309 0 : oValMap.end())
1310 : {
1311 : // If scale factor != 1.0, write value for GDAL, but
1312 : // this is not supported by CF-1.
1313 0 : if (!CPLIsEqual(dfValue, 1.0))
1314 : {
1315 0 : CPLError(
1316 : CE_Failure, CPLE_NotSupported,
1317 : "NetCDF driver export of LCC-1SP with scale "
1318 : "factor != 1.0 and no standard_parallel1 is "
1319 : "not CF-1 (bug #3324). Use the 2SP variant "
1320 : "which is supported by CF.");
1321 0 : bWriteVal = true;
1322 : }
1323 : // Else copy standard_parallel1 from
1324 : // latitude_of_origin, because scale_factor=1.0.
1325 : else
1326 : {
1327 : const auto oValIter2 = oValMap.find(
1328 0 : std::string(SRS_PP_LATITUDE_OF_ORIGIN));
1329 0 : if (oValIter2 != oValMap.end())
1330 : {
1331 0 : oOutList.push_back(std::make_pair(
1332 0 : std::string(CF_PP_STD_PARALLEL_1),
1333 0 : oValIter2->second));
1334 : }
1335 : else
1336 : {
1337 0 : CPLError(CE_Failure, CPLE_NotSupported,
1338 : "NetCDF driver export of LCC-1SP with "
1339 : "no standard_parallel1 "
1340 : "and no latitude_of_origin is not "
1341 : "supported (bug #3324).");
1342 : }
1343 : }
1344 : }
1345 : }
1346 214 : if (bWriteVal)
1347 214 : oOutList.push_back(std::make_pair(osNCDFAtt, dfValue));
1348 : }
1349 : #ifdef NCDF_DEBUG
1350 : else
1351 : {
1352 : CPLDebug("GDAL_netCDF", "NOT FOUND!");
1353 : }
1354 : #endif
1355 : }
1356 : }
1357 : else
1358 : {
1359 : // Generic mapping, loop over projected values.
1360 0 : for (const auto &oValIter : oValMap)
1361 : {
1362 0 : const auto &osGDALAtt = oValIter.first;
1363 0 : const double dfValue = oValIter.second;
1364 :
1365 0 : const auto &oAttIter = oAttMap.find(osGDALAtt);
1366 :
1367 0 : if (oAttIter != oAttMap.end())
1368 : {
1369 0 : oOutList.push_back(std::make_pair(oAttIter->second, dfValue));
1370 : }
1371 : /* for SRS_PP_SCALE_FACTOR write 2 mappings */
1372 0 : else if (EQUAL(osGDALAtt.c_str(), SRS_PP_SCALE_FACTOR))
1373 : {
1374 0 : oOutList.push_back(std::make_pair(
1375 0 : std::string(CF_PP_SCALE_FACTOR_MERIDIAN), dfValue));
1376 0 : oOutList.push_back(std::make_pair(
1377 0 : std::string(CF_PP_SCALE_FACTOR_ORIGIN), dfValue));
1378 : }
1379 : /* if not found insert the GDAL name */
1380 : else
1381 : {
1382 0 : oOutList.push_back(std::make_pair(osGDALAtt, dfValue));
1383 : }
1384 : }
1385 : }
1386 :
1387 86 : return oOutList;
1388 : }
1389 :
1390 : /************************************************************************/
1391 : /* exportToCF1() */
1392 : /************************************************************************/
1393 :
1394 : /**
1395 : * \brief Export a CRS to netCDF CF-1 definitions.
1396 : *
1397 : * http://cfconventions.org/cf-conventions/cf-conventions.html#appendix-grid-mappings
1398 : *
1399 : * This function is the equivalent of the C function OSRExportToCF1().
1400 : *
1401 : * @param[out] ppszGridMappingName Pointer to the suggested name for the grid
1402 : * mapping variable. ppszGridMappingName may be nullptr.
1403 : * *ppszGridMappingName should be freed with CPLFree().
1404 : *
1405 : * @param[out] ppapszKeyValues Pointer to a null-terminated list of key/value pairs,
1406 : * to write into the grid mapping variable. ppapszKeyValues may be
1407 : * nullptr. *ppapszKeyValues should be freed with CSLDestroy()
1408 : * Values may be of type string, double or a list of 2 double values (comma
1409 : * separated).
1410 : *
1411 : * @param[out] ppszUnits Pointer to the value of the "units" attribute of the
1412 : * X/Y arrays. ppszGridMappingName may be nullptr. *ppszUnits should be freed with
1413 : * CPLFree().
1414 : *
1415 : * @param[in] papszOptions Options. Currently none supported
1416 : *
1417 : * @return OGRERR_NONE on success or an error code in case of failure.
1418 : * @since 3.9
1419 : */
1420 :
1421 : OGRErr
1422 209 : OGRSpatialReference::exportToCF1(char **ppszGridMappingName,
1423 : char ***ppapszKeyValues, char **ppszUnits,
1424 : CPL_UNUSED CSLConstList papszOptions) const
1425 : {
1426 209 : if (ppszGridMappingName)
1427 123 : *ppszGridMappingName = nullptr;
1428 :
1429 209 : if (ppapszKeyValues)
1430 127 : *ppapszKeyValues = nullptr;
1431 :
1432 209 : if (ppszUnits)
1433 38 : *ppszUnits = nullptr;
1434 :
1435 209 : if (ppszGridMappingName || ppapszKeyValues)
1436 : {
1437 127 : bool bWriteWkt = true;
1438 :
1439 : struct Value
1440 : {
1441 : std::string key{};
1442 : std::string valueStr{};
1443 : std::vector<double> doubles{};
1444 : };
1445 :
1446 127 : std::vector<Value> oParams;
1447 :
1448 : const auto addParamString =
1449 762 : [&oParams](const char *key, const char *value)
1450 : {
1451 762 : Value v;
1452 381 : v.key = key;
1453 381 : v.valueStr = value;
1454 381 : oParams.emplace_back(std::move(v));
1455 381 : };
1456 :
1457 1186 : const auto addParamDouble = [&oParams](const char *key, double value)
1458 : {
1459 1186 : Value v;
1460 593 : v.key = key;
1461 593 : v.doubles.push_back(value);
1462 593 : oParams.emplace_back(std::move(v));
1463 593 : };
1464 :
1465 : const auto addParam2Double =
1466 6 : [&oParams](const char *key, double value1, double value2)
1467 : {
1468 6 : Value v;
1469 3 : v.key = key;
1470 3 : v.doubles.push_back(value1);
1471 3 : v.doubles.push_back(value2);
1472 3 : oParams.emplace_back(std::move(v));
1473 3 : };
1474 :
1475 127 : std::string osCFProjection;
1476 127 : if (IsProjected())
1477 : {
1478 : // Write CF-1.5 compliant Projected attributes.
1479 :
1480 43 : const OGR_SRSNode *poPROJCS = GetAttrNode("PROJCS");
1481 43 : if (poPROJCS == nullptr)
1482 0 : return OGRERR_FAILURE;
1483 43 : const char *pszProjName = GetAttrValue("PROJECTION");
1484 43 : if (pszProjName == nullptr)
1485 0 : return OGRERR_FAILURE;
1486 :
1487 : // Basic Projection info (grid_mapping and datum).
1488 1319 : for (int i = 0; poNetcdfSRS_PT[i].WKT_SRS != nullptr; i++)
1489 : {
1490 1319 : if (EQUAL(poNetcdfSRS_PT[i].WKT_SRS, pszProjName))
1491 : {
1492 43 : osCFProjection = poNetcdfSRS_PT[i].CF_SRS;
1493 43 : break;
1494 : }
1495 : }
1496 43 : if (osCFProjection.empty())
1497 0 : return OGRERR_FAILURE;
1498 :
1499 43 : addParamString(CF_GRD_MAPPING_NAME, osCFProjection.c_str());
1500 :
1501 : // Various projection attributes.
1502 : // PDS: keep in sync with SetProjection function
1503 86 : auto oOutList = NCDFGetProjAttribs(poPROJCS, pszProjName);
1504 :
1505 : /* Write all the values that were found */
1506 43 : double dfStdP[2] = {0, 0};
1507 43 : bool bFoundStdP1 = false;
1508 43 : bool bFoundStdP2 = false;
1509 261 : for (const auto &it : oOutList)
1510 : {
1511 218 : const char *pszParamVal = it.first.c_str();
1512 218 : double dfValue = it.second;
1513 : /* Handle the STD_PARALLEL attrib */
1514 218 : if (EQUAL(pszParamVal, CF_PP_STD_PARALLEL_1))
1515 : {
1516 3 : bFoundStdP1 = true;
1517 3 : dfStdP[0] = dfValue;
1518 : }
1519 215 : else if (EQUAL(pszParamVal, CF_PP_STD_PARALLEL_2))
1520 : {
1521 3 : bFoundStdP2 = true;
1522 3 : dfStdP[1] = dfValue;
1523 : }
1524 : else
1525 : {
1526 212 : addParamDouble(pszParamVal, dfValue);
1527 : }
1528 : }
1529 : /* Now write the STD_PARALLEL attrib */
1530 43 : if (bFoundStdP1)
1531 : {
1532 : /* one value */
1533 3 : if (!bFoundStdP2)
1534 : {
1535 0 : addParamDouble(CF_PP_STD_PARALLEL, dfStdP[0]);
1536 : }
1537 : else
1538 : {
1539 : // Two values.
1540 3 : addParam2Double(CF_PP_STD_PARALLEL, dfStdP[0], dfStdP[1]);
1541 : }
1542 : }
1543 :
1544 43 : if (EQUAL(pszProjName, SRS_PT_GEOSTATIONARY_SATELLITE))
1545 : {
1546 : const char *pszPredefProj4 =
1547 0 : GetExtension(GetRoot()->GetValue(), "PROJ4", nullptr);
1548 0 : const char *pszSweepAxisAngle =
1549 0 : (pszPredefProj4 != nullptr &&
1550 0 : strstr(pszPredefProj4, "+sweep=x"))
1551 : ? "x"
1552 : : "y";
1553 0 : addParamString(CF_PP_SWEEP_ANGLE_AXIS, pszSweepAxisAngle);
1554 : }
1555 : }
1556 84 : else if (IsDerivedGeographic())
1557 : {
1558 0 : const OGR_SRSNode *poConversion = GetAttrNode("DERIVINGCONVERSION");
1559 0 : if (poConversion == nullptr)
1560 0 : return OGRERR_FAILURE;
1561 0 : const char *pszMethod = GetAttrValue("METHOD");
1562 0 : if (pszMethod == nullptr)
1563 0 : return OGRERR_FAILURE;
1564 :
1565 0 : std::map<std::string, double> oValMap;
1566 0 : for (int iChild = 0; iChild < poConversion->GetChildCount();
1567 : iChild++)
1568 : {
1569 0 : const OGR_SRSNode *poNode = poConversion->GetChild(iChild);
1570 0 : if (!EQUAL(poNode->GetValue(), "PARAMETER") ||
1571 0 : poNode->GetChildCount() <= 2)
1572 0 : continue;
1573 0 : const char *pszParamStr = poNode->GetChild(0)->GetValue();
1574 0 : const char *pszParamVal = poNode->GetChild(1)->GetValue();
1575 0 : oValMap[pszParamStr] = CPLAtof(pszParamVal);
1576 : }
1577 :
1578 0 : constexpr const char *ROTATED_POLE_VAR_NAME = "rotated_pole";
1579 0 : if (EQUAL(pszMethod, "PROJ ob_tran o_proj=longlat"))
1580 : {
1581 : // Not enough interoperable to be written as WKT
1582 0 : bWriteWkt = false;
1583 :
1584 0 : const double dfLon0 = oValMap["lon_0"];
1585 0 : const double dfLonp = oValMap["o_lon_p"];
1586 0 : const double dfLatp = oValMap["o_lat_p"];
1587 :
1588 0 : osCFProjection = ROTATED_POLE_VAR_NAME;
1589 0 : addParamString(CF_GRD_MAPPING_NAME,
1590 : CF_PT_ROTATED_LATITUDE_LONGITUDE);
1591 0 : addParamDouble(CF_PP_GRID_NORTH_POLE_LONGITUDE, dfLon0 - 180);
1592 0 : addParamDouble(CF_PP_GRID_NORTH_POLE_LATITUDE, dfLatp);
1593 0 : addParamDouble(CF_PP_NORTH_POLE_GRID_LONGITUDE, dfLonp);
1594 : }
1595 0 : else if (EQUAL(pszMethod, "Pole rotation (netCDF CF convention)"))
1596 : {
1597 : // Not enough interoperable to be written as WKT
1598 0 : bWriteWkt = false;
1599 :
1600 : const double dfGridNorthPoleLat =
1601 0 : oValMap["Grid north pole latitude (netCDF CF convention)"];
1602 : const double dfGridNorthPoleLong =
1603 0 : oValMap["Grid north pole longitude (netCDF CF convention)"];
1604 : const double dfNorthPoleGridLong =
1605 0 : oValMap["North pole grid longitude (netCDF CF convention)"];
1606 :
1607 0 : osCFProjection = ROTATED_POLE_VAR_NAME;
1608 0 : addParamString(CF_GRD_MAPPING_NAME,
1609 : CF_PT_ROTATED_LATITUDE_LONGITUDE);
1610 0 : addParamDouble(CF_PP_GRID_NORTH_POLE_LONGITUDE,
1611 : dfGridNorthPoleLong);
1612 0 : addParamDouble(CF_PP_GRID_NORTH_POLE_LATITUDE,
1613 : dfGridNorthPoleLat);
1614 0 : addParamDouble(CF_PP_NORTH_POLE_GRID_LONGITUDE,
1615 : dfNorthPoleGridLong);
1616 : }
1617 0 : else if (EQUAL(pszMethod, "Pole rotation (GRIB convention)"))
1618 : {
1619 : // Not enough interoperable to be written as WKT
1620 0 : bWriteWkt = false;
1621 :
1622 : const double dfLatSouthernPole =
1623 0 : oValMap["Latitude of the southern pole (GRIB convention)"];
1624 : const double dfLonSouthernPole =
1625 0 : oValMap["Longitude of the southern pole (GRIB convention)"];
1626 : const double dfAxisRotation =
1627 0 : oValMap["Axis rotation (GRIB convention)"];
1628 :
1629 0 : const double dfLon0 = dfLonSouthernPole;
1630 0 : const double dfLonp = dfAxisRotation == 0 ? 0 : -dfAxisRotation;
1631 0 : const double dfLatp =
1632 0 : dfLatSouthernPole == 0 ? 0 : -dfLatSouthernPole;
1633 :
1634 0 : osCFProjection = ROTATED_POLE_VAR_NAME;
1635 0 : addParamString(CF_GRD_MAPPING_NAME,
1636 : CF_PT_ROTATED_LATITUDE_LONGITUDE);
1637 0 : addParamDouble(CF_PP_GRID_NORTH_POLE_LONGITUDE, dfLon0 - 180);
1638 0 : addParamDouble(CF_PP_GRID_NORTH_POLE_LATITUDE, dfLatp);
1639 0 : addParamDouble(CF_PP_NORTH_POLE_GRID_LONGITUDE, dfLonp);
1640 : }
1641 : else
1642 : {
1643 0 : CPLError(CE_Failure, CPLE_NotSupported,
1644 : "Unsupported method for DerivedGeographicCRS: %s",
1645 : pszMethod);
1646 0 : return OGRERR_FAILURE;
1647 : }
1648 : }
1649 : else
1650 : {
1651 : // Write CF-1.5 compliant Geographics attributes.
1652 : // Note: WKT information will not be preserved (e.g. WGS84).
1653 84 : osCFProjection = "crs";
1654 84 : addParamString(CF_GRD_MAPPING_NAME, CF_PT_LATITUDE_LONGITUDE);
1655 : }
1656 :
1657 127 : constexpr const char *CF_LNG_NAME = "long_name";
1658 127 : addParamString(CF_LNG_NAME, "CRS definition");
1659 :
1660 : // Write CF-1.5 compliant common attributes.
1661 :
1662 : // DATUM information.
1663 127 : addParamDouble(CF_PP_LONG_PRIME_MERIDIAN, GetPrimeMeridian());
1664 127 : addParamDouble(CF_PP_SEMI_MAJOR_AXIS, GetSemiMajor());
1665 127 : addParamDouble(CF_PP_INVERSE_FLATTENING, GetInvFlattening());
1666 :
1667 127 : if (bWriteWkt)
1668 : {
1669 127 : char *pszSpatialRef = nullptr;
1670 127 : exportToWkt(&pszSpatialRef);
1671 127 : if (pszSpatialRef && pszSpatialRef[0])
1672 : {
1673 127 : addParamString(NCDF_CRS_WKT, pszSpatialRef);
1674 : }
1675 127 : CPLFree(pszSpatialRef);
1676 : }
1677 :
1678 127 : if (ppszGridMappingName)
1679 123 : *ppszGridMappingName = CPLStrdup(osCFProjection.c_str());
1680 :
1681 127 : if (ppapszKeyValues)
1682 : {
1683 127 : CPLStringList aosKeyValues;
1684 1104 : for (const auto ¶m : oParams)
1685 : {
1686 977 : if (!param.valueStr.empty())
1687 : {
1688 : aosKeyValues.AddNameValue(param.key.c_str(),
1689 381 : param.valueStr.c_str());
1690 : }
1691 : else
1692 : {
1693 1192 : std::string osVal;
1694 1195 : for (const double dfVal : param.doubles)
1695 : {
1696 599 : if (!osVal.empty())
1697 3 : osVal += ',';
1698 599 : osVal += CPLSPrintf("%.18g", dfVal);
1699 : }
1700 596 : aosKeyValues.AddNameValue(param.key.c_str(), osVal.c_str());
1701 : }
1702 : }
1703 127 : *ppapszKeyValues = aosKeyValues.StealList();
1704 : }
1705 : }
1706 :
1707 209 : if (ppszUnits)
1708 : {
1709 38 : const char *pszUnits = nullptr;
1710 38 : const char *pszUnitsToWrite = "";
1711 :
1712 38 : const double dfUnits = GetLinearUnits(&pszUnits);
1713 38 : if (fabs(dfUnits - 1.0) < 1e-15 || pszUnits == nullptr ||
1714 1 : EQUAL(pszUnits, "m") || EQUAL(pszUnits, "metre"))
1715 : {
1716 37 : pszUnitsToWrite = "m";
1717 : }
1718 1 : else if (fabs(dfUnits - 1000.0) < 1e-15)
1719 : {
1720 0 : pszUnitsToWrite = "km";
1721 : }
1722 1 : else if (fabs(dfUnits - CPLAtof(SRS_UL_US_FOOT_CONV)) < 1e-15 ||
1723 1 : EQUAL(pszUnits, SRS_UL_US_FOOT) ||
1724 0 : EQUAL(pszUnits, "US survey foot"))
1725 : {
1726 1 : pszUnitsToWrite = "US_survey_foot";
1727 : }
1728 38 : if (pszUnitsToWrite)
1729 38 : *ppszUnits = CPLStrdup(pszUnitsToWrite);
1730 : }
1731 :
1732 209 : return OGRERR_NONE;
1733 : }
1734 :
1735 : /************************************************************************/
1736 : /* OSRExportToCF1() */
1737 : /************************************************************************/
1738 : /**
1739 : * \brief Export a CRS to netCDF CF-1 definitions.
1740 : *
1741 : * This function is the same as OGRSpatialReference::exportToCF1().
1742 : */
1743 5 : OGRErr OSRExportToCF1(OGRSpatialReferenceH hSRS, char **ppszGridMappingName,
1744 : char ***ppapszKeyValues, char **ppszUnits,
1745 : CSLConstList papszOptions)
1746 :
1747 : {
1748 5 : VALIDATE_POINTER1(hSRS, "OSRExportToCF1", OGRERR_FAILURE);
1749 :
1750 5 : return OGRSpatialReference::FromHandle(hSRS)->exportToCF1(
1751 5 : ppszGridMappingName, ppapszKeyValues, ppszUnits, papszOptions);
1752 : }
|