diff options
| -rw-r--r-- | include/proj/coordinateoperation.hpp | 11 | ||||
| -rw-r--r-- | include/proj/crs.hpp | 2 | ||||
| -rw-r--r-- | include/proj/internal/internal.hpp | 2 | ||||
| -rw-r--r-- | include/proj/io.hpp | 1 | ||||
| -rw-r--r-- | src/c_api.cpp | 147 | ||||
| -rw-r--r-- | src/coordinateoperation.cpp | 264 | ||||
| -rw-r--r-- | src/crs.cpp | 8 | ||||
| -rw-r--r-- | src/cs2cs.cpp | 327 | ||||
| -rw-r--r-- | src/io.cpp | 441 | ||||
| -rw-r--r-- | src/pj_apply_gridshift.c | 2 | ||||
| -rw-r--r-- | src/pj_fwd.c | 9 | ||||
| -rw-r--r-- | src/proj.h | 13 | ||||
| -rw-r--r-- | src/proj_4D_api.c | 7 | ||||
| -rw-r--r-- | test/gie/more_builtins.gie | 10 | ||||
| -rw-r--r-- | test/old/proj_outIGNF.dist | 42 | ||||
| -rwxr-xr-x | test/old/testIGNF | 12 | ||||
| -rwxr-xr-x | test/old/testdatumfile | 4 | ||||
| -rwxr-xr-x | test/old/testvarious | 24 | ||||
| -rw-r--r-- | test/old/tv_out.dist | 12 | ||||
| -rw-r--r-- | test/unit/test_c_api.cpp | 81 | ||||
| -rw-r--r-- | test/unit/test_io.cpp | 103 | ||||
| -rw-r--r-- | test/unit/test_operation.cpp | 128 |
22 files changed, 1278 insertions, 372 deletions
diff --git a/include/proj/coordinateoperation.hpp b/include/proj/coordinateoperation.hpp index 293ba480..4005176a 100644 --- a/include/proj/coordinateoperation.hpp +++ b/include/proj/coordinateoperation.hpp @@ -818,10 +818,6 @@ class PROJ_GCC_DLL Conversion : public SingleOperation { PROJ_DLL ~Conversion() override; //! @endcond - //! @cond Doxygen_Suppress - PROJ_INTERNAL ConversionNNPtr shallowClone() const; - //! @endcond - PROJ_DLL CoordinateOperationNNPtr inverse() const override; //! @cond Doxygen_Suppress @@ -1287,6 +1283,9 @@ class PROJ_GCC_DLL Conversion : public SingleOperation { PROJ_INTERNAL const char *getESRIMethodName() const; PROJ_INTERNAL const char *getWKT1GDALMethodName() const; + + PROJ_INTERNAL ConversionNNPtr shallowClone() const; + //! @endcond protected: @@ -1490,6 +1489,8 @@ class PROJ_GCC_DLL Transformation : public SingleOperation { PROJ_INTERNAL void _exportToWKT(io::WKTFormatter *formatter) const override; // throw(io::FormattingException) + PROJ_INTERNAL TransformationNNPtr shallowClone() const; + //! @endcond protected: @@ -1499,6 +1500,7 @@ class PROJ_GCC_DLL Transformation : public SingleOperation { const OperationMethodNNPtr &methodIn, const std::vector<GeneralParameterValueNNPtr> &values, const std::vector<metadata::PositionalAccuracyNNPtr> &accuracies); + PROJ_INTERNAL Transformation(const Transformation &other); INLINED_MAKE_SHARED PROJ_INTERNAL void _exportToPROJString(io::PROJStringFormatter *formatter) @@ -1508,7 +1510,6 @@ class PROJ_GCC_DLL Transformation : public SingleOperation { private: PROJ_OPAQUE_PRIVATE_DATA - Transformation(const Transformation &) = delete; }; // --------------------------------------------------------------------------- diff --git a/include/proj/crs.hpp b/include/proj/crs.hpp index 82b2bd49..10e0a639 100644 --- a/include/proj/crs.hpp +++ b/include/proj/crs.hpp @@ -108,6 +108,8 @@ class PROJ_GCC_DLL CRS : public common::ObjectUsage { PROJ_FOR_TEST CRSNNPtr shallowClone() const; + PROJ_INTERNAL const std::string &getExtensionProj4() const noexcept; + //! @endcond protected: diff --git a/include/proj/internal/internal.hpp b/include/proj/internal/internal.hpp index 85dd5ac3..b16e12bc 100644 --- a/include/proj/internal/internal.hpp +++ b/include/proj/internal/internal.hpp @@ -104,7 +104,7 @@ PROJ_FOR_TEST std::string replaceAll(const std::string &str, const std::string &before, const std::string &after); -size_t ci_find(const std::string &osStr, const char *needle) noexcept; +PROJ_DLL size_t ci_find(const std::string &osStr, const char *needle) noexcept; size_t ci_find(const std::string &osStr, const std::string &needle, size_t startPos = 0) noexcept; diff --git a/include/proj/io.hpp b/include/proj/io.hpp index 3b5019c1..653edfbc 100644 --- a/include/proj/io.hpp +++ b/include/proj/io.hpp @@ -365,6 +365,7 @@ class PROJ_GCC_DLL PROJStringFormatter { PROJ_DLL void stopInversion(); PROJ_INTERNAL bool isInverted() const; PROJ_INTERNAL bool getUseETMercForTMerc(bool &settingSetOut) const; + PROJ_INTERNAL void setCoordinateOperationOptimizations(bool enable); PROJ_DLL void ingestPROJString(const std::string &str); // throw ParsingException diff --git a/src/c_api.cpp b/src/c_api.cpp index ba1b9534..3b51d905 100644 --- a/src/c_api.cpp +++ b/src/c_api.cpp @@ -39,6 +39,7 @@ #include "proj/common.hpp" #include "proj/coordinateoperation.hpp" +#include "proj/coordinatesystem.hpp" #include "proj/crs.hpp" #include "proj/datum.hpp" #include "proj/io.hpp" @@ -56,6 +57,7 @@ using namespace NS_PROJ::common; using namespace NS_PROJ::crs; +using namespace NS_PROJ::cs; using namespace NS_PROJ::datum; using namespace NS_PROJ::io; using namespace NS_PROJ::internal; @@ -1354,8 +1356,8 @@ int proj_obj_prime_meridian_get_parameters(PJ_OBJ *prime_meridian, // --------------------------------------------------------------------------- -/** \brief Return the base CRS of a BoundCRS or the source CRS of a - * CoordinateOperation. +/** \brief Return the base CRS of a BoundCRS or a DerivedCRS/ProjectedCRS, or + * the source CRS of a CoordinateOperation. * * The returned object must be unreferenced with proj_obj_unref() after * use. @@ -1372,6 +1374,10 @@ PJ_OBJ *proj_obj_get_source_crs(PJ_OBJ *obj) { if (boundCRS) { return PJ_OBJ::create(obj->ctx, boundCRS->baseCRS()); } + auto derivedCRS = dynamic_cast<const DerivedCRS *>(ptr); + if (derivedCRS) { + return PJ_OBJ::create(obj->ctx, derivedCRS->baseCRS()); + } auto co = dynamic_cast<const CoordinateOperation *>(ptr); if (co) { auto sourceCRS = co->sourceCRS(); @@ -4071,3 +4077,140 @@ double proj_coordoperation_get_accuracy(PJ_OBJ *coordoperation) { } return -1; } + +// --------------------------------------------------------------------------- + +/** \brief Returns the coordinate system of a SingleCRS. + * + * The returned object must be unreferenced with proj_obj_unref() after + * use. + * It should be used by at most one thread at a time. + * + * @param crs Objet of type SingleCRS (must not be NULL) + * @return Object that must be unreferenced with proj_obj_unref(), or NULL + * in case of error. + */ +PJ_OBJ *proj_obj_crs_get_coordinate_system(PJ_OBJ *crs) { + assert(crs); + auto l_crs = dynamic_cast<const SingleCRS *>(crs->obj.get()); + if (!l_crs) { + proj_log_error(crs->ctx, __FUNCTION__, "Object is not a SingleCRS"); + return nullptr; + } + return PJ_OBJ::create(crs->ctx, l_crs->coordinateSystem()); +} + +// --------------------------------------------------------------------------- + +/** \brief Returns the type of the coordinate system. + * + * @param cs Objet of type CoordinateSystem (must not be NULL) + * @return type, or NULL in case of error. + */ +const char *proj_obj_cs_get_type(PJ_OBJ *cs) { + assert(cs); + auto l_cs = dynamic_cast<const CoordinateSystem *>(cs->obj.get()); + if (!l_cs) { + proj_log_error(cs->ctx, __FUNCTION__, + "Object is not a CoordinateSystem"); + return nullptr; + } + if (dynamic_cast<const CartesianCS *>(l_cs)) { + return "Cartesian"; + } + if (dynamic_cast<const EllipsoidalCS *>(l_cs)) { + return "Ellipsoidal"; + } + if (dynamic_cast<const VerticalCS *>(l_cs)) { + return "Vertical"; + } + if (dynamic_cast<const SphericalCS *>(l_cs)) { + return "Spherical"; + } + if (dynamic_cast<const OrdinalCS *>(l_cs)) { + return "Ordinal"; + } + if (dynamic_cast<const ParametricCS *>(l_cs)) { + return "Parametric"; + } + if (dynamic_cast<const DateTimeTemporalCS *>(l_cs)) { + return "DateTimeTemporal"; + } + if (dynamic_cast<const TemporalCountCS *>(l_cs)) { + return "TemporalCount"; + } + if (dynamic_cast<const TemporalMeasureCS *>(l_cs)) { + return "TemporalMeasure"; + } + return "unknown"; +} + +// --------------------------------------------------------------------------- + +/** \brief Returns the number of axis of the coordinate system. + * + * @param cs Objet of type CoordinateSystem (must not be NULL) + * @return number of axis, or -1 in case of error. + */ +int proj_obj_cs_get_axis_count(PJ_OBJ *cs) { + assert(cs); + auto l_cs = dynamic_cast<const CoordinateSystem *>(cs->obj.get()); + if (!l_cs) { + proj_log_error(cs->ctx, __FUNCTION__, + "Object is not a CoordinateSystem"); + return -1; + } + return static_cast<int>(l_cs->axisList().size()); +} + +// --------------------------------------------------------------------------- + +/** \brief Returns information on an axis + * + * @param cs Objet of type CoordinateSystem (must not be NULL) + * @param index Index of the coordinate system (between 0 and + * proj_obj_cs_get_axis_count() - 1) + * @param pName Pointer to a string value to store the axis name. or NULL + * @param pAbbrev Pointer to a string value to store the axis abbreviation. or + * NULL + * @param pDirection Pointer to a string value to store the axis direction. or + * NULL + * @param pUnitConvFactor Pointer to a double value to store the axis + * unit conversion factor. or NULL + * @param pUnitName Pointer to a string value to store the axis + * unit name. or NULL + * @return TRUE in case of success + */ +int proj_obj_cs_get_axis_info(PJ_OBJ *cs, int index, const char **pName, + const char **pAbbrev, const char **pDirection, + double *pUnitConvFactor, const char **pUnitName) { + assert(cs); + auto l_cs = dynamic_cast<const CoordinateSystem *>(cs->obj.get()); + if (!l_cs) { + proj_log_error(cs->ctx, __FUNCTION__, + "Object is not a CoordinateSystem"); + return false; + } + const auto &axisList = l_cs->axisList(); + if (index < 0 || static_cast<size_t>(index) >= axisList.size()) { + proj_log_error(cs->ctx, __FUNCTION__, "Invalid index"); + return false; + } + const auto &axis = axisList[index]; + if (pName) { + *pName = axis->nameStr().c_str(); + } + if (pAbbrev) { + *pAbbrev = axis->abbreviation().c_str(); + } + if (pDirection) { + *pDirection = axis->direction().toString().c_str(); + } + if (pUnitConvFactor) { + *pUnitConvFactor = axis->unit().conversionToSI(); + } + if (pUnitName) { + *pUnitName = axis->unit().name().c_str(); + } + return true; +} diff --git a/src/coordinateoperation.cpp b/src/coordinateoperation.cpp index e0e02931..893b52d3 100644 --- a/src/coordinateoperation.cpp +++ b/src/coordinateoperation.cpp @@ -5471,6 +5471,12 @@ Transformation::~Transformation() = default; // --------------------------------------------------------------------------- +Transformation::Transformation(const Transformation &other) + : CoordinateOperation(other), SingleOperation(other), + d(internal::make_unique<Private>(*other.d)) {} + +// --------------------------------------------------------------------------- + /** \brief Return the source crs::CRS of the transformation. * * @return the source CRS. @@ -5492,6 +5498,17 @@ const crs::CRSNNPtr &Transformation::targetCRS() PROJ_CONST_DEFN { // --------------------------------------------------------------------------- //! @cond Doxygen_Suppress +TransformationNNPtr Transformation::shallowClone() const { + auto conv = Transformation::nn_make_shared<Transformation>(*this); + conv->assignSelf(conv); + conv->setCRSs(this, false); + return conv; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress /** \brief Return the TOWGS84 parameters of the transformation. * * If this transformation uses Coordinate Frame Rotation, Position Vector @@ -7595,6 +7612,8 @@ void Transformation::_exportToPROJString( "Transformation cannot be exported as a PROJ.4 string"); } + formatter->setCoordinateOperationOptimizations(true); + bool positionVectorConvention = true; bool sevenParamsTransform = false; bool threeParamsTransform = false; @@ -10156,6 +10175,72 @@ void CoordinateOperationFactory::Private::createOperationsWithDatumPivot( const auto candidatesDstGeod( findCandidateGeodCRSForDatum(authFactory, geodDst->datum())); + auto createTransformations = [&](const crs::CRSNNPtr &candidateSrcGeod, + const crs::CRSNNPtr &candidateDstGeod, + const CoordinateOperationNNPtr &opFirst, + bool isNullFirst) { + const auto opsSecond = + createOperations(candidateSrcGeod, candidateDstGeod, context); + const auto opsThird = + createOperations(candidateDstGeod, targetCRS, context); + assert(!opsThird.empty()); + + for (auto &opSecond : opsSecond) { + // Check that it is not a transformation synthetized by + // ourselves + if (!hasIdentifiers(opSecond)) { + continue; + } + // And even if it is a referenced transformation, check that + // it is not a trivial one + auto so = dynamic_cast<const SingleOperation *>(opSecond.get()); + if (so && isAxisOrderReversal(so->method()->getEPSGCode())) { + continue; + } + + std::vector<CoordinateOperationNNPtr> subOps; + if (isNullFirst) { + opSecond->setCRSs( + sourceCRS, NN_CHECK_ASSERT(opSecond->targetCRS()), nullptr); + } else { + subOps.emplace_back(opFirst); + } + if (isNullTransformation(opsThird[0]->nameStr())) { + opSecond->setCRSs(NN_CHECK_ASSERT(opSecond->sourceCRS()), + targetCRS, nullptr); + subOps.emplace_back(opSecond); + } else { + subOps.emplace_back(opSecond); + subOps.emplace_back(opsThird[0]); + } + res.emplace_back(ConcatenatedOperation::createComputeMetadata( + subOps, !allowEmptyIntersection)); + } + }; + + // Start in priority with candidates that have exactly the same name as + // the sourcCRS and targetCRS. Typically for the case of init=IGNF:XXXX + for (const auto &candidateSrcGeod : candidatesSrcGeod) { + if (candidateSrcGeod->nameStr() == sourceCRS->nameStr()) { + for (const auto &candidateDstGeod : candidatesDstGeod) { + if (candidateDstGeod->nameStr() == targetCRS->nameStr()) { + const auto opsFirst = + createOperations(sourceCRS, candidateSrcGeod, context); + assert(!opsFirst.empty()); + const bool isNullFirst = + isNullTransformation(opsFirst[0]->nameStr()); + createTransformations(candidateSrcGeod, candidateDstGeod, + opsFirst[0], isNullFirst); + if (!res.empty()) { + return; + } + break; + } + } + break; + } + } + for (const auto &candidateSrcGeod : candidatesSrcGeod) { const auto opsFirst = createOperations(sourceCRS, candidateSrcGeod, context); @@ -10163,44 +10248,8 @@ void CoordinateOperationFactory::Private::createOperationsWithDatumPivot( const bool isNullFirst = isNullTransformation(opsFirst[0]->nameStr()); for (const auto &candidateDstGeod : candidatesDstGeod) { - const auto opsSecond = - createOperations(candidateSrcGeod, candidateDstGeod, context); - const auto opsThird = - createOperations(candidateDstGeod, targetCRS, context); - assert(!opsThird.empty()); - - for (auto &opSecond : opsSecond) { - // Check that it is not a transformation synthetized by - // ourselves - if (!hasIdentifiers(opSecond)) { - continue; - } - // And even if it is a referenced transformation, check that - // it is not a trivial one - auto so = dynamic_cast<const SingleOperation *>(opSecond.get()); - if (so && isAxisOrderReversal(so->method()->getEPSGCode())) { - continue; - } - - std::vector<CoordinateOperationNNPtr> subOps; - if (isNullFirst) { - opSecond->setCRSs(sourceCRS, - NN_CHECK_ASSERT(opSecond->targetCRS()), - nullptr); - } else { - subOps.emplace_back(opsFirst[0]); - } - if (isNullTransformation(opsThird[0]->nameStr())) { - opSecond->setCRSs(NN_CHECK_ASSERT(opSecond->sourceCRS()), - targetCRS, nullptr); - subOps.emplace_back(opSecond); - } else { - subOps.emplace_back(opSecond); - subOps.emplace_back(opsThird[0]); - } - res.emplace_back(ConcatenatedOperation::createComputeMetadata( - subOps, !allowEmptyIntersection)); - } + createTransformations(candidateSrcGeod, candidateDstGeod, + opsFirst[0], isNullFirst); } if (!res.empty()) { return; @@ -10261,6 +10310,56 @@ CoordinateOperationFactory::Private::createOperations( std::vector<CoordinateOperationNNPtr> res; const bool allowEmptyIntersection = true; + const auto &sourceProj4Ext = sourceCRS->getExtensionProj4(); + const auto &targetProj4Ext = targetCRS->getExtensionProj4(); + if (!sourceProj4Ext.empty() || !targetProj4Ext.empty()) { + + auto sourceProjExportable = + dynamic_cast<io::IPROJStringExportable *>(sourceCRS.get()); + auto targetProjExportable = + dynamic_cast<io::IPROJStringExportable *>(targetCRS.get()); + if (!sourceProjExportable) { + throw InvalidOperation("Source CRS is not PROJ exportable"); + } + if (!targetProjExportable) { + throw InvalidOperation("Target CRS is not PROJ exportable"); + } + auto projFormatter = io::PROJStringFormatter::create( + io::PROJStringFormatter::Convention::PROJ_4); + projFormatter->startInversion(); + sourceProjExportable->_exportToPROJString(projFormatter.get()); + + auto geogSrc = + dynamic_cast<const crs::GeographicCRS *>(sourceCRS.get()); + if (geogSrc) { + auto proj5Formatter = io::PROJStringFormatter::create( + io::PROJStringFormatter::Convention::PROJ_5); + geogSrc->addAngularUnitConvertAndAxisSwap(proj5Formatter.get()); + projFormatter->ingestPROJString(proj5Formatter->toString()); + } + + projFormatter->stopInversion(); + + targetProjExportable->_exportToPROJString(projFormatter.get()); + + auto geogDst = + dynamic_cast<const crs::GeographicCRS *>(targetCRS.get()); + if (geogDst) { + auto proj5Formatter = io::PROJStringFormatter::create( + io::PROJStringFormatter::Convention::PROJ_5); + geogDst->addAngularUnitConvertAndAxisSwap(proj5Formatter.get()); + projFormatter->ingestPROJString(proj5Formatter->toString()); + } + + const auto PROJString = projFormatter->toString(); + auto properties = util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, + buildTransfName(sourceCRS->nameStr(), targetCRS->nameStr())); + res.emplace_back(SingleOperation::createPROJBased( + properties, PROJString, sourceCRS, targetCRS, {})); + return res; + } + auto geodSrc = dynamic_cast<const crs::GeodeticCRS *>(sourceCRS.get()); auto geodDst = dynamic_cast<const crs::GeodeticCRS *>(targetCRS.get()); @@ -10317,7 +10416,9 @@ CoordinateOperationFactory::Private::createOperations( const auto &dstDatum = geodDst->datum(); const bool dstHasDatumWithId = dstDatum && !dstDatum->identifiers().empty(); - if (srcHasDatumWithId && dstHasDatumWithId) { + if (srcHasDatumWithId && dstHasDatumWithId && + !srcDatum->_isEquivalentTo( + dstDatum.get(), util::IComparable::Criterion::EQUIVALENT)) { createOperationsWithDatumPivot(res, sourceCRS, targetCRS, geodSrc, geodDst, context); doFilterAndCheckPerfectOp = !res.empty(); @@ -10443,11 +10544,10 @@ CoordinateOperationFactory::Private::createOperations( dynamic_cast<const crs::GeographicCRS *>(hubSrc.get()); auto geogCRSOfBaseOfBoundSrc = boundSrc->baseCRS()->extractGeographicCRS(); - if (hubSrcGeog && + if (hubSrcGeog && geogCRSOfBaseOfBoundSrc && (hubSrcGeog->_isEquivalentTo( geogDst, util::IComparable::Criterion::EQUIVALENT) || - hubSrcGeog->is2DPartOf3D(NN_NO_CHECK(geogDst))) && - geogCRSOfBaseOfBoundSrc) { + hubSrcGeog->is2DPartOf3D(NN_NO_CHECK(geogDst)))) { if (boundSrc->baseCRS() == geogCRSOfBaseOfBoundSrc) { // Optimization to avoid creating a useless concatenated // operation @@ -10471,6 +10571,86 @@ CoordinateOperationFactory::Private::createOperations( return res; } } + // If the datum are equivalent, this is also fine + } else if (geogCRSOfBaseOfBoundSrc && hubSrcGeog->datum() && + geogDst->datum() && + hubSrcGeog->datum()->_isEquivalentTo( + geogDst->datum().get(), + util::IComparable::Criterion::EQUIVALENT)) { + auto opsFirst = + createOperations(boundSrc->baseCRS(), + NN_NO_CHECK(geogCRSOfBaseOfBoundSrc), context); + auto opsLast = createOperations(hubSrc, targetCRS, context); + if (!opsFirst.empty() && !opsLast.empty()) { + for (const auto &opFirst : opsFirst) { + for (const auto &opLast : opsLast) { + try { + res.emplace_back( + ConcatenatedOperation::createComputeMetadata( + {opFirst, boundSrc->transformation(), + opLast}, + !allowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } + } + if (!res.empty()) { + return res; + } + } + // Consider WGS 84 and NAD83 as equivalent in that context if the + // geogCRSOfBaseOfBoundSrc ellipsoid is Clarke66 (for NAD27) + // Case of "+proj=latlong +ellps=clrk66 + // +nadgrids=ntv1_can.dat,conus" + // to "+proj=latlong +datum=NAD83" + } else if (geogCRSOfBaseOfBoundSrc && hubSrcGeog->datum() && + geogDst->datum() && + geogCRSOfBaseOfBoundSrc->ellipsoid()->_isEquivalentTo( + datum::Ellipsoid::CLARKE_1866.get(), + util::IComparable::Criterion::EQUIVALENT) && + hubSrcGeog->datum()->_isEquivalentTo( + datum::GeodeticReferenceFrame::EPSG_6326.get(), + util::IComparable::Criterion::EQUIVALENT) && + geogDst->datum()->_isEquivalentTo( + datum::GeodeticReferenceFrame::EPSG_6269.get(), + util::IComparable::Criterion::EQUIVALENT)) { + auto nnGeogCRSOfBaseOfBoundSrc = + NN_NO_CHECK(geogCRSOfBaseOfBoundSrc); + if (boundSrc->baseCRS()->_isEquivalentTo( + nnGeogCRSOfBaseOfBoundSrc.get(), + util::IComparable::Criterion::EQUIVALENT)) { + auto transf = boundSrc->transformation()->shallowClone(); + transf->setProperties(util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, + buildTransfName(boundSrc->baseCRS()->nameStr(), + targetCRS->nameStr()))); + transf->setCRSs(boundSrc->baseCRS(), targetCRS, nullptr); + res.emplace_back(transf); + return res; + } else { + auto opsFirst = createOperations( + boundSrc->baseCRS(), nnGeogCRSOfBaseOfBoundSrc, context); + auto transf = boundSrc->transformation()->shallowClone(); + transf->setProperties(util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, + buildTransfName(nnGeogCRSOfBaseOfBoundSrc->nameStr(), + targetCRS->nameStr()))); + transf->setCRSs(nnGeogCRSOfBaseOfBoundSrc, targetCRS, nullptr); + if (!opsFirst.empty()) { + for (const auto &opFirst : opsFirst) { + try { + res.emplace_back( + ConcatenatedOperation::createComputeMetadata( + {opFirst, transf}, + !allowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } + if (!res.empty()) { + return res; + } + } + } } if (hubSrcGeog && diff --git a/src/crs.cpp b/src/crs.cpp index a204a037..eec7a926 100644 --- a/src/crs.cpp +++ b/src/crs.cpp @@ -170,6 +170,14 @@ const GeodeticCRS *CRS::extractGeodeticCRSRaw() const { // --------------------------------------------------------------------------- +//! @cond Doxygen_Suppress +const std::string &CRS::getExtensionProj4() const noexcept { + return d->extensionProj4_; +} +//! @endcond + +// --------------------------------------------------------------------------- + /** \brief Return the GeographicCRS of the CRS. * * Returns the GeographicCRS contained in a CRS. This works currently with diff --git a/src/cs2cs.cpp b/src/cs2cs.cpp index 6f302ae3..6f4c4a55 100644 --- a/src/cs2cs.cpp +++ b/src/cs2cs.cpp @@ -26,6 +26,8 @@ * DEALINGS IN THE SOFTWARE. *****************************************************************************/ +#define FROM_PROJ_CPP + #include <ctype.h> #include <locale.h> #include <math.h> @@ -33,6 +35,11 @@ #include <stdlib.h> #include <string.h> +#include <cassert> +#include <string> + +#include <proj/internal/internal.hpp> + // PROJ include order is sensitive // clang-format off #include "proj.h" @@ -41,9 +48,15 @@ // clang-format on #define MAX_LINE 1000 -#define MAX_PARGS 100 -static projPJ fromProj, toProj; +static PJ *transformation = nullptr; + +static bool srcIsGeog = false; +static double srcToRadians = 0.0; + +static bool destIsGeog = false; +static double destToRadians = 0.0; +static bool destIsLatLong = false; static int reversein = 0, /* != 0 reverse input arguments */ reverseout = 0, /* != 0 reverse output arguments */ @@ -116,19 +129,45 @@ static void process(FILE *fid) } if (data.u != HUGE_VAL) { - if (pj_transform(fromProj, toProj, 1, 0, &(data.u), &(data.v), - &z) != 0) { - data.u = HUGE_VAL; - data.v = HUGE_VAL; - emess(-3, "pj_transform(): %s", pj_strerrno(pj_errno)); + + if (srcIsGeog) { + /* dmstor gives values to radians. Convert now to the SRS unit + */ + data.u /= srcToRadians; + data.v /= srcToRadians; } + + PJ_COORD coord; + coord.xyzt.x = data.u; + coord.xyzt.y = data.v; + coord.xyzt.z = z; + coord.xyzt.t = HUGE_VAL; + coord = proj_trans(transformation, PJ_FWD, coord); + data.u = coord.xyz.x; + data.v = coord.xyz.y; + z = coord.xyz.z; } if (data.u == HUGE_VAL) /* error output */ fputs(oterr, stdout); - else if (pj_is_latlong(toProj) && !oform) { /*ascii DMS output */ - if (reverseout) { + else if (destIsGeog && !oform) { /*ascii DMS output */ + + // rtodms() expect radians: convert from the output SRS unit + data.u *= destToRadians; + data.v *= destToRadians; + + if (destIsLatLong) { + if (reverseout) { + fputs(rtodms(pline, data.v, 'E', 'W'), stdout); + putchar('\t'); + fputs(rtodms(pline, data.u, 'N', 'S'), stdout); + } else { + fputs(rtodms(pline, data.u, 'N', 'S'), stdout); + putchar('\t'); + fputs(rtodms(pline, data.v, 'E', 'W'), stdout); + } + } else if (reverseout) { fputs(rtodms(pline, data.v, 'N', 'S'), stdout); putchar('\t'); fputs(rtodms(pline, data.u, 'E', 'W'), stdout); @@ -139,9 +178,9 @@ static void process(FILE *fid) } } else { /* x-y or decimal degree ascii output */ - if (proj_angular_output(toProj, PJ_FWD)) { - data.v *= RAD_TO_DEG; - data.u *= RAD_TO_DEG; + if (destIsGeog) { + data.v *= destToRadians * RAD_TO_DEG; + data.u *= destToRadians * RAD_TO_DEG; } if (reverseout) { printf(oform, data.v); @@ -167,17 +206,116 @@ static void process(FILE *fid) } /************************************************************************/ +/* instanciate_crs() */ +/************************************************************************/ + +static PJ_OBJ *instanciate_crs(const std::string &definition, + const char *const *optionsImportCRS, + bool &isGeog, double &toRadians, + bool &isLatFirst) { + PJ_OBJ *crs = proj_obj_create_from_user_input(nullptr, definition.c_str(), + optionsImportCRS); + if (!crs) { + return nullptr; + } + + isGeog = false; + toRadians = 0.0; + isLatFirst = false; + + auto type = proj_obj_get_type(crs); + if (type == PJ_OBJ_TYPE_BOUND_CRS) { + auto base = proj_obj_get_source_crs(crs); + proj_obj_unref(crs); + crs = base; + type = proj_obj_get_type(crs); + } + if (type == PJ_OBJ_TYPE_GEOGRAPHIC_2D_CRS || + type == PJ_OBJ_TYPE_GEOGRAPHIC_3D_CRS) { + auto cs = proj_obj_crs_get_coordinate_system(crs); + assert(cs); + + isGeog = true; + const char *axisName = ""; + proj_obj_cs_get_axis_info(cs, 0, + &axisName, // name, + nullptr, // abbrev + nullptr, // direction + &toRadians, + nullptr // unit name + ); + isLatFirst = + NS_PROJ::internal::ci_find(std::string(axisName), "latitude") != + std::string::npos; + + proj_obj_unref(cs); + } + + return crs; +} + +/************************************************************************/ +/* get_geog_crs_proj_string_from_proj_crs() */ +/************************************************************************/ + +static std::string get_geog_crs_proj_string_from_proj_crs(PJ_OBJ *src, + double &toRadians, + bool &isLatFirst) { + auto srcType = proj_obj_get_type(src); + if (srcType == PJ_OBJ_TYPE_BOUND_CRS) { + auto base = proj_obj_get_source_crs(src); + assert(base); + proj_obj_unref(src); + src = base; + srcType = proj_obj_get_type(src); + } + if (srcType != PJ_OBJ_TYPE_PROJECTED_CRS) { + return std::string(); + } + + auto base = proj_obj_get_source_crs(src); + assert(base); + auto baseType = proj_obj_get_type(base); + if (baseType != PJ_OBJ_TYPE_GEOGRAPHIC_2D_CRS && + baseType != PJ_OBJ_TYPE_GEOGRAPHIC_3D_CRS) { + proj_obj_unref(base); + return std::string(); + } + + auto cs = proj_obj_crs_get_coordinate_system(base); + assert(cs); + + const char *axisName = ""; + proj_obj_cs_get_axis_info(cs, 0, + &axisName, // name, + nullptr, // abbrev + nullptr, // direction + &toRadians, + nullptr // unit name + ); + isLatFirst = NS_PROJ::internal::ci_find(std::string(axisName), + "latitude") != std::string::npos; + + proj_obj_unref(cs); + + auto retCStr = proj_obj_as_proj_string(base, PJ_PROJ_5, nullptr); + std::string ret(retCStr ? retCStr : ""); + proj_obj_unref(base); + return ret; +} + +/************************************************************************/ /* main() */ /************************************************************************/ int main(int argc, char **argv) { char *arg; char **eargv = argv; - char *from_argv[MAX_PARGS]; - char *to_argv[MAX_PARGS]; + std::string fromStr; + std::string toStr; FILE *fid; - int from_argc = 0, to_argc = 0, eargc = 0, mon = 0; - int have_to_flag = 0, inverse = 0, i; + int eargc = 0, mon = 0; + int have_to_flag = 0, inverse = 0; int use_env_locale = 0; /* This is just to check that pj_init() is locale-safe */ @@ -185,6 +323,11 @@ int main(int argc, char **argv) { if (getenv("PROJ_USE_ENV_LOCALE") != nullptr) use_env_locale = 1; + /* Enable compatibility mode for init=epsg:XXXX by default */ + if (getenv("PROJ_USE_PROJ4_INIT_RULES") == nullptr) { + proj_context_use_proj4_init_rules(nullptr, true); + } + if ((emess_dat.Prog_name = strrchr(*argv, DIR_CHAR)) != nullptr) ++emess_dat.Prog_name; else @@ -194,9 +337,27 @@ int main(int argc, char **argv) { (void)fprintf(stderr, usage, pj_get_release(), emess_dat.Prog_name); exit(0); } + + // First pass to check if we have "cs2cs [-bla]* <SRC> <DEST>" syntax + int countNonOptionArg = 0; + for (int i = 1; i < argc; i++) { + if (argv[i][0] == '-') { + if (argv[i][1] == '\0') { + countNonOptionArg++; + } + } else { + if (strcmp(argv[i], "+to") == 0) { + countNonOptionArg = -1; + break; + } + countNonOptionArg++; + } + } + const bool isSrcDestSyntax = (countNonOptionArg == 2); + /* process run line arguments */ while (--argc > 0) { /* collect run line arguments */ - if (**++argv == '-') + if (**++argv == '-') { for (arg = *argv;;) { switch (*++arg) { case '\0': /* position of "stdin" */ @@ -325,21 +486,28 @@ int main(int argc, char **argv) { } break; } - else if (strcmp(*argv, "+to") == 0) { + } else if (isSrcDestSyntax) { + if (fromStr.empty()) + fromStr = *argv; + else + toStr = *argv; + } else if (strcmp(*argv, "+to") == 0) { have_to_flag = 1; } else if (**argv == '+') { /* + argument */ if (have_to_flag) { - if (to_argc < MAX_PARGS) - to_argv[to_argc++] = *argv + 1; - else - emess(1, "overflowed + argument table"); + if (!toStr.empty()) + toStr += ' '; + toStr += *argv; } else { - if (from_argc < MAX_PARGS) - from_argv[from_argc++] = *argv + 1; - else - emess(1, "overflowed + argument table"); + if (!fromStr.empty()) + fromStr += ' '; + fromStr += *argv; } + } else if (!have_to_flag) { + fromStr = *argv; + } else if (toStr.empty()) { + toStr = *argv; } else /* assumed to be input file name(s) */ eargv[eargc++] = *argv; } @@ -351,17 +519,7 @@ int main(int argc, char **argv) { * coordinate systems. */ if (inverse) { - int argcount; - - for (i = 0; i < MAX_PARGS; i++) { - arg = from_argv[i]; - from_argv[i] = to_argv[i]; - to_argv[i] = arg; - } - - argcount = from_argc; - from_argc = to_argc; - to_argc = argcount; + std::swap(fromStr, toStr); } if (use_env_locale) { @@ -369,48 +527,64 @@ int main(int argc, char **argv) { setlocale(LC_ALL, ""); } - if (from_argc == 0 && to_argc != 0) { - /* we will generate the from proj as the latlong of the +to in a bit */ - } else if (!(fromProj = pj_init(from_argc, from_argv))) { - printf("Using from definition: "); - for (i = 0; i < from_argc; i++) - printf("%s ", from_argv[i]); - printf("\n"); - - emess(3, "projection initialization failure\ncause: %s", - pj_strerrno(pj_errno)); + if (fromStr.empty() && toStr.empty()) { + emess(3, "missing source and target coordinate systems"); } - if (to_argc == 0) { - if (!(toProj = pj_latlong_from_proj(fromProj))) { - printf("Using to definition: "); - for (i = 0; i < to_argc; i++) - printf("%s ", to_argv[i]); - printf("\n"); + const char *const optionsProj4Mode[] = {"USE_PROJ4_INIT_RULES=YES", + nullptr}; + const char *const *optionsImportCRS = + proj_context_get_use_proj4_init_rules(nullptr) ? optionsProj4Mode + : nullptr; + + PJ_OBJ *src = nullptr; + if (!fromStr.empty()) { + bool ignored; + src = instanciate_crs(fromStr, optionsImportCRS, srcIsGeog, + srcToRadians, ignored); + if (!src) { + emess(3, "cannot instanciate source coordinate system"); + } + } - emess(3, "projection initialization failure\ncause: %s", - pj_strerrno(pj_errno)); + PJ_OBJ *dst = nullptr; + if (!toStr.empty()) { + dst = instanciate_crs(toStr, optionsImportCRS, destIsGeog, + destToRadians, destIsLatLong); + if (!dst) { + emess(3, "cannot instanciate target coordinate system"); } - } else if (!(toProj = pj_init(to_argc, to_argv))) { - printf("Using to definition: "); - for (i = 0; i < to_argc; i++) - printf("%s ", to_argv[i]); - printf("\n"); + } - emess(3, "projection initialization failure\ncause: %s", - pj_strerrno(pj_errno)); + if (toStr.empty()) { + assert(src); + toStr = get_geog_crs_proj_string_from_proj_crs(src, destToRadians, + destIsLatLong); + if (toStr.empty()) { + emess(3, + "missing target CRS and source CRS is not a projected CRS"); + } + destIsGeog = true; + } else if (fromStr.empty()) { + assert(dst); + bool ignored; + fromStr = + get_geog_crs_proj_string_from_proj_crs(dst, srcToRadians, ignored); + if (fromStr.empty()) { + emess(3, + "missing source CRS and target CRS is not a projected CRS"); + } + srcIsGeog = true; } - if (from_argc == 0 && toProj != nullptr) { - if (!(fromProj = pj_latlong_from_proj(toProj))) { - printf("Using to definition: "); - for (i = 0; i < to_argc; i++) - printf("%s ", to_argv[i]); - printf("\n"); + proj_obj_unref(src); + proj_obj_unref(dst); - emess(3, "projection initialization failure\ncause: %s", - pj_strerrno(pj_errno)); - } + transformation = proj_create_crs_to_crs(nullptr, fromStr.c_str(), + toStr.c_str(), nullptr); + if (!transformation) { + emess(3, "cannot initialize transformation\ncause: %s", + pj_strerrno(pj_errno)); } if (use_env_locale) { @@ -420,19 +594,19 @@ int main(int argc, char **argv) { if (mon) { printf("%c ---- From Coordinate System ----\n", tag); - pj_pr_list(fromProj); + printf("%s\n", fromStr.c_str()); printf("%c ---- To Coordinate System ----\n", tag); - pj_pr_list(toProj); + printf("%s\n", toStr.c_str()); } /* set input formatting control */ - if (!fromProj->is_latlong) + if (!srcIsGeog) informat = strtod; else { informat = dmstor; } - if (!toProj->is_latlong && !oform) + if (!destIsGeog && !oform) oform = "%.2f"; /* process input file list */ @@ -454,8 +628,7 @@ int main(int argc, char **argv) { emess_dat.File_name = nullptr; } - pj_free(fromProj); - pj_free(toProj); + proj_destroy(transformation); pj_deallocate_grids(); @@ -4422,6 +4422,7 @@ struct PROJStringFormatter::Private { bool useETMercForTMerc_ = false; bool useETMercForTMercSet_ = false; bool addNoDefs_ = true; + bool coordOperationOptimizations_ = false; std::string result_{}; @@ -4504,6 +4505,12 @@ const std::string &PROJStringFormatter::toString() const { step.paramValues[6].equals("s", "0") && step.paramValues[7].keyEquals("convention")))) { iter = d->steps_.erase(iter); + } else if (d->coordOperationOptimizations_ && + step.name == "unitconvert" && paramCount == 2 && + step.paramValues[0].keyEquals("xy_in") && + step.paramValues[1].keyEquals("xy_out") && + step.paramValues[0].value == step.paramValues[1].value) { + iter = d->steps_.erase(iter); } else { ++iter; } @@ -4925,6 +4932,12 @@ bool PROJStringFormatter::getUseETMercForTMerc(bool &settingSetOut) const { // --------------------------------------------------------------------------- +void PROJStringFormatter::setCoordinateOperationOptimizations(bool enable) { + d->coordOperationOptimizations_ = enable; +} + +// --------------------------------------------------------------------------- + void PROJStringFormatter::Private::appendToResult(const char *str) { if (!result_.empty()) { result_ += " "; @@ -4984,11 +4997,13 @@ PROJStringSyntaxParser(const std::string &projString, std::vector<Step> &steps, } prevWasStep = false; } else if (starts_with(word, "proj=")) { + auto stepName = word.substr(strlen("proj=")); if (prevWasInit) { - throw ParsingException("+init= found at unexpected place"); + steps.back() = Step(); + prevWasInit = false; + } else { + steps.push_back(Step()); } - auto stepName = word.substr(strlen("proj=")); - steps.push_back(Step()); steps.back().name = stepName; steps.back().inverted = inverted; prevWasStep = false; @@ -5041,7 +5056,7 @@ void PROJStringFormatter::ingestPROJString( std::string vto_meter; PROJStringSyntaxParser(str, steps, d->globalParamValues_, title, vunits, vto_meter); - d->steps_.insert(d->steps_.begin(), steps.begin(), steps.end()); + d->steps_.insert(d->steps_.end(), steps.begin(), steps.end()); } // --------------------------------------------------------------------------- @@ -5633,11 +5648,12 @@ PROJStringParser::Private::buildPrimeMeridian(const Step &step) { PrimeMeridianNNPtr pm = PrimeMeridian::GREENWICH; const auto &pmStr = getParamValue(step, "pm"); if (!pmStr.empty()) { - try { - double pmValue = c_locale_stod(pmStr); + char *end; + double pmValue = dmstor(pmStr.c_str(), &end) * RAD_TO_DEG; + if (pmValue != HUGE_VAL && *end == '\0') { pm = PrimeMeridian::create(createMapWithUnknownName(), Angle(pmValue)); - } catch (const std::invalid_argument &) { + } else { bool found = false; if (pmStr == "paris") { found = true; @@ -5650,9 +5666,8 @@ PROJStringParser::Private::buildPrimeMeridian(const Step &step) { found = true; std::string name = static_cast<char>(::toupper(pmStr[0])) + pmStr.substr(1); - double pmValue = - dmstor(proj_prime_meridians[i].defn, nullptr) * - RAD_TO_DEG; + pmValue = dmstor(proj_prime_meridians[i].defn, nullptr) * + RAD_TO_DEG; pm = PrimeMeridian::create( PropertyMap().set(IdentifiedObject::NAME_KEY, name), Angle(pmValue)); @@ -5681,12 +5696,20 @@ PROJStringParser::Private::buildDatum(const Step &step, const auto &ellpsStr = getParamValue(step, "ellps"); const auto &datumStr = getParamValue(step, "datum"); + const auto &RStr = getParamValue(step, "R"); const auto &aStr = getParamValue(step, "a"); const auto &bStr = getParamValue(step, "b"); const auto &rfStr = getParamValue(step, "rf"); const auto &fStr = getParamValue(step, "f"); - const auto &RStr = getParamValue(step, "R"); + const auto &esStr = getParamValue(step, "es"); + const auto &eStr = getParamValue(step, "e"); + double a = -1.0; + double b = -1.0; + double rf = -1.0; const util::optional<std::string> optionalEmptyString{}; + const bool numericParamPresent = + !RStr.empty() || !aStr.empty() || !bStr.empty() || !rfStr.empty() || + !fStr.empty() || !esStr.empty() || !eStr.empty(); PrimeMeridianNNPtr pm(buildPrimeMeridian(step)); PropertyMap grfMap; @@ -5709,104 +5732,151 @@ PROJStringParser::Private::buildDatum(const Step &step, } }; + // R take precedence + if (!RStr.empty()) { + double R; + try { + R = c_locale_stod(RStr); + } catch (const std::invalid_argument &) { + throw ParsingException("Invalid R value"); + } + auto ellipsoid = Ellipsoid::createSphere(createMapWithUnknownName(), + Length(R), guessBodyName(R)); + return GeodeticReferenceFrame::create( + grfMap.set(IdentifiedObject::NAME_KEY, + title.empty() ? "unknown" : title.c_str()), + ellipsoid, optionalEmptyString, fixupPrimeMeridan(ellipsoid, pm)); + } + if (!datumStr.empty()) { - if (datumStr == "WGS84") { - return overridePmIfNeeded(GeodeticReferenceFrame::EPSG_6326); - } else if (datumStr == "NAD83") { - return overridePmIfNeeded(GeodeticReferenceFrame::EPSG_6269); - } else if (datumStr == "NAD27") { - return overridePmIfNeeded(GeodeticReferenceFrame::EPSG_6267); - } else { + auto l_datum = [&datumStr, &overridePmIfNeeded, &grfMap, + &optionalEmptyString, &pm]() { + if (datumStr == "WGS84") { + return overridePmIfNeeded(GeodeticReferenceFrame::EPSG_6326); + } else if (datumStr == "NAD83") { + return overridePmIfNeeded(GeodeticReferenceFrame::EPSG_6269); + } else if (datumStr == "NAD27") { + return overridePmIfNeeded(GeodeticReferenceFrame::EPSG_6267); + } else { - for (const auto &datumDesc : datumDescs) { - if (datumStr == datumDesc.projName) { - (void)datumDesc.gcsName; // to please cppcheck - (void)datumDesc.gcsCode; // to please cppcheck - auto ellipsoid = Ellipsoid::createFlattenedSphere( - grfMap - .set(IdentifiedObject::NAME_KEY, - datumDesc.ellipsoidName) - .set(Identifier::CODESPACE_KEY, Identifier::EPSG) - .set(Identifier::CODE_KEY, datumDesc.ellipsoidCode), - Length(datumDesc.a), Scale(datumDesc.rf)); - return GeodeticReferenceFrame::create( - grfMap - .set(IdentifiedObject::NAME_KEY, - datumDesc.datumName) - .set(Identifier::CODESPACE_KEY, Identifier::EPSG) - .set(Identifier::CODE_KEY, datumDesc.datumCode), - ellipsoid, optionalEmptyString, pm); + for (const auto &datumDesc : datumDescs) { + if (datumStr == datumDesc.projName) { + (void)datumDesc.gcsName; // to please cppcheck + (void)datumDesc.gcsCode; // to please cppcheck + auto ellipsoid = Ellipsoid::createFlattenedSphere( + grfMap + .set(IdentifiedObject::NAME_KEY, + datumDesc.ellipsoidName) + .set(Identifier::CODESPACE_KEY, + Identifier::EPSG) + .set(Identifier::CODE_KEY, + datumDesc.ellipsoidCode), + Length(datumDesc.a), Scale(datumDesc.rf)); + return GeodeticReferenceFrame::create( + grfMap + .set(IdentifiedObject::NAME_KEY, + datumDesc.datumName) + .set(Identifier::CODESPACE_KEY, + Identifier::EPSG) + .set(Identifier::CODE_KEY, datumDesc.datumCode), + ellipsoid, optionalEmptyString, pm); + } } } + throw ParsingException("unknown datum " + datumStr); + }(); + if (!numericParamPresent) { + return l_datum; } - throw ParsingException("unknown datum " + datumStr); + a = l_datum->ellipsoid()->semiMajorAxis().getSIValue(); + rf = l_datum->ellipsoid()->computedInverseFlattening(); } else if (!ellpsStr.empty()) { - if (ellpsStr == "WGS84") { - return GeodeticReferenceFrame::create( - grfMap.set(IdentifiedObject::NAME_KEY, - title.empty() ? "Unknown based on WGS84 ellipsoid" - : title.c_str()), - Ellipsoid::WGS84, optionalEmptyString, pm); - } else if (ellpsStr == "GRS80") { - return GeodeticReferenceFrame::create( - grfMap.set(IdentifiedObject::NAME_KEY, - title.empty() ? "Unknown based on GRS80 ellipsoid" - : title.c_str()), - Ellipsoid::GRS1980, optionalEmptyString, pm); - } else { - auto proj_ellps = proj_list_ellps(); - for (int i = 0; proj_ellps[i].id != nullptr; i++) { - if (ellpsStr == proj_ellps[i].id) { - assert(strncmp(proj_ellps[i].major, "a=", 2) == 0); - const double a_iter = - c_locale_stod(proj_ellps[i].major + 2); - EllipsoidPtr ellipsoid; - PropertyMap ellpsMap; - if (strncmp(proj_ellps[i].ell, "b=", 2) == 0) { - const double b_iter = - c_locale_stod(proj_ellps[i].ell + 2); - ellipsoid = Ellipsoid::createTwoAxis( - ellpsMap.set(IdentifiedObject::NAME_KEY, - proj_ellps[i].name), - Length(a_iter), Length(b_iter)) - .as_nullable(); - } else { - assert(strncmp(proj_ellps[i].ell, "rf=", 3) == 0); - const double rf_iter = - c_locale_stod(proj_ellps[i].ell + 3); - ellipsoid = Ellipsoid::createFlattenedSphere( - ellpsMap.set(IdentifiedObject::NAME_KEY, - proj_ellps[i].name), - Length(a_iter), Scale(rf_iter)) - .as_nullable(); + auto l_datum = [&ellpsStr, &title, &grfMap, &optionalEmptyString, + &pm]() { + if (ellpsStr == "WGS84") { + return GeodeticReferenceFrame::create( + grfMap.set(IdentifiedObject::NAME_KEY, + title.empty() + ? "Unknown based on WGS84 ellipsoid" + : title.c_str()), + Ellipsoid::WGS84, optionalEmptyString, pm); + } else if (ellpsStr == "GRS80") { + return GeodeticReferenceFrame::create( + grfMap.set(IdentifiedObject::NAME_KEY, + title.empty() + ? "Unknown based on GRS80 ellipsoid" + : title.c_str()), + Ellipsoid::GRS1980, optionalEmptyString, pm); + } else { + auto proj_ellps = proj_list_ellps(); + for (int i = 0; proj_ellps[i].id != nullptr; i++) { + if (ellpsStr == proj_ellps[i].id) { + assert(strncmp(proj_ellps[i].major, "a=", 2) == 0); + const double a_iter = + c_locale_stod(proj_ellps[i].major + 2); + EllipsoidPtr ellipsoid; + PropertyMap ellpsMap; + if (strncmp(proj_ellps[i].ell, "b=", 2) == 0) { + const double b_iter = + c_locale_stod(proj_ellps[i].ell + 2); + ellipsoid = + Ellipsoid::createTwoAxis( + ellpsMap.set(IdentifiedObject::NAME_KEY, + proj_ellps[i].name), + Length(a_iter), Length(b_iter)) + .as_nullable(); + } else { + assert(strncmp(proj_ellps[i].ell, "rf=", 3) == 0); + const double rf_iter = + c_locale_stod(proj_ellps[i].ell + 3); + ellipsoid = + Ellipsoid::createFlattenedSphere( + ellpsMap.set(IdentifiedObject::NAME_KEY, + proj_ellps[i].name), + Length(a_iter), Scale(rf_iter)) + .as_nullable(); + } + return GeodeticReferenceFrame::create( + grfMap.set(IdentifiedObject::NAME_KEY, + title.empty() + ? std::string("Unknown based on ") + + proj_ellps[i].name + + " ellipsoid" + : title), + NN_NO_CHECK(ellipsoid), optionalEmptyString, pm); } - return GeodeticReferenceFrame::create( - grfMap.set(IdentifiedObject::NAME_KEY, - title.empty() - ? std::string("Unknown based on ") + - proj_ellps[i].name + " ellipsoid" - : title), - NN_NO_CHECK(ellipsoid), optionalEmptyString, pm); } + throw ParsingException("unknown ellipsoid " + ellpsStr); } - throw ParsingException("unknown ellipsoid " + ellpsStr); + }(); + if (!numericParamPresent) { + return l_datum; + } + a = l_datum->ellipsoid()->semiMajorAxis().getSIValue(); + if (l_datum->ellipsoid()->semiMinorAxis().has_value()) { + b = l_datum->ellipsoid()->semiMinorAxis()->getSIValue(); + } else { + rf = l_datum->ellipsoid()->computedInverseFlattening(); } } - else if (!aStr.empty() && !bStr.empty()) { - double a; + if (!aStr.empty()) { try { a = c_locale_stod(aStr); } catch (const std::invalid_argument &) { throw ParsingException("Invalid a value"); } - double b; - try { - b = c_locale_stod(bStr); - } catch (const std::invalid_argument &) { - throw ParsingException("Invalid b value"); + } + + if (a > 0 && (b > 0 || !bStr.empty())) { + if (!bStr.empty()) { + try { + b = c_locale_stod(bStr); + } catch (const std::invalid_argument &) { + throw ParsingException("Invalid b value"); + } } auto ellipsoid = Ellipsoid::createTwoAxis(createMapWithUnknownName(), Length(a), @@ -5818,18 +5888,13 @@ PROJStringParser::Private::buildDatum(const Step &step, ellipsoid, optionalEmptyString, fixupPrimeMeridan(ellipsoid, pm)); } - else if (!aStr.empty() && !rfStr.empty()) { - double a; - try { - a = c_locale_stod(aStr); - } catch (const std::invalid_argument &) { - throw ParsingException("Invalid a value"); - } - double rf; - try { - rf = c_locale_stod(rfStr); - } catch (const std::invalid_argument &) { - throw ParsingException("Invalid rf value"); + else if (a > 0 && (rf >= 0 || !rfStr.empty())) { + if (!rfStr.empty()) { + try { + rf = c_locale_stod(rfStr); + } catch (const std::invalid_argument &) { + throw ParsingException("Invalid rf value"); + } } auto ellipsoid = Ellipsoid::createFlattenedSphere( createMapWithUnknownName(), Length(a), Scale(rf), @@ -5841,13 +5906,7 @@ PROJStringParser::Private::buildDatum(const Step &step, ellipsoid, optionalEmptyString, fixupPrimeMeridan(ellipsoid, pm)); } - else if (!aStr.empty() && !fStr.empty()) { - double a; - try { - a = c_locale_stod(aStr); - } catch (const std::invalid_argument &) { - throw ParsingException("Invalid a value"); - } + else if (a > 0 && !fStr.empty()) { double f; try { f = c_locale_stod(fStr); @@ -5864,23 +5923,52 @@ PROJStringParser::Private::buildDatum(const Step &step, ellipsoid, optionalEmptyString, fixupPrimeMeridan(ellipsoid, pm)); } - else if (!RStr.empty()) { - double R; + else if (a > 0 && !eStr.empty()) { + double e; try { - R = c_locale_stod(RStr); + e = c_locale_stod(eStr); } catch (const std::invalid_argument &) { - throw ParsingException("Invalid R value"); + throw ParsingException("Invalid e value"); } - auto ellipsoid = Ellipsoid::createSphere(createMapWithUnknownName(), - Length(R), guessBodyName(R)); + double alpha = asin(e); /* angular eccentricity */ + double f = 1 - cos(alpha); /* = 1 - sqrt (1 - es); */ + auto ellipsoid = Ellipsoid::createFlattenedSphere( + createMapWithUnknownName(), Length(a), + Scale(f != 0.0 ? 1.0 / f : 0.0), guessBodyName(a)) + ->identify(); return GeodeticReferenceFrame::create( grfMap.set(IdentifiedObject::NAME_KEY, title.empty() ? "unknown" : title.c_str()), ellipsoid, optionalEmptyString, fixupPrimeMeridan(ellipsoid, pm)); } - if (!aStr.empty() && bStr.empty() && rfStr.empty()) { - throw ParsingException("a found, but b, f or rf missing"); + else if (a > 0 && !esStr.empty()) { + double es; + try { + es = c_locale_stod(esStr); + } catch (const std::invalid_argument &) { + throw ParsingException("Invalid es value"); + } + double f = 1 - sqrt(1 - es); + auto ellipsoid = Ellipsoid::createFlattenedSphere( + createMapWithUnknownName(), Length(a), + Scale(f != 0.0 ? 1.0 / f : 0.0), guessBodyName(a)) + ->identify(); + return GeodeticReferenceFrame::create( + grfMap.set(IdentifiedObject::NAME_KEY, + title.empty() ? "unknown" : title.c_str()), + ellipsoid, optionalEmptyString, fixupPrimeMeridan(ellipsoid, pm)); + } + + // If only a is specified, create a sphere + if (a > 0 && bStr.empty() && rfStr.empty() && eStr.empty() && + esStr.empty()) { + auto ellipsoid = Ellipsoid::createSphere(createMapWithUnknownName(), + Length(a), guessBodyName(a)); + return GeodeticReferenceFrame::create( + grfMap.set(IdentifiedObject::NAME_KEY, + title.empty() ? "unknown" : title.c_str()), + ellipsoid, optionalEmptyString, fixupPrimeMeridan(ellipsoid, pm)); } if (!bStr.empty() && aStr.empty()) { @@ -5895,6 +5983,14 @@ PROJStringParser::Private::buildDatum(const Step &step, throw ParsingException("f found, but a missing"); } + if (!eStr.empty() && aStr.empty()) { + throw ParsingException("e found, but a missing"); + } + + if (!esStr.empty() && aStr.empty()) { + throw ParsingException("es found, but a missing"); + } + return overridePmIfNeeded(GeodeticReferenceFrame::EPSG_6326); } @@ -6064,6 +6160,22 @@ PROJStringParser::Private::buildEllipsoidalCS(int iStep, int iUnitConvert, // --------------------------------------------------------------------------- +static double getNumericValue(const std::string ¶mValue, + bool *pHasError = nullptr) { + try { + double value = c_locale_stod(paramValue); + if (pHasError) + *pHasError = false; + return value; + } catch (const std::invalid_argument &) { + if (pHasError) + *pHasError = true; + return 0.0; + } +} + +// --------------------------------------------------------------------------- + GeographicCRSNNPtr PROJStringParser::Private::buildGeographicCRS(int iStep, int iUnitConvert, int iAxisSwap, bool ignoreVUnits, @@ -6077,7 +6189,10 @@ PROJStringParser::Private::buildGeographicCRS(int iStep, int iUnitConvert, auto props = PropertyMap().set(IdentifiedObject::NAME_KEY, title.empty() ? "unknown" : title); - if (l_isGeographicStep && hasParamValue(step, "wktext")) { + if (l_isGeographicStep && + (hasParamValue(step, "wktext") || + hasParamValue(step, "lon_wrap") | hasParamValue(step, "geoc") || + getNumericValue(getParamValue(step, "lon_0")) != 0.0)) { props.set("EXTENSION_PROJ4", projString_); } @@ -6214,22 +6329,6 @@ static double getAngularValue(const std::string ¶mValue, // --------------------------------------------------------------------------- -static double getNumericValue(const std::string ¶mValue, - bool *pHasError = nullptr) { - try { - double value = c_locale_stod(paramValue); - if (pHasError) - *pHasError = false; - return value; - } catch (const std::invalid_argument &) { - if (pHasError) - *pHasError = true; - return 0.0; - } -} - -// --------------------------------------------------------------------------- - CRSNNPtr PROJStringParser::Private::buildProjectedCRS( int iStep, GeographicCRSNNPtr geogCRS, int iUnitConvert, int iAxisSwap) { auto &step = steps_[iStep]; @@ -6285,7 +6384,10 @@ CRSNNPtr PROJStringParser::Private::buildProjectedCRS( mapping = getMapping(PROJ_WKT2_NAME_METHOD_GEOSTATIONARY_SATELLITE_SWEEP_Y); } else if (step.name == "omerc") { - if (hasParamValue(step, "no_uoff") || hasParamValue(step, "no_off")) { + if (hasParamValue(step, "no_rot")) { + mapping = nullptr; + } else if (hasParamValue(step, "no_uoff") || + hasParamValue(step, "no_off")) { mapping = getMapping(EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_A); } else if (hasParamValue(step, "lat_1") && @@ -6839,6 +6941,21 @@ PROJStringParser::Private::buildMolodenskyTransformation( // --------------------------------------------------------------------------- +//! @cond Doxygen_Suppress +static const metadata::ExtentPtr nullExtent{}; + +static const metadata::ExtentPtr &getExtent(const crs::CRS *crs) { + const auto &domains = crs->domains(); + if (!domains.empty()) { + return domains[0]->domainOfValidity(); + } + return nullExtent; +} + +//! @endcond + +// --------------------------------------------------------------------------- + /** \brief Instanciate a sub-class of BaseObject from a PROJ string. * @throw ParsingException */ @@ -6909,30 +7026,36 @@ PROJStringParser::createFromPROJString(const std::string &projString) { } auto obj = createFromUserInput(d->steps_[0].name, d->dbContext_, true); - auto geogCRS = dynamic_cast<GeographicCRS *>(obj.get()); - if (geogCRS) { - // Override with longitude latitude in radian - return GeographicCRS::create( - PropertyMap().set(IdentifiedObject::NAME_KEY, - geogCRS->nameStr()), - geogCRS->datum(), geogCRS->datumEnsemble(), - EllipsoidalCS::createLongitudeLatitude( - UnitOfMeasure::RADIAN)); - } - auto projCRS = dynamic_cast<ProjectedCRS *>(obj.get()); - if (projCRS) { - // Override with easting northing orer - auto conv = projCRS->derivingConversionRef(); - if (conv->method()->getEPSGCode() != - EPSG_CODE_METHOD_TRANSVERSE_MERCATOR_SOUTH_ORIENTATED) { - return ProjectedCRS::create( - PropertyMap().set(IdentifiedObject::NAME_KEY, - projCRS->nameStr()), - projCRS->baseCRS(), conv, - CartesianCS::createEastingNorthing( - projCRS->coordinateSystem() - ->axisList()[0] - ->unit())); + auto crs = dynamic_cast<CRS *>(obj.get()); + if (crs) { + PropertyMap properties; + properties.set(IdentifiedObject::NAME_KEY, crs->nameStr()); + const auto &extent = getExtent(crs); + if (extent) { + properties.set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + NN_NO_CHECK(extent)); + } + auto geogCRS = dynamic_cast<GeographicCRS *>(crs); + if (geogCRS) { + // Override with longitude latitude in radian + return GeographicCRS::create( + properties, geogCRS->datum(), geogCRS->datumEnsemble(), + EllipsoidalCS::createLongitudeLatitude( + UnitOfMeasure::RADIAN)); + } + auto projCRS = dynamic_cast<ProjectedCRS *>(crs); + if (projCRS) { + // Override with easting northing order + const auto &conv = projCRS->derivingConversionRef(); + if (conv->method()->getEPSGCode() != + EPSG_CODE_METHOD_TRANSVERSE_MERCATOR_SOUTH_ORIENTATED) { + return ProjectedCRS::create( + properties, projCRS->baseCRS(), conv, + CartesianCS::createEastingNorthing( + projCRS->coordinateSystem() + ->axisList()[0] + ->unit())); + } } } return obj; diff --git a/src/pj_apply_gridshift.c b/src/pj_apply_gridshift.c index 31e0124e..45ce5c8e 100644 --- a/src/pj_apply_gridshift.c +++ b/src/pj_apply_gridshift.c @@ -349,7 +349,7 @@ LP proj_hgrid_apply(PJ *P, LP lp, PJ_DIRECTION direction) { out = nad_cvt(lp, inverse, ct); if (out.lam == HUGE_VAL || out.phi == HUGE_VAL) - pj_ctx_set_errno(P->ctx, PJD_ERR_FAILED_TO_LOAD_GRID); + pj_ctx_set_errno(P->ctx, PJD_ERR_GRID_AREA); return out; diff --git a/src/pj_fwd.c b/src/pj_fwd.c index 1a970374..38443f07 100644 --- a/src/pj_fwd.c +++ b/src/pj_fwd.c @@ -103,7 +103,6 @@ static PJ_COORD fwd_prepare (PJ *P, PJ_COORD coo) { } - static PJ_COORD fwd_finalize (PJ *P, PJ_COORD coo) { switch (OUTPUT_UNITS) { @@ -138,6 +137,14 @@ static PJ_COORD fwd_finalize (PJ *P, PJ_COORD coo) { case PJ_IO_UNITS_ANGULAR: coo.lpz.z = P->vfr_meter * (coo.lpz.z + P->z0); + + if( P->is_long_wrap_set ) { + if( coo.lpz.lam != HUGE_VAL ) { + coo.lpz.lam = P->long_wrap_center + + adjlon(coo.lpz.lam - P->long_wrap_center); + } + } + break; } @@ -1381,6 +1381,19 @@ PJ_OBJ PROJ_DLL *proj_obj_crs_get_sub_crs(PJ_OBJ *crs, int index); PJ_OBJ PROJ_DLL *proj_obj_crs_create_bound_crs_to_WGS84(PJ_OBJ *crs); +PJ_OBJ PROJ_DLL *proj_obj_crs_get_coordinate_system(PJ_OBJ *crs); + +const char PROJ_DLL *proj_obj_cs_get_type(PJ_OBJ* cs); + +int PROJ_DLL proj_obj_cs_get_axis_count(PJ_OBJ *cs); + +int PROJ_DLL proj_obj_cs_get_axis_info(PJ_OBJ *cs, int index, + const char **pName, + const char **pAbbrev, + const char **pDirection, + double *pUnitConvFactor, + const char **pUnitName); + PJ_OBJ PROJ_DLL *proj_obj_get_ellipsoid(PJ_OBJ *obj); int PROJ_DLL proj_obj_ellipsoid_get_parameters(PJ_OBJ *ellipsoid, diff --git a/src/proj_4D_api.c b/src/proj_4D_api.c index 72e1a2d6..b7b500a7 100644 --- a/src/proj_4D_api.c +++ b/src/proj_4D_api.c @@ -794,7 +794,12 @@ PJ *proj_create_crs_to_crs (PJ_CONTEXT *ctx, const char *srid_from, const char return NULL; } - P = proj_create(ctx, proj_string); + if( proj_string[0] == '\0' ) { + /* Null transform ? */ + P = proj_create(ctx, "proj=affine"); + } else { + P = proj_create(ctx, proj_string); + } proj_obj_unref(op); diff --git a/test/gie/more_builtins.gie b/test/gie/more_builtins.gie index 272e503c..00c9d4a0 100644 --- a/test/gie/more_builtins.gie +++ b/test/gie/more_builtins.gie @@ -709,4 +709,14 @@ direction reverse accept 0 0 0 0 expect failure +------------------------------------------------------------------------------- +# Test lon_wrap +operation +proj=longlat +ellps=WGS84 +lon_wrap=180 +------------------------------------------------------------------------------- +direction forward +accept -1 10 0 +expect 359 10 0 + +------------------------------------------------------------------------------- + </gie> diff --git a/test/old/proj_outIGNF.dist b/test/old/proj_outIGNF.dist index 45112f60..611144b8 100644 --- a/test/old/proj_outIGNF.dist +++ b/test/old/proj_outIGNF.dist @@ -1,24 +1,24 @@ -+init=./IGNF:NTFG +to +init=./IGNF:RGF93G ++init=IGNF:NTFG +to +init=IGNF:RGF93G 3.300866856 43.4477976569 0.0000 3d18'0.915"E 43d26'52.077"N 0.000 -+init=./IGNF:LAMBE +to +init=./IGNF:LAMB93 - 600000.0000 2600545.4523 0.0000 652760.737 7033791.243 0.000 - 135638.3592 2418760.4094 0.0000 187194.062 6855928.882 0.000 - 998137.3947 2413822.2844 0.0000 1049052.258 6843776.562 0.000 ++init=IGNF:LAMBE +to +init=IGNF:LAMB93 + 600000.0000 2600545.4523 0.0000 652759.036 7033588.609 0.000 + 135638.3592 2418760.4094 0.0000 187444.148 6856142.911 0.000 + 998137.3947 2413822.2844 0.0000 1048843.997 6843923.913 0.000 600000.0000 2200000.0000 0.0000 649398.872 6633524.191 0.000 - 311552.5340 1906457.4840 0.0000 358799.172 6342652.486 0.000 - 960488.4138 1910172.8812 0.0000 1007068.686 6340907.237 0.000 - 600000.0000 1699510.8340 0.0000 645204.279 6133556.746 0.000 -1203792.5981 626873.17210 0.0000 1238875.764 5057405.016 0.000 -+init=./IGNF:LAMBE +to +init=./IGNF:GEOPORTALFXX - 600000.0000 2600545.4523 0.0000 179040.148 5610495.275 0.000 - 135638.3592 2418760.4094 0.0000 -303729.363 5410118.356 0.000 - 998137.3947 2413822.2844 0.0000 592842.792 5410120.554 0.000 + 311552.5340 1906457.4840 0.0000 358593.374 6342647.465 0.000 + 960488.4138 1910172.8812 0.0000 1007324.119 6340956.093 0.000 + 600000.0000 1699510.8340 0.0000 645201.753 6133255.515 0.000 +1203792.5981 626873.17210 0.0000 * * inf ++init=IGNF:LAMBE +to +init=IGNF:GEOPORTALFXX + 600000.0000 2600545.4523 0.0000 179040.150 5610292.766 0.000 + 135638.3592 2418760.4094 0.0000 -303490.059 5410353.890 0.000 + 998137.3947 2413822.2844 0.0000 592635.926 5410280.335 0.000 600000.0000 2200000.0000 0.0000 179041.670 5209746.080 0.000 - 311552.5340 1906457.4840 0.0000 -96825.465 4909184.136 0.000 - 960488.4138 1910172.8812 0.0000 523880.019 4909191.141 0.000 - 600000.0000 1699510.8340 0.0000 179047.633 4708817.007 0.000 -1203792.5981 626873.17210 0.0000 658287.395 3623739.237 0.000 -+init=./IGNF:RGF93G +to +init=./IGNF:GEOPORTALFXX + 311552.5340 1906457.4840 0.0000 -97021.878 4909167.981 0.000 + 960488.4138 1910172.8812 0.0000 524126.466 4909227.598 0.000 + 600000.0000 1699510.8340 0.0000 179047.637 4708515.623 0.000 +1203792.5981 626873.17210 0.0000 * * inf ++init=IGNF:RGF93G +to +init=IGNF:GEOPORTALFXX 2d20'11.4239243" 50d23'59.7718445" 0.0 179040.151 5610495.281 0.000 -3d57'49.4051448" 48d35'59.7121716" 0.0 -303729.365 5410118.352 0.000 7d44'12.1439796" 48d35'59.7832558" 0.0 592842.794 5410120.550 0.000 @@ -27,7 +27,7 @@ 6d50'12.2276489" 44d06'00.0517019" 0.0 523880.022 4909191.143 0.000 2d20'11.7754730" 42d18'00.0824436" 0.0 179047.634 4708817.010 0.000 9d32'12.6680218" 41d24'00.3542556" 0.0 730783.054 4608637.873 0.000 -+init=./IGNF:RGF93G +to +init=./IGNF:MILLER ++init=IGNF:RGF93G +to +init=IGNF:MILLER 2d20'11.4239243" 50d23'59.7718445" 0.0 260098.730 6140682.441 0.000 -3d57'49.4051448" 48d35'59.7121716" 0.0 -441239.699 5880610.004 0.000 7d44'12.1439796" 48d35'59.7832558" 0.0 861246.246 5880612.827 0.000 @@ -36,5 +36,5 @@ 6d50'12.2276489" 44d06'00.0517019" 0.0 761061.291 5252498.745 0.000 2d20'11.7754730" 42d18'00.0824436" 0.0 260109.601 5009175.714 0.000 9d32'12.6680218" 41d24'00.3542556" 0.0 1061637.534 4889066.592 0.000 -+init=./IGNF:RGR92 +to +init=./IGNF:REUN47 -3356123.5400 1303218.3090 5247430.6050 3353421.833 1304074.314 5248935.607 ++init=IGNF:RGR92 +to +init=IGNF:REUN47 +3356123.5400 1303218.3090 5247430.6050 3353420.949 1304075.021 5248935.144 diff --git a/test/old/testIGNF b/test/old/testIGNF index f785641e..abbaed94 100755 --- a/test/old/testIGNF +++ b/test/old/testIGNF @@ -41,13 +41,7 @@ echo "============================================" OUT=proj_outIGNF -REMOVE_IGNF=NO -if test ! -f IGNF; then - cp ${PROJ_LIB}/IGNF . - REMOVE_IGNF=YES -fi - -INIT_FILE=./IGNF +INIT_FILE=IGNF RES="-f %.3f" # echo "doing tests into file ${OUT}, please wait" @@ -162,10 +156,6 @@ $EXE +init=${INIT_FILE}:RGR92 +to +init=${INIT_FILE}:REUN47 -E $RES >>${OUT} <<E 3356123.5400 1303218.3090 5247430.6050 EOF -if test "${REMOVE_IGNF}" = "YES"; then - rm ./IGNF -fi - # # do 'diff' with distribution results echo "diff ${OUT} with ${OUT}.dist" diff --git a/test/old/testdatumfile b/test/old/testdatumfile index 6cd25b76..6520453d 100755 --- a/test/old/testdatumfile +++ b/test/old/testdatumfile @@ -77,7 +77,7 @@ echo "edge or even a wee bit outside (#141)." >> ${OUT} # Our test points are (1) right on mesh corner, (2) outside but within # epsilon (3) inside a bit (4) outside by more than epsilon # -$EXE +proj=latlong +ellps=WGS84 +nadgrids=ntf_r93.gsb \ +$EXE +proj=latlong +ellps=WGS84 +nadgrids=ntf_r93.gsb,null \ +to +proj=latlong +datum=WGS84 \ -E -f "%.12f" >>${OUT} <<EOF -5.5 52.0 @@ -87,7 +87,7 @@ $EXE +proj=latlong +ellps=WGS84 +nadgrids=ntf_r93.gsb \ EOF # $EXE +proj=latlong +datum=WGS84 \ - +to +proj=latlong +ellps=WGS84 +nadgrids=ntf_r93.gsb \ + +to +proj=latlong +ellps=WGS84 +nadgrids=ntf_r93.gsb,null \ -E -f "%.12f" >>${OUT} <<EOF -5.5 52.0 -5.5000000000001 52.0000000000001 diff --git a/test/old/testvarious b/test/old/testvarious index dbaeb0ce..b3ba4040 100755 --- a/test/old/testvarious +++ b/test/old/testvarious @@ -523,7 +523,7 @@ EOF # echo "##############################################################" >> ${OUT} echo "Test the Natural Earth Projection" >> ${OUT} -$EXE +proj=latlong +a=1 +lon_0=0 \ +$EXE +proj=latlong +a=6371008.7714 +b=6371008.7714 \ +to +proj=natearth +a=6371008.7714 +b=6371008.7714 -f '%.'7'f' \ -E >>${OUT} <<EOF 0.0 0.0 0 0.0 0.0 @@ -554,7 +554,7 @@ $EXE +proj=latlong +a=1 +lon_0=0 \ EOF echo "##############################################################" >> ${OUT} echo "Test the Natural Earth II Projection" >> ${OUT} -$EXE +proj=latlong +a=1 +lon_0=0 \ +$EXE +proj=latlong +a=6371008.7714 +b=6371008.7714 \ +to +proj=natearth2 +a=6371008.7714 +b=6371008.7714 -f '%.'7'f' \ -E >>${OUT} <<EOF 0.0 0.0 0 0.00000000 0.00000000 @@ -585,7 +585,7 @@ $EXE +proj=latlong +a=1 +lon_0=0 \ EOF echo "##############################################################" >> ${OUT} echo "Test the Compact Miller projection" >> ${OUT} -$EXE +proj=latlong +a=1 +lon_0=0 \ +$EXE +proj=latlong +a=6371008.7714 +b=6371008.7714 \ +to +proj=comill +a=6371008.7714 +b=6371008.7714 -f '%.'7'f' \ -E >>${OUT} <<EOF 0.0 0.0 0 0.0 0.0 @@ -656,14 +656,14 @@ EOF echo "##############################################################" >> ${OUT} echo "Check inverse error handling with ob_tran (#225)" >> ${OUT} $EXE +proj=ob_tran \ - +o_proj=moll +a=6378137 +es=0 +o_lon_p=LON_POLE +o_lat_p=LAT_POLE +lon_0=180 \ + +o_proj=moll +a=6378137 +es=0 +o_lon_p=0 +o_lat_p=0 +lon_0=180 \ -E >>${OUT} <<EOF 300000 400000 20000000 30000000 EOF echo "Test inverse handling" >> ${OUT} $EXE -I +proj=ob_tran \ - +o_proj=moll +a=6378137 +es=0 +o_lon_p=LON_POLE +o_lat_p=LAT_POLE +lon_0=180 \ + +o_proj=moll +a=6378137 +es=0 +o_lon_p=0 +o_lat_p=0 +lon_0=180 \ -E >>${OUT} <<EOF 10 20 EOF @@ -939,6 +939,20 @@ $EXE -f '%0.3f' \ 0 0 1000 EOF +echo "##############################################################" >> ${OUT} +echo "Test EPSG:4326 to EPSG:32631" >> ${OUT} +# Input is latitude, longitude order +$EXE EPSG:4326 +to EPSG:32631 -E >> ${OUT} <<EOF +49 3 0 +EOF + +echo "##############################################################" >> ${OUT} +echo "Test EPSG:32631 to EPSG:4326" >> ${OUT} +# Input is latitude, longitude order +$EXE EPSG:32631 EPSG:4326 -E >> ${OUT} <<EOF +400000 5000000 0 +EOF + # Done! # do 'diff' with distribution results diff --git a/test/old/tv_out.dist b/test/old/tv_out.dist index 222c0c5f..148d413d 100644 --- a/test/old/tv_out.dist +++ b/test/old/tv_out.dist @@ -139,7 +139,7 @@ Test healpix forward projection on ellipsoid 0 60.0 0.00000 3.35128 0.00000 0 -60.0 0.00000 -3.35128 0.00000 Test healpix inverse projection on ellipsoid -0 0.7853981633974483 * * 0.00000 +0 0.7853981633974483 * * inf -1.5707963267948966 0 -90.10072 0.00000 0.00000 0.0 0.0 0.00000 0.00000 0.00000 0.0 2.0547874222147415 0.00000 39.58811 0.00000 @@ -347,7 +347,7 @@ Test inverse calcofi projection ############################################################## Check inverse error handling with ob_tran (#225) 300000 400000 42d45'22.377"W 85d35'28.083"N 0.000 -20000000 30000000 * * 0.000 +20000000 30000000 * * inf Test inverse handling 10 20 -1384841.19 7581707.88 0.00 ############################################################## @@ -447,10 +447,16 @@ Test patterson inverse projection 20015114.352186374 -11409566.822831295 180.000 -90.000 0.000 ############################################################## Test Web Mercator to avoid issue #834 in the future -487147.594520173 4934316.46263998 -10370728.796 5552839.742 -0.000 +487147.594520173 4934316.46263998 -10370728.796 5552839.742 0.000 ############################################################## Test vto_meter 0 0 1 0.000 0.000 1000.000 0 0 1 0.000 0.000 1000.000 0 0 1000 0.000 0.000 1.000 0 0 1000 0.000 0.000 1.000 +############################################################## +Test EPSG:4326 to EPSG:32631 +49 3 0 500000.00 5427455.78 0.00 +############################################################## +Test EPSG:32631 to EPSG:4326 +400000 5000000 0 45d8'47.014"N 1d43'40.681"E 0.000 diff --git a/test/unit/test_c_api.cpp b/test/unit/test_c_api.cpp index 8c9f114b..6c22cade 100644 --- a/test/unit/test_c_api.cpp +++ b/test/unit/test_c_api.cpp @@ -809,6 +809,22 @@ TEST_F(CApi, proj_obj_get_source_target_crs_transformation) { // --------------------------------------------------------------------------- +TEST_F(CApi, proj_obj_get_source_crs_of_projected_crs) { + auto crs = proj_obj_create_from_wkt( + m_ctxt, + createProjectedCRS()->exportToWKT(WKTFormatter::create().get()).c_str(), + nullptr); + ASSERT_NE(crs, nullptr); + ObjectKeeper keeper(crs); + + auto sourceCRS = proj_obj_get_source_crs(crs); + ASSERT_NE(sourceCRS, nullptr); + ObjectKeeper keeper_sourceCRS(sourceCRS); + EXPECT_EQ(std::string(proj_obj_get_name(sourceCRS)), "WGS 84"); +} + +// --------------------------------------------------------------------------- + TEST_F(CApi, proj_obj_get_source_target_crs_conversion_without_crs) { auto obj = proj_obj_create_from_database( m_ctxt, "EPSG", "16031", PJ_OBJ_CATEGORY_COORDINATE_OPERATION, false, @@ -1955,4 +1971,69 @@ TEST_F(CApi, proj_obj_create_projections) { /* END: Generated by scripts/create_c_api_projections.py*/ } +// --------------------------------------------------------------------------- + +TEST_F(CApi, proj_obj_cs_get_axis_info) { + { + auto crs = proj_obj_create_from_database( + m_ctxt, "EPSG", "4326", PJ_OBJ_CATEGORY_CRS, false, nullptr); + ASSERT_NE(crs, nullptr); + ObjectKeeper keeper(crs); + + auto cs = proj_obj_crs_get_coordinate_system(crs); + ASSERT_NE(cs, nullptr); + ObjectKeeper keeperCs(cs); + + const char *type = proj_obj_cs_get_type(cs); + ASSERT_NE(type, nullptr); + EXPECT_EQ(std::string(type), "Ellipsoidal"); + + EXPECT_EQ(proj_obj_cs_get_axis_count(cs), 2); + + EXPECT_FALSE(proj_obj_cs_get_axis_info(cs, -1, nullptr, nullptr, + nullptr, nullptr, nullptr)); + + EXPECT_FALSE(proj_obj_cs_get_axis_info(cs, 2, nullptr, nullptr, nullptr, + nullptr, nullptr)); + + EXPECT_TRUE(proj_obj_cs_get_axis_info(cs, 0, nullptr, nullptr, nullptr, + nullptr, nullptr)); + + const char *name = nullptr; + const char *abbrev = nullptr; + const char *direction = nullptr; + double unitConvFactor = 0.0; + const char *unitName = nullptr; + + EXPECT_TRUE(proj_obj_cs_get_axis_info(cs, 0, &name, &abbrev, &direction, + &unitConvFactor, &unitName)); + ASSERT_NE(name, nullptr); + ASSERT_NE(abbrev, nullptr); + ASSERT_NE(direction, nullptr); + ASSERT_NE(unitName, nullptr); + EXPECT_EQ(std::string(name), "Geodetic latitude"); + EXPECT_EQ(std::string(abbrev), "Lat"); + EXPECT_EQ(std::string(direction), "north"); + EXPECT_EQ(unitConvFactor, 0.017453292519943295) << unitConvFactor; + EXPECT_EQ(std::string(unitName), "degree"); + } + + // Non CRS object + { + auto obj = proj_obj_create_from_database( + m_ctxt, "EPSG", "1170", PJ_OBJ_CATEGORY_COORDINATE_OPERATION, false, + nullptr); + ASSERT_NE(obj, nullptr); + ObjectKeeper keeper(obj); + EXPECT_EQ(proj_obj_crs_get_coordinate_system(obj), nullptr); + + EXPECT_EQ(proj_obj_cs_get_type(obj), nullptr); + + EXPECT_EQ(proj_obj_cs_get_axis_count(obj), -1); + + EXPECT_FALSE(proj_obj_cs_get_axis_info(obj, 0, nullptr, nullptr, + nullptr, nullptr, nullptr)); + } +} + } // namespace diff --git a/test/unit/test_io.cpp b/test/unit/test_io.cpp index 99f58739..a4974234 100644 --- a/test/unit/test_io.cpp +++ b/test/unit/test_io.cpp @@ -5686,23 +5686,33 @@ TEST(io, projparse_longlat_a_f_non_zero) { PROJStringParser().createFromPROJString("+proj=longlat +a=2 +f=0.5"); auto crs = nn_dynamic_pointer_cast<GeographicCRS>(obj); ASSERT_TRUE(crs != nullptr); - WKTFormatterNNPtr f(WKTFormatter::create()); - f->simulCurNodeHasId(); - crs->exportToWKT(f.get()); - auto expected = "GEODCRS[\"unknown\",\n" - " DATUM[\"unknown\",\n" - " ELLIPSOID[\"unknown\",2,2,\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " PRIMEM[\"Reference meridian\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " CS[ellipsoidal,2],\n" - " AXIS[\"longitude\",east,\n" - " ORDER[1],\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " AXIS[\"latitude\",north,\n" - " ORDER[2],\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]]]"; - EXPECT_EQ(f->toString(), expected); + EXPECT_EQ(crs->ellipsoid()->semiMajorAxis().getSIValue(), 2); + auto rf = crs->ellipsoid()->computedInverseFlattening(); + EXPECT_EQ(rf, 2) << rf; +} + +// --------------------------------------------------------------------------- + +TEST(io, projparse_longlat_a_e) { + auto obj = + PROJStringParser().createFromPROJString("+proj=longlat +a=2 +e=0.5"); + auto crs = nn_dynamic_pointer_cast<GeographicCRS>(obj); + ASSERT_TRUE(crs != nullptr); + EXPECT_EQ(crs->ellipsoid()->semiMajorAxis().getSIValue(), 2); + auto rf = crs->ellipsoid()->computedInverseFlattening(); + EXPECT_NEAR(rf, 7.46410161513775, 1e-14) << rf; +} + +// --------------------------------------------------------------------------- + +TEST(io, projparse_longlat_a_es) { + auto obj = + PROJStringParser().createFromPROJString("+proj=longlat +a=2 +es=0.5"); + auto crs = nn_dynamic_pointer_cast<GeographicCRS>(obj); + ASSERT_TRUE(crs != nullptr); + EXPECT_EQ(crs->ellipsoid()->semiMajorAxis().getSIValue(), 2); + auto rf = crs->ellipsoid()->computedInverseFlattening(); + EXPECT_EQ(rf, 3.4142135623730958) << rf; } // --------------------------------------------------------------------------- @@ -5711,23 +5721,31 @@ TEST(io, projparse_longlat_R) { auto obj = PROJStringParser().createFromPROJString("+proj=longlat +R=2"); auto crs = nn_dynamic_pointer_cast<GeographicCRS>(obj); ASSERT_TRUE(crs != nullptr); - WKTFormatterNNPtr f(WKTFormatter::create()); - f->simulCurNodeHasId(); - crs->exportToWKT(f.get()); - auto expected = "GEODCRS[\"unknown\",\n" - " DATUM[\"unknown\",\n" - " ELLIPSOID[\"unknown\",2,0,\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " PRIMEM[\"Reference meridian\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " CS[ellipsoidal,2],\n" - " AXIS[\"longitude\",east,\n" - " ORDER[1],\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " AXIS[\"latitude\",north,\n" - " ORDER[2],\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]]]"; - EXPECT_EQ(f->toString(), expected); + EXPECT_TRUE(crs->ellipsoid()->isSphere()); + EXPECT_EQ(crs->ellipsoid()->semiMajorAxis().getSIValue(), 2); +} + +// --------------------------------------------------------------------------- + +TEST(io, projparse_longlat_a) { + auto obj = PROJStringParser().createFromPROJString("+proj=longlat +a=2"); + auto crs = nn_dynamic_pointer_cast<GeographicCRS>(obj); + ASSERT_TRUE(crs != nullptr); + EXPECT_TRUE(crs->ellipsoid()->isSphere()); + EXPECT_EQ(crs->ellipsoid()->semiMajorAxis().getSIValue(), 2); +} + +// --------------------------------------------------------------------------- + +TEST(io, projparse_longlat_a_override_ellps) { + auto obj = PROJStringParser().createFromPROJString( + "+proj=longlat +a=2 +ellps=WGS84"); + auto crs = nn_dynamic_pointer_cast<GeographicCRS>(obj); + ASSERT_TRUE(crs != nullptr); + EXPECT_TRUE(!crs->ellipsoid()->isSphere()); + EXPECT_EQ(crs->ellipsoid()->semiMajorAxis().getSIValue(), 2); + EXPECT_EQ(crs->ellipsoid()->computedInverseFlattening(), 298.25722356300003) + << crs->ellipsoid()->computedInverseFlattening(); } // --------------------------------------------------------------------------- @@ -7505,6 +7523,16 @@ TEST(io, projparse_init) { EXPECT_EQ(co->exportToPROJString(PROJStringFormatter::create().get()), "+proj=pipeline +step +init=epsg:4326 +step +proj=longlat"); } + + { + auto obj = PROJStringParser().createFromPROJString( + "init=epsg:4326 proj=longlat ellps=GRS80"); + auto crs = nn_dynamic_pointer_cast<GeographicCRS>(obj); + ASSERT_TRUE(crs != nullptr); + EXPECT_EQ(crs->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=longlat +ellps=GRS80 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); + } } // --------------------------------------------------------------------------- @@ -7536,10 +7564,6 @@ TEST(io, projparse_errors) { EXPECT_THROW(PROJStringParser().createFromPROJString( "proj=pipeline step init=epsg:4326 init=epsg:4326"), ParsingException); - - EXPECT_THROW(PROJStringParser().createFromPROJString( - "proj=pipeline step init=epsg:4326 proj=longlat"), - ParsingException); } // --------------------------------------------------------------------------- @@ -7577,9 +7601,6 @@ TEST(io, projparse_longlat_errors) { PROJStringParser().createFromPROJString("+proj=longlat +R=invalid"), ParsingException); - EXPECT_THROW(PROJStringParser().createFromPROJString("+proj=longlat +a=1"), - ParsingException); - EXPECT_THROW(PROJStringParser().createFromPROJString("+proj=longlat +b=1"), ParsingException); diff --git a/test/unit/test_operation.cpp b/test/unit/test_operation.cpp index 528aa6b8..818c10ec 100644 --- a/test/unit/test_operation.cpp +++ b/test/unit/test_operation.cpp @@ -4477,6 +4477,38 @@ TEST(operation, geogCRS_to_geogCRS_CH1903_to_CH1903plus_context) { // --------------------------------------------------------------------------- +TEST(operation, geogCRS_to_geogCRS_init_IGNF_to_init_IGNF_context) { + + auto dbContext = DatabaseContext::create(); + + auto sourceCRS_obj = PROJStringParser() + .attachDatabaseContext(dbContext) + .setUsePROJ4InitRules(true) + .createFromPROJString("+init=IGNF:NTFG"); + auto sourceCRS = nn_dynamic_pointer_cast<CRS>(sourceCRS_obj); + ASSERT_TRUE(sourceCRS != nullptr); + + auto targetCRS_obj = PROJStringParser() + .attachDatabaseContext(dbContext) + .setUsePROJ4InitRules(true) + .createFromPROJString("+init=IGNF:RGF93G"); + auto targetCRS = nn_dynamic_pointer_cast<CRS>(targetCRS_obj); + ASSERT_TRUE(targetCRS != nullptr); + + auto authFactory = AuthorityFactory::create(dbContext, std::string()); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_CHECK_ASSERT(sourceCRS), NN_CHECK_ASSERT(targetCRS), ctxt); + ASSERT_EQ(list.size(), 1); + + EXPECT_EQ(list[0]->nameStr(), + "NOUVELLE TRIANGULATION DE LA FRANCE (NTF) vers RGF93 (ETRS89)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=hgridshift +grids=ntf_r93.gsb"); +} + +// --------------------------------------------------------------------------- + TEST(operation, geogCRS_to_geogCRS_3D) { auto geogcrs_m_obj = @@ -5110,6 +5142,83 @@ TEST(operation, boundCRS_of_geogCRS_to_unrelated_geogCRS) { // --------------------------------------------------------------------------- +TEST(operation, createOperation_boundCRS_identified_by_datum) { + auto objSrc = + PROJStringParser().createFromPROJString("+proj=longlat +datum=WGS84"); + auto src = nn_dynamic_pointer_cast<GeographicCRS>(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDest = PROJStringParser().createFromPROJString( + "+proj=utm +zone=32 +a=6378249.2 +b=6356515 " + "+towgs84=-263.0,6.0,431.0 +no_defs"); + auto dest = nn_dynamic_pointer_cast<BoundCRS>(objDest); + ASSERT_TRUE(dest != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dest)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=cart +ellps=WGS84 " + "+step +proj=helmert +x=263 +y=-6 +z=-431 " + "+step +inv +proj=cart +ellps=clrk80ign " + "+step +proj=utm +zone=32 +ellps=clrk80ign"); + + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), std::string()); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dest), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_TRUE(list[0]->isEquivalentTo(op.get())); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_of_clrk_66_geogCRS_to_nad83_geogCRS) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=latlong +ellps=clrk66 +nadgrids=ntv1_can.dat,conus"); + auto src = nn_dynamic_pointer_cast<CRS>(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDest = + PROJStringParser().createFromPROJString("+proj=latlong +datum=NAD83"); + auto dest = nn_dynamic_pointer_cast<CRS>(objDest); + ASSERT_TRUE(dest != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dest)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=hgridshift +grids=ntv1_can.dat,conus " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_of_clrk_66_projCRS_to_nad83_geogCRS) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=utm +zone=17 +ellps=clrk66 +nadgrids=ntv1_can.dat,conus"); + auto src = nn_dynamic_pointer_cast<CRS>(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDest = + PROJStringParser().createFromPROJString("+proj=latlong +datum=NAD83"); + auto dest = nn_dynamic_pointer_cast<CRS>(objDest); + ASSERT_TRUE(dest != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dest)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=utm +zone=17 +ellps=clrk66 " + "+step +proj=hgridshift +grids=ntv1_can.dat,conus " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + TEST(operation, boundCRS_of_projCRS_to_geogCRS) { auto utm31 = ProjectedCRS::create( PropertyMap(), GeographicCRS::EPSG_4807, @@ -5864,6 +5973,25 @@ TEST(operation, createOperation_on_crs_with_canonical_bound_crs) { // --------------------------------------------------------------------------- +TEST(operation, createOperation_fallback_to_proj4_strings) { + auto objDest = PROJStringParser().createFromPROJString( + "+proj=longlat +geoc +ellps=WGS84"); + auto dest = nn_dynamic_pointer_cast<GeographicCRS>(objDest); + ASSERT_TRUE(dest != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + GeographicCRS::EPSG_4326, NN_CHECK_ASSERT(dest)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +inv +proj=longlat +datum=WGS84 " + "+step +proj=longlat +geoc +ellps=WGS84 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + TEST(operation, mercator_variant_A_to_variant_B) { auto projCRS = ProjectedCRS::create( PropertyMap(), GeographicCRS::EPSG_4326, |
