1 /******************************************************************************
2  *
3  * Project:  PROJ
4  * Purpose:  ISO19111:2019 implementation
5  * Author:   Even Rouault <even dot rouault at spatialys dot com>
6  *
7  ******************************************************************************
8  * Copyright (c) 2018, 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 #ifndef FROM_PROJ_CPP
30 #define FROM_PROJ_CPP
31 #endif
32 
33 #include <string.h>
34 
35 #include "proj/coordinateoperation.hpp"
36 #include "proj/crs.hpp"
37 #include "proj/util.hpp"
38 
39 #include "proj/internal/internal.hpp"
40 #include "proj/internal/io_internal.hpp"
41 
42 #include "oputils.hpp"
43 #include "parammappings.hpp"
44 
45 #include "proj_constants.h"
46 
47 // ---------------------------------------------------------------------------
48 
49 NS_PROJ_START
50 
51 using namespace internal;
52 
53 namespace operation {
54 
55 // ---------------------------------------------------------------------------
56 
57 //! @cond Doxygen_Suppress
58 
59 const char *BALLPARK_GEOCENTRIC_TRANSLATION = "Ballpark geocentric translation";
60 const char *NULL_GEOGRAPHIC_OFFSET = "Null geographic offset";
61 const char *NULL_GEOCENTRIC_TRANSLATION = "Null geocentric translation";
62 const char *BALLPARK_GEOGRAPHIC_OFFSET = "Ballpark geographic offset";
63 const char *BALLPARK_VERTICAL_TRANSFORMATION =
64     " (ballpark vertical transformation)";
65 const char *BALLPARK_VERTICAL_TRANSFORMATION_NO_ELLIPSOID_VERT_HEIGHT =
66     " (ballpark vertical transformation, without ellipsoid height to vertical "
67     "height correction)";
68 
69 // ---------------------------------------------------------------------------
70 
createOpParamNameEPSGCode(int code)71 OperationParameterNNPtr createOpParamNameEPSGCode(int code) {
72     const char *name = OperationParameter::getNameForEPSGCode(code);
73     assert(name);
74     return OperationParameter::create(createMapNameEPSGCode(name, code));
75 }
76 
77 // ---------------------------------------------------------------------------
78 
createMethodMapNameEPSGCode(int code)79 util::PropertyMap createMethodMapNameEPSGCode(int code) {
80     const char *name = nullptr;
81     size_t nMethodNameCodes = 0;
82     const auto methodNameCodes = getMethodNameCodes(nMethodNameCodes);
83     for (size_t i = 0; i < nMethodNameCodes; ++i) {
84         const auto &tuple = methodNameCodes[i];
85         if (tuple.epsg_code == code) {
86             name = tuple.name;
87             break;
88         }
89     }
90     assert(name);
91     return createMapNameEPSGCode(name, code);
92 }
93 
94 // ---------------------------------------------------------------------------
95 
createMapNameEPSGCode(const std::string & name,int code)96 util::PropertyMap createMapNameEPSGCode(const std::string &name, int code) {
97     return util::PropertyMap()
98         .set(common::IdentifiedObject::NAME_KEY, name)
99         .set(metadata::Identifier::CODESPACE_KEY, metadata::Identifier::EPSG)
100         .set(metadata::Identifier::CODE_KEY, code);
101 }
102 
103 // ---------------------------------------------------------------------------
104 
createMapNameEPSGCode(const char * name,int code)105 util::PropertyMap createMapNameEPSGCode(const char *name, int code) {
106     return util::PropertyMap()
107         .set(common::IdentifiedObject::NAME_KEY, name)
108         .set(metadata::Identifier::CODESPACE_KEY, metadata::Identifier::EPSG)
109         .set(metadata::Identifier::CODE_KEY, code);
110 }
111 
112 // ---------------------------------------------------------------------------
113 
addDomains(util::PropertyMap & map,const common::ObjectUsage * obj)114 util::PropertyMap &addDomains(util::PropertyMap &map,
115                               const common::ObjectUsage *obj) {
116 
117     auto ar = util::ArrayOfBaseObject::create();
118     for (const auto &domain : obj->domains()) {
119         ar->add(domain);
120     }
121     if (!ar->empty()) {
122         map.set(common::ObjectUsage::OBJECT_DOMAIN_KEY, ar);
123     }
124     return map;
125 }
126 
127 // ---------------------------------------------------------------------------
128 
getCRSQualifierStr(const crs::CRSPtr & crs)129 static const char *getCRSQualifierStr(const crs::CRSPtr &crs) {
130     auto geod = dynamic_cast<crs::GeodeticCRS *>(crs.get());
131     if (geod) {
132         if (geod->isGeocentric()) {
133             return " (geocentric)";
134         }
135         auto geog = dynamic_cast<crs::GeographicCRS *>(geod);
136         if (geog) {
137             if (geog->coordinateSystem()->axisList().size() == 2) {
138                 return " (geog2D)";
139             } else {
140                 return " (geog3D)";
141             }
142         }
143     }
144     return "";
145 }
146 
147 // ---------------------------------------------------------------------------
148 
buildOpName(const char * opType,const crs::CRSPtr & source,const crs::CRSPtr & target)149 std::string buildOpName(const char *opType, const crs::CRSPtr &source,
150                         const crs::CRSPtr &target) {
151     std::string res(opType);
152     const auto &srcName = source->nameStr();
153     const auto &targetName = target->nameStr();
154     const char *srcQualifier = "";
155     const char *targetQualifier = "";
156     if (srcName == targetName) {
157         srcQualifier = getCRSQualifierStr(source);
158         targetQualifier = getCRSQualifierStr(target);
159         if (strcmp(srcQualifier, targetQualifier) == 0) {
160             srcQualifier = "";
161             targetQualifier = "";
162         }
163     }
164     res += " from ";
165     res += srcName;
166     res += srcQualifier;
167     res += " to ";
168     res += targetName;
169     res += targetQualifier;
170     return res;
171 }
172 
173 // ---------------------------------------------------------------------------
174 
addModifiedIdentifier(util::PropertyMap & map,const common::IdentifiedObject * obj,bool inverse,bool derivedFrom)175 void addModifiedIdentifier(util::PropertyMap &map,
176                            const common::IdentifiedObject *obj, bool inverse,
177                            bool derivedFrom) {
178     // If original operation is AUTH:CODE, then assign INVERSE(AUTH):CODE
179     // as identifier.
180 
181     auto ar = util::ArrayOfBaseObject::create();
182     for (const auto &idSrc : obj->identifiers()) {
183         auto authName = *(idSrc->codeSpace());
184         const auto &srcCode = idSrc->code();
185         if (derivedFrom) {
186             authName = concat("DERIVED_FROM(", authName, ")");
187         }
188         if (inverse) {
189             if (starts_with(authName, "INVERSE(") && authName.back() == ')') {
190                 authName = authName.substr(strlen("INVERSE("));
191                 authName.resize(authName.size() - 1);
192             } else {
193                 authName = concat("INVERSE(", authName, ")");
194             }
195         }
196         auto idsProp = util::PropertyMap().set(
197             metadata::Identifier::CODESPACE_KEY, authName);
198         ar->add(metadata::Identifier::create(srcCode, idsProp));
199     }
200     if (!ar->empty()) {
201         map.set(common::IdentifiedObject::IDENTIFIERS_KEY, ar);
202     }
203 }
204 
205 // ---------------------------------------------------------------------------
206 
207 util::PropertyMap
createPropertiesForInverse(const OperationMethodNNPtr & method)208 createPropertiesForInverse(const OperationMethodNNPtr &method) {
209     util::PropertyMap map;
210 
211     const std::string &forwardName = method->nameStr();
212     if (!forwardName.empty()) {
213         if (starts_with(forwardName, INVERSE_OF)) {
214             map.set(common::IdentifiedObject::NAME_KEY,
215                     forwardName.substr(INVERSE_OF.size()));
216         } else {
217             map.set(common::IdentifiedObject::NAME_KEY,
218                     INVERSE_OF + forwardName);
219         }
220     }
221 
222     addModifiedIdentifier(map, method.get(), true, false);
223 
224     return map;
225 }
226 
227 // ---------------------------------------------------------------------------
228 
createPropertiesForInverse(const CoordinateOperation * op,bool derivedFrom,bool approximateInversion)229 util::PropertyMap createPropertiesForInverse(const CoordinateOperation *op,
230                                              bool derivedFrom,
231                                              bool approximateInversion) {
232     assert(op);
233     util::PropertyMap map;
234 
235     // The domain(s) are unchanged by the inverse operation
236     addDomains(map, op);
237 
238     const std::string &forwardName = op->nameStr();
239 
240     // Forge a name for the inverse, either from the forward name, or
241     // from the source and target CRS names
242     const char *opType;
243     if (starts_with(forwardName, BALLPARK_GEOCENTRIC_TRANSLATION)) {
244         opType = BALLPARK_GEOCENTRIC_TRANSLATION;
245     } else if (starts_with(forwardName, BALLPARK_GEOGRAPHIC_OFFSET)) {
246         opType = BALLPARK_GEOGRAPHIC_OFFSET;
247     } else if (starts_with(forwardName, NULL_GEOGRAPHIC_OFFSET)) {
248         opType = NULL_GEOGRAPHIC_OFFSET;
249     } else if (starts_with(forwardName, NULL_GEOCENTRIC_TRANSLATION)) {
250         opType = NULL_GEOCENTRIC_TRANSLATION;
251     } else if (dynamic_cast<const Transformation *>(op) ||
252                starts_with(forwardName, "Transformation from ")) {
253         opType = "Transformation";
254     } else if (dynamic_cast<const Conversion *>(op)) {
255         opType = "Conversion";
256     } else {
257         opType = "Operation";
258     }
259 
260     auto sourceCRS = op->sourceCRS();
261     auto targetCRS = op->targetCRS();
262     std::string name;
263     if (!forwardName.empty()) {
264         if (dynamic_cast<const Transformation *>(op) == nullptr &&
265             dynamic_cast<const ConcatenatedOperation *>(op) == nullptr &&
266             (starts_with(forwardName, INVERSE_OF) ||
267              forwardName.find(" + ") != std::string::npos)) {
268             std::vector<std::string> tokens;
269             std::string curToken;
270             bool inString = false;
271             for (size_t i = 0; i < forwardName.size(); ++i) {
272                 if (inString) {
273                     curToken += forwardName[i];
274                     if (forwardName[i] == '\'') {
275                         inString = false;
276                     }
277                 } else if (i + 3 < forwardName.size() &&
278                            memcmp(&forwardName[i], " + ", 3) == 0) {
279                     tokens.push_back(curToken);
280                     curToken.clear();
281                     i += 2;
282                 } else if (forwardName[i] == '\'') {
283                     inString = true;
284                     curToken += forwardName[i];
285                 } else {
286                     curToken += forwardName[i];
287                 }
288             }
289             if (!curToken.empty()) {
290                 tokens.push_back(curToken);
291             }
292             for (size_t i = tokens.size(); i > 0;) {
293                 i--;
294                 if (!name.empty()) {
295                     name += " + ";
296                 }
297                 if (starts_with(tokens[i], INVERSE_OF)) {
298                     name += tokens[i].substr(INVERSE_OF.size());
299                 } else if (tokens[i] == AXIS_ORDER_CHANGE_2D_NAME ||
300                            tokens[i] == AXIS_ORDER_CHANGE_3D_NAME) {
301                     name += tokens[i];
302                 } else {
303                     name += INVERSE_OF + tokens[i];
304                 }
305             }
306         } else if (!sourceCRS || !targetCRS ||
307                    forwardName != buildOpName(opType, sourceCRS, targetCRS)) {
308             if (forwardName.find(" + ") != std::string::npos) {
309                 name = INVERSE_OF + '\'' + forwardName + '\'';
310             } else {
311                 name = INVERSE_OF + forwardName;
312             }
313         }
314     }
315     if (name.empty() && sourceCRS && targetCRS) {
316         name = buildOpName(opType, targetCRS, sourceCRS);
317     }
318     if (approximateInversion) {
319         name += " (approx. inversion)";
320     }
321 
322     if (!name.empty()) {
323         map.set(common::IdentifiedObject::NAME_KEY, name);
324     }
325 
326     const std::string &remarks = op->remarks();
327     if (!remarks.empty()) {
328         map.set(common::IdentifiedObject::REMARKS_KEY, remarks);
329     }
330 
331     addModifiedIdentifier(map, op, true, derivedFrom);
332 
333     const auto so = dynamic_cast<const SingleOperation *>(op);
334     if (so) {
335         const int soMethodEPSGCode = so->method()->getEPSGCode();
336         if (soMethodEPSGCode > 0) {
337             map.set("OPERATION_METHOD_EPSG_CODE", soMethodEPSGCode);
338         }
339     }
340 
341     return map;
342 }
343 
344 // ---------------------------------------------------------------------------
345 
addDefaultNameIfNeeded(const util::PropertyMap & properties,const std::string & defaultName)346 util::PropertyMap addDefaultNameIfNeeded(const util::PropertyMap &properties,
347                                          const std::string &defaultName) {
348     if (!properties.get(common::IdentifiedObject::NAME_KEY)) {
349         return util::PropertyMap(properties)
350             .set(common::IdentifiedObject::NAME_KEY, defaultName);
351     } else {
352         return properties;
353     }
354 }
355 
356 // ---------------------------------------------------------------------------
357 
createEntryEqParam(const std::string & a,const std::string & b)358 static std::string createEntryEqParam(const std::string &a,
359                                       const std::string &b) {
360     return a < b ? a + b : b + a;
361 }
362 
buildSetEquivalentParameters()363 static std::set<std::string> buildSetEquivalentParameters() {
364 
365     std::set<std::string> set;
366 
367     const char *const listOfEquivalentParameterNames[][7] = {
368         {"latitude_of_point_1", "Latitude_Of_1st_Point", nullptr},
369         {"longitude_of_point_1", "Longitude_Of_1st_Point", nullptr},
370         {"latitude_of_point_2", "Latitude_Of_2nd_Point", nullptr},
371         {"longitude_of_point_2", "Longitude_Of_2nd_Point", nullptr},
372 
373         {"satellite_height", "height", nullptr},
374 
375         {EPSG_NAME_PARAMETER_FALSE_EASTING,
376          EPSG_NAME_PARAMETER_EASTING_FALSE_ORIGIN,
377          EPSG_NAME_PARAMETER_EASTING_PROJECTION_CENTRE, nullptr},
378 
379         {EPSG_NAME_PARAMETER_FALSE_NORTHING,
380          EPSG_NAME_PARAMETER_NORTHING_FALSE_ORIGIN,
381          EPSG_NAME_PARAMETER_NORTHING_PROJECTION_CENTRE, nullptr},
382 
383         {EPSG_NAME_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN, WKT1_SCALE_FACTOR,
384          EPSG_NAME_PARAMETER_SCALE_FACTOR_INITIAL_LINE,
385          EPSG_NAME_PARAMETER_SCALE_FACTOR_PSEUDO_STANDARD_PARALLEL, nullptr},
386 
387         {WKT1_LATITUDE_OF_ORIGIN, WKT1_LATITUDE_OF_CENTER,
388          EPSG_NAME_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN,
389          EPSG_NAME_PARAMETER_LATITUDE_FALSE_ORIGIN,
390          EPSG_NAME_PARAMETER_LATITUDE_PROJECTION_CENTRE, "Central_Parallel",
391          nullptr},
392 
393         {WKT1_CENTRAL_MERIDIAN, WKT1_LONGITUDE_OF_CENTER,
394          EPSG_NAME_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN,
395          EPSG_NAME_PARAMETER_LONGITUDE_FALSE_ORIGIN,
396          EPSG_NAME_PARAMETER_LONGITUDE_PROJECTION_CENTRE,
397          EPSG_NAME_PARAMETER_LONGITUDE_OF_ORIGIN, nullptr},
398 
399         {"pseudo_standard_parallel_1", WKT1_STANDARD_PARALLEL_1, nullptr},
400     };
401 
402     for (const auto &paramList : listOfEquivalentParameterNames) {
403         for (size_t i = 0; paramList[i]; i++) {
404             auto a = metadata::Identifier::canonicalizeName(paramList[i]);
405             for (size_t j = i + 1; paramList[j]; j++) {
406                 auto b = metadata::Identifier::canonicalizeName(paramList[j]);
407                 set.insert(createEntryEqParam(a, b));
408             }
409         }
410     }
411     return set;
412 }
413 
areEquivalentParameters(const std::string & a,const std::string & b)414 bool areEquivalentParameters(const std::string &a, const std::string &b) {
415 
416     static const std::set<std::string> setEquivalentParameters =
417         buildSetEquivalentParameters();
418 
419     auto a_can = metadata::Identifier::canonicalizeName(a);
420     auto b_can = metadata::Identifier::canonicalizeName(b);
421     return setEquivalentParameters.find(createEntryEqParam(a_can, b_can)) !=
422            setEquivalentParameters.end();
423 }
424 
425 // ---------------------------------------------------------------------------
426 
isTimeDependent(const std::string & methodName)427 bool isTimeDependent(const std::string &methodName) {
428     return ci_find(methodName, "Time dependent") != std::string::npos ||
429            ci_find(methodName, "Time-dependent") != std::string::npos;
430 }
431 
432 // ---------------------------------------------------------------------------
433 
computeConcatenatedName(const std::vector<CoordinateOperationNNPtr> & flattenOps)434 std::string computeConcatenatedName(
435     const std::vector<CoordinateOperationNNPtr> &flattenOps) {
436     std::string name;
437     for (const auto &subOp : flattenOps) {
438         if (!name.empty()) {
439             name += " + ";
440         }
441         const auto &l_name = subOp->nameStr();
442         if (l_name.empty()) {
443             name += "unnamed";
444         } else {
445             name += l_name;
446         }
447     }
448     return name;
449 }
450 
451 // ---------------------------------------------------------------------------
452 
getExtent(const CoordinateOperationNNPtr & op,bool conversionExtentIsWorld,bool & emptyIntersection)453 metadata::ExtentPtr getExtent(const CoordinateOperationNNPtr &op,
454                               bool conversionExtentIsWorld,
455                               bool &emptyIntersection) {
456     auto conv = dynamic_cast<const Conversion *>(op.get());
457     if (conv) {
458         emptyIntersection = false;
459         return metadata::Extent::WORLD;
460     }
461     const auto &domains = op->domains();
462     if (!domains.empty()) {
463         emptyIntersection = false;
464         return domains[0]->domainOfValidity();
465     }
466     auto concatenated = dynamic_cast<const ConcatenatedOperation *>(op.get());
467     if (!concatenated) {
468         emptyIntersection = false;
469         return nullptr;
470     }
471     return getExtent(concatenated->operations(), conversionExtentIsWorld,
472                      emptyIntersection);
473 }
474 
475 // ---------------------------------------------------------------------------
476 
477 static const metadata::ExtentPtr nullExtent{};
478 
getExtent(const crs::CRSNNPtr & crs)479 const metadata::ExtentPtr &getExtent(const crs::CRSNNPtr &crs) {
480     const auto &domains = crs->domains();
481     if (!domains.empty()) {
482         return domains[0]->domainOfValidity();
483     }
484     const auto *boundCRS = dynamic_cast<const crs::BoundCRS *>(crs.get());
485     if (boundCRS) {
486         return getExtent(boundCRS->baseCRS());
487     }
488     return nullExtent;
489 }
490 
getExtentPossiblySynthetized(const crs::CRSNNPtr & crs,bool & approxOut)491 const metadata::ExtentPtr getExtentPossiblySynthetized(const crs::CRSNNPtr &crs,
492                                                        bool &approxOut) {
493     const auto &rawExtent(getExtent(crs));
494     approxOut = false;
495     if (rawExtent)
496         return rawExtent;
497     const auto compoundCRS = dynamic_cast<const crs::CompoundCRS *>(crs.get());
498     if (compoundCRS) {
499         // For a compoundCRS, take the intersection of the extent of its
500         // components.
501         const auto &components = compoundCRS->componentReferenceSystems();
502         metadata::ExtentPtr extent;
503         approxOut = true;
504         for (const auto &component : components) {
505             const auto &componentExtent(getExtent(component));
506             if (extent && componentExtent)
507                 extent = extent->intersection(NN_NO_CHECK(componentExtent));
508             else if (componentExtent)
509                 extent = componentExtent;
510         }
511         return extent;
512     }
513     return rawExtent;
514 }
515 
516 // ---------------------------------------------------------------------------
517 
getExtent(const std::vector<CoordinateOperationNNPtr> & ops,bool conversionExtentIsWorld,bool & emptyIntersection)518 metadata::ExtentPtr getExtent(const std::vector<CoordinateOperationNNPtr> &ops,
519                               bool conversionExtentIsWorld,
520                               bool &emptyIntersection) {
521     metadata::ExtentPtr res = nullptr;
522     for (const auto &subop : ops) {
523 
524         const auto &subExtent =
525             getExtent(subop, conversionExtentIsWorld, emptyIntersection);
526         if (!subExtent) {
527             if (emptyIntersection) {
528                 return nullptr;
529             }
530             continue;
531         }
532         if (res == nullptr) {
533             res = subExtent;
534         } else {
535             res = res->intersection(NN_NO_CHECK(subExtent));
536             if (!res) {
537                 emptyIntersection = true;
538                 return nullptr;
539             }
540         }
541     }
542     emptyIntersection = false;
543     return res;
544 }
545 
546 // ---------------------------------------------------------------------------
547 
548 // Returns the accuracy of an operation, or -1 if unknown
getAccuracy(const CoordinateOperationNNPtr & op)549 double getAccuracy(const CoordinateOperationNNPtr &op) {
550 
551     if (dynamic_cast<const Conversion *>(op.get())) {
552         // A conversion is perfectly accurate.
553         return 0.0;
554     }
555 
556     double accuracy = -1.0;
557     const auto &accuracies = op->coordinateOperationAccuracies();
558     if (!accuracies.empty()) {
559         try {
560             accuracy = c_locale_stod(accuracies[0]->value());
561         } catch (const std::exception &) {
562         }
563     } else {
564         auto concatenated =
565             dynamic_cast<const ConcatenatedOperation *>(op.get());
566         if (concatenated) {
567             accuracy = getAccuracy(concatenated->operations());
568         }
569     }
570     return accuracy;
571 }
572 
573 // ---------------------------------------------------------------------------
574 
575 // Returns the accuracy of a set of concatenated operations, or -1 if unknown
getAccuracy(const std::vector<CoordinateOperationNNPtr> & ops)576 double getAccuracy(const std::vector<CoordinateOperationNNPtr> &ops) {
577     double accuracy = -1.0;
578     for (const auto &subop : ops) {
579         const double subops_accuracy = getAccuracy(subop);
580         if (subops_accuracy < 0.0) {
581             return -1.0;
582         }
583         if (accuracy < 0.0) {
584             accuracy = 0.0;
585         }
586         accuracy += subops_accuracy;
587     }
588     return accuracy;
589 }
590 
591 // ---------------------------------------------------------------------------
592 
exportSourceCRSAndTargetCRSToWKT(const CoordinateOperation * co,io::WKTFormatter * formatter)593 void exportSourceCRSAndTargetCRSToWKT(const CoordinateOperation *co,
594                                       io::WKTFormatter *formatter) {
595     auto l_sourceCRS = co->sourceCRS();
596     assert(l_sourceCRS);
597     auto l_targetCRS = co->targetCRS();
598     assert(l_targetCRS);
599     const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2;
600     const bool canExportCRSId =
601         (isWKT2 && formatter->use2019Keywords() &&
602          !(formatter->idOnTopLevelOnly() && formatter->topLevelHasId()));
603 
604     const bool hasDomains = !co->domains().empty();
605     if (hasDomains) {
606         formatter->pushDisableUsage();
607     }
608 
609     formatter->startNode(io::WKTConstants::SOURCECRS, false);
610     if (canExportCRSId && !l_sourceCRS->identifiers().empty()) {
611         // fake that top node has no id, so that the sourceCRS id is
612         // considered
613         formatter->pushHasId(false);
614         l_sourceCRS->_exportToWKT(formatter);
615         formatter->popHasId();
616     } else {
617         l_sourceCRS->_exportToWKT(formatter);
618     }
619     formatter->endNode();
620 
621     formatter->startNode(io::WKTConstants::TARGETCRS, false);
622     if (canExportCRSId && !l_targetCRS->identifiers().empty()) {
623         // fake that top node has no id, so that the targetCRS id is
624         // considered
625         formatter->pushHasId(false);
626         l_targetCRS->_exportToWKT(formatter);
627         formatter->popHasId();
628     } else {
629         l_targetCRS->_exportToWKT(formatter);
630     }
631     formatter->endNode();
632 
633     if (hasDomains) {
634         formatter->popDisableUsage();
635     }
636 }
637 
638 //! @endcond
639 
640 // ---------------------------------------------------------------------------
641 
642 } // namespace operation
643 NS_PROJ_END
644