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 ¶mList : 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