diff --git a/Doxyfile b/Doxyfile index 6da7f7b94c..f76b6b1de3 100644 --- a/Doxyfile +++ b/Doxyfile @@ -766,7 +766,7 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = src/iso19111 include/proj src/proj.h src/proj_experimental.h src/general_doc.dox src/filemanager.cpp src/networkfilemanager.cpp +INPUT = src/iso19111 src/iso19111/operation include/proj src/proj.h src/proj_experimental.h src/general_doc.dox src/filemanager.cpp src/networkfilemanager.cpp # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/include/proj/internal/Makefile.am b/include/proj/internal/Makefile.am index ba14c2a56f..8e9edacf05 100644 --- a/include/proj/internal/Makefile.am +++ b/include/proj/internal/Makefile.am @@ -1,9 +1,6 @@ SUBDIRS = nlohmann noinst_HEADERS = \ - coordinateoperation_constants.hpp \ - coordinateoperation_internal.hpp \ - esri_projection_mappings.hpp \ coordinatesystem_internal.hpp \ internal.hpp \ io_internal.hpp \ diff --git a/scripts/build_esri_projection_mapping.py b/scripts/build_esri_projection_mapping.py index 00a49c6857..752f9850f2 100644 --- a/scripts/build_esri_projection_mapping.py +++ b/scripts/build_esri_projection_mapping.py @@ -751,7 +751,19 @@ def generate_mapping(WKT2_name, esri_proj_name, Params, suffix=''): all_projs.append([esri_proj_name, WKT2_name_s, c_name]) else: all_projs.append([esri_proj_name, WKT2_name, c_name]) - print('static const ESRIParamMapping %s[] = { ' % c_name) + + qualifier = 'static ' + if c_name in ('paramsESRI_Plate_Carree', + 'paramsESRI_Equidistant_Cylindrical', + 'paramsESRI_Gauss_Kruger', + 'paramsESRI_Transverse_Mercator', + 'paramsESRI_Hotine_Oblique_Mercator_Azimuth_Natural_Origin', + 'paramsESRI_Rectified_Skew_Orthomorphic_Natural_Origin', + 'paramsESRI_Hotine_Oblique_Mercator_Azimuth_Center', + 'paramsESRI_Rectified_Skew_Orthomorphic_Center'): + qualifier = '' + + print(qualifier + 'const ESRIParamMapping %s[] = { ' % c_name) for param in Params: for param_name in param: param_value = param[param_name] @@ -803,24 +815,22 @@ def generate_mapping(WKT2_name, esri_proj_name, Params, suffix=''): * DEALINGS IN THE SOFTWARE. ****************************************************************************/ -#ifndef FROM_COORDINATE_OPERATION_CPP -#error This file should only be included from coordinateoperation.cpp +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP #endif -#ifndef ESRI_PROJECTION_MAPPINGS_HH_INCLUDED -#define ESRI_PROJECTION_MAPPINGS_HH_INCLUDED +#include "esriparammappings.hpp" +#include "proj_constants.h" -#include "coordinateoperation_internal.hpp" +#include "proj/internal/internal.hpp" -//! @cond Doxygen_Suppress +NS_PROJ_START -// --------------------------------------------------------------------------- +using namespace internal; -// anonymous namespace -namespace { +namespace operation { -using namespace ::NS_PROJ; -using namespace ::NS_PROJ::operation; +//! @cond Doxygen_Suppress """) @@ -841,6 +851,7 @@ def generate_mapping(WKT2_name, esri_proj_name, Params, suffix=''): count += 1 print('') + print('static const ESRIMethodMapping esriMappings[] = {') for esri_proj_name, WKT2_name, c_name in all_projs: if WKT2_name.startswith('EPSG_'): @@ -852,11 +863,32 @@ def generate_mapping(WKT2_name, esri_proj_name, Params, suffix=''): print('};') print(""" + // --------------------------------------------------------------------------- -} // namespace { +const ESRIMethodMapping *getEsriMappings(size_t &nElts) { + nElts = sizeof(esriMappings) / sizeof(esriMappings[0]); + return esriMappings; +} + +// --------------------------------------------------------------------------- + +std::vector +getMappingsFromESRI(const std::string &esri_name) { + std::vector res; + for (const auto &mapping : esriMappings) { + if (ci_equal(esri_name, mapping.esri_name)) { + res.push_back(&mapping); + } + } + return res; +} //! @endcond -#endif // ESRI_PROJECTION_MAPPINGS_HH_INCLUDED +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END + """) diff --git a/scripts/doxygen.sh b/scripts/doxygen.sh index fd3a04684c..f169c78e41 100755 --- a/scripts/doxygen.sh +++ b/scripts/doxygen.sh @@ -38,7 +38,7 @@ rm -rf docs/build/xml/ # Ugly hack to workaround a bug of Doxygen 1.8.17 that erroneously detect proj_network_get_header_value_cbk_type/ as a variable sed "s/const char\* (\*proj_network_get_header_value_cbk_type/CONST_CHAR\* (\*proj_network_get_header_value_cbk_type/" < src/proj.h > docs/build/tmp_breathe/proj.h -(cat Doxyfile; printf "GENERATE_HTML=NO\nGENERATE_XML=YES\nINPUT= src/iso19111 include/proj docs/build/tmp_breathe/proj.h src/filemanager.cpp src/networkfilemanager.cpp docs/build/tmp_breathe/general_doc.dox.reworked.h") | doxygen - > docs/build/tmp_breathe/docs_log.txt 2>&1 +(cat Doxyfile; printf "GENERATE_HTML=NO\nGENERATE_XML=YES\nINPUT= src/iso19111 src/iso19111/operation include/proj docs/build/tmp_breathe/proj.h src/filemanager.cpp src/networkfilemanager.cpp docs/build/tmp_breathe/general_doc.dox.reworked.h") | doxygen - > docs/build/tmp_breathe/docs_log.txt 2>&1 if grep -i warning docs/build/tmp_breathe/docs_log.txt; then echo "Doxygen warnings found" && cat docs/build/tmp_breathe/docs_log.txt && /bin/false; else diff --git a/scripts/reformat_cpp.sh b/scripts/reformat_cpp.sh index 50a572a11e..89237b6555 100755 --- a/scripts/reformat_cpp.sh +++ b/scripts/reformat_cpp.sh @@ -16,7 +16,10 @@ esac TOPDIR="$SCRIPT_DIR/.." for i in "$TOPDIR"/include/proj/*.hpp "$TOPDIR"/include/proj/internal/*.hpp \ - "$TOPDIR"/src/iso19111/*.cpp "$TOPDIR"/test/unit/*.cpp \ + "$TOPDIR"/src/iso19111/*.cpp \ + "$TOPDIR"/src/iso19111/operation/*.cpp \ + "$TOPDIR"/src/iso19111/operation/*.hpp \ + "$TOPDIR"/test/unit/*.cpp \ "$TOPDIR"/src/apps/projinfo.cpp "$TOPDIR"/src/apps/projsync.cpp \ "$TOPDIR"/src/tracing.cpp "$TOPDIR"/src/grids.hpp "$TOPDIR"/src/grids.cpp \ "$TOPDIR"/src/filemanager.hpp "$TOPDIR"/src/filemanager.cpp \ diff --git a/src/Makefile.am b/src/Makefile.am index 5b36c8bd6a..7425ec36a0 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -65,7 +65,23 @@ libproj_la_SOURCES = \ iso19111/crs.cpp \ iso19111/datum.cpp \ iso19111/coordinatesystem.cpp \ - iso19111/coordinateoperation.cpp \ + iso19111/operation/concatenatedoperation.cpp \ + iso19111/operation/coordinateoperation_internal.hpp \ + iso19111/operation/coordinateoperation_private.hpp \ + iso19111/operation/coordinateoperationfactory.cpp \ + iso19111/operation/conversion.cpp \ + iso19111/operation/esriparammappings.hpp \ + iso19111/operation/esriparammappings.cpp \ + iso19111/operation/operationmethod_private.hpp \ + iso19111/operation/oputils.hpp \ + iso19111/operation/oputils.cpp \ + iso19111/operation/parammappings.hpp \ + iso19111/operation/parammappings.cpp \ + iso19111/operation/projbasedoperation.cpp \ + iso19111/operation/singleoperation.cpp \ + iso19111/operation/transformation.cpp \ + iso19111/operation/vectorofvaluesparams.hpp \ + iso19111/operation/vectorofvaluesparams.cpp \ iso19111/io.cpp \ iso19111/internal.cpp \ iso19111/factory.cpp \ diff --git a/src/iso19111/coordinateoperation.cpp b/src/iso19111/coordinateoperation.cpp deleted file mode 100644 index 0f1c680beb..0000000000 --- a/src/iso19111/coordinateoperation.cpp +++ /dev/null @@ -1,16185 +0,0 @@ -/****************************************************************************** - * - * Project: PROJ - * Purpose: ISO19111:2019 implementation - * Author: Even Rouault - * - ****************************************************************************** - * Copyright (c) 2018, Even Rouault - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - ****************************************************************************/ - -#ifndef FROM_PROJ_CPP -#define FROM_PROJ_CPP -#endif -#define FROM_COORDINATE_OPERATION_CPP - -#include "proj/coordinateoperation.hpp" -#include "proj/common.hpp" -#include "proj/crs.hpp" -#include "proj/io.hpp" -#include "proj/metadata.hpp" -#include "proj/util.hpp" - -#include "proj/internal/internal.hpp" -#include "proj/internal/io_internal.hpp" -#include "proj/internal/tracing.hpp" - -// PROJ include order is sensitive -// clang-format off -#include "proj.h" -#include "proj_internal.h" // M_PI -// clang-format on - -#include "proj_json_streaming_writer.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -// #define TRACE_CREATE_OPERATIONS -// #define DEBUG_SORT -// #define DEBUG_CONCATENATED_OPERATION -#if defined(DEBUG_SORT) || defined(DEBUG_CONCATENATED_OPERATION) -#include - -void dumpWKT(const NS_PROJ::crs::CRS *crs); -void dumpWKT(const NS_PROJ::crs::CRS *crs) { - auto f(NS_PROJ::io::WKTFormatter::create( - NS_PROJ::io::WKTFormatter::Convention::WKT2_2019)); - std::cerr << crs->exportToWKT(f.get()) << std::endl; -} - -void dumpWKT(const NS_PROJ::crs::CRSPtr &crs); -void dumpWKT(const NS_PROJ::crs::CRSPtr &crs) { dumpWKT(crs.get()); } - -void dumpWKT(const NS_PROJ::crs::CRSNNPtr &crs); -void dumpWKT(const NS_PROJ::crs::CRSNNPtr &crs) { - dumpWKT(crs.as_nullable().get()); -} - -void dumpWKT(const NS_PROJ::crs::GeographicCRSPtr &crs); -void dumpWKT(const NS_PROJ::crs::GeographicCRSPtr &crs) { dumpWKT(crs.get()); } - -void dumpWKT(const NS_PROJ::crs::GeographicCRSNNPtr &crs); -void dumpWKT(const NS_PROJ::crs::GeographicCRSNNPtr &crs) { - dumpWKT(crs.as_nullable().get()); -} - -#endif - -using namespace NS_PROJ::internal; - -#if 0 -namespace dropbox{ namespace oxygen { -template<> nn::~nn() = default; -template<> nn::~nn() = default; -template<> nn::~nn() = default; -template<> nn::~nn() = default; -template<> nn::~nn() = default; -template<> nn::~nn() = default; -template<> nn::~nn() = default; -template<> nn::~nn() = default; -template<> nn::~nn() = default; -template<> nn::~nn() = default; -template<> nn::~nn() = default; -template<> nn::~nn() = default; -template<> nn > >::~nn() = default; -template<> nn > >::~nn() = default; -}} -#endif - -#include "proj/internal/coordinateoperation_constants.hpp" -#include "proj/internal/coordinateoperation_internal.hpp" -#include "proj/internal/esri_projection_mappings.hpp" - -#if 0 -namespace dropbox{ namespace oxygen { -template<> nn>::~nn() = default; -template<> nn>::~nn() = default; -template<> nn>::~nn() = default; -template<> nn::~nn() = default; -}} -#endif - -// --------------------------------------------------------------------------- - -NS_PROJ_START -namespace operation { - -//! @cond Doxygen_Suppress - -constexpr double UTM_LATITUDE_OF_NATURAL_ORIGIN = 0.0; -constexpr double UTM_SCALE_FACTOR = 0.9996; -constexpr double UTM_FALSE_EASTING = 500000.0; -constexpr double UTM_NORTH_FALSE_NORTHING = 0.0; -constexpr double UTM_SOUTH_FALSE_NORTHING = 10000000.0; - -static const std::string INVERSE_OF = "Inverse of "; -static const char *BALLPARK_GEOCENTRIC_TRANSLATION = - "Ballpark geocentric translation"; -static const char *NULL_GEOGRAPHIC_OFFSET = "Null geographic offset"; -static const char *NULL_GEOCENTRIC_TRANSLATION = "Null geocentric translation"; -static const char *BALLPARK_GEOGRAPHIC_OFFSET = "Ballpark geographic offset"; -static const char *BALLPARK_VERTICAL_TRANSFORMATION = - " (ballpark vertical transformation)"; -static const char *BALLPARK_VERTICAL_TRANSFORMATION_NO_ELLIPSOID_VERT_HEIGHT = - " (ballpark vertical transformation, without ellipsoid height to vertical " - "height correction)"; - -static const std::string AXIS_ORDER_CHANGE_2D_NAME = "axis order change (2D)"; -static const std::string AXIS_ORDER_CHANGE_3D_NAME = - "axis order change (geographic3D horizontal)"; -//! @endcond - -//! @cond Doxygen_Suppress -static util::PropertyMap -createPropertiesForInverse(const CoordinateOperation *op, bool derivedFrom, - bool approximateInversion); -//! @endcond - -// --------------------------------------------------------------------------- - -#ifdef TRACE_CREATE_OPERATIONS - -//! @cond Doxygen_Suppress - -static std::string objectAsStr(const common::IdentifiedObject *obj) { - std::string ret(obj->nameStr()); - const auto &ids = obj->identifiers(); - if (!ids.empty()) { - ret += " ("; - ret += (*ids[0]->codeSpace()) + ":" + ids[0]->code(); - ret += ")"; - } - return ret; -} -//! @endcond - -#endif - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -class InvalidOperationEmptyIntersection : public InvalidOperation { - public: - explicit InvalidOperationEmptyIntersection(const std::string &message); - InvalidOperationEmptyIntersection( - const InvalidOperationEmptyIntersection &other); - ~InvalidOperationEmptyIntersection() override; -}; - -InvalidOperationEmptyIntersection::InvalidOperationEmptyIntersection( - const std::string &message) - : InvalidOperation(message) {} - -InvalidOperationEmptyIntersection::InvalidOperationEmptyIntersection( - const InvalidOperationEmptyIntersection &) = default; - -InvalidOperationEmptyIntersection::~InvalidOperationEmptyIntersection() = - default; - -// --------------------------------------------------------------------------- - -static std::string createEntryEqParam(const std::string &a, - const std::string &b) { - return a < b ? a + b : b + a; -} - -static std::set buildSetEquivalentParameters() { - - std::set set; - - const char *const listOfEquivalentParameterNames[][7] = { - {"latitude_of_point_1", "Latitude_Of_1st_Point", nullptr}, - {"longitude_of_point_1", "Longitude_Of_1st_Point", nullptr}, - {"latitude_of_point_2", "Latitude_Of_2nd_Point", nullptr}, - {"longitude_of_point_2", "Longitude_Of_2nd_Point", nullptr}, - - {"satellite_height", "height", nullptr}, - - {EPSG_NAME_PARAMETER_FALSE_EASTING, - EPSG_NAME_PARAMETER_EASTING_FALSE_ORIGIN, - EPSG_NAME_PARAMETER_EASTING_PROJECTION_CENTRE, nullptr}, - - {EPSG_NAME_PARAMETER_FALSE_NORTHING, - EPSG_NAME_PARAMETER_NORTHING_FALSE_ORIGIN, - EPSG_NAME_PARAMETER_NORTHING_PROJECTION_CENTRE, nullptr}, - - {EPSG_NAME_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN, WKT1_SCALE_FACTOR, - EPSG_NAME_PARAMETER_SCALE_FACTOR_INITIAL_LINE, - EPSG_NAME_PARAMETER_SCALE_FACTOR_PSEUDO_STANDARD_PARALLEL, nullptr}, - - {WKT1_LATITUDE_OF_ORIGIN, WKT1_LATITUDE_OF_CENTER, - EPSG_NAME_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN, - EPSG_NAME_PARAMETER_LATITUDE_FALSE_ORIGIN, - EPSG_NAME_PARAMETER_LATITUDE_PROJECTION_CENTRE, "Central_Parallel", - nullptr}, - - {WKT1_CENTRAL_MERIDIAN, WKT1_LONGITUDE_OF_CENTER, - EPSG_NAME_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN, - EPSG_NAME_PARAMETER_LONGITUDE_FALSE_ORIGIN, - EPSG_NAME_PARAMETER_LONGITUDE_PROJECTION_CENTRE, - EPSG_NAME_PARAMETER_LONGITUDE_OF_ORIGIN, nullptr}, - - {"pseudo_standard_parallel_1", WKT1_STANDARD_PARALLEL_1, nullptr}, - }; - - for (const auto ¶mList : listOfEquivalentParameterNames) { - for (size_t i = 0; paramList[i]; i++) { - auto a = metadata::Identifier::canonicalizeName(paramList[i]); - for (size_t j = i + 1; paramList[j]; j++) { - auto b = metadata::Identifier::canonicalizeName(paramList[j]); - set.insert(createEntryEqParam(a, b)); - } - } - } - return set; -} - -bool areEquivalentParameters(const std::string &a, const std::string &b) { - - static const std::set setEquivalentParameters = - buildSetEquivalentParameters(); - - auto a_can = metadata::Identifier::canonicalizeName(a); - auto b_can = metadata::Identifier::canonicalizeName(b); - return setEquivalentParameters.find(createEntryEqParam(a_can, b_can)) != - setEquivalentParameters.end(); -} - -// --------------------------------------------------------------------------- - -PROJ_NO_INLINE const MethodMapping *getMapping(int epsg_code) noexcept { - for (const auto &mapping : projectionMethodMappings) { - if (mapping.epsg_code == epsg_code) { - return &mapping; - } - } - return nullptr; -} - -// --------------------------------------------------------------------------- - -const MethodMapping *getMapping(const OperationMethod *method) noexcept { - const std::string &name(method->nameStr()); - const int epsg_code = method->getEPSGCode(); - for (const auto &mapping : projectionMethodMappings) { - if ((epsg_code != 0 && mapping.epsg_code == epsg_code) || - metadata::Identifier::isEquivalentName(mapping.wkt2_name, - name.c_str())) { - return &mapping; - } - } - return nullptr; -} - -// --------------------------------------------------------------------------- - -const MethodMapping *getMappingFromWKT1(const std::string &wkt1_name) noexcept { - // Unusual for a WKT1 projection name, but mentioned in OGC 12-063r5 C.4.2 - if (ci_starts_with(wkt1_name, "UTM zone")) { - return getMapping(EPSG_CODE_METHOD_TRANSVERSE_MERCATOR); - } - - for (const auto &mapping : projectionMethodMappings) { - if (mapping.wkt1_name && metadata::Identifier::isEquivalentName( - mapping.wkt1_name, wkt1_name.c_str())) { - return &mapping; - } - } - return nullptr; -} -// --------------------------------------------------------------------------- - -const MethodMapping *getMapping(const char *wkt2_name) noexcept { - for (const auto &mapping : projectionMethodMappings) { - if (metadata::Identifier::isEquivalentName(mapping.wkt2_name, - wkt2_name)) { - return &mapping; - } - } - for (const auto &mapping : otherMethodMappings) { - if (metadata::Identifier::isEquivalentName(mapping.wkt2_name, - wkt2_name)) { - return &mapping; - } - } - return nullptr; -} - -// --------------------------------------------------------------------------- - -std::vector -getMappingsFromPROJName(const std::string &projName) { - std::vector res; - for (const auto &mapping : projectionMethodMappings) { - if (mapping.proj_name_main && projName == mapping.proj_name_main) { - res.push_back(&mapping); - } - } - return res; -} - -// --------------------------------------------------------------------------- - -static const ParamMapping *getMapping(const MethodMapping *mapping, - const OperationParameterNNPtr ¶m) { - if (mapping->params == nullptr) { - return nullptr; - } - - // First try with id - const int epsg_code = param->getEPSGCode(); - if (epsg_code) { - for (int i = 0; mapping->params[i] != nullptr; ++i) { - const auto *paramMapping = mapping->params[i]; - if (paramMapping->epsg_code == epsg_code) { - return paramMapping; - } - } - } - - // then equivalent name - const std::string &name = param->nameStr(); - for (int i = 0; mapping->params[i] != nullptr; ++i) { - const auto *paramMapping = mapping->params[i]; - if (metadata::Identifier::isEquivalentName(paramMapping->wkt2_name, - name.c_str())) { - return paramMapping; - } - } - - // and finally different name, but equivalent parameter - for (int i = 0; mapping->params[i] != nullptr; ++i) { - const auto *paramMapping = mapping->params[i]; - if (areEquivalentParameters(paramMapping->wkt2_name, name)) { - return paramMapping; - } - } - - return nullptr; -} - -// --------------------------------------------------------------------------- - -const ParamMapping *getMappingFromWKT1(const MethodMapping *mapping, - const std::string &wkt1_name) { - for (int i = 0; mapping->params[i] != nullptr; ++i) { - const auto *paramMapping = mapping->params[i]; - if (paramMapping->wkt1_name && - (metadata::Identifier::isEquivalentName(paramMapping->wkt1_name, - wkt1_name.c_str()) || - areEquivalentParameters(paramMapping->wkt1_name, wkt1_name))) { - return paramMapping; - } - } - return nullptr; -} - -// --------------------------------------------------------------------------- - -std::vector -getMappingsFromESRI(const std::string &esri_name) { - std::vector res; - for (const auto &mapping : esriMappings) { - if (ci_equal(esri_name, mapping.esri_name)) { - res.push_back(&mapping); - } - } - return res; -} - -// --------------------------------------------------------------------------- - -static const ESRIMethodMapping *getESRIMapping(const std::string &wkt2_name, - int epsg_code) { - for (const auto &mapping : esriMappings) { - if ((epsg_code != 0 && mapping.epsg_code == epsg_code) || - ci_equal(wkt2_name, mapping.wkt2_name)) { - return &mapping; - } - } - return nullptr; -} - -// --------------------------------------------------------------------------- - -static double getAccuracy(const std::vector &ops); - -// Returns the accuracy of an operation, or -1 if unknown -static double getAccuracy(const CoordinateOperationNNPtr &op) { - - if (dynamic_cast(op.get())) { - // A conversion is perfectly accurate. - return 0.0; - } - - double accuracy = -1.0; - const auto &accuracies = op->coordinateOperationAccuracies(); - if (!accuracies.empty()) { - try { - accuracy = c_locale_stod(accuracies[0]->value()); - } catch (const std::exception &) { - } - } else { - auto concatenated = - dynamic_cast(op.get()); - if (concatenated) { - accuracy = getAccuracy(concatenated->operations()); - } - } - return accuracy; -} - -// --------------------------------------------------------------------------- - -// Returns the accuracy of a set of concatenated operations, or -1 if unknown -static double getAccuracy(const std::vector &ops) { - double accuracy = -1.0; - for (const auto &subop : ops) { - const double subops_accuracy = getAccuracy(subop); - if (subops_accuracy < 0.0) { - return -1.0; - } - if (accuracy < 0.0) { - accuracy = 0.0; - } - accuracy += subops_accuracy; - } - return accuracy; -} - -// --------------------------------------------------------------------------- - -static metadata::ExtentPtr -getExtent(const std::vector &ops, - bool conversionExtentIsWorld, bool &emptyIntersection); - -static metadata::ExtentPtr getExtent(const CoordinateOperationNNPtr &op, - bool conversionExtentIsWorld, - bool &emptyIntersection) { - auto conv = dynamic_cast(op.get()); - if (conv) { - emptyIntersection = false; - return metadata::Extent::WORLD; - } - const auto &domains = op->domains(); - if (!domains.empty()) { - emptyIntersection = false; - return domains[0]->domainOfValidity(); - } - auto concatenated = dynamic_cast(op.get()); - if (!concatenated) { - emptyIntersection = false; - return nullptr; - } - return getExtent(concatenated->operations(), conversionExtentIsWorld, - emptyIntersection); -} - -// --------------------------------------------------------------------------- - -static const metadata::ExtentPtr nullExtent{}; - -static const metadata::ExtentPtr &getExtent(const crs::CRSNNPtr &crs) { - const auto &domains = crs->domains(); - if (!domains.empty()) { - return domains[0]->domainOfValidity(); - } - const auto *boundCRS = dynamic_cast(crs.get()); - if (boundCRS) { - return getExtent(boundCRS->baseCRS()); - } - return nullExtent; -} - -static const metadata::ExtentPtr -getExtentPossiblySynthetized(const crs::CRSNNPtr &crs, bool &approxOut) { - const auto &rawExtent(getExtent(crs)); - approxOut = false; - if (rawExtent) - return rawExtent; - const auto compoundCRS = dynamic_cast(crs.get()); - if (compoundCRS) { - // For a compoundCRS, take the intersection of the extent of its - // components. - const auto &components = compoundCRS->componentReferenceSystems(); - metadata::ExtentPtr extent; - approxOut = true; - for (const auto &component : components) { - const auto &componentExtent(getExtent(component)); - if (extent && componentExtent) - extent = extent->intersection(NN_NO_CHECK(componentExtent)); - else if (componentExtent) - extent = componentExtent; - } - return extent; - } - return rawExtent; -} - -// --------------------------------------------------------------------------- - -static metadata::ExtentPtr -getExtent(const std::vector &ops, - bool conversionExtentIsWorld, bool &emptyIntersection) { - metadata::ExtentPtr res = nullptr; - for (const auto &subop : ops) { - - const auto &subExtent = - getExtent(subop, conversionExtentIsWorld, emptyIntersection); - if (!subExtent) { - if (emptyIntersection) { - return nullptr; - } - continue; - } - if (res == nullptr) { - res = subExtent; - } else { - res = res->intersection(NN_NO_CHECK(subExtent)); - if (!res) { - emptyIntersection = true; - return nullptr; - } - } - } - emptyIntersection = false; - return res; -} - -// --------------------------------------------------------------------------- - -static double getPseudoArea(const metadata::ExtentPtr &extent) { - if (!extent) - return 0.0; - const auto &geographicElements = extent->geographicElements(); - if (geographicElements.empty()) - return 0.0; - auto bbox = dynamic_cast( - geographicElements[0].get()); - if (!bbox) - return 0; - double w = bbox->westBoundLongitude(); - double s = bbox->southBoundLatitude(); - double e = bbox->eastBoundLongitude(); - double n = bbox->northBoundLatitude(); - if (w > e) { - e += 360.0; - } - // Integrate cos(lat) between south_lat and north_lat - return (e - w) * (std::sin(common::Angle(n).getSIValue()) - - std::sin(common::Angle(s).getSIValue())); -} - -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct CoordinateOperation::Private { - util::optional operationVersion_{}; - std::vector - coordinateOperationAccuracies_{}; - std::weak_ptr sourceCRSWeak_{}; - std::weak_ptr targetCRSWeak_{}; - crs::CRSPtr interpolationCRS_{}; - util::optional sourceCoordinateEpoch_{}; - util::optional targetCoordinateEpoch_{}; - bool hasBallparkTransformation_ = false; - bool use3DHelmert_ = false; - - // do not set this for a ProjectedCRS.definingConversion - struct CRSStrongRef { - crs::CRSNNPtr sourceCRS_; - crs::CRSNNPtr targetCRS_; - CRSStrongRef(const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn) - : sourceCRS_(sourceCRSIn), targetCRS_(targetCRSIn) {} - }; - std::unique_ptr strongRef_{}; - - Private() = default; - Private(const Private &other) - : operationVersion_(other.operationVersion_), - coordinateOperationAccuracies_(other.coordinateOperationAccuracies_), - sourceCRSWeak_(other.sourceCRSWeak_), - targetCRSWeak_(other.targetCRSWeak_), - interpolationCRS_(other.interpolationCRS_), - sourceCoordinateEpoch_(other.sourceCoordinateEpoch_), - targetCoordinateEpoch_(other.targetCoordinateEpoch_), - hasBallparkTransformation_(other.hasBallparkTransformation_), - strongRef_(other.strongRef_ ? internal::make_unique( - *(other.strongRef_)) - : nullptr) {} - - Private &operator=(const Private &) = delete; -}; - -// --------------------------------------------------------------------------- - -GridDescription::GridDescription() - : shortName{}, fullName{}, packageName{}, url{}, directDownload(false), - openLicense(false), available(false) {} - -GridDescription::~GridDescription() = default; - -GridDescription::GridDescription(const GridDescription &) = default; - -GridDescription::GridDescription(GridDescription &&other) noexcept - : shortName(std::move(other.shortName)), - fullName(std::move(other.fullName)), - packageName(std::move(other.packageName)), - url(std::move(other.url)), - directDownload(other.directDownload), - openLicense(other.openLicense), - available(other.available) {} - -//! @endcond - -// --------------------------------------------------------------------------- - -CoordinateOperation::CoordinateOperation() - : d(internal::make_unique()) {} - -// --------------------------------------------------------------------------- - -CoordinateOperation::CoordinateOperation(const CoordinateOperation &other) - : ObjectUsage(other), d(internal::make_unique(*other.d)) {} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -CoordinateOperation::~CoordinateOperation() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Return the version of the coordinate transformation (i.e. - * instantiation - * due to the stochastic nature of the parameters). - * - * Mandatory when describing a coordinate transformation or point motion - * operation, and should not be supplied for a coordinate conversion. - * - * @return version or empty. - */ -const util::optional & -CoordinateOperation::operationVersion() const { - return d->operationVersion_; -} - -// --------------------------------------------------------------------------- - -/** \brief Return estimate(s) of the impact of this coordinate operation on - * point accuracy. - * - * Gives position error estimates for target coordinates of this coordinate - * operation, assuming no errors in source coordinates. - * - * @return estimate(s) or empty vector. - */ -const std::vector & -CoordinateOperation::coordinateOperationAccuracies() const { - return d->coordinateOperationAccuracies_; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the source CRS of this coordinate operation. - * - * This should not be null, expect for of a derivingConversion of a DerivedCRS - * when the owning DerivedCRS has been destroyed. - * - * @return source CRS, or null. - */ -const crs::CRSPtr CoordinateOperation::sourceCRS() const { - return d->sourceCRSWeak_.lock(); -} - -// --------------------------------------------------------------------------- - -/** \brief Return the target CRS of this coordinate operation. - * - * This should not be null, expect for of a derivingConversion of a DerivedCRS - * when the owning DerivedCRS has been destroyed. - * - * @return target CRS, or null. - */ -const crs::CRSPtr CoordinateOperation::targetCRS() const { - return d->targetCRSWeak_.lock(); -} - -// --------------------------------------------------------------------------- - -/** \brief Return the interpolation CRS of this coordinate operation. - * - * @return interpolation CRS, or null. - */ -const crs::CRSPtr &CoordinateOperation::interpolationCRS() const { - return d->interpolationCRS_; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the source epoch of coordinates. - * - * @return source epoch of coordinates, or empty. - */ -const util::optional & -CoordinateOperation::sourceCoordinateEpoch() const { - return d->sourceCoordinateEpoch_; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the target epoch of coordinates. - * - * @return target epoch of coordinates, or empty. - */ -const util::optional & -CoordinateOperation::targetCoordinateEpoch() const { - return d->targetCoordinateEpoch_; -} - -// --------------------------------------------------------------------------- - -void CoordinateOperation::setWeakSourceTargetCRS( - std::weak_ptr sourceCRSIn, std::weak_ptr targetCRSIn) { - d->sourceCRSWeak_ = sourceCRSIn; - d->targetCRSWeak_ = targetCRSIn; -} - -// --------------------------------------------------------------------------- - -void CoordinateOperation::setCRSs(const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, - const crs::CRSPtr &interpolationCRSIn) { - d->strongRef_ = - internal::make_unique(sourceCRSIn, targetCRSIn); - d->sourceCRSWeak_ = sourceCRSIn.as_nullable(); - d->targetCRSWeak_ = targetCRSIn.as_nullable(); - d->interpolationCRS_ = interpolationCRSIn; -} -// --------------------------------------------------------------------------- - -void CoordinateOperation::setCRSs(const CoordinateOperation *in, - bool inverseSourceTarget) { - auto l_sourceCRS = in->sourceCRS(); - auto l_targetCRS = in->targetCRS(); - if (l_sourceCRS && l_targetCRS) { - auto nn_sourceCRS = NN_NO_CHECK(l_sourceCRS); - auto nn_targetCRS = NN_NO_CHECK(l_targetCRS); - if (inverseSourceTarget) { - setCRSs(nn_targetCRS, nn_sourceCRS, in->interpolationCRS()); - } else { - setCRSs(nn_sourceCRS, nn_targetCRS, in->interpolationCRS()); - } - } -} - -// --------------------------------------------------------------------------- - -void CoordinateOperation::setAccuracies( - const std::vector &accuracies) { - d->coordinateOperationAccuracies_ = accuracies; -} - -// --------------------------------------------------------------------------- - -/** \brief Return whether a coordinate operation can be instantiated as - * a PROJ pipeline, checking in particular that referenced grids are - * available. - */ -bool CoordinateOperation::isPROJInstantiable( - const io::DatabaseContextPtr &databaseContext, - bool considerKnownGridsAsAvailable) const { - try { - exportToPROJString(io::PROJStringFormatter::create().get()); - } catch (const std::exception &) { - return false; - } - for (const auto &gridDesc : - gridsNeeded(databaseContext, considerKnownGridsAsAvailable)) { - if (!gridDesc.available) { - return false; - } - } - return true; -} - -// --------------------------------------------------------------------------- - -/** \brief Return whether a coordinate operation has a "ballpark" - * transformation, - * that is a very approximate one, due to lack of more accurate transformations. - * - * Typically a null geographic offset between two horizontal datum, or a - * null vertical offset (or limited to unit changes) between two vertical - * datum. Errors of several tens to one hundred meters might be expected, - * compared to more accurate transformations. - */ -bool CoordinateOperation::hasBallparkTransformation() const { - return d->hasBallparkTransformation_; -} - -// --------------------------------------------------------------------------- - -void CoordinateOperation::setHasBallparkTransformation(bool b) { - d->hasBallparkTransformation_ = b; -} - -// --------------------------------------------------------------------------- - -void CoordinateOperation::setProperties( - const util::PropertyMap &properties) // throw(InvalidValueTypeException) -{ - ObjectUsage::setProperties(properties); - properties.getStringValue(OPERATION_VERSION_KEY, d->operationVersion_); -} - -// --------------------------------------------------------------------------- - -/** \brief Return a variation of the current coordinate operation whose axis - * order is the one expected for visualization purposes. - */ -CoordinateOperationNNPtr -CoordinateOperation::normalizeForVisualization() const { - auto l_sourceCRS = sourceCRS(); - auto l_targetCRS = targetCRS(); - if (!l_sourceCRS || !l_targetCRS) { - throw util::UnsupportedOperationException( - "Cannot retrieve source or target CRS"); - } - const bool swapSource = - l_sourceCRS->mustAxisOrderBeSwitchedForVisualization(); - const bool swapTarget = - l_targetCRS->mustAxisOrderBeSwitchedForVisualization(); - auto l_this = NN_NO_CHECK(std::dynamic_pointer_cast( - shared_from_this().as_nullable())); - if (!swapSource && !swapTarget) { - return l_this; - } - std::vector subOps; - if (swapSource) { - auto op = Conversion::createAxisOrderReversal(false); - op->setCRSs(l_sourceCRS->normalizeForVisualization(), - NN_NO_CHECK(l_sourceCRS), nullptr); - subOps.emplace_back(op); - } - subOps.emplace_back(l_this); - if (swapTarget) { - auto op = Conversion::createAxisOrderReversal(false); - op->setCRSs(NN_NO_CHECK(l_targetCRS), - l_targetCRS->normalizeForVisualization(), nullptr); - subOps.emplace_back(op); - } - return util::nn_static_pointer_cast( - ConcatenatedOperation::createComputeMetadata(subOps, true)); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -CoordinateOperationNNPtr CoordinateOperation::shallowClone() const { - return _shallowClone(); -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct OperationMethod::Private { - util::optional formula_{}; - util::optional formulaCitation_{}; - std::vector parameters_{}; - std::string projMethodOverride_{}; -}; -//! @endcond - -// --------------------------------------------------------------------------- - -OperationMethod::OperationMethod() : d(internal::make_unique()) {} - -// --------------------------------------------------------------------------- - -OperationMethod::OperationMethod(const OperationMethod &other) - : IdentifiedObject(other), d(internal::make_unique(*other.d)) {} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -OperationMethod::~OperationMethod() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Return the formula(s) or procedure used by this coordinate operation - * method. - * - * This may be a reference to a publication (in which case use - * formulaCitation()). - * - * Note that the operation method may not be analytic, in which case this - * attribute references or contains the procedure, not an analytic formula. - * - * @return the formula, or empty. - */ -const util::optional &OperationMethod::formula() PROJ_PURE_DEFN { - return d->formula_; -} - -// --------------------------------------------------------------------------- - -/** \brief Return a reference to a publication giving the formula(s) or - * procedure - * used by the coordinate operation method. - * - * @return the formula citation, or empty. - */ -const util::optional & -OperationMethod::formulaCitation() PROJ_PURE_DEFN { - return d->formulaCitation_; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the parameters of this operation method. - * - * @return the parameters. - */ -const std::vector & -OperationMethod::parameters() PROJ_PURE_DEFN { - return d->parameters_; -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a operation method from a vector of - * GeneralOperationParameter. - * - * @param properties See \ref general_properties. At minimum the name should be - * defined. - * @param parameters Vector of GeneralOperationParameterNNPtr. - * @return a new OperationMethod. - */ -OperationMethodNNPtr OperationMethod::create( - const util::PropertyMap &properties, - const std::vector ¶meters) { - OperationMethodNNPtr method( - OperationMethod::nn_make_shared()); - method->assignSelf(method); - method->setProperties(properties); - method->d->parameters_ = parameters; - properties.getStringValue("proj_method", method->d->projMethodOverride_); - return method; -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a operation method from a vector of OperationParameter. - * - * @param properties See \ref general_properties. At minimum the name should be - * defined. - * @param parameters Vector of OperationParameterNNPtr. - * @return a new OperationMethod. - */ -OperationMethodNNPtr OperationMethod::create( - const util::PropertyMap &properties, - const std::vector ¶meters) { - std::vector parametersGeneral; - parametersGeneral.reserve(parameters.size()); - for (const auto &p : parameters) { - parametersGeneral.push_back(p); - } - return create(properties, parametersGeneral); -} - -// --------------------------------------------------------------------------- - -/** \brief Return the EPSG code, either directly, or through the name - * @return code, or 0 if not found - */ -int OperationMethod::getEPSGCode() PROJ_PURE_DEFN { - int epsg_code = IdentifiedObject::getEPSGCode(); - if (epsg_code == 0) { - auto l_name = nameStr(); - if (ends_with(l_name, " (3D)")) { - l_name.resize(l_name.size() - strlen(" (3D)")); - } - for (const auto &tuple : methodNameCodes) { - if (metadata::Identifier::isEquivalentName(l_name.c_str(), - tuple.name)) { - return tuple.epsg_code; - } - } - } - return epsg_code; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -void OperationMethod::_exportToWKT(io::WKTFormatter *formatter) const { - const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; - formatter->startNode(isWKT2 ? io::WKTConstants::METHOD - : io::WKTConstants::PROJECTION, - !identifiers().empty()); - std::string l_name(nameStr()); - if (!isWKT2) { - const MethodMapping *mapping = getMapping(this); - if (mapping == nullptr) { - l_name = replaceAll(l_name, " ", "_"); - } else { - if (l_name == - PROJ_WKT2_NAME_METHOD_GEOSTATIONARY_SATELLITE_SWEEP_X) { - l_name = "Geostationary_Satellite"; - } else { - if (mapping->wkt1_name == nullptr) { - throw io::FormattingException( - std::string("Unsupported conversion method: ") + - mapping->wkt2_name); - } - l_name = mapping->wkt1_name; - } - } - } - formatter->addQuotedString(l_name); - if (formatter->outputId()) { - formatID(formatter); - } - formatter->endNode(); -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -void OperationMethod::_exportToJSON( - io::JSONFormatter *formatter) const // throw(FormattingException) -{ - auto writer = formatter->writer(); - auto objectContext(formatter->MakeObjectContext("OperationMethod", - !identifiers().empty())); - - writer->AddObjKey("name"); - writer->Add(nameStr()); - - if (formatter->outputId()) { - formatID(formatter); - } -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -bool OperationMethod::_isEquivalentTo( - const util::IComparable *other, util::IComparable::Criterion criterion, - const io::DatabaseContextPtr &dbContext) const { - auto otherOM = dynamic_cast(other); - if (otherOM == nullptr || - !IdentifiedObject::_isEquivalentTo(other, criterion, dbContext)) { - return false; - } - // TODO test formula and formulaCitation - const auto ¶ms = parameters(); - const auto &otherParams = otherOM->parameters(); - const auto paramsSize = params.size(); - if (paramsSize != otherParams.size()) { - return false; - } - if (criterion == util::IComparable::Criterion::STRICT) { - for (size_t i = 0; i < paramsSize; i++) { - if (!params[i]->_isEquivalentTo(otherParams[i].get(), criterion, - dbContext)) { - return false; - } - } - } else { - std::vector candidateIndices(paramsSize, true); - for (size_t i = 0; i < paramsSize; i++) { - bool found = false; - for (size_t j = 0; j < paramsSize; j++) { - if (candidateIndices[j] && - params[i]->_isEquivalentTo(otherParams[j].get(), criterion, - dbContext)) { - candidateIndices[j] = false; - found = true; - break; - } - } - if (!found) { - return false; - } - } - } - return true; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct GeneralParameterValue::Private {}; -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -GeneralParameterValue::GeneralParameterValue() : d(nullptr) {} - -// --------------------------------------------------------------------------- - -GeneralParameterValue::GeneralParameterValue(const GeneralParameterValue &) - : d(nullptr) {} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -GeneralParameterValue::~GeneralParameterValue() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct OperationParameterValue::Private { - OperationParameterNNPtr parameter; - ParameterValueNNPtr parameterValue; - - Private(const OperationParameterNNPtr ¶meterIn, - const ParameterValueNNPtr &valueIn) - : parameter(parameterIn), parameterValue(valueIn) {} -}; -//! @endcond - -// --------------------------------------------------------------------------- - -OperationParameterValue::OperationParameterValue( - const OperationParameterValue &other) - : GeneralParameterValue(other), - d(internal::make_unique(*other.d)) {} - -// --------------------------------------------------------------------------- - -OperationParameterValue::OperationParameterValue( - const OperationParameterNNPtr ¶meterIn, - const ParameterValueNNPtr &valueIn) - : GeneralParameterValue(), - d(internal::make_unique(parameterIn, valueIn)) {} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a OperationParameterValue. - * - * @param parameterIn Parameter (definition). - * @param valueIn Parameter value. - * @return a new OperationParameterValue. - */ -OperationParameterValueNNPtr -OperationParameterValue::create(const OperationParameterNNPtr ¶meterIn, - const ParameterValueNNPtr &valueIn) { - return OperationParameterValue::nn_make_shared( - parameterIn, valueIn); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -OperationParameterValue::~OperationParameterValue() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Return the parameter (definition) - * - * @return the parameter (definition). - */ -const OperationParameterNNPtr & -OperationParameterValue::parameter() PROJ_PURE_DEFN { - return d->parameter; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the parameter value. - * - * @return the parameter value. - */ -const ParameterValueNNPtr & -OperationParameterValue::parameterValue() PROJ_PURE_DEFN { - return d->parameterValue; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -void OperationParameterValue::_exportToWKT( - // cppcheck-suppress passedByValue - io::WKTFormatter *formatter) const { - _exportToWKT(formatter, nullptr); -} - -void OperationParameterValue::_exportToWKT(io::WKTFormatter *formatter, - const MethodMapping *mapping) const { - const ParamMapping *paramMapping = - mapping ? getMapping(mapping, d->parameter) : nullptr; - if (paramMapping && paramMapping->wkt1_name == nullptr) { - return; - } - const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; - if (isWKT2 && parameterValue()->type() == ParameterValue::Type::FILENAME) { - formatter->startNode(io::WKTConstants::PARAMETERFILE, - !parameter()->identifiers().empty()); - } else { - formatter->startNode(io::WKTConstants::PARAMETER, - !parameter()->identifiers().empty()); - } - if (paramMapping) { - formatter->addQuotedString(paramMapping->wkt1_name); - } else { - formatter->addQuotedString(parameter()->nameStr()); - } - parameterValue()->_exportToWKT(formatter); - if (formatter->outputId()) { - parameter()->formatID(formatter); - } - formatter->endNode(); -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -void OperationParameterValue::_exportToJSON( - io::JSONFormatter *formatter) const { - auto writer = formatter->writer(); - auto objectContext(formatter->MakeObjectContext( - "ParameterValue", !parameter()->identifiers().empty())); - - writer->AddObjKey("name"); - writer->Add(parameter()->nameStr()); - - const auto &l_value(parameterValue()); - if (l_value->type() == ParameterValue::Type::MEASURE) { - writer->AddObjKey("value"); - writer->Add(l_value->value().value(), 15); - writer->AddObjKey("unit"); - const auto &l_unit(l_value->value().unit()); - if (l_unit == common::UnitOfMeasure::METRE || - l_unit == common::UnitOfMeasure::DEGREE || - l_unit == common::UnitOfMeasure::SCALE_UNITY) { - writer->Add(l_unit.name()); - } else { - l_unit._exportToJSON(formatter); - } - } else if (l_value->type() == ParameterValue::Type::FILENAME) { - writer->AddObjKey("value"); - writer->Add(l_value->valueFile()); - } - - if (formatter->outputId()) { - parameter()->formatID(formatter); - } -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -/** Utility method used on WKT2 import to convert from abridged transformation - * to "normal" transformation parameters. - */ -bool OperationParameterValue::convertFromAbridged( - const std::string ¶mName, double &val, - const common::UnitOfMeasure *&unit, int ¶mEPSGCode) { - if (metadata::Identifier::isEquivalentName( - paramName.c_str(), EPSG_NAME_PARAMETER_X_AXIS_TRANSLATION) || - paramEPSGCode == EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION) { - unit = &common::UnitOfMeasure::METRE; - paramEPSGCode = EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION; - return true; - } else if (metadata::Identifier::isEquivalentName( - paramName.c_str(), EPSG_NAME_PARAMETER_Y_AXIS_TRANSLATION) || - paramEPSGCode == EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION) { - unit = &common::UnitOfMeasure::METRE; - paramEPSGCode = EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION; - return true; - } else if (metadata::Identifier::isEquivalentName( - paramName.c_str(), EPSG_NAME_PARAMETER_Z_AXIS_TRANSLATION) || - paramEPSGCode == EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION) { - unit = &common::UnitOfMeasure::METRE; - paramEPSGCode = EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION; - return true; - } else if (metadata::Identifier::isEquivalentName( - paramName.c_str(), EPSG_NAME_PARAMETER_X_AXIS_ROTATION) || - paramEPSGCode == EPSG_CODE_PARAMETER_X_AXIS_ROTATION) { - unit = &common::UnitOfMeasure::ARC_SECOND; - paramEPSGCode = EPSG_CODE_PARAMETER_X_AXIS_ROTATION; - return true; - } else if (metadata::Identifier::isEquivalentName( - paramName.c_str(), EPSG_NAME_PARAMETER_Y_AXIS_ROTATION) || - paramEPSGCode == EPSG_CODE_PARAMETER_Y_AXIS_ROTATION) { - unit = &common::UnitOfMeasure::ARC_SECOND; - paramEPSGCode = EPSG_CODE_PARAMETER_Y_AXIS_ROTATION; - return true; - - } else if (metadata::Identifier::isEquivalentName( - paramName.c_str(), EPSG_NAME_PARAMETER_Z_AXIS_ROTATION) || - paramEPSGCode == EPSG_CODE_PARAMETER_Z_AXIS_ROTATION) { - unit = &common::UnitOfMeasure::ARC_SECOND; - paramEPSGCode = EPSG_CODE_PARAMETER_Z_AXIS_ROTATION; - return true; - - } else if (metadata::Identifier::isEquivalentName( - paramName.c_str(), EPSG_NAME_PARAMETER_SCALE_DIFFERENCE) || - paramEPSGCode == EPSG_CODE_PARAMETER_SCALE_DIFFERENCE) { - val = (val - 1.0) * 1e6; - unit = &common::UnitOfMeasure::PARTS_PER_MILLION; - paramEPSGCode = EPSG_CODE_PARAMETER_SCALE_DIFFERENCE; - return true; - } - return false; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -bool OperationParameterValue::_isEquivalentTo( - const util::IComparable *other, util::IComparable::Criterion criterion, - const io::DatabaseContextPtr &dbContext) const { - auto otherOPV = dynamic_cast(other); - if (otherOPV == nullptr) { - return false; - } - if (!d->parameter->_isEquivalentTo(otherOPV->d->parameter.get(), criterion, - dbContext)) { - return false; - } - if (criterion == util::IComparable::Criterion::STRICT) { - return d->parameterValue->_isEquivalentTo( - otherOPV->d->parameterValue.get(), criterion); - } - if (d->parameterValue->_isEquivalentTo(otherOPV->d->parameterValue.get(), - criterion, dbContext)) { - return true; - } - if (d->parameter->getEPSGCode() == - EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE || - d->parameter->getEPSGCode() == - EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID) { - if (parameterValue()->type() == ParameterValue::Type::MEASURE && - otherOPV->parameterValue()->type() == - ParameterValue::Type::MEASURE) { - const double a = std::fmod(parameterValue()->value().convertToUnit( - common::UnitOfMeasure::DEGREE) + - 360.0, - 360.0); - const double b = - std::fmod(otherOPV->parameterValue()->value().convertToUnit( - common::UnitOfMeasure::DEGREE) + - 360.0, - 360.0); - return std::fabs(a - b) <= 1e-10 * std::fabs(a); - } - } - return false; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct GeneralOperationParameter::Private {}; -//! @endcond - -// --------------------------------------------------------------------------- - -GeneralOperationParameter::GeneralOperationParameter() : d(nullptr) {} - -// --------------------------------------------------------------------------- - -GeneralOperationParameter::GeneralOperationParameter( - const GeneralOperationParameter &other) - : IdentifiedObject(other), d(nullptr) {} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -GeneralOperationParameter::~GeneralOperationParameter() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct OperationParameter::Private {}; -//! @endcond - -// --------------------------------------------------------------------------- - -OperationParameter::OperationParameter() : d(nullptr) {} - -// --------------------------------------------------------------------------- - -OperationParameter::OperationParameter(const OperationParameter &other) - : GeneralOperationParameter(other), d(nullptr) {} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -OperationParameter::~OperationParameter() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a OperationParameter. - * - * @param properties See \ref general_properties. At minimum the name should be - * defined. - * @return a new OperationParameter. - */ -OperationParameterNNPtr -OperationParameter::create(const util::PropertyMap &properties) { - OperationParameterNNPtr op( - OperationParameter::nn_make_shared()); - op->assignSelf(op); - op->setProperties(properties); - return op; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -bool OperationParameter::_isEquivalentTo( - const util::IComparable *other, util::IComparable::Criterion criterion, - const io::DatabaseContextPtr &dbContext) const { - auto otherOP = dynamic_cast(other); - if (otherOP == nullptr) { - return false; - } - if (criterion == util::IComparable::Criterion::STRICT) { - return IdentifiedObject::_isEquivalentTo(other, criterion, dbContext); - } - if (IdentifiedObject::_isEquivalentTo(other, criterion, dbContext)) { - return true; - } - auto l_epsgCode = getEPSGCode(); - return l_epsgCode != 0 && l_epsgCode == otherOP->getEPSGCode(); -} -//! @endcond - -// --------------------------------------------------------------------------- - -void OperationParameter::_exportToWKT(io::WKTFormatter *) const {} - -// --------------------------------------------------------------------------- - -/** \brief Return the name of a parameter designed by its EPSG code - * @return name, or nullptr if not found - */ -const char *OperationParameter::getNameForEPSGCode(int epsg_code) noexcept { - for (const auto &tuple : paramNameCodes) { - if (tuple.epsg_code == epsg_code) { - return tuple.name; - } - } - return nullptr; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the EPSG code, either directly, or through the name - * @return code, or 0 if not found - */ -int OperationParameter::getEPSGCode() PROJ_PURE_DEFN { - int epsg_code = IdentifiedObject::getEPSGCode(); - if (epsg_code == 0) { - const auto &l_name = nameStr(); - for (const auto &tuple : paramNameCodes) { - if (metadata::Identifier::isEquivalentName(l_name.c_str(), - tuple.name)) { - return tuple.epsg_code; - } - } - if (metadata::Identifier::isEquivalentName(l_name.c_str(), - "Latitude of origin")) { - return EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN; - } - if (metadata::Identifier::isEquivalentName(l_name.c_str(), - "Scale factor")) { - return EPSG_CODE_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN; - } - } - return epsg_code; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct SingleOperation::Private { - std::vector parameterValues_{}; - OperationMethodNNPtr method_; - - explicit Private(const OperationMethodNNPtr &methodIn) - : method_(methodIn) {} -}; -//! @endcond - -// --------------------------------------------------------------------------- - -SingleOperation::SingleOperation(const OperationMethodNNPtr &methodIn) - : d(internal::make_unique(methodIn)) {} - -// --------------------------------------------------------------------------- - -SingleOperation::SingleOperation(const SingleOperation &other) - : -#if !defined(COMPILER_WARNS_ABOUT_ABSTRACT_VBASE_INIT) - CoordinateOperation(other), -#endif - d(internal::make_unique(*other.d)) { -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -SingleOperation::~SingleOperation() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Return the parameter values. - * - * @return the parameter values. - */ -const std::vector & -SingleOperation::parameterValues() PROJ_PURE_DEFN { - return d->parameterValues_; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the operation method associated to the operation. - * - * @return the operation method. - */ -const OperationMethodNNPtr &SingleOperation::method() PROJ_PURE_DEFN { - return d->method_; -} - -// --------------------------------------------------------------------------- - -void SingleOperation::setParameterValues( - const std::vector &values) { - d->parameterValues_ = values; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static const ParameterValuePtr nullParameterValue; -//! @endcond - -/** \brief Return the parameter value corresponding to a parameter name or - * EPSG code - * - * @param paramName the parameter name (or empty, in which case epsg_code - * should be non zero) - * @param epsg_code the parameter EPSG code (possibly zero) - * @return the value, or nullptr if not found. - */ -const ParameterValuePtr & -SingleOperation::parameterValue(const std::string ¶mName, - int epsg_code) const noexcept { - if (epsg_code) { - for (const auto &genOpParamvalue : parameterValues()) { - auto opParamvalue = dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue) { - const auto ¶meter = opParamvalue->parameter(); - if (parameter->getEPSGCode() == epsg_code) { - return opParamvalue->parameterValue(); - } - } - } - } - for (const auto &genOpParamvalue : parameterValues()) { - auto opParamvalue = dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue) { - const auto ¶meter = opParamvalue->parameter(); - if (metadata::Identifier::isEquivalentName( - paramName.c_str(), parameter->nameStr().c_str())) { - return opParamvalue->parameterValue(); - } - } - } - for (const auto &genOpParamvalue : parameterValues()) { - auto opParamvalue = dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue) { - const auto ¶meter = opParamvalue->parameter(); - if (areEquivalentParameters(paramName, parameter->nameStr())) { - return opParamvalue->parameterValue(); - } - } - } - return nullParameterValue; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the parameter value corresponding to a EPSG code - * - * @param epsg_code the parameter EPSG code - * @return the value, or nullptr if not found. - */ -const ParameterValuePtr &SingleOperation::parameterValue(int epsg_code) const - noexcept { - for (const auto &genOpParamvalue : parameterValues()) { - auto opParamvalue = dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue) { - const auto ¶meter = opParamvalue->parameter(); - if (parameter->getEPSGCode() == epsg_code) { - return opParamvalue->parameterValue(); - } - } - } - return nullParameterValue; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static const common::Measure nullMeasure; -//! @endcond - -/** \brief Return the parameter value, as a measure, corresponding to a - * parameter name or EPSG code - * - * @param paramName the parameter name (or empty, in which case epsg_code - * should be non zero) - * @param epsg_code the parameter EPSG code (possibly zero) - * @return the measure, or the empty Measure() object if not found. - */ -const common::Measure & -SingleOperation::parameterValueMeasure(const std::string ¶mName, - int epsg_code) const noexcept { - const auto &val = parameterValue(paramName, epsg_code); - if (val && val->type() == ParameterValue::Type::MEASURE) { - return val->value(); - } - return nullMeasure; -} - -/** \brief Return the parameter value, as a measure, corresponding to a - * EPSG code - * - * @param epsg_code the parameter EPSG code - * @return the measure, or the empty Measure() object if not found. - */ -const common::Measure & -SingleOperation::parameterValueMeasure(int epsg_code) const noexcept { - const auto &val = parameterValue(epsg_code); - if (val && val->type() == ParameterValue::Type::MEASURE) { - return val->value(); - } - return nullMeasure; -} - -//! @cond Doxygen_Suppress - -double SingleOperation::parameterValueNumericAsSI(int epsg_code) const - noexcept { - const auto &val = parameterValue(epsg_code); - if (val && val->type() == ParameterValue::Type::MEASURE) { - return val->value().getSIValue(); - } - return 0.0; -} - -double SingleOperation::parameterValueNumeric( - int epsg_code, const common::UnitOfMeasure &targetUnit) const noexcept { - const auto &val = parameterValue(epsg_code); - if (val && val->type() == ParameterValue::Type::MEASURE) { - return val->value().convertToUnit(targetUnit); - } - return 0.0; -} - -double SingleOperation::parameterValueNumeric( - const char *param_name, const common::UnitOfMeasure &targetUnit) const - noexcept { - const auto &val = parameterValue(param_name, 0); - if (val && val->type() == ParameterValue::Type::MEASURE) { - return val->value().convertToUnit(targetUnit); - } - return 0.0; -} - -//! @endcond -// --------------------------------------------------------------------------- - -/** \brief Instantiate a PROJ-based single operation. - * - * \note The operation might internally be a pipeline chaining several - * operations. - * The use of the SingleOperation modeling here is mostly to be able to get - * the PROJ string as a parameter. - * - * @param properties Properties - * @param PROJString the PROJ string. - * @param sourceCRS source CRS (might be null). - * @param targetCRS target CRS (might be null). - * @param accuracies Vector of positional accuracy (might be empty). - * @return the new instance - */ -SingleOperationNNPtr SingleOperation::createPROJBased( - const util::PropertyMap &properties, const std::string &PROJString, - const crs::CRSPtr &sourceCRS, const crs::CRSPtr &targetCRS, - const std::vector &accuracies) { - return util::nn_static_pointer_cast( - PROJBasedOperation::create(properties, PROJString, sourceCRS, targetCRS, - accuracies)); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static SingleOperationNNPtr createPROJBased( - const util::PropertyMap &properties, - const io::IPROJStringExportableNNPtr &projExportable, - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - const crs::CRSPtr &interpolationCRS, - const std::vector &accuracies, - bool hasBallparkTransformation) { - return util::nn_static_pointer_cast( - PROJBasedOperation::create(properties, projExportable, false, sourceCRS, - targetCRS, interpolationCRS, accuracies, - hasBallparkTransformation)); -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -bool SingleOperation::_isEquivalentTo( - const util::IComparable *other, util::IComparable::Criterion criterion, - const io::DatabaseContextPtr &dbContext) const { - return _isEquivalentTo(other, criterion, dbContext, false); -} - -bool SingleOperation::_isEquivalentTo(const util::IComparable *other, - util::IComparable::Criterion criterion, - const io::DatabaseContextPtr &dbContext, - bool inOtherDirection) const { - - auto otherSO = dynamic_cast(other); - if (otherSO == nullptr || - (criterion == util::IComparable::Criterion::STRICT && - !ObjectUsage::_isEquivalentTo(other, criterion, dbContext))) { - return false; - } - - const int methodEPSGCode = d->method_->getEPSGCode(); - const int otherMethodEPSGCode = otherSO->d->method_->getEPSGCode(); - - bool equivalentMethods = - (criterion == util::IComparable::Criterion::EQUIVALENT && - methodEPSGCode != 0 && methodEPSGCode == otherMethodEPSGCode) || - d->method_->_isEquivalentTo(otherSO->d->method_.get(), criterion, - dbContext); - if (!equivalentMethods && - criterion == util::IComparable::Criterion::EQUIVALENT) { - if ((methodEPSGCode == EPSG_CODE_METHOD_LAMBERT_AZIMUTHAL_EQUAL_AREA && - otherMethodEPSGCode == - EPSG_CODE_METHOD_LAMBERT_AZIMUTHAL_EQUAL_AREA_SPHERICAL) || - (otherMethodEPSGCode == - EPSG_CODE_METHOD_LAMBERT_AZIMUTHAL_EQUAL_AREA && - methodEPSGCode == - EPSG_CODE_METHOD_LAMBERT_AZIMUTHAL_EQUAL_AREA_SPHERICAL) || - (methodEPSGCode == - EPSG_CODE_METHOD_LAMBERT_CYLINDRICAL_EQUAL_AREA && - otherMethodEPSGCode == - EPSG_CODE_METHOD_LAMBERT_CYLINDRICAL_EQUAL_AREA_SPHERICAL) || - (otherMethodEPSGCode == - EPSG_CODE_METHOD_LAMBERT_CYLINDRICAL_EQUAL_AREA && - methodEPSGCode == - EPSG_CODE_METHOD_LAMBERT_CYLINDRICAL_EQUAL_AREA_SPHERICAL) || - (methodEPSGCode == EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL && - otherMethodEPSGCode == - EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL_SPHERICAL) || - (otherMethodEPSGCode == EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL && - methodEPSGCode == - EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL_SPHERICAL)) { - auto geodCRS = - dynamic_cast(sourceCRS().get()); - auto otherGeodCRS = dynamic_cast( - otherSO->sourceCRS().get()); - if (geodCRS && otherGeodCRS && geodCRS->ellipsoid()->isSphere() && - otherGeodCRS->ellipsoid()->isSphere()) { - equivalentMethods = true; - } - } - } - - if (!equivalentMethods) { - if (criterion == util::IComparable::Criterion::EQUIVALENT) { - - const auto isTOWGS84Transf = [](int code) { - return code == - EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOCENTRIC || - code == EPSG_CODE_METHOD_POSITION_VECTOR_GEOCENTRIC || - code == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC || - code == - EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_2D || - code == EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_2D || - code == - EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D || - code == - EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_3D || - code == EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_3D || - code == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D; - }; - - // Translation vs (PV or CF) - // or different PV vs CF convention - if (isTOWGS84Transf(methodEPSGCode) && - isTOWGS84Transf(otherMethodEPSGCode)) { - auto transf = static_cast(this); - auto otherTransf = static_cast(otherSO); - auto params = transf->getTOWGS84Parameters(); - auto otherParams = otherTransf->getTOWGS84Parameters(); - assert(params.size() == 7); - assert(otherParams.size() == 7); - for (size_t i = 0; i < 7; i++) { - if (std::fabs(params[i] - otherParams[i]) > - 1e-10 * std::fabs(params[i])) { - return false; - } - } - return true; - } - - // _1SP methods can sometimes be equivalent to _2SP ones - // Check it by using convertToOtherMethod() - if (methodEPSGCode == - EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP && - otherMethodEPSGCode == - EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP) { - // Convert from 2SP to 1SP as the other direction has more - // degree of liberties. - return otherSO->_isEquivalentTo(this, criterion, dbContext); - } else if ((methodEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_A && - otherMethodEPSGCode == - EPSG_CODE_METHOD_MERCATOR_VARIANT_B) || - (methodEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_B && - otherMethodEPSGCode == - EPSG_CODE_METHOD_MERCATOR_VARIANT_A) || - (methodEPSGCode == - EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP && - otherMethodEPSGCode == - EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP)) { - auto conv = dynamic_cast(this); - if (conv) { - auto eqConv = - conv->convertToOtherMethod(otherMethodEPSGCode); - if (eqConv) { - return eqConv->_isEquivalentTo(other, criterion, - dbContext); - } - } - } - } - - return false; - } - - const auto &values = d->parameterValues_; - const auto &otherValues = otherSO->d->parameterValues_; - const auto valuesSize = values.size(); - const auto otherValuesSize = otherValues.size(); - if (criterion == util::IComparable::Criterion::STRICT) { - if (valuesSize != otherValuesSize) { - return false; - } - for (size_t i = 0; i < valuesSize; i++) { - if (!values[i]->_isEquivalentTo(otherValues[i].get(), criterion, - dbContext)) { - return false; - } - } - return true; - } - - std::vector candidateIndices(otherValuesSize, true); - bool equivalent = true; - bool foundMissingArgs = valuesSize != otherValuesSize; - - for (size_t i = 0; equivalent && i < valuesSize; i++) { - auto opParamvalue = - dynamic_cast(values[i].get()); - if (!opParamvalue) - return false; - - equivalent = false; - bool sameNameDifferentValue = false; - for (size_t j = 0; j < otherValuesSize; j++) { - if (candidateIndices[j] && - values[i]->_isEquivalentTo(otherValues[j].get(), criterion, - dbContext)) { - candidateIndices[j] = false; - equivalent = true; - break; - } else if (candidateIndices[j]) { - auto otherOpParamvalue = - dynamic_cast( - otherValues[j].get()); - if (!otherOpParamvalue) - return false; - sameNameDifferentValue = - opParamvalue->parameter()->_isEquivalentTo( - otherOpParamvalue->parameter().get(), criterion, - dbContext); - if (sameNameDifferentValue) { - candidateIndices[j] = false; - break; - } - } - } - - if (!equivalent && - methodEPSGCode == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP) { - // For LCC_2SP, the standard parallels can be switched and - // this will result in the same result. - const int paramEPSGCode = opParamvalue->parameter()->getEPSGCode(); - if (paramEPSGCode == - EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL || - paramEPSGCode == - EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL) { - auto value_1st = parameterValue( - EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL); - auto value_2nd = parameterValue( - EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL); - if (value_1st && value_2nd) { - equivalent = - value_1st->_isEquivalentTo( - otherSO - ->parameterValue( - EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL) - .get(), - criterion, dbContext) && - value_2nd->_isEquivalentTo( - otherSO - ->parameterValue( - EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL) - .get(), - criterion, dbContext); - } - } - } - - if (equivalent) { - continue; - } - - if (sameNameDifferentValue) { - break; - } - - // If there are parameters in this method not found in the other one, - // check that they are set to a default neutral value, that is 1 - // for scale, and 0 otherwise. - foundMissingArgs = true; - const auto &value = opParamvalue->parameterValue(); - if (value->type() != ParameterValue::Type::MEASURE) { - break; - } - if (value->value().unit().type() == - common::UnitOfMeasure::Type::SCALE) { - equivalent = value->value().getSIValue() == 1.0; - } else { - equivalent = value->value().getSIValue() == 0.0; - } - } - - // In the case the arguments don't perfectly match, try the reverse - // check. - if (equivalent && foundMissingArgs && !inOtherDirection) { - return otherSO->_isEquivalentTo(this, criterion, dbContext, true); - } - - // Equivalent formulations of 2SP can have different parameters - // Then convert to 1SP and compare. - if (!equivalent && - methodEPSGCode == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP) { - auto conv = dynamic_cast(this); - auto otherConv = dynamic_cast(other); - if (conv && otherConv) { - auto thisAs1SP = conv->convertToOtherMethod( - EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP); - auto otherAs1SP = otherConv->convertToOtherMethod( - EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP); - if (thisAs1SP && otherAs1SP) { - equivalent = thisAs1SP->_isEquivalentTo(otherAs1SP.get(), - criterion, dbContext); - } - } - } - return equivalent; -} -//! @endcond - -// --------------------------------------------------------------------------- - -std::set -SingleOperation::gridsNeeded(const io::DatabaseContextPtr &databaseContext, - bool considerKnownGridsAsAvailable) const { - std::set res; - for (const auto &genOpParamvalue : parameterValues()) { - auto opParamvalue = dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue) { - const auto &value = opParamvalue->parameterValue(); - if (value->type() == ParameterValue::Type::FILENAME) { - const auto gridNames = split(value->valueFile(), ","); - for (const auto &gridName : gridNames) { - GridDescription desc; - desc.shortName = gridName; - if (databaseContext) { - databaseContext->lookForGridInfo( - desc.shortName, considerKnownGridsAsAvailable, - desc.fullName, desc.packageName, desc.url, - desc.directDownload, desc.openLicense, - desc.available); - } - res.insert(desc); - } - } - } - } - return res; -} - -// --------------------------------------------------------------------------- - -/** \brief Validate the parameters used by a coordinate operation. - * - * Return whether the method is known or not, or a list of missing or extra - * parameters for the operations recognized by this implementation. - */ -std::list SingleOperation::validateParameters() const { - std::list res; - - const auto &l_method = method(); - const auto &methodName = l_method->nameStr(); - const MethodMapping *methodMapping = nullptr; - const auto methodEPSGCode = l_method->getEPSGCode(); - for (const auto &mapping : projectionMethodMappings) { - if (metadata::Identifier::isEquivalentName(mapping.wkt2_name, - methodName.c_str()) || - (methodEPSGCode != 0 && methodEPSGCode == mapping.epsg_code)) { - methodMapping = &mapping; - } - } - if (methodMapping == nullptr) { - for (const auto &mapping : otherMethodMappings) { - if (metadata::Identifier::isEquivalentName(mapping.wkt2_name, - methodName.c_str()) || - (methodEPSGCode != 0 && methodEPSGCode == mapping.epsg_code)) { - methodMapping = &mapping; - } - } - } - if (!methodMapping) { - res.emplace_back("Unknown method " + methodName); - return res; - } - if (methodMapping->wkt2_name != methodName) { - if (metadata::Identifier::isEquivalentName(methodMapping->wkt2_name, - methodName.c_str())) { - std::string msg("Method name "); - msg += methodName; - msg += " is equivalent to official "; - msg += methodMapping->wkt2_name; - msg += " but not strictly equal"; - res.emplace_back(msg); - } else { - std::string msg("Method name "); - msg += methodName; - msg += ", matched to "; - msg += methodMapping->wkt2_name; - msg += ", through its EPSG code has not an equivalent name"; - res.emplace_back(msg); - } - } - if (methodEPSGCode != 0 && methodEPSGCode != methodMapping->epsg_code) { - std::string msg("Method of EPSG code "); - msg += toString(methodEPSGCode); - msg += " does not match official code ("; - msg += toString(methodMapping->epsg_code); - msg += ')'; - res.emplace_back(msg); - } - - // Check if expected parameters are found - for (int i = 0; - methodMapping->params && methodMapping->params[i] != nullptr; ++i) { - const auto *paramMapping = methodMapping->params[i]; - - const OperationParameterValue *opv = nullptr; - for (const auto &genOpParamvalue : parameterValues()) { - auto opParamvalue = dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue) { - const auto ¶meter = opParamvalue->parameter(); - if ((paramMapping->epsg_code != 0 && - parameter->getEPSGCode() == paramMapping->epsg_code) || - ci_equal(parameter->nameStr(), paramMapping->wkt2_name)) { - opv = opParamvalue; - break; - } - } - } - - if (!opv) { - if ((methodEPSGCode == EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL || - methodEPSGCode == - EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL_SPHERICAL) && - paramMapping == ¶mLatitudeNatOrigin) { - // extension of EPSG used by GDAL/PROJ, so we should not - // warn on its absence. - continue; - } - std::string msg("Cannot find expected parameter "); - msg += paramMapping->wkt2_name; - res.emplace_back(msg); - continue; - } - const auto ¶meter = opv->parameter(); - if (paramMapping->wkt2_name != parameter->nameStr()) { - if (ci_equal(parameter->nameStr(), paramMapping->wkt2_name)) { - std::string msg("Parameter name "); - msg += parameter->nameStr(); - msg += " is equivalent to official "; - msg += paramMapping->wkt2_name; - msg += " but not strictly equal"; - res.emplace_back(msg); - } else { - std::string msg("Parameter name "); - msg += parameter->nameStr(); - msg += ", matched to "; - msg += paramMapping->wkt2_name; - msg += ", through its EPSG code has not an equivalent name"; - res.emplace_back(msg); - } - } - const auto paramEPSGCode = parameter->getEPSGCode(); - if (paramEPSGCode != 0 && paramEPSGCode != paramMapping->epsg_code) { - std::string msg("Parameter of EPSG code "); - msg += toString(paramEPSGCode); - msg += " does not match official code ("; - msg += toString(paramMapping->epsg_code); - msg += ')'; - res.emplace_back(msg); - } - } - - // Check if there are extra parameters - for (const auto &genOpParamvalue : parameterValues()) { - auto opParamvalue = dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue) { - const auto ¶meter = opParamvalue->parameter(); - if (!getMapping(methodMapping, parameter)) { - std::string msg("Parameter "); - msg += parameter->nameStr(); - msg += " found but not expected for this method"; - res.emplace_back(msg); - } - } - } - - return res; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct ParameterValue::Private { - ParameterValue::Type type_{ParameterValue::Type::STRING}; - std::unique_ptr measure_{}; - std::unique_ptr stringValue_{}; - int integerValue_{}; - bool booleanValue_{}; - - explicit Private(const common::Measure &valueIn) - : type_(ParameterValue::Type::MEASURE), - measure_(internal::make_unique(valueIn)) {} - - Private(const std::string &stringValueIn, ParameterValue::Type typeIn) - : type_(typeIn), - stringValue_(internal::make_unique(stringValueIn)) {} - - explicit Private(int integerValueIn) - : type_(ParameterValue::Type::INTEGER), integerValue_(integerValueIn) {} - - explicit Private(bool booleanValueIn) - : type_(ParameterValue::Type::BOOLEAN), booleanValue_(booleanValueIn) {} -}; -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -ParameterValue::~ParameterValue() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -ParameterValue::ParameterValue(const common::Measure &measureIn) - : d(internal::make_unique(measureIn)) {} - -// --------------------------------------------------------------------------- - -ParameterValue::ParameterValue(const std::string &stringValueIn, - ParameterValue::Type typeIn) - : d(internal::make_unique(stringValueIn, typeIn)) {} - -// --------------------------------------------------------------------------- - -ParameterValue::ParameterValue(int integerValueIn) - : d(internal::make_unique(integerValueIn)) {} - -// --------------------------------------------------------------------------- - -ParameterValue::ParameterValue(bool booleanValueIn) - : d(internal::make_unique(booleanValueIn)) {} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a ParameterValue from a Measure (i.e. a value associated - * with a - * unit) - * - * @return a new ParameterValue. - */ -ParameterValueNNPtr ParameterValue::create(const common::Measure &measureIn) { - return ParameterValue::nn_make_shared(measureIn); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a ParameterValue from a string value. - * - * @return a new ParameterValue. - */ -ParameterValueNNPtr ParameterValue::create(const char *stringValueIn) { - return ParameterValue::nn_make_shared( - std::string(stringValueIn), ParameterValue::Type::STRING); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a ParameterValue from a string value. - * - * @return a new ParameterValue. - */ -ParameterValueNNPtr ParameterValue::create(const std::string &stringValueIn) { - return ParameterValue::nn_make_shared( - stringValueIn, ParameterValue::Type::STRING); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a ParameterValue from a filename. - * - * @return a new ParameterValue. - */ -ParameterValueNNPtr -ParameterValue::createFilename(const std::string &stringValueIn) { - return ParameterValue::nn_make_shared( - stringValueIn, ParameterValue::Type::FILENAME); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a ParameterValue from a integer value. - * - * @return a new ParameterValue. - */ -ParameterValueNNPtr ParameterValue::create(int integerValueIn) { - return ParameterValue::nn_make_shared(integerValueIn); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a ParameterValue from a boolean value. - * - * @return a new ParameterValue. - */ -ParameterValueNNPtr ParameterValue::create(bool booleanValueIn) { - return ParameterValue::nn_make_shared(booleanValueIn); -} - -// --------------------------------------------------------------------------- - -/** \brief Returns the type of a parameter value. - * - * @return the type. - */ -const ParameterValue::Type &ParameterValue::type() PROJ_PURE_DEFN { - return d->type_; -} - -// --------------------------------------------------------------------------- - -/** \brief Returns the value as a Measure (assumes type() == Type::MEASURE) - * @return the value as a Measure. - */ -const common::Measure &ParameterValue::value() PROJ_PURE_DEFN { - return *d->measure_; -} - -// --------------------------------------------------------------------------- - -/** \brief Returns the value as a string (assumes type() == Type::STRING) - * @return the value as a string. - */ -const std::string &ParameterValue::stringValue() PROJ_PURE_DEFN { - return *d->stringValue_; -} - -// --------------------------------------------------------------------------- - -/** \brief Returns the value as a filename (assumes type() == Type::FILENAME) - * @return the value as a filename. - */ -const std::string &ParameterValue::valueFile() PROJ_PURE_DEFN { - return *d->stringValue_; -} - -// --------------------------------------------------------------------------- - -/** \brief Returns the value as a integer (assumes type() == Type::INTEGER) - * @return the value as a integer. - */ -int ParameterValue::integerValue() PROJ_PURE_DEFN { return d->integerValue_; } - -// --------------------------------------------------------------------------- - -/** \brief Returns the value as a boolean (assumes type() == Type::BOOLEAN) - * @return the value as a boolean. - */ -bool ParameterValue::booleanValue() PROJ_PURE_DEFN { return d->booleanValue_; } - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -void ParameterValue::_exportToWKT(io::WKTFormatter *formatter) const { - const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; - - const auto &l_type = type(); - if (l_type == Type::MEASURE) { - const auto &l_value = value(); - if (formatter->abridgedTransformation()) { - const auto &unit = l_value.unit(); - const auto &unitType = unit.type(); - if (unitType == common::UnitOfMeasure::Type::LINEAR) { - formatter->add(l_value.getSIValue()); - } else if (unitType == common::UnitOfMeasure::Type::ANGULAR) { - formatter->add( - l_value.convertToUnit(common::UnitOfMeasure::ARC_SECOND)); - } else if (unit == common::UnitOfMeasure::PARTS_PER_MILLION) { - formatter->add(1.0 + l_value.value() * 1e-6); - } else { - formatter->add(l_value.value()); - } - } else { - const auto &unit = l_value.unit(); - if (isWKT2) { - formatter->add(l_value.value()); - } else { - // In WKT1, as we don't output the natural unit, output to the - // registered linear / angular unit. - const auto &unitType = unit.type(); - if (unitType == common::UnitOfMeasure::Type::LINEAR) { - const auto &targetUnit = *(formatter->axisLinearUnit()); - if (targetUnit.conversionToSI() == 0.0) { - throw io::FormattingException( - "cannot convert value to target linear unit"); - } - formatter->add(l_value.convertToUnit(targetUnit)); - } else if (unitType == common::UnitOfMeasure::Type::ANGULAR) { - const auto &targetUnit = *(formatter->axisAngularUnit()); - if (targetUnit.conversionToSI() == 0.0) { - throw io::FormattingException( - "cannot convert value to target angular unit"); - } - formatter->add(l_value.convertToUnit(targetUnit)); - } else { - formatter->add(l_value.getSIValue()); - } - } - if (isWKT2 && unit != common::UnitOfMeasure::NONE) { - if (!formatter - ->primeMeridianOrParameterUnitOmittedIfSameAsAxis() || - (unit != common::UnitOfMeasure::SCALE_UNITY && - unit != *(formatter->axisLinearUnit()) && - unit != *(formatter->axisAngularUnit()))) { - unit._exportToWKT(formatter); - } - } - } - } else if (l_type == Type::STRING || l_type == Type::FILENAME) { - formatter->addQuotedString(stringValue()); - } else if (l_type == Type::INTEGER) { - formatter->add(integerValue()); - } else { - throw io::FormattingException("boolean parameter value not handled"); - } -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -bool ParameterValue::_isEquivalentTo(const util::IComparable *other, - util::IComparable::Criterion criterion, - const io::DatabaseContextPtr &) const { - auto otherPV = dynamic_cast(other); - if (otherPV == nullptr) { - return false; - } - if (type() != otherPV->type()) { - return false; - } - switch (type()) { - case Type::MEASURE: { - return value()._isEquivalentTo(otherPV->value(), criterion, 2e-10); - } - - case Type::STRING: - case Type::FILENAME: { - return stringValue() == otherPV->stringValue(); - } - - case Type::INTEGER: { - return integerValue() == otherPV->integerValue(); - } - - case Type::BOOLEAN: { - return booleanValue() == otherPV->booleanValue(); - } - - default: { - assert(false); - break; - } - } - return true; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct Conversion::Private {}; -//! @endcond - -// --------------------------------------------------------------------------- - -Conversion::Conversion(const OperationMethodNNPtr &methodIn, - const std::vector &values) - : SingleOperation(methodIn), d(nullptr) { - setParameterValues(values); -} - -// --------------------------------------------------------------------------- - -Conversion::Conversion(const Conversion &other) - : CoordinateOperation(other), SingleOperation(other), d(nullptr) {} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -Conversion::~Conversion() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -ConversionNNPtr Conversion::shallowClone() const { - auto conv = Conversion::nn_make_shared(*this); - conv->assignSelf(conv); - conv->setCRSs(this, false); - return conv; -} - -CoordinateOperationNNPtr Conversion::_shallowClone() const { - return util::nn_static_pointer_cast(shallowClone()); -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -ConversionNNPtr -Conversion::alterParametersLinearUnit(const common::UnitOfMeasure &unit, - bool convertToNewUnit) const { - - std::vector newValues; - bool changesDone = false; - for (const auto &genOpParamvalue : parameterValues()) { - bool updated = false; - auto opParamvalue = dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue) { - const auto ¶mValue = opParamvalue->parameterValue(); - if (paramValue->type() == ParameterValue::Type::MEASURE) { - const auto &measure = paramValue->value(); - if (measure.unit().type() == - common::UnitOfMeasure::Type::LINEAR) { - if (!measure.unit()._isEquivalentTo( - unit, util::IComparable::Criterion::EQUIVALENT)) { - const double newValue = - convertToNewUnit ? measure.convertToUnit(unit) - : measure.value(); - newValues.emplace_back(OperationParameterValue::create( - opParamvalue->parameter(), - ParameterValue::create( - common::Measure(newValue, unit)))); - updated = true; - } - } - } - } - if (updated) { - changesDone = true; - } else { - newValues.emplace_back(genOpParamvalue); - } - } - if (changesDone) { - auto conv = create(util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, "unknown"), - method(), newValues); - conv->setCRSs(this, false); - return conv; - } else { - return NN_NO_CHECK( - util::nn_dynamic_pointer_cast(shared_from_this())); - } -} -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a Conversion from a vector of GeneralParameterValue. - * - * @param properties See \ref general_properties. At minimum the name should be - * defined. - * @param methodIn the operation method. - * @param values the values. - * @return a new Conversion. - * @throws InvalidOperation - */ -ConversionNNPtr Conversion::create(const util::PropertyMap &properties, - const OperationMethodNNPtr &methodIn, - const std::vector - &values) // throw InvalidOperation -{ - if (methodIn->parameters().size() != values.size()) { - throw InvalidOperation( - "Inconsistent number of parameters and parameter values"); - } - auto conv = Conversion::nn_make_shared(methodIn, values); - conv->assignSelf(conv); - conv->setProperties(properties); - return conv; -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a Conversion and its OperationMethod - * - * @param propertiesConversion See \ref general_properties of the conversion. - * At minimum the name should be defined. - * @param propertiesOperationMethod See \ref general_properties of the operation - * method. At minimum the name should be defined. - * @param parameters the operation parameters. - * @param values the operation values. Constraint: - * values.size() == parameters.size() - * @return a new Conversion. - * @throws InvalidOperation - */ -ConversionNNPtr Conversion::create( - const util::PropertyMap &propertiesConversion, - const util::PropertyMap &propertiesOperationMethod, - const std::vector ¶meters, - const std::vector &values) // throw InvalidOperation -{ - OperationMethodNNPtr op( - OperationMethod::create(propertiesOperationMethod, parameters)); - - if (parameters.size() != values.size()) { - throw InvalidOperation( - "Inconsistent number of parameters and parameter values"); - } - std::vector generalParameterValues; - generalParameterValues.reserve(values.size()); - for (size_t i = 0; i < values.size(); i++) { - generalParameterValues.push_back( - OperationParameterValue::create(parameters[i], values[i])); - } - return create(propertiesConversion, op, generalParameterValues); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -// --------------------------------------------------------------------------- - -static util::PropertyMap createMapNameEPSGCode(const std::string &name, - int code) { - return util::PropertyMap() - .set(common::IdentifiedObject::NAME_KEY, name) - .set(metadata::Identifier::CODESPACE_KEY, metadata::Identifier::EPSG) - .set(metadata::Identifier::CODE_KEY, code); -} - -// --------------------------------------------------------------------------- - -static util::PropertyMap createMapNameEPSGCode(const char *name, int code) { - return util::PropertyMap() - .set(common::IdentifiedObject::NAME_KEY, name) - .set(metadata::Identifier::CODESPACE_KEY, metadata::Identifier::EPSG) - .set(metadata::Identifier::CODE_KEY, code); -} - -// --------------------------------------------------------------------------- - -static util::PropertyMap createMethodMapNameEPSGCode(int code) { - const char *name = nullptr; - for (const auto &tuple : methodNameCodes) { - if (tuple.epsg_code == code) { - name = tuple.name; - break; - } - } - assert(name); - return createMapNameEPSGCode(name, code); -} - -// --------------------------------------------------------------------------- - -static util::PropertyMap -getUTMConversionProperty(const util::PropertyMap &properties, int zone, - bool north) { - if (!properties.get(common::IdentifiedObject::NAME_KEY)) { - std::string conversionName("UTM zone "); - conversionName += toString(zone); - conversionName += (north ? 'N' : 'S'); - - return createMapNameEPSGCode(conversionName, - (north ? 16000 : 17000) + zone); - } else { - return properties; - } -} - -// --------------------------------------------------------------------------- - -static util::PropertyMap -addDefaultNameIfNeeded(const util::PropertyMap &properties, - const std::string &defaultName) { - if (!properties.get(common::IdentifiedObject::NAME_KEY)) { - return util::PropertyMap(properties) - .set(common::IdentifiedObject::NAME_KEY, defaultName); - } else { - return properties; - } -} - -// --------------------------------------------------------------------------- - -static ConversionNNPtr -createConversion(const util::PropertyMap &properties, - const MethodMapping *mapping, - const std::vector &values) { - - std::vector parameters; - for (int i = 0; mapping->params[i] != nullptr; i++) { - const auto *param = mapping->params[i]; - auto paramProperties = util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, param->wkt2_name); - if (param->epsg_code != 0) { - paramProperties - .set(metadata::Identifier::CODESPACE_KEY, - metadata::Identifier::EPSG) - .set(metadata::Identifier::CODE_KEY, param->epsg_code); - } - auto parameter = OperationParameter::create(paramProperties); - parameters.push_back(parameter); - } - - auto methodProperties = util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, mapping->wkt2_name); - if (mapping->epsg_code != 0) { - methodProperties - .set(metadata::Identifier::CODESPACE_KEY, - metadata::Identifier::EPSG) - .set(metadata::Identifier::CODE_KEY, mapping->epsg_code); - } - return Conversion::create( - addDefaultNameIfNeeded(properties, mapping->wkt2_name), - methodProperties, parameters, values); -} -//! @endcond - -// --------------------------------------------------------------------------- - -ConversionNNPtr -Conversion::create(const util::PropertyMap &properties, int method_epsg_code, - const std::vector &values) { - const MethodMapping *mapping = getMapping(method_epsg_code); - assert(mapping); - return createConversion(properties, mapping, values); -} - -// --------------------------------------------------------------------------- - -ConversionNNPtr -Conversion::create(const util::PropertyMap &properties, - const char *method_wkt2_name, - const std::vector &values) { - const MethodMapping *mapping = getMapping(method_wkt2_name); - assert(mapping); - return createConversion(properties, mapping, values); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -struct VectorOfParameters : public std::vector { - VectorOfParameters() : std::vector() {} - explicit VectorOfParameters( - std::initializer_list list) - : std::vector(list) {} - VectorOfParameters(const VectorOfParameters &) = delete; - - ~VectorOfParameters(); -}; - -// This way, we disable inlining of destruction, and save a lot of space -VectorOfParameters::~VectorOfParameters() = default; - -struct VectorOfValues : public std::vector { - VectorOfValues() : std::vector() {} - explicit VectorOfValues(std::initializer_list list) - : std::vector(list) {} - - explicit VectorOfValues(std::initializer_list list); - VectorOfValues(const VectorOfValues &) = delete; - VectorOfValues(VectorOfValues &&) = default; - - ~VectorOfValues(); -}; - -static std::vector buildParameterValueFromMeasure( - const std::initializer_list &list) { - std::vector res; - for (const auto &v : list) { - res.emplace_back(ParameterValue::create(v)); - } - return res; -} - -VectorOfValues::VectorOfValues(std::initializer_list list) - : std::vector(buildParameterValueFromMeasure(list)) {} - -// This way, we disable inlining of destruction, and save a lot of space -VectorOfValues::~VectorOfValues() = default; - -PROJ_NO_INLINE static VectorOfValues createParams(const common::Measure &m1, - const common::Measure &m2, - const common::Measure &m3) { - return VectorOfValues{ParameterValue::create(m1), - ParameterValue::create(m2), - ParameterValue::create(m3)}; -} - -PROJ_NO_INLINE static VectorOfValues createParams(const common::Measure &m1, - const common::Measure &m2, - const common::Measure &m3, - const common::Measure &m4) { - return VectorOfValues{ - ParameterValue::create(m1), ParameterValue::create(m2), - ParameterValue::create(m3), ParameterValue::create(m4)}; -} - -PROJ_NO_INLINE static VectorOfValues createParams(const common::Measure &m1, - const common::Measure &m2, - const common::Measure &m3, - const common::Measure &m4, - const common::Measure &m5) { - return VectorOfValues{ - ParameterValue::create(m1), ParameterValue::create(m2), - ParameterValue::create(m3), ParameterValue::create(m4), - ParameterValue::create(m5), - }; -} - -PROJ_NO_INLINE static VectorOfValues -createParams(const common::Measure &m1, const common::Measure &m2, - const common::Measure &m3, const common::Measure &m4, - const common::Measure &m5, const common::Measure &m6) { - return VectorOfValues{ - ParameterValue::create(m1), ParameterValue::create(m2), - ParameterValue::create(m3), ParameterValue::create(m4), - ParameterValue::create(m5), ParameterValue::create(m6), - }; -} - -PROJ_NO_INLINE static VectorOfValues -createParams(const common::Measure &m1, const common::Measure &m2, - const common::Measure &m3, const common::Measure &m4, - const common::Measure &m5, const common::Measure &m6, - const common::Measure &m7) { - return VectorOfValues{ - ParameterValue::create(m1), ParameterValue::create(m2), - ParameterValue::create(m3), ParameterValue::create(m4), - ParameterValue::create(m5), ParameterValue::create(m6), - ParameterValue::create(m7), - }; -} - -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a [Universal Transverse Mercator] - *(https://proj.org/operations/projections/utm.html) conversion. - * - * UTM is a family of conversions, of EPSG codes from 16001 to 16060 for the - * northern hemisphere, and 17001 to 17060 for the southern hemisphere, - * based on the Transverse Mercator projection method. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param zone UTM zone number between 1 and 60. - * @param north true for UTM northern hemisphere, false for UTM southern - * hemisphere. - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createUTM(const util::PropertyMap &properties, - int zone, bool north) { - return create( - getUTMConversionProperty(properties, zone, north), - EPSG_CODE_METHOD_TRANSVERSE_MERCATOR, - createParams(common::Angle(UTM_LATITUDE_OF_NATURAL_ORIGIN), - common::Angle(zone * 6.0 - 183.0), - common::Scale(UTM_SCALE_FACTOR), - common::Length(UTM_FALSE_EASTING), - common::Length(north ? UTM_NORTH_FALSE_NORTHING - : UTM_SOUTH_FALSE_NORTHING))); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Transverse Mercator] - *(https://proj.org/operations/projections/tmerc.html) projection method. - * - * This method is defined as [EPSG:9807] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9807) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param scale See \ref scale - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createTransverseMercator( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Scale &scale, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_TRANSVERSE_MERCATOR, - createParams(centerLat, centerLong, scale, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Gauss Schreiber Transverse - *Mercator] - *(https://proj.org/operations/projections/gstmerc.html) projection method. - * - * This method is also known as Gauss-Laborde Reunion. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param scale See \ref scale - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createGaussSchreiberTransverseMercator( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Scale &scale, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, - PROJ_WKT2_NAME_METHOD_GAUSS_SCHREIBER_TRANSVERSE_MERCATOR, - createParams(centerLat, centerLong, scale, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Transverse Mercator South - *Orientated] - *(https://proj.org/operations/projections/tmerc.html) projection method. - * - * This method is defined as [EPSG:9808] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9808) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param scale See \ref scale - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createTransverseMercatorSouthOriented( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Scale &scale, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, - EPSG_CODE_METHOD_TRANSVERSE_MERCATOR_SOUTH_ORIENTATED, - createParams(centerLat, centerLong, scale, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Two Point Equidistant] - *(https://proj.org/operations/projections/tpeqd.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeFirstPoint Latitude of first point. - * @param longitudeFirstPoint Longitude of first point. - * @param latitudeSecondPoint Latitude of second point. - * @param longitudeSeconPoint Longitude of second point. - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr -Conversion::createTwoPointEquidistant(const util::PropertyMap &properties, - const common::Angle &latitudeFirstPoint, - const common::Angle &longitudeFirstPoint, - const common::Angle &latitudeSecondPoint, - const common::Angle &longitudeSeconPoint, - const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_TWO_POINT_EQUIDISTANT, - createParams(latitudeFirstPoint, longitudeFirstPoint, - latitudeSecondPoint, longitudeSeconPoint, - falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the Tunisia Mapping Grid projection - * method. - * - * This method is defined as [EPSG:9816] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9816) - * - * \note There is currently no implementation of the method formulas in PROJ. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createTunisiaMappingGrid( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create( - properties, EPSG_CODE_METHOD_TUNISIA_MAPPING_GRID, - createParams(centerLat, centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Albers Conic Equal Area] - *(https://proj.org/operations/projections/aea.html) projection method. - * - * This method is defined as [EPSG:9822] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9822) - * - * @note the order of arguments is conformant with the corresponding EPSG - * mode and different than OGRSpatialReference::setACEA() of GDAL <= 2.3 - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeFalseOrigin See \ref latitude_false_origin - * @param longitudeFalseOrigin See \ref longitude_false_origin - * @param latitudeFirstParallel See \ref latitude_first_std_parallel - * @param latitudeSecondParallel See \ref latitude_second_std_parallel - * @param eastingFalseOrigin See \ref easting_false_origin - * @param northingFalseOrigin See \ref northing_false_origin - * @return a new Conversion. - */ -ConversionNNPtr -Conversion::createAlbersEqualArea(const util::PropertyMap &properties, - const common::Angle &latitudeFalseOrigin, - const common::Angle &longitudeFalseOrigin, - const common::Angle &latitudeFirstParallel, - const common::Angle &latitudeSecondParallel, - const common::Length &eastingFalseOrigin, - const common::Length &northingFalseOrigin) { - return create(properties, EPSG_CODE_METHOD_ALBERS_EQUAL_AREA, - createParams(latitudeFalseOrigin, longitudeFalseOrigin, - latitudeFirstParallel, latitudeSecondParallel, - eastingFalseOrigin, northingFalseOrigin)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Lambert Conic Conformal 1SP] - *(https://proj.org/operations/projections/lcc.html) projection method. - * - * This method is defined as [EPSG:9801] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9801) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param scale See \ref scale - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createLambertConicConformal_1SP( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Scale &scale, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP, - createParams(centerLat, centerLong, scale, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Lambert Conic Conformal (2SP)] - *(https://proj.org/operations/projections/lcc.html) projection method. - * - * This method is defined as [EPSG:9802] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9802) - * - * @note the order of arguments is conformant with the corresponding EPSG - * mode and different than OGRSpatialReference::setLCC() of GDAL <= 2.3 - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeFalseOrigin See \ref latitude_false_origin - * @param longitudeFalseOrigin See \ref longitude_false_origin - * @param latitudeFirstParallel See \ref latitude_first_std_parallel - * @param latitudeSecondParallel See \ref latitude_second_std_parallel - * @param eastingFalseOrigin See \ref easting_false_origin - * @param northingFalseOrigin See \ref northing_false_origin - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createLambertConicConformal_2SP( - const util::PropertyMap &properties, - const common::Angle &latitudeFalseOrigin, - const common::Angle &longitudeFalseOrigin, - const common::Angle &latitudeFirstParallel, - const common::Angle &latitudeSecondParallel, - const common::Length &eastingFalseOrigin, - const common::Length &northingFalseOrigin) { - return create(properties, EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP, - createParams(latitudeFalseOrigin, longitudeFalseOrigin, - latitudeFirstParallel, latitudeSecondParallel, - eastingFalseOrigin, northingFalseOrigin)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Lambert Conic Conformal (2SP - *Michigan)] - *(https://proj.org/operations/projections/lcc.html) projection method. - * - * This method is defined as [EPSG:1051] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1051) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeFalseOrigin See \ref latitude_false_origin - * @param longitudeFalseOrigin See \ref longitude_false_origin - * @param latitudeFirstParallel See \ref latitude_first_std_parallel - * @param latitudeSecondParallel See \ref latitude_second_std_parallel - * @param eastingFalseOrigin See \ref easting_false_origin - * @param northingFalseOrigin See \ref northing_false_origin - * @param ellipsoidScalingFactor Ellipsoid scaling factor. - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createLambertConicConformal_2SP_Michigan( - const util::PropertyMap &properties, - const common::Angle &latitudeFalseOrigin, - const common::Angle &longitudeFalseOrigin, - const common::Angle &latitudeFirstParallel, - const common::Angle &latitudeSecondParallel, - const common::Length &eastingFalseOrigin, - const common::Length &northingFalseOrigin, - const common::Scale &ellipsoidScalingFactor) { - return create(properties, - EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP_MICHIGAN, - createParams(latitudeFalseOrigin, longitudeFalseOrigin, - latitudeFirstParallel, latitudeSecondParallel, - eastingFalseOrigin, northingFalseOrigin, - ellipsoidScalingFactor)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Lambert Conic Conformal (2SP - *Belgium)] - *(https://proj.org/operations/projections/lcc.html) projection method. - * - * This method is defined as [EPSG:9803] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9803) - * - * \warning The formulas used currently in PROJ are, incorrectly, the ones of - * the regular LCC_2SP method. - * - * @note the order of arguments is conformant with the corresponding EPSG - * mode and different than OGRSpatialReference::setLCCB() of GDAL <= 2.3 - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeFalseOrigin See \ref latitude_false_origin - * @param longitudeFalseOrigin See \ref longitude_false_origin - * @param latitudeFirstParallel See \ref latitude_first_std_parallel - * @param latitudeSecondParallel See \ref latitude_second_std_parallel - * @param eastingFalseOrigin See \ref easting_false_origin - * @param northingFalseOrigin See \ref northing_false_origin - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createLambertConicConformal_2SP_Belgium( - const util::PropertyMap &properties, - const common::Angle &latitudeFalseOrigin, - const common::Angle &longitudeFalseOrigin, - const common::Angle &latitudeFirstParallel, - const common::Angle &latitudeSecondParallel, - const common::Length &eastingFalseOrigin, - const common::Length &northingFalseOrigin) { - - return create(properties, - EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP_BELGIUM, - createParams(latitudeFalseOrigin, longitudeFalseOrigin, - latitudeFirstParallel, latitudeSecondParallel, - eastingFalseOrigin, northingFalseOrigin)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Modified Azimuthal - *Equidistant] - *(https://proj.org/operations/projections/aeqd.html) projection method. - * - * This method is defined as [EPSG:9832] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9832) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeNatOrigin See \ref center_latitude - * @param longitudeNatOrigin See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createAzimuthalEquidistant( - const util::PropertyMap &properties, const common::Angle &latitudeNatOrigin, - const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_MODIFIED_AZIMUTHAL_EQUIDISTANT, - createParams(latitudeNatOrigin, longitudeNatOrigin, - falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Guam Projection] - *(https://proj.org/operations/projections/aeqd.html) projection method. - * - * This method is defined as [EPSG:9831] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9831) - * - * @param properties See \ref general_properties of the conversion. If the name - *is - * not provided, it is automatically set. - * @param latitudeNatOrigin See \ref center_latitude - * @param longitudeNatOrigin See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createGuamProjection( - const util::PropertyMap &properties, const common::Angle &latitudeNatOrigin, - const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_GUAM_PROJECTION, - createParams(latitudeNatOrigin, longitudeNatOrigin, - falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Bonne] - *(https://proj.org/operations/projections/bonne.html) projection method. - * - * This method is defined as [EPSG:9827] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9827) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeNatOrigin See \ref center_latitude . PROJ calls its the - * standard parallel 1. - * @param longitudeNatOrigin See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createBonne(const util::PropertyMap &properties, - const common::Angle &latitudeNatOrigin, - const common::Angle &longitudeNatOrigin, - const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_BONNE, - createParams(latitudeNatOrigin, longitudeNatOrigin, - falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Lambert Cylindrical Equal Area - *(Spherical)] - *(https://proj.org/operations/projections/cea.html) projection method. - * - * This method is defined as [EPSG:9834] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9834) - * - * \warning The PROJ cea computation code would select the ellipsoidal form if - * a non-spherical ellipsoid is used for the base GeographicCRS. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeFirstParallel See \ref latitude_first_std_parallel. - * @param longitudeNatOrigin See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createLambertCylindricalEqualAreaSpherical( - const util::PropertyMap &properties, - const common::Angle &latitudeFirstParallel, - const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, - EPSG_CODE_METHOD_LAMBERT_CYLINDRICAL_EQUAL_AREA_SPHERICAL, - createParams(latitudeFirstParallel, longitudeNatOrigin, - falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Lambert Cylindrical Equal Area - *(ellipsoidal form)] - *(https://proj.org/operations/projections/cea.html) projection method. - * - * This method is defined as [EPSG:9835] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9835) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeFirstParallel See \ref latitude_first_std_parallel. - * @param longitudeNatOrigin See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createLambertCylindricalEqualArea( - const util::PropertyMap &properties, - const common::Angle &latitudeFirstParallel, - const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_LAMBERT_CYLINDRICAL_EQUAL_AREA, - createParams(latitudeFirstParallel, longitudeNatOrigin, - falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Cassini-Soldner] - * (https://proj.org/operations/projections/cass.html) projection method. - * - * This method is defined as [EPSG:9806] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9806) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createCassiniSoldner( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create( - properties, EPSG_CODE_METHOD_CASSINI_SOLDNER, - createParams(centerLat, centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Equidistant Conic] - *(https://proj.org/operations/projections/eqdc.html) projection method. - * - * There is no equivalent in EPSG. - * - * @note Although not found in EPSG, the order of arguments is conformant with - * the "spirit" of EPSG and different than OGRSpatialReference::setEC() of GDAL - *<= 2.3 * @param properties See \ref general_properties of the conversion. - *If the name - * is not provided, it is automatically set. - * - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param latitudeFirstParallel See \ref latitude_first_std_parallel - * @param latitudeSecondParallel See \ref latitude_second_std_parallel - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createEquidistantConic( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Angle &latitudeFirstParallel, - const common::Angle &latitudeSecondParallel, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_EQUIDISTANT_CONIC, - createParams(centerLat, centerLong, latitudeFirstParallel, - latitudeSecondParallel, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Eckert I] - * (https://proj.org/operations/projections/eck1.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createEckertI(const util::PropertyMap &properties, - const common::Angle ¢erLong, - const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_ECKERT_I, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Eckert II] - * (https://proj.org/operations/projections/eck2.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createEckertII( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_ECKERT_II, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Eckert III] - * (https://proj.org/operations/projections/eck3.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createEckertIII( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_ECKERT_III, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Eckert IV] - * (https://proj.org/operations/projections/eck4.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createEckertIV( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_ECKERT_IV, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Eckert V] - * (https://proj.org/operations/projections/eck5.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createEckertV(const util::PropertyMap &properties, - const common::Angle ¢erLong, - const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_ECKERT_V, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Eckert VI] - * (https://proj.org/operations/projections/eck6.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createEckertVI( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_ECKERT_VI, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Equidistant Cylindrical] - *(https://proj.org/operations/projections/eqc.html) projection method. - * - * This is also known as the Equirectangular method, and in the particular case - * where the latitude of first parallel is 0. - * - * This method is defined as [EPSG:1028] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1028) - * - * @note This is the equivalent OGRSpatialReference::SetEquirectangular2( - * 0.0, latitudeFirstParallel, falseEasting, falseNorthing ) of GDAL <= 2.3, - * where the lat_0 / center_latitude parameter is forced to 0. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeFirstParallel See \ref latitude_first_std_parallel. - * @param longitudeNatOrigin See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createEquidistantCylindrical( - const util::PropertyMap &properties, - const common::Angle &latitudeFirstParallel, - const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL, - createParams(latitudeFirstParallel, 0.0, longitudeNatOrigin, - falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Equidistant Cylindrical - *(Spherical)] - *(https://proj.org/operations/projections/eqc.html) projection method. - * - * This is also known as the Equirectangular method, and in the particular case - * where the latitude of first parallel is 0. - * - * This method is defined as [EPSG:1029] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1029) - * - * @note This is the equivalent OGRSpatialReference::SetEquirectangular2( - * 0.0, latitudeFirstParallel, falseEasting, falseNorthing ) of GDAL <= 2.3, - * where the lat_0 / center_latitude parameter is forced to 0. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeFirstParallel See \ref latitude_first_std_parallel. - * @param longitudeNatOrigin See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createEquidistantCylindricalSpherical( - const util::PropertyMap &properties, - const common::Angle &latitudeFirstParallel, - const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, - EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL_SPHERICAL, - createParams(latitudeFirstParallel, 0.0, longitudeNatOrigin, - falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Gall (Stereographic)] - * (https://proj.org/operations/projections/gall.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createGall(const util::PropertyMap &properties, - const common::Angle ¢erLong, - const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_GALL_STEREOGRAPHIC, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Goode Homolosine] - * (https://proj.org/operations/projections/goode.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createGoodeHomolosine( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_GOODE_HOMOLOSINE, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Interrupted Goode Homolosine] - * (https://proj.org/operations/projections/igh.html) projection method. - * - * There is no equivalent in EPSG. - * - * @note OGRSpatialReference::SetIGH() of GDAL <= 2.3 assumes the 3 - * projection - * parameters to be zero and this is the nominal case. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createInterruptedGoodeHomolosine( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, - PROJ_WKT2_NAME_METHOD_INTERRUPTED_GOODE_HOMOLOSINE, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Geostationary Satellite View] - * (https://proj.org/operations/projections/geos.html) projection method, - * with the sweep angle axis of the viewing instrument being x - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param height Height of the view point above the Earth. - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createGeostationarySatelliteSweepX( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &height, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create( - properties, PROJ_WKT2_NAME_METHOD_GEOSTATIONARY_SATELLITE_SWEEP_X, - createParams(centerLong, height, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Geostationary Satellite View] - * (https://proj.org/operations/projections/geos.html) projection method, - * with the sweep angle axis of the viewing instrument being y. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param height Height of the view point above the Earth. - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createGeostationarySatelliteSweepY( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &height, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create( - properties, PROJ_WKT2_NAME_METHOD_GEOSTATIONARY_SATELLITE_SWEEP_Y, - createParams(centerLong, height, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Gnomonic] - *(https://proj.org/operations/projections/gnom.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createGnomonic( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create( - properties, PROJ_WKT2_NAME_METHOD_GNOMONIC, - createParams(centerLat, centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Hotine Oblique Mercator - *(Variant A)] - *(https://proj.org/operations/projections/omerc.html) projection method - * - * This is the variant with the no_uoff parameter, which corresponds to - * GDAL >=2.3 Hotine_Oblique_Mercator projection. - * In this variant, the false grid coordinates are - * defined at the intersection of the initial line and the aposphere (the - * equator on one of the intermediate surfaces inherent in the method), that is - * at the natural origin of the coordinate system). - * - * This method is defined as [EPSG:9812] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9812) - * - * \note In the case where azimuthInitialLine = angleFromRectifiedToSkrewGrid = - *90deg, - * this maps to the [Swiss Oblique Mercator] - *(https://proj.org/operations/projections/somerc.html) formulas. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeProjectionCentre See \ref latitude_projection_centre - * @param longitudeProjectionCentre See \ref longitude_projection_centre - * @param azimuthInitialLine See \ref azimuth_initial_line - * @param angleFromRectifiedToSkrewGrid See - * \ref angle_from_recitfied_to_skrew_grid - * @param scale See \ref scale_factor_initial_line - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createHotineObliqueMercatorVariantA( - const util::PropertyMap &properties, - const common::Angle &latitudeProjectionCentre, - const common::Angle &longitudeProjectionCentre, - const common::Angle &azimuthInitialLine, - const common::Angle &angleFromRectifiedToSkrewGrid, - const common::Scale &scale, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create( - properties, EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_A, - createParams(latitudeProjectionCentre, longitudeProjectionCentre, - azimuthInitialLine, angleFromRectifiedToSkrewGrid, scale, - falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Hotine Oblique Mercator - *(Variant B)] - *(https://proj.org/operations/projections/omerc.html) projection method - * - * This is the variant without the no_uoff parameter, which corresponds to - * GDAL >=2.3 Hotine_Oblique_Mercator_Azimuth_Center projection. - * In this variant, the false grid coordinates are defined at the projection - *centre. - * - * This method is defined as [EPSG:9815] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9815) - * - * \note In the case where azimuthInitialLine = angleFromRectifiedToSkrewGrid = - *90deg, - * this maps to the [Swiss Oblique Mercator] - *(https://proj.org/operations/projections/somerc.html) formulas. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeProjectionCentre See \ref latitude_projection_centre - * @param longitudeProjectionCentre See \ref longitude_projection_centre - * @param azimuthInitialLine See \ref azimuth_initial_line - * @param angleFromRectifiedToSkrewGrid See - * \ref angle_from_recitfied_to_skrew_grid - * @param scale See \ref scale_factor_initial_line - * @param eastingProjectionCentre See \ref easting_projection_centre - * @param northingProjectionCentre See \ref northing_projection_centre - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createHotineObliqueMercatorVariantB( - const util::PropertyMap &properties, - const common::Angle &latitudeProjectionCentre, - const common::Angle &longitudeProjectionCentre, - const common::Angle &azimuthInitialLine, - const common::Angle &angleFromRectifiedToSkrewGrid, - const common::Scale &scale, const common::Length &eastingProjectionCentre, - const common::Length &northingProjectionCentre) { - return create( - properties, EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_B, - createParams(latitudeProjectionCentre, longitudeProjectionCentre, - azimuthInitialLine, angleFromRectifiedToSkrewGrid, scale, - eastingProjectionCentre, northingProjectionCentre)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Hotine Oblique Mercator Two - *Point Natural Origin] - *(https://proj.org/operations/projections/omerc.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeProjectionCentre See \ref latitude_projection_centre - * @param latitudePoint1 Latitude of point 1. - * @param longitudePoint1 Latitude of point 1. - * @param latitudePoint2 Latitude of point 2. - * @param longitudePoint2 Longitude of point 2. - * @param scale See \ref scale_factor_initial_line - * @param eastingProjectionCentre See \ref easting_projection_centre - * @param northingProjectionCentre See \ref northing_projection_centre - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createHotineObliqueMercatorTwoPointNaturalOrigin( - const util::PropertyMap &properties, - const common::Angle &latitudeProjectionCentre, - const common::Angle &latitudePoint1, const common::Angle &longitudePoint1, - const common::Angle &latitudePoint2, const common::Angle &longitudePoint2, - const common::Scale &scale, const common::Length &eastingProjectionCentre, - const common::Length &northingProjectionCentre) { - return create( - properties, - PROJ_WKT2_NAME_METHOD_HOTINE_OBLIQUE_MERCATOR_TWO_POINT_NATURAL_ORIGIN, - { - ParameterValue::create(latitudeProjectionCentre), - ParameterValue::create(latitudePoint1), - ParameterValue::create(longitudePoint1), - ParameterValue::create(latitudePoint2), - ParameterValue::create(longitudePoint2), - ParameterValue::create(scale), - ParameterValue::create(eastingProjectionCentre), - ParameterValue::create(northingProjectionCentre), - }); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Laborde Oblique Mercator] - *(https://proj.org/operations/projections/labrd.html) projection method. - * - * This method is defined as [EPSG:9813] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9813) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeProjectionCentre See \ref latitude_projection_centre - * @param longitudeProjectionCentre See \ref longitude_projection_centre - * @param azimuthInitialLine See \ref azimuth_initial_line - * @param scale See \ref scale_factor_initial_line - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createLabordeObliqueMercator( - const util::PropertyMap &properties, - const common::Angle &latitudeProjectionCentre, - const common::Angle &longitudeProjectionCentre, - const common::Angle &azimuthInitialLine, const common::Scale &scale, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_LABORDE_OBLIQUE_MERCATOR, - createParams(latitudeProjectionCentre, - longitudeProjectionCentre, azimuthInitialLine, - scale, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [International Map of the World - *Polyconic] - *(https://proj.org/operations/projections/imw_p.html) projection method. - * - * There is no equivalent in EPSG. - * - * @note the order of arguments is conformant with the corresponding EPSG - * mode and different than OGRSpatialReference::SetIWMPolyconic() of GDAL <= - *2.3 - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param latitudeFirstParallel See \ref latitude_first_std_parallel - * @param latitudeSecondParallel See \ref latitude_second_std_parallel - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createInternationalMapWorldPolyconic( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Angle &latitudeFirstParallel, - const common::Angle &latitudeSecondParallel, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_INTERNATIONAL_MAP_WORLD_POLYCONIC, - createParams(centerLong, latitudeFirstParallel, - latitudeSecondParallel, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Krovak (north oriented)] - *(https://proj.org/operations/projections/krovak.html) projection method. - * - * This method is defined as [EPSG:1041] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1041) - * - * The coordinates are returned in the "GIS friendly" order: easting, northing. - * This method is similar to createKrovak(), except that the later returns - * projected values as southing, westing, where - * southing(Krovak) = -northing(Krovak_North) and - * westing(Krovak) = -easting(Krovak_North). - * - * @note The current PROJ implementation of Krovak hard-codes - * colatitudeConeAxis = 30deg17'17.30311" - * and latitudePseudoStandardParallel = 78deg30'N, which are the values used for - * the ProjectedCRS S-JTSK (Ferro) / Krovak East North (EPSG:5221). - * It also hard-codes the parameters of the Bessel ellipsoid typically used for - * Krovak. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeProjectionCentre See \ref latitude_projection_centre - * @param longitudeOfOrigin See \ref longitude_of_origin - * @param colatitudeConeAxis See \ref colatitude_cone_axis - * @param latitudePseudoStandardParallel See \ref - *latitude_pseudo_standard_parallel - * @param scaleFactorPseudoStandardParallel See \ref - *scale_factor_pseudo_standard_parallel - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createKrovakNorthOriented( - const util::PropertyMap &properties, - const common::Angle &latitudeProjectionCentre, - const common::Angle &longitudeOfOrigin, - const common::Angle &colatitudeConeAxis, - const common::Angle &latitudePseudoStandardParallel, - const common::Scale &scaleFactorPseudoStandardParallel, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_KROVAK_NORTH_ORIENTED, - createParams(latitudeProjectionCentre, longitudeOfOrigin, - colatitudeConeAxis, - latitudePseudoStandardParallel, - scaleFactorPseudoStandardParallel, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Krovak] - *(https://proj.org/operations/projections/krovak.html) projection method. - * - * This method is defined as [EPSG:9819] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9819) - * - * The coordinates are returned in the historical order: southing, westing - * This method is similar to createKrovakNorthOriented(), except that the later - *returns - * projected values as easting, northing, where - * easting(Krovak_North) = -westing(Krovak) and - * northing(Krovak_North) = -southing(Krovak). - * - * @note The current PROJ implementation of Krovak hard-codes - * colatitudeConeAxis = 30deg17'17.30311" - * and latitudePseudoStandardParallel = 78deg30'N, which are the values used for - * the ProjectedCRS S-JTSK (Ferro) / Krovak East North (EPSG:5221). - * It also hard-codes the parameters of the Bessel ellipsoid typically used for - * Krovak. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeProjectionCentre See \ref latitude_projection_centre - * @param longitudeOfOrigin See \ref longitude_of_origin - * @param colatitudeConeAxis See \ref colatitude_cone_axis - * @param latitudePseudoStandardParallel See \ref - *latitude_pseudo_standard_parallel - * @param scaleFactorPseudoStandardParallel See \ref - *scale_factor_pseudo_standard_parallel - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr -Conversion::createKrovak(const util::PropertyMap &properties, - const common::Angle &latitudeProjectionCentre, - const common::Angle &longitudeOfOrigin, - const common::Angle &colatitudeConeAxis, - const common::Angle &latitudePseudoStandardParallel, - const common::Scale &scaleFactorPseudoStandardParallel, - const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_KROVAK, - createParams(latitudeProjectionCentre, longitudeOfOrigin, - colatitudeConeAxis, - latitudePseudoStandardParallel, - scaleFactorPseudoStandardParallel, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Lambert Azimuthal Equal Area] - *(https://proj.org/operations/projections/laea.html) projection method. - * - * This method is defined as [EPSG:9820] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9820) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeNatOrigin See \ref center_latitude - * @param longitudeNatOrigin See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createLambertAzimuthalEqualArea( - const util::PropertyMap &properties, const common::Angle &latitudeNatOrigin, - const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_LAMBERT_AZIMUTHAL_EQUAL_AREA, - createParams(latitudeNatOrigin, longitudeNatOrigin, - falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Miller Cylindrical] - *(https://proj.org/operations/projections/mill.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createMillerCylindrical( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_MILLER_CYLINDRICAL, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Mercator] - *(https://proj.org/operations/projections/merc.html) projection method. - * - * This is the variant, also known as Mercator (1SP), defined with the scale - * factor. Note that latitude of natural origin (centerLat) is a parameter, - * but unused in the transformation formulas. - * - * This method is defined as [EPSG:9804] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9804) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude . Should be 0. - * @param centerLong See \ref center_longitude - * @param scale See \ref scale - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createMercatorVariantA( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Scale &scale, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_MERCATOR_VARIANT_A, - createParams(centerLat, centerLong, scale, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Mercator] - *(https://proj.org/operations/projections/merc.html) projection method. - * - * This is the variant, also known as Mercator (2SP), defined with the latitude - * of the first standard parallel (the second standard parallel is implicitly - * the opposite value). The latitude of natural origin is fixed to zero. - * - * This method is defined as [EPSG:9805] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9805) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeFirstParallel See \ref latitude_first_std_parallel - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createMercatorVariantB( - const util::PropertyMap &properties, - const common::Angle &latitudeFirstParallel, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_MERCATOR_VARIANT_B, - createParams(latitudeFirstParallel, centerLong, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Popular Visualisation Pseudo - *Mercator] - *(https://proj.org/operations/projections/webmerc.html) projection method. - * - * Also known as WebMercator. Mostly/only used for Projected CRS EPSG:3857 - * (WGS 84 / Pseudo-Mercator) - * - * This method is defined as [EPSG:1024] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1024) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude . Usually 0 - * @param centerLong See \ref center_longitude . Usually 0 - * @param falseEasting See \ref false_easting . Usually 0 - * @param falseNorthing See \ref false_northing . Usually 0 - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createPopularVisualisationPseudoMercator( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create( - properties, EPSG_CODE_METHOD_POPULAR_VISUALISATION_PSEUDO_MERCATOR, - createParams(centerLat, centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Mollweide] - * (https://proj.org/operations/projections/moll.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createMollweide( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_MOLLWEIDE, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [New Zealand Map Grid] - * (https://proj.org/operations/projections/nzmg.html) projection method. - * - * This method is defined as [EPSG:9811] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9811) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createNewZealandMappingGrid( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create( - properties, EPSG_CODE_METHOD_NZMG, - createParams(centerLat, centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Oblique Stereographic - *(Alternative)] - *(https://proj.org/operations/projections/sterea.html) projection method. - * - * This method is defined as [EPSG:9809] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9809) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param scale See \ref scale - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createObliqueStereographic( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Scale &scale, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_OBLIQUE_STEREOGRAPHIC, - createParams(centerLat, centerLong, scale, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Orthographic] - *(https://proj.org/operations/projections/ortho.html) projection method. - * - * This method is defined as [EPSG:9840] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9840) - * - * \note Before PROJ 7.2, only the spherical formulation was implemented. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createOrthographic( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create( - properties, EPSG_CODE_METHOD_ORTHOGRAPHIC, - createParams(centerLat, centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [American Polyconic] - *(https://proj.org/operations/projections/poly.html) projection method. - * - * This method is defined as [EPSG:9818] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9818) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createAmericanPolyconic( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create( - properties, EPSG_CODE_METHOD_AMERICAN_POLYCONIC, - createParams(centerLat, centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Polar Stereographic (Variant - *A)] - *(https://proj.org/operations/projections/stere.html) projection method. - * - * This method is defined as [EPSG:9810] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9810) - * - * This is the variant of polar stereographic defined with a scale factor. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude . Should be 90 deg ou -90 deg. - * @param centerLong See \ref center_longitude - * @param scale See \ref scale - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createPolarStereographicVariantA( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Scale &scale, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_POLAR_STEREOGRAPHIC_VARIANT_A, - createParams(centerLat, centerLong, scale, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Polar Stereographic (Variant - *B)] - *(https://proj.org/operations/projections/stere.html) projection method. - * - * This method is defined as [EPSG:9829] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9829) - * - * This is the variant of polar stereographic defined with a latitude of - * standard parallel. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeStandardParallel See \ref latitude_std_parallel - * @param longitudeOfOrigin See \ref longitude_of_origin - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createPolarStereographicVariantB( - const util::PropertyMap &properties, - const common::Angle &latitudeStandardParallel, - const common::Angle &longitudeOfOrigin, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_POLAR_STEREOGRAPHIC_VARIANT_B, - createParams(latitudeStandardParallel, longitudeOfOrigin, - falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Robinson] - * (https://proj.org/operations/projections/robin.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createRobinson( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_ROBINSON, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Sinusoidal] - * (https://proj.org/operations/projections/sinu.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createSinusoidal( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_SINUSOIDAL, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Stereographic] - *(https://proj.org/operations/projections/stere.html) projection method. - * - * There is no equivalent in EPSG. This method implements the original "Oblique - * Stereographic" method described in "Snyder's Map Projections - A Working - *manual", - * which is different from the "Oblique Stereographic (alternative") method - * implemented in createObliqueStereographic(). - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param scale See \ref scale - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createStereographic( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Scale &scale, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_STEREOGRAPHIC, - createParams(centerLat, centerLong, scale, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Van der Grinten] - * (https://proj.org/operations/projections/vandg.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createVanDerGrinten( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_VAN_DER_GRINTEN, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Wagner I] - * (https://proj.org/operations/projections/wag1.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createWagnerI(const util::PropertyMap &properties, - const common::Angle ¢erLong, - const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_I, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Wagner II] - * (https://proj.org/operations/projections/wag2.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createWagnerII( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_II, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Wagner III] - * (https://proj.org/operations/projections/wag3.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param latitudeTrueScale Latitude of true scale. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createWagnerIII( - const util::PropertyMap &properties, const common::Angle &latitudeTrueScale, - const common::Angle ¢erLong, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_III, - createParams(latitudeTrueScale, centerLong, falseEasting, - falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Wagner IV] - * (https://proj.org/operations/projections/wag4.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createWagnerIV( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_IV, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Wagner V] - * (https://proj.org/operations/projections/wag5.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createWagnerV(const util::PropertyMap &properties, - const common::Angle ¢erLong, - const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_V, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Wagner VI] - * (https://proj.org/operations/projections/wag6.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createWagnerVI( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_VI, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Wagner VII] - * (https://proj.org/operations/projections/wag7.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createWagnerVII( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_VII, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Quadrilateralized Spherical - *Cube] - *(https://proj.org/operations/projections/qsc.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLat See \ref center_latitude - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createQuadrilateralizedSphericalCube( - const util::PropertyMap &properties, const common::Angle ¢erLat, - const common::Angle ¢erLong, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create( - properties, PROJ_WKT2_NAME_METHOD_QUADRILATERALIZED_SPHERICAL_CUBE, - createParams(centerLat, centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Spherical Cross-Track Height] - *(https://proj.org/operations/projections/sch.html) projection method. - * - * There is no equivalent in EPSG. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param pegPointLat Peg point latitude. - * @param pegPointLong Peg point longitude. - * @param pegPointHeading Peg point heading. - * @param pegPointHeight Peg point height. - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createSphericalCrossTrackHeight( - const util::PropertyMap &properties, const common::Angle &pegPointLat, - const common::Angle &pegPointLong, const common::Angle &pegPointHeading, - const common::Length &pegPointHeight) { - return create(properties, - PROJ_WKT2_NAME_METHOD_SPHERICAL_CROSS_TRACK_HEIGHT, - createParams(pegPointLat, pegPointLong, pegPointHeading, - pegPointHeight)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Equal Earth] - * (https://proj.org/operations/projections/eqearth.html) projection method. - * - * This method is defined as [EPSG:1078] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1078) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param centerLong See \ref center_longitude - * @param falseEasting See \ref false_easting - * @param falseNorthing See \ref false_northing - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createEqualEarth( - const util::PropertyMap &properties, const common::Angle ¢erLong, - const common::Length &falseEasting, const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_EQUAL_EARTH, - createParams(centerLong, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the [Vertical Perspective] - * (https://proj.org/operations/projections/nsper.html) projection method. - * - * This method is defined as [EPSG:9838] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9838) - * - * The PROJ implementation of the EPSG Vertical Perspective has the current - * limitations with respect to the method described in EPSG: - *
    - *
  • it is a 2D-only method, ignoring the ellipsoidal height of the point to - * project.
  • - *
  • it has only a spherical development.
  • - *
  • the height of the topocentric origin is ignored, and thus assumed to be - * 0.
  • - *
- * - * For completeness, PROJ adds the falseEasting and falseNorthing parameter, - * which are not described in EPSG. They should usually be set to 0. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param topoOriginLat Latitude of topocentric origin - * @param topoOriginLong Longitude of topocentric origin - * @param topoOriginHeight Ellipsoidal height of topocentric origin. Ignored by - * PROJ (that is assumed to be 0) - * @param viewPointHeight Viewpoint height with respect to the - * topocentric/mapping plane. In the case where topoOriginHeight = 0, this is - * the height above the ellipsoid surface at topoOriginLat, topoOriginLong. - * @param falseEasting See \ref false_easting . (not in EPSG) - * @param falseNorthing See \ref false_northing . (not in EPSG) - * @return a new Conversion. - * - * @since 6.3 - */ -ConversionNNPtr Conversion::createVerticalPerspective( - const util::PropertyMap &properties, const common::Angle &topoOriginLat, - const common::Angle &topoOriginLong, const common::Length &topoOriginHeight, - const common::Length &viewPointHeight, const common::Length &falseEasting, - const common::Length &falseNorthing) { - return create(properties, EPSG_CODE_METHOD_VERTICAL_PERSPECTIVE, - createParams(topoOriginLat, topoOriginLong, topoOriginHeight, - viewPointHeight, falseEasting, falseNorthing)); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the Pole Rotation method, using - * the conventions of the GRIB 1 and GRIB 2 data formats. - * - * Those are mentioned in the Note 2 of - * https://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_doc/grib2_temp3-1.shtml - * - * Several conventions for the pole rotation method exists. - * The parameters provided in this method are remapped to the PROJ ob_tran - * operation with: - *
- * +proj=ob_tran +o_proj=longlat +o_lon_p=-rotationAngle
- *                               +o_lat_p=-southPoleLatInUnrotatedCRS
- *                               +lon_0=southPoleLongInUnrotatedCRS
- * 
- * - * Another implementation of that convention is also in the netcdf-java library: - * https://github.com/Unidata/netcdf-java/blob/3ce72c0cd167609ed8c69152bb4a004d1daa9273/cdm/core/src/main/java/ucar/unidata/geoloc/projection/RotatedLatLon.java - * - * The PROJ implementation of this method assumes a spherical ellipsoid. - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param southPoleLatInUnrotatedCRS Latitude of the point from the unrotated - * CRS, expressed in the unrotated CRS, that will become the south pole of the - * rotated CRS. - * @param southPoleLongInUnrotatedCRS Longitude of the point from the unrotated - * CRS, expressed in the unrotated CRS, that will become the south pole of the - * rotated CRS. - * @param axisRotation The angle of rotation about the new polar - * axis (measured clockwise when looking from the southern to the northern pole) - * of the coordinate system, assuming the new axis to have been obtained by - * first rotating the sphere through southPoleLongInUnrotatedCRS degrees about - * the geographic polar axis and then rotating through - * (90 + southPoleLatInUnrotatedCRS) degrees so that the southern pole moved - * along the (previously rotated) Greenwich meridian. - * @return a new Conversion. - * - * @since 7.0 - */ -ConversionNNPtr Conversion::createPoleRotationGRIBConvention( - const util::PropertyMap &properties, - const common::Angle &southPoleLatInUnrotatedCRS, - const common::Angle &southPoleLongInUnrotatedCRS, - const common::Angle &axisRotation) { - return create(properties, - PROJ_WKT2_NAME_METHOD_POLE_ROTATION_GRIB_CONVENTION, - createParams(southPoleLatInUnrotatedCRS, - southPoleLongInUnrotatedCRS, axisRotation)); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -static OperationParameterNNPtr createOpParamNameEPSGCode(int code) { - const char *name = OperationParameter::getNameForEPSGCode(code); - assert(name); - return OperationParameter::create(createMapNameEPSGCode(name, code)); -} -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the Change of Vertical Unit - * method. - * - * This method is defined as [EPSG:1069] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1069) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param factor Conversion factor - * @return a new Conversion. - */ -ConversionNNPtr -Conversion::createChangeVerticalUnit(const util::PropertyMap &properties, - const common::Scale &factor) { - return create(properties, createMethodMapNameEPSGCode( - EPSG_CODE_METHOD_CHANGE_VERTICAL_UNIT), - VectorOfParameters{ - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR), - }, - VectorOfValues{ - factor, - }); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the Height Depth Reversal - * method. - * - * This method is defined as [EPSG:1068] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1068) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @return a new Conversion. - * @since 6.3 - */ -ConversionNNPtr -Conversion::createHeightDepthReversal(const util::PropertyMap &properties) { - return create(properties, createMethodMapNameEPSGCode( - EPSG_CODE_METHOD_HEIGHT_DEPTH_REVERSAL), - {}, {}); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the Axis order reversal method - * - * This swaps the longitude, latitude axis. - * - * This method is defined as [EPSG:9843] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9843), - * or for 3D as [EPSG:9844] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9844) - * - * @param is3D Whether this should apply on 3D geographicCRS - * @return a new Conversion. - */ -ConversionNNPtr Conversion::createAxisOrderReversal(bool is3D) { - if (is3D) { - return create(createMapNameEPSGCode(AXIS_ORDER_CHANGE_3D_NAME, 15499), - createMethodMapNameEPSGCode( - EPSG_CODE_METHOD_AXIS_ORDER_REVERSAL_3D), - {}, {}); - } else { - return create(createMapNameEPSGCode(AXIS_ORDER_CHANGE_2D_NAME, 15498), - createMethodMapNameEPSGCode( - EPSG_CODE_METHOD_AXIS_ORDER_REVERSAL_2D), - {}, {}); - } -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a conversion based on the Geographic/Geocentric method. - * - * This method is defined as [EPSG:9602] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9602), - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @return a new Conversion. - */ -ConversionNNPtr -Conversion::createGeographicGeocentric(const util::PropertyMap &properties) { - return create(properties, createMethodMapNameEPSGCode( - EPSG_CODE_METHOD_GEOGRAPHIC_GEOCENTRIC), - {}, {}); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -static const char *getCRSQualifierStr(const crs::CRSPtr &crs) { - auto geod = dynamic_cast(crs.get()); - if (geod) { - if (geod->isGeocentric()) { - return " (geocentric)"; - } - auto geog = dynamic_cast(geod); - if (geog) { - if (geog->coordinateSystem()->axisList().size() == 2) { - return " (geog2D)"; - } else { - return " (geog3D)"; - } - } - } - return ""; -} - -// --------------------------------------------------------------------------- - -static std::string buildOpName(const char *opType, const crs::CRSPtr &source, - const crs::CRSPtr &target) { - std::string res(opType); - const auto &srcName = source->nameStr(); - const auto &targetName = target->nameStr(); - const char *srcQualifier = ""; - const char *targetQualifier = ""; - if (srcName == targetName) { - srcQualifier = getCRSQualifierStr(source); - targetQualifier = getCRSQualifierStr(target); - if (strcmp(srcQualifier, targetQualifier) == 0) { - srcQualifier = ""; - targetQualifier = ""; - } - } - res += " from "; - res += srcName; - res += srcQualifier; - res += " to "; - res += targetName; - res += targetQualifier; - return res; -} - -// --------------------------------------------------------------------------- - -ConversionNNPtr -Conversion::createGeographicGeocentric(const crs::CRSNNPtr &sourceCRS, - const crs::CRSNNPtr &targetCRS) { - auto properties = util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, - buildOpName("Conversion", sourceCRS, targetCRS)); - auto conv = createGeographicGeocentric(properties); - conv->setCRSs(sourceCRS, targetCRS, nullptr); - return conv; -} -// --------------------------------------------------------------------------- - -static util::PropertyMap &addDomains(util::PropertyMap &map, - const common::ObjectUsage *obj) { - - auto ar = util::ArrayOfBaseObject::create(); - for (const auto &domain : obj->domains()) { - ar->add(domain); - } - if (!ar->empty()) { - map.set(common::ObjectUsage::OBJECT_DOMAIN_KEY, ar); - } - return map; -} - -// --------------------------------------------------------------------------- - -static void addModifiedIdentifier(util::PropertyMap &map, - const common::IdentifiedObject *obj, - bool inverse, bool derivedFrom) { - // If original operation is AUTH:CODE, then assign INVERSE(AUTH):CODE - // as identifier. - - auto ar = util::ArrayOfBaseObject::create(); - for (const auto &idSrc : obj->identifiers()) { - auto authName = *(idSrc->codeSpace()); - const auto &srcCode = idSrc->code(); - if (derivedFrom) { - authName = concat("DERIVED_FROM(", authName, ")"); - } - if (inverse) { - if (starts_with(authName, "INVERSE(") && authName.back() == ')') { - authName = authName.substr(strlen("INVERSE(")); - authName.resize(authName.size() - 1); - } else { - authName = concat("INVERSE(", authName, ")"); - } - } - auto idsProp = util::PropertyMap().set( - metadata::Identifier::CODESPACE_KEY, authName); - ar->add(metadata::Identifier::create(srcCode, idsProp)); - } - if (!ar->empty()) { - map.set(common::IdentifiedObject::IDENTIFIERS_KEY, ar); - } -} - -// --------------------------------------------------------------------------- - -static util::PropertyMap -createPropertiesForInverse(const OperationMethodNNPtr &method) { - util::PropertyMap map; - - const std::string &forwardName = method->nameStr(); - if (!forwardName.empty()) { - if (starts_with(forwardName, INVERSE_OF)) { - map.set(common::IdentifiedObject::NAME_KEY, - forwardName.substr(INVERSE_OF.size())); - } else { - map.set(common::IdentifiedObject::NAME_KEY, - INVERSE_OF + forwardName); - } - } - - addModifiedIdentifier(map, method.get(), true, false); - - return map; -} - -// --------------------------------------------------------------------------- - -InverseConversion::InverseConversion(const ConversionNNPtr &forward) - : Conversion( - OperationMethod::create(createPropertiesForInverse(forward->method()), - forward->method()->parameters()), - forward->parameterValues()), - InverseCoordinateOperation(forward, true) { - setPropertiesFromForward(); -} - -// --------------------------------------------------------------------------- - -InverseConversion::~InverseConversion() = default; - -// --------------------------------------------------------------------------- - -ConversionNNPtr InverseConversion::inverseAsConversion() const { - return NN_NO_CHECK( - util::nn_dynamic_pointer_cast(forwardOperation_)); -} - -// --------------------------------------------------------------------------- - -CoordinateOperationNNPtr -InverseConversion::create(const ConversionNNPtr &forward) { - auto conv = util::nn_make_shared(forward); - conv->assignSelf(conv); - return conv; -} - -// --------------------------------------------------------------------------- - -CoordinateOperationNNPtr InverseConversion::_shallowClone() const { - auto op = InverseConversion::nn_make_shared( - inverseAsConversion()->shallowClone()); - op->assignSelf(op); - op->setCRSs(this, false); - return util::nn_static_pointer_cast(op); -} - -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -static bool isAxisOrderReversal2D(int methodEPSGCode) { - return methodEPSGCode == EPSG_CODE_METHOD_AXIS_ORDER_REVERSAL_2D; -} - -static bool isAxisOrderReversal3D(int methodEPSGCode) { - return methodEPSGCode == EPSG_CODE_METHOD_AXIS_ORDER_REVERSAL_3D; -} - -bool isAxisOrderReversal(int methodEPSGCode) { - return isAxisOrderReversal2D(methodEPSGCode) || - isAxisOrderReversal3D(methodEPSGCode); -} -//! @endcond - -// --------------------------------------------------------------------------- - -CoordinateOperationNNPtr Conversion::inverse() const { - const int methodEPSGCode = method()->getEPSGCode(); - - if (methodEPSGCode == EPSG_CODE_METHOD_CHANGE_VERTICAL_UNIT) { - const double convFactor = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR); - auto conv = createChangeVerticalUnit( - createPropertiesForInverse(this, false, false), - common::Scale(1.0 / convFactor)); - conv->setCRSs(this, true); - return conv; - } - - const bool l_isAxisOrderReversal2D = isAxisOrderReversal2D(methodEPSGCode); - const bool l_isAxisOrderReversal3D = isAxisOrderReversal3D(methodEPSGCode); - if (l_isAxisOrderReversal2D || l_isAxisOrderReversal3D) { - auto conv = createAxisOrderReversal(l_isAxisOrderReversal3D); - conv->setCRSs(this, true); - return conv; - } - - if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC_GEOCENTRIC) { - - auto conv = createGeographicGeocentric( - createPropertiesForInverse(this, false, false)); - conv->setCRSs(this, true); - return conv; - } - - if (methodEPSGCode == EPSG_CODE_METHOD_HEIGHT_DEPTH_REVERSAL) { - - auto conv = createHeightDepthReversal( - createPropertiesForInverse(this, false, false)); - conv->setCRSs(this, true); - return conv; - } - - return InverseConversion::create(NN_NO_CHECK( - util::nn_dynamic_pointer_cast(shared_from_this()))); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -static double msfn(double phi, double e2) { - const double sinphi = std::sin(phi); - const double cosphi = std::cos(phi); - return pj_msfn(sinphi, cosphi, e2); -} - -// --------------------------------------------------------------------------- - -static double tsfn(double phi, double ec) { - const double sinphi = std::sin(phi); - return pj_tsfn(phi, sinphi, ec); -} - -// --------------------------------------------------------------------------- - -// Function whose zeroes are the sin of the standard parallels of LCC_2SP -static double lcc_1sp_to_2sp_f(double sinphi, double K, double ec, double n) { - const double x = sinphi; - const double ecx = ec * x; - return (1 - x * x) / (1 - ecx * ecx) - - K * K * std::pow((1.0 - x) / (1.0 + x) * - std::pow((1.0 + ecx) / (1.0 - ecx), ec), - n); -} - -// --------------------------------------------------------------------------- - -// Find the sin of the standard parallels of LCC_2SP -static double find_zero_lcc_1sp_to_2sp_f(double sinphi0, bool bNorth, double K, - double ec) { - double a, b; - double f_a; - if (bNorth) { - // Look for zero above phi0 - a = sinphi0; - b = 1.0; // sin(North pole) - f_a = 1.0; // some positive value, but we only care about the sign - } else { - // Look for zero below phi0 - a = -1.0; // sin(South pole) - b = sinphi0; - f_a = -1.0; // minus infinity in fact, but we only care about the sign - } - // We use dichotomy search. lcc_1sp_to_2sp_f() is positive at sinphi_init, - // has a zero in ]-1,sinphi0[ and ]sinphi0,1[ ranges - for (int N = 0; N < 100; N++) { - double c = (a + b) / 2; - double f_c = lcc_1sp_to_2sp_f(c, K, ec, sinphi0); - if (f_c == 0.0 || (b - a) < 1e-18) { - return c; - } - if ((f_c > 0 && f_a > 0) || (f_c < 0 && f_a < 0)) { - a = c; - f_a = f_c; - } else { - b = c; - } - } - return (a + b) / 2; -} - -static inline double DegToRad(double x) { return x / 180.0 * M_PI; } -static inline double RadToDeg(double x) { return x / M_PI * 180.0; } - -//! @endcond - -// --------------------------------------------------------------------------- - -/** - * \brief Return an equivalent projection. - * - * Currently implemented: - *
    - *
  • EPSG_CODE_METHOD_MERCATOR_VARIANT_A (1SP) to - * EPSG_CODE_METHOD_MERCATOR_VARIANT_B (2SP)
  • - *
  • EPSG_CODE_METHOD_MERCATOR_VARIANT_B (2SP) to - * EPSG_CODE_METHOD_MERCATOR_VARIANT_A (1SP)
  • - *
  • EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP to - * EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP
  • - *
  • EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP to - * EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP
  • - *
- * - * @param targetEPSGCode EPSG code of the target method. - * @return new conversion, or nullptr - */ -ConversionPtr Conversion::convertToOtherMethod(int targetEPSGCode) const { - const int current_epsg_code = method()->getEPSGCode(); - if (current_epsg_code == targetEPSGCode) { - return util::nn_dynamic_pointer_cast(shared_from_this()); - } - - auto geogCRS = dynamic_cast(sourceCRS().get()); - if (!geogCRS) { - return nullptr; - } - - const double e2 = geogCRS->ellipsoid()->squaredEccentricity(); - if (e2 < 0) { - return nullptr; - } - - if (current_epsg_code == EPSG_CODE_METHOD_MERCATOR_VARIANT_A && - targetEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_B && - parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN) == 0.0) { - const double k0 = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN); - if (!(k0 > 0 && k0 <= 1.0 + 1e-10)) - return nullptr; - const double dfStdP1Lat = - (k0 >= 1.0) - ? 0.0 - : std::acos(std::sqrt((1.0 - e2) / ((1.0 / (k0 * k0)) - e2))); - auto latitudeFirstParallel = common::Angle( - common::Angle(dfStdP1Lat, common::UnitOfMeasure::RADIAN) - .convertToUnit(common::UnitOfMeasure::DEGREE), - common::UnitOfMeasure::DEGREE); - auto conv = createMercatorVariantB( - util::PropertyMap(), latitudeFirstParallel, - common::Angle(parameterValueMeasure( - EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN)), - common::Length( - parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_EASTING)), - common::Length( - parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_NORTHING))); - conv->setCRSs(this, false); - return conv.as_nullable(); - } - - if (current_epsg_code == EPSG_CODE_METHOD_MERCATOR_VARIANT_B && - targetEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_A) { - const double phi1 = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL); - if (!(std::fabs(phi1) < M_PI / 2)) - return nullptr; - const double k0 = msfn(phi1, e2); - auto conv = createMercatorVariantA( - util::PropertyMap(), - common::Angle(0.0, common::UnitOfMeasure::DEGREE), - common::Angle(parameterValueMeasure( - EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN)), - common::Scale(k0, common::UnitOfMeasure::SCALE_UNITY), - common::Length( - parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_EASTING)), - common::Length( - parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_NORTHING))); - conv->setCRSs(this, false); - return conv.as_nullable(); - } - - if (current_epsg_code == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP && - targetEPSGCode == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP) { - // Notations m0, t0, n, m1, t1, F are those of the EPSG guidance - // "1.3.1.1 Lambert Conic Conformal (2SP)" and - // "1.3.1.2 Lambert Conic Conformal (1SP)" and - // or Snyder pages 106-109 - auto latitudeOfOrigin = common::Angle(parameterValueMeasure( - EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN)); - const double phi0 = latitudeOfOrigin.getSIValue(); - const double k0 = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN); - if (!(std::fabs(phi0) < M_PI / 2)) - return nullptr; - if (!(k0 > 0 && k0 <= 1.0 + 1e-10)) - return nullptr; - const double ec = std::sqrt(e2); - const double m0 = msfn(phi0, e2); - const double t0 = tsfn(phi0, ec); - const double n = sin(phi0); - if (std::fabs(n) < 1e-10) - return nullptr; - if (fabs(k0 - 1.0) <= 1e-10) { - auto conv = createLambertConicConformal_2SP( - util::PropertyMap(), latitudeOfOrigin, - common::Angle(parameterValueMeasure( - EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN)), - latitudeOfOrigin, latitudeOfOrigin, - common::Length( - parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_EASTING)), - common::Length( - parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_NORTHING))); - conv->setCRSs(this, false); - return conv.as_nullable(); - } else { - const double K = k0 * m0 / std::pow(t0, n); - const double phi1 = - std::asin(find_zero_lcc_1sp_to_2sp_f(n, true, K, ec)); - const double phi2 = - std::asin(find_zero_lcc_1sp_to_2sp_f(n, false, K, ec)); - double phi1Deg = RadToDeg(phi1); - double phi2Deg = RadToDeg(phi2); - - // Try to round to hundreth of degree if very close to it - if (std::fabs(phi1Deg * 1000 - std::floor(phi1Deg * 1000 + 0.5)) < - 1e-8) - phi1Deg = floor(phi1Deg * 1000 + 0.5) / 1000; - if (std::fabs(phi2Deg * 1000 - std::floor(phi2Deg * 1000 + 0.5)) < - 1e-8) - phi2Deg = std::floor(phi2Deg * 1000 + 0.5) / 1000; - - // The following improvement is too turn the LCC1SP equivalent of - // EPSG:2154 to the real LCC2SP - // If the computed latitude of origin is close to .0 or .5 degrees - // then check if rounding it to it will get a false northing - // close to an integer - const double FN = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_FALSE_NORTHING); - const double latitudeOfOriginDeg = - latitudeOfOrigin.convertToUnit(common::UnitOfMeasure::DEGREE); - if (std::fabs(latitudeOfOriginDeg * 2 - - std::floor(latitudeOfOriginDeg * 2 + 0.5)) < 0.2) { - const double dfRoundedLatOfOrig = - std::floor(latitudeOfOriginDeg * 2 + 0.5) / 2; - const double m1 = msfn(phi1, e2); - const double t1 = tsfn(phi1, ec); - const double F = m1 / (n * std::pow(t1, n)); - const double a = - geogCRS->ellipsoid()->semiMajorAxis().getSIValue(); - const double tRoundedLatOfOrig = - tsfn(DegToRad(dfRoundedLatOfOrig), ec); - const double FN_correction = - a * F * (std::pow(tRoundedLatOfOrig, n) - std::pow(t0, n)); - const double FN_corrected = FN - FN_correction; - const double FN_corrected_rounded = - std::floor(FN_corrected + 0.5); - if (std::fabs(FN_corrected - FN_corrected_rounded) < 1e-8) { - auto conv = createLambertConicConformal_2SP( - util::PropertyMap(), - common::Angle(dfRoundedLatOfOrig, - common::UnitOfMeasure::DEGREE), - common::Angle(parameterValueMeasure( - EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN)), - common::Angle(phi1Deg, common::UnitOfMeasure::DEGREE), - common::Angle(phi2Deg, common::UnitOfMeasure::DEGREE), - common::Length(parameterValueMeasure( - EPSG_CODE_PARAMETER_FALSE_EASTING)), - common::Length(FN_corrected_rounded)); - conv->setCRSs(this, false); - return conv.as_nullable(); - } - } - - auto conv = createLambertConicConformal_2SP( - util::PropertyMap(), latitudeOfOrigin, - common::Angle(parameterValueMeasure( - EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN)), - common::Angle(phi1Deg, common::UnitOfMeasure::DEGREE), - common::Angle(phi2Deg, common::UnitOfMeasure::DEGREE), - common::Length( - parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_EASTING)), - common::Length(FN)); - conv->setCRSs(this, false); - return conv.as_nullable(); - } - } - - if (current_epsg_code == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP && - targetEPSGCode == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP) { - // Notations m0, t0, m1, t1, m2, t2 n, F are those of the EPSG guidance - // "1.3.1.1 Lambert Conic Conformal (2SP)" and - // "1.3.1.2 Lambert Conic Conformal (1SP)" and - // or Snyder pages 106-109 - const double phiF = - parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_FALSE_ORIGIN) - .getSIValue(); - const double phi1 = - parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL) - .getSIValue(); - const double phi2 = - parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL) - .getSIValue(); - if (!(std::fabs(phiF) < M_PI / 2)) - return nullptr; - if (!(std::fabs(phi1) < M_PI / 2)) - return nullptr; - if (!(std::fabs(phi2) < M_PI / 2)) - return nullptr; - const double ec = std::sqrt(e2); - const double m1 = msfn(phi1, e2); - const double m2 = msfn(phi2, e2); - const double t1 = tsfn(phi1, ec); - const double t2 = tsfn(phi2, ec); - const double n_denom = std::log(t1) - std::log(t2); - const double n = (std::fabs(n_denom) < 1e-10) - ? std::sin(phi1) - : (std::log(m1) - std::log(m2)) / n_denom; - if (std::fabs(n) < 1e-10) - return nullptr; - const double F = m1 / (n * std::pow(t1, n)); - const double phi0 = std::asin(n); - const double m0 = msfn(phi0, e2); - const double t0 = tsfn(phi0, ec); - const double F0 = m0 / (n * std::pow(t0, n)); - const double k0 = F / F0; - const double a = geogCRS->ellipsoid()->semiMajorAxis().getSIValue(); - const double tF = tsfn(phiF, ec); - const double FN_correction = - a * F * (std::pow(tF, n) - std::pow(t0, n)); - - double phi0Deg = RadToDeg(phi0); - // Try to round to thousandth of degree if very close to it - if (std::fabs(phi0Deg * 1000 - std::floor(phi0Deg * 1000 + 0.5)) < 1e-8) - phi0Deg = std::floor(phi0Deg * 1000 + 0.5) / 1000; - - auto conv = createLambertConicConformal_1SP( - util::PropertyMap(), - common::Angle(phi0Deg, common::UnitOfMeasure::DEGREE), - common::Angle(parameterValueMeasure( - EPSG_CODE_PARAMETER_LONGITUDE_FALSE_ORIGIN)), - common::Scale(k0), common::Length(parameterValueMeasure( - EPSG_CODE_PARAMETER_EASTING_FALSE_ORIGIN)), - common::Length( - parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_NORTHING_FALSE_ORIGIN) + - (std::fabs(FN_correction) > 1e-8 ? FN_correction : 0))); - conv->setCRSs(this, false); - return conv.as_nullable(); - } - - return nullptr; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -static void getESRIMethodNameAndParams(const Conversion *conv, - const std::string &methodName, - int methodEPSGCode, - const char *&esriMethodName, - const ESRIParamMapping *&esriParams) { - esriParams = nullptr; - esriMethodName = nullptr; - const auto *esriMapping = getESRIMapping(methodName, methodEPSGCode); - const auto l_targetCRS = conv->targetCRS(); - if (esriMapping) { - esriParams = esriMapping->params; - esriMethodName = esriMapping->esri_name; - if (esriMapping->epsg_code == - EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL || - esriMapping->epsg_code == - EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL_SPHERICAL) { - if (l_targetCRS && - ci_find(l_targetCRS->nameStr(), "Plate Carree") != - std::string::npos && - conv->parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN) == 0.0) { - esriParams = paramsESRI_Plate_Carree; - esriMethodName = "Plate_Carree"; - } else { - esriParams = paramsESRI_Equidistant_Cylindrical; - esriMethodName = "Equidistant_Cylindrical"; - } - } else if (esriMapping->epsg_code == - EPSG_CODE_METHOD_TRANSVERSE_MERCATOR) { - if (ci_find(conv->nameStr(), "Gauss Kruger") != std::string::npos || - (l_targetCRS && (ci_find(l_targetCRS->nameStr(), "Gauss") != - std::string::npos || - ci_find(l_targetCRS->nameStr(), "GK_") != - std::string::npos))) { - esriParams = paramsESRI_Gauss_Kruger; - esriMethodName = "Gauss_Kruger"; - } else { - esriParams = paramsESRI_Transverse_Mercator; - esriMethodName = "Transverse_Mercator"; - } - } else if (esriMapping->epsg_code == - EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_A) { - if (std::abs( - conv->parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE) - - conv->parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID)) < - 1e-15) { - esriParams = - paramsESRI_Hotine_Oblique_Mercator_Azimuth_Natural_Origin; - esriMethodName = - "Hotine_Oblique_Mercator_Azimuth_Natural_Origin"; - } else { - esriParams = - paramsESRI_Rectified_Skew_Orthomorphic_Natural_Origin; - esriMethodName = "Rectified_Skew_Orthomorphic_Natural_Origin"; - } - } else if (esriMapping->epsg_code == - EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_B) { - if (std::abs( - conv->parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE) - - conv->parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID)) < - 1e-15) { - esriParams = paramsESRI_Hotine_Oblique_Mercator_Azimuth_Center; - esriMethodName = "Hotine_Oblique_Mercator_Azimuth_Center"; - } else { - esriParams = paramsESRI_Rectified_Skew_Orthomorphic_Center; - esriMethodName = "Rectified_Skew_Orthomorphic_Center"; - } - } else if (esriMapping->epsg_code == - EPSG_CODE_METHOD_POLAR_STEREOGRAPHIC_VARIANT_B) { - if (conv->parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_LATITUDE_STD_PARALLEL) > 0) { - esriMethodName = "Stereographic_North_Pole"; - } else { - esriMethodName = "Stereographic_South_Pole"; - } - } - } -} - -// --------------------------------------------------------------------------- - -const char *Conversion::getESRIMethodName() const { - const auto &l_method = method(); - const auto &methodName = l_method->nameStr(); - const auto methodEPSGCode = l_method->getEPSGCode(); - const ESRIParamMapping *esriParams = nullptr; - const char *esriMethodName = nullptr; - getESRIMethodNameAndParams(this, methodName, methodEPSGCode, esriMethodName, - esriParams); - return esriMethodName; -} - -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -const char *Conversion::getWKT1GDALMethodName() const { - const auto &l_method = method(); - const auto methodEPSGCode = l_method->getEPSGCode(); - if (methodEPSGCode == - EPSG_CODE_METHOD_POPULAR_VISUALISATION_PSEUDO_MERCATOR) { - return "Mercator_1SP"; - } - const MethodMapping *mapping = getMapping(l_method.get()); - return mapping ? mapping->wkt1_name : nullptr; -} - -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -void Conversion::_exportToWKT(io::WKTFormatter *formatter) const { - const auto &l_method = method(); - const auto &methodName = l_method->nameStr(); - const auto methodEPSGCode = l_method->getEPSGCode(); - const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; - - if (!isWKT2 && formatter->useESRIDialect()) { - if (methodEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_A) { - auto eqConv = - convertToOtherMethod(EPSG_CODE_METHOD_MERCATOR_VARIANT_B); - if (eqConv) { - eqConv->_exportToWKT(formatter); - return; - } - } - } - - if (isWKT2) { - formatter->startNode(formatter->useDerivingConversion() - ? io::WKTConstants::DERIVINGCONVERSION - : io::WKTConstants::CONVERSION, - !identifiers().empty()); - formatter->addQuotedString(nameStr()); - } else { - formatter->enter(); - formatter->pushOutputUnit(false); - formatter->pushOutputId(false); - } - -#ifdef DEBUG_CONVERSION_ID - if (sourceCRS() && targetCRS()) { - formatter->startNode("SOURCECRS_ID", false); - sourceCRS()->formatID(formatter); - formatter->endNode(); - formatter->startNode("TARGETCRS_ID", false); - targetCRS()->formatID(formatter); - formatter->endNode(); - } -#endif - - bool bAlreadyWritten = false; - if (!isWKT2 && formatter->useESRIDialect()) { - const ESRIParamMapping *esriParams = nullptr; - const char *esriMethodName = nullptr; - getESRIMethodNameAndParams(this, methodName, methodEPSGCode, - esriMethodName, esriParams); - if (esriMethodName && esriParams) { - formatter->startNode(io::WKTConstants::PROJECTION, false); - formatter->addQuotedString(esriMethodName); - formatter->endNode(); - - for (int i = 0; esriParams[i].esri_name != nullptr; i++) { - const auto &esriParam = esriParams[i]; - formatter->startNode(io::WKTConstants::PARAMETER, false); - formatter->addQuotedString(esriParam.esri_name); - if (esriParam.wkt2_name) { - const auto &pv = parameterValue(esriParam.wkt2_name, - esriParam.epsg_code); - if (pv && pv->type() == ParameterValue::Type::MEASURE) { - const auto &v = pv->value(); - // as we don't output the natural unit, output - // to the registered linear / angular unit. - const auto &unitType = v.unit().type(); - if (unitType == common::UnitOfMeasure::Type::LINEAR) { - formatter->add(v.convertToUnit( - *(formatter->axisLinearUnit()))); - } else if (unitType == - common::UnitOfMeasure::Type::ANGULAR) { - const auto &angUnit = - *(formatter->axisAngularUnit()); - double val = v.convertToUnit(angUnit); - if (angUnit == common::UnitOfMeasure::DEGREE) { - if (val > 180.0) { - val -= 360.0; - } else if (val < -180.0) { - val += 360.0; - } - } - formatter->add(val); - } else { - formatter->add(v.getSIValue()); - } - } else if (ci_find(esriParam.esri_name, "scale") != - std::string::npos) { - formatter->add(1.0); - } else { - formatter->add(0.0); - } - } else { - formatter->add(esriParam.fixed_value); - } - formatter->endNode(); - } - bAlreadyWritten = true; - } - } else if (!isWKT2) { - if (methodEPSGCode == - EPSG_CODE_METHOD_POPULAR_VISUALISATION_PSEUDO_MERCATOR) { - const double latitudeOrigin = parameterValueNumeric( - EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN, - common::UnitOfMeasure::DEGREE); - if (latitudeOrigin != 0) { - throw io::FormattingException( - std::string("Unsupported value for ") + - EPSG_NAME_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN); - } - - bAlreadyWritten = true; - formatter->startNode(io::WKTConstants::PROJECTION, false); - formatter->addQuotedString("Mercator_1SP"); - formatter->endNode(); - - formatter->startNode(io::WKTConstants::PARAMETER, false); - formatter->addQuotedString("central_meridian"); - const double centralMeridian = parameterValueNumeric( - EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN, - common::UnitOfMeasure::DEGREE); - formatter->add(centralMeridian); - formatter->endNode(); - - formatter->startNode(io::WKTConstants::PARAMETER, false); - formatter->addQuotedString("scale_factor"); - formatter->add(1.0); - formatter->endNode(); - - formatter->startNode(io::WKTConstants::PARAMETER, false); - formatter->addQuotedString("false_easting"); - const double falseEasting = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_FALSE_EASTING); - formatter->add(falseEasting); - formatter->endNode(); - - formatter->startNode(io::WKTConstants::PARAMETER, false); - formatter->addQuotedString("false_northing"); - const double falseNorthing = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_FALSE_NORTHING); - formatter->add(falseNorthing); - formatter->endNode(); - } else if (starts_with(methodName, "PROJ ")) { - bAlreadyWritten = true; - formatter->startNode(io::WKTConstants::PROJECTION, false); - formatter->addQuotedString("custom_proj4"); - formatter->endNode(); - } - } - - if (!bAlreadyWritten) { - l_method->_exportToWKT(formatter); - - const MethodMapping *mapping = - !isWKT2 ? getMapping(l_method.get()) : nullptr; - for (const auto &genOpParamvalue : parameterValues()) { - - // EPSG has normally no Latitude of natural origin for Equidistant - // Cylindrical but PROJ can handle it, so output the parameter if - // not zero - if ((methodEPSGCode == EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL || - methodEPSGCode == - EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL_SPHERICAL)) { - auto opParamvalue = - dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue && - opParamvalue->parameter()->getEPSGCode() == - EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN) { - const auto ¶mValue = opParamvalue->parameterValue(); - if (paramValue->type() == ParameterValue::Type::MEASURE) { - const auto &measure = paramValue->value(); - if (measure.getSIValue() == 0) { - continue; - } - } - } - } - // Same for false easting / false northing for Vertical Perspective - else if (methodEPSGCode == EPSG_CODE_METHOD_VERTICAL_PERSPECTIVE) { - auto opParamvalue = - dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue) { - const auto paramEPSGCode = - opParamvalue->parameter()->getEPSGCode(); - if (paramEPSGCode == EPSG_CODE_PARAMETER_FALSE_EASTING || - paramEPSGCode == EPSG_CODE_PARAMETER_FALSE_NORTHING) { - const auto ¶mValue = opParamvalue->parameterValue(); - if (paramValue->type() == - ParameterValue::Type::MEASURE) { - const auto &measure = paramValue->value(); - if (measure.getSIValue() == 0) { - continue; - } - } - } - } - } - genOpParamvalue->_exportToWKT(formatter, mapping); - } - } - - if (isWKT2) { - if (formatter->outputId()) { - formatID(formatter); - } - formatter->endNode(); - } else { - formatter->popOutputUnit(); - formatter->popOutputId(); - formatter->leave(); - } -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -void Conversion::_exportToJSON( - io::JSONFormatter *formatter) const // throw(FormattingException) -{ - auto writer = formatter->writer(); - auto objectContext( - formatter->MakeObjectContext("Conversion", !identifiers().empty())); - - writer->AddObjKey("name"); - auto l_name = nameStr(); - if (l_name.empty()) { - writer->Add("unnamed"); - } else { - writer->Add(l_name); - } - - writer->AddObjKey("method"); - formatter->setOmitTypeInImmediateChild(); - formatter->setAllowIDInImmediateChild(); - method()->_exportToJSON(formatter); - - const auto &l_parameterValues = parameterValues(); - if (!l_parameterValues.empty()) { - writer->AddObjKey("parameters"); - { - auto parametersContext(writer->MakeArrayContext(false)); - for (const auto &genOpParamvalue : l_parameterValues) { - formatter->setAllowIDInImmediateChild(); - formatter->setOmitTypeInImmediateChild(); - genOpParamvalue->_exportToJSON(formatter); - } - } - } - - if (formatter->outputId()) { - formatID(formatter); - } -} - -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static bool createPROJ4WebMercator(const Conversion *conv, - io::PROJStringFormatter *formatter) { - const double centralMeridian = conv->parameterValueNumeric( - EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN, - common::UnitOfMeasure::DEGREE); - - const double falseEasting = - conv->parameterValueNumericAsSI(EPSG_CODE_PARAMETER_FALSE_EASTING); - - const double falseNorthing = - conv->parameterValueNumericAsSI(EPSG_CODE_PARAMETER_FALSE_NORTHING); - - auto sourceCRS = conv->sourceCRS(); - auto geogCRS = dynamic_cast(sourceCRS.get()); - if (!geogCRS) { - return false; - } - - std::string units("m"); - auto targetCRS = conv->targetCRS(); - auto targetProjCRS = - dynamic_cast(targetCRS.get()); - if (targetProjCRS) { - const auto &axisList = targetProjCRS->coordinateSystem()->axisList(); - const auto &unit = axisList[0]->unit(); - if (!unit._isEquivalentTo(common::UnitOfMeasure::METRE, - util::IComparable::Criterion::EQUIVALENT)) { - auto projUnit = unit.exportToPROJString(); - if (!projUnit.empty()) { - units = projUnit; - } else { - return false; - } - } - } - - formatter->addStep("merc"); - const double a = geogCRS->ellipsoid()->semiMajorAxis().getSIValue(); - formatter->addParam("a", a); - formatter->addParam("b", a); - formatter->addParam("lat_ts", 0.0); - formatter->addParam("lon_0", centralMeridian); - formatter->addParam("x_0", falseEasting); - formatter->addParam("y_0", falseNorthing); - formatter->addParam("k", 1.0); - formatter->addParam("units", units); - formatter->addParam("nadgrids", "@null"); - formatter->addParam("wktext"); - formatter->addParam("no_defs"); - return true; -} - -// --------------------------------------------------------------------------- - -static bool -createPROJExtensionFromCustomProj(const Conversion *conv, - io::PROJStringFormatter *formatter, - bool forExtensionNode) { - const auto &methodName = conv->method()->nameStr(); - assert(starts_with(methodName, "PROJ ")); - auto tokens = split(methodName, ' '); - - formatter->addStep(tokens[1]); - - if (forExtensionNode) { - auto sourceCRS = conv->sourceCRS(); - auto geogCRS = - dynamic_cast(sourceCRS.get()); - if (!geogCRS) { - return false; - } - geogCRS->addDatumInfoToPROJString(formatter); - } - - for (size_t i = 2; i < tokens.size(); i++) { - auto kv = split(tokens[i], '='); - if (kv.size() == 2) { - formatter->addParam(kv[0], kv[1]); - } else { - formatter->addParam(tokens[i]); - } - } - - for (const auto &genOpParamvalue : conv->parameterValues()) { - auto opParamvalue = dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue) { - const auto ¶mName = opParamvalue->parameter()->nameStr(); - const auto ¶mValue = opParamvalue->parameterValue(); - if (paramValue->type() == ParameterValue::Type::MEASURE) { - const auto &measure = paramValue->value(); - const auto unitType = measure.unit().type(); - if (unitType == common::UnitOfMeasure::Type::LINEAR) { - formatter->addParam(paramName, measure.getSIValue()); - } else if (unitType == common::UnitOfMeasure::Type::ANGULAR) { - formatter->addParam( - paramName, - measure.convertToUnit(common::UnitOfMeasure::DEGREE)); - } else { - formatter->addParam(paramName, measure.value()); - } - } - } - } - - if (forExtensionNode) { - formatter->addParam("wktext"); - formatter->addParam("no_defs"); - } - return true; -} -//! @endcond - -// --------------------------------------------------------------------------- - -bool Conversion::addWKTExtensionNode(io::WKTFormatter *formatter) const { - const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; - if (!isWKT2) { - const auto &l_method = method(); - const auto &methodName = l_method->nameStr(); - const int methodEPSGCode = l_method->getEPSGCode(); - if (l_method->getPrivate()->projMethodOverride_ == "tmerc approx" || - l_method->getPrivate()->projMethodOverride_ == "utm approx") { - auto projFormatter = io::PROJStringFormatter::create(); - projFormatter->setCRSExport(true); - projFormatter->setUseApproxTMerc(true); - formatter->startNode(io::WKTConstants::EXTENSION, false); - formatter->addQuotedString("PROJ4"); - _exportToPROJString(projFormatter.get()); - projFormatter->addParam("no_defs"); - formatter->addQuotedString(projFormatter->toString()); - formatter->endNode(); - return true; - } else if (methodEPSGCode == - EPSG_CODE_METHOD_POPULAR_VISUALISATION_PSEUDO_MERCATOR || - nameStr() == "Popular Visualisation Mercator") { - - auto projFormatter = io::PROJStringFormatter::create(); - projFormatter->setCRSExport(true); - if (createPROJ4WebMercator(this, projFormatter.get())) { - formatter->startNode(io::WKTConstants::EXTENSION, false); - formatter->addQuotedString("PROJ4"); - formatter->addQuotedString(projFormatter->toString()); - formatter->endNode(); - return true; - } - } else if (starts_with(methodName, "PROJ ")) { - auto projFormatter = io::PROJStringFormatter::create(); - projFormatter->setCRSExport(true); - if (createPROJExtensionFromCustomProj(this, projFormatter.get(), - true)) { - formatter->startNode(io::WKTConstants::EXTENSION, false); - formatter->addQuotedString("PROJ4"); - formatter->addQuotedString(projFormatter->toString()); - formatter->endNode(); - return true; - } - } else if (methodName == - PROJ_WKT2_NAME_METHOD_GEOSTATIONARY_SATELLITE_SWEEP_X) { - auto projFormatter = io::PROJStringFormatter::create(); - projFormatter->setCRSExport(true); - formatter->startNode(io::WKTConstants::EXTENSION, false); - formatter->addQuotedString("PROJ4"); - _exportToPROJString(projFormatter.get()); - projFormatter->addParam("no_defs"); - formatter->addQuotedString(projFormatter->toString()); - formatter->endNode(); - return true; - } - } - return false; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -void Conversion::_exportToPROJString( - io::PROJStringFormatter *formatter) const // throw(FormattingException) -{ - const auto &l_method = method(); - const auto &methodName = l_method->nameStr(); - const int methodEPSGCode = l_method->getEPSGCode(); - const bool isZUnitConversion = - methodEPSGCode == EPSG_CODE_METHOD_CHANGE_VERTICAL_UNIT; - const bool isAffineParametric = - methodEPSGCode == EPSG_CODE_METHOD_AFFINE_PARAMETRIC_TRANSFORMATION; - const bool isGeographicGeocentric = - methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC_GEOCENTRIC; - const bool isHeightDepthReversal = - methodEPSGCode == EPSG_CODE_METHOD_HEIGHT_DEPTH_REVERSAL; - const bool applySourceCRSModifiers = - !isZUnitConversion && !isAffineParametric && - !isAxisOrderReversal(methodEPSGCode) && !isGeographicGeocentric && - !isHeightDepthReversal; - bool applyTargetCRSModifiers = applySourceCRSModifiers; - - auto l_sourceCRS = sourceCRS(); - if (!formatter->getCRSExport() && l_sourceCRS && applySourceCRSModifiers) { - - crs::CRS *horiz = l_sourceCRS.get(); - const auto compound = dynamic_cast(horiz); - if (compound) { - const auto &components = compound->componentReferenceSystems(); - if (!components.empty()) { - horiz = components.front().get(); - } - } - - auto geogCRS = dynamic_cast(horiz); - if (geogCRS) { - formatter->setOmitProjLongLatIfPossible(true); - formatter->startInversion(); - geogCRS->_exportToPROJString(formatter); - formatter->stopInversion(); - formatter->setOmitProjLongLatIfPossible(false); - } - - auto projCRS = dynamic_cast(horiz); - if (projCRS) { - formatter->startInversion(); - formatter->pushOmitZUnitConversion(); - projCRS->addUnitConvertAndAxisSwap(formatter, false); - formatter->popOmitZUnitConversion(); - formatter->stopInversion(); - } - } - - const auto &convName = nameStr(); - bool bConversionDone = false; - bool bEllipsoidParametersDone = false; - bool useApprox = false; - if (methodEPSGCode == EPSG_CODE_METHOD_TRANSVERSE_MERCATOR) { - // Check for UTM - int zone = 0; - bool north = true; - useApprox = - formatter->getUseApproxTMerc() || - l_method->getPrivate()->projMethodOverride_ == "tmerc approx" || - l_method->getPrivate()->projMethodOverride_ == "utm approx"; - if (isUTM(zone, north)) { - bConversionDone = true; - formatter->addStep("utm"); - if (useApprox) { - formatter->addParam("approx"); - } - formatter->addParam("zone", zone); - if (!north) { - formatter->addParam("south"); - } - } - } else if (methodEPSGCode == - EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_A) { - const double azimuth = - parameterValueNumeric(EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE, - common::UnitOfMeasure::DEGREE); - const double angleRectifiedToSkewGrid = parameterValueNumeric( - EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID, - common::UnitOfMeasure::DEGREE); - // Map to Swiss Oblique Mercator / somerc - if (std::fabs(azimuth - 90) < 1e-4 && - std::fabs(angleRectifiedToSkewGrid - 90) < 1e-4) { - bConversionDone = true; - formatter->addStep("somerc"); - formatter->addParam( - "lat_0", parameterValueNumeric( - EPSG_CODE_PARAMETER_LATITUDE_PROJECTION_CENTRE, - common::UnitOfMeasure::DEGREE)); - formatter->addParam( - "lon_0", parameterValueNumeric( - EPSG_CODE_PARAMETER_LONGITUDE_PROJECTION_CENTRE, - common::UnitOfMeasure::DEGREE)); - formatter->addParam( - "k_0", parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_SCALE_FACTOR_INITIAL_LINE)); - formatter->addParam("x_0", parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_FALSE_EASTING)); - formatter->addParam("y_0", parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_FALSE_NORTHING)); - } - } else if (methodEPSGCode == - EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_B) { - const double azimuth = - parameterValueNumeric(EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE, - common::UnitOfMeasure::DEGREE); - const double angleRectifiedToSkewGrid = parameterValueNumeric( - EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID, - common::UnitOfMeasure::DEGREE); - // Map to Swiss Oblique Mercator / somerc - if (std::fabs(azimuth - 90) < 1e-4 && - std::fabs(angleRectifiedToSkewGrid - 90) < 1e-4) { - bConversionDone = true; - formatter->addStep("somerc"); - formatter->addParam( - "lat_0", parameterValueNumeric( - EPSG_CODE_PARAMETER_LATITUDE_PROJECTION_CENTRE, - common::UnitOfMeasure::DEGREE)); - formatter->addParam( - "lon_0", parameterValueNumeric( - EPSG_CODE_PARAMETER_LONGITUDE_PROJECTION_CENTRE, - common::UnitOfMeasure::DEGREE)); - formatter->addParam( - "k_0", parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_SCALE_FACTOR_INITIAL_LINE)); - formatter->addParam( - "x_0", parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_EASTING_PROJECTION_CENTRE)); - formatter->addParam( - "y_0", parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_NORTHING_PROJECTION_CENTRE)); - } - } else if (methodEPSGCode == EPSG_CODE_METHOD_KROVAK_NORTH_ORIENTED) { - double colatitude = - parameterValueNumeric(EPSG_CODE_PARAMETER_COLATITUDE_CONE_AXIS, - common::UnitOfMeasure::DEGREE); - double latitudePseudoStandardParallel = parameterValueNumeric( - EPSG_CODE_PARAMETER_LATITUDE_PSEUDO_STANDARD_PARALLEL, - common::UnitOfMeasure::DEGREE); - // 30deg 17' 17.30311'' = 30.28813975277777776 - // 30deg 17' 17.303'' = 30.288139722222223 as used in GDAL WKT1 - if (std::fabs(colatitude - 30.2881397) > 1e-7) { - throw io::FormattingException( - std::string("Unsupported value for ") + - EPSG_NAME_PARAMETER_COLATITUDE_CONE_AXIS); - } - if (std::fabs(latitudePseudoStandardParallel - 78.5) > 1e-8) { - throw io::FormattingException( - std::string("Unsupported value for ") + - EPSG_NAME_PARAMETER_LATITUDE_PSEUDO_STANDARD_PARALLEL); - } - } else if (methodEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_A) { - double latitudeOrigin = parameterValueNumeric( - EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN, - common::UnitOfMeasure::DEGREE); - if (latitudeOrigin != 0) { - throw io::FormattingException( - std::string("Unsupported value for ") + - EPSG_NAME_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN); - } - } else if (methodEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_B) { - const auto &scaleFactor = parameterValueMeasure(WKT1_SCALE_FACTOR, 0); - if (scaleFactor.unit().type() != common::UnitOfMeasure::Type::UNKNOWN && - std::fabs(scaleFactor.getSIValue() - 1.0) > 1e-10) { - throw io::FormattingException( - "Unexpected presence of scale factor in Mercator (variant B)"); - } - double latitudeOrigin = parameterValueNumeric( - EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN, - common::UnitOfMeasure::DEGREE); - if (latitudeOrigin != 0) { - throw io::FormattingException( - std::string("Unsupported value for ") + - EPSG_NAME_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN); - } - } else if (methodEPSGCode == - EPSG_CODE_METHOD_TRANSVERSE_MERCATOR_SOUTH_ORIENTATED) { - // We map TMSO to tmerc with axis=wsu. This only works if false easting - // and northings are zero, which is the case in practice for South - // African and Namibian EPSG CRS - const auto falseEasting = parameterValueNumeric( - EPSG_CODE_PARAMETER_FALSE_EASTING, common::UnitOfMeasure::METRE); - if (falseEasting != 0) { - throw io::FormattingException( - std::string("Unsupported value for ") + - EPSG_NAME_PARAMETER_FALSE_EASTING); - } - const auto falseNorthing = parameterValueNumeric( - EPSG_CODE_PARAMETER_FALSE_NORTHING, common::UnitOfMeasure::METRE); - if (falseNorthing != 0) { - throw io::FormattingException( - std::string("Unsupported value for ") + - EPSG_NAME_PARAMETER_FALSE_NORTHING); - } - // PROJ.4 specific hack for webmercator - } else if (formatter->getCRSExport() && - methodEPSGCode == - EPSG_CODE_METHOD_POPULAR_VISUALISATION_PSEUDO_MERCATOR) { - if (!createPROJ4WebMercator(this, formatter)) { - throw io::FormattingException( - std::string("Cannot export ") + - EPSG_NAME_METHOD_POPULAR_VISUALISATION_PSEUDO_MERCATOR + - " as PROJ.4 string outside of a ProjectedCRS context"); - } - bConversionDone = true; - bEllipsoidParametersDone = true; - applyTargetCRSModifiers = false; - } else if (ci_equal(convName, "Popular Visualisation Mercator")) { - if (formatter->getCRSExport()) { - if (!createPROJ4WebMercator(this, formatter)) { - throw io::FormattingException(concat( - "Cannot export ", convName, - " as PROJ.4 string outside of a ProjectedCRS context")); - } - applyTargetCRSModifiers = false; - } else { - formatter->addStep("webmerc"); - if (l_sourceCRS) { - datum::Ellipsoid::WGS84->_exportToPROJString(formatter); - } - } - bConversionDone = true; - bEllipsoidParametersDone = true; - } else if (starts_with(methodName, "PROJ ")) { - bConversionDone = true; - createPROJExtensionFromCustomProj(this, formatter, false); - } else if (ci_equal(methodName, - PROJ_WKT2_NAME_METHOD_POLE_ROTATION_GRIB_CONVENTION)) { - double southPoleLat = parameterValueNumeric( - PROJ_WKT2_NAME_PARAMETER_SOUTH_POLE_LATITUDE_GRIB_CONVENTION, - common::UnitOfMeasure::DEGREE); - double southPoleLon = parameterValueNumeric( - PROJ_WKT2_NAME_PARAMETER_SOUTH_POLE_LONGITUDE_GRIB_CONVENTION, - common::UnitOfMeasure::DEGREE); - double rotation = parameterValueNumeric( - PROJ_WKT2_NAME_PARAMETER_AXIS_ROTATION_GRIB_CONVENTION, - common::UnitOfMeasure::DEGREE); - formatter->addStep("ob_tran"); - formatter->addParam("o_proj", "longlat"); - formatter->addParam("o_lon_p", -rotation); - formatter->addParam("o_lat_p", -southPoleLat); - formatter->addParam("lon_0", southPoleLon); - bConversionDone = true; - } else if (ci_equal(methodName, "Adams_Square_II")) { - // Look for ESRI method and parameter names (to be opposed - // to the OGC WKT2 names we use elsewhere, because there's no mapping - // of those parameters to OGC WKT2) - // We also reject non-default values for a number of parameters, - // because they are not implemented on PROJ side. The subset we - // support can handle ESRI:54098 WGS_1984_Adams_Square_II, but not - // ESRI:54099 WGS_1984_Spilhaus_Ocean_Map_in_Square - const double falseEasting = parameterValueNumeric( - "False_Easting", common::UnitOfMeasure::METRE); - const double falseNorthing = parameterValueNumeric( - "False_Northing", common::UnitOfMeasure::METRE); - const double scaleFactor = - parameterValue("Scale_Factor", 0) - ? parameterValueNumeric("Scale_Factor", - common::UnitOfMeasure::SCALE_UNITY) - : 1.0; - const double azimuth = - parameterValueNumeric("Azimuth", common::UnitOfMeasure::DEGREE); - const double longitudeOfCenter = parameterValueNumeric( - "Longitude_Of_Center", common::UnitOfMeasure::DEGREE); - const double latitudeOfCenter = parameterValueNumeric( - "Latitude_Of_Center", common::UnitOfMeasure::DEGREE); - const double XYPlaneRotation = parameterValueNumeric( - "XY_Plane_Rotation", common::UnitOfMeasure::DEGREE); - if (scaleFactor != 1.0 || azimuth != 0.0 || latitudeOfCenter != 0.0 || - XYPlaneRotation != 0.0) { - throw io::FormattingException("Unsupported value for one or " - "several parameters of " - "Adams_Square_II"); - } - formatter->addStep("adams_ws2"); - formatter->addParam("lon_0", longitudeOfCenter); - formatter->addParam("x_0", falseEasting); - formatter->addParam("y_0", falseNorthing); - bConversionDone = true; - } else if (formatter->convention() == - io::PROJStringFormatter::Convention::PROJ_5 && - isZUnitConversion) { - double convFactor = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR); - auto uom = common::UnitOfMeasure(std::string(), convFactor, - common::UnitOfMeasure::Type::LINEAR) - .exportToPROJString(); - auto reverse_uom = - convFactor == 0.0 - ? std::string() - : common::UnitOfMeasure(std::string(), 1.0 / convFactor, - common::UnitOfMeasure::Type::LINEAR) - .exportToPROJString(); - if (uom == "m") { - // do nothing - } else if (!uom.empty()) { - formatter->addStep("unitconvert"); - formatter->addParam("z_in", uom); - formatter->addParam("z_out", "m"); - } else if (!reverse_uom.empty()) { - formatter->addStep("unitconvert"); - formatter->addParam("z_in", "m"); - formatter->addParam("z_out", reverse_uom); - } else { - formatter->addStep("affine"); - formatter->addParam("s33", convFactor); - } - bConversionDone = true; - bEllipsoidParametersDone = true; - } - - auto l_targetCRS = targetCRS(); - - bool bAxisSpecFound = false; - if (!bConversionDone) { - const MethodMapping *mapping = getMapping(l_method.get()); - if (mapping && mapping->proj_name_main) { - formatter->addStep(mapping->proj_name_main); - if (useApprox) { - formatter->addParam("approx"); - } - if (mapping->proj_name_aux) { - bool addAux = true; - if (internal::starts_with(mapping->proj_name_aux, "axis=")) { - if (mapping->epsg_code == EPSG_CODE_METHOD_KROVAK) { - auto projCRS = dynamic_cast( - l_targetCRS.get()); - if (projCRS) { - const auto &axisList = - projCRS->coordinateSystem()->axisList(); - if (axisList[0]->direction() == - cs::AxisDirection::WEST && - axisList[1]->direction() == - cs::AxisDirection::SOUTH) { - formatter->addParam("czech"); - addAux = false; - } - } - } - bAxisSpecFound = true; - } - - // No need to add explicit f=0 if the ellipsoid is a sphere - if (strcmp(mapping->proj_name_aux, "f=0") == 0) { - crs::CRS *horiz = l_sourceCRS.get(); - const auto compound = - dynamic_cast(horiz); - if (compound) { - const auto &components = - compound->componentReferenceSystems(); - if (!components.empty()) { - horiz = components.front().get(); - } - } - - auto geogCRS = - dynamic_cast(horiz); - if (geogCRS && geogCRS->ellipsoid()->isSphere()) { - addAux = false; - } - } - - if (addAux) { - auto kv = split(mapping->proj_name_aux, '='); - if (kv.size() == 2) { - formatter->addParam(kv[0], kv[1]); - } else { - formatter->addParam(mapping->proj_name_aux); - } - } - } - - if (mapping->epsg_code == - EPSG_CODE_METHOD_POLAR_STEREOGRAPHIC_VARIANT_B) { - double latitudeStdParallel = parameterValueNumeric( - EPSG_CODE_PARAMETER_LATITUDE_STD_PARALLEL, - common::UnitOfMeasure::DEGREE); - formatter->addParam("lat_0", - (latitudeStdParallel >= 0) ? 90.0 : -90.0); - } - - for (int i = 0; mapping->params[i] != nullptr; i++) { - const auto *param = mapping->params[i]; - if (!param->proj_name) { - continue; - } - const auto &value = - parameterValueMeasure(param->wkt2_name, param->epsg_code); - double valueConverted = 0; - if (value == nullMeasure) { - // Deal with missing values. In an ideal world, this would - // not happen - if (param->epsg_code == - EPSG_CODE_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN) { - valueConverted = 1.0; - } - } else if (param->unit_type == - common::UnitOfMeasure::Type::ANGULAR) { - valueConverted = - value.convertToUnit(common::UnitOfMeasure::DEGREE); - } else { - valueConverted = value.getSIValue(); - } - - if (mapping->epsg_code == - EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP && - strcmp(param->proj_name, "lat_1") == 0) { - formatter->addParam(param->proj_name, valueConverted); - formatter->addParam("lat_0", valueConverted); - } else { - formatter->addParam(param->proj_name, valueConverted); - } - } - - } else { - if (!exportToPROJStringGeneric(formatter)) { - throw io::FormattingException( - concat("Unsupported conversion method: ", methodName)); - } - } - } - - if (l_targetCRS && applyTargetCRSModifiers) { - crs::CRS *horiz = l_targetCRS.get(); - const auto compound = dynamic_cast(horiz); - if (compound) { - const auto &components = compound->componentReferenceSystems(); - if (!components.empty()) { - horiz = components.front().get(); - } - } - - if (!bEllipsoidParametersDone) { - auto targetGeogCRS = horiz->extractGeographicCRS(); - if (targetGeogCRS) { - if (formatter->getCRSExport()) { - targetGeogCRS->addDatumInfoToPROJString(formatter); - } else { - targetGeogCRS->ellipsoid()->_exportToPROJString(formatter); - targetGeogCRS->primeMeridian()->_exportToPROJString( - formatter); - } - } - } - - auto projCRS = dynamic_cast(horiz); - if (projCRS) { - formatter->pushOmitZUnitConversion(); - projCRS->addUnitConvertAndAxisSwap(formatter, bAxisSpecFound); - formatter->popOmitZUnitConversion(); - } - - auto derivedGeographicCRS = - dynamic_cast(horiz); - if (!formatter->getCRSExport() && derivedGeographicCRS) { - formatter->setOmitProjLongLatIfPossible(true); - derivedGeographicCRS->addAngularUnitConvertAndAxisSwap(formatter); - formatter->setOmitProjLongLatIfPossible(false); - } - } -} -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Return whether a conversion is a [Universal Transverse Mercator] - * (https://proj.org/operations/projections/utm.html) conversion. - * - * @param[out] zone UTM zone number between 1 and 60. - * @param[out] north true for UTM northern hemisphere, false for UTM southern - * hemisphere. - * @return true if it is a UTM conversion. - */ -bool Conversion::isUTM(int &zone, bool &north) const { - zone = 0; - north = true; - - if (method()->getEPSGCode() == EPSG_CODE_METHOD_TRANSVERSE_MERCATOR) { - // Check for UTM - - bool bLatitudeNatOriginUTM = false; - bool bScaleFactorUTM = false; - bool bFalseEastingUTM = false; - bool bFalseNorthingUTM = false; - for (const auto &genOpParamvalue : parameterValues()) { - auto opParamvalue = dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue) { - const auto epsg_code = opParamvalue->parameter()->getEPSGCode(); - const auto &l_parameterValue = opParamvalue->parameterValue(); - if (l_parameterValue->type() == ParameterValue::Type::MEASURE) { - const auto &measure = l_parameterValue->value(); - if (epsg_code == - EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN && - std::fabs(measure.value() - - UTM_LATITUDE_OF_NATURAL_ORIGIN) < 1e-10) { - bLatitudeNatOriginUTM = true; - } else if ( - (epsg_code == - EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN || - epsg_code == - EPSG_CODE_PARAMETER_LONGITUDE_OF_ORIGIN) && - measure.unit()._isEquivalentTo( - common::UnitOfMeasure::DEGREE, - util::IComparable::Criterion::EQUIVALENT)) { - double dfZone = (measure.value() + 183.0) / 6.0; - if (dfZone > 0.9 && dfZone < 60.1 && - std::abs(dfZone - std::round(dfZone)) < 1e-10) { - zone = static_cast(std::lround(dfZone)); - } - } else if ( - epsg_code == - EPSG_CODE_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN && - measure.unit()._isEquivalentTo( - common::UnitOfMeasure::SCALE_UNITY, - util::IComparable::Criterion::EQUIVALENT) && - std::fabs(measure.value() - UTM_SCALE_FACTOR) < 1e-10) { - bScaleFactorUTM = true; - } else if (epsg_code == EPSG_CODE_PARAMETER_FALSE_EASTING && - measure.value() == UTM_FALSE_EASTING && - measure.unit()._isEquivalentTo( - common::UnitOfMeasure::METRE, - util::IComparable::Criterion::EQUIVALENT)) { - bFalseEastingUTM = true; - } else if (epsg_code == - EPSG_CODE_PARAMETER_FALSE_NORTHING && - measure.unit()._isEquivalentTo( - common::UnitOfMeasure::METRE, - util::IComparable::Criterion::EQUIVALENT)) { - if (std::fabs(measure.value() - - UTM_NORTH_FALSE_NORTHING) < 1e-10) { - bFalseNorthingUTM = true; - north = true; - } else if (std::fabs(measure.value() - - UTM_SOUTH_FALSE_NORTHING) < - 1e-10) { - bFalseNorthingUTM = true; - north = false; - } - } - } - } - } - if (bLatitudeNatOriginUTM && zone > 0 && bScaleFactorUTM && - bFalseEastingUTM && bFalseNorthingUTM) { - return true; - } - } - return false; -} - -// --------------------------------------------------------------------------- - -/** \brief Return a Conversion object where some parameters are better - * identified. - * - * @return a new Conversion. - */ -ConversionNNPtr Conversion::identify() const { - auto newConversion = Conversion::nn_make_shared(*this); - newConversion->assignSelf(newConversion); - - if (method()->getEPSGCode() == EPSG_CODE_METHOD_TRANSVERSE_MERCATOR) { - // Check for UTM - int zone = 0; - bool north = true; - if (isUTM(zone, north)) { - newConversion->setProperties( - getUTMConversionProperty(util::PropertyMap(), zone, north)); - } - } - - return newConversion; -} - -//! @cond Doxygen_Suppress -// --------------------------------------------------------------------------- - -InvalidOperation::InvalidOperation(const char *message) : Exception(message) {} - -// --------------------------------------------------------------------------- - -InvalidOperation::InvalidOperation(const std::string &message) - : Exception(message) {} - -// --------------------------------------------------------------------------- - -InvalidOperation::InvalidOperation(const InvalidOperation &) = default; - -// --------------------------------------------------------------------------- - -InvalidOperation::~InvalidOperation() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct Transformation::Private { - - TransformationPtr forwardOperation_{}; - - static TransformationNNPtr registerInv(const Transformation *thisIn, - TransformationNNPtr invTransform); -}; -//! @endcond - -// --------------------------------------------------------------------------- - -Transformation::Transformation( - const crs::CRSNNPtr &sourceCRSIn, const crs::CRSNNPtr &targetCRSIn, - const crs::CRSPtr &interpolationCRSIn, const OperationMethodNNPtr &methodIn, - const std::vector &values, - const std::vector &accuracies) - : SingleOperation(methodIn), d(internal::make_unique()) { - setParameterValues(values); - setCRSs(sourceCRSIn, targetCRSIn, interpolationCRSIn); - setAccuracies(accuracies); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -Transformation::~Transformation() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -Transformation::Transformation(const Transformation &other) - : CoordinateOperation(other), SingleOperation(other), - d(internal::make_unique(*other.d)) {} - -// --------------------------------------------------------------------------- - -/** \brief Return the source crs::CRS of the transformation. - * - * @return the source CRS. - */ -const crs::CRSNNPtr &Transformation::sourceCRS() PROJ_PURE_DEFN { - return CoordinateOperation::getPrivate()->strongRef_->sourceCRS_; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the target crs::CRS of the transformation. - * - * @return the target CRS. - */ -const crs::CRSNNPtr &Transformation::targetCRS() PROJ_PURE_DEFN { - return CoordinateOperation::getPrivate()->strongRef_->targetCRS_; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -TransformationNNPtr Transformation::shallowClone() const { - auto transf = Transformation::nn_make_shared(*this); - transf->assignSelf(transf); - transf->setCRSs(this, false); - if (transf->d->forwardOperation_) { - transf->d->forwardOperation_ = - transf->d->forwardOperation_->shallowClone().as_nullable(); - } - return transf; -} - -CoordinateOperationNNPtr Transformation::_shallowClone() const { - return util::nn_static_pointer_cast(shallowClone()); -} - -// --------------------------------------------------------------------------- - -TransformationNNPtr -Transformation::promoteTo3D(const std::string &, - const io::DatabaseContextPtr &dbContext) const { - auto transf = shallowClone(); - transf->setCRSs(sourceCRS()->promoteTo3D(std::string(), dbContext), - targetCRS()->promoteTo3D(std::string(), dbContext), - interpolationCRS()); - return transf; -} - -// --------------------------------------------------------------------------- - -TransformationNNPtr -Transformation::demoteTo2D(const std::string &, - const io::DatabaseContextPtr &dbContext) const { - auto transf = shallowClone(); - transf->setCRSs(sourceCRS()->demoteTo2D(std::string(), dbContext), - targetCRS()->demoteTo2D(std::string(), dbContext), - interpolationCRS()); - return transf; -} - -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -/** \brief Return the TOWGS84 parameters of the transformation. - * - * If this transformation uses Coordinate Frame Rotation, Position Vector - * transformation or Geocentric translations, a vector of 7 double values - * using the Position Vector convention (EPSG:9606) is returned. Those values - * can be used as the value of the WKT1 TOWGS84 parameter or - * PROJ +towgs84 parameter. - * - * @return a vector of 7 values if valid, otherwise a io::FormattingException - * is thrown. - * @throws io::FormattingException - */ -std::vector -Transformation::getTOWGS84Parameters() const // throw(io::FormattingException) -{ - // GDAL WKT1 assumes EPSG:9606 / Position Vector convention - - bool sevenParamsTransform = false; - bool threeParamsTransform = false; - bool invertRotSigns = false; - const auto &l_method = method(); - const auto &methodName = l_method->nameStr(); - const int methodEPSGCode = l_method->getEPSGCode(); - const auto paramCount = parameterValues().size(); - if ((paramCount == 7 && - ci_find(methodName, "Coordinate Frame") != std::string::npos) || - methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC || - methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D || - methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D) { - sevenParamsTransform = true; - invertRotSigns = true; - } else if ((paramCount == 7 && - ci_find(methodName, "Position Vector") != std::string::npos) || - methodEPSGCode == EPSG_CODE_METHOD_POSITION_VECTOR_GEOCENTRIC || - methodEPSGCode == - EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_2D || - methodEPSGCode == - EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_3D) { - sevenParamsTransform = true; - invertRotSigns = false; - } else if ((paramCount == 3 && - ci_find(methodName, "Geocentric translations") != - std::string::npos) || - methodEPSGCode == - EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOCENTRIC || - methodEPSGCode == - EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_2D || - methodEPSGCode == - EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_3D) { - threeParamsTransform = true; - } - - if (threeParamsTransform || sevenParamsTransform) { - std::vector params(7, 0.0); - bool foundX = false; - bool foundY = false; - bool foundZ = false; - bool foundRotX = false; - bool foundRotY = false; - bool foundRotZ = false; - bool foundScale = false; - const double rotSign = invertRotSigns ? -1.0 : 1.0; - - const auto fixNegativeZero = [](double x) { - if (x == 0.0) - return 0.0; - return x; - }; - - for (const auto &genOpParamvalue : parameterValues()) { - auto opParamvalue = dynamic_cast( - genOpParamvalue.get()); - if (opParamvalue) { - const auto ¶meter = opParamvalue->parameter(); - const auto epsg_code = parameter->getEPSGCode(); - const auto &l_parameterValue = opParamvalue->parameterValue(); - if (l_parameterValue->type() == ParameterValue::Type::MEASURE) { - const auto &measure = l_parameterValue->value(); - if (epsg_code == EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION) { - params[0] = measure.getSIValue(); - foundX = true; - } else if (epsg_code == - EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION) { - params[1] = measure.getSIValue(); - foundY = true; - } else if (epsg_code == - EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION) { - params[2] = measure.getSIValue(); - foundZ = true; - } else if (epsg_code == - EPSG_CODE_PARAMETER_X_AXIS_ROTATION) { - params[3] = fixNegativeZero( - rotSign * - measure.convertToUnit( - common::UnitOfMeasure::ARC_SECOND)); - foundRotX = true; - } else if (epsg_code == - EPSG_CODE_PARAMETER_Y_AXIS_ROTATION) { - params[4] = fixNegativeZero( - rotSign * - measure.convertToUnit( - common::UnitOfMeasure::ARC_SECOND)); - foundRotY = true; - } else if (epsg_code == - EPSG_CODE_PARAMETER_Z_AXIS_ROTATION) { - params[5] = fixNegativeZero( - rotSign * - measure.convertToUnit( - common::UnitOfMeasure::ARC_SECOND)); - foundRotZ = true; - } else if (epsg_code == - EPSG_CODE_PARAMETER_SCALE_DIFFERENCE) { - params[6] = measure.convertToUnit( - common::UnitOfMeasure::PARTS_PER_MILLION); - foundScale = true; - } - } - } - } - if (foundX && foundY && foundZ && - (threeParamsTransform || - (foundRotX && foundRotY && foundRotZ && foundScale))) { - return params; - } else { - throw io::FormattingException( - "Missing required parameter values in transformation"); - } - } - -#if 0 - if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC2D_OFFSETS || - methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC3D_OFFSETS) { - auto offsetLat = - parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_OFFSET); - auto offsetLong = - parameterValueMeasure(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET); - - auto offsetHeight = - parameterValueMeasure(EPSG_CODE_PARAMETER_VERTICAL_OFFSET); - - if (offsetLat.getSIValue() == 0.0 && offsetLong.getSIValue() == 0.0 && - offsetHeight.getSIValue() == 0.0) { - std::vector params(7, 0.0); - return params; - } - } -#endif - - throw io::FormattingException( - "Transformation cannot be formatted as WKT1 TOWGS84 parameters"); -} -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation from a vector of GeneralParameterValue. - * - * @param properties See \ref general_properties. At minimum the name should be - * defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param interpolationCRSIn Interpolation CRS (might be null) - * @param methodIn Operation method. - * @param values Vector of GeneralOperationParameterNNPtr. - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - * @throws InvalidOperation - */ -TransformationNNPtr Transformation::create( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, const crs::CRSPtr &interpolationCRSIn, - const OperationMethodNNPtr &methodIn, - const std::vector &values, - const std::vector &accuracies) { - if (methodIn->parameters().size() != values.size()) { - throw InvalidOperation( - "Inconsistent number of parameters and parameter values"); - } - auto transf = Transformation::nn_make_shared( - sourceCRSIn, targetCRSIn, interpolationCRSIn, methodIn, values, - accuracies); - transf->assignSelf(transf); - transf->setProperties(properties); - std::string name; - if (properties.getStringValue(common::IdentifiedObject::NAME_KEY, name) && - ci_find(name, "ballpark") != std::string::npos) { - transf->setHasBallparkTransformation(true); - } - return transf; -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation ands its OperationMethod. - * - * @param propertiesTransformation The \ref general_properties of the - * Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param interpolationCRSIn Interpolation CRS (might be null) - * @param propertiesOperationMethod The \ref general_properties of the - * OperationMethod. - * At minimum the name should be defined. - * @param parameters Vector of parameters of the operation method. - * @param values Vector of ParameterValueNNPtr. Constraint: - * values.size() == parameters.size() - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - * @throws InvalidOperation - */ -TransformationNNPtr -Transformation::create(const util::PropertyMap &propertiesTransformation, - const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, - const crs::CRSPtr &interpolationCRSIn, - const util::PropertyMap &propertiesOperationMethod, - const std::vector ¶meters, - const std::vector &values, - const std::vector - &accuracies) // throw InvalidOperation -{ - OperationMethodNNPtr op( - OperationMethod::create(propertiesOperationMethod, parameters)); - - if (parameters.size() != values.size()) { - throw InvalidOperation( - "Inconsistent number of parameters and parameter values"); - } - std::vector generalParameterValues; - generalParameterValues.reserve(values.size()); - for (size_t i = 0; i < values.size(); i++) { - generalParameterValues.push_back( - OperationParameterValue::create(parameters[i], values[i])); - } - return create(propertiesTransformation, sourceCRSIn, targetCRSIn, - interpolationCRSIn, op, generalParameterValues, accuracies); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -// --------------------------------------------------------------------------- - -static TransformationNNPtr createSevenParamsTransform( - const util::PropertyMap &properties, - const util::PropertyMap &methodProperties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, double translationXMetre, - double translationYMetre, double translationZMetre, - double rotationXArcSecond, double rotationYArcSecond, - double rotationZArcSecond, double scaleDifferencePPM, - const std::vector &accuracies) { - return Transformation::create( - properties, sourceCRSIn, targetCRSIn, nullptr, methodProperties, - VectorOfParameters{ - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_X_AXIS_ROTATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Y_AXIS_ROTATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Z_AXIS_ROTATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_SCALE_DIFFERENCE), - }, - createParams(common::Length(translationXMetre), - common::Length(translationYMetre), - common::Length(translationZMetre), - common::Angle(rotationXArcSecond, - common::UnitOfMeasure::ARC_SECOND), - common::Angle(rotationYArcSecond, - common::UnitOfMeasure::ARC_SECOND), - common::Angle(rotationZArcSecond, - common::UnitOfMeasure::ARC_SECOND), - common::Scale(scaleDifferencePPM, - common::UnitOfMeasure::PARTS_PER_MILLION)), - accuracies); -} - -// --------------------------------------------------------------------------- - -static void getTransformationType(const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, - bool &isGeocentric, bool &isGeog2D, - bool &isGeog3D) { - auto sourceCRSGeod = - dynamic_cast(sourceCRSIn.get()); - auto targetCRSGeod = - dynamic_cast(targetCRSIn.get()); - isGeocentric = sourceCRSGeod && sourceCRSGeod->isGeocentric() && - targetCRSGeod && targetCRSGeod->isGeocentric(); - if (isGeocentric) { - isGeog2D = false; - isGeog3D = false; - return; - } - isGeocentric = false; - - auto sourceCRSGeog = - dynamic_cast(sourceCRSIn.get()); - auto targetCRSGeog = - dynamic_cast(targetCRSIn.get()); - if (!sourceCRSGeog || !targetCRSGeog) { - throw InvalidOperation("Inconsistent CRS type"); - } - const auto nSrcAxisCount = - sourceCRSGeog->coordinateSystem()->axisList().size(); - const auto nTargetAxisCount = - targetCRSGeog->coordinateSystem()->axisList().size(); - isGeog2D = nSrcAxisCount == 2 && nTargetAxisCount == 2; - isGeog3D = !isGeog2D && nSrcAxisCount >= 2 && nTargetAxisCount >= 2; -} - -// --------------------------------------------------------------------------- - -static int -useOperationMethodEPSGCodeIfPresent(const util::PropertyMap &properties, - int nDefaultOperationMethodEPSGCode) { - const auto *operationMethodEPSGCode = - properties.get("OPERATION_METHOD_EPSG_CODE"); - if (operationMethodEPSGCode) { - const auto boxedValue = dynamic_cast( - (*operationMethodEPSGCode).get()); - if (boxedValue && - boxedValue->type() == util::BoxedValue::Type::INTEGER) { - return boxedValue->integerValue(); - } - } - return nDefaultOperationMethodEPSGCode; -} -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation with Geocentric Translations method. - * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param translationXMetre Value of the Translation_X parameter (in metre). - * @param translationYMetre Value of the Translation_Y parameter (in metre). - * @param translationZMetre Value of the Translation_Z parameter (in metre). - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - */ -TransformationNNPtr Transformation::createGeocentricTranslations( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, double translationXMetre, - double translationYMetre, double translationZMetre, - const std::vector &accuracies) { - bool isGeocentric; - bool isGeog2D; - bool isGeog3D; - getTransformationType(sourceCRSIn, targetCRSIn, isGeocentric, isGeog2D, - isGeog3D); - return create( - properties, sourceCRSIn, targetCRSIn, nullptr, - createMethodMapNameEPSGCode(useOperationMethodEPSGCodeIfPresent( - properties, - isGeocentric - ? EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOCENTRIC - : isGeog2D - ? EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_2D - : EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_3D)), - VectorOfParameters{ - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION), - }, - createParams(common::Length(translationXMetre), - common::Length(translationYMetre), - common::Length(translationZMetre)), - accuracies); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation with Position vector transformation - * method. - * - * This is similar to createCoordinateFrameRotation(), except that the sign of - * the rotation terms is inverted. - * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param translationXMetre Value of the Translation_X parameter (in metre). - * @param translationYMetre Value of the Translation_Y parameter (in metre). - * @param translationZMetre Value of the Translation_Z parameter (in metre). - * @param rotationXArcSecond Value of the Rotation_X parameter (in - * arc-second). - * @param rotationYArcSecond Value of the Rotation_Y parameter (in - * arc-second). - * @param rotationZArcSecond Value of the Rotation_Z parameter (in - * arc-second). - * @param scaleDifferencePPM Value of the Scale_Difference parameter (in - * parts-per-million). - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - */ -TransformationNNPtr Transformation::createPositionVector( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, double translationXMetre, - double translationYMetre, double translationZMetre, - double rotationXArcSecond, double rotationYArcSecond, - double rotationZArcSecond, double scaleDifferencePPM, - const std::vector &accuracies) { - - bool isGeocentric; - bool isGeog2D; - bool isGeog3D; - getTransformationType(sourceCRSIn, targetCRSIn, isGeocentric, isGeog2D, - isGeog3D); - return createSevenParamsTransform( - properties, - createMethodMapNameEPSGCode(useOperationMethodEPSGCodeIfPresent( - properties, - isGeocentric - ? EPSG_CODE_METHOD_POSITION_VECTOR_GEOCENTRIC - : isGeog2D ? EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_2D - : EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_3D)), - sourceCRSIn, targetCRSIn, translationXMetre, translationYMetre, - translationZMetre, rotationXArcSecond, rotationYArcSecond, - rotationZArcSecond, scaleDifferencePPM, accuracies); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation with Coordinate Frame Rotation method. - * - * This is similar to createPositionVector(), except that the sign of - * the rotation terms is inverted. - * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param translationXMetre Value of the Translation_X parameter (in metre). - * @param translationYMetre Value of the Translation_Y parameter (in metre). - * @param translationZMetre Value of the Translation_Z parameter (in metre). - * @param rotationXArcSecond Value of the Rotation_X parameter (in - * arc-second). - * @param rotationYArcSecond Value of the Rotation_Y parameter (in - * arc-second). - * @param rotationZArcSecond Value of the Rotation_Z parameter (in - * arc-second). - * @param scaleDifferencePPM Value of the Scale_Difference parameter (in - * parts-per-million). - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - */ -TransformationNNPtr Transformation::createCoordinateFrameRotation( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, double translationXMetre, - double translationYMetre, double translationZMetre, - double rotationXArcSecond, double rotationYArcSecond, - double rotationZArcSecond, double scaleDifferencePPM, - const std::vector &accuracies) { - bool isGeocentric; - bool isGeog2D; - bool isGeog3D; - getTransformationType(sourceCRSIn, targetCRSIn, isGeocentric, isGeog2D, - isGeog3D); - return createSevenParamsTransform( - properties, - createMethodMapNameEPSGCode(useOperationMethodEPSGCodeIfPresent( - properties, - isGeocentric - ? EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC - : isGeog2D ? EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D - : EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D)), - sourceCRSIn, targetCRSIn, translationXMetre, translationYMetre, - translationZMetre, rotationXArcSecond, rotationYArcSecond, - rotationZArcSecond, scaleDifferencePPM, accuracies); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static TransformationNNPtr createFifteenParamsTransform( - const util::PropertyMap &properties, - const util::PropertyMap &methodProperties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, double translationXMetre, - double translationYMetre, double translationZMetre, - double rotationXArcSecond, double rotationYArcSecond, - double rotationZArcSecond, double scaleDifferencePPM, - double rateTranslationX, double rateTranslationY, double rateTranslationZ, - double rateRotationX, double rateRotationY, double rateRotationZ, - double rateScaleDifference, double referenceEpochYear, - const std::vector &accuracies) { - return Transformation::create( - properties, sourceCRSIn, targetCRSIn, nullptr, methodProperties, - VectorOfParameters{ - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_X_AXIS_ROTATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Y_AXIS_ROTATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Z_AXIS_ROTATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_SCALE_DIFFERENCE), - - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_RATE_X_AXIS_TRANSLATION), - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_RATE_Y_AXIS_TRANSLATION), - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_RATE_Z_AXIS_TRANSLATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_RATE_X_AXIS_ROTATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_RATE_Y_AXIS_ROTATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_RATE_Z_AXIS_ROTATION), - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_RATE_SCALE_DIFFERENCE), - - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_REFERENCE_EPOCH), - }, - VectorOfValues{ - common::Length(translationXMetre), - common::Length(translationYMetre), - common::Length(translationZMetre), - common::Angle(rotationXArcSecond, - common::UnitOfMeasure::ARC_SECOND), - common::Angle(rotationYArcSecond, - common::UnitOfMeasure::ARC_SECOND), - common::Angle(rotationZArcSecond, - common::UnitOfMeasure::ARC_SECOND), - common::Scale(scaleDifferencePPM, - common::UnitOfMeasure::PARTS_PER_MILLION), - common::Measure(rateTranslationX, - common::UnitOfMeasure::METRE_PER_YEAR), - common::Measure(rateTranslationY, - common::UnitOfMeasure::METRE_PER_YEAR), - common::Measure(rateTranslationZ, - common::UnitOfMeasure::METRE_PER_YEAR), - common::Measure(rateRotationX, - common::UnitOfMeasure::ARC_SECOND_PER_YEAR), - common::Measure(rateRotationY, - common::UnitOfMeasure::ARC_SECOND_PER_YEAR), - common::Measure(rateRotationZ, - common::UnitOfMeasure::ARC_SECOND_PER_YEAR), - common::Measure(rateScaleDifference, - common::UnitOfMeasure::PPM_PER_YEAR), - common::Measure(referenceEpochYear, common::UnitOfMeasure::YEAR), - }, - accuracies); -} -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation with Time Dependent position vector - * transformation method. - * - * This is similar to createTimeDependentCoordinateFrameRotation(), except that - * the sign of - * the rotation terms is inverted. - * - * This method is defined as [EPSG:1053] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1053) - * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param translationXMetre Value of the Translation_X parameter (in metre). - * @param translationYMetre Value of the Translation_Y parameter (in metre). - * @param translationZMetre Value of the Translation_Z parameter (in metre). - * @param rotationXArcSecond Value of the Rotation_X parameter (in - * arc-second). - * @param rotationYArcSecond Value of the Rotation_Y parameter (in - * arc-second). - * @param rotationZArcSecond Value of the Rotation_Z parameter (in - * arc-second). - * @param scaleDifferencePPM Value of the Scale_Difference parameter (in - * parts-per-million). - * @param rateTranslationX Value of the rate of change of X-axis translation (in - * metre/year) - * @param rateTranslationY Value of the rate of change of Y-axis translation (in - * metre/year) - * @param rateTranslationZ Value of the rate of change of Z-axis translation (in - * metre/year) - * @param rateRotationX Value of the rate of change of X-axis rotation (in - * arc-second/year) - * @param rateRotationY Value of the rate of change of Y-axis rotation (in - * arc-second/year) - * @param rateRotationZ Value of the rate of change of Z-axis rotation (in - * arc-second/year) - * @param rateScaleDifference Value of the rate of change of scale difference - * (in PPM/year) - * @param referenceEpochYear Parameter reference epoch (in decimal year) - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - */ -TransformationNNPtr Transformation::createTimeDependentPositionVector( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, double translationXMetre, - double translationYMetre, double translationZMetre, - double rotationXArcSecond, double rotationYArcSecond, - double rotationZArcSecond, double scaleDifferencePPM, - double rateTranslationX, double rateTranslationY, double rateTranslationZ, - double rateRotationX, double rateRotationY, double rateRotationZ, - double rateScaleDifference, double referenceEpochYear, - const std::vector &accuracies) { - bool isGeocentric; - bool isGeog2D; - bool isGeog3D; - getTransformationType(sourceCRSIn, targetCRSIn, isGeocentric, isGeog2D, - isGeog3D); - return createFifteenParamsTransform( - properties, - createMethodMapNameEPSGCode(useOperationMethodEPSGCodeIfPresent( - properties, - isGeocentric - ? EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOCENTRIC - : isGeog2D - ? EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOGRAPHIC_2D - : EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOGRAPHIC_3D)), - sourceCRSIn, targetCRSIn, translationXMetre, translationYMetre, - translationZMetre, rotationXArcSecond, rotationYArcSecond, - rotationZArcSecond, scaleDifferencePPM, rateTranslationX, - rateTranslationY, rateTranslationZ, rateRotationX, rateRotationY, - rateRotationZ, rateScaleDifference, referenceEpochYear, accuracies); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation with Time Dependent Position coordinate - * frame rotation transformation method. - * - * This is similar to createTimeDependentPositionVector(), except that the sign - * of - * the rotation terms is inverted. - * - * This method is defined as [EPSG:1056] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1056) - * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param translationXMetre Value of the Translation_X parameter (in metre). - * @param translationYMetre Value of the Translation_Y parameter (in metre). - * @param translationZMetre Value of the Translation_Z parameter (in metre). - * @param rotationXArcSecond Value of the Rotation_X parameter (in - * arc-second). - * @param rotationYArcSecond Value of the Rotation_Y parameter (in - * arc-second). - * @param rotationZArcSecond Value of the Rotation_Z parameter (in - * arc-second). - * @param scaleDifferencePPM Value of the Scale_Difference parameter (in - * parts-per-million). - * @param rateTranslationX Value of the rate of change of X-axis translation (in - * metre/year) - * @param rateTranslationY Value of the rate of change of Y-axis translation (in - * metre/year) - * @param rateTranslationZ Value of the rate of change of Z-axis translation (in - * metre/year) - * @param rateRotationX Value of the rate of change of X-axis rotation (in - * arc-second/year) - * @param rateRotationY Value of the rate of change of Y-axis rotation (in - * arc-second/year) - * @param rateRotationZ Value of the rate of change of Z-axis rotation (in - * arc-second/year) - * @param rateScaleDifference Value of the rate of change of scale difference - * (in PPM/year) - * @param referenceEpochYear Parameter reference epoch (in decimal year) - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - * @throws InvalidOperation - */ -TransformationNNPtr Transformation::createTimeDependentCoordinateFrameRotation( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, double translationXMetre, - double translationYMetre, double translationZMetre, - double rotationXArcSecond, double rotationYArcSecond, - double rotationZArcSecond, double scaleDifferencePPM, - double rateTranslationX, double rateTranslationY, double rateTranslationZ, - double rateRotationX, double rateRotationY, double rateRotationZ, - double rateScaleDifference, double referenceEpochYear, - const std::vector &accuracies) { - - bool isGeocentric; - bool isGeog2D; - bool isGeog3D; - getTransformationType(sourceCRSIn, targetCRSIn, isGeocentric, isGeog2D, - isGeog3D); - return createFifteenParamsTransform( - properties, - createMethodMapNameEPSGCode(useOperationMethodEPSGCodeIfPresent( - properties, - isGeocentric - ? EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOCENTRIC - : isGeog2D - ? EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOGRAPHIC_2D - : EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOGRAPHIC_3D)), - sourceCRSIn, targetCRSIn, translationXMetre, translationYMetre, - translationZMetre, rotationXArcSecond, rotationYArcSecond, - rotationZArcSecond, scaleDifferencePPM, rateTranslationX, - rateTranslationY, rateTranslationZ, rateRotationX, rateRotationY, - rateRotationZ, rateScaleDifference, referenceEpochYear, accuracies); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static TransformationNNPtr _createMolodensky( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, int methodEPSGCode, - double translationXMetre, double translationYMetre, - double translationZMetre, double semiMajorAxisDifferenceMetre, - double flattingDifference, - const std::vector &accuracies) { - return Transformation::create( - properties, sourceCRSIn, targetCRSIn, nullptr, - createMethodMapNameEPSGCode(methodEPSGCode), - VectorOfParameters{ - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION), - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_SEMI_MAJOR_AXIS_DIFFERENCE), - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_FLATTENING_DIFFERENCE), - }, - createParams( - common::Length(translationXMetre), - common::Length(translationYMetre), - common::Length(translationZMetre), - common::Length(semiMajorAxisDifferenceMetre), - common::Measure(flattingDifference, common::UnitOfMeasure::NONE)), - accuracies); -} -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation with Molodensky method. - * - * @see createAbridgedMolodensky() for a related method. - * - * This method is defined as [EPSG:9604] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9604) - * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param translationXMetre Value of the Translation_X parameter (in metre). - * @param translationYMetre Value of the Translation_Y parameter (in metre). - * @param translationZMetre Value of the Translation_Z parameter (in metre). - * @param semiMajorAxisDifferenceMetre The difference between the semi-major - * axis values of the ellipsoids used in the target and source CRS (in metre). - * @param flattingDifference The difference between the flattening values of - * the ellipsoids used in the target and source CRS. - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - * @throws InvalidOperation - */ -TransformationNNPtr Transformation::createMolodensky( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, double translationXMetre, - double translationYMetre, double translationZMetre, - double semiMajorAxisDifferenceMetre, double flattingDifference, - const std::vector &accuracies) { - return _createMolodensky( - properties, sourceCRSIn, targetCRSIn, EPSG_CODE_METHOD_MOLODENSKY, - translationXMetre, translationYMetre, translationZMetre, - semiMajorAxisDifferenceMetre, flattingDifference, accuracies); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation with Abridged Molodensky method. - * - * @see createdMolodensky() for a related method. - * - * This method is defined as [EPSG:9605] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9605) - * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param translationXMetre Value of the Translation_X parameter (in metre). - * @param translationYMetre Value of the Translation_Y parameter (in metre). - * @param translationZMetre Value of the Translation_Z parameter (in metre). - * @param semiMajorAxisDifferenceMetre The difference between the semi-major - * axis values of the ellipsoids used in the target and source CRS (in metre). - * @param flattingDifference The difference between the flattening values of - * the ellipsoids used in the target and source CRS. - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - * @throws InvalidOperation - */ -TransformationNNPtr Transformation::createAbridgedMolodensky( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, double translationXMetre, - double translationYMetre, double translationZMetre, - double semiMajorAxisDifferenceMetre, double flattingDifference, - const std::vector &accuracies) { - return _createMolodensky(properties, sourceCRSIn, targetCRSIn, - EPSG_CODE_METHOD_ABRIDGED_MOLODENSKY, - translationXMetre, translationYMetre, - translationZMetre, semiMajorAxisDifferenceMetre, - flattingDifference, accuracies); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation from TOWGS84 parameters. - * - * This is a helper of createPositionVector() with the source CRS being the - * GeographicCRS of sourceCRSIn, and the target CRS being EPSG:4326 - * - * @param sourceCRSIn Source CRS. - * @param TOWGS84Parameters The vector of 3 double values (Translation_X,_Y,_Z) - * or 7 double values (Translation_X,_Y,_Z, Rotation_X,_Y,_Z, Scale_Difference) - * passed to createPositionVector() - * @return new Transformation. - * @throws InvalidOperation - */ -TransformationNNPtr Transformation::createTOWGS84( - const crs::CRSNNPtr &sourceCRSIn, - const std::vector &TOWGS84Parameters) // throw InvalidOperation -{ - if (TOWGS84Parameters.size() != 3 && TOWGS84Parameters.size() != 7) { - throw InvalidOperation( - "Invalid number of elements in TOWGS84Parameters"); - } - - crs::CRSPtr transformSourceCRS = sourceCRSIn->extractGeodeticCRS(); - if (!transformSourceCRS) { - throw InvalidOperation( - "Cannot find GeodeticCRS in sourceCRS of TOWGS84 transformation"); - } - - util::PropertyMap properties; - properties.set(common::IdentifiedObject::NAME_KEY, - concat("Transformation from ", transformSourceCRS->nameStr(), - " to WGS84")); - - auto targetCRS = - dynamic_cast(transformSourceCRS.get()) - ? util::nn_static_pointer_cast( - crs::GeographicCRS::EPSG_4326) - : util::nn_static_pointer_cast( - crs::GeodeticCRS::EPSG_4978); - - if (TOWGS84Parameters.size() == 3) { - return createGeocentricTranslations( - properties, NN_NO_CHECK(transformSourceCRS), targetCRS, - TOWGS84Parameters[0], TOWGS84Parameters[1], TOWGS84Parameters[2], - {}); - } - - return createPositionVector(properties, NN_NO_CHECK(transformSourceCRS), - targetCRS, TOWGS84Parameters[0], - TOWGS84Parameters[1], TOWGS84Parameters[2], - TOWGS84Parameters[3], TOWGS84Parameters[4], - TOWGS84Parameters[5], TOWGS84Parameters[6], {}); -} - -// --------------------------------------------------------------------------- -/** \brief Instantiate a transformation with NTv2 method. - * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param filename NTv2 filename. - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - */ -TransformationNNPtr Transformation::createNTv2( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, const std::string &filename, - const std::vector &accuracies) { - - return create(properties, sourceCRSIn, targetCRSIn, nullptr, - createMethodMapNameEPSGCode(EPSG_CODE_METHOD_NTV2), - VectorOfParameters{createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE)}, - VectorOfValues{ParameterValue::createFilename(filename)}, - accuracies); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static TransformationNNPtr _createGravityRelatedHeightToGeographic3D( - const util::PropertyMap &properties, bool inverse, - const crs::CRSNNPtr &sourceCRSIn, const crs::CRSNNPtr &targetCRSIn, - const crs::CRSPtr &interpolationCRSIn, const std::string &filename, - const std::vector &accuracies) { - - return Transformation::create( - properties, sourceCRSIn, targetCRSIn, interpolationCRSIn, - util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, - inverse ? INVERSE_OF + PROJ_WKT2_NAME_METHOD_HEIGHT_TO_GEOG3D - : PROJ_WKT2_NAME_METHOD_HEIGHT_TO_GEOG3D), - VectorOfParameters{createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_GEOID_CORRECTION_FILENAME)}, - VectorOfValues{ParameterValue::createFilename(filename)}, accuracies); -} -//! @endcond - -// --------------------------------------------------------------------------- -/** \brief Instantiate a transformation from GravityRelatedHeight to - * Geographic3D - * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param interpolationCRSIn Interpolation CRS. (might be null) - * @param filename GRID filename. - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - */ -TransformationNNPtr Transformation::createGravityRelatedHeightToGeographic3D( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, const crs::CRSPtr &interpolationCRSIn, - const std::string &filename, - const std::vector &accuracies) { - - return _createGravityRelatedHeightToGeographic3D( - properties, false, sourceCRSIn, targetCRSIn, interpolationCRSIn, - filename, accuracies); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation with method VERTCON - * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param filename GRID filename. - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - */ -TransformationNNPtr Transformation::createVERTCON( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, const std::string &filename, - const std::vector &accuracies) { - - return create(properties, sourceCRSIn, targetCRSIn, nullptr, - createMethodMapNameEPSGCode(EPSG_CODE_METHOD_VERTCON), - VectorOfParameters{createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_VERTICAL_OFFSET_FILE)}, - VectorOfValues{ParameterValue::createFilename(filename)}, - accuracies); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static inline std::vector -buildAccuracyZero() { - return std::vector{ - metadata::PositionalAccuracy::create("0")}; -} - -// --------------------------------------------------------------------------- - -//! @endcond - -/** \brief Instantiate a transformation with method Longitude rotation - * - * This method is defined as [EPSG:9601] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9601) - * * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param offset Longitude offset to add. - * @return new Transformation. - */ -TransformationNNPtr Transformation::createLongitudeRotation( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, const common::Angle &offset) { - - return create( - properties, sourceCRSIn, targetCRSIn, nullptr, - createMethodMapNameEPSGCode(EPSG_CODE_METHOD_LONGITUDE_ROTATION), - VectorOfParameters{ - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET)}, - VectorOfValues{ParameterValue::create(offset)}, buildAccuracyZero()); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -bool Transformation::isLongitudeRotation() const { - return method()->getEPSGCode() == EPSG_CODE_METHOD_LONGITUDE_ROTATION; -} - -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation with method Geographic 2D offsets - * - * This method is defined as [EPSG:9619] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9619) - * * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param offsetLat Latitude offset to add. - * @param offsetLon Longitude offset to add. - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - */ -TransformationNNPtr Transformation::createGeographic2DOffsets( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, const common::Angle &offsetLat, - const common::Angle &offsetLon, - const std::vector &accuracies) { - return create( - properties, sourceCRSIn, targetCRSIn, nullptr, - createMethodMapNameEPSGCode(EPSG_CODE_METHOD_GEOGRAPHIC2D_OFFSETS), - VectorOfParameters{ - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LATITUDE_OFFSET), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET)}, - VectorOfValues{offsetLat, offsetLon}, accuracies); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation with method Geographic 3D offsets - * - * This method is defined as [EPSG:9660] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9660) - * * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param offsetLat Latitude offset to add. - * @param offsetLon Longitude offset to add. - * @param offsetHeight Height offset to add. - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - */ -TransformationNNPtr Transformation::createGeographic3DOffsets( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, const common::Angle &offsetLat, - const common::Angle &offsetLon, const common::Length &offsetHeight, - const std::vector &accuracies) { - return create( - properties, sourceCRSIn, targetCRSIn, nullptr, - createMethodMapNameEPSGCode(EPSG_CODE_METHOD_GEOGRAPHIC3D_OFFSETS), - VectorOfParameters{ - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LATITUDE_OFFSET), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_VERTICAL_OFFSET)}, - VectorOfValues{offsetLat, offsetLon, offsetHeight}, accuracies); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation with method Geographic 2D with - * height - * offsets - * - * This method is defined as [EPSG:9618] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9618) - * * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param offsetLat Latitude offset to add. - * @param offsetLon Longitude offset to add. - * @param offsetHeight Geoid undulation to add. - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - */ -TransformationNNPtr Transformation::createGeographic2DWithHeightOffsets( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, const common::Angle &offsetLat, - const common::Angle &offsetLon, const common::Length &offsetHeight, - const std::vector &accuracies) { - return create( - properties, sourceCRSIn, targetCRSIn, nullptr, - createMethodMapNameEPSGCode( - EPSG_CODE_METHOD_GEOGRAPHIC2D_WITH_HEIGHT_OFFSETS), - VectorOfParameters{ - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LATITUDE_OFFSET), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET), - createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_GEOID_UNDULATION)}, - VectorOfValues{offsetLat, offsetLon, offsetHeight}, accuracies); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation with method Vertical Offset. - * - * This method is defined as [EPSG:9616] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9616) - * * - * @param properties See \ref general_properties of the Transformation. - * At minimum the name should be defined. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param offsetHeight Geoid undulation to add. - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - */ -TransformationNNPtr Transformation::createVerticalOffset( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, const common::Length &offsetHeight, - const std::vector &accuracies) { - return create(properties, sourceCRSIn, targetCRSIn, nullptr, - createMethodMapNameEPSGCode(EPSG_CODE_METHOD_VERTICAL_OFFSET), - VectorOfParameters{createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_VERTICAL_OFFSET)}, - VectorOfValues{offsetHeight}, accuracies); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a transformation based on the Change of Vertical Unit - * method. - * - * This method is defined as [EPSG:1069] - * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1069) - * - * @param properties See \ref general_properties of the conversion. If the name - * is not provided, it is automatically set. - * @param sourceCRSIn Source CRS. - * @param targetCRSIn Target CRS. - * @param factor Conversion factor - * @param accuracies Vector of positional accuracy (might be empty). - * @return a new Transformation. - */ -TransformationNNPtr Transformation::createChangeVerticalUnit( - const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, - const crs::CRSNNPtr &targetCRSIn, const common::Scale &factor, - const std::vector &accuracies) { - return create( - properties, sourceCRSIn, targetCRSIn, nullptr, - createMethodMapNameEPSGCode(EPSG_CODE_METHOD_CHANGE_VERTICAL_UNIT), - VectorOfParameters{ - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR), - }, - VectorOfValues{ - factor, - }, - accuracies); -} - -// --------------------------------------------------------------------------- - -static util::PropertyMap -createPropertiesForInverse(const CoordinateOperation *op, bool derivedFrom, - bool approximateInversion) { - assert(op); - util::PropertyMap map; - - // The domain(s) are unchanged by the inverse operation - addDomains(map, op); - - const std::string &forwardName = op->nameStr(); - - // Forge a name for the inverse, either from the forward name, or - // from the source and target CRS names - const char *opType; - if (starts_with(forwardName, BALLPARK_GEOCENTRIC_TRANSLATION)) { - opType = BALLPARK_GEOCENTRIC_TRANSLATION; - } else if (starts_with(forwardName, BALLPARK_GEOGRAPHIC_OFFSET)) { - opType = BALLPARK_GEOGRAPHIC_OFFSET; - } else if (starts_with(forwardName, NULL_GEOGRAPHIC_OFFSET)) { - opType = NULL_GEOGRAPHIC_OFFSET; - } else if (starts_with(forwardName, NULL_GEOCENTRIC_TRANSLATION)) { - opType = NULL_GEOCENTRIC_TRANSLATION; - } else if (dynamic_cast(op) || - starts_with(forwardName, "Transformation from ")) { - opType = "Transformation"; - } else if (dynamic_cast(op)) { - opType = "Conversion"; - } else { - opType = "Operation"; - } - - auto sourceCRS = op->sourceCRS(); - auto targetCRS = op->targetCRS(); - std::string name; - if (!forwardName.empty()) { - if (dynamic_cast(op) == nullptr && - dynamic_cast(op) == nullptr && - (starts_with(forwardName, INVERSE_OF) || - forwardName.find(" + ") != std::string::npos)) { - std::vector tokens; - std::string curToken; - bool inString = false; - for (size_t i = 0; i < forwardName.size(); ++i) { - if (inString) { - curToken += forwardName[i]; - if (forwardName[i] == '\'') { - inString = false; - } - } else if (i + 3 < forwardName.size() && - memcmp(&forwardName[i], " + ", 3) == 0) { - tokens.push_back(curToken); - curToken.clear(); - i += 2; - } else if (forwardName[i] == '\'') { - inString = true; - curToken += forwardName[i]; - } else { - curToken += forwardName[i]; - } - } - if (!curToken.empty()) { - tokens.push_back(curToken); - } - for (size_t i = tokens.size(); i > 0;) { - i--; - if (!name.empty()) { - name += " + "; - } - if (starts_with(tokens[i], INVERSE_OF)) { - name += tokens[i].substr(INVERSE_OF.size()); - } else if (tokens[i] == AXIS_ORDER_CHANGE_2D_NAME || - tokens[i] == AXIS_ORDER_CHANGE_3D_NAME) { - name += tokens[i]; - } else { - name += INVERSE_OF + tokens[i]; - } - } - } else if (!sourceCRS || !targetCRS || - forwardName != buildOpName(opType, sourceCRS, targetCRS)) { - if (forwardName.find(" + ") != std::string::npos) { - name = INVERSE_OF + '\'' + forwardName + '\''; - } else { - name = INVERSE_OF + forwardName; - } - } - } - if (name.empty() && sourceCRS && targetCRS) { - name = buildOpName(opType, targetCRS, sourceCRS); - } - if (approximateInversion) { - name += " (approx. inversion)"; - } - - if (!name.empty()) { - map.set(common::IdentifiedObject::NAME_KEY, name); - } - - const std::string &remarks = op->remarks(); - if (!remarks.empty()) { - map.set(common::IdentifiedObject::REMARKS_KEY, remarks); - } - - addModifiedIdentifier(map, op, true, derivedFrom); - - const auto so = dynamic_cast(op); - if (so) { - const int soMethodEPSGCode = so->method()->getEPSGCode(); - if (soMethodEPSGCode > 0) { - map.set("OPERATION_METHOD_EPSG_CODE", soMethodEPSGCode); - } - } - - return map; -} - -// --------------------------------------------------------------------------- - -static bool isTimeDependent(const std::string &methodName) { - return ci_find(methodName, "Time dependent") != std::string::npos || - ci_find(methodName, "Time-dependent") != std::string::npos; -} - -// --------------------------------------------------------------------------- - -// to avoid -0... -static double negate(double val) { - if (val != 0) { - return -val; - } - return 0.0; -} - -// --------------------------------------------------------------------------- - -static CoordinateOperationPtr -createApproximateInverseIfPossible(const Transformation *op) { - bool sevenParamsTransform = false; - bool fifteenParamsTransform = false; - const auto &method = op->method(); - const auto &methodName = method->nameStr(); - const int methodEPSGCode = method->getEPSGCode(); - const auto paramCount = op->parameterValues().size(); - const bool isPositionVector = - ci_find(methodName, "Position Vector") != std::string::npos; - const bool isCoordinateFrame = - ci_find(methodName, "Coordinate Frame") != std::string::npos; - - // See end of "2.4.3.3 Helmert 7-parameter transformations" - // in EPSG 7-2 guidance - // For practical purposes, the inverse of 7- or 15-parameters Helmert - // can be obtained by using the forward method with all parameters - // negated - // (except reference epoch!) - // So for WKT export use that. But for PROJ string, we use the +inv flag - // so as to get "perfect" round-tripability. - if ((paramCount == 7 && isCoordinateFrame && - !isTimeDependent(methodName)) || - methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC || - methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D || - methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D) { - sevenParamsTransform = true; - } else if ( - (paramCount == 15 && isCoordinateFrame && - isTimeDependent(methodName)) || - methodEPSGCode == - EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOCENTRIC || - methodEPSGCode == - EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOGRAPHIC_2D || - methodEPSGCode == - EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOGRAPHIC_3D) { - fifteenParamsTransform = true; - } else if ((paramCount == 7 && isPositionVector && - !isTimeDependent(methodName)) || - methodEPSGCode == EPSG_CODE_METHOD_POSITION_VECTOR_GEOCENTRIC || - methodEPSGCode == - EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_2D || - methodEPSGCode == - EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_3D) { - sevenParamsTransform = true; - } else if ( - (paramCount == 15 && isPositionVector && isTimeDependent(methodName)) || - methodEPSGCode == - EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOCENTRIC || - methodEPSGCode == - EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOGRAPHIC_2D || - methodEPSGCode == - EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOGRAPHIC_3D) { - fifteenParamsTransform = true; - } - if (sevenParamsTransform || fifteenParamsTransform) { - double neg_x = negate(op->parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION)); - double neg_y = negate(op->parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION)); - double neg_z = negate(op->parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION)); - double neg_rx = negate( - op->parameterValueNumeric(EPSG_CODE_PARAMETER_X_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND)); - double neg_ry = negate( - op->parameterValueNumeric(EPSG_CODE_PARAMETER_Y_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND)); - double neg_rz = negate( - op->parameterValueNumeric(EPSG_CODE_PARAMETER_Z_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND)); - double neg_scaleDiff = negate(op->parameterValueNumeric( - EPSG_CODE_PARAMETER_SCALE_DIFFERENCE, - common::UnitOfMeasure::PARTS_PER_MILLION)); - auto methodProperties = util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, methodName); - int method_epsg_code = method->getEPSGCode(); - if (method_epsg_code) { - methodProperties - .set(metadata::Identifier::CODESPACE_KEY, - metadata::Identifier::EPSG) - .set(metadata::Identifier::CODE_KEY, method_epsg_code); - } - if (fifteenParamsTransform) { - double neg_rate_x = negate(op->parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_X_AXIS_TRANSLATION, - common::UnitOfMeasure::METRE_PER_YEAR)); - double neg_rate_y = negate(op->parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_Y_AXIS_TRANSLATION, - common::UnitOfMeasure::METRE_PER_YEAR)); - double neg_rate_z = negate(op->parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_Z_AXIS_TRANSLATION, - common::UnitOfMeasure::METRE_PER_YEAR)); - double neg_rate_rx = negate(op->parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_X_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND_PER_YEAR)); - double neg_rate_ry = negate(op->parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_Y_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND_PER_YEAR)); - double neg_rate_rz = negate(op->parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_Z_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND_PER_YEAR)); - double neg_rate_scaleDiff = negate(op->parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_SCALE_DIFFERENCE, - common::UnitOfMeasure::PPM_PER_YEAR)); - double referenceEpochYear = - op->parameterValueNumeric(EPSG_CODE_PARAMETER_REFERENCE_EPOCH, - common::UnitOfMeasure::YEAR); - return util::nn_static_pointer_cast( - createFifteenParamsTransform( - createPropertiesForInverse(op, false, true), - methodProperties, op->targetCRS(), op->sourceCRS(), - neg_x, neg_y, neg_z, neg_rx, neg_ry, neg_rz, - neg_scaleDiff, neg_rate_x, neg_rate_y, neg_rate_z, - neg_rate_rx, neg_rate_ry, neg_rate_rz, - neg_rate_scaleDiff, referenceEpochYear, - op->coordinateOperationAccuracies())) - .as_nullable(); - } else { - return util::nn_static_pointer_cast( - createSevenParamsTransform( - createPropertiesForInverse(op, false, true), - methodProperties, op->targetCRS(), op->sourceCRS(), - neg_x, neg_y, neg_z, neg_rx, neg_ry, neg_rz, - neg_scaleDiff, op->coordinateOperationAccuracies())) - .as_nullable(); - } - } - - return nullptr; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -TransformationNNPtr -Transformation::Private::registerInv(const Transformation *thisIn, - TransformationNNPtr invTransform) { - invTransform->d->forwardOperation_ = thisIn->shallowClone().as_nullable(); - invTransform->setHasBallparkTransformation( - thisIn->hasBallparkTransformation()); - return invTransform; -} -//! @endcond - -// --------------------------------------------------------------------------- - -CoordinateOperationNNPtr Transformation::inverse() const { - return inverseAsTransformation(); -} - -// --------------------------------------------------------------------------- - -TransformationNNPtr Transformation::inverseAsTransformation() const { - - if (d->forwardOperation_) { - return NN_NO_CHECK(d->forwardOperation_); - } - const auto &l_method = method(); - const auto &methodName = l_method->nameStr(); - const int methodEPSGCode = l_method->getEPSGCode(); - const auto &l_sourceCRS = sourceCRS(); - const auto &l_targetCRS = targetCRS(); - - // For geocentric translation, the inverse is exactly the negation of - // the parameters. - if (ci_find(methodName, "Geocentric translations") != std::string::npos || - methodEPSGCode == EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOCENTRIC || - methodEPSGCode == - EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_2D || - methodEPSGCode == - EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_3D) { - double x = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION); - double y = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION); - double z = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION); - auto properties = createPropertiesForInverse(this, false, false); - return Private::registerInv( - this, create(properties, l_targetCRS, l_sourceCRS, nullptr, - createMethodMapNameEPSGCode( - useOperationMethodEPSGCodeIfPresent( - properties, methodEPSGCode)), - VectorOfParameters{ - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION), - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION), - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION), - }, - createParams(common::Length(negate(x)), - common::Length(negate(y)), - common::Length(negate(z))), - coordinateOperationAccuracies())); - } - - if (methodEPSGCode == EPSG_CODE_METHOD_MOLODENSKY || - methodEPSGCode == EPSG_CODE_METHOD_ABRIDGED_MOLODENSKY) { - double x = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION); - double y = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION); - double z = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION); - double da = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_SEMI_MAJOR_AXIS_DIFFERENCE); - double df = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_FLATTENING_DIFFERENCE); - - if (methodEPSGCode == EPSG_CODE_METHOD_ABRIDGED_MOLODENSKY) { - return Private::registerInv( - this, - createAbridgedMolodensky( - createPropertiesForInverse(this, false, false), l_targetCRS, - l_sourceCRS, negate(x), negate(y), negate(z), negate(da), - negate(df), coordinateOperationAccuracies())); - } else { - return Private::registerInv( - this, - createMolodensky(createPropertiesForInverse(this, false, false), - l_targetCRS, l_sourceCRS, negate(x), negate(y), - negate(z), negate(da), negate(df), - coordinateOperationAccuracies())); - } - } - - if (isLongitudeRotation()) { - auto offset = - parameterValueMeasure(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET); - const common::Angle newOffset(negate(offset.value()), offset.unit()); - return Private::registerInv( - this, createLongitudeRotation( - createPropertiesForInverse(this, false, false), - l_targetCRS, l_sourceCRS, newOffset)); - } - - if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC2D_OFFSETS) { - auto offsetLat = - parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_OFFSET); - const common::Angle newOffsetLat(negate(offsetLat.value()), - offsetLat.unit()); - - auto offsetLong = - parameterValueMeasure(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET); - const common::Angle newOffsetLong(negate(offsetLong.value()), - offsetLong.unit()); - - return Private::registerInv( - this, createGeographic2DOffsets( - createPropertiesForInverse(this, false, false), - l_targetCRS, l_sourceCRS, newOffsetLat, newOffsetLong, - coordinateOperationAccuracies())); - } - - if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC3D_OFFSETS) { - auto offsetLat = - parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_OFFSET); - const common::Angle newOffsetLat(negate(offsetLat.value()), - offsetLat.unit()); - - auto offsetLong = - parameterValueMeasure(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET); - const common::Angle newOffsetLong(negate(offsetLong.value()), - offsetLong.unit()); - - auto offsetHeight = - parameterValueMeasure(EPSG_CODE_PARAMETER_VERTICAL_OFFSET); - const common::Length newOffsetHeight(negate(offsetHeight.value()), - offsetHeight.unit()); - - return Private::registerInv( - this, createGeographic3DOffsets( - createPropertiesForInverse(this, false, false), - l_targetCRS, l_sourceCRS, newOffsetLat, newOffsetLong, - newOffsetHeight, coordinateOperationAccuracies())); - } - - if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC2D_WITH_HEIGHT_OFFSETS) { - auto offsetLat = - parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_OFFSET); - const common::Angle newOffsetLat(negate(offsetLat.value()), - offsetLat.unit()); - - auto offsetLong = - parameterValueMeasure(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET); - const common::Angle newOffsetLong(negate(offsetLong.value()), - offsetLong.unit()); - - auto offsetHeight = - parameterValueMeasure(EPSG_CODE_PARAMETER_GEOID_UNDULATION); - const common::Length newOffsetHeight(negate(offsetHeight.value()), - offsetHeight.unit()); - - return Private::registerInv( - this, createGeographic2DWithHeightOffsets( - createPropertiesForInverse(this, false, false), - l_targetCRS, l_sourceCRS, newOffsetLat, newOffsetLong, - newOffsetHeight, coordinateOperationAccuracies())); - } - - if (methodEPSGCode == EPSG_CODE_METHOD_VERTICAL_OFFSET) { - - auto offsetHeight = - parameterValueMeasure(EPSG_CODE_PARAMETER_VERTICAL_OFFSET); - const common::Length newOffsetHeight(negate(offsetHeight.value()), - offsetHeight.unit()); - - return Private::registerInv( - this, - createVerticalOffset(createPropertiesForInverse(this, false, false), - l_targetCRS, l_sourceCRS, newOffsetHeight, - coordinateOperationAccuracies())); - } - - if (methodEPSGCode == EPSG_CODE_METHOD_CHANGE_VERTICAL_UNIT) { - const double convFactor = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR); - return Private::registerInv( - this, createChangeVerticalUnit( - createPropertiesForInverse(this, false, false), - l_targetCRS, l_sourceCRS, common::Scale(1.0 / convFactor), - coordinateOperationAccuracies())); - } - -#ifdef notdef - // We don't need that currently, but we might... - if (methodEPSGCode == EPSG_CODE_METHOD_HEIGHT_DEPTH_REVERSAL) { - return Private::registerInv( - this, - createHeightDepthReversal( - createPropertiesForInverse(this, false, false), l_targetCRS, - l_sourceCRS, coordinateOperationAccuracies())); - } -#endif - - return InverseTransformation::create(NN_NO_CHECK( - util::nn_dynamic_pointer_cast(shared_from_this()))); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -// --------------------------------------------------------------------------- - -InverseTransformation::InverseTransformation(const TransformationNNPtr &forward) - : Transformation( - forward->targetCRS(), forward->sourceCRS(), - forward->interpolationCRS(), - OperationMethod::create(createPropertiesForInverse(forward->method()), - forward->method()->parameters()), - forward->parameterValues(), forward->coordinateOperationAccuracies()), - InverseCoordinateOperation(forward, true) { - setPropertiesFromForward(); -} - -// --------------------------------------------------------------------------- - -InverseTransformation::~InverseTransformation() = default; - -// --------------------------------------------------------------------------- - -TransformationNNPtr -InverseTransformation::create(const TransformationNNPtr &forward) { - auto conv = util::nn_make_shared(forward); - conv->assignSelf(conv); - return conv; -} - -// --------------------------------------------------------------------------- - -TransformationNNPtr InverseTransformation::inverseAsTransformation() const { - return NN_NO_CHECK( - util::nn_dynamic_pointer_cast(forwardOperation_)); -} - -// --------------------------------------------------------------------------- - -void InverseTransformation::_exportToWKT(io::WKTFormatter *formatter) const { - - auto approxInverse = createApproximateInverseIfPossible( - util::nn_dynamic_pointer_cast(forwardOperation_).get()); - if (approxInverse) { - approxInverse->_exportToWKT(formatter); - } else { - Transformation::_exportToWKT(formatter); - } -} - -// --------------------------------------------------------------------------- - -CoordinateOperationNNPtr InverseTransformation::_shallowClone() const { - auto op = InverseTransformation::nn_make_shared( - inverseAsTransformation()->shallowClone()); - op->assignSelf(op); - op->setCRSs(this, false); - return util::nn_static_pointer_cast(op); -} - -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -void Transformation::_exportToWKT(io::WKTFormatter *formatter) const { - exportTransformationToWKT(formatter); -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -void Transformation::_exportToJSON( - io::JSONFormatter *formatter) const // throw(FormattingException) -{ - auto writer = formatter->writer(); - auto objectContext(formatter->MakeObjectContext( - formatter->abridgedTransformation() ? "AbridgedTransformation" - : "Transformation", - !identifiers().empty())); - - writer->AddObjKey("name"); - auto l_name = nameStr(); - if (l_name.empty()) { - writer->Add("unnamed"); - } else { - writer->Add(l_name); - } - - if (!formatter->abridgedTransformation()) { - writer->AddObjKey("source_crs"); - formatter->setAllowIDInImmediateChild(); - sourceCRS()->_exportToJSON(formatter); - - writer->AddObjKey("target_crs"); - formatter->setAllowIDInImmediateChild(); - targetCRS()->_exportToJSON(formatter); - - const auto &l_interpolationCRS = interpolationCRS(); - if (l_interpolationCRS) { - writer->AddObjKey("interpolation_crs"); - formatter->setAllowIDInImmediateChild(); - l_interpolationCRS->_exportToJSON(formatter); - } - } - - writer->AddObjKey("method"); - formatter->setOmitTypeInImmediateChild(); - formatter->setAllowIDInImmediateChild(); - method()->_exportToJSON(formatter); - - writer->AddObjKey("parameters"); - { - auto parametersContext(writer->MakeArrayContext(false)); - for (const auto &genOpParamvalue : parameterValues()) { - formatter->setAllowIDInImmediateChild(); - formatter->setOmitTypeInImmediateChild(); - genOpParamvalue->_exportToJSON(formatter); - } - } - - if (!formatter->abridgedTransformation()) { - if (!coordinateOperationAccuracies().empty()) { - writer->AddObjKey("accuracy"); - writer->Add(coordinateOperationAccuracies()[0]->value()); - } - } - - if (formatter->abridgedTransformation()) { - if (formatter->outputId()) { - formatID(formatter); - } - } else { - ObjectUsage::baseExportToJSON(formatter); - } -} - -//! @endcond - -// --------------------------------------------------------------------------- - -static void exportSourceCRSAndTargetCRSToWKT(const CoordinateOperation *co, - io::WKTFormatter *formatter) { - auto l_sourceCRS = co->sourceCRS(); - assert(l_sourceCRS); - auto l_targetCRS = co->targetCRS(); - assert(l_targetCRS); - const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; - const bool canExportCRSId = - (isWKT2 && formatter->use2019Keywords() && - !(formatter->idOnTopLevelOnly() && formatter->topLevelHasId())); - - const bool hasDomains = !co->domains().empty(); - if (hasDomains) { - formatter->pushDisableUsage(); - } - - formatter->startNode(io::WKTConstants::SOURCECRS, false); - if (canExportCRSId && !l_sourceCRS->identifiers().empty()) { - // fake that top node has no id, so that the sourceCRS id is - // considered - formatter->pushHasId(false); - l_sourceCRS->_exportToWKT(formatter); - formatter->popHasId(); - } else { - l_sourceCRS->_exportToWKT(formatter); - } - formatter->endNode(); - - formatter->startNode(io::WKTConstants::TARGETCRS, false); - if (canExportCRSId && !l_targetCRS->identifiers().empty()) { - // fake that top node has no id, so that the targetCRS id is - // considered - formatter->pushHasId(false); - l_targetCRS->_exportToWKT(formatter); - formatter->popHasId(); - } else { - l_targetCRS->_exportToWKT(formatter); - } - formatter->endNode(); - - if (hasDomains) { - formatter->popDisableUsage(); - } -} - -// --------------------------------------------------------------------------- - -void SingleOperation::exportTransformationToWKT( - io::WKTFormatter *formatter) const { - const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; - if (!isWKT2) { - throw io::FormattingException( - "Transformation can only be exported to WKT2"); - } - - if (formatter->abridgedTransformation()) { - formatter->startNode(io::WKTConstants::ABRIDGEDTRANSFORMATION, - !identifiers().empty()); - } else { - formatter->startNode(io::WKTConstants::COORDINATEOPERATION, - !identifiers().empty()); - } - - formatter->addQuotedString(nameStr()); - - if (formatter->use2019Keywords()) { - const auto &version = operationVersion(); - if (version.has_value()) { - formatter->startNode(io::WKTConstants::VERSION, false); - formatter->addQuotedString(*version); - formatter->endNode(); - } - } - - if (!formatter->abridgedTransformation()) { - exportSourceCRSAndTargetCRSToWKT(this, formatter); - } - - method()->_exportToWKT(formatter); - - for (const auto ¶mValue : parameterValues()) { - paramValue->_exportToWKT(formatter, nullptr); - } - - if (!formatter->abridgedTransformation()) { - if (interpolationCRS()) { - formatter->startNode(io::WKTConstants::INTERPOLATIONCRS, false); - interpolationCRS()->_exportToWKT(formatter); - formatter->endNode(); - } - - if (!coordinateOperationAccuracies().empty()) { - formatter->startNode(io::WKTConstants::OPERATIONACCURACY, false); - formatter->add(coordinateOperationAccuracies()[0]->value()); - formatter->endNode(); - } - } - - ObjectUsage::baseExportToWKT(formatter); - formatter->endNode(); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static const std::string nullString; - -static const std::string &_getNTv2Filename(const Transformation *op, - bool allowInverse) { - - const auto &l_method = op->method(); - if (l_method->getEPSGCode() == EPSG_CODE_METHOD_NTV2 || - (allowInverse && - ci_equal(l_method->nameStr(), INVERSE_OF + EPSG_NAME_METHOD_NTV2))) { - const auto &fileParameter = op->parameterValue( - EPSG_NAME_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE, - EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE); - if (fileParameter && - fileParameter->type() == ParameterValue::Type::FILENAME) { - return fileParameter->valueFile(); - } - } - return nullString; -} -//! @endcond - -// --------------------------------------------------------------------------- -//! @cond Doxygen_Suppress -const std::string &Transformation::getNTv2Filename() const { - - return _getNTv2Filename(this, false); -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static const std::string &_getNTv1Filename(const Transformation *op, - bool allowInverse) { - - const auto &l_method = op->method(); - const auto &methodName = l_method->nameStr(); - if (l_method->getEPSGCode() == EPSG_CODE_METHOD_NTV1 || - (allowInverse && - ci_equal(methodName, INVERSE_OF + EPSG_NAME_METHOD_NTV1))) { - const auto &fileParameter = op->parameterValue( - EPSG_NAME_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE, - EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE); - if (fileParameter && - fileParameter->type() == ParameterValue::Type::FILENAME) { - return fileParameter->valueFile(); - } - } - return nullString; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static const std::string &_getCTABLE2Filename(const Transformation *op, - bool allowInverse) { - const auto &l_method = op->method(); - const auto &methodName = l_method->nameStr(); - if (ci_equal(methodName, PROJ_WKT2_NAME_METHOD_CTABLE2) || - (allowInverse && - ci_equal(methodName, INVERSE_OF + PROJ_WKT2_NAME_METHOD_CTABLE2))) { - const auto &fileParameter = op->parameterValue( - EPSG_NAME_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE, - EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE); - if (fileParameter && - fileParameter->type() == ParameterValue::Type::FILENAME) { - return fileParameter->valueFile(); - } - } - return nullString; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static const std::string & -_getHorizontalShiftGTIFFFilename(const Transformation *op, bool allowInverse) { - const auto &l_method = op->method(); - const auto &methodName = l_method->nameStr(); - if (ci_equal(methodName, PROJ_WKT2_NAME_METHOD_HORIZONTAL_SHIFT_GTIFF) || - (allowInverse && - ci_equal(methodName, - INVERSE_OF + PROJ_WKT2_NAME_METHOD_HORIZONTAL_SHIFT_GTIFF))) { - const auto &fileParameter = op->parameterValue( - EPSG_NAME_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE, - EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE); - if (fileParameter && - fileParameter->type() == ParameterValue::Type::FILENAME) { - return fileParameter->valueFile(); - } - } - return nullString; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static const std::string & -_getGeocentricTranslationFilename(const Transformation *op, bool allowInverse) { - - const auto &l_method = op->method(); - const auto &methodName = l_method->nameStr(); - if (l_method->getEPSGCode() == - EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_BY_GRID_INTERPOLATION_IGN || - (allowInverse && - ci_equal( - methodName, - INVERSE_OF + - EPSG_NAME_METHOD_GEOCENTRIC_TRANSLATION_BY_GRID_INTERPOLATION_IGN))) { - const auto &fileParameter = - op->parameterValue(EPSG_NAME_PARAMETER_GEOCENTRIC_TRANSLATION_FILE, - EPSG_CODE_PARAMETER_GEOCENTRIC_TRANSLATION_FILE); - if (fileParameter && - fileParameter->type() == ParameterValue::Type::FILENAME) { - return fileParameter->valueFile(); - } - } - return nullString; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static const std::string & -_getHeightToGeographic3DFilename(const Transformation *op, bool allowInverse) { - - const auto &methodName = op->method()->nameStr(); - - if (ci_equal(methodName, PROJ_WKT2_NAME_METHOD_HEIGHT_TO_GEOG3D) || - (allowInverse && - ci_equal(methodName, - INVERSE_OF + PROJ_WKT2_NAME_METHOD_HEIGHT_TO_GEOG3D))) { - const auto &fileParameter = - op->parameterValue(EPSG_NAME_PARAMETER_GEOID_CORRECTION_FILENAME, - EPSG_CODE_PARAMETER_GEOID_CORRECTION_FILENAME); - if (fileParameter && - fileParameter->type() == ParameterValue::Type::FILENAME) { - return fileParameter->valueFile(); - } - } - return nullString; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static bool -isGeographic3DToGravityRelatedHeight(const OperationMethodNNPtr &method, - bool allowInverse) { - const auto &methodName = method->nameStr(); - static const char *const methodCodes[] = { - "1025", // Geographic3D to GravityRelatedHeight (EGM2008) - "1030", // Geographic3D to GravityRelatedHeight (NZgeoid) - "1045", // Geographic3D to GravityRelatedHeight (OSGM02-Ire) - "1047", // Geographic3D to GravityRelatedHeight (Gravsoft) - "1048", // Geographic3D to GravityRelatedHeight (Ausgeoid v2) - "1050", // Geographic3D to GravityRelatedHeight (CI) - "1059", // Geographic3D to GravityRelatedHeight (PNG) - "1060", // Geographic3D to GravityRelatedHeight (CGG2013) - "1072", // Geographic3D to GravityRelatedHeight (OSGM15-Ire) - "1073", // Geographic3D to GravityRelatedHeight (IGN2009) - "1081", // Geographic3D to GravityRelatedHeight (BEV AT) - "1083", // Geog3D to Geog2D+Vertical (AUSGeoid v2) - "9661", // Geographic3D to GravityRelatedHeight (EGM) - "9662", // Geographic3D to GravityRelatedHeight (Ausgeoid98) - "9663", // Geographic3D to GravityRelatedHeight (OSGM-GB) - "9664", // Geographic3D to GravityRelatedHeight (IGN1997) - "9665", // Geographic3D to GravityRelatedHeight (US .gtx) - "9635", // Geog3D to Geog2D+GravityRelatedHeight (US .gtx) - }; - - if (ci_find(methodName, "Geographic3D to GravityRelatedHeight") == 0) { - return true; - } - if (allowInverse && - ci_find(methodName, - INVERSE_OF + "Geographic3D to GravityRelatedHeight") == 0) { - return true; - } - - for (const auto &code : methodCodes) { - for (const auto &idSrc : method->identifiers()) { - const auto &srcAuthName = *(idSrc->codeSpace()); - const auto &srcCode = idSrc->code(); - if (ci_equal(srcAuthName, "EPSG") && srcCode == code) { - return true; - } - if (allowInverse && ci_equal(srcAuthName, "INVERSE(EPSG)") && - srcCode == code) { - return true; - } - } - } - return false; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -const std::string &Transformation::getHeightToGeographic3DFilename() const { - - const std::string &ret = _getHeightToGeographic3DFilename(this, false); - if (!ret.empty()) - return ret; - if (isGeographic3DToGravityRelatedHeight(method(), false)) { - const auto &fileParameter = - parameterValue(EPSG_NAME_PARAMETER_GEOID_CORRECTION_FILENAME, - EPSG_CODE_PARAMETER_GEOID_CORRECTION_FILENAME); - if (fileParameter && - fileParameter->type() == ParameterValue::Type::FILENAME) { - return fileParameter->valueFile(); - } - } - return nullString; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static util::PropertyMap -createSimilarPropertiesMethod(common::IdentifiedObjectNNPtr obj) { - util::PropertyMap map; - - const std::string &forwardName = obj->nameStr(); - if (!forwardName.empty()) { - map.set(common::IdentifiedObject::NAME_KEY, forwardName); - } - - { - auto ar = util::ArrayOfBaseObject::create(); - for (const auto &idSrc : obj->identifiers()) { - const auto &srcAuthName = *(idSrc->codeSpace()); - const auto &srcCode = idSrc->code(); - auto idsProp = util::PropertyMap().set( - metadata::Identifier::CODESPACE_KEY, srcAuthName); - ar->add(metadata::Identifier::create(srcCode, idsProp)); - } - if (!ar->empty()) { - map.set(common::IdentifiedObject::IDENTIFIERS_KEY, ar); - } - } - - return map; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static util::PropertyMap -createSimilarPropertiesTransformation(TransformationNNPtr obj) { - util::PropertyMap map; - - // The domain(s) are unchanged - addDomains(map, obj.get()); - - const std::string &forwardName = obj->nameStr(); - if (!forwardName.empty()) { - map.set(common::IdentifiedObject::NAME_KEY, forwardName); - } - - const std::string &remarks = obj->remarks(); - if (!remarks.empty()) { - map.set(common::IdentifiedObject::REMARKS_KEY, remarks); - } - - addModifiedIdentifier(map, obj.get(), false, true); - - return map; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static TransformationNNPtr -createNTv1(const util::PropertyMap &properties, - const crs::CRSNNPtr &sourceCRSIn, const crs::CRSNNPtr &targetCRSIn, - const std::string &filename, - const std::vector &accuracies) { - return Transformation::create( - properties, sourceCRSIn, targetCRSIn, nullptr, - createMethodMapNameEPSGCode(EPSG_CODE_METHOD_NTV1), - {OperationParameter::create( - util::PropertyMap() - .set(common::IdentifiedObject::NAME_KEY, - EPSG_NAME_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE) - .set(metadata::Identifier::CODESPACE_KEY, - metadata::Identifier::EPSG) - .set(metadata::Identifier::CODE_KEY, - EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE))}, - {ParameterValue::createFilename(filename)}, accuracies); -} -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Return an equivalent transformation to the current one, but using - * PROJ alternative grid names. - */ -TransformationNNPtr Transformation::substitutePROJAlternativeGridNames( - io::DatabaseContextNNPtr databaseContext) const { - auto self = NN_NO_CHECK(std::dynamic_pointer_cast( - shared_from_this().as_nullable())); - - const auto &l_method = method(); - const int methodEPSGCode = l_method->getEPSGCode(); - - std::string projFilename; - std::string projGridFormat; - bool inverseDirection = false; - - const auto &NTv1Filename = _getNTv1Filename(this, false); - const auto &NTv2Filename = _getNTv2Filename(this, false); - std::string lasFilename; - if (methodEPSGCode == EPSG_CODE_METHOD_NADCON) { - const auto &latitudeFileParameter = - parameterValue(EPSG_NAME_PARAMETER_LATITUDE_DIFFERENCE_FILE, - EPSG_CODE_PARAMETER_LATITUDE_DIFFERENCE_FILE); - const auto &longitudeFileParameter = - parameterValue(EPSG_NAME_PARAMETER_LONGITUDE_DIFFERENCE_FILE, - EPSG_CODE_PARAMETER_LONGITUDE_DIFFERENCE_FILE); - if (latitudeFileParameter && - latitudeFileParameter->type() == ParameterValue::Type::FILENAME && - longitudeFileParameter && - longitudeFileParameter->type() == ParameterValue::Type::FILENAME) { - lasFilename = latitudeFileParameter->valueFile(); - } - } - const auto &horizontalGridName = - !NTv1Filename.empty() ? NTv1Filename : !NTv2Filename.empty() - ? NTv2Filename - : lasFilename; - - if (!horizontalGridName.empty() && - databaseContext->lookForGridAlternative(horizontalGridName, - projFilename, projGridFormat, - inverseDirection)) { - - if (horizontalGridName == projFilename) { - if (inverseDirection) { - throw util::UnsupportedOperationException( - "Inverse direction for " + projFilename + " not supported"); - } - return self; - } - - const auto &l_sourceCRS = sourceCRS(); - const auto &l_targetCRS = targetCRS(); - const auto &l_accuracies = coordinateOperationAccuracies(); - if (projGridFormat == "GTiff") { - auto parameters = - std::vector{createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE)}; - auto methodProperties = util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, - PROJ_WKT2_NAME_METHOD_HORIZONTAL_SHIFT_GTIFF); - auto values = std::vector{ - ParameterValue::createFilename(projFilename)}; - if (inverseDirection) { - return create(createPropertiesForInverse( - self.as_nullable().get(), true, false), - l_targetCRS, l_sourceCRS, nullptr, - methodProperties, parameters, values, - l_accuracies) - ->inverseAsTransformation(); - - } else { - return create(createSimilarPropertiesTransformation(self), - l_sourceCRS, l_targetCRS, nullptr, - methodProperties, parameters, values, - l_accuracies); - } - } else if (projGridFormat == "NTv1") { - if (inverseDirection) { - return createNTv1(createPropertiesForInverse( - self.as_nullable().get(), true, false), - l_targetCRS, l_sourceCRS, projFilename, - l_accuracies) - ->inverseAsTransformation(); - } else { - return createNTv1(createSimilarPropertiesTransformation(self), - l_sourceCRS, l_targetCRS, projFilename, - l_accuracies); - } - } else if (projGridFormat == "NTv2") { - if (inverseDirection) { - return createNTv2(createPropertiesForInverse( - self.as_nullable().get(), true, false), - l_targetCRS, l_sourceCRS, projFilename, - l_accuracies) - ->inverseAsTransformation(); - } else { - return createNTv2(createSimilarPropertiesTransformation(self), - l_sourceCRS, l_targetCRS, projFilename, - l_accuracies); - } - } else if (projGridFormat == "CTable2") { - auto parameters = - std::vector{createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE)}; - auto methodProperties = - util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, - PROJ_WKT2_NAME_METHOD_CTABLE2); - auto values = std::vector{ - ParameterValue::createFilename(projFilename)}; - if (inverseDirection) { - return create(createPropertiesForInverse( - self.as_nullable().get(), true, false), - l_targetCRS, l_sourceCRS, nullptr, - methodProperties, parameters, values, - l_accuracies) - ->inverseAsTransformation(); - - } else { - return create(createSimilarPropertiesTransformation(self), - l_sourceCRS, l_targetCRS, nullptr, - methodProperties, parameters, values, - l_accuracies); - } - } - } - - if (isGeographic3DToGravityRelatedHeight(method(), false)) { - const auto &fileParameter = - parameterValue(EPSG_NAME_PARAMETER_GEOID_CORRECTION_FILENAME, - EPSG_CODE_PARAMETER_GEOID_CORRECTION_FILENAME); - if (fileParameter && - fileParameter->type() == ParameterValue::Type::FILENAME) { - auto filename = fileParameter->valueFile(); - if (databaseContext->lookForGridAlternative( - filename, projFilename, projGridFormat, inverseDirection)) { - - if (inverseDirection) { - throw util::UnsupportedOperationException( - "Inverse direction for " - "Geographic3DToGravityRelatedHeight not supported"); - } - - if (filename == projFilename) { - return self; - } - - auto parameters = std::vector{ - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_GEOID_CORRECTION_FILENAME)}; -#ifdef disabled_for_now - if (inverseDirection) { - return create(createPropertiesForInverse( - self.as_nullable().get(), true, false), - targetCRS(), sourceCRS(), nullptr, - createSimilarPropertiesMethod(method()), - parameters, {ParameterValue::createFilename( - projFilename)}, - coordinateOperationAccuracies()) - ->inverseAsTransformation(); - } else -#endif - { - return create( - createSimilarPropertiesTransformation(self), - sourceCRS(), targetCRS(), nullptr, - createSimilarPropertiesMethod(method()), parameters, - {ParameterValue::createFilename(projFilename)}, - coordinateOperationAccuracies()); - } - } - } - } - - const auto &geocentricTranslationFilename = - _getGeocentricTranslationFilename(this, false); - if (!geocentricTranslationFilename.empty()) { - if (databaseContext->lookForGridAlternative( - geocentricTranslationFilename, projFilename, projGridFormat, - inverseDirection)) { - - if (inverseDirection) { - throw util::UnsupportedOperationException( - "Inverse direction for " - "GeocentricTranslation not supported"); - } - - if (geocentricTranslationFilename == projFilename) { - return self; - } - - auto parameters = - std::vector{createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_GEOCENTRIC_TRANSLATION_FILE)}; - return create(createSimilarPropertiesTransformation(self), - sourceCRS(), targetCRS(), interpolationCRS(), - createSimilarPropertiesMethod(method()), parameters, - {ParameterValue::createFilename(projFilename)}, - coordinateOperationAccuracies()); - } - } - - if (methodEPSGCode == EPSG_CODE_METHOD_VERTCON || - methodEPSGCode == EPSG_CODE_METHOD_VERTICALGRID_NZLVD || - methodEPSGCode == EPSG_CODE_METHOD_VERTICALGRID_BEV_AT || - methodEPSGCode == EPSG_CODE_METHOD_VERTICALGRID_GTX) { - auto fileParameter = - parameterValue(EPSG_NAME_PARAMETER_VERTICAL_OFFSET_FILE, - EPSG_CODE_PARAMETER_VERTICAL_OFFSET_FILE); - if (fileParameter && - fileParameter->type() == ParameterValue::Type::FILENAME) { - - auto filename = fileParameter->valueFile(); - if (databaseContext->lookForGridAlternative( - filename, projFilename, projGridFormat, inverseDirection)) { - - if (filename == projFilename) { - if (inverseDirection) { - throw util::UnsupportedOperationException( - "Inverse direction for " + projFilename + - " not supported"); - } - return self; - } - - auto parameters = std::vector{ - createOpParamNameEPSGCode( - EPSG_CODE_PARAMETER_VERTICAL_OFFSET_FILE)}; - if (inverseDirection) { - return create(createPropertiesForInverse( - self.as_nullable().get(), true, false), - targetCRS(), sourceCRS(), nullptr, - createSimilarPropertiesMethod(method()), - parameters, {ParameterValue::createFilename( - projFilename)}, - coordinateOperationAccuracies()) - ->inverseAsTransformation(); - } else { - return create( - createSimilarPropertiesTransformation(self), - sourceCRS(), targetCRS(), nullptr, - createSimilarPropertiesMethod(method()), parameters, - {ParameterValue::createFilename(projFilename)}, - coordinateOperationAccuracies()); - } - } - } - } - - return self; -} -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -static void ThrowExceptionNotGeodeticGeographic(const char *trfrm_name) { - throw io::FormattingException(concat("Can apply ", std::string(trfrm_name), - " only to GeodeticCRS / " - "GeographicCRS")); -} - -// --------------------------------------------------------------------------- - -// If crs is a geographic CRS, or a compound CRS of a geographic CRS, -// or a compoundCRS of a bound CRS of a geographic CRS, return that -// geographic CRS -static crs::GeographicCRSPtr -extractGeographicCRSIfGeographicCRSOrEquivalent(const crs::CRSNNPtr &crs) { - auto geogCRS = util::nn_dynamic_pointer_cast(crs); - if (!geogCRS) { - auto compoundCRS = util::nn_dynamic_pointer_cast(crs); - if (compoundCRS) { - const auto &components = compoundCRS->componentReferenceSystems(); - if (!components.empty()) { - geogCRS = util::nn_dynamic_pointer_cast( - components[0]); - if (!geogCRS) { - auto boundCRS = - util::nn_dynamic_pointer_cast( - components[0]); - if (boundCRS) { - geogCRS = - util::nn_dynamic_pointer_cast( - boundCRS->baseCRS()); - } - } - } - } else { - auto boundCRS = util::nn_dynamic_pointer_cast(crs); - if (boundCRS) { - geogCRS = util::nn_dynamic_pointer_cast( - boundCRS->baseCRS()); - } - } - } - return geogCRS; -} - -// --------------------------------------------------------------------------- - -static void setupPROJGeodeticSourceCRS(io::PROJStringFormatter *formatter, - const crs::CRSNNPtr &crs, bool addPushV3, - const char *trfrm_name) { - auto sourceCRSGeog = extractGeographicCRSIfGeographicCRSOrEquivalent(crs); - if (sourceCRSGeog) { - formatter->startInversion(); - sourceCRSGeog->_exportToPROJString(formatter); - formatter->stopInversion(); - if (util::isOfExactType( - *(sourceCRSGeog.get()))) { - // The export of a DerivedGeographicCRS in non-CRS mode adds - // unit conversion and axis swapping. We must compensate for that - formatter->startInversion(); - sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - formatter->stopInversion(); - } - - if (addPushV3) { - formatter->addStep("push"); - formatter->addParam("v_3"); - } - - formatter->addStep("cart"); - sourceCRSGeog->ellipsoid()->_exportToPROJString(formatter); - } else { - auto sourceCRSGeod = dynamic_cast(crs.get()); - if (!sourceCRSGeod) { - ThrowExceptionNotGeodeticGeographic(trfrm_name); - } - formatter->startInversion(); - sourceCRSGeod->addGeocentricUnitConversionIntoPROJString(formatter); - formatter->stopInversion(); - } -} -// --------------------------------------------------------------------------- - -static void setupPROJGeodeticTargetCRS(io::PROJStringFormatter *formatter, - const crs::CRSNNPtr &crs, bool addPopV3, - const char *trfrm_name) { - auto targetCRSGeog = extractGeographicCRSIfGeographicCRSOrEquivalent(crs); - if (targetCRSGeog) { - formatter->addStep("cart"); - formatter->setCurrentStepInverted(true); - targetCRSGeog->ellipsoid()->_exportToPROJString(formatter); - - if (addPopV3) { - formatter->addStep("pop"); - formatter->addParam("v_3"); - } - if (util::isOfExactType( - *(targetCRSGeog.get()))) { - // The export of a DerivedGeographicCRS in non-CRS mode adds - // unit conversion and axis swapping. We must compensate for that - targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - } - targetCRSGeog->_exportToPROJString(formatter); - } else { - auto targetCRSGeod = dynamic_cast(crs.get()); - if (!targetCRSGeod) { - ThrowExceptionNotGeodeticGeographic(trfrm_name); - } - targetCRSGeod->addGeocentricUnitConversionIntoPROJString(formatter); - } -} - -//! @endcond -// --------------------------------------------------------------------------- - -void Transformation::_exportToPROJString( - io::PROJStringFormatter *formatter) const // throw(FormattingException) -{ - if (formatter->convention() == - io::PROJStringFormatter::Convention::PROJ_4) { - throw io::FormattingException( - "Transformation cannot be exported as a PROJ.4 string"); - } - - formatter->setCoordinateOperationOptimizations(true); - - bool positionVectorConvention = true; - bool sevenParamsTransform = false; - bool threeParamsTransform = false; - bool fifteenParamsTransform = false; - const auto &l_method = method(); - const int methodEPSGCode = l_method->getEPSGCode(); - const auto &methodName = l_method->nameStr(); - const auto paramCount = parameterValues().size(); - const bool l_isTimeDependent = isTimeDependent(methodName); - const bool isPositionVector = - ci_find(methodName, "Position Vector") != std::string::npos || - ci_find(methodName, "PV") != std::string::npos; - const bool isCoordinateFrame = - ci_find(methodName, "Coordinate Frame") != std::string::npos || - ci_find(methodName, "CF") != std::string::npos; - if ((paramCount == 7 && isCoordinateFrame && !l_isTimeDependent) || - methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC || - methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D || - methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D) { - positionVectorConvention = false; - sevenParamsTransform = true; - } else if ( - (paramCount == 15 && isCoordinateFrame && l_isTimeDependent) || - methodEPSGCode == - EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOCENTRIC || - methodEPSGCode == - EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOGRAPHIC_2D || - methodEPSGCode == - EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOGRAPHIC_3D) { - positionVectorConvention = false; - fifteenParamsTransform = true; - } else if ((paramCount == 7 && isPositionVector && !l_isTimeDependent) || - methodEPSGCode == EPSG_CODE_METHOD_POSITION_VECTOR_GEOCENTRIC || - methodEPSGCode == - EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_2D || - methodEPSGCode == - EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_3D) { - sevenParamsTransform = true; - } else if ( - (paramCount == 15 && isPositionVector && l_isTimeDependent) || - methodEPSGCode == - EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOCENTRIC || - methodEPSGCode == - EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOGRAPHIC_2D || - methodEPSGCode == - EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOGRAPHIC_3D) { - fifteenParamsTransform = true; - } else if ((paramCount == 3 && - ci_find(methodName, "Geocentric translations") != - std::string::npos) || - methodEPSGCode == - EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOCENTRIC || - methodEPSGCode == - EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_2D || - methodEPSGCode == - EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_3D) { - threeParamsTransform = true; - } - if (threeParamsTransform || sevenParamsTransform || - fifteenParamsTransform) { - double x = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION); - double y = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION); - double z = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION); - - auto sourceCRSGeog = - dynamic_cast(sourceCRS().get()); - auto targetCRSGeog = - dynamic_cast(targetCRS().get()); - const bool addPushPopV3 = - !CoordinateOperation::getPrivate()->use3DHelmert_ && - ((sourceCRSGeog && - sourceCRSGeog->coordinateSystem()->axisList().size() == 2) || - (targetCRSGeog && - targetCRSGeog->coordinateSystem()->axisList().size() == 2)); - - setupPROJGeodeticSourceCRS(formatter, sourceCRS(), addPushPopV3, - "Helmert"); - - formatter->addStep("helmert"); - formatter->addParam("x", x); - formatter->addParam("y", y); - formatter->addParam("z", z); - if (sevenParamsTransform || fifteenParamsTransform) { - double rx = - parameterValueNumeric(EPSG_CODE_PARAMETER_X_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND); - double ry = - parameterValueNumeric(EPSG_CODE_PARAMETER_Y_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND); - double rz = - parameterValueNumeric(EPSG_CODE_PARAMETER_Z_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND); - double scaleDiff = - parameterValueNumeric(EPSG_CODE_PARAMETER_SCALE_DIFFERENCE, - common::UnitOfMeasure::PARTS_PER_MILLION); - formatter->addParam("rx", rx); - formatter->addParam("ry", ry); - formatter->addParam("rz", rz); - formatter->addParam("s", scaleDiff); - if (fifteenParamsTransform) { - double rate_x = parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_X_AXIS_TRANSLATION, - common::UnitOfMeasure::METRE_PER_YEAR); - double rate_y = parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_Y_AXIS_TRANSLATION, - common::UnitOfMeasure::METRE_PER_YEAR); - double rate_z = parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_Z_AXIS_TRANSLATION, - common::UnitOfMeasure::METRE_PER_YEAR); - double rate_rx = parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_X_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND_PER_YEAR); - double rate_ry = parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_Y_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND_PER_YEAR); - double rate_rz = parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_Z_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND_PER_YEAR); - double rate_scaleDiff = parameterValueNumeric( - EPSG_CODE_PARAMETER_RATE_SCALE_DIFFERENCE, - common::UnitOfMeasure::PPM_PER_YEAR); - double referenceEpochYear = - parameterValueNumeric(EPSG_CODE_PARAMETER_REFERENCE_EPOCH, - common::UnitOfMeasure::YEAR); - formatter->addParam("dx", rate_x); - formatter->addParam("dy", rate_y); - formatter->addParam("dz", rate_z); - formatter->addParam("drx", rate_rx); - formatter->addParam("dry", rate_ry); - formatter->addParam("drz", rate_rz); - formatter->addParam("ds", rate_scaleDiff); - formatter->addParam("t_epoch", referenceEpochYear); - } - if (positionVectorConvention) { - formatter->addParam("convention", "position_vector"); - } else { - formatter->addParam("convention", "coordinate_frame"); - } - } - - setupPROJGeodeticTargetCRS(formatter, targetCRS(), addPushPopV3, - "Helmert"); - - return; - } - - if (methodEPSGCode == EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_CF_GEOCENTRIC || - methodEPSGCode == EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOCENTRIC || - methodEPSGCode == - EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_CF_GEOGRAPHIC_3D || - methodEPSGCode == - EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOGRAPHIC_3D || - methodEPSGCode == - EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_CF_GEOGRAPHIC_2D || - methodEPSGCode == - EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOGRAPHIC_2D) { - - positionVectorConvention = - isPositionVector || - methodEPSGCode == - EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOCENTRIC || - methodEPSGCode == - EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOGRAPHIC_3D || - methodEPSGCode == - EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOGRAPHIC_2D; - - double x = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION); - double y = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION); - double z = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION); - double rx = parameterValueNumeric(EPSG_CODE_PARAMETER_X_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND); - double ry = parameterValueNumeric(EPSG_CODE_PARAMETER_Y_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND); - double rz = parameterValueNumeric(EPSG_CODE_PARAMETER_Z_AXIS_ROTATION, - common::UnitOfMeasure::ARC_SECOND); - double scaleDiff = - parameterValueNumeric(EPSG_CODE_PARAMETER_SCALE_DIFFERENCE, - common::UnitOfMeasure::PARTS_PER_MILLION); - - double px = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_ORDINATE_1_EVAL_POINT); - double py = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_ORDINATE_2_EVAL_POINT); - double pz = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_ORDINATE_3_EVAL_POINT); - - bool addPushPopV3 = - (methodEPSGCode == - EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOGRAPHIC_2D || - methodEPSGCode == - EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_CF_GEOGRAPHIC_2D); - - setupPROJGeodeticSourceCRS(formatter, sourceCRS(), addPushPopV3, - "Molodensky-Badekas"); - - formatter->addStep("molobadekas"); - formatter->addParam("x", x); - formatter->addParam("y", y); - formatter->addParam("z", z); - formatter->addParam("rx", rx); - formatter->addParam("ry", ry); - formatter->addParam("rz", rz); - formatter->addParam("s", scaleDiff); - formatter->addParam("px", px); - formatter->addParam("py", py); - formatter->addParam("pz", pz); - if (positionVectorConvention) { - formatter->addParam("convention", "position_vector"); - } else { - formatter->addParam("convention", "coordinate_frame"); - } - - setupPROJGeodeticTargetCRS(formatter, targetCRS(), addPushPopV3, - "Molodensky-Badekas"); - - return; - } - - if (methodEPSGCode == EPSG_CODE_METHOD_MOLODENSKY || - methodEPSGCode == EPSG_CODE_METHOD_ABRIDGED_MOLODENSKY) { - double x = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION); - double y = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION); - double z = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION); - double da = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_SEMI_MAJOR_AXIS_DIFFERENCE); - double df = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_FLATTENING_DIFFERENCE); - - auto sourceCRSGeog = - dynamic_cast(sourceCRS().get()); - if (!sourceCRSGeog) { - throw io::FormattingException( - "Can apply Molodensky only to GeographicCRS"); - } - - auto targetCRSGeog = - dynamic_cast(targetCRS().get()); - if (!targetCRSGeog) { - throw io::FormattingException( - "Can apply Molodensky only to GeographicCRS"); - } - - formatter->startInversion(); - sourceCRSGeog->_exportToPROJString(formatter); - formatter->stopInversion(); - - formatter->addStep("molodensky"); - sourceCRSGeog->ellipsoid()->_exportToPROJString(formatter); - formatter->addParam("dx", x); - formatter->addParam("dy", y); - formatter->addParam("dz", z); - formatter->addParam("da", da); - formatter->addParam("df", df); - - if (ci_find(methodName, "Abridged") != std::string::npos || - methodEPSGCode == EPSG_CODE_METHOD_ABRIDGED_MOLODENSKY) { - formatter->addParam("abridged"); - } - - targetCRSGeog->_exportToPROJString(formatter); - - return; - } - - if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC2D_OFFSETS) { - double offsetLat = - parameterValueNumeric(EPSG_CODE_PARAMETER_LATITUDE_OFFSET, - common::UnitOfMeasure::ARC_SECOND); - double offsetLong = - parameterValueNumeric(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET, - common::UnitOfMeasure::ARC_SECOND); - - auto sourceCRSGeog = - dynamic_cast(sourceCRS().get()); - if (!sourceCRSGeog) { - throw io::FormattingException( - "Can apply Geographic 2D offsets only to GeographicCRS"); - } - - auto targetCRSGeog = - dynamic_cast(targetCRS().get()); - if (!targetCRSGeog) { - throw io::FormattingException( - "Can apply Geographic 2D offsets only to GeographicCRS"); - } - - formatter->startInversion(); - sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - formatter->stopInversion(); - - if (offsetLat != 0.0 || offsetLong != 0.0) { - formatter->addStep("geogoffset"); - formatter->addParam("dlat", offsetLat); - formatter->addParam("dlon", offsetLong); - } - - targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - - return; - } - - if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC3D_OFFSETS) { - double offsetLat = - parameterValueNumeric(EPSG_CODE_PARAMETER_LATITUDE_OFFSET, - common::UnitOfMeasure::ARC_SECOND); - double offsetLong = - parameterValueNumeric(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET, - common::UnitOfMeasure::ARC_SECOND); - double offsetHeight = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_VERTICAL_OFFSET); - - auto sourceCRSGeog = - dynamic_cast(sourceCRS().get()); - if (!sourceCRSGeog) { - throw io::FormattingException( - "Can apply Geographic 3D offsets only to GeographicCRS"); - } - - auto targetCRSGeog = - dynamic_cast(targetCRS().get()); - if (!targetCRSGeog) { - throw io::FormattingException( - "Can apply Geographic 3D offsets only to GeographicCRS"); - } - - formatter->startInversion(); - sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - formatter->stopInversion(); - - if (offsetLat != 0.0 || offsetLong != 0.0 || offsetHeight != 0.0) { - formatter->addStep("geogoffset"); - formatter->addParam("dlat", offsetLat); - formatter->addParam("dlon", offsetLong); - formatter->addParam("dh", offsetHeight); - } - - targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - - return; - } - - if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC2D_WITH_HEIGHT_OFFSETS) { - double offsetLat = - parameterValueNumeric(EPSG_CODE_PARAMETER_LATITUDE_OFFSET, - common::UnitOfMeasure::ARC_SECOND); - double offsetLong = - parameterValueNumeric(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET, - common::UnitOfMeasure::ARC_SECOND); - double offsetHeight = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_GEOID_UNDULATION); - - auto sourceCRSGeog = - dynamic_cast(sourceCRS().get()); - if (!sourceCRSGeog) { - auto sourceCRSCompound = - dynamic_cast(sourceCRS().get()); - if (sourceCRSCompound) { - sourceCRSGeog = sourceCRSCompound->extractGeographicCRS().get(); - } - if (!sourceCRSGeog) { - throw io::FormattingException("Can apply Geographic 2D with " - "height offsets only to " - "GeographicCRS / CompoundCRS"); - } - } - - auto targetCRSGeog = - dynamic_cast(targetCRS().get()); - if (!targetCRSGeog) { - auto targetCRSCompound = - dynamic_cast(targetCRS().get()); - if (targetCRSCompound) { - targetCRSGeog = targetCRSCompound->extractGeographicCRS().get(); - } - if (!targetCRSGeog) { - throw io::FormattingException("Can apply Geographic 2D with " - "height offsets only to " - "GeographicCRS / CompoundCRS"); - } - } - - formatter->startInversion(); - sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - formatter->stopInversion(); - - if (offsetLat != 0.0 || offsetLong != 0.0 || offsetHeight != 0.0) { - formatter->addStep("geogoffset"); - formatter->addParam("dlat", offsetLat); - formatter->addParam("dlon", offsetLong); - formatter->addParam("dh", offsetHeight); - } - - targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - - return; - } - - if (methodEPSGCode == EPSG_CODE_METHOD_VERTICAL_OFFSET) { - - auto sourceCRSVert = - dynamic_cast(sourceCRS().get()); - if (!sourceCRSVert) { - throw io::FormattingException( - "Can apply Vertical offset only to VerticalCRS"); - } - - auto targetCRSVert = - dynamic_cast(targetCRS().get()); - if (!targetCRSVert) { - throw io::FormattingException( - "Can apply Vertical offset only to VerticalCRS"); - } - - auto offsetHeight = - parameterValueNumericAsSI(EPSG_CODE_PARAMETER_VERTICAL_OFFSET); - - formatter->startInversion(); - sourceCRSVert->addLinearUnitConvert(formatter); - formatter->stopInversion(); - - formatter->addStep("geogoffset"); - formatter->addParam("dh", offsetHeight); - - targetCRSVert->addLinearUnitConvert(formatter); - - return; - } - - // Substitute grid names with PROJ friendly names. - if (formatter->databaseContext()) { - auto alternate = substitutePROJAlternativeGridNames( - NN_NO_CHECK(formatter->databaseContext())); - auto self = NN_NO_CHECK(std::dynamic_pointer_cast( - shared_from_this().as_nullable())); - - if (alternate != self) { - alternate->_exportToPROJString(formatter); - return; - } - } - - const bool isMethodInverseOf = starts_with(methodName, INVERSE_OF); - - const auto &NTv1Filename = _getNTv1Filename(this, true); - const auto &NTv2Filename = _getNTv2Filename(this, true); - const auto &CTABLE2Filename = _getCTABLE2Filename(this, true); - const auto &HorizontalShiftGTIFFFilename = - _getHorizontalShiftGTIFFFilename(this, true); - const auto &hGridShiftFilename = - !HorizontalShiftGTIFFFilename.empty() - ? HorizontalShiftGTIFFFilename - : !NTv1Filename.empty() ? NTv1Filename : !NTv2Filename.empty() - ? NTv2Filename - : CTABLE2Filename; - if (!hGridShiftFilename.empty()) { - auto sourceCRSGeog = - extractGeographicCRSIfGeographicCRSOrEquivalent(sourceCRS()); - if (!sourceCRSGeog) { - throw io::FormattingException( - concat("Can apply ", methodName, " only to GeographicCRS")); - } - - auto targetCRSGeog = - extractGeographicCRSIfGeographicCRSOrEquivalent(targetCRS()); - if (!targetCRSGeog) { - throw io::FormattingException( - concat("Can apply ", methodName, " only to GeographicCRS")); - } - - formatter->startInversion(); - sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - formatter->stopInversion(); - - if (isMethodInverseOf) { - formatter->startInversion(); - } - formatter->addStep("hgridshift"); - formatter->addParam("grids", hGridShiftFilename); - if (isMethodInverseOf) { - formatter->stopInversion(); - } - - targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - - return; - } - - const auto &geocentricTranslationFilename = - _getGeocentricTranslationFilename(this, true); - if (!geocentricTranslationFilename.empty()) { - auto sourceCRSGeog = - dynamic_cast(sourceCRS().get()); - if (!sourceCRSGeog) { - throw io::FormattingException( - concat("Can apply ", methodName, " only to GeographicCRS")); - } - - auto targetCRSGeog = - dynamic_cast(targetCRS().get()); - if (!targetCRSGeog) { - throw io::FormattingException( - concat("Can apply ", methodName, " only to GeographicCRS")); - } - - const auto &interpCRS = interpolationCRS(); - if (!interpCRS) { - throw io::FormattingException( - "InterpolationCRS required " - "for" - " " EPSG_NAME_METHOD_GEOCENTRIC_TRANSLATION_BY_GRID_INTERPOLATION_IGN); - } - const bool interpIsSrc = interpCRS->_isEquivalentTo( - sourceCRS().get(), util::IComparable::Criterion::EQUIVALENT); - const bool interpIsTarget = interpCRS->_isEquivalentTo( - targetCRS().get(), util::IComparable::Criterion::EQUIVALENT); - if (!interpIsSrc && !interpIsTarget) { - throw io::FormattingException( - "For" - " " EPSG_NAME_METHOD_GEOCENTRIC_TRANSLATION_BY_GRID_INTERPOLATION_IGN - ", interpolation CRS should be the source or target CRS"); - } - - formatter->startInversion(); - sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - formatter->stopInversion(); - - if (isMethodInverseOf) { - formatter->startInversion(); - } - - formatter->addStep("push"); - formatter->addParam("v_3"); - - formatter->addStep("cart"); - sourceCRSGeog->ellipsoid()->_exportToPROJString(formatter); - - formatter->addStep("xyzgridshift"); - formatter->addParam("grids", geocentricTranslationFilename); - formatter->addParam("grid_ref", - interpIsTarget ? "output_crs" : "input_crs"); - (interpIsTarget ? targetCRSGeog : sourceCRSGeog) - ->ellipsoid() - ->_exportToPROJString(formatter); - - formatter->startInversion(); - formatter->addStep("cart"); - targetCRSGeog->ellipsoid()->_exportToPROJString(formatter); - formatter->stopInversion(); - - formatter->addStep("pop"); - formatter->addParam("v_3"); - - if (isMethodInverseOf) { - formatter->stopInversion(); - } - - targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - - return; - } - - const auto &heightFilename = _getHeightToGeographic3DFilename(this, true); - if (!heightFilename.empty()) { - auto targetCRSGeog = - extractGeographicCRSIfGeographicCRSOrEquivalent(targetCRS()); - if (!targetCRSGeog) { - throw io::FormattingException( - concat("Can apply ", methodName, " only to GeographicCRS")); - } - - if (!formatter->omitHorizontalConversionInVertTransformation()) { - formatter->startInversion(); - formatter->pushOmitZUnitConversion(); - targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - formatter->popOmitZUnitConversion(); - formatter->stopInversion(); - } - - if (isMethodInverseOf) { - formatter->startInversion(); - } - formatter->addStep("vgridshift"); - formatter->addParam("grids", heightFilename); - formatter->addParam("multiplier", 1.0); - if (isMethodInverseOf) { - formatter->stopInversion(); - } - - if (!formatter->omitHorizontalConversionInVertTransformation()) { - formatter->pushOmitZUnitConversion(); - targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - formatter->popOmitZUnitConversion(); - } - - return; - } - - if (isGeographic3DToGravityRelatedHeight(method(), true)) { - auto fileParameter = - parameterValue(EPSG_NAME_PARAMETER_GEOID_CORRECTION_FILENAME, - EPSG_CODE_PARAMETER_GEOID_CORRECTION_FILENAME); - if (fileParameter && - fileParameter->type() == ParameterValue::Type::FILENAME) { - auto filename = fileParameter->valueFile(); - - auto sourceCRSGeog = - extractGeographicCRSIfGeographicCRSOrEquivalent(sourceCRS()); - if (!sourceCRSGeog) { - throw io::FormattingException( - concat("Can apply ", methodName, " only to GeographicCRS")); - } - - if (!formatter->omitHorizontalConversionInVertTransformation()) { - formatter->startInversion(); - formatter->pushOmitZUnitConversion(); - sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - formatter->popOmitZUnitConversion(); - formatter->stopInversion(); - } - - bool doInversion = isMethodInverseOf; - // The EPSG Geog3DToHeight is the reverse convention of PROJ ! - doInversion = !doInversion; - if (doInversion) { - formatter->startInversion(); - } - formatter->addStep("vgridshift"); - formatter->addParam("grids", filename); - formatter->addParam("multiplier", 1.0); - if (doInversion) { - formatter->stopInversion(); - } - - if (!formatter->omitHorizontalConversionInVertTransformation()) { - formatter->pushOmitZUnitConversion(); - sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - formatter->popOmitZUnitConversion(); - } - - return; - } - } - - if (methodEPSGCode == EPSG_CODE_METHOD_VERTCON) { - auto fileParameter = - parameterValue(EPSG_NAME_PARAMETER_VERTICAL_OFFSET_FILE, - EPSG_CODE_PARAMETER_VERTICAL_OFFSET_FILE); - if (fileParameter && - fileParameter->type() == ParameterValue::Type::FILENAME) { - formatter->addStep("vgridshift"); - formatter->addParam("grids", fileParameter->valueFile()); - if (fileParameter->valueFile().find(".tif") != std::string::npos) { - formatter->addParam("multiplier", 1.0); - } else { - // The vertcon grids go from NGVD 29 to NAVD 88, with units - // in millimeter (see - // https://github.com/OSGeo/proj.4/issues/1071), for gtx files - formatter->addParam("multiplier", 0.001); - } - return; - } - } - - if (methodEPSGCode == EPSG_CODE_METHOD_VERTICALGRID_NZLVD || - methodEPSGCode == EPSG_CODE_METHOD_VERTICALGRID_BEV_AT || - methodEPSGCode == EPSG_CODE_METHOD_VERTICALGRID_GTX) { - auto fileParameter = - parameterValue(EPSG_NAME_PARAMETER_VERTICAL_OFFSET_FILE, - EPSG_CODE_PARAMETER_VERTICAL_OFFSET_FILE); - if (fileParameter && - fileParameter->type() == ParameterValue::Type::FILENAME) { - formatter->addStep("vgridshift"); - formatter->addParam("grids", fileParameter->valueFile()); - formatter->addParam("multiplier", 1.0); - return; - } - } - - if (isLongitudeRotation()) { - double offsetDeg = - parameterValueNumeric(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET, - common::UnitOfMeasure::DEGREE); - - auto sourceCRSGeog = - dynamic_cast(sourceCRS().get()); - if (!sourceCRSGeog) { - throw io::FormattingException( - concat("Can apply ", methodName, " only to GeographicCRS")); - } - - auto targetCRSGeog = - dynamic_cast(targetCRS().get()); - if (!targetCRSGeog) { - throw io::FormattingException( - concat("Can apply ", methodName + " only to GeographicCRS")); - } - - if (!sourceCRSGeog->ellipsoid()->_isEquivalentTo( - targetCRSGeog->ellipsoid().get(), - util::IComparable::Criterion::EQUIVALENT)) { - // This is arguable if we should check this... - throw io::FormattingException("Can apply Longitude rotation " - "only to SRS with same " - "ellipsoid"); - } - - formatter->startInversion(); - sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - formatter->stopInversion(); - - bool done = false; - if (offsetDeg != 0.0) { - // Optimization: as we are doing nominally a +step=inv, - // if the negation of the offset value is a well-known name, - // then use forward case with this name. - auto projPMName = datum::PrimeMeridian::getPROJStringWellKnownName( - common::Angle(-offsetDeg)); - if (!projPMName.empty()) { - done = true; - formatter->addStep("longlat"); - sourceCRSGeog->ellipsoid()->_exportToPROJString(formatter); - formatter->addParam("pm", projPMName); - } - } - if (!done) { - // To actually add the offset, we must use the reverse longlat - // operation. - formatter->startInversion(); - formatter->addStep("longlat"); - sourceCRSGeog->ellipsoid()->_exportToPROJString(formatter); - datum::PrimeMeridian::create(util::PropertyMap(), - common::Angle(offsetDeg)) - ->_exportToPROJString(formatter); - formatter->stopInversion(); - } - - targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); - - return; - } - - if (exportToPROJStringGeneric(formatter)) { - return; - } - - throw io::FormattingException("Unimplemented"); -} - -// --------------------------------------------------------------------------- - -bool SingleOperation::exportToPROJStringGeneric( - io::PROJStringFormatter *formatter) const { - const int methodEPSGCode = method()->getEPSGCode(); - - if (methodEPSGCode == EPSG_CODE_METHOD_AFFINE_PARAMETRIC_TRANSFORMATION) { - const double A0 = parameterValueMeasure(EPSG_CODE_PARAMETER_A0).value(); - const double A1 = parameterValueMeasure(EPSG_CODE_PARAMETER_A1).value(); - const double A2 = parameterValueMeasure(EPSG_CODE_PARAMETER_A2).value(); - const double B0 = parameterValueMeasure(EPSG_CODE_PARAMETER_B0).value(); - const double B1 = parameterValueMeasure(EPSG_CODE_PARAMETER_B1).value(); - const double B2 = parameterValueMeasure(EPSG_CODE_PARAMETER_B2).value(); - - // Do not mess with axis unit and order for that transformation - - formatter->addStep("affine"); - formatter->addParam("xoff", A0); - formatter->addParam("s11", A1); - formatter->addParam("s12", A2); - formatter->addParam("yoff", B0); - formatter->addParam("s21", B1); - formatter->addParam("s22", B2); - - return true; - } - - if (isAxisOrderReversal(methodEPSGCode)) { - formatter->addStep("axisswap"); - formatter->addParam("order", "2,1"); - auto sourceCRSGeog = - dynamic_cast(sourceCRS().get()); - auto targetCRSGeog = - dynamic_cast(targetCRS().get()); - if (sourceCRSGeog && targetCRSGeog) { - const auto &unitSrc = - sourceCRSGeog->coordinateSystem()->axisList()[0]->unit(); - const auto &unitDst = - targetCRSGeog->coordinateSystem()->axisList()[0]->unit(); - if (!unitSrc._isEquivalentTo( - unitDst, util::IComparable::Criterion::EQUIVALENT)) { - formatter->addStep("unitconvert"); - auto projUnit = unitSrc.exportToPROJString(); - if (projUnit.empty()) { - formatter->addParam("xy_in", unitSrc.conversionToSI()); - } else { - formatter->addParam("xy_in", projUnit); - } - projUnit = unitDst.exportToPROJString(); - if (projUnit.empty()) { - formatter->addParam("xy_out", unitDst.conversionToSI()); - } else { - formatter->addParam("xy_out", projUnit); - } - } - } - return true; - } - - if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC_GEOCENTRIC) { - - auto sourceCRSGeod = - dynamic_cast(sourceCRS().get()); - auto targetCRSGeod = - dynamic_cast(targetCRS().get()); - if (sourceCRSGeod && targetCRSGeod) { - auto sourceCRSGeog = - dynamic_cast(sourceCRSGeod); - auto targetCRSGeog = - dynamic_cast(targetCRSGeod); - bool isSrcGeocentric = sourceCRSGeod->isGeocentric(); - bool isSrcGeographic = sourceCRSGeog != nullptr; - bool isTargetGeocentric = targetCRSGeod->isGeocentric(); - bool isTargetGeographic = targetCRSGeog != nullptr; - if ((isSrcGeocentric && isTargetGeographic) || - (isSrcGeographic && isTargetGeocentric)) { - - formatter->startInversion(); - sourceCRSGeod->_exportToPROJString(formatter); - formatter->stopInversion(); - - targetCRSGeod->_exportToPROJString(formatter); - - return true; - } - } - - throw io::FormattingException("Invalid nature of source and/or " - "targetCRS for Geographic/Geocentric " - "conversion"); - } - - if (methodEPSGCode == EPSG_CODE_METHOD_CHANGE_VERTICAL_UNIT) { - double convFactor = parameterValueNumericAsSI( - EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR); - auto uom = common::UnitOfMeasure(std::string(), convFactor, - common::UnitOfMeasure::Type::LINEAR) - .exportToPROJString(); - auto reverse_uom = - common::UnitOfMeasure(std::string(), 1.0 / convFactor, - common::UnitOfMeasure::Type::LINEAR) - .exportToPROJString(); - if (uom == "m") { - // do nothing - } else if (!uom.empty()) { - formatter->addStep("unitconvert"); - formatter->addParam("z_in", uom); - formatter->addParam("z_out", "m"); - } else if (!reverse_uom.empty()) { - formatter->addStep("unitconvert"); - formatter->addParam("z_in", "m"); - formatter->addParam("z_out", reverse_uom); - } else { - formatter->addStep("affine"); - formatter->addParam("s33", convFactor); - } - return true; - } - - if (methodEPSGCode == EPSG_CODE_METHOD_HEIGHT_DEPTH_REVERSAL) { - formatter->addStep("axisswap"); - formatter->addParam("order", "1,2,-3"); - return true; - } - - return false; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -PointMotionOperation::~PointMotionOperation() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct ConcatenatedOperation::Private { - std::vector operations_{}; - bool computedName_ = false; - - explicit Private(const std::vector &operationsIn) - : operations_(operationsIn) {} - Private(const Private &) = default; -}; -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -ConcatenatedOperation::~ConcatenatedOperation() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -ConcatenatedOperation::ConcatenatedOperation(const ConcatenatedOperation &other) - : CoordinateOperation(other), - d(internal::make_unique(*(other.d))) {} -//! @endcond - -// --------------------------------------------------------------------------- - -ConcatenatedOperation::ConcatenatedOperation( - const std::vector &operationsIn) - : CoordinateOperation(), d(internal::make_unique(operationsIn)) {} - -// --------------------------------------------------------------------------- - -/** \brief Return the operation steps of the concatenated operation. - * - * @return the operation steps. - */ -const std::vector & -ConcatenatedOperation::operations() const { - return d->operations_; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static bool compareStepCRS(const crs::CRS *a, const crs::CRS *b) { - const auto &aIds = a->identifiers(); - const auto &bIds = b->identifiers(); - if (aIds.size() == 1 && bIds.size() == 1 && - aIds[0]->code() == bIds[0]->code() && - *aIds[0]->codeSpace() == *bIds[0]->codeSpace()) { - return true; - } - return a->_isEquivalentTo(b, util::IComparable::Criterion::EQUIVALENT); -} -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a ConcatenatedOperation - * - * @param properties See \ref general_properties. At minimum the name should - * be - * defined. - * @param operationsIn Vector of the CoordinateOperation steps. - * @param accuracies Vector of positional accuracy (might be empty). - * @return new Transformation. - * @throws InvalidOperation - */ -ConcatenatedOperationNNPtr ConcatenatedOperation::create( - const util::PropertyMap &properties, - const std::vector &operationsIn, - const std::vector - &accuracies) // throw InvalidOperation -{ - if (operationsIn.size() < 2) { - throw InvalidOperation( - "ConcatenatedOperation must have at least 2 operations"); - } - crs::CRSPtr lastTargetCRS; - - crs::CRSPtr interpolationCRS; - bool interpolationCRSValid = true; - for (size_t i = 0; i < operationsIn.size(); i++) { - auto l_sourceCRS = operationsIn[i]->sourceCRS(); - auto l_targetCRS = operationsIn[i]->targetCRS(); - - if (interpolationCRSValid) { - auto subOpInterpCRS = operationsIn[i]->interpolationCRS(); - if (interpolationCRS == nullptr) - interpolationCRS = subOpInterpCRS; - else if (subOpInterpCRS == nullptr || - !(subOpInterpCRS->isEquivalentTo( - interpolationCRS.get(), - util::IComparable::Criterion::EQUIVALENT))) { - interpolationCRS = nullptr; - interpolationCRSValid = false; - } - } - - if (l_sourceCRS == nullptr || l_targetCRS == nullptr) { - throw InvalidOperation("At least one of the operation lacks a " - "source and/or target CRS"); - } - if (i >= 1) { - if (!compareStepCRS(l_sourceCRS.get(), lastTargetCRS.get())) { -#ifdef DEBUG_CONCATENATED_OPERATION - std::cerr << "Step " << i - 1 << ": " - << operationsIn[i - 1]->nameStr() << std::endl; - std::cerr << "Step " << i << ": " << operationsIn[i]->nameStr() - << std::endl; - { - auto f(io::WKTFormatter::create( - io::WKTFormatter::Convention::WKT2_2019)); - std::cerr << "Source CRS of step " << i << ":" << std::endl; - std::cerr << l_sourceCRS->exportToWKT(f.get()) << std::endl; - } - { - auto f(io::WKTFormatter::create( - io::WKTFormatter::Convention::WKT2_2019)); - std::cerr << "Target CRS of step " << i - 1 << ":" - << std::endl; - std::cerr << lastTargetCRS->exportToWKT(f.get()) - << std::endl; - } -#endif - throw InvalidOperation( - "Inconsistent chaining of CRS in operations"); - } - } - lastTargetCRS = l_targetCRS; - } - auto op = ConcatenatedOperation::nn_make_shared( - operationsIn); - op->assignSelf(op); - op->setProperties(properties); - op->setCRSs(NN_NO_CHECK(operationsIn[0]->sourceCRS()), - NN_NO_CHECK(operationsIn.back()->targetCRS()), - interpolationCRS); - op->setAccuracies(accuracies); -#ifdef DEBUG_CONCATENATED_OPERATION - { - auto f( - io::WKTFormatter::create(io::WKTFormatter::Convention::WKT2_2019)); - std::cerr << "ConcatenatedOperation::create()" << std::endl; - std::cerr << op->exportToWKT(f.get()) << std::endl; - } -#endif - return op; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -// --------------------------------------------------------------------------- - -void ConcatenatedOperation::fixStepsDirection( - const crs::CRSNNPtr &concatOpSourceCRS, - const crs::CRSNNPtr &concatOpTargetCRS, - std::vector &operationsInOut) { - - // Set of heuristics to assign CRS to steps, and possibly reverse them. - - const auto isGeographic = [](const crs::CRS *crs) -> bool { - return dynamic_cast(crs) != nullptr; - }; - - const auto isGeocentric = [](const crs::CRS *crs) -> bool { - auto geodCRS = dynamic_cast(crs); - if (geodCRS && geodCRS->coordinateSystem()->axisList().size() == 3) - return true; - return false; - }; - - for (size_t i = 0; i < operationsInOut.size(); ++i) { - auto &op = operationsInOut[i]; - auto l_sourceCRS = op->sourceCRS(); - auto l_targetCRS = op->targetCRS(); - auto conv = dynamic_cast(op.get()); - if (conv && i == 0 && !l_sourceCRS && !l_targetCRS) { - auto derivedCRS = - dynamic_cast(concatOpSourceCRS.get()); - if (derivedCRS) { - if (i + 1 < operationsInOut.size()) { - // use the sourceCRS of the next operation as our target CRS - l_targetCRS = operationsInOut[i + 1]->sourceCRS(); - // except if it looks like the next operation should - // actually be reversed !!! - if (l_targetCRS && - !compareStepCRS(l_targetCRS.get(), - derivedCRS->baseCRS().get()) && - operationsInOut[i + 1]->targetCRS() && - compareStepCRS( - operationsInOut[i + 1]->targetCRS().get(), - derivedCRS->baseCRS().get())) { - l_targetCRS = operationsInOut[i + 1]->targetCRS(); - } - } - if (!l_targetCRS) { - l_targetCRS = derivedCRS->baseCRS().as_nullable(); - } - auto invConv = - util::nn_dynamic_pointer_cast(op); - auto nn_targetCRS = NN_NO_CHECK(l_targetCRS); - if (invConv) { - invConv->inverse()->setCRSs(nn_targetCRS, concatOpSourceCRS, - nullptr); - op->setCRSs(concatOpSourceCRS, nn_targetCRS, nullptr); - } else { - op->setCRSs(nn_targetCRS, concatOpSourceCRS, nullptr); - op = op->inverse(); - } - } else if (i + 1 < operationsInOut.size()) { - /* coverity[copy_paste_error] */ - l_targetCRS = operationsInOut[i + 1]->sourceCRS(); - if (l_targetCRS) { - op->setCRSs(concatOpSourceCRS, NN_NO_CHECK(l_targetCRS), - nullptr); - } - } - } else if (conv && i + 1 == operationsInOut.size() && !l_sourceCRS && - !l_targetCRS) { - auto derivedCRS = - dynamic_cast(concatOpTargetCRS.get()); - if (derivedCRS) { - if (i >= 1) { - // use the sourceCRS of the previous operation as our source - // CRS - l_sourceCRS = operationsInOut[i - 1]->targetCRS(); - // except if it looks like the previous operation should - // actually be reversed !!! - if (l_sourceCRS && - !compareStepCRS(l_sourceCRS.get(), - derivedCRS->baseCRS().get()) && - operationsInOut[i - 1]->sourceCRS() && - compareStepCRS( - operationsInOut[i - 1]->sourceCRS().get(), - derivedCRS->baseCRS().get())) { - l_targetCRS = operationsInOut[i - 1]->sourceCRS(); - } - } - if (!l_sourceCRS) { - l_sourceCRS = derivedCRS->baseCRS().as_nullable(); - } - op->setCRSs(NN_NO_CHECK(l_sourceCRS), concatOpTargetCRS, - nullptr); - } else if (i >= 1) { - l_sourceCRS = operationsInOut[i - 1]->targetCRS(); - if (l_sourceCRS) { - derivedCRS = dynamic_cast( - l_sourceCRS.get()); - if (conv->isEquivalentTo( - derivedCRS->derivingConversion().get(), - util::IComparable::Criterion::EQUIVALENT)) { - op->setCRSs(concatOpTargetCRS, NN_NO_CHECK(l_sourceCRS), - nullptr); - op = op->inverse(); - } - op->setCRSs(NN_NO_CHECK(l_sourceCRS), concatOpTargetCRS, - nullptr); - } - } - } else if (conv && i > 0 && i < operationsInOut.size() - 1) { - // For an intermediate conversion, use the target CRS of the - // previous step and the source CRS of the next step - l_sourceCRS = operationsInOut[i - 1]->targetCRS(); - l_targetCRS = operationsInOut[i + 1]->sourceCRS(); - if (l_sourceCRS && l_targetCRS) { - op->setCRSs(NN_NO_CHECK(l_sourceCRS), NN_NO_CHECK(l_targetCRS), - nullptr); - } - } else if (!conv && l_sourceCRS && l_targetCRS) { - - // Transformations might be mentioned in their forward directions, - // whereas we should instead use the reverse path. - auto prevOpTarget = (i == 0) ? concatOpSourceCRS.as_nullable() - : operationsInOut[i - 1]->targetCRS(); - if (compareStepCRS(l_sourceCRS.get(), prevOpTarget.get())) { - // do nothing - } else if (compareStepCRS(l_targetCRS.get(), prevOpTarget.get())) { - op = op->inverse(); - } - // Below is needed for EPSG:9103 which chains NAD83(2011) geographic - // 2D with NAD83(2011) geocentric - else if (l_sourceCRS->nameStr() == prevOpTarget->nameStr() && - ((isGeographic(l_sourceCRS.get()) && - isGeocentric(prevOpTarget.get())) || - (isGeocentric(l_sourceCRS.get()) && - isGeographic(prevOpTarget.get())))) { - auto newOp(Conversion::createGeographicGeocentric( - NN_NO_CHECK(prevOpTarget), NN_NO_CHECK(l_sourceCRS))); - operationsInOut.insert(operationsInOut.begin() + i, newOp); - } else if (l_targetCRS->nameStr() == prevOpTarget->nameStr() && - ((isGeographic(l_targetCRS.get()) && - isGeocentric(prevOpTarget.get())) || - (isGeocentric(l_targetCRS.get()) && - isGeographic(prevOpTarget.get())))) { - auto newOp(Conversion::createGeographicGeocentric( - NN_NO_CHECK(prevOpTarget), NN_NO_CHECK(l_targetCRS))); - operationsInOut.insert(operationsInOut.begin() + i, newOp); - } - } - } - - if (!operationsInOut.empty()) { - auto l_sourceCRS = operationsInOut.front()->sourceCRS(); - if (l_sourceCRS && - !compareStepCRS(l_sourceCRS.get(), concatOpSourceCRS.get())) { - throw InvalidOperation("The source CRS of the first step of " - "concatenated operation is not the same " - "as the source CRS of the concatenated " - "operation itself"); - } - - auto l_targetCRS = operationsInOut.back()->targetCRS(); - if (l_targetCRS && - !compareStepCRS(l_targetCRS.get(), concatOpTargetCRS.get())) { - if (l_targetCRS->nameStr() == concatOpTargetCRS->nameStr() && - ((isGeographic(l_targetCRS.get()) && - isGeocentric(concatOpTargetCRS.get())) || - (isGeocentric(l_targetCRS.get()) && - isGeographic(concatOpTargetCRS.get())))) { - auto newOp(Conversion::createGeographicGeocentric( - NN_NO_CHECK(l_targetCRS), concatOpTargetCRS)); - operationsInOut.push_back(newOp); - } else { - throw InvalidOperation("The target CRS of the last step of " - "concatenated operation is not the same " - "as the target CRS of the concatenated " - "operation itself"); - } - } - } -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static std::string computeConcatenatedName( - const std::vector &flattenOps) { - std::string name; - for (const auto &subOp : flattenOps) { - if (!name.empty()) { - name += " + "; - } - const auto &l_name = subOp->nameStr(); - if (l_name.empty()) { - name += "unnamed"; - } else { - name += l_name; - } - } - return name; -} -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a ConcatenatedOperation, or return a single - * coordinate - * operation. - * - * This computes its accuracy from the sum of its member operations, its - * extent - * - * @param operationsIn Vector of the CoordinateOperation steps. - * @param checkExtent Whether we should check the non-emptyness of the - * intersection - * of the extents of the operations - * @throws InvalidOperation - */ -CoordinateOperationNNPtr ConcatenatedOperation::createComputeMetadata( - const std::vector &operationsIn, - bool checkExtent) // throw InvalidOperation -{ - util::PropertyMap properties; - - if (operationsIn.size() == 1) { - return operationsIn[0]; - } - - std::vector flattenOps; - bool hasBallparkTransformation = false; - for (const auto &subOp : operationsIn) { - hasBallparkTransformation |= subOp->hasBallparkTransformation(); - auto subOpConcat = - dynamic_cast(subOp.get()); - if (subOpConcat) { - auto subOps = subOpConcat->operations(); - for (const auto &subSubOp : subOps) { - flattenOps.emplace_back(subSubOp); - } - } else { - flattenOps.emplace_back(subOp); - } - } - if (flattenOps.size() == 1) { - return flattenOps[0]; - } - - properties.set(common::IdentifiedObject::NAME_KEY, - computeConcatenatedName(flattenOps)); - - bool emptyIntersection = false; - auto extent = getExtent(flattenOps, false, emptyIntersection); - if (checkExtent && emptyIntersection) { - std::string msg( - "empty intersection of area of validity of concatenated " - "operations"); - throw InvalidOperationEmptyIntersection(msg); - } - if (extent) { - properties.set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - NN_NO_CHECK(extent)); - } - - std::vector accuracies; - const double accuracy = getAccuracy(flattenOps); - if (accuracy >= 0.0) { - accuracies.emplace_back( - metadata::PositionalAccuracy::create(toString(accuracy))); - } - - auto op = create(properties, flattenOps, accuracies); - op->setHasBallparkTransformation(hasBallparkTransformation); - op->d->computedName_ = true; - return op; -} - -// --------------------------------------------------------------------------- - -CoordinateOperationNNPtr ConcatenatedOperation::inverse() const { - std::vector inversedOperations; - auto l_operations = operations(); - inversedOperations.reserve(l_operations.size()); - for (const auto &operation : l_operations) { - inversedOperations.emplace_back(operation->inverse()); - } - std::reverse(inversedOperations.begin(), inversedOperations.end()); - - auto properties = createPropertiesForInverse(this, false, false); - if (d->computedName_) { - properties.set(common::IdentifiedObject::NAME_KEY, - computeConcatenatedName(inversedOperations)); - } - - auto op = - create(properties, inversedOperations, coordinateOperationAccuracies()); - op->d->computedName_ = d->computedName_; - op->setHasBallparkTransformation(hasBallparkTransformation()); - return op; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -void ConcatenatedOperation::_exportToWKT(io::WKTFormatter *formatter) const { - const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; - if (!isWKT2 || !formatter->use2019Keywords()) { - throw io::FormattingException( - "Transformation can only be exported to WKT2:2019"); - } - - formatter->startNode(io::WKTConstants::CONCATENATEDOPERATION, - !identifiers().empty()); - formatter->addQuotedString(nameStr()); - - if (formatter->use2019Keywords()) { - const auto &version = operationVersion(); - if (version.has_value()) { - formatter->startNode(io::WKTConstants::VERSION, false); - formatter->addQuotedString(*version); - formatter->endNode(); - } - } - - exportSourceCRSAndTargetCRSToWKT(this, formatter); - - const bool canExportOperationId = - !(formatter->idOnTopLevelOnly() && formatter->topLevelHasId()); - - const bool hasDomains = !domains().empty(); - if (hasDomains) { - formatter->pushDisableUsage(); - } - - for (const auto &operation : operations()) { - formatter->startNode(io::WKTConstants::STEP, false); - if (canExportOperationId && !operation->identifiers().empty()) { - // fake that top node has no id, so that the operation id is - // considered - formatter->pushHasId(false); - operation->_exportToWKT(formatter); - formatter->popHasId(); - } else { - operation->_exportToWKT(formatter); - } - formatter->endNode(); - } - - if (hasDomains) { - formatter->popDisableUsage(); - } - - ObjectUsage::baseExportToWKT(formatter); - formatter->endNode(); -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -void ConcatenatedOperation::_exportToJSON( - io::JSONFormatter *formatter) const // throw(FormattingException) -{ - auto writer = formatter->writer(); - auto objectContext(formatter->MakeObjectContext("ConcatenatedOperation", - !identifiers().empty())); - - writer->AddObjKey("name"); - auto l_name = nameStr(); - if (l_name.empty()) { - writer->Add("unnamed"); - } else { - writer->Add(l_name); - } - - writer->AddObjKey("source_crs"); - formatter->setAllowIDInImmediateChild(); - sourceCRS()->_exportToJSON(formatter); - - writer->AddObjKey("target_crs"); - formatter->setAllowIDInImmediateChild(); - targetCRS()->_exportToJSON(formatter); - - writer->AddObjKey("steps"); - { - auto parametersContext(writer->MakeArrayContext(false)); - for (const auto &operation : operations()) { - formatter->setAllowIDInImmediateChild(); - operation->_exportToJSON(formatter); - } - } - - ObjectUsage::baseExportToJSON(formatter); -} - -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -CoordinateOperationNNPtr ConcatenatedOperation::_shallowClone() const { - auto op = - ConcatenatedOperation::nn_make_shared(*this); - std::vector ops; - for (const auto &subOp : d->operations_) { - ops.emplace_back(subOp->shallowClone()); - } - op->d->operations_ = ops; - op->assignSelf(op); - op->setCRSs(this, false); - return util::nn_static_pointer_cast(op); -} -//! @endcond - -// --------------------------------------------------------------------------- - -void ConcatenatedOperation::_exportToPROJString( - io::PROJStringFormatter *formatter) const // throw(FormattingException) -{ - for (const auto &operation : operations()) { - operation->_exportToPROJString(formatter); - } -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -bool ConcatenatedOperation::_isEquivalentTo( - const util::IComparable *other, util::IComparable::Criterion criterion, - const io::DatabaseContextPtr &dbContext) const { - auto otherCO = dynamic_cast(other); - if (otherCO == nullptr || - (criterion == util::IComparable::Criterion::STRICT && - !ObjectUsage::_isEquivalentTo(other, criterion, dbContext))) { - return false; - } - const auto &steps = operations(); - const auto &otherSteps = otherCO->operations(); - if (steps.size() != otherSteps.size()) { - return false; - } - for (size_t i = 0; i < steps.size(); i++) { - if (!steps[i]->_isEquivalentTo(otherSteps[i].get(), criterion, - dbContext)) { - return false; - } - } - return true; -} -//! @endcond - -// --------------------------------------------------------------------------- - -std::set ConcatenatedOperation::gridsNeeded( - const io::DatabaseContextPtr &databaseContext, - bool considerKnownGridsAsAvailable) const { - std::set res; - for (const auto &operation : operations()) { - const auto l_gridsNeeded = operation->gridsNeeded( - databaseContext, considerKnownGridsAsAvailable); - for (const auto &gridDesc : l_gridsNeeded) { - res.insert(gridDesc); - } - } - return res; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct CoordinateOperationContext::Private { - io::AuthorityFactoryPtr authorityFactory_{}; - metadata::ExtentPtr extent_{}; - double accuracy_ = 0.0; - SourceTargetCRSExtentUse sourceAndTargetCRSExtentUse_ = - CoordinateOperationContext::SourceTargetCRSExtentUse::SMALLEST; - SpatialCriterion spatialCriterion_ = - CoordinateOperationContext::SpatialCriterion::STRICT_CONTAINMENT; - bool usePROJNames_ = true; - GridAvailabilityUse gridAvailabilityUse_ = - GridAvailabilityUse::USE_FOR_SORTING; - IntermediateCRSUse allowUseIntermediateCRS_ = CoordinateOperationContext:: - IntermediateCRSUse::IF_NO_DIRECT_TRANSFORMATION; - std::vector> - intermediateCRSAuthCodes_{}; - bool discardSuperseded_ = true; - bool allowBallpark_ = true; -}; -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -CoordinateOperationContext::~CoordinateOperationContext() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -CoordinateOperationContext::CoordinateOperationContext() - : d(internal::make_unique()) {} - -// --------------------------------------------------------------------------- - -/** \brief Return the authority factory, or null */ -const io::AuthorityFactoryPtr & -CoordinateOperationContext::getAuthorityFactory() const { - return d->authorityFactory_; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the desired area of interest, or null */ -const metadata::ExtentPtr & -CoordinateOperationContext::getAreaOfInterest() const { - return d->extent_; -} - -// --------------------------------------------------------------------------- - -/** \brief Set the desired area of interest, or null */ -void CoordinateOperationContext::setAreaOfInterest( - const metadata::ExtentPtr &extent) { - d->extent_ = extent; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the desired accuracy (in metre), or 0 */ -double CoordinateOperationContext::getDesiredAccuracy() const { - return d->accuracy_; -} - -// --------------------------------------------------------------------------- - -/** \brief Set the desired accuracy (in metre), or 0 */ -void CoordinateOperationContext::setDesiredAccuracy(double accuracy) { - d->accuracy_ = accuracy; -} - -// --------------------------------------------------------------------------- - -/** \brief Return whether ballpark transformations are allowed */ -bool CoordinateOperationContext::getAllowBallparkTransformations() const { - return d->allowBallpark_; -} - -// --------------------------------------------------------------------------- - -/** \brief Set whether ballpark transformations are allowed */ -void CoordinateOperationContext::setAllowBallparkTransformations(bool allow) { - d->allowBallpark_ = allow; -} - -// --------------------------------------------------------------------------- - -/** \brief Set how source and target CRS extent should be used - * when considering if a transformation can be used (only takes effect if - * no area of interest is explicitly defined). - * - * The default is - * CoordinateOperationContext::SourceTargetCRSExtentUse::SMALLEST. - */ -void CoordinateOperationContext::setSourceAndTargetCRSExtentUse( - SourceTargetCRSExtentUse use) { - d->sourceAndTargetCRSExtentUse_ = use; -} - -// --------------------------------------------------------------------------- - -/** \brief Return how source and target CRS extent should be used - * when considering if a transformation can be used (only takes effect if - * no area of interest is explicitly defined). - * - * The default is - * CoordinateOperationContext::SourceTargetCRSExtentUse::SMALLEST. - */ -CoordinateOperationContext::SourceTargetCRSExtentUse -CoordinateOperationContext::getSourceAndTargetCRSExtentUse() const { - return d->sourceAndTargetCRSExtentUse_; -} - -// --------------------------------------------------------------------------- - -/** \brief Set the spatial criterion to use when comparing the area of - * validity - * of coordinate operations with the area of interest / area of validity of - * source and target CRS. - * - * The default is STRICT_CONTAINMENT. - */ -void CoordinateOperationContext::setSpatialCriterion( - SpatialCriterion criterion) { - d->spatialCriterion_ = criterion; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the spatial criterion to use when comparing the area of - * validity - * of coordinate operations with the area of interest / area of validity of - * source and target CRS. - * - * The default is STRICT_CONTAINMENT. - */ -CoordinateOperationContext::SpatialCriterion -CoordinateOperationContext::getSpatialCriterion() const { - return d->spatialCriterion_; -} - -// --------------------------------------------------------------------------- - -/** \brief Set whether PROJ alternative grid names should be substituted to - * the official authority names. - * - * This only has effect is an authority factory with a non-null database context - * has been attached to this context. - * - * If set to false, it is still possible to - * obtain later the substitution by using io::PROJStringFormatter::create() - * with a non-null database context. - * - * The default is true. - */ -void CoordinateOperationContext::setUsePROJAlternativeGridNames( - bool usePROJNames) { - d->usePROJNames_ = usePROJNames; -} - -// --------------------------------------------------------------------------- - -/** \brief Return whether PROJ alternative grid names should be substituted to - * the official authority names. - * - * The default is true. - */ -bool CoordinateOperationContext::getUsePROJAlternativeGridNames() const { - return d->usePROJNames_; -} - -// --------------------------------------------------------------------------- - -/** \brief Return whether transformations that are superseded (but not - * deprecated) - * should be discarded. - * - * The default is true. - */ -bool CoordinateOperationContext::getDiscardSuperseded() const { - return d->discardSuperseded_; -} - -// --------------------------------------------------------------------------- - -/** \brief Set whether transformations that are superseded (but not deprecated) - * should be discarded. - * - * The default is true. - */ -void CoordinateOperationContext::setDiscardSuperseded(bool discard) { - d->discardSuperseded_ = discard; -} - -// --------------------------------------------------------------------------- - -/** \brief Set how grid availability is used. - * - * The default is USE_FOR_SORTING. - */ -void CoordinateOperationContext::setGridAvailabilityUse( - GridAvailabilityUse use) { - d->gridAvailabilityUse_ = use; -} - -// --------------------------------------------------------------------------- - -/** \brief Return how grid availability is used. - * - * The default is USE_FOR_SORTING. - */ -CoordinateOperationContext::GridAvailabilityUse -CoordinateOperationContext::getGridAvailabilityUse() const { - return d->gridAvailabilityUse_; -} - -// --------------------------------------------------------------------------- - -/** \brief Set whether an intermediate pivot CRS can be used for researching - * coordinate operations between a source and target CRS. - * - * Concretely if in the database there is an operation from A to C - * (or C to A), and another one from C to B (or B to C), but no direct - * operation between A and B, setting this parameter to - * ALWAYS/IF_NO_DIRECT_TRANSFORMATION, allow chaining both operations. - * - * The current implementation is limited to researching one intermediate - * step. - * - * By default, with the IF_NO_DIRECT_TRANSFORMATION stratgey, all potential - * C candidates will be used if there is no direct transformation. - */ -void CoordinateOperationContext::setAllowUseIntermediateCRS( - IntermediateCRSUse use) { - d->allowUseIntermediateCRS_ = use; -} - -// --------------------------------------------------------------------------- - -/** \brief Return whether an intermediate pivot CRS can be used for researching - * coordinate operations between a source and target CRS. - * - * Concretely if in the database there is an operation from A to C - * (or C to A), and another one from C to B (or B to C), but no direct - * operation between A and B, setting this parameter to - * ALWAYS/IF_NO_DIRECT_TRANSFORMATION, allow chaining both operations. - * - * The default is IF_NO_DIRECT_TRANSFORMATION. - */ -CoordinateOperationContext::IntermediateCRSUse -CoordinateOperationContext::getAllowUseIntermediateCRS() const { - return d->allowUseIntermediateCRS_; -} - -// --------------------------------------------------------------------------- - -/** \brief Restrict the potential pivot CRSs that can be used when trying to - * build a coordinate operation between two CRS that have no direct operation. - * - * @param intermediateCRSAuthCodes a vector of (auth_name, code) that can be - * used as potential pivot RS - */ -void CoordinateOperationContext::setIntermediateCRS( - const std::vector> - &intermediateCRSAuthCodes) { - d->intermediateCRSAuthCodes_ = intermediateCRSAuthCodes; -} - -// --------------------------------------------------------------------------- - -/** \brief Return the potential pivot CRSs that can be used when trying to - * build a coordinate operation between two CRS that have no direct operation. - * - */ -const std::vector> & -CoordinateOperationContext::getIntermediateCRS() const { - return d->intermediateCRSAuthCodes_; -} - -// --------------------------------------------------------------------------- - -/** \brief Creates a context for a coordinate operation. - * - * If a non null authorityFactory is provided, the resulting context should - * not be used simultaneously by more than one thread. - * - * If authorityFactory->getAuthority() is the empty string, then coordinate - * operations from any authority will be searched, with the restrictions set - * in the authority_to_authority_preference database table. - * If authorityFactory->getAuthority() is set to "any", then coordinate - * operations from any authority will be searched - * If authorityFactory->getAuthority() is a non-empty string different of "any", - * then coordinate operatiosn will be searched only in that authority namespace. - * - * @param authorityFactory Authority factory, or null if no database lookup - * is allowed. - * Use io::authorityFactory::create(context, std::string()) to allow all - * authorities to be used. - * @param extent Area of interest, or null if none is known. - * @param accuracy Maximum allowed accuracy in metre, as specified in or - * 0 to get best accuracy. - * @return a new context. - */ -CoordinateOperationContextNNPtr CoordinateOperationContext::create( - const io::AuthorityFactoryPtr &authorityFactory, - const metadata::ExtentPtr &extent, double accuracy) { - auto ctxt = NN_NO_CHECK( - CoordinateOperationContext::make_unique()); - ctxt->d->authorityFactory_ = authorityFactory; - ctxt->d->extent_ = extent; - ctxt->d->accuracy_ = accuracy; - return ctxt; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -struct CoordinateOperationFactory::Private { - - struct Context { - // This is the extent of the source CRS and target CRS of the initial - // CoordinateOperationFactory::createOperations() public call, not - // necessarily the ones of intermediate - // CoordinateOperationFactory::Private::createOperations() calls. - // This is used to compare transformations area of use against the - // area of use of the source & target CRS. - const metadata::ExtentPtr &extent1; - const metadata::ExtentPtr &extent2; - const CoordinateOperationContextNNPtr &context; - bool inCreateOperationsWithDatumPivotAntiRecursion = false; - bool inCreateOperationsGeogToVertWithAlternativeGeog = false; - bool inCreateOperationsGeogToVertWithIntermediateVert = false; - bool skipHorizontalTransformation = false; - std::map, - std::list>> - cacheNameToCRS{}; - - Context(const metadata::ExtentPtr &extent1In, - const metadata::ExtentPtr &extent2In, - const CoordinateOperationContextNNPtr &contextIn) - : extent1(extent1In), extent2(extent2In), context(contextIn) {} - }; - - static std::vector - createOperations(const crs::CRSNNPtr &sourceCRS, - const crs::CRSNNPtr &targetCRS, Context &context); - - private: - static constexpr bool disallowEmptyIntersection = true; - - static void - buildCRSIds(const crs::CRSNNPtr &crs, Private::Context &context, - std::list> &ids); - - static std::vector findOpsInRegistryDirect( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, bool &resNonEmptyBeforeFiltering); - - static std::vector - findOpsInRegistryDirectTo(const crs::CRSNNPtr &targetCRS, - Private::Context &context); - - static std::vector - findsOpsInRegistryWithIntermediate( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, - bool useCreateBetweenGeodeticCRSWithDatumBasedIntermediates); - - static void createOperationsFromProj4Ext( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - const crs::BoundCRS *boundSrc, const crs::BoundCRS *boundDst, - std::vector &res); - - static bool createOperationsFromDatabase( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::GeodeticCRS *geodSrc, - const crs::GeodeticCRS *geodDst, const crs::GeographicCRS *geogSrc, - const crs::GeographicCRS *geogDst, const crs::VerticalCRS *vertSrc, - const crs::VerticalCRS *vertDst, - std::vector &res); - - static std::vector - createOperationsGeogToVertFromGeoid(const crs::CRSNNPtr &sourceCRS, - const crs::CRSNNPtr &targetCRS, - const crs::VerticalCRS *vertDst, - Context &context); - - static std::vector - createOperationsGeogToVertWithIntermediateVert( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - const crs::VerticalCRS *vertDst, Context &context); - - static std::vector - createOperationsGeogToVertWithAlternativeGeog( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Context &context); - - static void createOperationsFromDatabaseWithVertCRS( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::GeographicCRS *geogSrc, - const crs::GeographicCRS *geogDst, const crs::VerticalCRS *vertSrc, - const crs::VerticalCRS *vertDst, - std::vector &res); - - static void createOperationsGeodToGeod( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::GeodeticCRS *geodSrc, - const crs::GeodeticCRS *geodDst, - std::vector &res); - - static void createOperationsDerivedTo( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::DerivedCRS *derivedSrc, - std::vector &res); - - static void createOperationsBoundToGeog( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::BoundCRS *boundSrc, - const crs::GeographicCRS *geogDst, - std::vector &res); - - static void createOperationsBoundToVert( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::BoundCRS *boundSrc, - const crs::VerticalCRS *vertDst, - std::vector &res); - - static void createOperationsVertToVert( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::VerticalCRS *vertSrc, - const crs::VerticalCRS *vertDst, - std::vector &res); - - static void createOperationsVertToGeog( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::VerticalCRS *vertSrc, - const crs::GeographicCRS *geogDst, - std::vector &res); - - static void createOperationsVertToGeogBallpark( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::VerticalCRS *vertSrc, - const crs::GeographicCRS *geogDst, - std::vector &res); - - static void createOperationsBoundToBound( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::BoundCRS *boundSrc, - const crs::BoundCRS *boundDst, - std::vector &res); - - static void createOperationsCompoundToGeog( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::CompoundCRS *compoundSrc, - const crs::GeographicCRS *geogDst, - std::vector &res); - - static void createOperationsToGeod( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::GeodeticCRS *geodDst, - std::vector &res); - - static void createOperationsCompoundToCompound( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::CompoundCRS *compoundSrc, - const crs::CompoundCRS *compoundDst, - std::vector &res); - - static void createOperationsBoundToCompound( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::BoundCRS *boundSrc, - const crs::CompoundCRS *compoundDst, - std::vector &res); - - static std::vector createOperationsGeogToGeog( - std::vector &res, - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::GeographicCRS *geogSrc, - const crs::GeographicCRS *geogDst); - - static void createOperationsWithDatumPivot( - std::vector &res, - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - const crs::GeodeticCRS *geodSrc, const crs::GeodeticCRS *geodDst, - Context &context); - - static bool - hasPerfectAccuracyResult(const std::vector &res, - const Context &context); - - static void setCRSs(CoordinateOperation *co, const crs::CRSNNPtr &sourceCRS, - const crs::CRSNNPtr &targetCRS); -}; -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -CoordinateOperationFactory::~CoordinateOperationFactory() = default; -//! @endcond - -// --------------------------------------------------------------------------- - -CoordinateOperationFactory::CoordinateOperationFactory() : d(nullptr) {} - -// --------------------------------------------------------------------------- - -/** \brief Find a CoordinateOperation from sourceCRS to targetCRS. - * - * This is a helper of createOperations(), using a coordinate operation - * context - * with no authority factory (so no catalog searching is done), no desired - * accuracy and no area of interest. - * This returns the first operation of the result set of createOperations(), - * or null if none found. - * - * @param sourceCRS source CRS. - * @param targetCRS source CRS. - * @return a CoordinateOperation or nullptr. - */ -CoordinateOperationPtr CoordinateOperationFactory::createOperation( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS) const { - auto res = createOperations( - sourceCRS, targetCRS, - CoordinateOperationContext::create(nullptr, nullptr, 0.0)); - if (!res.empty()) { - return res[0]; - } - return nullptr; -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -// --------------------------------------------------------------------------- - -struct PrecomputedOpCharacteristics { - double area_{}; - double accuracy_{}; - bool isPROJExportable_ = false; - bool hasGrids_ = false; - bool gridsAvailable_ = false; - bool gridsKnown_ = false; - size_t stepCount_ = 0; - bool isApprox_ = false; - bool hasBallparkVertical_ = false; - bool isNullTransformation_ = false; - - PrecomputedOpCharacteristics() = default; - PrecomputedOpCharacteristics(double area, double accuracy, - bool isPROJExportable, bool hasGrids, - bool gridsAvailable, bool gridsKnown, - size_t stepCount, bool isApprox, - bool hasBallparkVertical, - bool isNullTransformation) - : area_(area), accuracy_(accuracy), isPROJExportable_(isPROJExportable), - hasGrids_(hasGrids), gridsAvailable_(gridsAvailable), - gridsKnown_(gridsKnown), stepCount_(stepCount), isApprox_(isApprox), - hasBallparkVertical_(hasBallparkVertical), - isNullTransformation_(isNullTransformation) {} -}; - -// --------------------------------------------------------------------------- - -// We could have used a lambda instead of this old-school way, but -// filterAndSort() is already huge. -struct SortFunction { - - const std::map ↦ - - explicit SortFunction(const std::map &mapIn) - : map(mapIn) {} - - // Sorting function - // Return true if a < b - bool compare(const CoordinateOperationNNPtr &a, - const CoordinateOperationNNPtr &b) const { - auto iterA = map.find(a.get()); - assert(iterA != map.end()); - auto iterB = map.find(b.get()); - assert(iterB != map.end()); - - // CAUTION: the order of the comparisons is extremely important - // to get the intended result. - - if (iterA->second.isPROJExportable_ && - !iterB->second.isPROJExportable_) { - return true; - } - if (!iterA->second.isPROJExportable_ && - iterB->second.isPROJExportable_) { - return false; - } - - if (!iterA->second.isApprox_ && iterB->second.isApprox_) { - return true; - } - if (iterA->second.isApprox_ && !iterB->second.isApprox_) { - return false; - } - - if (!iterA->second.hasBallparkVertical_ && - iterB->second.hasBallparkVertical_) { - return true; - } - if (iterA->second.hasBallparkVertical_ && - !iterB->second.hasBallparkVertical_) { - return false; - } - - if (!iterA->second.isNullTransformation_ && - iterB->second.isNullTransformation_) { - return true; - } - if (iterA->second.isNullTransformation_ && - !iterB->second.isNullTransformation_) { - return false; - } - - // Operations where grids are all available go before other - if (iterA->second.gridsAvailable_ && !iterB->second.gridsAvailable_) { - return true; - } - if (iterB->second.gridsAvailable_ && !iterA->second.gridsAvailable_) { - return false; - } - - // Operations where grids are all known in our DB go before other - if (iterA->second.gridsKnown_ && !iterB->second.gridsKnown_) { - return true; - } - if (iterB->second.gridsKnown_ && !iterA->second.gridsKnown_) { - return false; - } - - // Operations with known accuracy go before those with unknown accuracy - const double accuracyA = iterA->second.accuracy_; - const double accuracyB = iterB->second.accuracy_; - if (accuracyA >= 0 && accuracyB < 0) { - return true; - } - if (accuracyB >= 0 && accuracyA < 0) { - return false; - } - - if (accuracyA < 0 && accuracyB < 0) { - // unknown accuracy ? then prefer operations with grids, which - // are likely to have best practical accuracy - if (iterA->second.hasGrids_ && !iterB->second.hasGrids_) { - return true; - } - if (!iterA->second.hasGrids_ && iterB->second.hasGrids_) { - return false; - } - } - - // Operations with larger non-zero area of use go before those with - // lower one - const double areaA = iterA->second.area_; - const double areaB = iterB->second.area_; - if (areaA > 0) { - if (areaA > areaB) { - return true; - } - if (areaA < areaB) { - return false; - } - } else if (areaB > 0) { - return false; - } - - // Operations with better accuracy go before those with worse one - if (accuracyA >= 0 && accuracyA < accuracyB) { - return true; - } - if (accuracyB >= 0 && accuracyB < accuracyA) { - return false; - } - - if (accuracyA >= 0 && accuracyA == accuracyB) { - // same accuracy ? then prefer operations without grids - if (!iterA->second.hasGrids_ && iterB->second.hasGrids_) { - return true; - } - if (iterA->second.hasGrids_ && !iterB->second.hasGrids_) { - return false; - } - } - - // The less intermediate steps, the better - if (iterA->second.stepCount_ < iterB->second.stepCount_) { - return true; - } - if (iterB->second.stepCount_ < iterA->second.stepCount_) { - return false; - } - - const auto &a_name = a->nameStr(); - const auto &b_name = b->nameStr(); - // The shorter name, the better ? - if (a_name.size() < b_name.size()) { - return true; - } - if (b_name.size() < a_name.size()) { - return false; - } - - // Arbitrary final criterion. We actually return the greater element - // first, so that "Amersfoort to WGS 84 (4)" is presented before - // "Amersfoort to WGS 84 (3)", which is probably a better guess. - - // Except for French NTF (Paris) to NTF, where the (1) conversion - // should be preferred because in the remarks of (2), it is mentioned - // OGP prefers value from IGN Paris (code 1467)... - if (a_name.find("NTF (Paris) to NTF (1)") != std::string::npos && - b_name.find("NTF (Paris) to NTF (2)") != std::string::npos) { - return true; - } - if (a_name.find("NTF (Paris) to NTF (2)") != std::string::npos && - b_name.find("NTF (Paris) to NTF (1)") != std::string::npos) { - return false; - } - if (a_name.find("NTF (Paris) to RGF93 (1)") != std::string::npos && - b_name.find("NTF (Paris) to RGF93 (2)") != std::string::npos) { - return true; - } - if (a_name.find("NTF (Paris) to RGF93 (2)") != std::string::npos && - b_name.find("NTF (Paris) to RGF93 (1)") != std::string::npos) { - return false; - } - - return a_name > b_name; - } - - bool operator()(const CoordinateOperationNNPtr &a, - const CoordinateOperationNNPtr &b) const { - const bool ret = compare(a, b); -#if 0 - std::cerr << a->nameStr() << " < " << b->nameStr() << " : " << ret << std::endl; -#endif - return ret; - } -}; - -// --------------------------------------------------------------------------- - -static size_t getStepCount(const CoordinateOperationNNPtr &op) { - auto concat = dynamic_cast(op.get()); - size_t stepCount = 1; - if (concat) { - stepCount = concat->operations().size(); - } - return stepCount; -} - -// --------------------------------------------------------------------------- - -// Return number of steps that are transformations (and not conversions) -static size_t getTransformationStepCount(const CoordinateOperationNNPtr &op) { - auto concat = dynamic_cast(op.get()); - size_t stepCount = 1; - if (concat) { - stepCount = 0; - for (const auto &subOp : concat->operations()) { - if (dynamic_cast(subOp.get()) == nullptr) { - stepCount++; - } - } - } - return stepCount; -} - -// --------------------------------------------------------------------------- - -static bool isNullTransformation(const std::string &name) { - if (name.find(" + ") != std::string::npos) - return false; - return starts_with(name, BALLPARK_GEOCENTRIC_TRANSLATION) || - starts_with(name, BALLPARK_GEOGRAPHIC_OFFSET) || - starts_with(name, NULL_GEOGRAPHIC_OFFSET) || - starts_with(name, NULL_GEOCENTRIC_TRANSLATION); -} - -// --------------------------------------------------------------------------- - -struct FilterResults { - - FilterResults(const std::vector &sourceListIn, - const CoordinateOperationContextNNPtr &contextIn, - const metadata::ExtentPtr &extent1In, - const metadata::ExtentPtr &extent2In, - bool forceStrictContainmentTest) - : sourceList(sourceListIn), context(contextIn), extent1(extent1In), - extent2(extent2In), areaOfInterest(context->getAreaOfInterest()), - desiredAccuracy(context->getDesiredAccuracy()), - sourceAndTargetCRSExtentUse( - context->getSourceAndTargetCRSExtentUse()) { - - computeAreaOfInterest(); - filterOut(forceStrictContainmentTest); - } - - FilterResults &andSort() { - sort(); - - // And now that we have a sorted list, we can remove uninteresting - // results - // ... - removeSyntheticNullTransforms(); - removeUninterestingOps(); - removeDuplicateOps(); - removeSyntheticNullTransforms(); - return *this; - } - - // ---------------------------------------------------------------------- - - // cppcheck-suppress functionStatic - const std::vector &getRes() { return res; } - - // ---------------------------------------------------------------------- - private: - const std::vector &sourceList; - const CoordinateOperationContextNNPtr &context; - const metadata::ExtentPtr &extent1; - const metadata::ExtentPtr &extent2; - metadata::ExtentPtr areaOfInterest; - const double desiredAccuracy = context->getDesiredAccuracy(); - const CoordinateOperationContext::SourceTargetCRSExtentUse - sourceAndTargetCRSExtentUse; - - bool hasOpThatContainsAreaOfInterestAndNoGrid = false; - std::vector res{}; - - // ---------------------------------------------------------------------- - void computeAreaOfInterest() { - - // Compute an area of interest from the CRS extent if the user did - // not specify one - if (!areaOfInterest) { - if (sourceAndTargetCRSExtentUse == - CoordinateOperationContext::SourceTargetCRSExtentUse:: - INTERSECTION) { - if (extent1 && extent2) { - areaOfInterest = - extent1->intersection(NN_NO_CHECK(extent2)); - } - } else if (sourceAndTargetCRSExtentUse == - CoordinateOperationContext::SourceTargetCRSExtentUse:: - SMALLEST) { - if (extent1 && extent2) { - if (getPseudoArea(extent1) < getPseudoArea(extent2)) { - areaOfInterest = extent1; - } else { - areaOfInterest = extent2; - } - } else if (extent1) { - areaOfInterest = extent1; - } else { - areaOfInterest = extent2; - } - } - } - } - - // --------------------------------------------------------------------------- - - void filterOut(bool forceStrictContainmentTest) { - - // Filter out operations that do not match the expected accuracy - // and area of use. - const auto spatialCriterion = - forceStrictContainmentTest - ? CoordinateOperationContext::SpatialCriterion:: - STRICT_CONTAINMENT - : context->getSpatialCriterion(); - bool hasOnlyBallpark = true; - bool hasNonBallparkWithoutExtent = false; - bool hasNonBallparkOpWithExtent = false; - const bool allowBallpark = context->getAllowBallparkTransformations(); - for (const auto &op : sourceList) { - if (desiredAccuracy != 0) { - const double accuracy = getAccuracy(op); - if (accuracy < 0 || accuracy > desiredAccuracy) { - continue; - } - } - if (!allowBallpark && op->hasBallparkTransformation()) { - continue; - } - if (areaOfInterest) { - bool emptyIntersection = false; - auto extent = getExtent(op, true, emptyIntersection); - if (!extent) { - if (!op->hasBallparkTransformation()) { - hasNonBallparkWithoutExtent = true; - } - continue; - } - if (!op->hasBallparkTransformation()) { - hasNonBallparkOpWithExtent = true; - } - bool extentContains = - extent->contains(NN_NO_CHECK(areaOfInterest)); - if (!hasOpThatContainsAreaOfInterestAndNoGrid && - extentContains) { - if (!op->hasBallparkTransformation() && - op->gridsNeeded(nullptr, true).empty()) { - hasOpThatContainsAreaOfInterestAndNoGrid = true; - } - } - if (spatialCriterion == - CoordinateOperationContext::SpatialCriterion:: - STRICT_CONTAINMENT && - !extentContains) { - continue; - } - if (spatialCriterion == - CoordinateOperationContext::SpatialCriterion:: - PARTIAL_INTERSECTION && - !extent->intersects(NN_NO_CHECK(areaOfInterest))) { - continue; - } - } else if (sourceAndTargetCRSExtentUse == - CoordinateOperationContext::SourceTargetCRSExtentUse:: - BOTH) { - bool emptyIntersection = false; - auto extent = getExtent(op, true, emptyIntersection); - if (!extent) { - if (!op->hasBallparkTransformation()) { - hasNonBallparkWithoutExtent = true; - } - continue; - } - if (!op->hasBallparkTransformation()) { - hasNonBallparkOpWithExtent = true; - } - bool extentContainsExtent1 = - !extent1 || extent->contains(NN_NO_CHECK(extent1)); - bool extentContainsExtent2 = - !extent2 || extent->contains(NN_NO_CHECK(extent2)); - if (!hasOpThatContainsAreaOfInterestAndNoGrid && - extentContainsExtent1 && extentContainsExtent2) { - if (!op->hasBallparkTransformation() && - op->gridsNeeded(nullptr, true).empty()) { - hasOpThatContainsAreaOfInterestAndNoGrid = true; - } - } - if (spatialCriterion == - CoordinateOperationContext::SpatialCriterion:: - STRICT_CONTAINMENT) { - if (!extentContainsExtent1 || !extentContainsExtent2) { - continue; - } - } else if (spatialCriterion == - CoordinateOperationContext::SpatialCriterion:: - PARTIAL_INTERSECTION) { - bool extentIntersectsExtent1 = - !extent1 || extent->intersects(NN_NO_CHECK(extent1)); - bool extentIntersectsExtent2 = - extent2 && extent->intersects(NN_NO_CHECK(extent2)); - if (!extentIntersectsExtent1 || !extentIntersectsExtent2) { - continue; - } - } - } - if (!op->hasBallparkTransformation()) { - hasOnlyBallpark = false; - } - res.emplace_back(op); - } - - // In case no operation has an extent and no result is found, - // retain all initial operations that match accuracy criterion. - if ((res.empty() && !hasNonBallparkOpWithExtent) || - (hasOnlyBallpark && hasNonBallparkWithoutExtent)) { - for (const auto &op : sourceList) { - if (desiredAccuracy != 0) { - const double accuracy = getAccuracy(op); - if (accuracy < 0 || accuracy > desiredAccuracy) { - continue; - } - } - if (!allowBallpark && op->hasBallparkTransformation()) { - continue; - } - res.emplace_back(op); - } - } - } - - // ---------------------------------------------------------------------- - - void sort() { - - // Precompute a number of parameters for each operation that will be - // useful for the sorting. - std::map map; - const auto gridAvailabilityUse = context->getGridAvailabilityUse(); - for (const auto &op : res) { - bool dummy = false; - auto extentOp = getExtent(op, true, dummy); - double area = 0.0; - if (extentOp) { - if (areaOfInterest) { - area = getPseudoArea( - extentOp->intersection(NN_NO_CHECK(areaOfInterest))); - } else if (extent1 && extent2) { - auto x = extentOp->intersection(NN_NO_CHECK(extent1)); - auto y = extentOp->intersection(NN_NO_CHECK(extent2)); - area = getPseudoArea(x) + getPseudoArea(y) - - ((x && y) - ? getPseudoArea(x->intersection(NN_NO_CHECK(y))) - : 0.0); - } else if (extent1) { - area = getPseudoArea( - extentOp->intersection(NN_NO_CHECK(extent1))); - } else if (extent2) { - area = getPseudoArea( - extentOp->intersection(NN_NO_CHECK(extent2))); - } else { - area = getPseudoArea(extentOp); - } - } - - bool hasGrids = false; - bool gridsAvailable = true; - bool gridsKnown = true; - if (context->getAuthorityFactory()) { - const auto gridsNeeded = op->gridsNeeded( - context->getAuthorityFactory()->databaseContext(), - gridAvailabilityUse == - CoordinateOperationContext::GridAvailabilityUse:: - KNOWN_AVAILABLE); - for (const auto &gridDesc : gridsNeeded) { - hasGrids = true; - if (gridAvailabilityUse == - CoordinateOperationContext::GridAvailabilityUse:: - USE_FOR_SORTING && - !gridDesc.available) { - gridsAvailable = false; - } - if (gridDesc.packageName.empty() && - !(!gridDesc.url.empty() && gridDesc.openLicense) && - !gridDesc.available) { - gridsKnown = false; - } - } - } - - const auto stepCount = getStepCount(op); - - bool isPROJExportable = false; - auto formatter = io::PROJStringFormatter::create(); - try { - op->exportToPROJString(formatter.get()); - // Grids might be missing, but at least this is something - // PROJ could potentially process - isPROJExportable = true; - } catch (const std::exception &) { - } - -#if 0 - std::cerr << op->nameStr() << " "; - std::cerr << area << " "; - std::cerr << getAccuracy(op) << " "; - std::cerr << isPROJExportable << " "; - std::cerr << hasGrids << " "; - std::cerr << gridsAvailable << " "; - std::cerr << gridsKnown << " "; - std::cerr << stepCount << " "; - std::cerr << op->hasBallparkTransformation() << " "; - std::cerr << isNullTransformation(op->nameStr()) << " "; - std::cerr << std::endl; -#endif - map[op.get()] = PrecomputedOpCharacteristics( - area, getAccuracy(op), isPROJExportable, hasGrids, - gridsAvailable, gridsKnown, stepCount, - op->hasBallparkTransformation(), - op->nameStr().find("ballpark vertical transformation") != - std::string::npos, - isNullTransformation(op->nameStr())); - } - - // Sort ! - SortFunction sortFunc(map); - std::sort(res.begin(), res.end(), sortFunc); - -// Debug code to check consistency of the sort function -#ifdef DEBUG_SORT - constexpr bool debugSort = true; -#elif !defined(NDEBUG) - const bool debugSort = getenv("PROJ_DEBUG_SORT_FUNCT") != nullptr; -#endif -#if defined(DEBUG_SORT) || !defined(NDEBUG) - if (debugSort) { - const bool assertIfIssue = - !(getenv("PROJ_DEBUG_SORT_FUNCT_ASSERT") != nullptr); - for (size_t i = 0; i < res.size(); ++i) { - for (size_t j = i + 1; j < res.size(); ++j) { - if (sortFunc(res[j], res[i])) { -#ifdef DEBUG_SORT - std::cerr << "Sorting issue with entry " << i << "(" - << res[i]->nameStr() << ") and " << j << "(" - << res[j]->nameStr() << ")" << std::endl; -#endif - if (assertIfIssue) { - assert(false); - } - } - } - } - } -#endif - } - - // ---------------------------------------------------------------------- - - void removeSyntheticNullTransforms() { - - // If we have more than one result, and than the last result is the - // default "Ballpark geographic offset" or "Ballpark geocentric - // translation" operations we have synthetized, and that at least one - // operation has the desired area of interest and does not require the - // use of grids, remove it as all previous results are necessarily - // better - if (hasOpThatContainsAreaOfInterestAndNoGrid && res.size() > 1) { - const auto &opLast = res.back(); - if (opLast->hasBallparkTransformation() || - isNullTransformation(opLast->nameStr())) { - std::vector resTemp; - for (size_t i = 0; i < res.size() - 1; i++) { - resTemp.emplace_back(res[i]); - } - res = std::move(resTemp); - } - } - } - - // ---------------------------------------------------------------------- - - void removeUninterestingOps() { - - // Eliminate operations that bring nothing, ie for a given area of use, - // do not keep operations that have similar or worse accuracy, but - // involve more (non conversion) steps - std::vector resTemp; - metadata::ExtentPtr lastExtent; - double lastAccuracy = -1; - size_t lastStepCount = 0; - CoordinateOperationPtr lastOp; - - bool first = true; - for (const auto &op : res) { - const auto curAccuracy = getAccuracy(op); - bool dummy = false; - const auto curExtent = getExtent(op, true, dummy); - const auto curStepCount = getTransformationStepCount(op); - - if (first) { - resTemp.emplace_back(op); - first = false; - } else { - if (lastOp->_isEquivalentTo(op.get())) { - continue; - } - const bool sameExtent = - ((!curExtent && !lastExtent) || - (curExtent && lastExtent && - curExtent->contains(NN_NO_CHECK(lastExtent)) && - lastExtent->contains(NN_NO_CHECK(curExtent)))); - if (((curAccuracy >= lastAccuracy && lastAccuracy >= 0) || - (curAccuracy < 0 && lastAccuracy >= 0)) && - sameExtent && curStepCount > lastStepCount) { - continue; - } - - resTemp.emplace_back(op); - } - - lastOp = op.as_nullable(); - lastStepCount = curStepCount; - lastExtent = curExtent; - lastAccuracy = curAccuracy; - } - res = std::move(resTemp); - } - - // ---------------------------------------------------------------------- - - // cppcheck-suppress functionStatic - void removeDuplicateOps() { - - if (res.size() <= 1) { - return; - } - - // When going from EPSG:4807 (NTF Paris) to EPSG:4171 (RGC93), we get - // EPSG:7811, NTF (Paris) to RGF93 (2), 1 m - // and unknown id, NTF (Paris) to NTF (1) + Inverse of RGF93 to NTF (2), - // 1 m - // both have same PROJ string and extent - // Do not keep the later (that has more steps) as it adds no value. - - std::set setPROJPlusExtent; - std::vector resTemp; - for (const auto &op : res) { - auto formatter = io::PROJStringFormatter::create(); - try { - std::string key(op->exportToPROJString(formatter.get())); - bool dummy = false; - auto extentOp = getExtent(op, true, dummy); - if (extentOp) { - const auto &geogElts = extentOp->geographicElements(); - if (geogElts.size() == 1) { - auto bbox = dynamic_cast< - const metadata::GeographicBoundingBox *>( - geogElts[0].get()); - if (bbox) { - double w = bbox->westBoundLongitude(); - double s = bbox->southBoundLatitude(); - double e = bbox->eastBoundLongitude(); - double n = bbox->northBoundLatitude(); - key += "-"; - key += toString(w); - key += "-"; - key += toString(s); - key += "-"; - key += toString(e); - key += "-"; - key += toString(n); - } - } - } - - if (setPROJPlusExtent.find(key) == setPROJPlusExtent.end()) { - resTemp.emplace_back(op); - setPROJPlusExtent.insert(key); - } - } catch (const std::exception &) { - resTemp.emplace_back(op); - } - } - res = std::move(resTemp); - } -}; - -// --------------------------------------------------------------------------- - -/** \brief Filter operations and sort them given context. - * - * If a desired accuracy is specified, only keep operations whose accuracy - * is at least the desired one. - * If an area of interest is specified, only keep operations whose area of - * use include the area of interest. - * Then sort remaining operations by descending area of use, and increasing - * accuracy. - */ -static std::vector -filterAndSort(const std::vector &sourceList, - const CoordinateOperationContextNNPtr &context, - const metadata::ExtentPtr &extent1, - const metadata::ExtentPtr &extent2) { -#ifdef TRACE_CREATE_OPERATIONS - ENTER_FUNCTION(); - logTrace("number of results before filter and sort: " + - toString(static_cast(sourceList.size()))); -#endif - auto resFiltered = - FilterResults(sourceList, context, extent1, extent2, false) - .andSort() - .getRes(); -#ifdef TRACE_CREATE_OPERATIONS - logTrace("number of results after filter and sort: " + - toString(static_cast(resFiltered.size()))); -#endif - return resFiltered; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -// Apply the inverse() method on all elements of the input list -static std::vector -applyInverse(const std::vector &list) { - auto res = list; - for (auto &op : res) { -#ifdef DEBUG - auto opNew = op->inverse(); - assert(opNew->targetCRS()->isEquivalentTo(op->sourceCRS().get())); - assert(opNew->sourceCRS()->isEquivalentTo(op->targetCRS().get())); - op = opNew; -#else - op = op->inverse(); -#endif - } - return res; -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -void CoordinateOperationFactory::Private::buildCRSIds( - const crs::CRSNNPtr &crs, Private::Context &context, - std::list> &ids) { - const auto &authFactory = context.context->getAuthorityFactory(); - assert(authFactory); - for (const auto &id : crs->identifiers()) { - const auto &authName = *(id->codeSpace()); - const auto &code = id->code(); - if (!authName.empty()) { - const auto tmpAuthFactory = io::AuthorityFactory::create( - authFactory->databaseContext(), authName); - try { - // Consistency check for the ID attached to the object. - // See https://github.com/OSGeo/PROJ/issues/1982 where EPSG:4656 - // is attached to a GeographicCRS whereas it is a ProjectedCRS - if (tmpAuthFactory->createCoordinateReferenceSystem(code) - ->_isEquivalentTo( - crs.get(), - util::IComparable::Criterion:: - EQUIVALENT_EXCEPT_AXIS_ORDER_GEOGCRS)) { - ids.emplace_back(authName, code); - } else { - // TODO? log this inconsistency - } - } catch (const std::exception &) { - // TODO? log this inconsistency - } - } - } - if (ids.empty()) { - std::vector allowedObjects; - auto geogCRS = dynamic_cast(crs.get()); - if (geogCRS) { - allowedObjects.push_back( - geogCRS->coordinateSystem()->axisList().size() == 2 - ? io::AuthorityFactory::ObjectType::GEOGRAPHIC_2D_CRS - : io::AuthorityFactory::ObjectType::GEOGRAPHIC_3D_CRS); - } else if (dynamic_cast(crs.get())) { - allowedObjects.push_back( - io::AuthorityFactory::ObjectType::PROJECTED_CRS); - } else if (dynamic_cast(crs.get())) { - allowedObjects.push_back( - io::AuthorityFactory::ObjectType::VERTICAL_CRS); - } - if (!allowedObjects.empty()) { - - const std::pair key( - allowedObjects[0], crs->nameStr()); - auto iter = context.cacheNameToCRS.find(key); - if (iter != context.cacheNameToCRS.end()) { - ids = iter->second; - return; - } - - const auto &authFactoryName = authFactory->getAuthority(); - try { - const auto tmpAuthFactory = io::AuthorityFactory::create( - authFactory->databaseContext(), - (authFactoryName.empty() || authFactoryName == "any") - ? std::string() - : authFactoryName); - - auto matches = tmpAuthFactory->createObjectsFromName( - crs->nameStr(), allowedObjects, false, 2); - if (matches.size() == 1 && - crs->_isEquivalentTo( - matches.front().get(), - util::IComparable::Criterion::EQUIVALENT) && - !matches.front()->identifiers().empty()) { - const auto &tmpIds = matches.front()->identifiers(); - ids.emplace_back(*(tmpIds[0]->codeSpace()), - tmpIds[0]->code()); - } - } catch (const std::exception &) { - } - context.cacheNameToCRS[key] = ids; - } - } -} - -// --------------------------------------------------------------------------- - -static std::vector -getCandidateAuthorities(const io::AuthorityFactoryPtr &authFactory, - const std::string &srcAuthName, - const std::string &targetAuthName) { - const auto &authFactoryName = authFactory->getAuthority(); - std::vector authorities; - if (authFactoryName == "any") { - authorities.emplace_back(); - } - if (authFactoryName.empty()) { - authorities = authFactory->databaseContext()->getAllowedAuthorities( - srcAuthName, targetAuthName); - if (authorities.empty()) { - authorities.emplace_back(); - } - } else { - authorities.emplace_back(authFactoryName); - } - return authorities; -} - -// --------------------------------------------------------------------------- - -// Look in the authority registry for operations from sourceCRS to targetCRS -std::vector -CoordinateOperationFactory::Private::findOpsInRegistryDirect( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, bool &resNonEmptyBeforeFiltering) { - const auto &authFactory = context.context->getAuthorityFactory(); - assert(authFactory); - -#ifdef TRACE_CREATE_OPERATIONS - ENTER_BLOCK("findOpsInRegistryDirect(" + objectAsStr(sourceCRS.get()) + - " --> " + objectAsStr(targetCRS.get()) + ")"); -#endif - - resNonEmptyBeforeFiltering = false; - std::list> sourceIds; - std::list> targetIds; - buildCRSIds(sourceCRS, context, sourceIds); - buildCRSIds(targetCRS, context, targetIds); - - const auto gridAvailabilityUse = context.context->getGridAvailabilityUse(); - for (const auto &idSrc : sourceIds) { - const auto &srcAuthName = idSrc.first; - const auto &srcCode = idSrc.second; - for (const auto &idTarget : targetIds) { - const auto &targetAuthName = idTarget.first; - const auto &targetCode = idTarget.second; - - const auto authorities(getCandidateAuthorities( - authFactory, srcAuthName, targetAuthName)); - std::vector res; - for (const auto &authority : authorities) { - const auto authName = - authority == "any" ? std::string() : authority; - const auto tmpAuthFactory = io::AuthorityFactory::create( - authFactory->databaseContext(), authName); - auto resTmp = - tmpAuthFactory->createFromCoordinateReferenceSystemCodes( - srcAuthName, srcCode, targetAuthName, targetCode, - context.context->getUsePROJAlternativeGridNames(), - gridAvailabilityUse == - CoordinateOperationContext:: - GridAvailabilityUse:: - DISCARD_OPERATION_IF_MISSING_GRID || - gridAvailabilityUse == - CoordinateOperationContext:: - GridAvailabilityUse::KNOWN_AVAILABLE, - gridAvailabilityUse == - CoordinateOperationContext::GridAvailabilityUse:: - KNOWN_AVAILABLE, - context.context->getDiscardSuperseded(), true, false, - context.extent1, context.extent2); - res.insert(res.end(), resTmp.begin(), resTmp.end()); - if (authName == "PROJ") { - continue; - } - if (!res.empty()) { - resNonEmptyBeforeFiltering = true; - auto resFiltered = - FilterResults(res, context.context, context.extent1, - context.extent2, false) - .getRes(); -#ifdef TRACE_CREATE_OPERATIONS - logTrace("filtering reduced from " + - toString(static_cast(res.size())) + " to " + - toString(static_cast(resFiltered.size()))); -#endif - return resFiltered; - } - } - } - } - return std::vector(); -} - -// --------------------------------------------------------------------------- - -// Look in the authority registry for operations to targetCRS -std::vector -CoordinateOperationFactory::Private::findOpsInRegistryDirectTo( - const crs::CRSNNPtr &targetCRS, Private::Context &context) { -#ifdef TRACE_CREATE_OPERATIONS - ENTER_BLOCK("findOpsInRegistryDirectTo({any} -->" + - objectAsStr(targetCRS.get()) + ")"); -#endif - - const auto &authFactory = context.context->getAuthorityFactory(); - assert(authFactory); - - std::list> ids; - buildCRSIds(targetCRS, context, ids); - - const auto gridAvailabilityUse = context.context->getGridAvailabilityUse(); - for (const auto &id : ids) { - const auto &targetAuthName = id.first; - const auto &targetCode = id.second; - - const auto authorities(getCandidateAuthorities( - authFactory, targetAuthName, targetAuthName)); - for (const auto &authority : authorities) { - const auto tmpAuthFactory = io::AuthorityFactory::create( - authFactory->databaseContext(), - authority == "any" ? std::string() : authority); - auto res = tmpAuthFactory->createFromCoordinateReferenceSystemCodes( - std::string(), std::string(), targetAuthName, targetCode, - context.context->getUsePROJAlternativeGridNames(), - - gridAvailabilityUse == - CoordinateOperationContext::GridAvailabilityUse:: - DISCARD_OPERATION_IF_MISSING_GRID || - gridAvailabilityUse == - CoordinateOperationContext::GridAvailabilityUse:: - KNOWN_AVAILABLE, - gridAvailabilityUse == CoordinateOperationContext:: - GridAvailabilityUse::KNOWN_AVAILABLE, - context.context->getDiscardSuperseded(), true, true, - context.extent1, context.extent2); - if (!res.empty()) { - auto resFiltered = - FilterResults(res, context.context, context.extent1, - context.extent2, false) - .getRes(); -#ifdef TRACE_CREATE_OPERATIONS - logTrace("filtering reduced from " + - toString(static_cast(res.size())) + " to " + - toString(static_cast(resFiltered.size()))); -#endif - return resFiltered; - } - } - } - return std::vector(); -} - -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -// Look in the authority registry for operations from sourceCRS to targetCRS -// using an intermediate pivot -std::vector -CoordinateOperationFactory::Private::findsOpsInRegistryWithIntermediate( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, - bool useCreateBetweenGeodeticCRSWithDatumBasedIntermediates) { - -#ifdef TRACE_CREATE_OPERATIONS - ENTER_BLOCK("findsOpsInRegistryWithIntermediate(" + - objectAsStr(sourceCRS.get()) + " --> " + - objectAsStr(targetCRS.get()) + ")"); -#endif - - const auto &authFactory = context.context->getAuthorityFactory(); - assert(authFactory); - - std::list> sourceIds; - std::list> targetIds; - buildCRSIds(sourceCRS, context, sourceIds); - buildCRSIds(targetCRS, context, targetIds); - - const auto gridAvailabilityUse = context.context->getGridAvailabilityUse(); - for (const auto &idSrc : sourceIds) { - const auto &srcAuthName = idSrc.first; - const auto &srcCode = idSrc.second; - for (const auto &idTarget : targetIds) { - const auto &targetAuthName = idTarget.first; - const auto &targetCode = idTarget.second; - - const auto authorities(getCandidateAuthorities( - authFactory, srcAuthName, targetAuthName)); - assert(!authorities.empty()); - - const auto tmpAuthFactory = io::AuthorityFactory::create( - authFactory->databaseContext(), - (authFactory->getAuthority() == "any" || authorities.size() > 1) - ? std::string() - : authorities.front()); - - std::vector res; - if (useCreateBetweenGeodeticCRSWithDatumBasedIntermediates) { - res = - tmpAuthFactory - ->createBetweenGeodeticCRSWithDatumBasedIntermediates( - sourceCRS, srcAuthName, srcCode, targetCRS, - targetAuthName, targetCode, - context.context->getUsePROJAlternativeGridNames(), - gridAvailabilityUse == - CoordinateOperationContext:: - GridAvailabilityUse:: - DISCARD_OPERATION_IF_MISSING_GRID || - gridAvailabilityUse == - CoordinateOperationContext:: - GridAvailabilityUse::KNOWN_AVAILABLE, - gridAvailabilityUse == - CoordinateOperationContext:: - GridAvailabilityUse::KNOWN_AVAILABLE, - context.context->getDiscardSuperseded(), - authFactory->getAuthority() != "any" && - authorities.size() > 1 - ? authorities - : std::vector(), - context.extent1, context.extent2); - } else { - io::AuthorityFactory::ObjectType intermediateObjectType = - io::AuthorityFactory::ObjectType::CRS; - - // If doing GeogCRS --> GeogCRS, only use GeogCRS as - // intermediate CRS - // Avoid weird behavior when doing NAD83 -> NAD83(2011) - // that would go through NAVD88 otherwise. - if (context.context->getIntermediateCRS().empty() && - dynamic_cast(sourceCRS.get()) && - dynamic_cast(targetCRS.get())) { - intermediateObjectType = - io::AuthorityFactory::ObjectType::GEOGRAPHIC_CRS; - } - res = tmpAuthFactory->createFromCRSCodesWithIntermediates( - srcAuthName, srcCode, targetAuthName, targetCode, - context.context->getUsePROJAlternativeGridNames(), - gridAvailabilityUse == - CoordinateOperationContext::GridAvailabilityUse:: - DISCARD_OPERATION_IF_MISSING_GRID || - gridAvailabilityUse == - CoordinateOperationContext::GridAvailabilityUse:: - KNOWN_AVAILABLE, - gridAvailabilityUse == - CoordinateOperationContext::GridAvailabilityUse:: - KNOWN_AVAILABLE, - context.context->getDiscardSuperseded(), - context.context->getIntermediateCRS(), - intermediateObjectType, - authFactory->getAuthority() != "any" && - authorities.size() > 1 - ? authorities - : std::vector(), - context.extent1, context.extent2); - } - if (!res.empty()) { - - auto resFiltered = - FilterResults(res, context.context, context.extent1, - context.extent2, false) - .getRes(); -#ifdef TRACE_CREATE_OPERATIONS - logTrace("filtering reduced from " + - toString(static_cast(res.size())) + " to " + - toString(static_cast(resFiltered.size()))); -#endif - return resFiltered; - } - } - } - return std::vector(); -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress -static TransformationNNPtr -createBallparkGeographicOffset(const crs::CRSNNPtr &sourceCRS, - const crs::CRSNNPtr &targetCRS, - const io::DatabaseContextPtr &dbContext) { - - const crs::GeographicCRS *geogSrc = - dynamic_cast(sourceCRS.get()); - const crs::GeographicCRS *geogDst = - dynamic_cast(targetCRS.get()); - const bool isSameDatum = geogSrc && geogDst && - geogSrc->datumNonNull(dbContext)->_isEquivalentTo( - geogDst->datumNonNull(dbContext).get(), - util::IComparable::Criterion::EQUIVALENT); - - auto name = buildOpName(isSameDatum ? NULL_GEOGRAPHIC_OFFSET - : BALLPARK_GEOGRAPHIC_OFFSET, - sourceCRS, targetCRS); - - const auto &sourceCRSExtent = getExtent(sourceCRS); - const auto &targetCRSExtent = getExtent(targetCRS); - const bool sameExtent = - sourceCRSExtent && targetCRSExtent && - sourceCRSExtent->_isEquivalentTo( - targetCRSExtent.get(), util::IComparable::Criterion::EQUIVALENT); - - util::PropertyMap map; - map.set(common::IdentifiedObject::NAME_KEY, name) - .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - sameExtent ? NN_NO_CHECK(sourceCRSExtent) - : metadata::Extent::WORLD); - const common::Angle angle0(0); - - std::vector accuracies; - if (isSameDatum) { - accuracies.emplace_back(metadata::PositionalAccuracy::create("0")); - } - - if (dynamic_cast(sourceCRS.get()) - ->coordinateSystem() - ->axisList() - .size() == 3 || - dynamic_cast(targetCRS.get()) - ->coordinateSystem() - ->axisList() - .size() == 3) { - return Transformation::createGeographic3DOffsets( - map, sourceCRS, targetCRS, angle0, angle0, common::Length(0), - accuracies); - } else { - return Transformation::createGeographic2DOffsets( - map, sourceCRS, targetCRS, angle0, angle0, accuracies); - } -} -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -// --------------------------------------------------------------------------- - -struct MyPROJStringExportableGeodToGeod final - : public io::IPROJStringExportable { - crs::GeodeticCRSPtr geodSrc{}; - crs::GeodeticCRSPtr geodDst{}; - - MyPROJStringExportableGeodToGeod(const crs::GeodeticCRSPtr &geodSrcIn, - const crs::GeodeticCRSPtr &geodDstIn) - : geodSrc(geodSrcIn), geodDst(geodDstIn) {} - - ~MyPROJStringExportableGeodToGeod() override; - - void - // cppcheck-suppress functionStatic - _exportToPROJString(io::PROJStringFormatter *formatter) const override { - - formatter->startInversion(); - geodSrc->_exportToPROJString(formatter); - formatter->stopInversion(); - geodDst->_exportToPROJString(formatter); - } -}; - -MyPROJStringExportableGeodToGeod::~MyPROJStringExportableGeodToGeod() = default; - -// --------------------------------------------------------------------------- - -struct MyPROJStringExportableHorizVertical final - : public io::IPROJStringExportable { - CoordinateOperationPtr horizTransform{}; - CoordinateOperationPtr verticalTransform{}; - crs::GeographicCRSPtr geogDst{}; - - MyPROJStringExportableHorizVertical( - const CoordinateOperationPtr &horizTransformIn, - const CoordinateOperationPtr &verticalTransformIn, - const crs::GeographicCRSPtr &geogDstIn) - : horizTransform(horizTransformIn), - verticalTransform(verticalTransformIn), geogDst(geogDstIn) {} - - ~MyPROJStringExportableHorizVertical() override; - - void - // cppcheck-suppress functionStatic - _exportToPROJString(io::PROJStringFormatter *formatter) const override { - - formatter->pushOmitZUnitConversion(); - - horizTransform->_exportToPROJString(formatter); - - formatter->startInversion(); - geogDst->addAngularUnitConvertAndAxisSwap(formatter); - formatter->stopInversion(); - - formatter->popOmitZUnitConversion(); - - formatter->pushOmitHorizontalConversionInVertTransformation(); - verticalTransform->_exportToPROJString(formatter); - formatter->popOmitHorizontalConversionInVertTransformation(); - - formatter->pushOmitZUnitConversion(); - geogDst->addAngularUnitConvertAndAxisSwap(formatter); - formatter->popOmitZUnitConversion(); - } -}; - -MyPROJStringExportableHorizVertical::~MyPROJStringExportableHorizVertical() = - default; - -// --------------------------------------------------------------------------- - -struct MyPROJStringExportableHorizVerticalHorizPROJBased final - : public io::IPROJStringExportable { - CoordinateOperationPtr opSrcCRSToGeogCRS{}; - CoordinateOperationPtr verticalTransform{}; - CoordinateOperationPtr opGeogCRStoDstCRS{}; - crs::GeographicCRSPtr interpolationGeogCRS{}; - - MyPROJStringExportableHorizVerticalHorizPROJBased( - const CoordinateOperationPtr &opSrcCRSToGeogCRSIn, - const CoordinateOperationPtr &verticalTransformIn, - const CoordinateOperationPtr &opGeogCRStoDstCRSIn, - const crs::GeographicCRSPtr &interpolationGeogCRSIn) - : opSrcCRSToGeogCRS(opSrcCRSToGeogCRSIn), - verticalTransform(verticalTransformIn), - opGeogCRStoDstCRS(opGeogCRStoDstCRSIn), - interpolationGeogCRS(interpolationGeogCRSIn) {} - - ~MyPROJStringExportableHorizVerticalHorizPROJBased() override; - - void - // cppcheck-suppress functionStatic - _exportToPROJString(io::PROJStringFormatter *formatter) const override { - - formatter->pushOmitZUnitConversion(); - - opSrcCRSToGeogCRS->_exportToPROJString(formatter); - - formatter->startInversion(); - interpolationGeogCRS->addAngularUnitConvertAndAxisSwap(formatter); - formatter->stopInversion(); - - formatter->popOmitZUnitConversion(); - - formatter->pushOmitHorizontalConversionInVertTransformation(); - verticalTransform->_exportToPROJString(formatter); - formatter->popOmitHorizontalConversionInVertTransformation(); - - formatter->pushOmitZUnitConversion(); - - interpolationGeogCRS->addAngularUnitConvertAndAxisSwap(formatter); - - opGeogCRStoDstCRS->_exportToPROJString(formatter); - - formatter->popOmitZUnitConversion(); - } -}; - -MyPROJStringExportableHorizVerticalHorizPROJBased:: - ~MyPROJStringExportableHorizVerticalHorizPROJBased() = default; - -//! @endcond - -} // namespace operation -NS_PROJ_END - -#if 0 -namespace dropbox{ namespace oxygen { -template<> nn>::~nn() = default; -template<> nn>::~nn() = default; -template<> nn>::~nn() = default; -}} -#endif - -NS_PROJ_START -namespace operation { - -//! @cond Doxygen_Suppress - -// --------------------------------------------------------------------------- - -static std::string buildTransfName(const std::string &srcName, - const std::string &targetName) { - std::string name("Transformation from "); - name += srcName; - name += " to "; - name += targetName; - return name; -} - -// --------------------------------------------------------------------------- - -static CoordinateOperationNNPtr -createGeodToGeodPROJBased(const crs::CRSNNPtr &geodSrc, - const crs::CRSNNPtr &geodDst) { - - auto exportable = util::nn_make_shared( - util::nn_dynamic_pointer_cast(geodSrc), - util::nn_dynamic_pointer_cast(geodDst)); - - auto properties = util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, - buildTransfName(geodSrc->nameStr(), geodDst->nameStr())); - return createPROJBased(properties, exportable, geodSrc, geodDst, nullptr, - {}, false); -} - -// --------------------------------------------------------------------------- - -static std::string -getRemarks(const std::vector &ops) { - std::string remarks; - for (const auto &op : ops) { - const auto &opRemarks = op->remarks(); - if (!opRemarks.empty()) { - if (!remarks.empty()) { - remarks += '\n'; - } - - std::string opName(op->nameStr()); - if (starts_with(opName, INVERSE_OF)) { - opName = opName.substr(INVERSE_OF.size()); - } - - remarks += "For "; - remarks += opName; - - const auto &ids = op->identifiers(); - if (!ids.empty()) { - std::string authority(*ids.front()->codeSpace()); - if (starts_with(authority, "INVERSE(") && - authority.back() == ')') { - authority = authority.substr(strlen("INVERSE("), - authority.size() - 1 - - strlen("INVERSE(")); - } - if (starts_with(authority, "DERIVED_FROM(") && - authority.back() == ')') { - authority = authority.substr(strlen("DERIVED_FROM("), - authority.size() - 1 - - strlen("DERIVED_FROM(")); - } - - remarks += " ("; - remarks += authority; - remarks += ':'; - remarks += ids.front()->code(); - remarks += ')'; - } - remarks += ": "; - remarks += opRemarks; - } - } - return remarks; -} - -// --------------------------------------------------------------------------- - -static CoordinateOperationNNPtr createHorizVerticalPROJBased( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - const operation::CoordinateOperationNNPtr &horizTransform, - const operation::CoordinateOperationNNPtr &verticalTransform, - bool checkExtent) { - - auto geogDst = util::nn_dynamic_pointer_cast(targetCRS); - assert(geogDst); - - auto exportable = util::nn_make_shared( - horizTransform, verticalTransform, geogDst); - - const bool horizTransformIsNoOp = - starts_with(horizTransform->nameStr(), NULL_GEOGRAPHIC_OFFSET) && - horizTransform->nameStr().find(" + ") == std::string::npos; - if (horizTransformIsNoOp) { - auto properties = util::PropertyMap(); - properties.set(common::IdentifiedObject::NAME_KEY, - verticalTransform->nameStr()); - bool dummy = false; - auto extent = getExtent(verticalTransform, true, dummy); - if (extent) { - properties.set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - NN_NO_CHECK(extent)); - } - const auto &remarks = verticalTransform->remarks(); - if (!remarks.empty()) { - properties.set(common::IdentifiedObject::REMARKS_KEY, remarks); - } - return createPROJBased( - properties, exportable, sourceCRS, targetCRS, nullptr, - verticalTransform->coordinateOperationAccuracies(), - verticalTransform->hasBallparkTransformation()); - } else { - bool emptyIntersection = false; - auto ops = std::vector{horizTransform, - verticalTransform}; - auto extent = getExtent(ops, true, emptyIntersection); - if (checkExtent && emptyIntersection) { - std::string msg( - "empty intersection of area of validity of concatenated " - "operations"); - throw InvalidOperationEmptyIntersection(msg); - } - auto properties = util::PropertyMap(); - properties.set(common::IdentifiedObject::NAME_KEY, - computeConcatenatedName(ops)); - - if (extent) { - properties.set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - NN_NO_CHECK(extent)); - } - - const auto remarks = getRemarks(ops); - if (!remarks.empty()) { - properties.set(common::IdentifiedObject::REMARKS_KEY, remarks); - } - - std::vector accuracies; - const double accuracy = getAccuracy(ops); - if (accuracy >= 0.0) { - accuracies.emplace_back( - metadata::PositionalAccuracy::create(toString(accuracy))); - } - - return createPROJBased( - properties, exportable, sourceCRS, targetCRS, nullptr, accuracies, - horizTransform->hasBallparkTransformation() || - verticalTransform->hasBallparkTransformation()); - } -} - -// --------------------------------------------------------------------------- - -static CoordinateOperationNNPtr createHorizVerticalHorizPROJBased( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - const operation::CoordinateOperationNNPtr &opSrcCRSToGeogCRS, - const operation::CoordinateOperationNNPtr &verticalTransform, - const operation::CoordinateOperationNNPtr &opGeogCRStoDstCRS, - const crs::GeographicCRSPtr &interpolationGeogCRS, bool checkExtent) { - - auto exportable = - util::nn_make_shared( - opSrcCRSToGeogCRS, verticalTransform, opGeogCRStoDstCRS, - interpolationGeogCRS); - - std::vector ops; - if (!(starts_with(opSrcCRSToGeogCRS->nameStr(), NULL_GEOGRAPHIC_OFFSET) && - opSrcCRSToGeogCRS->nameStr().find(" + ") == std::string::npos)) { - ops.emplace_back(opSrcCRSToGeogCRS); - } - ops.emplace_back(verticalTransform); - if (!(starts_with(opGeogCRStoDstCRS->nameStr(), NULL_GEOGRAPHIC_OFFSET) && - opGeogCRStoDstCRS->nameStr().find(" + ") == std::string::npos)) { - ops.emplace_back(opGeogCRStoDstCRS); - } - - bool hasBallparkTransformation = false; - for (const auto &op : ops) { - hasBallparkTransformation |= op->hasBallparkTransformation(); - } - bool emptyIntersection = false; - auto extent = getExtent(ops, false, emptyIntersection); - if (checkExtent && emptyIntersection) { - std::string msg( - "empty intersection of area of validity of concatenated " - "operations"); - throw InvalidOperationEmptyIntersection(msg); - } - auto properties = util::PropertyMap(); - properties.set(common::IdentifiedObject::NAME_KEY, - computeConcatenatedName(ops)); - - if (extent) { - properties.set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - NN_NO_CHECK(extent)); - } - - const auto remarks = getRemarks(ops); - if (!remarks.empty()) { - properties.set(common::IdentifiedObject::REMARKS_KEY, remarks); - } - - std::vector accuracies; - const double accuracy = getAccuracy(ops); - if (accuracy >= 0.0) { - accuracies.emplace_back( - metadata::PositionalAccuracy::create(toString(accuracy))); - } - - return createPROJBased(properties, exportable, sourceCRS, targetCRS, - nullptr, accuracies, hasBallparkTransformation); -} - -//! @endcond - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -std::vector -CoordinateOperationFactory::Private::createOperationsGeogToGeog( - std::vector &res, const crs::CRSNNPtr &sourceCRS, - const crs::CRSNNPtr &targetCRS, Private::Context &context, - const crs::GeographicCRS *geogSrc, const crs::GeographicCRS *geogDst) { - - assert(sourceCRS.get() == geogSrc); - assert(targetCRS.get() == geogDst); - - const auto &src_pm = geogSrc->primeMeridian()->longitude(); - const auto &dst_pm = geogDst->primeMeridian()->longitude(); - auto offset_pm = - (src_pm.unit() == dst_pm.unit()) - ? common::Angle(src_pm.value() - dst_pm.value(), src_pm.unit()) - : common::Angle( - src_pm.convertToUnit(common::UnitOfMeasure::DEGREE) - - dst_pm.convertToUnit(common::UnitOfMeasure::DEGREE), - common::UnitOfMeasure::DEGREE); - - double vconvSrc = 1.0; - const auto &srcCS = geogSrc->coordinateSystem(); - const auto &srcAxisList = srcCS->axisList(); - if (srcAxisList.size() == 3) { - vconvSrc = srcAxisList[2]->unit().conversionToSI(); - } - double vconvDst = 1.0; - const auto &dstCS = geogDst->coordinateSystem(); - const auto &dstAxisList = dstCS->axisList(); - if (dstAxisList.size() == 3) { - vconvDst = dstAxisList[2]->unit().conversionToSI(); - } - - std::string name(buildTransfName(geogSrc->nameStr(), geogDst->nameStr())); - - const auto &authFactory = context.context->getAuthorityFactory(); - const auto dbContext = - authFactory ? authFactory->databaseContext().as_nullable() : nullptr; - - const bool sameDatum = geogSrc->datumNonNull(dbContext)->_isEquivalentTo( - geogDst->datumNonNull(dbContext).get(), - util::IComparable::Criterion::EQUIVALENT); - - // Do the CRS differ by their axis order ? - bool axisReversal2D = false; - bool axisReversal3D = false; - if (!srcCS->_isEquivalentTo(dstCS.get(), - util::IComparable::Criterion::EQUIVALENT)) { - auto srcOrder = srcCS->axisOrder(); - auto dstOrder = dstCS->axisOrder(); - if (((srcOrder == cs::EllipsoidalCS::AxisOrder::LAT_NORTH_LONG_EAST || - srcOrder == cs::EllipsoidalCS::AxisOrder:: - LAT_NORTH_LONG_EAST_HEIGHT_UP) && - (dstOrder == cs::EllipsoidalCS::AxisOrder::LONG_EAST_LAT_NORTH || - dstOrder == cs::EllipsoidalCS::AxisOrder:: - LONG_EAST_LAT_NORTH_HEIGHT_UP)) || - ((srcOrder == cs::EllipsoidalCS::AxisOrder::LONG_EAST_LAT_NORTH || - srcOrder == cs::EllipsoidalCS::AxisOrder:: - LONG_EAST_LAT_NORTH_HEIGHT_UP) && - (dstOrder == cs::EllipsoidalCS::AxisOrder::LAT_NORTH_LONG_EAST || - dstOrder == cs::EllipsoidalCS::AxisOrder:: - LAT_NORTH_LONG_EAST_HEIGHT_UP))) { - if (srcAxisList.size() == 3 || dstAxisList.size() == 3) - axisReversal3D = true; - else - axisReversal2D = true; - } - } - - // Do they differ by vertical units ? - if (vconvSrc != vconvDst && - geogSrc->ellipsoid()->_isEquivalentTo( - geogDst->ellipsoid().get(), - util::IComparable::Criterion::EQUIVALENT)) { - if (offset_pm.value() == 0 && !axisReversal2D && !axisReversal3D) { - // If only by vertical units, use a Change of Vertical - // Unit - // transformation - const double factor = vconvSrc / vconvDst; - auto conv = Conversion::createChangeVerticalUnit( - util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, - name), - common::Scale(factor)); - conv->setCRSs(sourceCRS, targetCRS, nullptr); - conv->setHasBallparkTransformation(!sameDatum); - res.push_back(conv); - return res; - } else { - auto op = createGeodToGeodPROJBased(sourceCRS, targetCRS); - op->setHasBallparkTransformation(!sameDatum); - res.emplace_back(op); - return res; - } - } - - // Do the CRS differ only by their axis order ? - if (sameDatum && (axisReversal2D || axisReversal3D)) { - auto conv = Conversion::createAxisOrderReversal(axisReversal3D); - conv->setCRSs(sourceCRS, targetCRS, nullptr); - res.emplace_back(conv); - return res; - } - - std::vector steps; - // If both are geographic and only differ by their prime - // meridian, - // apply a longitude rotation transformation. - if (geogSrc->ellipsoid()->_isEquivalentTo( - geogDst->ellipsoid().get(), - util::IComparable::Criterion::EQUIVALENT) && - src_pm.getSIValue() != dst_pm.getSIValue()) { - - steps.emplace_back(Transformation::createLongitudeRotation( - util::PropertyMap() - .set(common::IdentifiedObject::NAME_KEY, name) - .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - metadata::Extent::WORLD), - sourceCRS, targetCRS, offset_pm)); - // If only the target has a non-zero prime meridian, chain a - // null geographic offset and then the longitude rotation - } else if (src_pm.getSIValue() == 0 && dst_pm.getSIValue() != 0) { - auto datum = datum::GeodeticReferenceFrame::create( - util::PropertyMap(), geogDst->ellipsoid(), - util::optional(), geogSrc->primeMeridian()); - std::string interm_crs_name(geogDst->nameStr()); - interm_crs_name += " altered to use prime meridian of "; - interm_crs_name += geogSrc->nameStr(); - auto interm_crs = - util::nn_static_pointer_cast(crs::GeographicCRS::create( - util::PropertyMap() - .set(common::IdentifiedObject::NAME_KEY, interm_crs_name) - .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - metadata::Extent::WORLD), - datum, dstCS)); - - steps.emplace_back( - createBallparkGeographicOffset(sourceCRS, interm_crs, dbContext)); - - steps.emplace_back(Transformation::createLongitudeRotation( - util::PropertyMap() - .set(common::IdentifiedObject::NAME_KEY, - buildTransfName(geogSrc->nameStr(), interm_crs->nameStr())) - .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - metadata::Extent::WORLD), - interm_crs, targetCRS, offset_pm)); - - } else { - // If the prime meridians are different, chain a longitude - // rotation and the null geographic offset. - if (src_pm.getSIValue() != dst_pm.getSIValue()) { - auto datum = datum::GeodeticReferenceFrame::create( - util::PropertyMap(), geogSrc->ellipsoid(), - util::optional(), geogDst->primeMeridian()); - std::string interm_crs_name(geogSrc->nameStr()); - interm_crs_name += " altered to use prime meridian of "; - interm_crs_name += geogDst->nameStr(); - auto interm_crs = util::nn_static_pointer_cast( - crs::GeographicCRS::create( - util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, - interm_crs_name), - datum, srcCS)); - - steps.emplace_back(Transformation::createLongitudeRotation( - util::PropertyMap() - .set(common::IdentifiedObject::NAME_KEY, - buildTransfName(geogSrc->nameStr(), - interm_crs->nameStr())) - .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - metadata::Extent::WORLD), - sourceCRS, interm_crs, offset_pm)); - steps.emplace_back(createBallparkGeographicOffset( - interm_crs, targetCRS, dbContext)); - } else { - steps.emplace_back(createBallparkGeographicOffset( - sourceCRS, targetCRS, dbContext)); - } - } - - auto op = ConcatenatedOperation::createComputeMetadata( - steps, disallowEmptyIntersection); - op->setHasBallparkTransformation(!sameDatum); - res.emplace_back(op); - return res; -} - -// --------------------------------------------------------------------------- - -static bool hasIdentifiers(const CoordinateOperationNNPtr &op) { - if (!op->identifiers().empty()) { - return true; - } - auto concatenated = dynamic_cast(op.get()); - if (concatenated) { - for (const auto &subOp : concatenated->operations()) { - if (hasIdentifiers(subOp)) { - return true; - } - } - } - return false; -} - -// --------------------------------------------------------------------------- - -static std::vector -findCandidateGeodCRSForDatum(const io::AuthorityFactoryPtr &authFactory, - const crs::GeodeticCRS *crs, - const datum::GeodeticReferenceFrame *datum) { - std::vector candidates; - assert(datum); - const auto &ids = datum->identifiers(); - const auto &datumName = datum->nameStr(); - if (!ids.empty()) { - for (const auto &id : ids) { - const auto &authName = *(id->codeSpace()); - const auto &code = id->code(); - if (!authName.empty()) { - const auto crsIds = crs->identifiers(); - const auto tmpFactory = - (crsIds.size() == 1 && - *(crsIds.front()->codeSpace()) == authName) - ? io::AuthorityFactory::create( - authFactory->databaseContext(), authName) - .as_nullable() - : authFactory; - auto l_candidates = tmpFactory->createGeodeticCRSFromDatum( - authName, code, std::string()); - for (const auto &candidate : l_candidates) { - candidates.emplace_back(candidate); - } - } - } - } else if (datumName != "unknown" && datumName != "unnamed") { - auto matches = authFactory->createObjectsFromName( - datumName, - {io::AuthorityFactory::ObjectType::GEODETIC_REFERENCE_FRAME}, false, - 2); - if (matches.size() == 1) { - const auto &match = matches.front(); - if (datum->_isEquivalentTo( - match.get(), util::IComparable::Criterion::EQUIVALENT) && - !match->identifiers().empty()) { - return findCandidateGeodCRSForDatum( - authFactory, crs, - dynamic_cast( - match.get())); - } - } - } - return candidates; -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::setCRSs( - CoordinateOperation *co, const crs::CRSNNPtr &sourceCRS, - const crs::CRSNNPtr &targetCRS) { - co->setCRSs(sourceCRS, targetCRS, nullptr); - - auto invCO = dynamic_cast(co); - if (invCO) { - invCO->forwardOperation()->setCRSs(targetCRS, sourceCRS, nullptr); - } - - auto transf = dynamic_cast(co); - if (transf) { - transf->inverseAsTransformation()->setCRSs(targetCRS, sourceCRS, - nullptr); - } - - auto concat = dynamic_cast(co); - if (concat) { - auto first = concat->operations().front().get(); - auto &firstTarget(first->targetCRS()); - if (firstTarget) { - setCRSs(first, sourceCRS, NN_NO_CHECK(firstTarget)); - } - auto last = concat->operations().back().get(); - auto &lastSource(last->sourceCRS()); - if (lastSource) { - setCRSs(last, NN_NO_CHECK(lastSource), targetCRS); - } - } -} - -// --------------------------------------------------------------------------- - -static bool hasResultSetOnlyResultsWithPROJStep( - const std::vector &res) { - for (const auto &op : res) { - auto concat = dynamic_cast(op.get()); - if (concat) { - bool hasPROJStep = false; - const auto &steps = concat->operations(); - for (const auto &step : steps) { - const auto &ids = step->identifiers(); - if (!ids.empty()) { - const auto &opAuthority = *(ids.front()->codeSpace()); - if (opAuthority == "PROJ" || - opAuthority == "INVERSE(PROJ)" || - opAuthority == "DERIVED_FROM(PROJ)") { - hasPROJStep = true; - break; - } - } - } - if (!hasPROJStep) { - return false; - } - } else { - return false; - } - } - return true; -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsWithDatumPivot( - std::vector &res, const crs::CRSNNPtr &sourceCRS, - const crs::CRSNNPtr &targetCRS, const crs::GeodeticCRS *geodSrc, - const crs::GeodeticCRS *geodDst, Private::Context &context) { - -#ifdef TRACE_CREATE_OPERATIONS - ENTER_BLOCK("createOperationsWithDatumPivot(" + - objectAsStr(sourceCRS.get()) + "," + - objectAsStr(targetCRS.get()) + ")"); -#endif - - struct CreateOperationsWithDatumPivotAntiRecursion { - Context &context; - - explicit CreateOperationsWithDatumPivotAntiRecursion(Context &contextIn) - : context(contextIn) { - assert(!context.inCreateOperationsWithDatumPivotAntiRecursion); - context.inCreateOperationsWithDatumPivotAntiRecursion = true; - } - - ~CreateOperationsWithDatumPivotAntiRecursion() { - context.inCreateOperationsWithDatumPivotAntiRecursion = false; - } - }; - CreateOperationsWithDatumPivotAntiRecursion guard(context); - - const auto &authFactory = context.context->getAuthorityFactory(); - const auto &dbContext = authFactory->databaseContext(); - - const auto candidatesSrcGeod(findCandidateGeodCRSForDatum( - authFactory, geodSrc, - geodSrc->datumNonNull(dbContext.as_nullable()).get())); - const auto candidatesDstGeod(findCandidateGeodCRSForDatum( - authFactory, geodDst, - geodDst->datumNonNull(dbContext.as_nullable()).get())); - - const bool sourceAndTargetAre3D = - geodSrc->coordinateSystem()->axisList().size() == 3 && - geodDst->coordinateSystem()->axisList().size() == 3; - - 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(opSecond.get()); - if (so && isAxisOrderReversal(so->method()->getEPSGCode())) { - continue; - } - - std::vector subOps; - const bool isNullThird = - isNullTransformation(opsThird[0]->nameStr()); - CoordinateOperationNNPtr opSecondCloned( - (isNullFirst || isNullThird || sourceAndTargetAre3D) - ? opSecond->shallowClone() - : opSecond); - if (isNullFirst || isNullThird) { - if (opSecondCloned->identifiers().size() == 1 && - (*opSecondCloned->identifiers()[0]->codeSpace()) - .find("DERIVED_FROM") == std::string::npos) { - { - util::PropertyMap map; - addModifiedIdentifier(map, opSecondCloned.get(), false, - true); - opSecondCloned->setProperties(map); - } - auto invCO = dynamic_cast( - opSecondCloned.get()); - if (invCO) { - auto invCOForward = invCO->forwardOperation().get(); - if (invCOForward->identifiers().size() == 1 && - (*invCOForward->identifiers()[0]->codeSpace()) - .find("DERIVED_FROM") == - std::string::npos) { - util::PropertyMap map; - addModifiedIdentifier(map, invCOForward, false, - true); - invCOForward->setProperties(map); - } - } - } - } - if (sourceAndTargetAre3D) { - opSecondCloned->getPrivate()->use3DHelmert_ = true; - auto invCO = dynamic_cast( - opSecondCloned.get()); - if (invCO) { - auto invCOForward = invCO->forwardOperation().get(); - invCOForward->getPrivate()->use3DHelmert_ = true; - } - } - if (isNullFirst) { - auto oldTarget(NN_CHECK_ASSERT(opSecondCloned->targetCRS())); - setCRSs(opSecondCloned.get(), sourceCRS, oldTarget); - } else { - subOps.emplace_back(opFirst); - } - if (isNullThird) { - auto oldSource(NN_CHECK_ASSERT(opSecondCloned->sourceCRS())); - setCRSs(opSecondCloned.get(), oldSource, targetCRS); - subOps.emplace_back(opSecondCloned); - } else { - subOps.emplace_back(opSecondCloned); - subOps.emplace_back(opsThird[0]); - } -#ifdef TRACE_CREATE_OPERATIONS - std::string debugStr; - for (const auto &op : subOps) { - if (!debugStr.empty()) { - debugStr += " + "; - } - debugStr += objectAsStr(op.get()); - debugStr += " ("; - debugStr += objectAsStr(op->sourceCRS().get()); - debugStr += "->"; - debugStr += objectAsStr(op->targetCRS().get()); - debugStr += ")"; - } - logTrace("transformation " + debugStr); -#endif - try { - res.emplace_back(ConcatenatedOperation::createComputeMetadata( - subOps, disallowEmptyIntersection)); - } catch (const InvalidOperationEmptyIntersection &) { - } - } - }; - - // Start in priority with candidates that have exactly the same name as - // the sourcCRS and targetCRS. Typically for the case of init=IGNF:XXXX - - // Transformation from IGNF:NTFP to IGNF:RGF93G, - // using - // NTF geographiques Paris (gr) vers NTF GEOGRAPHIQUES GREENWICH (DMS) + - // NOUVELLE TRIANGULATION DE LA FRANCE (NTF) vers RGF93 (ETRS89) - // that is using ntf_r93.gsb, is horribly dependent - // of IGNF:RGF93G being returned before IGNF:RGF93GEO in candidatesDstGeod. - // If RGF93GEO is returned before then we go through WGS84 and use - // instead a Helmert transformation. - // The below logic is thus quite fragile, and attempts at changing it - // result in degraded results for other use cases... - - for (const auto &candidateSrcGeod : candidatesSrcGeod) { - if (candidateSrcGeod->nameStr() == sourceCRS->nameStr()) { - for (const auto &candidateDstGeod : candidatesDstGeod) { - if (candidateDstGeod->nameStr() == targetCRS->nameStr()) { -#ifdef TRACE_CREATE_OPERATIONS - ENTER_BLOCK("try " + objectAsStr(sourceCRS.get()) + "->" + - objectAsStr(candidateSrcGeod.get()) + "->" + - objectAsStr(candidateDstGeod.get()) + "->" + - objectAsStr(targetCRS.get()) + ")"); -#endif - 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()) { - if (hasResultSetOnlyResultsWithPROJStep(res)) { - continue; - } - return; - } - } - } - } - } - - for (const auto &candidateSrcGeod : candidatesSrcGeod) { - const bool bSameSrcName = - candidateSrcGeod->nameStr() == sourceCRS->nameStr(); -#ifdef TRACE_CREATE_OPERATIONS - ENTER_BLOCK(""); -#endif - const auto opsFirst = - createOperations(sourceCRS, candidateSrcGeod, context); - assert(!opsFirst.empty()); - const bool isNullFirst = isNullTransformation(opsFirst[0]->nameStr()); - - for (const auto &candidateDstGeod : candidatesDstGeod) { - if (bSameSrcName && - candidateDstGeod->nameStr() == targetCRS->nameStr()) { - continue; - } - -#ifdef TRACE_CREATE_OPERATIONS - ENTER_BLOCK("try " + objectAsStr(sourceCRS.get()) + "->" + - objectAsStr(candidateSrcGeod.get()) + "->" + - objectAsStr(candidateDstGeod.get()) + "->" + - objectAsStr(targetCRS.get()) + ")"); -#endif - createTransformations(candidateSrcGeod, candidateDstGeod, - opsFirst[0], isNullFirst); - if (!res.empty() && !hasResultSetOnlyResultsWithPROJStep(res)) { - return; - } - } - } -} - -// --------------------------------------------------------------------------- - -static CoordinateOperationNNPtr -createBallparkGeocentricTranslation(const crs::CRSNNPtr &sourceCRS, - const crs::CRSNNPtr &targetCRS) { - std::string name(BALLPARK_GEOCENTRIC_TRANSLATION); - name += " from "; - name += sourceCRS->nameStr(); - name += " to "; - name += targetCRS->nameStr(); - - return util::nn_static_pointer_cast( - Transformation::createGeocentricTranslations( - util::PropertyMap() - .set(common::IdentifiedObject::NAME_KEY, name) - .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - metadata::Extent::WORLD), - sourceCRS, targetCRS, 0.0, 0.0, 0.0, {})); -} - -// --------------------------------------------------------------------------- - -bool CoordinateOperationFactory::Private::hasPerfectAccuracyResult( - const std::vector &res, const Context &context) { - auto resTmp = FilterResults(res, context.context, context.extent1, - context.extent2, true) - .getRes(); - for (const auto &op : resTmp) { - const double acc = getAccuracy(op); - if (acc == 0.0) { - return true; - } - } - return false; -} - -// --------------------------------------------------------------------------- - -std::vector -CoordinateOperationFactory::Private::createOperations( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context) { - -#ifdef TRACE_CREATE_OPERATIONS - ENTER_BLOCK("createOperations(" + objectAsStr(sourceCRS.get()) + " --> " + - objectAsStr(targetCRS.get()) + ")"); -#endif - - std::vector res; - - auto boundSrc = dynamic_cast(sourceCRS.get()); - auto boundDst = dynamic_cast(targetCRS.get()); - - const auto &sourceProj4Ext = boundSrc - ? boundSrc->baseCRS()->getExtensionProj4() - : sourceCRS->getExtensionProj4(); - const auto &targetProj4Ext = boundDst - ? boundDst->baseCRS()->getExtensionProj4() - : targetCRS->getExtensionProj4(); - if (!sourceProj4Ext.empty() || !targetProj4Ext.empty()) { - createOperationsFromProj4Ext(sourceCRS, targetCRS, boundSrc, boundDst, - res); - return res; - } - - auto geodSrc = dynamic_cast(sourceCRS.get()); - auto geodDst = dynamic_cast(targetCRS.get()); - auto geogSrc = dynamic_cast(sourceCRS.get()); - auto geogDst = dynamic_cast(targetCRS.get()); - auto vertSrc = dynamic_cast(sourceCRS.get()); - auto vertDst = dynamic_cast(targetCRS.get()); - - // First look-up if the registry provide us with operations. - auto derivedSrc = dynamic_cast(sourceCRS.get()); - auto derivedDst = dynamic_cast(targetCRS.get()); - const auto &authFactory = context.context->getAuthorityFactory(); - if (authFactory && - (derivedSrc == nullptr || - !derivedSrc->baseCRS()->_isEquivalentTo( - targetCRS.get(), util::IComparable::Criterion::EQUIVALENT)) && - (derivedDst == nullptr || - !derivedDst->baseCRS()->_isEquivalentTo( - sourceCRS.get(), util::IComparable::Criterion::EQUIVALENT))) { - - if (createOperationsFromDatabase(sourceCRS, targetCRS, context, geodSrc, - geodDst, geogSrc, geogDst, vertSrc, - vertDst, res)) { - return res; - } - } - - // Special case if both CRS are geodetic - if (geodSrc && geodDst && !derivedSrc && !derivedDst) { - createOperationsGeodToGeod(sourceCRS, targetCRS, context, geodSrc, - geodDst, res); - return res; - } - - // If the source is a derived CRS, then chain the inverse of its - // deriving conversion, with transforms from its baseCRS to the - // targetCRS - if (derivedSrc) { - createOperationsDerivedTo(sourceCRS, targetCRS, context, derivedSrc, - res); - return res; - } - - // reverse of previous case - if (derivedDst) { - return applyInverse(createOperations(targetCRS, sourceCRS, context)); - } - - // Order of comparison between the geogDst vs geodDst is impotant - if (boundSrc && geogDst) { - createOperationsBoundToGeog(sourceCRS, targetCRS, context, boundSrc, - geogDst, res); - return res; - } else if (boundSrc && geodDst) { - createOperationsToGeod(sourceCRS, targetCRS, context, geodDst, res); - return res; - } - - // reverse of previous case - if (geodSrc && boundDst) { - return applyInverse(createOperations(targetCRS, sourceCRS, context)); - } - - // vertCRS (as boundCRS with transformation to target vertCRS) to - // vertCRS - if (boundSrc && vertDst) { - createOperationsBoundToVert(sourceCRS, targetCRS, context, boundSrc, - vertDst, res); - return res; - } - - // reverse of previous case - if (boundDst && vertSrc) { - return applyInverse(createOperations(targetCRS, sourceCRS, context)); - } - - if (vertSrc && vertDst) { - createOperationsVertToVert(sourceCRS, targetCRS, context, vertSrc, - vertDst, res); - return res; - } - - // A bit odd case as we are comparing apples to oranges, but in case - // the vertical unit differ, do something useful. - if (vertSrc && geogDst) { - createOperationsVertToGeog(sourceCRS, targetCRS, context, vertSrc, - geogDst, res); - return res; - } - - // reverse of previous case - if (vertDst && geogSrc) { - return applyInverse(createOperations(targetCRS, sourceCRS, context)); - } - - // boundCRS to boundCRS - if (boundSrc && boundDst) { - createOperationsBoundToBound(sourceCRS, targetCRS, context, boundSrc, - boundDst, res); - return res; - } - - auto compoundSrc = dynamic_cast(sourceCRS.get()); - // Order of comparison between the geogDst vs geodDst is impotant - if (compoundSrc && geogDst) { - createOperationsCompoundToGeog(sourceCRS, targetCRS, context, - compoundSrc, geogDst, res); - return res; - } else if (compoundSrc && geodDst) { - createOperationsToGeod(sourceCRS, targetCRS, context, geodDst, res); - return res; - } - - // reverse of previous cases - auto compoundDst = dynamic_cast(targetCRS.get()); - if (geodSrc && compoundDst) { - return applyInverse(createOperations(targetCRS, sourceCRS, context)); - } - - if (compoundSrc && compoundDst) { - createOperationsCompoundToCompound(sourceCRS, targetCRS, context, - compoundSrc, compoundDst, res); - return res; - } - - // '+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +type=crs' to - // '+proj=longlat +ellps=GRS80 +nadgrids=@bar.gsb +geoidgrids=@bar.gtx - // +type=crs' - if (boundSrc && compoundDst) { - createOperationsBoundToCompound(sourceCRS, targetCRS, context, boundSrc, - compoundDst, res); - return res; - } - - // reverse of previous case - if (boundDst && compoundSrc) { - return applyInverse(createOperations(targetCRS, sourceCRS, context)); - } - - return res; -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsFromProj4Ext( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - const crs::BoundCRS *boundSrc, const crs::BoundCRS *boundDst, - std::vector &res) { - - ENTER_FUNCTION(); - - auto sourceProjExportable = dynamic_cast( - boundSrc ? boundSrc : sourceCRS.get()); - auto targetProjExportable = dynamic_cast( - boundDst ? boundDst : 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(); - projFormatter->setCRSExport(true); - projFormatter->setLegacyCRSToCRSContext(true); - projFormatter->startInversion(); - sourceProjExportable->_exportToPROJString(projFormatter.get()); - auto geogSrc = dynamic_cast(sourceCRS.get()); - if (geogSrc) { - auto tmpFormatter = io::PROJStringFormatter::create(); - geogSrc->addAngularUnitConvertAndAxisSwap(tmpFormatter.get()); - projFormatter->ingestPROJString(tmpFormatter->toString()); - } - - projFormatter->stopInversion(); - - targetProjExportable->_exportToPROJString(projFormatter.get()); - auto geogDst = dynamic_cast(targetCRS.get()); - if (geogDst) { - auto tmpFormatter = io::PROJStringFormatter::create(); - geogDst->addAngularUnitConvertAndAxisSwap(tmpFormatter.get()); - projFormatter->ingestPROJString(tmpFormatter->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, {})); -} - -// --------------------------------------------------------------------------- - -bool CoordinateOperationFactory::Private::createOperationsFromDatabase( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::GeodeticCRS *geodSrc, - const crs::GeodeticCRS *geodDst, const crs::GeographicCRS *geogSrc, - const crs::GeographicCRS *geogDst, const crs::VerticalCRS *vertSrc, - const crs::VerticalCRS *vertDst, - std::vector &res) { - - ENTER_FUNCTION(); - - if (geogSrc && vertDst) { - createOperationsFromDatabase(targetCRS, sourceCRS, context, geodDst, - geodSrc, geogDst, geogSrc, vertDst, - vertSrc, res); - res = applyInverse(res); - } else if (geogDst && vertSrc) { - res = applyInverse(createOperationsGeogToVertFromGeoid( - targetCRS, sourceCRS, vertSrc, context)); - if (!res.empty()) { - createOperationsVertToGeogBallpark(sourceCRS, targetCRS, context, - vertSrc, geogDst, res); - } - } - - if (!res.empty()) { - return true; - } - - bool resFindDirectNonEmptyBeforeFiltering = false; - res = findOpsInRegistryDirect(sourceCRS, targetCRS, context, - resFindDirectNonEmptyBeforeFiltering); - - // If we get at least a result with perfect accuracy, do not - // bother generating synthetic transforms. - if (hasPerfectAccuracyResult(res, context)) { - return true; - } - - bool doFilterAndCheckPerfectOp = false; - - bool sameGeodeticDatum = false; - - if (vertSrc || vertDst) { - if (res.empty()) { - if (geogSrc && - geogSrc->coordinateSystem()->axisList().size() == 2 && - vertDst) { - auto dbContext = - context.context->getAuthorityFactory()->databaseContext(); - auto resTmp = findOpsInRegistryDirect( - sourceCRS->promoteTo3D(std::string(), dbContext), targetCRS, - context, resFindDirectNonEmptyBeforeFiltering); - for (auto &op : resTmp) { - auto newOp = op->shallowClone(); - setCRSs(newOp.get(), sourceCRS, targetCRS); - res.emplace_back(newOp); - } - } else if (geogDst && - geogDst->coordinateSystem()->axisList().size() == 2 && - vertSrc) { - auto dbContext = - context.context->getAuthorityFactory()->databaseContext(); - auto resTmp = findOpsInRegistryDirect( - sourceCRS, targetCRS->promoteTo3D(std::string(), dbContext), - context, resFindDirectNonEmptyBeforeFiltering); - for (auto &op : resTmp) { - auto newOp = op->shallowClone(); - setCRSs(newOp.get(), sourceCRS, targetCRS); - res.emplace_back(newOp); - } - } - } - if (res.empty()) { - createOperationsFromDatabaseWithVertCRS(sourceCRS, targetCRS, - context, geogSrc, geogDst, - vertSrc, vertDst, res); - } - } else if (geodSrc && geodDst) { - - const auto &authFactory = context.context->getAuthorityFactory(); - const auto dbContext = authFactory->databaseContext().as_nullable(); - - const auto srcDatum = geodSrc->datumNonNull(dbContext); - const auto dstDatum = geodDst->datumNonNull(dbContext); - sameGeodeticDatum = srcDatum->_isEquivalentTo( - dstDatum.get(), util::IComparable::Criterion::EQUIVALENT); - - if (res.empty() && !sameGeodeticDatum && - !context.inCreateOperationsWithDatumPivotAntiRecursion) { - // If we still didn't find a transformation, and that the source - // and target are GeodeticCRS, then go through their underlying - // datum to find potential transformations between other - // GeodeticCRSs - // that are made of those datum - // The typical example is if transforming between two - // GeographicCRS, - // but transformations are only available between their - // corresponding geocentric CRS. - createOperationsWithDatumPivot(res, sourceCRS, targetCRS, geodSrc, - geodDst, context); - doFilterAndCheckPerfectOp = !res.empty(); - } - } - - bool foundInstantiableOp = false; - // FIXME: the limitation to .size() == 1 is just for the - // -s EPSG:4959+5759 -t "EPSG:4959+7839" case - // finding EPSG:7860 'NZVD2016 height to Auckland 1946 - // height (1)', which uses the EPSG:1071 'Vertical Offset by Grid - // Interpolation (NZLVD)' method which is not currently implemented by PROJ - // (cannot deal with .csv files) - // Initially the test was written to iterate over for all operations of a - // non-empty res, but this causes failures in the test suite when no grids - // are installed at all. Ideally we should tweak the test suite to be - // robust to that, or skip some tests. - if (res.size() == 1) { - try { - res.front()->exportToPROJString( - io::PROJStringFormatter::create().get()); - foundInstantiableOp = true; - } catch (const std::exception &) { - } - if (!foundInstantiableOp) { - resFindDirectNonEmptyBeforeFiltering = false; - } - } else if (res.size() > 1) { - foundInstantiableOp = true; - } - - // NAD27 to NAD83 has tens of results already. No need to look - // for a pivot - if (!sameGeodeticDatum && - (((res.empty() || !foundInstantiableOp) && - !resFindDirectNonEmptyBeforeFiltering && - context.context->getAllowUseIntermediateCRS() == - CoordinateOperationContext::IntermediateCRSUse:: - IF_NO_DIRECT_TRANSFORMATION) || - context.context->getAllowUseIntermediateCRS() == - CoordinateOperationContext::IntermediateCRSUse::ALWAYS || - getenv("PROJ_FORCE_SEARCH_PIVOT"))) { - auto resWithIntermediate = findsOpsInRegistryWithIntermediate( - sourceCRS, targetCRS, context, false); - res.insert(res.end(), resWithIntermediate.begin(), - resWithIntermediate.end()); - doFilterAndCheckPerfectOp = !res.empty(); - - } else if (!context.inCreateOperationsWithDatumPivotAntiRecursion && - !resFindDirectNonEmptyBeforeFiltering && geodSrc && geodDst && - !sameGeodeticDatum && - context.context->getIntermediateCRS().empty() && - context.context->getAllowUseIntermediateCRS() != - CoordinateOperationContext::IntermediateCRSUse::NEVER) { - - bool tryWithGeodeticDatumIntermediate = res.empty(); - if (!tryWithGeodeticDatumIntermediate) { - // This is in particular for the GDA94 to WGS 84 (G1762) case - // As we have a WGS 84 -> WGS 84 (G1762) null-transformation in the - // PROJ authority, previous steps might have use that WGS 84 - // intermediate directly. They might also have generated a path - // through ITRF2008, as there is a path - // GDA94 (geoc.) -> ITRF2008 (geoc.) -> WGS84 (G1762) (geoc.) - // But there's a better path using - // GDA94 (geog.) --> GDA2020 (geog.) and - // GDA2020 (geoc.) -> WGS84 (G1762) (geoc.) that requires to - // explore intermediates through their datum, and not directly - // trough the CRS code. - // Do that only if the number of results we got through other - // algorithms is small, or if all results we have go through an - // operation in the PROJ authority. - constexpr size_t ARBITRARY_SMALL_NUMBER = 5U; - tryWithGeodeticDatumIntermediate = - res.size() < ARBITRARY_SMALL_NUMBER || - hasResultSetOnlyResultsWithPROJStep(res); - } - if (tryWithGeodeticDatumIntermediate) { - auto resWithIntermediate = findsOpsInRegistryWithIntermediate( - sourceCRS, targetCRS, context, true); - res.insert(res.end(), resWithIntermediate.begin(), - resWithIntermediate.end()); - doFilterAndCheckPerfectOp = !res.empty(); - } - } - - if (doFilterAndCheckPerfectOp) { - // If we get at least a result with perfect accuracy, do not bother - // generating synthetic transforms. - if (hasPerfectAccuracyResult(res, context)) { - return true; - } - } - return false; -} - -// --------------------------------------------------------------------------- - -static std::vector -findCandidateVertCRSForDatum(const io::AuthorityFactoryPtr &authFactory, - const datum::VerticalReferenceFrame *datum) { - std::vector candidates; - assert(datum); - const auto &ids = datum->identifiers(); - const auto &datumName = datum->nameStr(); - if (!ids.empty()) { - for (const auto &id : ids) { - const auto &authName = *(id->codeSpace()); - const auto &code = id->code(); - if (!authName.empty()) { - auto l_candidates = - authFactory->createVerticalCRSFromDatum(authName, code); - for (const auto &candidate : l_candidates) { - candidates.emplace_back(candidate); - } - } - } - } else if (datumName != "unknown" && datumName != "unnamed") { - auto matches = authFactory->createObjectsFromName( - datumName, - {io::AuthorityFactory::ObjectType::VERTICAL_REFERENCE_FRAME}, false, - 2); - if (matches.size() == 1) { - const auto &match = matches.front(); - if (datum->_isEquivalentTo( - match.get(), util::IComparable::Criterion::EQUIVALENT) && - !match->identifiers().empty()) { - return findCandidateVertCRSForDatum( - authFactory, - dynamic_cast( - match.get())); - } - } - } - return candidates; -} - -// --------------------------------------------------------------------------- - -std::vector -CoordinateOperationFactory::Private::createOperationsGeogToVertFromGeoid( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - const crs::VerticalCRS *vertDst, Private::Context &context) { - - ENTER_FUNCTION(); - - const auto useTransf = [&targetCRS, &context, - vertDst](const CoordinateOperationNNPtr &op) { - const auto targetOp = - dynamic_cast(op->targetCRS().get()); - assert(targetOp); - if (targetOp->_isEquivalentTo( - vertDst, util::IComparable::Criterion::EQUIVALENT)) { - return op; - } - std::vector tmp; - createOperationsVertToVert(NN_NO_CHECK(op->targetCRS()), targetCRS, - context, targetOp, vertDst, tmp); - assert(!tmp.empty()); - auto ret = ConcatenatedOperation::createComputeMetadata( - {op, tmp.front()}, disallowEmptyIntersection); - return ret; - }; - - const auto getProjGeoidTransformation = [&sourceCRS, &targetCRS, &vertDst, - &context]( - const CoordinateOperationNNPtr &model, - const std::string &projFilename) { - - const auto getNameVertCRSMetre = [](const std::string &name) { - if (name.empty()) - return std::string("unnamed"); - auto ret(name); - bool haveOriginalUnit = false; - if (name.back() == ')') { - const auto pos = ret.rfind(" ("); - if (pos != std::string::npos) { - haveOriginalUnit = true; - ret = ret.substr(0, pos); - } - } - const auto pos = ret.rfind(" depth"); - if (pos != std::string::npos) { - ret = ret.substr(0, pos) + " height"; - } - if (!haveOriginalUnit) { - ret += " (metre)"; - } - return ret; - }; - - const auto &axis = vertDst->coordinateSystem()->axisList()[0]; - const auto geogSrcCRS = - dynamic_cast(model->interpolationCRS().get()) - ? NN_NO_CHECK(model->interpolationCRS()) - : sourceCRS; - const auto vertCRSMetre = - axis->unit() == common::UnitOfMeasure::METRE && - axis->direction() == cs::AxisDirection::UP - ? targetCRS - : util::nn_static_pointer_cast( - crs::VerticalCRS::create( - util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, - getNameVertCRSMetre(targetCRS->nameStr())), - vertDst->datum(), vertDst->datumEnsemble(), - cs::VerticalCS::createGravityRelatedHeight( - common::UnitOfMeasure::METRE))); - const auto properties = util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, - buildOpName("Transformation", vertCRSMetre, geogSrcCRS)); - - // Try to find a representative value for the accuracy of this grid - // from the registered transformations. - std::vector accuracies; - const auto &modelAccuracies = model->coordinateOperationAccuracies(); - if (modelAccuracies.empty()) { - const auto &authFactory = context.context->getAuthorityFactory(); - if (authFactory) { - const auto transformationsForGrid = - io::DatabaseContext::getTransformationsForGridName( - authFactory->databaseContext(), projFilename); - double accuracy = -1; - for (const auto &transf : transformationsForGrid) { - accuracy = std::max(accuracy, getAccuracy(transf)); - } - if (accuracy >= 0) { - accuracies.emplace_back( - metadata::PositionalAccuracy::create( - toString(accuracy))); - } - } - } - - return Transformation::createGravityRelatedHeightToGeographic3D( - properties, vertCRSMetre, geogSrcCRS, nullptr, projFilename, - !modelAccuracies.empty() ? modelAccuracies : accuracies); - }; - - std::vector res; - const auto &authFactory = context.context->getAuthorityFactory(); - if (authFactory) { - const auto &models = vertDst->geoidModel(); - for (const auto &model : models) { - const auto &modelName = model->nameStr(); - const auto transformations = - starts_with(modelName, "PROJ ") - ? std::vector< - CoordinateOperationNNPtr>{getProjGeoidTransformation( - model, modelName.substr(strlen("PROJ ")))} - : authFactory->getTransformationsForGeoid( - modelName, - context.context->getUsePROJAlternativeGridNames()); - for (const auto &transf : transformations) { - if (dynamic_cast( - transf->sourceCRS().get()) && - dynamic_cast( - transf->targetCRS().get())) { - res.push_back(useTransf(transf)); - } else if (dynamic_cast( - transf->targetCRS().get()) && - dynamic_cast( - transf->sourceCRS().get())) { - res.push_back(useTransf(transf->inverse())); - } - } - } - } - - return res; -} - -// --------------------------------------------------------------------------- - -std::vector CoordinateOperationFactory::Private:: - createOperationsGeogToVertWithIntermediateVert( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - const crs::VerticalCRS *vertDst, Private::Context &context) { - - ENTER_FUNCTION(); - - std::vector res; - - struct AntiRecursionGuard { - Context &context; - - explicit AntiRecursionGuard(Context &contextIn) : context(contextIn) { - assert(!context.inCreateOperationsGeogToVertWithIntermediateVert); - context.inCreateOperationsGeogToVertWithIntermediateVert = true; - } - - ~AntiRecursionGuard() { - context.inCreateOperationsGeogToVertWithIntermediateVert = false; - } - }; - AntiRecursionGuard guard(context); - const auto &authFactory = context.context->getAuthorityFactory(); - const auto dbContext = authFactory->databaseContext().as_nullable(); - - auto candidatesVert = findCandidateVertCRSForDatum( - authFactory, vertDst->datumNonNull(dbContext).get()); - for (const auto &candidateVert : candidatesVert) { - auto resTmp = createOperations(sourceCRS, candidateVert, context); - if (!resTmp.empty()) { - const auto opsSecond = - createOperations(candidateVert, targetCRS, context); - if (!opsSecond.empty()) { - // The transformation from candidateVert to targetCRS should - // be just a unit change typically, so take only the first one, - // which is likely/hopefully the only one. - for (const auto &opFirst : resTmp) { - if (hasIdentifiers(opFirst)) { - if (candidateVert->_isEquivalentTo( - targetCRS.get(), - util::IComparable::Criterion::EQUIVALENT)) { - res.emplace_back(opFirst); - } else { - res.emplace_back( - ConcatenatedOperation::createComputeMetadata( - {opFirst, opsSecond.front()}, - disallowEmptyIntersection)); - } - } - } - if (!res.empty()) - break; - } - } - } - - return res; -} - -// --------------------------------------------------------------------------- - -std::vector CoordinateOperationFactory::Private:: - createOperationsGeogToVertWithAlternativeGeog( - const crs::CRSNNPtr & /*sourceCRS*/, // geographic CRS - const crs::CRSNNPtr &targetCRS, // vertical CRS - Private::Context &context) { - - ENTER_FUNCTION(); - - std::vector res; - - struct AntiRecursionGuard { - Context &context; - - explicit AntiRecursionGuard(Context &contextIn) : context(contextIn) { - assert(!context.inCreateOperationsGeogToVertWithAlternativeGeog); - context.inCreateOperationsGeogToVertWithAlternativeGeog = true; - } - - ~AntiRecursionGuard() { - context.inCreateOperationsGeogToVertWithAlternativeGeog = false; - } - }; - AntiRecursionGuard guard(context); - - // Generally EPSG has operations from GeogCrs to VertCRS - auto ops = findOpsInRegistryDirectTo(targetCRS, context); - - for (const auto &op : ops) { - const auto tmpCRS = op->sourceCRS(); - if (tmpCRS && dynamic_cast(tmpCRS.get())) { - res.emplace_back(op); - } - } - - return res; -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private:: - createOperationsFromDatabaseWithVertCRS( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::GeographicCRS *geogSrc, - const crs::GeographicCRS *geogDst, const crs::VerticalCRS *vertSrc, - const crs::VerticalCRS *vertDst, - std::vector &res) { - - // Typically to transform from "NAVD88 height (ftUS)" to a geog CRS - // by using transformations of "NAVD88 height" (metre) to that geog CRS - if (res.empty() && - !context.inCreateOperationsGeogToVertWithIntermediateVert && geogSrc && - vertDst) { - res = createOperationsGeogToVertWithIntermediateVert( - sourceCRS, targetCRS, vertDst, context); - } else if (res.empty() && - !context.inCreateOperationsGeogToVertWithIntermediateVert && - geogDst && vertSrc) { - res = applyInverse(createOperationsGeogToVertWithIntermediateVert( - targetCRS, sourceCRS, vertSrc, context)); - } - - // NAD83 only exists in 2D version in EPSG, so if it has been - // promoted to 3D, when researching a vertical to geog - // transformation, try to down cast to 2D. - const auto geog3DToVertTryThroughGeog2D = [&res, &context]( - const crs::GeographicCRS *geogSrcIn, const crs::VerticalCRS *vertDstIn, - const crs::CRSNNPtr &targetCRSIn) { - if (res.empty() && geogSrcIn && vertDstIn && - geogSrcIn->coordinateSystem()->axisList().size() == 3) { - const auto &authFactory = context.context->getAuthorityFactory(); - const auto dbContext = - authFactory ? authFactory->databaseContext().as_nullable() - : nullptr; - const auto candidatesSrcGeod(findCandidateGeodCRSForDatum( - authFactory, geogSrcIn, - geogSrcIn->datumNonNull(dbContext).get())); - for (const auto &candidate : candidatesSrcGeod) { - auto geogCandidate = - util::nn_dynamic_pointer_cast( - candidate); - if (geogCandidate && - geogCandidate->coordinateSystem()->axisList().size() == 2) { - bool ignored; - res = - findOpsInRegistryDirect(NN_NO_CHECK(geogCandidate), - targetCRSIn, context, ignored); - break; - } - } - return true; - } - return false; - }; - - if (geog3DToVertTryThroughGeog2D(geogSrc, vertDst, targetCRS)) { - // do nothing - } else if (geog3DToVertTryThroughGeog2D(geogDst, vertSrc, sourceCRS)) { - res = applyInverse(res); - } - - // There's no direct transformation from NAVD88 height to WGS84, - // so try to research all transformations from NAVD88 to another - // intermediate GeographicCRS. - if (res.empty() && - !context.inCreateOperationsGeogToVertWithAlternativeGeog && geogSrc && - vertDst) { - res = createOperationsGeogToVertWithAlternativeGeog(sourceCRS, - targetCRS, context); - } else if (res.empty() && - !context.inCreateOperationsGeogToVertWithAlternativeGeog && - geogDst && vertSrc) { - res = applyInverse(createOperationsGeogToVertWithAlternativeGeog( - targetCRS, sourceCRS, context)); - } -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsGeodToGeod( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::GeodeticCRS *geodSrc, - const crs::GeodeticCRS *geodDst, - std::vector &res) { - - ENTER_FUNCTION(); - - if (geodSrc->ellipsoid()->celestialBody() != - geodDst->ellipsoid()->celestialBody()) { - throw util::UnsupportedOperationException( - "Source and target ellipsoid do not belong to the same " - "celestial body"); - } - - auto geogSrc = dynamic_cast(geodSrc); - auto geogDst = dynamic_cast(geodDst); - - if (geogSrc && geogDst) { - createOperationsGeogToGeog(res, sourceCRS, targetCRS, context, geogSrc, - geogDst); - return; - } - - const bool isSrcGeocentric = geodSrc->isGeocentric(); - const bool isSrcGeographic = geogSrc != nullptr; - const bool isTargetGeocentric = geodDst->isGeocentric(); - const bool isTargetGeographic = geogDst != nullptr; - - const auto IsSameDatum = [&context, &geodSrc, &geodDst]() { - const auto &authFactory = context.context->getAuthorityFactory(); - const auto dbContext = - authFactory ? authFactory->databaseContext().as_nullable() - : nullptr; - - return geodSrc->datumNonNull(dbContext)->_isEquivalentTo( - geodDst->datumNonNull(dbContext).get(), - util::IComparable::Criterion::EQUIVALENT); - }; - - if (((isSrcGeocentric && isTargetGeographic) || - (isSrcGeographic && isTargetGeocentric))) { - - // Same datum ? - if (IsSameDatum()) { - res.emplace_back( - Conversion::createGeographicGeocentric(sourceCRS, targetCRS)); - } else if (isSrcGeocentric && geogDst) { - std::string interm_crs_name(geogDst->nameStr()); - interm_crs_name += " (geocentric)"; - auto interm_crs = - util::nn_static_pointer_cast(crs::GeodeticCRS::create( - addDomains(util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, - interm_crs_name), - geogDst), - geogDst->datum(), geogDst->datumEnsemble(), - NN_CHECK_ASSERT( - util::nn_dynamic_pointer_cast( - geodSrc->coordinateSystem())))); - auto opFirst = - createBallparkGeocentricTranslation(sourceCRS, interm_crs); - auto opSecond = - Conversion::createGeographicGeocentric(interm_crs, targetCRS); - res.emplace_back(ConcatenatedOperation::createComputeMetadata( - {opFirst, opSecond}, disallowEmptyIntersection)); - } else { - // Apply previous case in reverse way - std::vector resTmp; - createOperationsGeodToGeod(targetCRS, sourceCRS, context, geodDst, - geodSrc, resTmp); - assert(resTmp.size() == 1); - res.emplace_back(resTmp.front()->inverse()); - } - - return; - } - - if (isSrcGeocentric && isTargetGeocentric) { - if (sourceCRS->_isEquivalentTo( - targetCRS.get(), util::IComparable::Criterion::EQUIVALENT) || - IsSameDatum()) { - std::string name(NULL_GEOCENTRIC_TRANSLATION); - name += " from "; - name += sourceCRS->nameStr(); - name += " to "; - name += targetCRS->nameStr(); - res.emplace_back(Transformation::createGeocentricTranslations( - util::PropertyMap() - .set(common::IdentifiedObject::NAME_KEY, name) - .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - metadata::Extent::WORLD), - sourceCRS, targetCRS, 0.0, 0.0, 0.0, - {metadata::PositionalAccuracy::create("0")})); - } else { - res.emplace_back( - createBallparkGeocentricTranslation(sourceCRS, targetCRS)); - } - return; - } - - // Transformation between two geodetic systems of unknown type - // This should normally not be triggered with "standard" CRS - res.emplace_back(createGeodToGeodPROJBased(sourceCRS, targetCRS)); -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsDerivedTo( - const crs::CRSNNPtr & /*sourceCRS*/, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::DerivedCRS *derivedSrc, - std::vector &res) { - - ENTER_FUNCTION(); - - auto opFirst = derivedSrc->derivingConversion()->inverse(); - // Small optimization if the targetCRS is the baseCRS of the source - // derivedCRS. - if (derivedSrc->baseCRS()->_isEquivalentTo( - targetCRS.get(), util::IComparable::Criterion::EQUIVALENT)) { - res.emplace_back(opFirst); - return; - } - auto opsSecond = - createOperations(derivedSrc->baseCRS(), targetCRS, context); - for (const auto &opSecond : opsSecond) { - try { - res.emplace_back(ConcatenatedOperation::createComputeMetadata( - {opFirst, opSecond}, disallowEmptyIntersection)); - } catch (const InvalidOperationEmptyIntersection &) { - } - } -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsBoundToGeog( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::BoundCRS *boundSrc, - const crs::GeographicCRS *geogDst, - std::vector &res) { - - ENTER_FUNCTION(); - - const auto &hubSrc = boundSrc->hubCRS(); - auto hubSrcGeog = dynamic_cast(hubSrc.get()); - auto geogCRSOfBaseOfBoundSrc = boundSrc->baseCRS()->extractGeographicCRS(); - { - // If geogCRSOfBaseOfBoundSrc is a DerivedGeographicCRS, use its base - // instead (if it is a GeographicCRS) - auto derivedGeogCRS = - std::dynamic_pointer_cast( - geogCRSOfBaseOfBoundSrc); - if (derivedGeogCRS) { - auto baseCRS = std::dynamic_pointer_cast( - derivedGeogCRS->baseCRS().as_nullable()); - if (baseCRS) { - geogCRSOfBaseOfBoundSrc = baseCRS; - } - } - } - - const auto &authFactory = context.context->getAuthorityFactory(); - const auto dbContext = - authFactory ? authFactory->databaseContext().as_nullable() : nullptr; - - const auto geogDstDatum = geogDst->datumNonNull(dbContext); - - // If the underlying datum of the source is the same as the target, do - // not consider the boundCRS at all, but just its base - if (geogCRSOfBaseOfBoundSrc) { - auto geogCRSOfBaseOfBoundSrcDatum = - geogCRSOfBaseOfBoundSrc->datumNonNull(dbContext); - if (geogCRSOfBaseOfBoundSrcDatum->_isEquivalentTo( - geogDstDatum.get(), util::IComparable::Criterion::EQUIVALENT)) { - res = createOperations(boundSrc->baseCRS(), targetCRS, context); - return; - } - } - - bool triedBoundCrsToGeogCRSSameAsHubCRS = false; - // Is it: boundCRS to a geogCRS that is the same as the hubCRS ? - if (hubSrcGeog && geogCRSOfBaseOfBoundSrc && - (hubSrcGeog->_isEquivalentTo( - geogDst, util::IComparable::Criterion::EQUIVALENT) || - hubSrcGeog->is2DPartOf3D(NN_NO_CHECK(geogDst), dbContext))) { - triedBoundCrsToGeogCRSSameAsHubCRS = true; - - CoordinateOperationPtr opIntermediate; - if (!geogCRSOfBaseOfBoundSrc->_isEquivalentTo( - boundSrc->transformation()->sourceCRS().get(), - util::IComparable::Criterion::EQUIVALENT)) { - auto opsIntermediate = createOperations( - NN_NO_CHECK(geogCRSOfBaseOfBoundSrc), - boundSrc->transformation()->sourceCRS(), context); - assert(!opsIntermediate.empty()); - opIntermediate = opsIntermediate.front(); - } - - if (boundSrc->baseCRS() == geogCRSOfBaseOfBoundSrc) { - if (opIntermediate) { - try { - res.emplace_back( - ConcatenatedOperation::createComputeMetadata( - {NN_NO_CHECK(opIntermediate), - boundSrc->transformation()}, - disallowEmptyIntersection)); - } catch (const InvalidOperationEmptyIntersection &) { - } - } else { - // Optimization to avoid creating a useless concatenated - // operation - res.emplace_back(boundSrc->transformation()); - } - return; - } - auto opsFirst = createOperations( - boundSrc->baseCRS(), NN_NO_CHECK(geogCRSOfBaseOfBoundSrc), context); - if (!opsFirst.empty()) { - for (const auto &opFirst : opsFirst) { - try { - std::vector subops; - subops.emplace_back(opFirst); - if (opIntermediate) { - subops.emplace_back(NN_NO_CHECK(opIntermediate)); - } - subops.emplace_back(boundSrc->transformation()); - res.emplace_back( - ConcatenatedOperation::createComputeMetadata( - subops, disallowEmptyIntersection)); - } catch (const InvalidOperationEmptyIntersection &) { - } - } - if (!res.empty()) { - return; - } - } - // If the datum are equivalent, this is also fine - } else if (geogCRSOfBaseOfBoundSrc && hubSrcGeog && - hubSrcGeog->datumNonNull(dbContext)->_isEquivalentTo( - geogDstDatum.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()) { - CoordinateOperationPtr opIntermediate; - if (!geogCRSOfBaseOfBoundSrc->_isEquivalentTo( - boundSrc->transformation()->sourceCRS().get(), - util::IComparable::Criterion::EQUIVALENT)) { - auto opsIntermediate = createOperations( - NN_NO_CHECK(geogCRSOfBaseOfBoundSrc), - boundSrc->transformation()->sourceCRS(), context); - assert(!opsIntermediate.empty()); - opIntermediate = opsIntermediate.front(); - } - for (const auto &opFirst : opsFirst) { - for (const auto &opLast : opsLast) { - try { - std::vector subops; - subops.emplace_back(opFirst); - if (opIntermediate) { - subops.emplace_back(NN_NO_CHECK(opIntermediate)); - } - subops.emplace_back(boundSrc->transformation()); - subops.emplace_back(opLast); - res.emplace_back( - ConcatenatedOperation::createComputeMetadata( - subops, disallowEmptyIntersection)); - } catch (const InvalidOperationEmptyIntersection &) { - } - } - } - if (!res.empty()) { - return; - } - } - // 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 && - geogCRSOfBaseOfBoundSrc->ellipsoid()->_isEquivalentTo( - datum::Ellipsoid::CLARKE_1866.get(), - util::IComparable::Criterion::EQUIVALENT) && - hubSrcGeog->datumNonNull(dbContext)->_isEquivalentTo( - datum::GeodeticReferenceFrame::EPSG_6326.get(), - util::IComparable::Criterion::EQUIVALENT) && - geogDstDatum->_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; - } 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}, disallowEmptyIntersection)); - } catch (const InvalidOperationEmptyIntersection &) { - } - } - if (!res.empty()) { - return; - } - } - } - } - - if (hubSrcGeog && - hubSrcGeog->_isEquivalentTo(geogDst, - util::IComparable::Criterion::EQUIVALENT) && - dynamic_cast(boundSrc->baseCRS().get())) { - auto transfSrc = boundSrc->transformation()->sourceCRS(); - if (dynamic_cast(transfSrc.get()) && - !boundSrc->baseCRS()->_isEquivalentTo( - transfSrc.get(), util::IComparable::Criterion::EQUIVALENT)) { - auto opsFirst = - createOperations(boundSrc->baseCRS(), transfSrc, context); - for (const auto &opFirst : opsFirst) { - try { - res.emplace_back( - ConcatenatedOperation::createComputeMetadata( - {opFirst, boundSrc->transformation()}, - disallowEmptyIntersection)); - } catch (const InvalidOperationEmptyIntersection &) { - } - } - return; - } - - res.emplace_back(boundSrc->transformation()); - return; - } - - if (!triedBoundCrsToGeogCRSSameAsHubCRS && hubSrcGeog && - geogCRSOfBaseOfBoundSrc) { - // This one should go to the above 'Is it: boundCRS to a geogCRS - // that is the same as the hubCRS ?' case - auto opsFirst = createOperations(sourceCRS, hubSrc, context); - auto opsLast = createOperations(hubSrc, targetCRS, context); - if (!opsFirst.empty() && !opsLast.empty()) { - for (const auto &opFirst : opsFirst) { - for (const auto &opLast : opsLast) { - // Exclude artificial transformations from the hub - // to the target CRS, if it is the only one. - if (opsLast.size() > 1 || - !opLast->hasBallparkTransformation()) { - try { - res.emplace_back( - ConcatenatedOperation::createComputeMetadata( - {opFirst, opLast}, - disallowEmptyIntersection)); - } catch (const InvalidOperationEmptyIntersection &) { - } - } else { - // std::cerr << "excluded " << opLast->nameStr() << - // std::endl; - } - } - } - if (!res.empty()) { - return; - } - } - } - - auto vertCRSOfBaseOfBoundSrc = - dynamic_cast(boundSrc->baseCRS().get()); - if (vertCRSOfBaseOfBoundSrc && hubSrcGeog) { - auto opsFirst = createOperations(sourceCRS, hubSrc, context); - if (context.skipHorizontalTransformation) { - if (!opsFirst.empty()) { - const auto &hubAxisList = - hubSrcGeog->coordinateSystem()->axisList(); - const auto &targetAxisList = - geogDst->coordinateSystem()->axisList(); - if (hubAxisList.size() == 3 && targetAxisList.size() == 3 && - !hubAxisList[2]->_isEquivalentTo( - targetAxisList[2].get(), - util::IComparable::Criterion::EQUIVALENT)) { - - const auto &srcAxis = hubAxisList[2]; - const double convSrc = srcAxis->unit().conversionToSI(); - const auto &dstAxis = targetAxisList[2]; - const double convDst = dstAxis->unit().conversionToSI(); - const bool srcIsUp = - srcAxis->direction() == cs::AxisDirection::UP; - const bool srcIsDown = - srcAxis->direction() == cs::AxisDirection::DOWN; - const bool dstIsUp = - dstAxis->direction() == cs::AxisDirection::UP; - const bool dstIsDown = - dstAxis->direction() == cs::AxisDirection::DOWN; - const bool heightDepthReversal = - ((srcIsUp && dstIsDown) || (srcIsDown && dstIsUp)); - - const double factor = convSrc / convDst; - auto conv = Conversion::createChangeVerticalUnit( - util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, - "Change of vertical unit"), - common::Scale(heightDepthReversal ? -factor : factor)); - - conv->setCRSs( - hubSrc, - hubSrc->demoteTo2D(std::string(), dbContext) - ->promoteTo3D(std::string(), dbContext, dstAxis), - nullptr); - - for (const auto &op : opsFirst) { - try { - res.emplace_back( - ConcatenatedOperation::createComputeMetadata( - {op, conv}, disallowEmptyIntersection)); - } catch (const InvalidOperationEmptyIntersection &) { - } - } - } else { - res = opsFirst; - } - } - return; - } else { - auto opsSecond = createOperations(hubSrc, targetCRS, context); - if (!opsFirst.empty() && !opsSecond.empty()) { - for (const auto &opFirst : opsFirst) { - for (const auto &opLast : opsSecond) { - // Exclude artificial transformations from the hub - // to the target CRS - if (!opLast->hasBallparkTransformation()) { - try { - res.emplace_back( - ConcatenatedOperation:: - createComputeMetadata( - {opFirst, opLast}, - disallowEmptyIntersection)); - } catch ( - const InvalidOperationEmptyIntersection &) { - } - } else { - // std::cerr << "excluded " << opLast->nameStr() << - // std::endl; - } - } - } - if (!res.empty()) { - return; - } - } - } - } - - res = createOperations(boundSrc->baseCRS(), targetCRS, context); -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsBoundToVert( - const crs::CRSNNPtr & /*sourceCRS*/, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::BoundCRS *boundSrc, - const crs::VerticalCRS *vertDst, - std::vector &res) { - - ENTER_FUNCTION(); - - auto baseSrcVert = - dynamic_cast(boundSrc->baseCRS().get()); - const auto &hubSrc = boundSrc->hubCRS(); - auto hubSrcVert = dynamic_cast(hubSrc.get()); - if (baseSrcVert && hubSrcVert && - vertDst->_isEquivalentTo(hubSrcVert, - util::IComparable::Criterion::EQUIVALENT)) { - res.emplace_back(boundSrc->transformation()); - return; - } - - res = createOperations(boundSrc->baseCRS(), targetCRS, context); -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsVertToVert( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::VerticalCRS *vertSrc, - const crs::VerticalCRS *vertDst, - std::vector &res) { - - ENTER_FUNCTION(); - - const auto &authFactory = context.context->getAuthorityFactory(); - const auto dbContext = - authFactory ? authFactory->databaseContext().as_nullable() : nullptr; - - const auto srcDatum = vertSrc->datumNonNull(dbContext); - const auto dstDatum = vertDst->datumNonNull(dbContext); - const bool equivalentVDatum = srcDatum->_isEquivalentTo( - dstDatum.get(), util::IComparable::Criterion::EQUIVALENT); - - const auto &srcAxis = vertSrc->coordinateSystem()->axisList()[0]; - const double convSrc = srcAxis->unit().conversionToSI(); - const auto &dstAxis = vertDst->coordinateSystem()->axisList()[0]; - const double convDst = dstAxis->unit().conversionToSI(); - const bool srcIsUp = srcAxis->direction() == cs::AxisDirection::UP; - const bool srcIsDown = srcAxis->direction() == cs::AxisDirection::DOWN; - const bool dstIsUp = dstAxis->direction() == cs::AxisDirection::UP; - const bool dstIsDown = dstAxis->direction() == cs::AxisDirection::DOWN; - const bool heightDepthReversal = - ((srcIsUp && dstIsDown) || (srcIsDown && dstIsUp)); - - const double factor = convSrc / convDst; - auto name = buildTransfName(sourceCRS->nameStr(), targetCRS->nameStr()); - if (!equivalentVDatum) { - name += BALLPARK_VERTICAL_TRANSFORMATION; - auto conv = Transformation::createChangeVerticalUnit( - util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, name), - sourceCRS, targetCRS, - // In case of a height depth reversal, we should probably have - // 2 steps instead of putting a negative factor... - common::Scale(heightDepthReversal ? -factor : factor), {}); - conv->setHasBallparkTransformation(true); - res.push_back(conv); - } else if (convSrc != convDst || !heightDepthReversal) { - auto conv = Conversion::createChangeVerticalUnit( - util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, name), - // In case of a height depth reversal, we should probably have - // 2 steps instead of putting a negative factor... - common::Scale(heightDepthReversal ? -factor : factor)); - conv->setCRSs(sourceCRS, targetCRS, nullptr); - res.push_back(conv); - } else { - auto conv = Conversion::createHeightDepthReversal( - util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, name)); - conv->setCRSs(sourceCRS, targetCRS, nullptr); - res.push_back(conv); - } -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsVertToGeog( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::VerticalCRS *vertSrc, - const crs::GeographicCRS *geogDst, - std::vector &res) { - - ENTER_FUNCTION(); - - if (vertSrc->identifiers().empty()) { - const auto &vertSrcName = vertSrc->nameStr(); - const auto &authFactory = context.context->getAuthorityFactory(); - if (authFactory != nullptr && vertSrcName != "unnamed" && - vertSrcName != "unknown") { - auto matches = authFactory->createObjectsFromName( - vertSrcName, {io::AuthorityFactory::ObjectType::VERTICAL_CRS}, - false, 2); - if (matches.size() == 1) { - const auto &match = matches.front(); - if (vertSrc->_isEquivalentTo( - match.get(), - util::IComparable::Criterion::EQUIVALENT) && - !match->identifiers().empty()) { - auto resTmp = createOperations( - NN_NO_CHECK( - util::nn_dynamic_pointer_cast( - match)), - targetCRS, context); - res.insert(res.end(), resTmp.begin(), resTmp.end()); - return; - } - } - } - } - - createOperationsVertToGeogBallpark(sourceCRS, targetCRS, context, vertSrc, - geogDst, res); -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsVertToGeogBallpark( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &, const crs::VerticalCRS *vertSrc, - const crs::GeographicCRS *geogDst, - std::vector &res) { - - ENTER_FUNCTION(); - - const auto &srcAxis = vertSrc->coordinateSystem()->axisList()[0]; - const double convSrc = srcAxis->unit().conversionToSI(); - double convDst = 1.0; - const auto &geogAxis = geogDst->coordinateSystem()->axisList(); - bool dstIsUp = true; - bool dstIsDown = true; - if (geogAxis.size() == 3) { - const auto &dstAxis = geogAxis[2]; - convDst = dstAxis->unit().conversionToSI(); - dstIsUp = dstAxis->direction() == cs::AxisDirection::UP; - dstIsDown = dstAxis->direction() == cs::AxisDirection::DOWN; - } - const bool srcIsUp = srcAxis->direction() == cs::AxisDirection::UP; - const bool srcIsDown = srcAxis->direction() == cs::AxisDirection::DOWN; - const bool heightDepthReversal = - ((srcIsUp && dstIsDown) || (srcIsDown && dstIsUp)); - - const double factor = convSrc / convDst; - - const auto &sourceCRSExtent = getExtent(sourceCRS); - const auto &targetCRSExtent = getExtent(targetCRS); - const bool sameExtent = - sourceCRSExtent && targetCRSExtent && - sourceCRSExtent->_isEquivalentTo( - targetCRSExtent.get(), util::IComparable::Criterion::EQUIVALENT); - - util::PropertyMap map; - map.set(common::IdentifiedObject::NAME_KEY, - buildTransfName(sourceCRS->nameStr(), targetCRS->nameStr()) + - BALLPARK_VERTICAL_TRANSFORMATION_NO_ELLIPSOID_VERT_HEIGHT) - .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - sameExtent ? NN_NO_CHECK(sourceCRSExtent) - : metadata::Extent::WORLD); - - auto conv = Transformation::createChangeVerticalUnit( - map, sourceCRS, targetCRS, - common::Scale(heightDepthReversal ? -factor : factor), {}); - conv->setHasBallparkTransformation(true); - res.push_back(conv); -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsBoundToBound( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::BoundCRS *boundSrc, - const crs::BoundCRS *boundDst, std::vector &res) { - - ENTER_FUNCTION(); - - // BoundCRS to BoundCRS of horizontal CRS using the same (geographic) hub - const auto &hubSrc = boundSrc->hubCRS(); - auto hubSrcGeog = dynamic_cast(hubSrc.get()); - const auto &hubDst = boundDst->hubCRS(); - auto hubDstGeog = dynamic_cast(hubDst.get()); - auto geogCRSOfBaseOfBoundSrc = boundSrc->baseCRS()->extractGeographicCRS(); - auto geogCRSOfBaseOfBoundDst = boundDst->baseCRS()->extractGeographicCRS(); - if (hubSrcGeog && hubDstGeog && - hubSrcGeog->_isEquivalentTo(hubDstGeog, - util::IComparable::Criterion::EQUIVALENT) && - geogCRSOfBaseOfBoundSrc && geogCRSOfBaseOfBoundDst) { - const bool firstIsNoOp = geogCRSOfBaseOfBoundSrc->_isEquivalentTo( - boundSrc->baseCRS().get(), - util::IComparable::Criterion::EQUIVALENT); - const bool lastIsNoOp = geogCRSOfBaseOfBoundDst->_isEquivalentTo( - boundDst->baseCRS().get(), - util::IComparable::Criterion::EQUIVALENT); - auto opsFirst = createOperations( - boundSrc->baseCRS(), NN_NO_CHECK(geogCRSOfBaseOfBoundSrc), context); - auto opsLast = createOperations(NN_NO_CHECK(geogCRSOfBaseOfBoundDst), - boundDst->baseCRS(), context); - if (!opsFirst.empty() && !opsLast.empty()) { - const auto &opSecond = boundSrc->transformation(); - auto opThird = boundDst->transformation()->inverse(); - for (const auto &opFirst : opsFirst) { - for (const auto &opLast : opsLast) { - try { - std::vector ops; - if (!firstIsNoOp) { - ops.push_back(opFirst); - } - ops.push_back(opSecond); - ops.push_back(opThird); - if (!lastIsNoOp) { - ops.push_back(opLast); - } - res.emplace_back( - ConcatenatedOperation::createComputeMetadata( - ops, disallowEmptyIntersection)); - } catch (const InvalidOperationEmptyIntersection &) { - } - } - } - if (!res.empty()) { - return; - } - } - } - - // BoundCRS to BoundCRS of vertical CRS using the same vertical datum - // ==> ignore the bound transformation - auto baseOfBoundSrcAsVertCRS = - dynamic_cast(boundSrc->baseCRS().get()); - auto baseOfBoundDstAsVertCRS = - dynamic_cast(boundDst->baseCRS().get()); - if (baseOfBoundSrcAsVertCRS && baseOfBoundDstAsVertCRS) { - - const auto &authFactory = context.context->getAuthorityFactory(); - const auto dbContext = - authFactory ? authFactory->databaseContext().as_nullable() - : nullptr; - - const auto datumSrc = baseOfBoundSrcAsVertCRS->datumNonNull(dbContext); - const auto datumDst = baseOfBoundDstAsVertCRS->datumNonNull(dbContext); - if (datumSrc->nameStr() == datumDst->nameStr() && - (datumSrc->nameStr() != "unknown" || - boundSrc->transformation()->_isEquivalentTo( - boundDst->transformation().get(), - util::IComparable::Criterion::EQUIVALENT))) { - res = createOperations(boundSrc->baseCRS(), boundDst->baseCRS(), - context); - return; - } - } - - // BoundCRS to BoundCRS of vertical CRS - auto vertCRSOfBaseOfBoundSrc = boundSrc->baseCRS()->extractVerticalCRS(); - auto vertCRSOfBaseOfBoundDst = boundDst->baseCRS()->extractVerticalCRS(); - if (hubSrcGeog && hubDstGeog && - hubSrcGeog->_isEquivalentTo(hubDstGeog, - util::IComparable::Criterion::EQUIVALENT) && - vertCRSOfBaseOfBoundSrc && vertCRSOfBaseOfBoundDst) { - auto opsFirst = createOperations(sourceCRS, hubSrc, 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, opLast}, disallowEmptyIntersection)); - } catch (const InvalidOperationEmptyIntersection &) { - } - } - } - if (!res.empty()) { - return; - } - } - } - - res = createOperations(boundSrc->baseCRS(), boundDst->baseCRS(), context); -} - -// --------------------------------------------------------------------------- - -static std::vector -getOps(const CoordinateOperationNNPtr &op) { - auto concatenated = dynamic_cast(op.get()); - if (concatenated) - return concatenated->operations(); - return {op}; -} - -// --------------------------------------------------------------------------- - -static bool useDifferentTransformationsForSameSourceTarget( - const CoordinateOperationNNPtr &opA, const CoordinateOperationNNPtr &opB) { - auto subOpsA = getOps(opA); - auto subOpsB = getOps(opB); - for (const auto &subOpA : subOpsA) { - if (!dynamic_cast(subOpA.get())) - continue; - if (subOpA->sourceCRS()->nameStr() == "unknown" || - subOpA->targetCRS()->nameStr() == "unknown") - continue; - for (const auto &subOpB : subOpsB) { - if (!dynamic_cast(subOpB.get())) - continue; - if (subOpB->sourceCRS()->nameStr() == "unknown" || - subOpB->targetCRS()->nameStr() == "unknown") - continue; - - if (subOpA->sourceCRS()->nameStr() == - subOpB->sourceCRS()->nameStr() && - subOpA->targetCRS()->nameStr() == - subOpB->targetCRS()->nameStr()) { - if (starts_with(subOpA->nameStr(), NULL_GEOGRAPHIC_OFFSET) && - starts_with(subOpB->nameStr(), NULL_GEOGRAPHIC_OFFSET)) { - continue; - } - - if (!subOpA->isEquivalentTo(subOpB.get())) { - return true; - } - } else if (subOpA->sourceCRS()->nameStr() == - subOpB->targetCRS()->nameStr() && - subOpA->targetCRS()->nameStr() == - subOpB->sourceCRS()->nameStr()) { - if (starts_with(subOpA->nameStr(), NULL_GEOGRAPHIC_OFFSET) && - starts_with(subOpB->nameStr(), NULL_GEOGRAPHIC_OFFSET)) { - continue; - } - - if (!subOpA->isEquivalentTo(subOpB->inverse().get())) { - return true; - } - } - } - } - return false; -} - -// --------------------------------------------------------------------------- - -static crs::GeographicCRSPtr -getInterpolationGeogCRS(const CoordinateOperationNNPtr &verticalTransform, - const io::DatabaseContextPtr &dbContext) { - crs::GeographicCRSPtr interpolationGeogCRS; - auto transformationVerticalTransform = - dynamic_cast(verticalTransform.get()); - if (transformationVerticalTransform == nullptr) { - const auto concat = dynamic_cast( - verticalTransform.get()); - if (concat) { - const auto &steps = concat->operations(); - // Is this change of unit and/or height depth reversal + - // transformation ? - for (const auto &step : steps) { - const auto transf = - dynamic_cast(step.get()); - if (transf) { - // Only support a single Transformation in the steps - if (transformationVerticalTransform != nullptr) { - transformationVerticalTransform = nullptr; - break; - } - transformationVerticalTransform = transf; - } - } - } - } - if (transformationVerticalTransform && - !transformationVerticalTransform->hasBallparkTransformation()) { - auto interpTransformCRS = - transformationVerticalTransform->interpolationCRS(); - if (interpTransformCRS) { - interpolationGeogCRS = - std::dynamic_pointer_cast( - interpTransformCRS); - } else { - // If no explicit interpolation CRS, then - // this will be the geographic CRS of the - // vertical to geog transformation - interpolationGeogCRS = - std::dynamic_pointer_cast( - transformationVerticalTransform->targetCRS().as_nullable()); - } - } - - if (interpolationGeogCRS) { - if (interpolationGeogCRS->coordinateSystem()->axisList().size() == 3) { - // We need to force the interpolation CRS, which - // will - // frequently be 3D, to 2D to avoid transformations - // between source CRS and interpolation CRS to have - // 3D terms. - interpolationGeogCRS = - interpolationGeogCRS->demoteTo2D(std::string(), dbContext) - .as_nullable(); - } - } - - return interpolationGeogCRS; -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsCompoundToGeog( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::CompoundCRS *compoundSrc, - const crs::GeographicCRS *geogDst, - std::vector &res) { - - ENTER_FUNCTION(); - - const auto &authFactory = context.context->getAuthorityFactory(); - const auto &componentsSrc = compoundSrc->componentReferenceSystems(); - if (!componentsSrc.empty()) { - - if (componentsSrc.size() == 2) { - auto derivedHSrc = - dynamic_cast(componentsSrc[0].get()); - if (derivedHSrc) { - std::vector intermComponents{ - derivedHSrc->baseCRS(), componentsSrc[1]}; - auto properties = util::PropertyMap().set( - common::IdentifiedObject::NAME_KEY, - intermComponents[0]->nameStr() + " + " + - intermComponents[1]->nameStr()); - auto intermCompound = - crs::CompoundCRS::create(properties, intermComponents); - auto opsFirst = - createOperations(sourceCRS, intermCompound, context); - assert(!opsFirst.empty()); - auto opsLast = - createOperations(intermCompound, targetCRS, context); - for (const auto &opLast : opsLast) { - try { - res.emplace_back( - ConcatenatedOperation::createComputeMetadata( - {opsFirst.front(), opLast}, - disallowEmptyIntersection)); - } catch (const std::exception &) { - } - } - return; - } - } - - std::vector horizTransforms; - auto srcGeogCRS = componentsSrc[0]->extractGeographicCRS(); - if (srcGeogCRS) { - horizTransforms = - createOperations(componentsSrc[0], targetCRS, context); - } - std::vector verticalTransforms; - - const auto dbContext = - authFactory ? authFactory->databaseContext().as_nullable() - : nullptr; - if (componentsSrc.size() >= 2 && - componentsSrc[1]->extractVerticalCRS()) { - - struct SetSkipHorizontalTransform { - Context &context; - - explicit SetSkipHorizontalTransform(Context &contextIn) - : context(contextIn) { - assert(!context.skipHorizontalTransformation); - context.skipHorizontalTransformation = true; - } - - ~SetSkipHorizontalTransform() { - context.skipHorizontalTransformation = false; - } - }; - SetSkipHorizontalTransform setSkipHorizontalTransform(context); - - verticalTransforms = createOperations( - componentsSrc[1], - targetCRS->promoteTo3D(std::string(), dbContext), context); - bool foundRegisteredTransformWithAllGridsAvailable = false; - const auto gridAvailabilityUse = - context.context->getGridAvailabilityUse(); - const bool ignoreMissingGrids = - gridAvailabilityUse == - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY; - for (const auto &op : verticalTransforms) { - if (hasIdentifiers(op) && dbContext) { - bool missingGrid = false; - if (!ignoreMissingGrids) { - const auto gridsNeeded = op->gridsNeeded( - dbContext, - gridAvailabilityUse == - CoordinateOperationContext:: - GridAvailabilityUse::KNOWN_AVAILABLE); - for (const auto &gridDesc : gridsNeeded) { - if (!gridDesc.available) { - missingGrid = true; - break; - } - } - } - if (!missingGrid) { - foundRegisteredTransformWithAllGridsAvailable = true; - break; - } - } - } - if (!foundRegisteredTransformWithAllGridsAvailable && srcGeogCRS && - !srcGeogCRS->_isEquivalentTo( - geogDst, util::IComparable::Criterion::EQUIVALENT) && - !srcGeogCRS->is2DPartOf3D(NN_NO_CHECK(geogDst), dbContext)) { - auto verticalTransformsTmp = createOperations( - componentsSrc[1], - NN_NO_CHECK(srcGeogCRS) - ->promoteTo3D(std::string(), dbContext), - context); - bool foundRegisteredTransform = false; - foundRegisteredTransformWithAllGridsAvailable = false; - for (const auto &op : verticalTransformsTmp) { - if (hasIdentifiers(op) && dbContext) { - bool missingGrid = false; - if (!ignoreMissingGrids) { - const auto gridsNeeded = op->gridsNeeded( - dbContext, - gridAvailabilityUse == - CoordinateOperationContext:: - GridAvailabilityUse::KNOWN_AVAILABLE); - for (const auto &gridDesc : gridsNeeded) { - if (!gridDesc.available) { - missingGrid = true; - break; - } - } - } - foundRegisteredTransform = true; - if (!missingGrid) { - foundRegisteredTransformWithAllGridsAvailable = - true; - break; - } - } - } - if (foundRegisteredTransformWithAllGridsAvailable) { - verticalTransforms = verticalTransformsTmp; - } else if (foundRegisteredTransform) { - verticalTransforms.insert(verticalTransforms.end(), - verticalTransformsTmp.begin(), - verticalTransformsTmp.end()); - } - } - } - - if (horizTransforms.empty() || verticalTransforms.empty()) { - res = horizTransforms; - return; - } - - typedef std::pair, - std::vector> - PairOfTransforms; - std::map - cacheHorizToInterpAndInterpToTarget; - - for (const auto &verticalTransform : verticalTransforms) { -#ifdef TRACE_CREATE_OPERATIONS - ENTER_BLOCK("Considering vertical transform " + - objectAsStr(verticalTransform.get())); -#endif - crs::GeographicCRSPtr interpolationGeogCRS = - getInterpolationGeogCRS(verticalTransform, dbContext); - if (interpolationGeogCRS) { -#ifdef TRACE_CREATE_OPERATIONS - logTrace("Using " + objectAsStr(interpolationGeogCRS.get()) + - " as interpolation CRS"); -#endif - std::vector srcToInterpOps; - std::vector interpToTargetOps; - - std::string key; - const auto &ids = interpolationGeogCRS->identifiers(); - if (!ids.empty()) { - key = - (*ids.front()->codeSpace()) + ':' + ids.front()->code(); - } - - const auto computeOpsToInterp = - [&srcToInterpOps, &interpToTargetOps, &componentsSrc, - &interpolationGeogCRS, &targetCRS, &dbContext, - &context]() { - srcToInterpOps = createOperations( - componentsSrc[0], NN_NO_CHECK(interpolationGeogCRS), - context); - auto target2D = - targetCRS->demoteTo2D(std::string(), dbContext); - if (!componentsSrc[0]->isEquivalentTo( - target2D.get(), - util::IComparable::Criterion::EQUIVALENT)) { - // We do the transformation from the - // interpolationCRS - // to the target one in 3D (see #2225) - // But we don't do that between sourceCRS and - // interpolationCRS, as this would mess with an - // orthometric elevation. - auto interp3D = interpolationGeogCRS->promoteTo3D( - std::string(), dbContext); - interpToTargetOps = - createOperations(interp3D, targetCRS, context); - } - }; - - if (!key.empty()) { - auto iter = cacheHorizToInterpAndInterpToTarget.find(key); - if (iter == cacheHorizToInterpAndInterpToTarget.end()) { -#ifdef TRACE_CREATE_OPERATIONS - ENTER_BLOCK("looking for horizontal transformation " - "from source to interpCRS and interpCRS to " - "target"); -#endif - computeOpsToInterp(); - cacheHorizToInterpAndInterpToTarget[key] = - PairOfTransforms(srcToInterpOps, interpToTargetOps); - } else { - srcToInterpOps = iter->second.first; - interpToTargetOps = iter->second.second; - } - } else { -#ifdef TRACE_CREATE_OPERATIONS - ENTER_BLOCK("looking for horizontal transformation " - "from source to interpCRS and interpCRS to " - "target"); -#endif - computeOpsToInterp(); - } - -#ifdef TRACE_CREATE_OPERATIONS - ENTER_BLOCK("creating HorizVerticalHorizPROJBased operations"); -#endif - for (const auto &srcToInterp : srcToInterpOps) { - if (interpToTargetOps.empty()) { - try { - auto op = createHorizVerticalHorizPROJBased( - sourceCRS, targetCRS, srcToInterp, - verticalTransform, srcToInterp->inverse(), - interpolationGeogCRS, true); - res.emplace_back(op); - } catch (const std::exception &) { - } - } else { - for (const auto &interpToTarget : interpToTargetOps) { - - if (useDifferentTransformationsForSameSourceTarget( - srcToInterp, interpToTarget)) { - continue; - } - - try { - auto op = createHorizVerticalHorizPROJBased( - sourceCRS, targetCRS, srcToInterp, - verticalTransform, interpToTarget, - interpolationGeogCRS, true); - res.emplace_back(op); - } catch (const std::exception &) { - } - } - } - } - } else { - // This case is probably only correct if - // verticalTransform and horizTransform are independent - // and in particular that verticalTransform does not - // involve a grid, because of the rather arbitrary order - // horizontal then vertical applied - for (const auto &horizTransform : horizTransforms) { - try { - auto op = createHorizVerticalPROJBased( - sourceCRS, targetCRS, horizTransform, - verticalTransform, disallowEmptyIntersection); - res.emplace_back(op); - } catch (const std::exception &) { - } - } - } - } - } -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsToGeod( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::GeodeticCRS *geodDst, - std::vector &res) { - - auto cs = cs::EllipsoidalCS::createLatitudeLongitudeEllipsoidalHeight( - common::UnitOfMeasure::DEGREE, common::UnitOfMeasure::METRE); - auto intermGeog3DCRS = - util::nn_static_pointer_cast(crs::GeographicCRS::create( - util::PropertyMap() - .set(common::IdentifiedObject::NAME_KEY, geodDst->nameStr()) - .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - metadata::Extent::WORLD), - geodDst->datum(), geodDst->datumEnsemble(), cs)); - auto sourceToGeog3DOps = - createOperations(sourceCRS, intermGeog3DCRS, context); - auto geog3DToTargetOps = - createOperations(intermGeog3DCRS, targetCRS, context); - if (!geog3DToTargetOps.empty()) { - for (const auto &op : sourceToGeog3DOps) { - auto newOp = op->shallowClone(); - setCRSs(newOp.get(), sourceCRS, intermGeog3DCRS); - try { - res.emplace_back(ConcatenatedOperation::createComputeMetadata( - {newOp, geog3DToTargetOps.front()}, - disallowEmptyIntersection)); - } catch (const InvalidOperationEmptyIntersection &) { - } - } - } -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsCompoundToCompound( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::CompoundCRS *compoundSrc, - const crs::CompoundCRS *compoundDst, - std::vector &res) { - - const auto &componentsSrc = compoundSrc->componentReferenceSystems(); - const auto &componentsDst = compoundDst->componentReferenceSystems(); - if (componentsSrc.empty() || componentsSrc.size() != componentsDst.size()) { - return; - } - const auto srcGeog = componentsSrc[0]->extractGeographicCRS(); - const auto dstGeog = componentsDst[0]->extractGeographicCRS(); - if (srcGeog == nullptr || dstGeog == nullptr) { - return; - } - - std::vector verticalTransforms; - if (componentsSrc.size() >= 2 && componentsSrc[1]->extractVerticalCRS() && - componentsDst[1]->extractVerticalCRS()) { - if (!componentsSrc[1]->_isEquivalentTo(componentsDst[1].get())) { - verticalTransforms = - createOperations(componentsSrc[1], componentsDst[1], context); - } - } - - // If we didn't find a non-ballpark transformation between - // the 2 vertical CRS, then try through intermediate geographic CRS - // For example - // WGS 84 + EGM96 --> ETRS89 + Belfast height where - // there is a geoid model for EGM96 referenced to WGS 84 - // and a geoid model for Belfast height referenced to ETRS89 - if (verticalTransforms.size() == 1 && - verticalTransforms.front()->hasBallparkTransformation()) { - auto dbContext = - context.context->getAuthorityFactory()->databaseContext(); - const auto intermGeogSrc = - srcGeog->promoteTo3D(std::string(), dbContext); - const bool intermGeogSrcIsSameAsIntermGeogDst = - srcGeog->_isEquivalentTo(dstGeog.get()); - const auto intermGeogDst = - intermGeogSrcIsSameAsIntermGeogDst - ? intermGeogSrc - : dstGeog->promoteTo3D(std::string(), dbContext); - const auto opsSrcToGeog = - createOperations(sourceCRS, intermGeogSrc, context); - const auto opsGeogToTarget = - createOperations(intermGeogDst, targetCRS, context); - const bool hasNonTrivalSrcTransf = - !opsSrcToGeog.empty() && - !opsSrcToGeog.front()->hasBallparkTransformation(); - const bool hasNonTrivialTargetTransf = - !opsGeogToTarget.empty() && - !opsGeogToTarget.front()->hasBallparkTransformation(); - if (hasNonTrivalSrcTransf && hasNonTrivialTargetTransf) { - const auto opsGeogSrcToGeogDst = - createOperations(intermGeogSrc, intermGeogDst, context); - for (const auto &op1 : opsSrcToGeog) { - if (op1->hasBallparkTransformation()) { - // std::cerr << "excluded " << op1->nameStr() << std::endl; - continue; - } - for (const auto &op2 : opsGeogSrcToGeogDst) { - for (const auto &op3 : opsGeogToTarget) { - if (op3->hasBallparkTransformation()) { - // std::cerr << "excluded " << op3->nameStr() << - // std::endl; - continue; - } - try { - res.emplace_back( - ConcatenatedOperation::createComputeMetadata( - intermGeogSrcIsSameAsIntermGeogDst - ? std::vector< - CoordinateOperationNNPtr>{op1, - op3} - : std::vector< - CoordinateOperationNNPtr>{op1, - op2, - op3}, - disallowEmptyIntersection)); - } catch (const std::exception &) { - } - } - } - } - } - if (!res.empty()) { - return; - } - } - - for (const auto &verticalTransform : verticalTransforms) { - auto interpolationGeogCRS = NN_NO_CHECK(srcGeog); - auto interpTransformCRS = verticalTransform->interpolationCRS(); - if (interpTransformCRS) { - auto nn_interpTransformCRS = NN_NO_CHECK(interpTransformCRS); - if (dynamic_cast( - nn_interpTransformCRS.get())) { - interpolationGeogCRS = NN_NO_CHECK( - util::nn_dynamic_pointer_cast( - nn_interpTransformCRS)); - } - } else { - auto compSrc0BoundCrs = - dynamic_cast(componentsSrc[0].get()); - auto compDst0BoundCrs = - dynamic_cast(componentsDst[0].get()); - if (compSrc0BoundCrs && compDst0BoundCrs && - dynamic_cast( - compSrc0BoundCrs->hubCRS().get()) && - compSrc0BoundCrs->hubCRS()->_isEquivalentTo( - compDst0BoundCrs->hubCRS().get())) { - interpolationGeogCRS = NN_NO_CHECK( - util::nn_dynamic_pointer_cast( - compSrc0BoundCrs->hubCRS())); - } - } - auto opSrcCRSToGeogCRS = - createOperations(componentsSrc[0], interpolationGeogCRS, context); - auto opGeogCRStoDstCRS = - createOperations(interpolationGeogCRS, componentsDst[0], context); - for (const auto &opSrc : opSrcCRSToGeogCRS) { - for (const auto &opDst : opGeogCRStoDstCRS) { - - try { - auto op = createHorizVerticalHorizPROJBased( - sourceCRS, targetCRS, opSrc, verticalTransform, opDst, - interpolationGeogCRS, true); - res.emplace_back(op); - } catch (const InvalidOperationEmptyIntersection &) { - } catch (const io::FormattingException &) { - } - } - } - } - - if (verticalTransforms.empty()) { - auto resTmp = - createOperations(componentsSrc[0], componentsDst[0], context); - for (const auto &op : resTmp) { - auto opClone = op->shallowClone(); - setCRSs(opClone.get(), sourceCRS, targetCRS); - res.emplace_back(opClone); - } - } -} - -// --------------------------------------------------------------------------- - -void CoordinateOperationFactory::Private::createOperationsBoundToCompound( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - Private::Context &context, const crs::BoundCRS *boundSrc, - const crs::CompoundCRS *compoundDst, - std::vector &res) { - - const auto &authFactory = context.context->getAuthorityFactory(); - const auto dbContext = - authFactory ? authFactory->databaseContext().as_nullable() : nullptr; - - const auto &componentsDst = compoundDst->componentReferenceSystems(); - if (!componentsDst.empty()) { - auto compDst0BoundCrs = - dynamic_cast(componentsDst[0].get()); - if (compDst0BoundCrs) { - auto boundSrcHubAsGeogCRS = - dynamic_cast(boundSrc->hubCRS().get()); - auto compDst0BoundCrsHubAsGeogCRS = - dynamic_cast( - compDst0BoundCrs->hubCRS().get()); - if (boundSrcHubAsGeogCRS && compDst0BoundCrsHubAsGeogCRS) { - const auto boundSrcHubAsGeogCRSDatum = - boundSrcHubAsGeogCRS->datumNonNull(dbContext); - const auto compDst0BoundCrsHubAsGeogCRSDatum = - compDst0BoundCrsHubAsGeogCRS->datumNonNull(dbContext); - if (boundSrcHubAsGeogCRSDatum->_isEquivalentTo( - compDst0BoundCrsHubAsGeogCRSDatum.get())) { - auto cs = cs::EllipsoidalCS:: - createLatitudeLongitudeEllipsoidalHeight( - common::UnitOfMeasure::DEGREE, - common::UnitOfMeasure::METRE); - auto intermGeog3DCRS = util::nn_static_pointer_cast< - crs::CRS>(crs::GeographicCRS::create( - util::PropertyMap() - .set(common::IdentifiedObject::NAME_KEY, - boundSrcHubAsGeogCRS->nameStr()) - .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, - metadata::Extent::WORLD), - boundSrcHubAsGeogCRS->datum(), - boundSrcHubAsGeogCRS->datumEnsemble(), cs)); - auto sourceToGeog3DOps = - createOperations(sourceCRS, intermGeog3DCRS, context); - auto geog3DToTargetOps = - createOperations(intermGeog3DCRS, targetCRS, context); - for (const auto &opSrc : sourceToGeog3DOps) { - for (const auto &opDst : geog3DToTargetOps) { - if (opSrc->targetCRS() && opDst->sourceCRS() && - !opSrc->targetCRS()->_isEquivalentTo( - opDst->sourceCRS().get())) { - // Shouldn't happen normally, but typically - // one of them can be 2D and the other 3D - // due to above createOperations() not - // exactly setting the expected source and - // target CRS. - // So create an adapter operation... - auto intermOps = createOperations( - NN_NO_CHECK(opSrc->targetCRS()), - NN_NO_CHECK(opDst->sourceCRS()), context); - if (!intermOps.empty()) { - res.emplace_back( - ConcatenatedOperation:: - createComputeMetadata( - {opSrc, intermOps.front(), - opDst}, - disallowEmptyIntersection)); - } - } else { - res.emplace_back( - ConcatenatedOperation:: - createComputeMetadata( - {opSrc, opDst}, - disallowEmptyIntersection)); - } - } - } - return; - } - } - } - } - - // There might be better things to do, but for now just ignore the - // transformation of the bound CRS - res = createOperations(boundSrc->baseCRS(), targetCRS, context); -} -//! @endcond - -// --------------------------------------------------------------------------- - -/** \brief Find a list of CoordinateOperation from sourceCRS to targetCRS. - * - * The operations are sorted with the most relevant ones first: by - * descending - * area (intersection of the transformation area with the area of interest, - * or intersection of the transformation with the area of use of the CRS), - * and - * by increasing accuracy. Operations with unknown accuracy are sorted last, - * whatever their area. - * - * When one of the source or target CRS has a vertical component but not the - * other one, the one that has no vertical component is automatically promoted - * to a 3D version, where its vertical axis is the ellipsoidal height in metres, - * using the ellipsoid of the base geodetic CRS. - * - * @param sourceCRS source CRS. - * @param targetCRS target CRS. - * @param context Search context. - * @return a list - */ -std::vector -CoordinateOperationFactory::createOperations( - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - const CoordinateOperationContextNNPtr &context) const { - -#ifdef TRACE_CREATE_OPERATIONS - ENTER_FUNCTION(); -#endif - // Look if we are called on CRS that have a link to a 'canonical' - // BoundCRS - // If so, use that one as input - const auto &srcBoundCRS = sourceCRS->canonicalBoundCRS(); - const auto &targetBoundCRS = targetCRS->canonicalBoundCRS(); - auto l_sourceCRS = srcBoundCRS ? NN_NO_CHECK(srcBoundCRS) : sourceCRS; - auto l_targetCRS = targetBoundCRS ? NN_NO_CHECK(targetBoundCRS) : targetCRS; - const auto &authFactory = context->getAuthorityFactory(); - - metadata::ExtentPtr sourceCRSExtent; - auto l_resolvedSourceCRS = - crs::CRS::getResolvedCRS(l_sourceCRS, authFactory, sourceCRSExtent); - metadata::ExtentPtr targetCRSExtent; - auto l_resolvedTargetCRS = - crs::CRS::getResolvedCRS(l_targetCRS, authFactory, targetCRSExtent); - Private::Context contextPrivate(sourceCRSExtent, targetCRSExtent, context); - - if (context->getSourceAndTargetCRSExtentUse() == - CoordinateOperationContext::SourceTargetCRSExtentUse::INTERSECTION) { - if (sourceCRSExtent && targetCRSExtent && - !sourceCRSExtent->intersects(NN_NO_CHECK(targetCRSExtent))) { - return std::vector(); - } - } - - return filterAndSort(Private::createOperations(l_resolvedSourceCRS, - l_resolvedTargetCRS, - contextPrivate), - context, sourceCRSExtent, targetCRSExtent); -} - -// --------------------------------------------------------------------------- - -/** \brief Instantiate a CoordinateOperationFactory. - */ -CoordinateOperationFactoryNNPtr CoordinateOperationFactory::create() { - return NN_NO_CHECK( - CoordinateOperationFactory::make_unique()); -} - -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -InverseCoordinateOperation::~InverseCoordinateOperation() = default; - -// --------------------------------------------------------------------------- - -InverseCoordinateOperation::InverseCoordinateOperation( - const CoordinateOperationNNPtr &forwardOperationIn, - bool wktSupportsInversion) - : forwardOperation_(forwardOperationIn), - wktSupportsInversion_(wktSupportsInversion) {} - -// --------------------------------------------------------------------------- - -void InverseCoordinateOperation::setPropertiesFromForward() { - setProperties( - createPropertiesForInverse(forwardOperation_.get(), false, false)); - setAccuracies(forwardOperation_->coordinateOperationAccuracies()); - if (forwardOperation_->sourceCRS() && forwardOperation_->targetCRS()) { - setCRSs(forwardOperation_.get(), true); - } - setHasBallparkTransformation( - forwardOperation_->hasBallparkTransformation()); -} - -// --------------------------------------------------------------------------- - -CoordinateOperationNNPtr InverseCoordinateOperation::inverse() const { - return forwardOperation_; -} - -// --------------------------------------------------------------------------- - -void InverseCoordinateOperation::_exportToPROJString( - io::PROJStringFormatter *formatter) const { - formatter->startInversion(); - forwardOperation_->_exportToPROJString(formatter); - formatter->stopInversion(); -} - -// --------------------------------------------------------------------------- - -bool InverseCoordinateOperation::_isEquivalentTo( - const util::IComparable *other, util::IComparable::Criterion criterion, - const io::DatabaseContextPtr &dbContext) const { - auto otherICO = dynamic_cast(other); - if (otherICO == nullptr || - !ObjectUsage::_isEquivalentTo(other, criterion, dbContext)) { - return false; - } - return inverse()->_isEquivalentTo(otherICO->inverse().get(), criterion, - dbContext); -} - -// --------------------------------------------------------------------------- - -PROJBasedOperation::~PROJBasedOperation() = default; - -// --------------------------------------------------------------------------- - -PROJBasedOperation::PROJBasedOperation(const OperationMethodNNPtr &methodIn) - : SingleOperation(methodIn) {} - -// --------------------------------------------------------------------------- - -PROJBasedOperationNNPtr PROJBasedOperation::create( - const util::PropertyMap &properties, const std::string &PROJString, - const crs::CRSPtr &sourceCRS, const crs::CRSPtr &targetCRS, - const std::vector &accuracies) { - auto method = OperationMethod::create( - util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, - "PROJ-based operation method: " + PROJString), - std::vector{}); - auto op = PROJBasedOperation::nn_make_shared(method); - op->assignSelf(op); - op->projString_ = PROJString; - if (sourceCRS && targetCRS) { - op->setCRSs(NN_NO_CHECK(sourceCRS), NN_NO_CHECK(targetCRS), nullptr); - } - op->setProperties( - addDefaultNameIfNeeded(properties, "PROJ-based coordinate operation")); - op->setAccuracies(accuracies); - return op; -} - -// --------------------------------------------------------------------------- - -PROJBasedOperationNNPtr PROJBasedOperation::create( - const util::PropertyMap &properties, - const io::IPROJStringExportableNNPtr &projExportable, bool inverse, - const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, - const crs::CRSPtr &interpolationCRS, - const std::vector &accuracies, - bool hasBallparkTransformation) { - - auto formatter = io::PROJStringFormatter::create(); - if (inverse) { - formatter->startInversion(); - } - projExportable->_exportToPROJString(formatter.get()); - if (inverse) { - formatter->stopInversion(); - } - auto projString = formatter->toString(); - - auto method = OperationMethod::create( - util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, - "PROJ-based operation method (approximate): " + - projString), - std::vector{}); - auto op = PROJBasedOperation::nn_make_shared(method); - op->assignSelf(op); - op->projString_ = projString; - op->setCRSs(sourceCRS, targetCRS, interpolationCRS); - op->setProperties( - addDefaultNameIfNeeded(properties, "PROJ-based coordinate operation")); - op->setAccuracies(accuracies); - op->projStringExportable_ = projExportable.as_nullable(); - op->inverse_ = inverse; - op->setHasBallparkTransformation(hasBallparkTransformation); - return op; -} - -// --------------------------------------------------------------------------- - -CoordinateOperationNNPtr PROJBasedOperation::inverse() const { - - if (projStringExportable_ && sourceCRS() && targetCRS()) { - return util::nn_static_pointer_cast( - PROJBasedOperation::create( - createPropertiesForInverse(this, false, false), - NN_NO_CHECK(projStringExportable_), !inverse_, - NN_NO_CHECK(targetCRS()), NN_NO_CHECK(sourceCRS()), - interpolationCRS(), coordinateOperationAccuracies(), - hasBallparkTransformation())); - } - - auto formatter = io::PROJStringFormatter::create(); - formatter->startInversion(); - try { - formatter->ingestPROJString(projString_); - } catch (const io::ParsingException &e) { - throw util::UnsupportedOperationException( - std::string("PROJBasedOperation::inverse() failed: ") + e.what()); - } - formatter->stopInversion(); - - auto op = PROJBasedOperation::create( - createPropertiesForInverse(this, false, false), formatter->toString(), - targetCRS(), sourceCRS(), coordinateOperationAccuracies()); - if (sourceCRS() && targetCRS()) { - op->setCRSs(NN_NO_CHECK(targetCRS()), NN_NO_CHECK(sourceCRS()), - interpolationCRS()); - } - op->setHasBallparkTransformation(hasBallparkTransformation()); - return util::nn_static_pointer_cast(op); -} - -// --------------------------------------------------------------------------- - -void PROJBasedOperation::_exportToWKT(io::WKTFormatter *formatter) const { - - if (sourceCRS() && targetCRS()) { - exportTransformationToWKT(formatter); - return; - } - - const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; - if (!isWKT2) { - throw io::FormattingException( - "PROJBasedOperation can only be exported to WKT2"); - } - - formatter->startNode(io::WKTConstants::CONVERSION, false); - formatter->addQuotedString(nameStr()); - method()->_exportToWKT(formatter); - - for (const auto ¶mValue : parameterValues()) { - paramValue->_exportToWKT(formatter); - } - formatter->endNode(); -} - -// --------------------------------------------------------------------------- - -void PROJBasedOperation::_exportToJSON( - io::JSONFormatter *formatter) const // throw(FormattingException) -{ - auto writer = formatter->writer(); - auto objectContext(formatter->MakeObjectContext( - (sourceCRS() && targetCRS()) ? "Transformation" : "Conversion", - !identifiers().empty())); - - writer->AddObjKey("name"); - auto l_name = nameStr(); - if (l_name.empty()) { - writer->Add("unnamed"); - } else { - writer->Add(l_name); - } - - if (sourceCRS() && targetCRS()) { - writer->AddObjKey("source_crs"); - formatter->setAllowIDInImmediateChild(); - sourceCRS()->_exportToJSON(formatter); - - writer->AddObjKey("target_crs"); - formatter->setAllowIDInImmediateChild(); - targetCRS()->_exportToJSON(formatter); - } - - writer->AddObjKey("method"); - formatter->setOmitTypeInImmediateChild(); - formatter->setAllowIDInImmediateChild(); - method()->_exportToJSON(formatter); - - const auto &l_parameterValues = parameterValues(); - if (!l_parameterValues.empty()) { - writer->AddObjKey("parameters"); - { - auto parametersContext(writer->MakeArrayContext(false)); - for (const auto &genOpParamvalue : l_parameterValues) { - formatter->setAllowIDInImmediateChild(); - formatter->setOmitTypeInImmediateChild(); - genOpParamvalue->_exportToJSON(formatter); - } - } - } -} - -// --------------------------------------------------------------------------- - -void PROJBasedOperation::_exportToPROJString( - io::PROJStringFormatter *formatter) const { - if (projStringExportable_) { - if (inverse_) { - formatter->startInversion(); - } - projStringExportable_->_exportToPROJString(formatter); - if (inverse_) { - formatter->stopInversion(); - } - return; - } - - try { - formatter->ingestPROJString(projString_); - } catch (const io::ParsingException &e) { - throw io::FormattingException( - std::string("PROJBasedOperation::exportToPROJString() failed: ") + - e.what()); - } -} - -// --------------------------------------------------------------------------- - -CoordinateOperationNNPtr PROJBasedOperation::_shallowClone() const { - auto op = PROJBasedOperation::nn_make_shared(*this); - op->assignSelf(op); - op->setCRSs(this, false); - return util::nn_static_pointer_cast(op); -} - -// --------------------------------------------------------------------------- - -std::set -PROJBasedOperation::gridsNeeded(const io::DatabaseContextPtr &databaseContext, - bool considerKnownGridsAsAvailable) const { - std::set res; - - try { - auto formatterOut = io::PROJStringFormatter::create(); - auto formatter = io::PROJStringFormatter::create(); - formatter->ingestPROJString(exportToPROJString(formatterOut.get())); - const auto usedGridNames = formatter->getUsedGridNames(); - for (const auto &shortName : usedGridNames) { - GridDescription desc; - desc.shortName = shortName; - if (databaseContext) { - databaseContext->lookForGridInfo( - desc.shortName, considerKnownGridsAsAvailable, - desc.fullName, desc.packageName, desc.url, - desc.directDownload, desc.openLicense, desc.available); - } - res.insert(desc); - } - } catch (const io::ParsingException &) { - } - - return res; -} - -//! @endcond - -// --------------------------------------------------------------------------- - -} // namespace operation - -namespace crs { -// --------------------------------------------------------------------------- - -//! @cond Doxygen_Suppress - -crs::CRSNNPtr CRS::getResolvedCRS(const crs::CRSNNPtr &crs, - const io::AuthorityFactoryPtr &authFactory, - metadata::ExtentPtr &extentOut) { - const auto &ids = crs->identifiers(); - const auto &name = crs->nameStr(); - - bool approxExtent; - extentOut = getExtentPossiblySynthetized(crs, approxExtent); - - // We try to "identify" the provided CRS with the ones of the database, - // but in a more restricted way that what identify() does. - // If we get a match from id in priority, and from name as a fallback, and - // that they are equivalent to the input CRS, then use the identified CRS. - // Even if they aren't equivalent, we update extentOut with the one of the - // identified CRS if our input one is absent/not reliable. - - const auto tryToIdentifyByName = [&crs, &name, &authFactory, approxExtent, - &extentOut]( - io::AuthorityFactory::ObjectType objectType) { - if (name != "unknown" && name != "unnamed") { - auto matches = authFactory->createObjectsFromName( - name, {objectType}, false, 2); - if (matches.size() == 1) { - const auto match = - util::nn_static_pointer_cast(matches.front()); - if (approxExtent || !extentOut) { - extentOut = getExtent(match); - } - if (match->isEquivalentTo( - crs.get(), util::IComparable::Criterion::EQUIVALENT)) { - return match; - } - } - } - return crs; - }; - - auto geogCRS = dynamic_cast(crs.get()); - if (geogCRS && authFactory) { - if (!ids.empty()) { - const auto tmpAuthFactory = io::AuthorityFactory::create( - authFactory->databaseContext(), *ids.front()->codeSpace()); - try { - auto resolvedCrs( - tmpAuthFactory->createGeographicCRS(ids.front()->code())); - if (approxExtent || !extentOut) { - extentOut = getExtent(resolvedCrs); - } - if (resolvedCrs->isEquivalentTo( - crs.get(), util::IComparable::Criterion::EQUIVALENT)) { - return util::nn_static_pointer_cast(resolvedCrs); - } - } catch (const std::exception &) { - } - } else { - return tryToIdentifyByName( - geogCRS->coordinateSystem()->axisList().size() == 2 - ? io::AuthorityFactory::ObjectType::GEOGRAPHIC_2D_CRS - : io::AuthorityFactory::ObjectType::GEOGRAPHIC_3D_CRS); - } - } - - auto projectedCrs = dynamic_cast(crs.get()); - if (projectedCrs && authFactory) { - if (!ids.empty()) { - const auto tmpAuthFactory = io::AuthorityFactory::create( - authFactory->databaseContext(), *ids.front()->codeSpace()); - try { - auto resolvedCrs( - tmpAuthFactory->createProjectedCRS(ids.front()->code())); - if (approxExtent || !extentOut) { - extentOut = getExtent(resolvedCrs); - } - if (resolvedCrs->isEquivalentTo( - crs.get(), util::IComparable::Criterion::EQUIVALENT)) { - return util::nn_static_pointer_cast(resolvedCrs); - } - } catch (const std::exception &) { - } - } else { - return tryToIdentifyByName( - io::AuthorityFactory::ObjectType::PROJECTED_CRS); - } - } - - auto compoundCrs = dynamic_cast(crs.get()); - if (compoundCrs && authFactory) { - if (!ids.empty()) { - const auto tmpAuthFactory = io::AuthorityFactory::create( - authFactory->databaseContext(), *ids.front()->codeSpace()); - try { - auto resolvedCrs( - tmpAuthFactory->createCompoundCRS(ids.front()->code())); - if (approxExtent || !extentOut) { - extentOut = getExtent(resolvedCrs); - } - if (resolvedCrs->isEquivalentTo( - crs.get(), util::IComparable::Criterion::EQUIVALENT)) { - return util::nn_static_pointer_cast(resolvedCrs); - } - } catch (const std::exception &) { - } - } else { - auto outCrs = tryToIdentifyByName( - io::AuthorityFactory::ObjectType::COMPOUND_CRS); - const auto &components = compoundCrs->componentReferenceSystems(); - if (outCrs.get() != crs.get()) { - bool hasGeoid = false; - if (components.size() == 2) { - auto vertCRS = - dynamic_cast(components[1].get()); - if (vertCRS && !vertCRS->geoidModel().empty()) { - hasGeoid = true; - } - } - if (!hasGeoid) { - return outCrs; - } - } - if (approxExtent || !extentOut) { - // If we still did not get a reliable extent, then try to - // resolve the components of the compoundCRS, and take the - // intersection of their extent. - extentOut = metadata::ExtentPtr(); - for (const auto &component : components) { - metadata::ExtentPtr componentExtent; - getResolvedCRS(component, authFactory, componentExtent); - if (extentOut && componentExtent) - extentOut = extentOut->intersection( - NN_NO_CHECK(componentExtent)); - else if (componentExtent) - extentOut = componentExtent; - } - } - } - } - return crs; -} - -//! @endcond - -} // namespace crs -NS_PROJ_END diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp index fdb11c6203..a6ed2e2264 100644 --- a/src/iso19111/factory.cpp +++ b/src/iso19111/factory.cpp @@ -39,12 +39,14 @@ #include "proj/metadata.hpp" #include "proj/util.hpp" -#include "proj/internal/coordinateoperation_internal.hpp" #include "proj/internal/internal.hpp" #include "proj/internal/io_internal.hpp" #include "proj/internal/lru_cache.hpp" #include "proj/internal/tracing.hpp" +#include "operation/coordinateoperation_internal.hpp" +#include "operation/parammappings.hpp" + #include "sqlite3_utils.hpp" #include diff --git a/src/iso19111/io.cpp b/src/iso19111/io.cpp index 9876417ba5..800ec95a3e 100644 --- a/src/iso19111/io.cpp +++ b/src/iso19111/io.cpp @@ -53,7 +53,11 @@ #include "proj/metadata.hpp" #include "proj/util.hpp" -#include "proj/internal/coordinateoperation_internal.hpp" +#include "operation/coordinateoperation_internal.hpp" +#include "operation/esriparammappings.hpp" +#include "operation/oputils.hpp" +#include "operation/parammappings.hpp" + #include "proj/internal/coordinatesystem_internal.hpp" #include "proj/internal/internal.hpp" #include "proj/internal/io_internal.hpp" diff --git a/src/iso19111/operation/concatenatedoperation.cpp b/src/iso19111/operation/concatenatedoperation.cpp new file mode 100644 index 0000000000..1c65a24b0a --- /dev/null +++ b/src/iso19111/operation/concatenatedoperation.cpp @@ -0,0 +1,675 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP +#endif + +#include "proj/common.hpp" +#include "proj/coordinateoperation.hpp" +#include "proj/crs.hpp" +#include "proj/io.hpp" +#include "proj/metadata.hpp" +#include "proj/util.hpp" + +#include "proj/internal/internal.hpp" +#include "proj/internal/io_internal.hpp" + +#include "coordinateoperation_internal.hpp" +#include "oputils.hpp" + +// PROJ include order is sensitive +// clang-format off +#include "proj.h" +#include "proj_internal.h" // M_PI +// clang-format on + +#include "proj_json_streaming_writer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace NS_PROJ::internal; + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct ConcatenatedOperation::Private { + std::vector operations_{}; + bool computedName_ = false; + + explicit Private(const std::vector &operationsIn) + : operations_(operationsIn) {} + Private(const Private &) = default; +}; +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +ConcatenatedOperation::~ConcatenatedOperation() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +ConcatenatedOperation::ConcatenatedOperation(const ConcatenatedOperation &other) + : CoordinateOperation(other), + d(internal::make_unique(*(other.d))) {} +//! @endcond + +// --------------------------------------------------------------------------- + +ConcatenatedOperation::ConcatenatedOperation( + const std::vector &operationsIn) + : CoordinateOperation(), d(internal::make_unique(operationsIn)) {} + +// --------------------------------------------------------------------------- + +/** \brief Return the operation steps of the concatenated operation. + * + * @return the operation steps. + */ +const std::vector & +ConcatenatedOperation::operations() const { + return d->operations_; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static bool compareStepCRS(const crs::CRS *a, const crs::CRS *b) { + const auto &aIds = a->identifiers(); + const auto &bIds = b->identifiers(); + if (aIds.size() == 1 && bIds.size() == 1 && + aIds[0]->code() == bIds[0]->code() && + *aIds[0]->codeSpace() == *bIds[0]->codeSpace()) { + return true; + } + return a->_isEquivalentTo(b, util::IComparable::Criterion::EQUIVALENT); +} +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a ConcatenatedOperation + * + * @param properties See \ref general_properties. At minimum the name should + * be + * defined. + * @param operationsIn Vector of the CoordinateOperation steps. + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + * @throws InvalidOperation + */ +ConcatenatedOperationNNPtr ConcatenatedOperation::create( + const util::PropertyMap &properties, + const std::vector &operationsIn, + const std::vector + &accuracies) // throw InvalidOperation +{ + if (operationsIn.size() < 2) { + throw InvalidOperation( + "ConcatenatedOperation must have at least 2 operations"); + } + crs::CRSPtr lastTargetCRS; + + crs::CRSPtr interpolationCRS; + bool interpolationCRSValid = true; + for (size_t i = 0; i < operationsIn.size(); i++) { + auto l_sourceCRS = operationsIn[i]->sourceCRS(); + auto l_targetCRS = operationsIn[i]->targetCRS(); + + if (interpolationCRSValid) { + auto subOpInterpCRS = operationsIn[i]->interpolationCRS(); + if (interpolationCRS == nullptr) + interpolationCRS = subOpInterpCRS; + else if (subOpInterpCRS == nullptr || + !(subOpInterpCRS->isEquivalentTo( + interpolationCRS.get(), + util::IComparable::Criterion::EQUIVALENT))) { + interpolationCRS = nullptr; + interpolationCRSValid = false; + } + } + + if (l_sourceCRS == nullptr || l_targetCRS == nullptr) { + throw InvalidOperation("At least one of the operation lacks a " + "source and/or target CRS"); + } + if (i >= 1) { + if (!compareStepCRS(l_sourceCRS.get(), lastTargetCRS.get())) { +#ifdef DEBUG_CONCATENATED_OPERATION + std::cerr << "Step " << i - 1 << ": " + << operationsIn[i - 1]->nameStr() << std::endl; + std::cerr << "Step " << i << ": " << operationsIn[i]->nameStr() + << std::endl; + { + auto f(io::WKTFormatter::create( + io::WKTFormatter::Convention::WKT2_2019)); + std::cerr << "Source CRS of step " << i << ":" << std::endl; + std::cerr << l_sourceCRS->exportToWKT(f.get()) << std::endl; + } + { + auto f(io::WKTFormatter::create( + io::WKTFormatter::Convention::WKT2_2019)); + std::cerr << "Target CRS of step " << i - 1 << ":" + << std::endl; + std::cerr << lastTargetCRS->exportToWKT(f.get()) + << std::endl; + } +#endif + throw InvalidOperation( + "Inconsistent chaining of CRS in operations"); + } + } + lastTargetCRS = l_targetCRS; + } + auto op = ConcatenatedOperation::nn_make_shared( + operationsIn); + op->assignSelf(op); + op->setProperties(properties); + op->setCRSs(NN_NO_CHECK(operationsIn[0]->sourceCRS()), + NN_NO_CHECK(operationsIn.back()->targetCRS()), + interpolationCRS); + op->setAccuracies(accuracies); +#ifdef DEBUG_CONCATENATED_OPERATION + { + auto f( + io::WKTFormatter::create(io::WKTFormatter::Convention::WKT2_2019)); + std::cerr << "ConcatenatedOperation::create()" << std::endl; + std::cerr << op->exportToWKT(f.get()) << std::endl; + } +#endif + return op; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +// --------------------------------------------------------------------------- + +void ConcatenatedOperation::fixStepsDirection( + const crs::CRSNNPtr &concatOpSourceCRS, + const crs::CRSNNPtr &concatOpTargetCRS, + std::vector &operationsInOut) { + + // Set of heuristics to assign CRS to steps, and possibly reverse them. + + const auto isGeographic = [](const crs::CRS *crs) -> bool { + return dynamic_cast(crs) != nullptr; + }; + + const auto isGeocentric = [](const crs::CRS *crs) -> bool { + auto geodCRS = dynamic_cast(crs); + if (geodCRS && geodCRS->coordinateSystem()->axisList().size() == 3) + return true; + return false; + }; + + for (size_t i = 0; i < operationsInOut.size(); ++i) { + auto &op = operationsInOut[i]; + auto l_sourceCRS = op->sourceCRS(); + auto l_targetCRS = op->targetCRS(); + auto conv = dynamic_cast(op.get()); + if (conv && i == 0 && !l_sourceCRS && !l_targetCRS) { + auto derivedCRS = + dynamic_cast(concatOpSourceCRS.get()); + if (derivedCRS) { + if (i + 1 < operationsInOut.size()) { + // use the sourceCRS of the next operation as our target CRS + l_targetCRS = operationsInOut[i + 1]->sourceCRS(); + // except if it looks like the next operation should + // actually be reversed !!! + if (l_targetCRS && + !compareStepCRS(l_targetCRS.get(), + derivedCRS->baseCRS().get()) && + operationsInOut[i + 1]->targetCRS() && + compareStepCRS( + operationsInOut[i + 1]->targetCRS().get(), + derivedCRS->baseCRS().get())) { + l_targetCRS = operationsInOut[i + 1]->targetCRS(); + } + } + if (!l_targetCRS) { + l_targetCRS = derivedCRS->baseCRS().as_nullable(); + } + auto invConv = + util::nn_dynamic_pointer_cast(op); + auto nn_targetCRS = NN_NO_CHECK(l_targetCRS); + if (invConv) { + invConv->inverse()->setCRSs(nn_targetCRS, concatOpSourceCRS, + nullptr); + op->setCRSs(concatOpSourceCRS, nn_targetCRS, nullptr); + } else { + op->setCRSs(nn_targetCRS, concatOpSourceCRS, nullptr); + op = op->inverse(); + } + } else if (i + 1 < operationsInOut.size()) { + /* coverity[copy_paste_error] */ + l_targetCRS = operationsInOut[i + 1]->sourceCRS(); + if (l_targetCRS) { + op->setCRSs(concatOpSourceCRS, NN_NO_CHECK(l_targetCRS), + nullptr); + } + } + } else if (conv && i + 1 == operationsInOut.size() && !l_sourceCRS && + !l_targetCRS) { + auto derivedCRS = + dynamic_cast(concatOpTargetCRS.get()); + if (derivedCRS) { + if (i >= 1) { + // use the sourceCRS of the previous operation as our source + // CRS + l_sourceCRS = operationsInOut[i - 1]->targetCRS(); + // except if it looks like the previous operation should + // actually be reversed !!! + if (l_sourceCRS && + !compareStepCRS(l_sourceCRS.get(), + derivedCRS->baseCRS().get()) && + operationsInOut[i - 1]->sourceCRS() && + compareStepCRS( + operationsInOut[i - 1]->sourceCRS().get(), + derivedCRS->baseCRS().get())) { + l_targetCRS = operationsInOut[i - 1]->sourceCRS(); + } + } + if (!l_sourceCRS) { + l_sourceCRS = derivedCRS->baseCRS().as_nullable(); + } + op->setCRSs(NN_NO_CHECK(l_sourceCRS), concatOpTargetCRS, + nullptr); + } else if (i >= 1) { + l_sourceCRS = operationsInOut[i - 1]->targetCRS(); + if (l_sourceCRS) { + derivedCRS = dynamic_cast( + l_sourceCRS.get()); + if (conv->isEquivalentTo( + derivedCRS->derivingConversion().get(), + util::IComparable::Criterion::EQUIVALENT)) { + op->setCRSs(concatOpTargetCRS, NN_NO_CHECK(l_sourceCRS), + nullptr); + op = op->inverse(); + } + op->setCRSs(NN_NO_CHECK(l_sourceCRS), concatOpTargetCRS, + nullptr); + } + } + } else if (conv && i > 0 && i < operationsInOut.size() - 1) { + // For an intermediate conversion, use the target CRS of the + // previous step and the source CRS of the next step + l_sourceCRS = operationsInOut[i - 1]->targetCRS(); + l_targetCRS = operationsInOut[i + 1]->sourceCRS(); + if (l_sourceCRS && l_targetCRS) { + op->setCRSs(NN_NO_CHECK(l_sourceCRS), NN_NO_CHECK(l_targetCRS), + nullptr); + } + } else if (!conv && l_sourceCRS && l_targetCRS) { + + // Transformations might be mentioned in their forward directions, + // whereas we should instead use the reverse path. + auto prevOpTarget = (i == 0) ? concatOpSourceCRS.as_nullable() + : operationsInOut[i - 1]->targetCRS(); + if (compareStepCRS(l_sourceCRS.get(), prevOpTarget.get())) { + // do nothing + } else if (compareStepCRS(l_targetCRS.get(), prevOpTarget.get())) { + op = op->inverse(); + } + // Below is needed for EPSG:9103 which chains NAD83(2011) geographic + // 2D with NAD83(2011) geocentric + else if (l_sourceCRS->nameStr() == prevOpTarget->nameStr() && + ((isGeographic(l_sourceCRS.get()) && + isGeocentric(prevOpTarget.get())) || + (isGeocentric(l_sourceCRS.get()) && + isGeographic(prevOpTarget.get())))) { + auto newOp(Conversion::createGeographicGeocentric( + NN_NO_CHECK(prevOpTarget), NN_NO_CHECK(l_sourceCRS))); + operationsInOut.insert(operationsInOut.begin() + i, newOp); + } else if (l_targetCRS->nameStr() == prevOpTarget->nameStr() && + ((isGeographic(l_targetCRS.get()) && + isGeocentric(prevOpTarget.get())) || + (isGeocentric(l_targetCRS.get()) && + isGeographic(prevOpTarget.get())))) { + auto newOp(Conversion::createGeographicGeocentric( + NN_NO_CHECK(prevOpTarget), NN_NO_CHECK(l_targetCRS))); + operationsInOut.insert(operationsInOut.begin() + i, newOp); + } + } + } + + if (!operationsInOut.empty()) { + auto l_sourceCRS = operationsInOut.front()->sourceCRS(); + if (l_sourceCRS && + !compareStepCRS(l_sourceCRS.get(), concatOpSourceCRS.get())) { + throw InvalidOperation("The source CRS of the first step of " + "concatenated operation is not the same " + "as the source CRS of the concatenated " + "operation itself"); + } + + auto l_targetCRS = operationsInOut.back()->targetCRS(); + if (l_targetCRS && + !compareStepCRS(l_targetCRS.get(), concatOpTargetCRS.get())) { + if (l_targetCRS->nameStr() == concatOpTargetCRS->nameStr() && + ((isGeographic(l_targetCRS.get()) && + isGeocentric(concatOpTargetCRS.get())) || + (isGeocentric(l_targetCRS.get()) && + isGeographic(concatOpTargetCRS.get())))) { + auto newOp(Conversion::createGeographicGeocentric( + NN_NO_CHECK(l_targetCRS), concatOpTargetCRS)); + operationsInOut.push_back(newOp); + } else { + throw InvalidOperation("The target CRS of the last step of " + "concatenated operation is not the same " + "as the target CRS of the concatenated " + "operation itself"); + } + } + } +} +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a ConcatenatedOperation, or return a single + * coordinate + * operation. + * + * This computes its accuracy from the sum of its member operations, its + * extent + * + * @param operationsIn Vector of the CoordinateOperation steps. + * @param checkExtent Whether we should check the non-emptyness of the + * intersection + * of the extents of the operations + * @throws InvalidOperation + */ +CoordinateOperationNNPtr ConcatenatedOperation::createComputeMetadata( + const std::vector &operationsIn, + bool checkExtent) // throw InvalidOperation +{ + util::PropertyMap properties; + + if (operationsIn.size() == 1) { + return operationsIn[0]; + } + + std::vector flattenOps; + bool hasBallparkTransformation = false; + for (const auto &subOp : operationsIn) { + hasBallparkTransformation |= subOp->hasBallparkTransformation(); + auto subOpConcat = + dynamic_cast(subOp.get()); + if (subOpConcat) { + auto subOps = subOpConcat->operations(); + for (const auto &subSubOp : subOps) { + flattenOps.emplace_back(subSubOp); + } + } else { + flattenOps.emplace_back(subOp); + } + } + if (flattenOps.size() == 1) { + return flattenOps[0]; + } + + properties.set(common::IdentifiedObject::NAME_KEY, + computeConcatenatedName(flattenOps)); + + bool emptyIntersection = false; + auto extent = getExtent(flattenOps, false, emptyIntersection); + if (checkExtent && emptyIntersection) { + std::string msg( + "empty intersection of area of validity of concatenated " + "operations"); + throw InvalidOperationEmptyIntersection(msg); + } + if (extent) { + properties.set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + NN_NO_CHECK(extent)); + } + + std::vector accuracies; + const double accuracy = getAccuracy(flattenOps); + if (accuracy >= 0.0) { + accuracies.emplace_back( + metadata::PositionalAccuracy::create(toString(accuracy))); + } + + auto op = create(properties, flattenOps, accuracies); + op->setHasBallparkTransformation(hasBallparkTransformation); + op->d->computedName_ = true; + return op; +} + +// --------------------------------------------------------------------------- + +CoordinateOperationNNPtr ConcatenatedOperation::inverse() const { + std::vector inversedOperations; + auto l_operations = operations(); + inversedOperations.reserve(l_operations.size()); + for (const auto &operation : l_operations) { + inversedOperations.emplace_back(operation->inverse()); + } + std::reverse(inversedOperations.begin(), inversedOperations.end()); + + auto properties = createPropertiesForInverse(this, false, false); + if (d->computedName_) { + properties.set(common::IdentifiedObject::NAME_KEY, + computeConcatenatedName(inversedOperations)); + } + + auto op = + create(properties, inversedOperations, coordinateOperationAccuracies()); + op->d->computedName_ = d->computedName_; + op->setHasBallparkTransformation(hasBallparkTransformation()); + return op; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void ConcatenatedOperation::_exportToWKT(io::WKTFormatter *formatter) const { + const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; + if (!isWKT2 || !formatter->use2019Keywords()) { + throw io::FormattingException( + "Transformation can only be exported to WKT2:2019"); + } + + formatter->startNode(io::WKTConstants::CONCATENATEDOPERATION, + !identifiers().empty()); + formatter->addQuotedString(nameStr()); + + if (formatter->use2019Keywords()) { + const auto &version = operationVersion(); + if (version.has_value()) { + formatter->startNode(io::WKTConstants::VERSION, false); + formatter->addQuotedString(*version); + formatter->endNode(); + } + } + + exportSourceCRSAndTargetCRSToWKT(this, formatter); + + const bool canExportOperationId = + !(formatter->idOnTopLevelOnly() && formatter->topLevelHasId()); + + const bool hasDomains = !domains().empty(); + if (hasDomains) { + formatter->pushDisableUsage(); + } + + for (const auto &operation : operations()) { + formatter->startNode(io::WKTConstants::STEP, false); + if (canExportOperationId && !operation->identifiers().empty()) { + // fake that top node has no id, so that the operation id is + // considered + formatter->pushHasId(false); + operation->_exportToWKT(formatter); + formatter->popHasId(); + } else { + operation->_exportToWKT(formatter); + } + formatter->endNode(); + } + + if (hasDomains) { + formatter->popDisableUsage(); + } + + ObjectUsage::baseExportToWKT(formatter); + formatter->endNode(); +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void ConcatenatedOperation::_exportToJSON( + io::JSONFormatter *formatter) const // throw(FormattingException) +{ + auto writer = formatter->writer(); + auto objectContext(formatter->MakeObjectContext("ConcatenatedOperation", + !identifiers().empty())); + + writer->AddObjKey("name"); + auto l_name = nameStr(); + if (l_name.empty()) { + writer->Add("unnamed"); + } else { + writer->Add(l_name); + } + + writer->AddObjKey("source_crs"); + formatter->setAllowIDInImmediateChild(); + sourceCRS()->_exportToJSON(formatter); + + writer->AddObjKey("target_crs"); + formatter->setAllowIDInImmediateChild(); + targetCRS()->_exportToJSON(formatter); + + writer->AddObjKey("steps"); + { + auto parametersContext(writer->MakeArrayContext(false)); + for (const auto &operation : operations()) { + formatter->setAllowIDInImmediateChild(); + operation->_exportToJSON(formatter); + } + } + + ObjectUsage::baseExportToJSON(formatter); +} + +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +CoordinateOperationNNPtr ConcatenatedOperation::_shallowClone() const { + auto op = + ConcatenatedOperation::nn_make_shared(*this); + std::vector ops; + for (const auto &subOp : d->operations_) { + ops.emplace_back(subOp->shallowClone()); + } + op->d->operations_ = ops; + op->assignSelf(op); + op->setCRSs(this, false); + return util::nn_static_pointer_cast(op); +} +//! @endcond + +// --------------------------------------------------------------------------- + +void ConcatenatedOperation::_exportToPROJString( + io::PROJStringFormatter *formatter) const // throw(FormattingException) +{ + for (const auto &operation : operations()) { + operation->_exportToPROJString(formatter); + } +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +bool ConcatenatedOperation::_isEquivalentTo( + const util::IComparable *other, util::IComparable::Criterion criterion, + const io::DatabaseContextPtr &dbContext) const { + auto otherCO = dynamic_cast(other); + if (otherCO == nullptr || + (criterion == util::IComparable::Criterion::STRICT && + !ObjectUsage::_isEquivalentTo(other, criterion, dbContext))) { + return false; + } + const auto &steps = operations(); + const auto &otherSteps = otherCO->operations(); + if (steps.size() != otherSteps.size()) { + return false; + } + for (size_t i = 0; i < steps.size(); i++) { + if (!steps[i]->_isEquivalentTo(otherSteps[i].get(), criterion, + dbContext)) { + return false; + } + } + return true; +} +//! @endcond + +// --------------------------------------------------------------------------- + +std::set ConcatenatedOperation::gridsNeeded( + const io::DatabaseContextPtr &databaseContext, + bool considerKnownGridsAsAvailable) const { + std::set res; + for (const auto &operation : operations()) { + const auto l_gridsNeeded = operation->gridsNeeded( + databaseContext, considerKnownGridsAsAvailable); + for (const auto &gridDesc : l_gridsNeeded) { + res.insert(gridDesc); + } + } + return res; +} + +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END diff --git a/src/iso19111/operation/conversion.cpp b/src/iso19111/operation/conversion.cpp new file mode 100644 index 0000000000..66adf6d9fd --- /dev/null +++ b/src/iso19111/operation/conversion.cpp @@ -0,0 +1,3916 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP +#endif + +#include "proj/common.hpp" +#include "proj/coordinateoperation.hpp" +#include "proj/crs.hpp" +#include "proj/io.hpp" +#include "proj/metadata.hpp" +#include "proj/util.hpp" + +#include "proj/internal/internal.hpp" +#include "proj/internal/io_internal.hpp" +#include "proj/internal/tracing.hpp" + +#include "coordinateoperation_internal.hpp" +#include "esriparammappings.hpp" +#include "operationmethod_private.hpp" +#include "oputils.hpp" +#include "parammappings.hpp" +#include "vectorofvaluesparams.hpp" + +// PROJ include order is sensitive +// clang-format off +#include "proj.h" +#include "proj_internal.h" // M_PI +// clang-format on +#include "proj_constants.h" + +#include "proj_json_streaming_writer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace NS_PROJ::internal; + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +//! @cond Doxygen_Suppress +constexpr double UTM_LATITUDE_OF_NATURAL_ORIGIN = 0.0; +constexpr double UTM_SCALE_FACTOR = 0.9996; +constexpr double UTM_FALSE_EASTING = 500000.0; +constexpr double UTM_NORTH_FALSE_NORTHING = 0.0; +constexpr double UTM_SOUTH_FALSE_NORTHING = 10000000.0; +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct Conversion::Private {}; +//! @endcond + +// --------------------------------------------------------------------------- + +Conversion::Conversion(const OperationMethodNNPtr &methodIn, + const std::vector &values) + : SingleOperation(methodIn), d(nullptr) { + setParameterValues(values); +} + +// --------------------------------------------------------------------------- + +Conversion::Conversion(const Conversion &other) + : CoordinateOperation(other), SingleOperation(other), d(nullptr) {} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +Conversion::~Conversion() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +ConversionNNPtr Conversion::shallowClone() const { + auto conv = Conversion::nn_make_shared(*this); + conv->assignSelf(conv); + conv->setCRSs(this, false); + return conv; +} + +CoordinateOperationNNPtr Conversion::_shallowClone() const { + return util::nn_static_pointer_cast(shallowClone()); +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +ConversionNNPtr +Conversion::alterParametersLinearUnit(const common::UnitOfMeasure &unit, + bool convertToNewUnit) const { + + std::vector newValues; + bool changesDone = false; + for (const auto &genOpParamvalue : parameterValues()) { + bool updated = false; + auto opParamvalue = dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue) { + const auto ¶mValue = opParamvalue->parameterValue(); + if (paramValue->type() == ParameterValue::Type::MEASURE) { + const auto &measure = paramValue->value(); + if (measure.unit().type() == + common::UnitOfMeasure::Type::LINEAR) { + if (!measure.unit()._isEquivalentTo( + unit, util::IComparable::Criterion::EQUIVALENT)) { + const double newValue = + convertToNewUnit ? measure.convertToUnit(unit) + : measure.value(); + newValues.emplace_back(OperationParameterValue::create( + opParamvalue->parameter(), + ParameterValue::create( + common::Measure(newValue, unit)))); + updated = true; + } + } + } + } + if (updated) { + changesDone = true; + } else { + newValues.emplace_back(genOpParamvalue); + } + } + if (changesDone) { + auto conv = create(util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, "unknown"), + method(), newValues); + conv->setCRSs(this, false); + return conv; + } else { + return NN_NO_CHECK( + util::nn_dynamic_pointer_cast(shared_from_this())); + } +} +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a Conversion from a vector of GeneralParameterValue. + * + * @param properties See \ref general_properties. At minimum the name should be + * defined. + * @param methodIn the operation method. + * @param values the values. + * @return a new Conversion. + * @throws InvalidOperation + */ +ConversionNNPtr Conversion::create(const util::PropertyMap &properties, + const OperationMethodNNPtr &methodIn, + const std::vector + &values) // throw InvalidOperation +{ + if (methodIn->parameters().size() != values.size()) { + throw InvalidOperation( + "Inconsistent number of parameters and parameter values"); + } + auto conv = Conversion::nn_make_shared(methodIn, values); + conv->assignSelf(conv); + conv->setProperties(properties); + return conv; +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a Conversion and its OperationMethod + * + * @param propertiesConversion See \ref general_properties of the conversion. + * At minimum the name should be defined. + * @param propertiesOperationMethod See \ref general_properties of the operation + * method. At minimum the name should be defined. + * @param parameters the operation parameters. + * @param values the operation values. Constraint: + * values.size() == parameters.size() + * @return a new Conversion. + * @throws InvalidOperation + */ +ConversionNNPtr Conversion::create( + const util::PropertyMap &propertiesConversion, + const util::PropertyMap &propertiesOperationMethod, + const std::vector ¶meters, + const std::vector &values) // throw InvalidOperation +{ + OperationMethodNNPtr op( + OperationMethod::create(propertiesOperationMethod, parameters)); + + if (parameters.size() != values.size()) { + throw InvalidOperation( + "Inconsistent number of parameters and parameter values"); + } + std::vector generalParameterValues; + generalParameterValues.reserve(values.size()); + for (size_t i = 0; i < values.size(); i++) { + generalParameterValues.push_back( + OperationParameterValue::create(parameters[i], values[i])); + } + return create(propertiesConversion, op, generalParameterValues); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +// --------------------------------------------------------------------------- + +static util::PropertyMap +getUTMConversionProperty(const util::PropertyMap &properties, int zone, + bool north) { + if (!properties.get(common::IdentifiedObject::NAME_KEY)) { + std::string conversionName("UTM zone "); + conversionName += toString(zone); + conversionName += (north ? 'N' : 'S'); + + return createMapNameEPSGCode(conversionName, + (north ? 16000 : 17000) + zone); + } else { + return properties; + } +} + +// --------------------------------------------------------------------------- + +static ConversionNNPtr +createConversion(const util::PropertyMap &properties, + const MethodMapping *mapping, + const std::vector &values) { + + std::vector parameters; + for (int i = 0; mapping->params[i] != nullptr; i++) { + const auto *param = mapping->params[i]; + auto paramProperties = util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, param->wkt2_name); + if (param->epsg_code != 0) { + paramProperties + .set(metadata::Identifier::CODESPACE_KEY, + metadata::Identifier::EPSG) + .set(metadata::Identifier::CODE_KEY, param->epsg_code); + } + auto parameter = OperationParameter::create(paramProperties); + parameters.push_back(parameter); + } + + auto methodProperties = util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, mapping->wkt2_name); + if (mapping->epsg_code != 0) { + methodProperties + .set(metadata::Identifier::CODESPACE_KEY, + metadata::Identifier::EPSG) + .set(metadata::Identifier::CODE_KEY, mapping->epsg_code); + } + return Conversion::create( + addDefaultNameIfNeeded(properties, mapping->wkt2_name), + methodProperties, parameters, values); +} +//! @endcond + +// --------------------------------------------------------------------------- + +ConversionNNPtr +Conversion::create(const util::PropertyMap &properties, int method_epsg_code, + const std::vector &values) { + const MethodMapping *mapping = getMapping(method_epsg_code); + assert(mapping); + return createConversion(properties, mapping, values); +} + +// --------------------------------------------------------------------------- + +ConversionNNPtr +Conversion::create(const util::PropertyMap &properties, + const char *method_wkt2_name, + const std::vector &values) { + const MethodMapping *mapping = getMapping(method_wkt2_name); + assert(mapping); + return createConversion(properties, mapping, values); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a [Universal Transverse Mercator] + *(https://proj.org/operations/projections/utm.html) conversion. + * + * UTM is a family of conversions, of EPSG codes from 16001 to 16060 for the + * northern hemisphere, and 17001 to 17060 for the southern hemisphere, + * based on the Transverse Mercator projection method. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param zone UTM zone number between 1 and 60. + * @param north true for UTM northern hemisphere, false for UTM southern + * hemisphere. + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createUTM(const util::PropertyMap &properties, + int zone, bool north) { + return create( + getUTMConversionProperty(properties, zone, north), + EPSG_CODE_METHOD_TRANSVERSE_MERCATOR, + createParams(common::Angle(UTM_LATITUDE_OF_NATURAL_ORIGIN), + common::Angle(zone * 6.0 - 183.0), + common::Scale(UTM_SCALE_FACTOR), + common::Length(UTM_FALSE_EASTING), + common::Length(north ? UTM_NORTH_FALSE_NORTHING + : UTM_SOUTH_FALSE_NORTHING))); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Transverse Mercator] + *(https://proj.org/operations/projections/tmerc.html) projection method. + * + * This method is defined as [EPSG:9807] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9807) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param scale See \ref scale + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createTransverseMercator( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Scale &scale, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_TRANSVERSE_MERCATOR, + createParams(centerLat, centerLong, scale, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Gauss Schreiber Transverse + *Mercator] + *(https://proj.org/operations/projections/gstmerc.html) projection method. + * + * This method is also known as Gauss-Laborde Reunion. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param scale See \ref scale + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createGaussSchreiberTransverseMercator( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Scale &scale, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, + PROJ_WKT2_NAME_METHOD_GAUSS_SCHREIBER_TRANSVERSE_MERCATOR, + createParams(centerLat, centerLong, scale, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Transverse Mercator South + *Orientated] + *(https://proj.org/operations/projections/tmerc.html) projection method. + * + * This method is defined as [EPSG:9808] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9808) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param scale See \ref scale + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createTransverseMercatorSouthOriented( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Scale &scale, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, + EPSG_CODE_METHOD_TRANSVERSE_MERCATOR_SOUTH_ORIENTATED, + createParams(centerLat, centerLong, scale, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Two Point Equidistant] + *(https://proj.org/operations/projections/tpeqd.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeFirstPoint Latitude of first point. + * @param longitudeFirstPoint Longitude of first point. + * @param latitudeSecondPoint Latitude of second point. + * @param longitudeSeconPoint Longitude of second point. + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr +Conversion::createTwoPointEquidistant(const util::PropertyMap &properties, + const common::Angle &latitudeFirstPoint, + const common::Angle &longitudeFirstPoint, + const common::Angle &latitudeSecondPoint, + const common::Angle &longitudeSeconPoint, + const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_TWO_POINT_EQUIDISTANT, + createParams(latitudeFirstPoint, longitudeFirstPoint, + latitudeSecondPoint, longitudeSeconPoint, + falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the Tunisia Mapping Grid projection + * method. + * + * This method is defined as [EPSG:9816] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9816) + * + * \note There is currently no implementation of the method formulas in PROJ. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createTunisiaMappingGrid( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create( + properties, EPSG_CODE_METHOD_TUNISIA_MAPPING_GRID, + createParams(centerLat, centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Albers Conic Equal Area] + *(https://proj.org/operations/projections/aea.html) projection method. + * + * This method is defined as [EPSG:9822] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9822) + * + * @note the order of arguments is conformant with the corresponding EPSG + * mode and different than OGRSpatialReference::setACEA() of GDAL <= 2.3 + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeFalseOrigin See \ref latitude_false_origin + * @param longitudeFalseOrigin See \ref longitude_false_origin + * @param latitudeFirstParallel See \ref latitude_first_std_parallel + * @param latitudeSecondParallel See \ref latitude_second_std_parallel + * @param eastingFalseOrigin See \ref easting_false_origin + * @param northingFalseOrigin See \ref northing_false_origin + * @return a new Conversion. + */ +ConversionNNPtr +Conversion::createAlbersEqualArea(const util::PropertyMap &properties, + const common::Angle &latitudeFalseOrigin, + const common::Angle &longitudeFalseOrigin, + const common::Angle &latitudeFirstParallel, + const common::Angle &latitudeSecondParallel, + const common::Length &eastingFalseOrigin, + const common::Length &northingFalseOrigin) { + return create(properties, EPSG_CODE_METHOD_ALBERS_EQUAL_AREA, + createParams(latitudeFalseOrigin, longitudeFalseOrigin, + latitudeFirstParallel, latitudeSecondParallel, + eastingFalseOrigin, northingFalseOrigin)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Lambert Conic Conformal 1SP] + *(https://proj.org/operations/projections/lcc.html) projection method. + * + * This method is defined as [EPSG:9801] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9801) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param scale See \ref scale + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createLambertConicConformal_1SP( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Scale &scale, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP, + createParams(centerLat, centerLong, scale, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Lambert Conic Conformal (2SP)] + *(https://proj.org/operations/projections/lcc.html) projection method. + * + * This method is defined as [EPSG:9802] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9802) + * + * @note the order of arguments is conformant with the corresponding EPSG + * mode and different than OGRSpatialReference::setLCC() of GDAL <= 2.3 + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeFalseOrigin See \ref latitude_false_origin + * @param longitudeFalseOrigin See \ref longitude_false_origin + * @param latitudeFirstParallel See \ref latitude_first_std_parallel + * @param latitudeSecondParallel See \ref latitude_second_std_parallel + * @param eastingFalseOrigin See \ref easting_false_origin + * @param northingFalseOrigin See \ref northing_false_origin + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createLambertConicConformal_2SP( + const util::PropertyMap &properties, + const common::Angle &latitudeFalseOrigin, + const common::Angle &longitudeFalseOrigin, + const common::Angle &latitudeFirstParallel, + const common::Angle &latitudeSecondParallel, + const common::Length &eastingFalseOrigin, + const common::Length &northingFalseOrigin) { + return create(properties, EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP, + createParams(latitudeFalseOrigin, longitudeFalseOrigin, + latitudeFirstParallel, latitudeSecondParallel, + eastingFalseOrigin, northingFalseOrigin)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Lambert Conic Conformal (2SP + *Michigan)] + *(https://proj.org/operations/projections/lcc.html) projection method. + * + * This method is defined as [EPSG:1051] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1051) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeFalseOrigin See \ref latitude_false_origin + * @param longitudeFalseOrigin See \ref longitude_false_origin + * @param latitudeFirstParallel See \ref latitude_first_std_parallel + * @param latitudeSecondParallel See \ref latitude_second_std_parallel + * @param eastingFalseOrigin See \ref easting_false_origin + * @param northingFalseOrigin See \ref northing_false_origin + * @param ellipsoidScalingFactor Ellipsoid scaling factor. + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createLambertConicConformal_2SP_Michigan( + const util::PropertyMap &properties, + const common::Angle &latitudeFalseOrigin, + const common::Angle &longitudeFalseOrigin, + const common::Angle &latitudeFirstParallel, + const common::Angle &latitudeSecondParallel, + const common::Length &eastingFalseOrigin, + const common::Length &northingFalseOrigin, + const common::Scale &ellipsoidScalingFactor) { + return create(properties, + EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP_MICHIGAN, + createParams(latitudeFalseOrigin, longitudeFalseOrigin, + latitudeFirstParallel, latitudeSecondParallel, + eastingFalseOrigin, northingFalseOrigin, + ellipsoidScalingFactor)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Lambert Conic Conformal (2SP + *Belgium)] + *(https://proj.org/operations/projections/lcc.html) projection method. + * + * This method is defined as [EPSG:9803] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9803) + * + * \warning The formulas used currently in PROJ are, incorrectly, the ones of + * the regular LCC_2SP method. + * + * @note the order of arguments is conformant with the corresponding EPSG + * mode and different than OGRSpatialReference::setLCCB() of GDAL <= 2.3 + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeFalseOrigin See \ref latitude_false_origin + * @param longitudeFalseOrigin See \ref longitude_false_origin + * @param latitudeFirstParallel See \ref latitude_first_std_parallel + * @param latitudeSecondParallel See \ref latitude_second_std_parallel + * @param eastingFalseOrigin See \ref easting_false_origin + * @param northingFalseOrigin See \ref northing_false_origin + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createLambertConicConformal_2SP_Belgium( + const util::PropertyMap &properties, + const common::Angle &latitudeFalseOrigin, + const common::Angle &longitudeFalseOrigin, + const common::Angle &latitudeFirstParallel, + const common::Angle &latitudeSecondParallel, + const common::Length &eastingFalseOrigin, + const common::Length &northingFalseOrigin) { + + return create(properties, + EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP_BELGIUM, + createParams(latitudeFalseOrigin, longitudeFalseOrigin, + latitudeFirstParallel, latitudeSecondParallel, + eastingFalseOrigin, northingFalseOrigin)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Modified Azimuthal + *Equidistant] + *(https://proj.org/operations/projections/aeqd.html) projection method. + * + * This method is defined as [EPSG:9832] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9832) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeNatOrigin See \ref center_latitude + * @param longitudeNatOrigin See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createAzimuthalEquidistant( + const util::PropertyMap &properties, const common::Angle &latitudeNatOrigin, + const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_MODIFIED_AZIMUTHAL_EQUIDISTANT, + createParams(latitudeNatOrigin, longitudeNatOrigin, + falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Guam Projection] + *(https://proj.org/operations/projections/aeqd.html) projection method. + * + * This method is defined as [EPSG:9831] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9831) + * + * @param properties See \ref general_properties of the conversion. If the name + *is + * not provided, it is automatically set. + * @param latitudeNatOrigin See \ref center_latitude + * @param longitudeNatOrigin See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createGuamProjection( + const util::PropertyMap &properties, const common::Angle &latitudeNatOrigin, + const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_GUAM_PROJECTION, + createParams(latitudeNatOrigin, longitudeNatOrigin, + falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Bonne] + *(https://proj.org/operations/projections/bonne.html) projection method. + * + * This method is defined as [EPSG:9827] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9827) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeNatOrigin See \ref center_latitude . PROJ calls its the + * standard parallel 1. + * @param longitudeNatOrigin See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createBonne(const util::PropertyMap &properties, + const common::Angle &latitudeNatOrigin, + const common::Angle &longitudeNatOrigin, + const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_BONNE, + createParams(latitudeNatOrigin, longitudeNatOrigin, + falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Lambert Cylindrical Equal Area + *(Spherical)] + *(https://proj.org/operations/projections/cea.html) projection method. + * + * This method is defined as [EPSG:9834] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9834) + * + * \warning The PROJ cea computation code would select the ellipsoidal form if + * a non-spherical ellipsoid is used for the base GeographicCRS. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeFirstParallel See \ref latitude_first_std_parallel. + * @param longitudeNatOrigin See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createLambertCylindricalEqualAreaSpherical( + const util::PropertyMap &properties, + const common::Angle &latitudeFirstParallel, + const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, + EPSG_CODE_METHOD_LAMBERT_CYLINDRICAL_EQUAL_AREA_SPHERICAL, + createParams(latitudeFirstParallel, longitudeNatOrigin, + falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Lambert Cylindrical Equal Area + *(ellipsoidal form)] + *(https://proj.org/operations/projections/cea.html) projection method. + * + * This method is defined as [EPSG:9835] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9835) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeFirstParallel See \ref latitude_first_std_parallel. + * @param longitudeNatOrigin See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createLambertCylindricalEqualArea( + const util::PropertyMap &properties, + const common::Angle &latitudeFirstParallel, + const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_LAMBERT_CYLINDRICAL_EQUAL_AREA, + createParams(latitudeFirstParallel, longitudeNatOrigin, + falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Cassini-Soldner] + * (https://proj.org/operations/projections/cass.html) projection method. + * + * This method is defined as [EPSG:9806] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9806) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createCassiniSoldner( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create( + properties, EPSG_CODE_METHOD_CASSINI_SOLDNER, + createParams(centerLat, centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Equidistant Conic] + *(https://proj.org/operations/projections/eqdc.html) projection method. + * + * There is no equivalent in EPSG. + * + * @note Although not found in EPSG, the order of arguments is conformant with + * the "spirit" of EPSG and different than OGRSpatialReference::setEC() of GDAL + *<= 2.3 * @param properties See \ref general_properties of the conversion. + *If the name + * is not provided, it is automatically set. + * + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param latitudeFirstParallel See \ref latitude_first_std_parallel + * @param latitudeSecondParallel See \ref latitude_second_std_parallel + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createEquidistantConic( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Angle &latitudeFirstParallel, + const common::Angle &latitudeSecondParallel, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_EQUIDISTANT_CONIC, + createParams(centerLat, centerLong, latitudeFirstParallel, + latitudeSecondParallel, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Eckert I] + * (https://proj.org/operations/projections/eck1.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createEckertI(const util::PropertyMap &properties, + const common::Angle ¢erLong, + const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_ECKERT_I, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Eckert II] + * (https://proj.org/operations/projections/eck2.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createEckertII( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_ECKERT_II, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Eckert III] + * (https://proj.org/operations/projections/eck3.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createEckertIII( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_ECKERT_III, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Eckert IV] + * (https://proj.org/operations/projections/eck4.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createEckertIV( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_ECKERT_IV, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Eckert V] + * (https://proj.org/operations/projections/eck5.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createEckertV(const util::PropertyMap &properties, + const common::Angle ¢erLong, + const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_ECKERT_V, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Eckert VI] + * (https://proj.org/operations/projections/eck6.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createEckertVI( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_ECKERT_VI, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Equidistant Cylindrical] + *(https://proj.org/operations/projections/eqc.html) projection method. + * + * This is also known as the Equirectangular method, and in the particular case + * where the latitude of first parallel is 0. + * + * This method is defined as [EPSG:1028] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1028) + * + * @note This is the equivalent OGRSpatialReference::SetEquirectangular2( + * 0.0, latitudeFirstParallel, falseEasting, falseNorthing ) of GDAL <= 2.3, + * where the lat_0 / center_latitude parameter is forced to 0. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeFirstParallel See \ref latitude_first_std_parallel. + * @param longitudeNatOrigin See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createEquidistantCylindrical( + const util::PropertyMap &properties, + const common::Angle &latitudeFirstParallel, + const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL, + createParams(latitudeFirstParallel, 0.0, longitudeNatOrigin, + falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Equidistant Cylindrical + *(Spherical)] + *(https://proj.org/operations/projections/eqc.html) projection method. + * + * This is also known as the Equirectangular method, and in the particular case + * where the latitude of first parallel is 0. + * + * This method is defined as [EPSG:1029] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1029) + * + * @note This is the equivalent OGRSpatialReference::SetEquirectangular2( + * 0.0, latitudeFirstParallel, falseEasting, falseNorthing ) of GDAL <= 2.3, + * where the lat_0 / center_latitude parameter is forced to 0. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeFirstParallel See \ref latitude_first_std_parallel. + * @param longitudeNatOrigin See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createEquidistantCylindricalSpherical( + const util::PropertyMap &properties, + const common::Angle &latitudeFirstParallel, + const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, + EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL_SPHERICAL, + createParams(latitudeFirstParallel, 0.0, longitudeNatOrigin, + falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Gall (Stereographic)] + * (https://proj.org/operations/projections/gall.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createGall(const util::PropertyMap &properties, + const common::Angle ¢erLong, + const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_GALL_STEREOGRAPHIC, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Goode Homolosine] + * (https://proj.org/operations/projections/goode.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createGoodeHomolosine( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_GOODE_HOMOLOSINE, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Interrupted Goode Homolosine] + * (https://proj.org/operations/projections/igh.html) projection method. + * + * There is no equivalent in EPSG. + * + * @note OGRSpatialReference::SetIGH() of GDAL <= 2.3 assumes the 3 + * projection + * parameters to be zero and this is the nominal case. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createInterruptedGoodeHomolosine( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, + PROJ_WKT2_NAME_METHOD_INTERRUPTED_GOODE_HOMOLOSINE, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Geostationary Satellite View] + * (https://proj.org/operations/projections/geos.html) projection method, + * with the sweep angle axis of the viewing instrument being x + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param height Height of the view point above the Earth. + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createGeostationarySatelliteSweepX( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &height, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create( + properties, PROJ_WKT2_NAME_METHOD_GEOSTATIONARY_SATELLITE_SWEEP_X, + createParams(centerLong, height, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Geostationary Satellite View] + * (https://proj.org/operations/projections/geos.html) projection method, + * with the sweep angle axis of the viewing instrument being y. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param height Height of the view point above the Earth. + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createGeostationarySatelliteSweepY( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &height, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create( + properties, PROJ_WKT2_NAME_METHOD_GEOSTATIONARY_SATELLITE_SWEEP_Y, + createParams(centerLong, height, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Gnomonic] + *(https://proj.org/operations/projections/gnom.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createGnomonic( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create( + properties, PROJ_WKT2_NAME_METHOD_GNOMONIC, + createParams(centerLat, centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Hotine Oblique Mercator + *(Variant A)] + *(https://proj.org/operations/projections/omerc.html) projection method + * + * This is the variant with the no_uoff parameter, which corresponds to + * GDAL >=2.3 Hotine_Oblique_Mercator projection. + * In this variant, the false grid coordinates are + * defined at the intersection of the initial line and the aposphere (the + * equator on one of the intermediate surfaces inherent in the method), that is + * at the natural origin of the coordinate system). + * + * This method is defined as [EPSG:9812] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9812) + * + * \note In the case where azimuthInitialLine = angleFromRectifiedToSkrewGrid = + *90deg, + * this maps to the [Swiss Oblique Mercator] + *(https://proj.org/operations/projections/somerc.html) formulas. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeProjectionCentre See \ref latitude_projection_centre + * @param longitudeProjectionCentre See \ref longitude_projection_centre + * @param azimuthInitialLine See \ref azimuth_initial_line + * @param angleFromRectifiedToSkrewGrid See + * \ref angle_from_recitfied_to_skrew_grid + * @param scale See \ref scale_factor_initial_line + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createHotineObliqueMercatorVariantA( + const util::PropertyMap &properties, + const common::Angle &latitudeProjectionCentre, + const common::Angle &longitudeProjectionCentre, + const common::Angle &azimuthInitialLine, + const common::Angle &angleFromRectifiedToSkrewGrid, + const common::Scale &scale, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create( + properties, EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_A, + createParams(latitudeProjectionCentre, longitudeProjectionCentre, + azimuthInitialLine, angleFromRectifiedToSkrewGrid, scale, + falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Hotine Oblique Mercator + *(Variant B)] + *(https://proj.org/operations/projections/omerc.html) projection method + * + * This is the variant without the no_uoff parameter, which corresponds to + * GDAL >=2.3 Hotine_Oblique_Mercator_Azimuth_Center projection. + * In this variant, the false grid coordinates are defined at the projection + *centre. + * + * This method is defined as [EPSG:9815] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9815) + * + * \note In the case where azimuthInitialLine = angleFromRectifiedToSkrewGrid = + *90deg, + * this maps to the [Swiss Oblique Mercator] + *(https://proj.org/operations/projections/somerc.html) formulas. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeProjectionCentre See \ref latitude_projection_centre + * @param longitudeProjectionCentre See \ref longitude_projection_centre + * @param azimuthInitialLine See \ref azimuth_initial_line + * @param angleFromRectifiedToSkrewGrid See + * \ref angle_from_recitfied_to_skrew_grid + * @param scale See \ref scale_factor_initial_line + * @param eastingProjectionCentre See \ref easting_projection_centre + * @param northingProjectionCentre See \ref northing_projection_centre + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createHotineObliqueMercatorVariantB( + const util::PropertyMap &properties, + const common::Angle &latitudeProjectionCentre, + const common::Angle &longitudeProjectionCentre, + const common::Angle &azimuthInitialLine, + const common::Angle &angleFromRectifiedToSkrewGrid, + const common::Scale &scale, const common::Length &eastingProjectionCentre, + const common::Length &northingProjectionCentre) { + return create( + properties, EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_B, + createParams(latitudeProjectionCentre, longitudeProjectionCentre, + azimuthInitialLine, angleFromRectifiedToSkrewGrid, scale, + eastingProjectionCentre, northingProjectionCentre)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Hotine Oblique Mercator Two + *Point Natural Origin] + *(https://proj.org/operations/projections/omerc.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeProjectionCentre See \ref latitude_projection_centre + * @param latitudePoint1 Latitude of point 1. + * @param longitudePoint1 Latitude of point 1. + * @param latitudePoint2 Latitude of point 2. + * @param longitudePoint2 Longitude of point 2. + * @param scale See \ref scale_factor_initial_line + * @param eastingProjectionCentre See \ref easting_projection_centre + * @param northingProjectionCentre See \ref northing_projection_centre + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createHotineObliqueMercatorTwoPointNaturalOrigin( + const util::PropertyMap &properties, + const common::Angle &latitudeProjectionCentre, + const common::Angle &latitudePoint1, const common::Angle &longitudePoint1, + const common::Angle &latitudePoint2, const common::Angle &longitudePoint2, + const common::Scale &scale, const common::Length &eastingProjectionCentre, + const common::Length &northingProjectionCentre) { + return create( + properties, + PROJ_WKT2_NAME_METHOD_HOTINE_OBLIQUE_MERCATOR_TWO_POINT_NATURAL_ORIGIN, + { + ParameterValue::create(latitudeProjectionCentre), + ParameterValue::create(latitudePoint1), + ParameterValue::create(longitudePoint1), + ParameterValue::create(latitudePoint2), + ParameterValue::create(longitudePoint2), + ParameterValue::create(scale), + ParameterValue::create(eastingProjectionCentre), + ParameterValue::create(northingProjectionCentre), + }); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Laborde Oblique Mercator] + *(https://proj.org/operations/projections/labrd.html) projection method. + * + * This method is defined as [EPSG:9813] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9813) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeProjectionCentre See \ref latitude_projection_centre + * @param longitudeProjectionCentre See \ref longitude_projection_centre + * @param azimuthInitialLine See \ref azimuth_initial_line + * @param scale See \ref scale_factor_initial_line + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createLabordeObliqueMercator( + const util::PropertyMap &properties, + const common::Angle &latitudeProjectionCentre, + const common::Angle &longitudeProjectionCentre, + const common::Angle &azimuthInitialLine, const common::Scale &scale, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_LABORDE_OBLIQUE_MERCATOR, + createParams(latitudeProjectionCentre, + longitudeProjectionCentre, azimuthInitialLine, + scale, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [International Map of the World + *Polyconic] + *(https://proj.org/operations/projections/imw_p.html) projection method. + * + * There is no equivalent in EPSG. + * + * @note the order of arguments is conformant with the corresponding EPSG + * mode and different than OGRSpatialReference::SetIWMPolyconic() of GDAL <= + *2.3 + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param latitudeFirstParallel See \ref latitude_first_std_parallel + * @param latitudeSecondParallel See \ref latitude_second_std_parallel + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createInternationalMapWorldPolyconic( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Angle &latitudeFirstParallel, + const common::Angle &latitudeSecondParallel, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_INTERNATIONAL_MAP_WORLD_POLYCONIC, + createParams(centerLong, latitudeFirstParallel, + latitudeSecondParallel, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Krovak (north oriented)] + *(https://proj.org/operations/projections/krovak.html) projection method. + * + * This method is defined as [EPSG:1041] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1041) + * + * The coordinates are returned in the "GIS friendly" order: easting, northing. + * This method is similar to createKrovak(), except that the later returns + * projected values as southing, westing, where + * southing(Krovak) = -northing(Krovak_North) and + * westing(Krovak) = -easting(Krovak_North). + * + * @note The current PROJ implementation of Krovak hard-codes + * colatitudeConeAxis = 30deg17'17.30311" + * and latitudePseudoStandardParallel = 78deg30'N, which are the values used for + * the ProjectedCRS S-JTSK (Ferro) / Krovak East North (EPSG:5221). + * It also hard-codes the parameters of the Bessel ellipsoid typically used for + * Krovak. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeProjectionCentre See \ref latitude_projection_centre + * @param longitudeOfOrigin See \ref longitude_of_origin + * @param colatitudeConeAxis See \ref colatitude_cone_axis + * @param latitudePseudoStandardParallel See \ref + *latitude_pseudo_standard_parallel + * @param scaleFactorPseudoStandardParallel See \ref + *scale_factor_pseudo_standard_parallel + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createKrovakNorthOriented( + const util::PropertyMap &properties, + const common::Angle &latitudeProjectionCentre, + const common::Angle &longitudeOfOrigin, + const common::Angle &colatitudeConeAxis, + const common::Angle &latitudePseudoStandardParallel, + const common::Scale &scaleFactorPseudoStandardParallel, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_KROVAK_NORTH_ORIENTED, + createParams(latitudeProjectionCentre, longitudeOfOrigin, + colatitudeConeAxis, + latitudePseudoStandardParallel, + scaleFactorPseudoStandardParallel, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Krovak] + *(https://proj.org/operations/projections/krovak.html) projection method. + * + * This method is defined as [EPSG:9819] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9819) + * + * The coordinates are returned in the historical order: southing, westing + * This method is similar to createKrovakNorthOriented(), except that the later + *returns + * projected values as easting, northing, where + * easting(Krovak_North) = -westing(Krovak) and + * northing(Krovak_North) = -southing(Krovak). + * + * @note The current PROJ implementation of Krovak hard-codes + * colatitudeConeAxis = 30deg17'17.30311" + * and latitudePseudoStandardParallel = 78deg30'N, which are the values used for + * the ProjectedCRS S-JTSK (Ferro) / Krovak East North (EPSG:5221). + * It also hard-codes the parameters of the Bessel ellipsoid typically used for + * Krovak. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeProjectionCentre See \ref latitude_projection_centre + * @param longitudeOfOrigin See \ref longitude_of_origin + * @param colatitudeConeAxis See \ref colatitude_cone_axis + * @param latitudePseudoStandardParallel See \ref + *latitude_pseudo_standard_parallel + * @param scaleFactorPseudoStandardParallel See \ref + *scale_factor_pseudo_standard_parallel + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr +Conversion::createKrovak(const util::PropertyMap &properties, + const common::Angle &latitudeProjectionCentre, + const common::Angle &longitudeOfOrigin, + const common::Angle &colatitudeConeAxis, + const common::Angle &latitudePseudoStandardParallel, + const common::Scale &scaleFactorPseudoStandardParallel, + const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_KROVAK, + createParams(latitudeProjectionCentre, longitudeOfOrigin, + colatitudeConeAxis, + latitudePseudoStandardParallel, + scaleFactorPseudoStandardParallel, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Lambert Azimuthal Equal Area] + *(https://proj.org/operations/projections/laea.html) projection method. + * + * This method is defined as [EPSG:9820] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9820) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeNatOrigin See \ref center_latitude + * @param longitudeNatOrigin See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createLambertAzimuthalEqualArea( + const util::PropertyMap &properties, const common::Angle &latitudeNatOrigin, + const common::Angle &longitudeNatOrigin, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_LAMBERT_AZIMUTHAL_EQUAL_AREA, + createParams(latitudeNatOrigin, longitudeNatOrigin, + falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Miller Cylindrical] + *(https://proj.org/operations/projections/mill.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createMillerCylindrical( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_MILLER_CYLINDRICAL, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Mercator] + *(https://proj.org/operations/projections/merc.html) projection method. + * + * This is the variant, also known as Mercator (1SP), defined with the scale + * factor. Note that latitude of natural origin (centerLat) is a parameter, + * but unused in the transformation formulas. + * + * This method is defined as [EPSG:9804] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9804) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude . Should be 0. + * @param centerLong See \ref center_longitude + * @param scale See \ref scale + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createMercatorVariantA( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Scale &scale, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_MERCATOR_VARIANT_A, + createParams(centerLat, centerLong, scale, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Mercator] + *(https://proj.org/operations/projections/merc.html) projection method. + * + * This is the variant, also known as Mercator (2SP), defined with the latitude + * of the first standard parallel (the second standard parallel is implicitly + * the opposite value). The latitude of natural origin is fixed to zero. + * + * This method is defined as [EPSG:9805] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9805) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeFirstParallel See \ref latitude_first_std_parallel + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createMercatorVariantB( + const util::PropertyMap &properties, + const common::Angle &latitudeFirstParallel, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_MERCATOR_VARIANT_B, + createParams(latitudeFirstParallel, centerLong, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Popular Visualisation Pseudo + *Mercator] + *(https://proj.org/operations/projections/webmerc.html) projection method. + * + * Also known as WebMercator. Mostly/only used for Projected CRS EPSG:3857 + * (WGS 84 / Pseudo-Mercator) + * + * This method is defined as [EPSG:1024] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1024) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude . Usually 0 + * @param centerLong See \ref center_longitude . Usually 0 + * @param falseEasting See \ref false_easting . Usually 0 + * @param falseNorthing See \ref false_northing . Usually 0 + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createPopularVisualisationPseudoMercator( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create( + properties, EPSG_CODE_METHOD_POPULAR_VISUALISATION_PSEUDO_MERCATOR, + createParams(centerLat, centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Mollweide] + * (https://proj.org/operations/projections/moll.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createMollweide( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_MOLLWEIDE, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [New Zealand Map Grid] + * (https://proj.org/operations/projections/nzmg.html) projection method. + * + * This method is defined as [EPSG:9811] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9811) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createNewZealandMappingGrid( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create( + properties, EPSG_CODE_METHOD_NZMG, + createParams(centerLat, centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Oblique Stereographic + *(Alternative)] + *(https://proj.org/operations/projections/sterea.html) projection method. + * + * This method is defined as [EPSG:9809] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9809) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param scale See \ref scale + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createObliqueStereographic( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Scale &scale, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_OBLIQUE_STEREOGRAPHIC, + createParams(centerLat, centerLong, scale, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Orthographic] + *(https://proj.org/operations/projections/ortho.html) projection method. + * + * This method is defined as [EPSG:9840] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9840) + * + * \note Before PROJ 7.2, only the spherical formulation was implemented. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createOrthographic( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create( + properties, EPSG_CODE_METHOD_ORTHOGRAPHIC, + createParams(centerLat, centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [American Polyconic] + *(https://proj.org/operations/projections/poly.html) projection method. + * + * This method is defined as [EPSG:9818] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9818) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createAmericanPolyconic( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create( + properties, EPSG_CODE_METHOD_AMERICAN_POLYCONIC, + createParams(centerLat, centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Polar Stereographic (Variant + *A)] + *(https://proj.org/operations/projections/stere.html) projection method. + * + * This method is defined as [EPSG:9810] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9810) + * + * This is the variant of polar stereographic defined with a scale factor. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude . Should be 90 deg ou -90 deg. + * @param centerLong See \ref center_longitude + * @param scale See \ref scale + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createPolarStereographicVariantA( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Scale &scale, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_POLAR_STEREOGRAPHIC_VARIANT_A, + createParams(centerLat, centerLong, scale, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Polar Stereographic (Variant + *B)] + *(https://proj.org/operations/projections/stere.html) projection method. + * + * This method is defined as [EPSG:9829] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9829) + * + * This is the variant of polar stereographic defined with a latitude of + * standard parallel. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeStandardParallel See \ref latitude_std_parallel + * @param longitudeOfOrigin See \ref longitude_of_origin + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createPolarStereographicVariantB( + const util::PropertyMap &properties, + const common::Angle &latitudeStandardParallel, + const common::Angle &longitudeOfOrigin, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_POLAR_STEREOGRAPHIC_VARIANT_B, + createParams(latitudeStandardParallel, longitudeOfOrigin, + falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Robinson] + * (https://proj.org/operations/projections/robin.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createRobinson( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_ROBINSON, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Sinusoidal] + * (https://proj.org/operations/projections/sinu.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createSinusoidal( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_SINUSOIDAL, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Stereographic] + *(https://proj.org/operations/projections/stere.html) projection method. + * + * There is no equivalent in EPSG. This method implements the original "Oblique + * Stereographic" method described in "Snyder's Map Projections - A Working + *manual", + * which is different from the "Oblique Stereographic (alternative") method + * implemented in createObliqueStereographic(). + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param scale See \ref scale + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createStereographic( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Scale &scale, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_STEREOGRAPHIC, + createParams(centerLat, centerLong, scale, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Van der Grinten] + * (https://proj.org/operations/projections/vandg.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createVanDerGrinten( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_VAN_DER_GRINTEN, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Wagner I] + * (https://proj.org/operations/projections/wag1.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createWagnerI(const util::PropertyMap &properties, + const common::Angle ¢erLong, + const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_I, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Wagner II] + * (https://proj.org/operations/projections/wag2.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createWagnerII( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_II, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Wagner III] + * (https://proj.org/operations/projections/wag3.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param latitudeTrueScale Latitude of true scale. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createWagnerIII( + const util::PropertyMap &properties, const common::Angle &latitudeTrueScale, + const common::Angle ¢erLong, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_III, + createParams(latitudeTrueScale, centerLong, falseEasting, + falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Wagner IV] + * (https://proj.org/operations/projections/wag4.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createWagnerIV( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_IV, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Wagner V] + * (https://proj.org/operations/projections/wag5.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createWagnerV(const util::PropertyMap &properties, + const common::Angle ¢erLong, + const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_V, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Wagner VI] + * (https://proj.org/operations/projections/wag6.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createWagnerVI( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_VI, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Wagner VII] + * (https://proj.org/operations/projections/wag7.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createWagnerVII( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, PROJ_WKT2_NAME_METHOD_WAGNER_VII, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Quadrilateralized Spherical + *Cube] + *(https://proj.org/operations/projections/qsc.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLat See \ref center_latitude + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createQuadrilateralizedSphericalCube( + const util::PropertyMap &properties, const common::Angle ¢erLat, + const common::Angle ¢erLong, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create( + properties, PROJ_WKT2_NAME_METHOD_QUADRILATERALIZED_SPHERICAL_CUBE, + createParams(centerLat, centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Spherical Cross-Track Height] + *(https://proj.org/operations/projections/sch.html) projection method. + * + * There is no equivalent in EPSG. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param pegPointLat Peg point latitude. + * @param pegPointLong Peg point longitude. + * @param pegPointHeading Peg point heading. + * @param pegPointHeight Peg point height. + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createSphericalCrossTrackHeight( + const util::PropertyMap &properties, const common::Angle &pegPointLat, + const common::Angle &pegPointLong, const common::Angle &pegPointHeading, + const common::Length &pegPointHeight) { + return create(properties, + PROJ_WKT2_NAME_METHOD_SPHERICAL_CROSS_TRACK_HEIGHT, + createParams(pegPointLat, pegPointLong, pegPointHeading, + pegPointHeight)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Equal Earth] + * (https://proj.org/operations/projections/eqearth.html) projection method. + * + * This method is defined as [EPSG:1078] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1078) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param centerLong See \ref center_longitude + * @param falseEasting See \ref false_easting + * @param falseNorthing See \ref false_northing + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createEqualEarth( + const util::PropertyMap &properties, const common::Angle ¢erLong, + const common::Length &falseEasting, const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_EQUAL_EARTH, + createParams(centerLong, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the [Vertical Perspective] + * (https://proj.org/operations/projections/nsper.html) projection method. + * + * This method is defined as [EPSG:9838] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9838) + * + * The PROJ implementation of the EPSG Vertical Perspective has the current + * limitations with respect to the method described in EPSG: + *
    + *
  • it is a 2D-only method, ignoring the ellipsoidal height of the point to + * project.
  • + *
  • it has only a spherical development.
  • + *
  • the height of the topocentric origin is ignored, and thus assumed to be + * 0.
  • + *
+ * + * For completeness, PROJ adds the falseEasting and falseNorthing parameter, + * which are not described in EPSG. They should usually be set to 0. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param topoOriginLat Latitude of topocentric origin + * @param topoOriginLong Longitude of topocentric origin + * @param topoOriginHeight Ellipsoidal height of topocentric origin. Ignored by + * PROJ (that is assumed to be 0) + * @param viewPointHeight Viewpoint height with respect to the + * topocentric/mapping plane. In the case where topoOriginHeight = 0, this is + * the height above the ellipsoid surface at topoOriginLat, topoOriginLong. + * @param falseEasting See \ref false_easting . (not in EPSG) + * @param falseNorthing See \ref false_northing . (not in EPSG) + * @return a new Conversion. + * + * @since 6.3 + */ +ConversionNNPtr Conversion::createVerticalPerspective( + const util::PropertyMap &properties, const common::Angle &topoOriginLat, + const common::Angle &topoOriginLong, const common::Length &topoOriginHeight, + const common::Length &viewPointHeight, const common::Length &falseEasting, + const common::Length &falseNorthing) { + return create(properties, EPSG_CODE_METHOD_VERTICAL_PERSPECTIVE, + createParams(topoOriginLat, topoOriginLong, topoOriginHeight, + viewPointHeight, falseEasting, falseNorthing)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the Pole Rotation method, using + * the conventions of the GRIB 1 and GRIB 2 data formats. + * + * Those are mentioned in the Note 2 of + * https://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_doc/grib2_temp3-1.shtml + * + * Several conventions for the pole rotation method exists. + * The parameters provided in this method are remapped to the PROJ ob_tran + * operation with: + *
+ * +proj=ob_tran +o_proj=longlat +o_lon_p=-rotationAngle
+ *                               +o_lat_p=-southPoleLatInUnrotatedCRS
+ *                               +lon_0=southPoleLongInUnrotatedCRS
+ * 
+ * + * Another implementation of that convention is also in the netcdf-java library: + * https://github.com/Unidata/netcdf-java/blob/3ce72c0cd167609ed8c69152bb4a004d1daa9273/cdm/core/src/main/java/ucar/unidata/geoloc/projection/RotatedLatLon.java + * + * The PROJ implementation of this method assumes a spherical ellipsoid. + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param southPoleLatInUnrotatedCRS Latitude of the point from the unrotated + * CRS, expressed in the unrotated CRS, that will become the south pole of the + * rotated CRS. + * @param southPoleLongInUnrotatedCRS Longitude of the point from the unrotated + * CRS, expressed in the unrotated CRS, that will become the south pole of the + * rotated CRS. + * @param axisRotation The angle of rotation about the new polar + * axis (measured clockwise when looking from the southern to the northern pole) + * of the coordinate system, assuming the new axis to have been obtained by + * first rotating the sphere through southPoleLongInUnrotatedCRS degrees about + * the geographic polar axis and then rotating through + * (90 + southPoleLatInUnrotatedCRS) degrees so that the southern pole moved + * along the (previously rotated) Greenwich meridian. + * @return a new Conversion. + * + * @since 7.0 + */ +ConversionNNPtr Conversion::createPoleRotationGRIBConvention( + const util::PropertyMap &properties, + const common::Angle &southPoleLatInUnrotatedCRS, + const common::Angle &southPoleLongInUnrotatedCRS, + const common::Angle &axisRotation) { + return create(properties, + PROJ_WKT2_NAME_METHOD_POLE_ROTATION_GRIB_CONVENTION, + createParams(southPoleLatInUnrotatedCRS, + southPoleLongInUnrotatedCRS, axisRotation)); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the Change of Vertical Unit + * method. + * + * This method is defined as [EPSG:1069] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1069) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param factor Conversion factor + * @return a new Conversion. + */ +ConversionNNPtr +Conversion::createChangeVerticalUnit(const util::PropertyMap &properties, + const common::Scale &factor) { + return create(properties, createMethodMapNameEPSGCode( + EPSG_CODE_METHOD_CHANGE_VERTICAL_UNIT), + VectorOfParameters{ + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR), + }, + VectorOfValues{ + factor, + }); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the Height Depth Reversal + * method. + * + * This method is defined as [EPSG:1068] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1068) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @return a new Conversion. + * @since 6.3 + */ +ConversionNNPtr +Conversion::createHeightDepthReversal(const util::PropertyMap &properties) { + return create(properties, createMethodMapNameEPSGCode( + EPSG_CODE_METHOD_HEIGHT_DEPTH_REVERSAL), + {}, {}); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the Axis order reversal method + * + * This swaps the longitude, latitude axis. + * + * This method is defined as [EPSG:9843] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9843), + * or for 3D as [EPSG:9844] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9844) + * + * @param is3D Whether this should apply on 3D geographicCRS + * @return a new Conversion. + */ +ConversionNNPtr Conversion::createAxisOrderReversal(bool is3D) { + if (is3D) { + return create(createMapNameEPSGCode(AXIS_ORDER_CHANGE_3D_NAME, 15499), + createMethodMapNameEPSGCode( + EPSG_CODE_METHOD_AXIS_ORDER_REVERSAL_3D), + {}, {}); + } else { + return create(createMapNameEPSGCode(AXIS_ORDER_CHANGE_2D_NAME, 15498), + createMethodMapNameEPSGCode( + EPSG_CODE_METHOD_AXIS_ORDER_REVERSAL_2D), + {}, {}); + } +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a conversion based on the Geographic/Geocentric method. + * + * This method is defined as [EPSG:9602] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9602), + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @return a new Conversion. + */ +ConversionNNPtr +Conversion::createGeographicGeocentric(const util::PropertyMap &properties) { + return create(properties, createMethodMapNameEPSGCode( + EPSG_CODE_METHOD_GEOGRAPHIC_GEOCENTRIC), + {}, {}); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +ConversionNNPtr +Conversion::createGeographicGeocentric(const crs::CRSNNPtr &sourceCRS, + const crs::CRSNNPtr &targetCRS) { + auto properties = util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, + buildOpName("Conversion", sourceCRS, targetCRS)); + auto conv = createGeographicGeocentric(properties); + conv->setCRSs(sourceCRS, targetCRS, nullptr); + return conv; +} + +// --------------------------------------------------------------------------- + +InverseConversion::InverseConversion(const ConversionNNPtr &forward) + : Conversion( + OperationMethod::create(createPropertiesForInverse(forward->method()), + forward->method()->parameters()), + forward->parameterValues()), + InverseCoordinateOperation(forward, true) { + setPropertiesFromForward(); +} + +// --------------------------------------------------------------------------- + +InverseConversion::~InverseConversion() = default; + +// --------------------------------------------------------------------------- + +ConversionNNPtr InverseConversion::inverseAsConversion() const { + return NN_NO_CHECK( + util::nn_dynamic_pointer_cast(forwardOperation_)); +} + +// --------------------------------------------------------------------------- + +CoordinateOperationNNPtr +InverseConversion::create(const ConversionNNPtr &forward) { + auto conv = util::nn_make_shared(forward); + conv->assignSelf(conv); + return conv; +} + +// --------------------------------------------------------------------------- + +CoordinateOperationNNPtr InverseConversion::_shallowClone() const { + auto op = InverseConversion::nn_make_shared( + inverseAsConversion()->shallowClone()); + op->assignSelf(op); + op->setCRSs(this, false); + return util::nn_static_pointer_cast(op); +} + +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +static bool isAxisOrderReversal2D(int methodEPSGCode) { + return methodEPSGCode == EPSG_CODE_METHOD_AXIS_ORDER_REVERSAL_2D; +} + +static bool isAxisOrderReversal3D(int methodEPSGCode) { + return methodEPSGCode == EPSG_CODE_METHOD_AXIS_ORDER_REVERSAL_3D; +} + +bool isAxisOrderReversal(int methodEPSGCode) { + return isAxisOrderReversal2D(methodEPSGCode) || + isAxisOrderReversal3D(methodEPSGCode); +} +//! @endcond + +// --------------------------------------------------------------------------- + +CoordinateOperationNNPtr Conversion::inverse() const { + const int methodEPSGCode = method()->getEPSGCode(); + + if (methodEPSGCode == EPSG_CODE_METHOD_CHANGE_VERTICAL_UNIT) { + const double convFactor = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR); + auto conv = createChangeVerticalUnit( + createPropertiesForInverse(this, false, false), + common::Scale(1.0 / convFactor)); + conv->setCRSs(this, true); + return conv; + } + + const bool l_isAxisOrderReversal2D = isAxisOrderReversal2D(methodEPSGCode); + const bool l_isAxisOrderReversal3D = isAxisOrderReversal3D(methodEPSGCode); + if (l_isAxisOrderReversal2D || l_isAxisOrderReversal3D) { + auto conv = createAxisOrderReversal(l_isAxisOrderReversal3D); + conv->setCRSs(this, true); + return conv; + } + + if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC_GEOCENTRIC) { + + auto conv = createGeographicGeocentric( + createPropertiesForInverse(this, false, false)); + conv->setCRSs(this, true); + return conv; + } + + if (methodEPSGCode == EPSG_CODE_METHOD_HEIGHT_DEPTH_REVERSAL) { + + auto conv = createHeightDepthReversal( + createPropertiesForInverse(this, false, false)); + conv->setCRSs(this, true); + return conv; + } + + return InverseConversion::create(NN_NO_CHECK( + util::nn_dynamic_pointer_cast(shared_from_this()))); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +static double msfn(double phi, double e2) { + const double sinphi = std::sin(phi); + const double cosphi = std::cos(phi); + return pj_msfn(sinphi, cosphi, e2); +} + +// --------------------------------------------------------------------------- + +static double tsfn(double phi, double ec) { + const double sinphi = std::sin(phi); + return pj_tsfn(phi, sinphi, ec); +} + +// --------------------------------------------------------------------------- + +// Function whose zeroes are the sin of the standard parallels of LCC_2SP +static double lcc_1sp_to_2sp_f(double sinphi, double K, double ec, double n) { + const double x = sinphi; + const double ecx = ec * x; + return (1 - x * x) / (1 - ecx * ecx) - + K * K * std::pow((1.0 - x) / (1.0 + x) * + std::pow((1.0 + ecx) / (1.0 - ecx), ec), + n); +} + +// --------------------------------------------------------------------------- + +// Find the sin of the standard parallels of LCC_2SP +static double find_zero_lcc_1sp_to_2sp_f(double sinphi0, bool bNorth, double K, + double ec) { + double a, b; + double f_a; + if (bNorth) { + // Look for zero above phi0 + a = sinphi0; + b = 1.0; // sin(North pole) + f_a = 1.0; // some positive value, but we only care about the sign + } else { + // Look for zero below phi0 + a = -1.0; // sin(South pole) + b = sinphi0; + f_a = -1.0; // minus infinity in fact, but we only care about the sign + } + // We use dichotomy search. lcc_1sp_to_2sp_f() is positive at sinphi_init, + // has a zero in ]-1,sinphi0[ and ]sinphi0,1[ ranges + for (int N = 0; N < 100; N++) { + double c = (a + b) / 2; + double f_c = lcc_1sp_to_2sp_f(c, K, ec, sinphi0); + if (f_c == 0.0 || (b - a) < 1e-18) { + return c; + } + if ((f_c > 0 && f_a > 0) || (f_c < 0 && f_a < 0)) { + a = c; + f_a = f_c; + } else { + b = c; + } + } + return (a + b) / 2; +} + +static inline double DegToRad(double x) { return x / 180.0 * M_PI; } +static inline double RadToDeg(double x) { return x / M_PI * 180.0; } + +//! @endcond + +// --------------------------------------------------------------------------- + +/** + * \brief Return an equivalent projection. + * + * Currently implemented: + *
    + *
  • EPSG_CODE_METHOD_MERCATOR_VARIANT_A (1SP) to + * EPSG_CODE_METHOD_MERCATOR_VARIANT_B (2SP)
  • + *
  • EPSG_CODE_METHOD_MERCATOR_VARIANT_B (2SP) to + * EPSG_CODE_METHOD_MERCATOR_VARIANT_A (1SP)
  • + *
  • EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP to + * EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP
  • + *
  • EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP to + * EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP
  • + *
+ * + * @param targetEPSGCode EPSG code of the target method. + * @return new conversion, or nullptr + */ +ConversionPtr Conversion::convertToOtherMethod(int targetEPSGCode) const { + const int current_epsg_code = method()->getEPSGCode(); + if (current_epsg_code == targetEPSGCode) { + return util::nn_dynamic_pointer_cast(shared_from_this()); + } + + auto geogCRS = dynamic_cast(sourceCRS().get()); + if (!geogCRS) { + return nullptr; + } + + const double e2 = geogCRS->ellipsoid()->squaredEccentricity(); + if (e2 < 0) { + return nullptr; + } + + if (current_epsg_code == EPSG_CODE_METHOD_MERCATOR_VARIANT_A && + targetEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_B && + parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN) == 0.0) { + const double k0 = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN); + if (!(k0 > 0 && k0 <= 1.0 + 1e-10)) + return nullptr; + const double dfStdP1Lat = + (k0 >= 1.0) + ? 0.0 + : std::acos(std::sqrt((1.0 - e2) / ((1.0 / (k0 * k0)) - e2))); + auto latitudeFirstParallel = common::Angle( + common::Angle(dfStdP1Lat, common::UnitOfMeasure::RADIAN) + .convertToUnit(common::UnitOfMeasure::DEGREE), + common::UnitOfMeasure::DEGREE); + auto conv = createMercatorVariantB( + util::PropertyMap(), latitudeFirstParallel, + common::Angle(parameterValueMeasure( + EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN)), + common::Length( + parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_EASTING)), + common::Length( + parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_NORTHING))); + conv->setCRSs(this, false); + return conv.as_nullable(); + } + + if (current_epsg_code == EPSG_CODE_METHOD_MERCATOR_VARIANT_B && + targetEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_A) { + const double phi1 = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL); + if (!(std::fabs(phi1) < M_PI / 2)) + return nullptr; + const double k0 = msfn(phi1, e2); + auto conv = createMercatorVariantA( + util::PropertyMap(), + common::Angle(0.0, common::UnitOfMeasure::DEGREE), + common::Angle(parameterValueMeasure( + EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN)), + common::Scale(k0, common::UnitOfMeasure::SCALE_UNITY), + common::Length( + parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_EASTING)), + common::Length( + parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_NORTHING))); + conv->setCRSs(this, false); + return conv.as_nullable(); + } + + if (current_epsg_code == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP && + targetEPSGCode == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP) { + // Notations m0, t0, n, m1, t1, F are those of the EPSG guidance + // "1.3.1.1 Lambert Conic Conformal (2SP)" and + // "1.3.1.2 Lambert Conic Conformal (1SP)" and + // or Snyder pages 106-109 + auto latitudeOfOrigin = common::Angle(parameterValueMeasure( + EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN)); + const double phi0 = latitudeOfOrigin.getSIValue(); + const double k0 = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN); + if (!(std::fabs(phi0) < M_PI / 2)) + return nullptr; + if (!(k0 > 0 && k0 <= 1.0 + 1e-10)) + return nullptr; + const double ec = std::sqrt(e2); + const double m0 = msfn(phi0, e2); + const double t0 = tsfn(phi0, ec); + const double n = sin(phi0); + if (std::fabs(n) < 1e-10) + return nullptr; + if (fabs(k0 - 1.0) <= 1e-10) { + auto conv = createLambertConicConformal_2SP( + util::PropertyMap(), latitudeOfOrigin, + common::Angle(parameterValueMeasure( + EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN)), + latitudeOfOrigin, latitudeOfOrigin, + common::Length( + parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_EASTING)), + common::Length( + parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_NORTHING))); + conv->setCRSs(this, false); + return conv.as_nullable(); + } else { + const double K = k0 * m0 / std::pow(t0, n); + const double phi1 = + std::asin(find_zero_lcc_1sp_to_2sp_f(n, true, K, ec)); + const double phi2 = + std::asin(find_zero_lcc_1sp_to_2sp_f(n, false, K, ec)); + double phi1Deg = RadToDeg(phi1); + double phi2Deg = RadToDeg(phi2); + + // Try to round to hundreth of degree if very close to it + if (std::fabs(phi1Deg * 1000 - std::floor(phi1Deg * 1000 + 0.5)) < + 1e-8) + phi1Deg = floor(phi1Deg * 1000 + 0.5) / 1000; + if (std::fabs(phi2Deg * 1000 - std::floor(phi2Deg * 1000 + 0.5)) < + 1e-8) + phi2Deg = std::floor(phi2Deg * 1000 + 0.5) / 1000; + + // The following improvement is too turn the LCC1SP equivalent of + // EPSG:2154 to the real LCC2SP + // If the computed latitude of origin is close to .0 or .5 degrees + // then check if rounding it to it will get a false northing + // close to an integer + const double FN = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_FALSE_NORTHING); + const double latitudeOfOriginDeg = + latitudeOfOrigin.convertToUnit(common::UnitOfMeasure::DEGREE); + if (std::fabs(latitudeOfOriginDeg * 2 - + std::floor(latitudeOfOriginDeg * 2 + 0.5)) < 0.2) { + const double dfRoundedLatOfOrig = + std::floor(latitudeOfOriginDeg * 2 + 0.5) / 2; + const double m1 = msfn(phi1, e2); + const double t1 = tsfn(phi1, ec); + const double F = m1 / (n * std::pow(t1, n)); + const double a = + geogCRS->ellipsoid()->semiMajorAxis().getSIValue(); + const double tRoundedLatOfOrig = + tsfn(DegToRad(dfRoundedLatOfOrig), ec); + const double FN_correction = + a * F * (std::pow(tRoundedLatOfOrig, n) - std::pow(t0, n)); + const double FN_corrected = FN - FN_correction; + const double FN_corrected_rounded = + std::floor(FN_corrected + 0.5); + if (std::fabs(FN_corrected - FN_corrected_rounded) < 1e-8) { + auto conv = createLambertConicConformal_2SP( + util::PropertyMap(), + common::Angle(dfRoundedLatOfOrig, + common::UnitOfMeasure::DEGREE), + common::Angle(parameterValueMeasure( + EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN)), + common::Angle(phi1Deg, common::UnitOfMeasure::DEGREE), + common::Angle(phi2Deg, common::UnitOfMeasure::DEGREE), + common::Length(parameterValueMeasure( + EPSG_CODE_PARAMETER_FALSE_EASTING)), + common::Length(FN_corrected_rounded)); + conv->setCRSs(this, false); + return conv.as_nullable(); + } + } + + auto conv = createLambertConicConformal_2SP( + util::PropertyMap(), latitudeOfOrigin, + common::Angle(parameterValueMeasure( + EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN)), + common::Angle(phi1Deg, common::UnitOfMeasure::DEGREE), + common::Angle(phi2Deg, common::UnitOfMeasure::DEGREE), + common::Length( + parameterValueMeasure(EPSG_CODE_PARAMETER_FALSE_EASTING)), + common::Length(FN)); + conv->setCRSs(this, false); + return conv.as_nullable(); + } + } + + if (current_epsg_code == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP && + targetEPSGCode == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP) { + // Notations m0, t0, m1, t1, m2, t2 n, F are those of the EPSG guidance + // "1.3.1.1 Lambert Conic Conformal (2SP)" and + // "1.3.1.2 Lambert Conic Conformal (1SP)" and + // or Snyder pages 106-109 + const double phiF = + parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_FALSE_ORIGIN) + .getSIValue(); + const double phi1 = + parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL) + .getSIValue(); + const double phi2 = + parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL) + .getSIValue(); + if (!(std::fabs(phiF) < M_PI / 2)) + return nullptr; + if (!(std::fabs(phi1) < M_PI / 2)) + return nullptr; + if (!(std::fabs(phi2) < M_PI / 2)) + return nullptr; + const double ec = std::sqrt(e2); + const double m1 = msfn(phi1, e2); + const double m2 = msfn(phi2, e2); + const double t1 = tsfn(phi1, ec); + const double t2 = tsfn(phi2, ec); + const double n_denom = std::log(t1) - std::log(t2); + const double n = (std::fabs(n_denom) < 1e-10) + ? std::sin(phi1) + : (std::log(m1) - std::log(m2)) / n_denom; + if (std::fabs(n) < 1e-10) + return nullptr; + const double F = m1 / (n * std::pow(t1, n)); + const double phi0 = std::asin(n); + const double m0 = msfn(phi0, e2); + const double t0 = tsfn(phi0, ec); + const double F0 = m0 / (n * std::pow(t0, n)); + const double k0 = F / F0; + const double a = geogCRS->ellipsoid()->semiMajorAxis().getSIValue(); + const double tF = tsfn(phiF, ec); + const double FN_correction = + a * F * (std::pow(tF, n) - std::pow(t0, n)); + + double phi0Deg = RadToDeg(phi0); + // Try to round to thousandth of degree if very close to it + if (std::fabs(phi0Deg * 1000 - std::floor(phi0Deg * 1000 + 0.5)) < 1e-8) + phi0Deg = std::floor(phi0Deg * 1000 + 0.5) / 1000; + + auto conv = createLambertConicConformal_1SP( + util::PropertyMap(), + common::Angle(phi0Deg, common::UnitOfMeasure::DEGREE), + common::Angle(parameterValueMeasure( + EPSG_CODE_PARAMETER_LONGITUDE_FALSE_ORIGIN)), + common::Scale(k0), common::Length(parameterValueMeasure( + EPSG_CODE_PARAMETER_EASTING_FALSE_ORIGIN)), + common::Length( + parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_NORTHING_FALSE_ORIGIN) + + (std::fabs(FN_correction) > 1e-8 ? FN_correction : 0))); + conv->setCRSs(this, false); + return conv.as_nullable(); + } + + return nullptr; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +static const ESRIMethodMapping *getESRIMapping(const std::string &wkt2_name, + int epsg_code) { + size_t nEsriMappings = 0; + const auto esriMappings = getEsriMappings(nEsriMappings); + for (size_t i = 0; i < nEsriMappings; ++i) { + const auto &mapping = esriMappings[i]; + if ((epsg_code != 0 && mapping.epsg_code == epsg_code) || + ci_equal(wkt2_name, mapping.wkt2_name)) { + return &mapping; + } + } + return nullptr; +} + +// --------------------------------------------------------------------------- + +static void getESRIMethodNameAndParams(const Conversion *conv, + const std::string &methodName, + int methodEPSGCode, + const char *&esriMethodName, + const ESRIParamMapping *&esriParams) { + esriParams = nullptr; + esriMethodName = nullptr; + const auto *esriMapping = getESRIMapping(methodName, methodEPSGCode); + const auto l_targetCRS = conv->targetCRS(); + if (esriMapping) { + esriParams = esriMapping->params; + esriMethodName = esriMapping->esri_name; + if (esriMapping->epsg_code == + EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL || + esriMapping->epsg_code == + EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL_SPHERICAL) { + if (l_targetCRS && + ci_find(l_targetCRS->nameStr(), "Plate Carree") != + std::string::npos && + conv->parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN) == 0.0) { + esriParams = paramsESRI_Plate_Carree; + esriMethodName = "Plate_Carree"; + } else { + esriParams = paramsESRI_Equidistant_Cylindrical; + esriMethodName = "Equidistant_Cylindrical"; + } + } else if (esriMapping->epsg_code == + EPSG_CODE_METHOD_TRANSVERSE_MERCATOR) { + if (ci_find(conv->nameStr(), "Gauss Kruger") != std::string::npos || + (l_targetCRS && (ci_find(l_targetCRS->nameStr(), "Gauss") != + std::string::npos || + ci_find(l_targetCRS->nameStr(), "GK_") != + std::string::npos))) { + esriParams = paramsESRI_Gauss_Kruger; + esriMethodName = "Gauss_Kruger"; + } else { + esriParams = paramsESRI_Transverse_Mercator; + esriMethodName = "Transverse_Mercator"; + } + } else if (esriMapping->epsg_code == + EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_A) { + if (std::abs( + conv->parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE) - + conv->parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID)) < + 1e-15) { + esriParams = + paramsESRI_Hotine_Oblique_Mercator_Azimuth_Natural_Origin; + esriMethodName = + "Hotine_Oblique_Mercator_Azimuth_Natural_Origin"; + } else { + esriParams = + paramsESRI_Rectified_Skew_Orthomorphic_Natural_Origin; + esriMethodName = "Rectified_Skew_Orthomorphic_Natural_Origin"; + } + } else if (esriMapping->epsg_code == + EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_B) { + if (std::abs( + conv->parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE) - + conv->parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID)) < + 1e-15) { + esriParams = paramsESRI_Hotine_Oblique_Mercator_Azimuth_Center; + esriMethodName = "Hotine_Oblique_Mercator_Azimuth_Center"; + } else { + esriParams = paramsESRI_Rectified_Skew_Orthomorphic_Center; + esriMethodName = "Rectified_Skew_Orthomorphic_Center"; + } + } else if (esriMapping->epsg_code == + EPSG_CODE_METHOD_POLAR_STEREOGRAPHIC_VARIANT_B) { + if (conv->parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_LATITUDE_STD_PARALLEL) > 0) { + esriMethodName = "Stereographic_North_Pole"; + } else { + esriMethodName = "Stereographic_South_Pole"; + } + } + } +} + +// --------------------------------------------------------------------------- + +const char *Conversion::getESRIMethodName() const { + const auto &l_method = method(); + const auto &methodName = l_method->nameStr(); + const auto methodEPSGCode = l_method->getEPSGCode(); + const ESRIParamMapping *esriParams = nullptr; + const char *esriMethodName = nullptr; + getESRIMethodNameAndParams(this, methodName, methodEPSGCode, esriMethodName, + esriParams); + return esriMethodName; +} + +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +const char *Conversion::getWKT1GDALMethodName() const { + const auto &l_method = method(); + const auto methodEPSGCode = l_method->getEPSGCode(); + if (methodEPSGCode == + EPSG_CODE_METHOD_POPULAR_VISUALISATION_PSEUDO_MERCATOR) { + return "Mercator_1SP"; + } + const MethodMapping *mapping = getMapping(l_method.get()); + return mapping ? mapping->wkt1_name : nullptr; +} + +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +void Conversion::_exportToWKT(io::WKTFormatter *formatter) const { + const auto &l_method = method(); + const auto &methodName = l_method->nameStr(); + const auto methodEPSGCode = l_method->getEPSGCode(); + const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; + + if (!isWKT2 && formatter->useESRIDialect()) { + if (methodEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_A) { + auto eqConv = + convertToOtherMethod(EPSG_CODE_METHOD_MERCATOR_VARIANT_B); + if (eqConv) { + eqConv->_exportToWKT(formatter); + return; + } + } + } + + if (isWKT2) { + formatter->startNode(formatter->useDerivingConversion() + ? io::WKTConstants::DERIVINGCONVERSION + : io::WKTConstants::CONVERSION, + !identifiers().empty()); + formatter->addQuotedString(nameStr()); + } else { + formatter->enter(); + formatter->pushOutputUnit(false); + formatter->pushOutputId(false); + } + +#ifdef DEBUG_CONVERSION_ID + if (sourceCRS() && targetCRS()) { + formatter->startNode("SOURCECRS_ID", false); + sourceCRS()->formatID(formatter); + formatter->endNode(); + formatter->startNode("TARGETCRS_ID", false); + targetCRS()->formatID(formatter); + formatter->endNode(); + } +#endif + + bool bAlreadyWritten = false; + if (!isWKT2 && formatter->useESRIDialect()) { + const ESRIParamMapping *esriParams = nullptr; + const char *esriMethodName = nullptr; + getESRIMethodNameAndParams(this, methodName, methodEPSGCode, + esriMethodName, esriParams); + if (esriMethodName && esriParams) { + formatter->startNode(io::WKTConstants::PROJECTION, false); + formatter->addQuotedString(esriMethodName); + formatter->endNode(); + + for (int i = 0; esriParams[i].esri_name != nullptr; i++) { + const auto &esriParam = esriParams[i]; + formatter->startNode(io::WKTConstants::PARAMETER, false); + formatter->addQuotedString(esriParam.esri_name); + if (esriParam.wkt2_name) { + const auto &pv = parameterValue(esriParam.wkt2_name, + esriParam.epsg_code); + if (pv && pv->type() == ParameterValue::Type::MEASURE) { + const auto &v = pv->value(); + // as we don't output the natural unit, output + // to the registered linear / angular unit. + const auto &unitType = v.unit().type(); + if (unitType == common::UnitOfMeasure::Type::LINEAR) { + formatter->add(v.convertToUnit( + *(formatter->axisLinearUnit()))); + } else if (unitType == + common::UnitOfMeasure::Type::ANGULAR) { + const auto &angUnit = + *(formatter->axisAngularUnit()); + double val = v.convertToUnit(angUnit); + if (angUnit == common::UnitOfMeasure::DEGREE) { + if (val > 180.0) { + val -= 360.0; + } else if (val < -180.0) { + val += 360.0; + } + } + formatter->add(val); + } else { + formatter->add(v.getSIValue()); + } + } else if (ci_find(esriParam.esri_name, "scale") != + std::string::npos) { + formatter->add(1.0); + } else { + formatter->add(0.0); + } + } else { + formatter->add(esriParam.fixed_value); + } + formatter->endNode(); + } + bAlreadyWritten = true; + } + } else if (!isWKT2) { + if (methodEPSGCode == + EPSG_CODE_METHOD_POPULAR_VISUALISATION_PSEUDO_MERCATOR) { + const double latitudeOrigin = parameterValueNumeric( + EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN, + common::UnitOfMeasure::DEGREE); + if (latitudeOrigin != 0) { + throw io::FormattingException( + std::string("Unsupported value for ") + + EPSG_NAME_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN); + } + + bAlreadyWritten = true; + formatter->startNode(io::WKTConstants::PROJECTION, false); + formatter->addQuotedString("Mercator_1SP"); + formatter->endNode(); + + formatter->startNode(io::WKTConstants::PARAMETER, false); + formatter->addQuotedString("central_meridian"); + const double centralMeridian = parameterValueNumeric( + EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN, + common::UnitOfMeasure::DEGREE); + formatter->add(centralMeridian); + formatter->endNode(); + + formatter->startNode(io::WKTConstants::PARAMETER, false); + formatter->addQuotedString("scale_factor"); + formatter->add(1.0); + formatter->endNode(); + + formatter->startNode(io::WKTConstants::PARAMETER, false); + formatter->addQuotedString("false_easting"); + const double falseEasting = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_FALSE_EASTING); + formatter->add(falseEasting); + formatter->endNode(); + + formatter->startNode(io::WKTConstants::PARAMETER, false); + formatter->addQuotedString("false_northing"); + const double falseNorthing = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_FALSE_NORTHING); + formatter->add(falseNorthing); + formatter->endNode(); + } else if (starts_with(methodName, "PROJ ")) { + bAlreadyWritten = true; + formatter->startNode(io::WKTConstants::PROJECTION, false); + formatter->addQuotedString("custom_proj4"); + formatter->endNode(); + } + } + + if (!bAlreadyWritten) { + l_method->_exportToWKT(formatter); + + const MethodMapping *mapping = + !isWKT2 ? getMapping(l_method.get()) : nullptr; + for (const auto &genOpParamvalue : parameterValues()) { + + // EPSG has normally no Latitude of natural origin for Equidistant + // Cylindrical but PROJ can handle it, so output the parameter if + // not zero + if ((methodEPSGCode == EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL || + methodEPSGCode == + EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL_SPHERICAL)) { + auto opParamvalue = + dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue && + opParamvalue->parameter()->getEPSGCode() == + EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN) { + const auto ¶mValue = opParamvalue->parameterValue(); + if (paramValue->type() == ParameterValue::Type::MEASURE) { + const auto &measure = paramValue->value(); + if (measure.getSIValue() == 0) { + continue; + } + } + } + } + // Same for false easting / false northing for Vertical Perspective + else if (methodEPSGCode == EPSG_CODE_METHOD_VERTICAL_PERSPECTIVE) { + auto opParamvalue = + dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue) { + const auto paramEPSGCode = + opParamvalue->parameter()->getEPSGCode(); + if (paramEPSGCode == EPSG_CODE_PARAMETER_FALSE_EASTING || + paramEPSGCode == EPSG_CODE_PARAMETER_FALSE_NORTHING) { + const auto ¶mValue = opParamvalue->parameterValue(); + if (paramValue->type() == + ParameterValue::Type::MEASURE) { + const auto &measure = paramValue->value(); + if (measure.getSIValue() == 0) { + continue; + } + } + } + } + } + genOpParamvalue->_exportToWKT(formatter, mapping); + } + } + + if (isWKT2) { + if (formatter->outputId()) { + formatID(formatter); + } + formatter->endNode(); + } else { + formatter->popOutputUnit(); + formatter->popOutputId(); + formatter->leave(); + } +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void Conversion::_exportToJSON( + io::JSONFormatter *formatter) const // throw(FormattingException) +{ + auto writer = formatter->writer(); + auto objectContext( + formatter->MakeObjectContext("Conversion", !identifiers().empty())); + + writer->AddObjKey("name"); + auto l_name = nameStr(); + if (l_name.empty()) { + writer->Add("unnamed"); + } else { + writer->Add(l_name); + } + + writer->AddObjKey("method"); + formatter->setOmitTypeInImmediateChild(); + formatter->setAllowIDInImmediateChild(); + method()->_exportToJSON(formatter); + + const auto &l_parameterValues = parameterValues(); + if (!l_parameterValues.empty()) { + writer->AddObjKey("parameters"); + { + auto parametersContext(writer->MakeArrayContext(false)); + for (const auto &genOpParamvalue : l_parameterValues) { + formatter->setAllowIDInImmediateChild(); + formatter->setOmitTypeInImmediateChild(); + genOpParamvalue->_exportToJSON(formatter); + } + } + } + + if (formatter->outputId()) { + formatID(formatter); + } +} + +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static bool createPROJ4WebMercator(const Conversion *conv, + io::PROJStringFormatter *formatter) { + const double centralMeridian = conv->parameterValueNumeric( + EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN, + common::UnitOfMeasure::DEGREE); + + const double falseEasting = + conv->parameterValueNumericAsSI(EPSG_CODE_PARAMETER_FALSE_EASTING); + + const double falseNorthing = + conv->parameterValueNumericAsSI(EPSG_CODE_PARAMETER_FALSE_NORTHING); + + auto sourceCRS = conv->sourceCRS(); + auto geogCRS = dynamic_cast(sourceCRS.get()); + if (!geogCRS) { + return false; + } + + std::string units("m"); + auto targetCRS = conv->targetCRS(); + auto targetProjCRS = + dynamic_cast(targetCRS.get()); + if (targetProjCRS) { + const auto &axisList = targetProjCRS->coordinateSystem()->axisList(); + const auto &unit = axisList[0]->unit(); + if (!unit._isEquivalentTo(common::UnitOfMeasure::METRE, + util::IComparable::Criterion::EQUIVALENT)) { + auto projUnit = unit.exportToPROJString(); + if (!projUnit.empty()) { + units = projUnit; + } else { + return false; + } + } + } + + formatter->addStep("merc"); + const double a = geogCRS->ellipsoid()->semiMajorAxis().getSIValue(); + formatter->addParam("a", a); + formatter->addParam("b", a); + formatter->addParam("lat_ts", 0.0); + formatter->addParam("lon_0", centralMeridian); + formatter->addParam("x_0", falseEasting); + formatter->addParam("y_0", falseNorthing); + formatter->addParam("k", 1.0); + formatter->addParam("units", units); + formatter->addParam("nadgrids", "@null"); + formatter->addParam("wktext"); + formatter->addParam("no_defs"); + return true; +} + +// --------------------------------------------------------------------------- + +static bool +createPROJExtensionFromCustomProj(const Conversion *conv, + io::PROJStringFormatter *formatter, + bool forExtensionNode) { + const auto &methodName = conv->method()->nameStr(); + assert(starts_with(methodName, "PROJ ")); + auto tokens = split(methodName, ' '); + + formatter->addStep(tokens[1]); + + if (forExtensionNode) { + auto sourceCRS = conv->sourceCRS(); + auto geogCRS = + dynamic_cast(sourceCRS.get()); + if (!geogCRS) { + return false; + } + geogCRS->addDatumInfoToPROJString(formatter); + } + + for (size_t i = 2; i < tokens.size(); i++) { + auto kv = split(tokens[i], '='); + if (kv.size() == 2) { + formatter->addParam(kv[0], kv[1]); + } else { + formatter->addParam(tokens[i]); + } + } + + for (const auto &genOpParamvalue : conv->parameterValues()) { + auto opParamvalue = dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue) { + const auto ¶mName = opParamvalue->parameter()->nameStr(); + const auto ¶mValue = opParamvalue->parameterValue(); + if (paramValue->type() == ParameterValue::Type::MEASURE) { + const auto &measure = paramValue->value(); + const auto unitType = measure.unit().type(); + if (unitType == common::UnitOfMeasure::Type::LINEAR) { + formatter->addParam(paramName, measure.getSIValue()); + } else if (unitType == common::UnitOfMeasure::Type::ANGULAR) { + formatter->addParam( + paramName, + measure.convertToUnit(common::UnitOfMeasure::DEGREE)); + } else { + formatter->addParam(paramName, measure.value()); + } + } + } + } + + if (forExtensionNode) { + formatter->addParam("wktext"); + formatter->addParam("no_defs"); + } + return true; +} +//! @endcond + +// --------------------------------------------------------------------------- + +bool Conversion::addWKTExtensionNode(io::WKTFormatter *formatter) const { + const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; + if (!isWKT2) { + const auto &l_method = method(); + const auto &methodName = l_method->nameStr(); + const int methodEPSGCode = l_method->getEPSGCode(); + if (l_method->getPrivate()->projMethodOverride_ == "tmerc approx" || + l_method->getPrivate()->projMethodOverride_ == "utm approx") { + auto projFormatter = io::PROJStringFormatter::create(); + projFormatter->setCRSExport(true); + projFormatter->setUseApproxTMerc(true); + formatter->startNode(io::WKTConstants::EXTENSION, false); + formatter->addQuotedString("PROJ4"); + _exportToPROJString(projFormatter.get()); + projFormatter->addParam("no_defs"); + formatter->addQuotedString(projFormatter->toString()); + formatter->endNode(); + return true; + } else if (methodEPSGCode == + EPSG_CODE_METHOD_POPULAR_VISUALISATION_PSEUDO_MERCATOR || + nameStr() == "Popular Visualisation Mercator") { + + auto projFormatter = io::PROJStringFormatter::create(); + projFormatter->setCRSExport(true); + if (createPROJ4WebMercator(this, projFormatter.get())) { + formatter->startNode(io::WKTConstants::EXTENSION, false); + formatter->addQuotedString("PROJ4"); + formatter->addQuotedString(projFormatter->toString()); + formatter->endNode(); + return true; + } + } else if (starts_with(methodName, "PROJ ")) { + auto projFormatter = io::PROJStringFormatter::create(); + projFormatter->setCRSExport(true); + if (createPROJExtensionFromCustomProj(this, projFormatter.get(), + true)) { + formatter->startNode(io::WKTConstants::EXTENSION, false); + formatter->addQuotedString("PROJ4"); + formatter->addQuotedString(projFormatter->toString()); + formatter->endNode(); + return true; + } + } else if (methodName == + PROJ_WKT2_NAME_METHOD_GEOSTATIONARY_SATELLITE_SWEEP_X) { + auto projFormatter = io::PROJStringFormatter::create(); + projFormatter->setCRSExport(true); + formatter->startNode(io::WKTConstants::EXTENSION, false); + formatter->addQuotedString("PROJ4"); + _exportToPROJString(projFormatter.get()); + projFormatter->addParam("no_defs"); + formatter->addQuotedString(projFormatter->toString()); + formatter->endNode(); + return true; + } + } + return false; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void Conversion::_exportToPROJString( + io::PROJStringFormatter *formatter) const // throw(FormattingException) +{ + const auto &l_method = method(); + const auto &methodName = l_method->nameStr(); + const int methodEPSGCode = l_method->getEPSGCode(); + const bool isZUnitConversion = + methodEPSGCode == EPSG_CODE_METHOD_CHANGE_VERTICAL_UNIT; + const bool isAffineParametric = + methodEPSGCode == EPSG_CODE_METHOD_AFFINE_PARAMETRIC_TRANSFORMATION; + const bool isGeographicGeocentric = + methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC_GEOCENTRIC; + const bool isHeightDepthReversal = + methodEPSGCode == EPSG_CODE_METHOD_HEIGHT_DEPTH_REVERSAL; + const bool applySourceCRSModifiers = + !isZUnitConversion && !isAffineParametric && + !isAxisOrderReversal(methodEPSGCode) && !isGeographicGeocentric && + !isHeightDepthReversal; + bool applyTargetCRSModifiers = applySourceCRSModifiers; + + auto l_sourceCRS = sourceCRS(); + if (!formatter->getCRSExport() && l_sourceCRS && applySourceCRSModifiers) { + + crs::CRS *horiz = l_sourceCRS.get(); + const auto compound = dynamic_cast(horiz); + if (compound) { + const auto &components = compound->componentReferenceSystems(); + if (!components.empty()) { + horiz = components.front().get(); + } + } + + auto geogCRS = dynamic_cast(horiz); + if (geogCRS) { + formatter->setOmitProjLongLatIfPossible(true); + formatter->startInversion(); + geogCRS->_exportToPROJString(formatter); + formatter->stopInversion(); + formatter->setOmitProjLongLatIfPossible(false); + } + + auto projCRS = dynamic_cast(horiz); + if (projCRS) { + formatter->startInversion(); + formatter->pushOmitZUnitConversion(); + projCRS->addUnitConvertAndAxisSwap(formatter, false); + formatter->popOmitZUnitConversion(); + formatter->stopInversion(); + } + } + + const auto &convName = nameStr(); + bool bConversionDone = false; + bool bEllipsoidParametersDone = false; + bool useApprox = false; + if (methodEPSGCode == EPSG_CODE_METHOD_TRANSVERSE_MERCATOR) { + // Check for UTM + int zone = 0; + bool north = true; + useApprox = + formatter->getUseApproxTMerc() || + l_method->getPrivate()->projMethodOverride_ == "tmerc approx" || + l_method->getPrivate()->projMethodOverride_ == "utm approx"; + if (isUTM(zone, north)) { + bConversionDone = true; + formatter->addStep("utm"); + if (useApprox) { + formatter->addParam("approx"); + } + formatter->addParam("zone", zone); + if (!north) { + formatter->addParam("south"); + } + } + } else if (methodEPSGCode == + EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_A) { + const double azimuth = + parameterValueNumeric(EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE, + common::UnitOfMeasure::DEGREE); + const double angleRectifiedToSkewGrid = parameterValueNumeric( + EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID, + common::UnitOfMeasure::DEGREE); + // Map to Swiss Oblique Mercator / somerc + if (std::fabs(azimuth - 90) < 1e-4 && + std::fabs(angleRectifiedToSkewGrid - 90) < 1e-4) { + bConversionDone = true; + formatter->addStep("somerc"); + formatter->addParam( + "lat_0", parameterValueNumeric( + EPSG_CODE_PARAMETER_LATITUDE_PROJECTION_CENTRE, + common::UnitOfMeasure::DEGREE)); + formatter->addParam( + "lon_0", parameterValueNumeric( + EPSG_CODE_PARAMETER_LONGITUDE_PROJECTION_CENTRE, + common::UnitOfMeasure::DEGREE)); + formatter->addParam( + "k_0", parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_SCALE_FACTOR_INITIAL_LINE)); + formatter->addParam("x_0", parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_FALSE_EASTING)); + formatter->addParam("y_0", parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_FALSE_NORTHING)); + } + } else if (methodEPSGCode == + EPSG_CODE_METHOD_HOTINE_OBLIQUE_MERCATOR_VARIANT_B) { + const double azimuth = + parameterValueNumeric(EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE, + common::UnitOfMeasure::DEGREE); + const double angleRectifiedToSkewGrid = parameterValueNumeric( + EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID, + common::UnitOfMeasure::DEGREE); + // Map to Swiss Oblique Mercator / somerc + if (std::fabs(azimuth - 90) < 1e-4 && + std::fabs(angleRectifiedToSkewGrid - 90) < 1e-4) { + bConversionDone = true; + formatter->addStep("somerc"); + formatter->addParam( + "lat_0", parameterValueNumeric( + EPSG_CODE_PARAMETER_LATITUDE_PROJECTION_CENTRE, + common::UnitOfMeasure::DEGREE)); + formatter->addParam( + "lon_0", parameterValueNumeric( + EPSG_CODE_PARAMETER_LONGITUDE_PROJECTION_CENTRE, + common::UnitOfMeasure::DEGREE)); + formatter->addParam( + "k_0", parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_SCALE_FACTOR_INITIAL_LINE)); + formatter->addParam( + "x_0", parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_EASTING_PROJECTION_CENTRE)); + formatter->addParam( + "y_0", parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_NORTHING_PROJECTION_CENTRE)); + } + } else if (methodEPSGCode == EPSG_CODE_METHOD_KROVAK_NORTH_ORIENTED) { + double colatitude = + parameterValueNumeric(EPSG_CODE_PARAMETER_COLATITUDE_CONE_AXIS, + common::UnitOfMeasure::DEGREE); + double latitudePseudoStandardParallel = parameterValueNumeric( + EPSG_CODE_PARAMETER_LATITUDE_PSEUDO_STANDARD_PARALLEL, + common::UnitOfMeasure::DEGREE); + // 30deg 17' 17.30311'' = 30.28813975277777776 + // 30deg 17' 17.303'' = 30.288139722222223 as used in GDAL WKT1 + if (std::fabs(colatitude - 30.2881397) > 1e-7) { + throw io::FormattingException( + std::string("Unsupported value for ") + + EPSG_NAME_PARAMETER_COLATITUDE_CONE_AXIS); + } + if (std::fabs(latitudePseudoStandardParallel - 78.5) > 1e-8) { + throw io::FormattingException( + std::string("Unsupported value for ") + + EPSG_NAME_PARAMETER_LATITUDE_PSEUDO_STANDARD_PARALLEL); + } + } else if (methodEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_A) { + double latitudeOrigin = parameterValueNumeric( + EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN, + common::UnitOfMeasure::DEGREE); + if (latitudeOrigin != 0) { + throw io::FormattingException( + std::string("Unsupported value for ") + + EPSG_NAME_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN); + } + } else if (methodEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_B) { + const auto &scaleFactor = parameterValueMeasure(WKT1_SCALE_FACTOR, 0); + if (scaleFactor.unit().type() != common::UnitOfMeasure::Type::UNKNOWN && + std::fabs(scaleFactor.getSIValue() - 1.0) > 1e-10) { + throw io::FormattingException( + "Unexpected presence of scale factor in Mercator (variant B)"); + } + double latitudeOrigin = parameterValueNumeric( + EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN, + common::UnitOfMeasure::DEGREE); + if (latitudeOrigin != 0) { + throw io::FormattingException( + std::string("Unsupported value for ") + + EPSG_NAME_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN); + } + } else if (methodEPSGCode == + EPSG_CODE_METHOD_TRANSVERSE_MERCATOR_SOUTH_ORIENTATED) { + // We map TMSO to tmerc with axis=wsu. This only works if false easting + // and northings are zero, which is the case in practice for South + // African and Namibian EPSG CRS + const auto falseEasting = parameterValueNumeric( + EPSG_CODE_PARAMETER_FALSE_EASTING, common::UnitOfMeasure::METRE); + if (falseEasting != 0) { + throw io::FormattingException( + std::string("Unsupported value for ") + + EPSG_NAME_PARAMETER_FALSE_EASTING); + } + const auto falseNorthing = parameterValueNumeric( + EPSG_CODE_PARAMETER_FALSE_NORTHING, common::UnitOfMeasure::METRE); + if (falseNorthing != 0) { + throw io::FormattingException( + std::string("Unsupported value for ") + + EPSG_NAME_PARAMETER_FALSE_NORTHING); + } + // PROJ.4 specific hack for webmercator + } else if (formatter->getCRSExport() && + methodEPSGCode == + EPSG_CODE_METHOD_POPULAR_VISUALISATION_PSEUDO_MERCATOR) { + if (!createPROJ4WebMercator(this, formatter)) { + throw io::FormattingException( + std::string("Cannot export ") + + EPSG_NAME_METHOD_POPULAR_VISUALISATION_PSEUDO_MERCATOR + + " as PROJ.4 string outside of a ProjectedCRS context"); + } + bConversionDone = true; + bEllipsoidParametersDone = true; + applyTargetCRSModifiers = false; + } else if (ci_equal(convName, "Popular Visualisation Mercator")) { + if (formatter->getCRSExport()) { + if (!createPROJ4WebMercator(this, formatter)) { + throw io::FormattingException(concat( + "Cannot export ", convName, + " as PROJ.4 string outside of a ProjectedCRS context")); + } + applyTargetCRSModifiers = false; + } else { + formatter->addStep("webmerc"); + if (l_sourceCRS) { + datum::Ellipsoid::WGS84->_exportToPROJString(formatter); + } + } + bConversionDone = true; + bEllipsoidParametersDone = true; + } else if (starts_with(methodName, "PROJ ")) { + bConversionDone = true; + createPROJExtensionFromCustomProj(this, formatter, false); + } else if (ci_equal(methodName, + PROJ_WKT2_NAME_METHOD_POLE_ROTATION_GRIB_CONVENTION)) { + double southPoleLat = parameterValueNumeric( + PROJ_WKT2_NAME_PARAMETER_SOUTH_POLE_LATITUDE_GRIB_CONVENTION, + common::UnitOfMeasure::DEGREE); + double southPoleLon = parameterValueNumeric( + PROJ_WKT2_NAME_PARAMETER_SOUTH_POLE_LONGITUDE_GRIB_CONVENTION, + common::UnitOfMeasure::DEGREE); + double rotation = parameterValueNumeric( + PROJ_WKT2_NAME_PARAMETER_AXIS_ROTATION_GRIB_CONVENTION, + common::UnitOfMeasure::DEGREE); + formatter->addStep("ob_tran"); + formatter->addParam("o_proj", "longlat"); + formatter->addParam("o_lon_p", -rotation); + formatter->addParam("o_lat_p", -southPoleLat); + formatter->addParam("lon_0", southPoleLon); + bConversionDone = true; + } else if (ci_equal(methodName, "Adams_Square_II")) { + // Look for ESRI method and parameter names (to be opposed + // to the OGC WKT2 names we use elsewhere, because there's no mapping + // of those parameters to OGC WKT2) + // We also reject non-default values for a number of parameters, + // because they are not implemented on PROJ side. The subset we + // support can handle ESRI:54098 WGS_1984_Adams_Square_II, but not + // ESRI:54099 WGS_1984_Spilhaus_Ocean_Map_in_Square + const double falseEasting = parameterValueNumeric( + "False_Easting", common::UnitOfMeasure::METRE); + const double falseNorthing = parameterValueNumeric( + "False_Northing", common::UnitOfMeasure::METRE); + const double scaleFactor = + parameterValue("Scale_Factor", 0) + ? parameterValueNumeric("Scale_Factor", + common::UnitOfMeasure::SCALE_UNITY) + : 1.0; + const double azimuth = + parameterValueNumeric("Azimuth", common::UnitOfMeasure::DEGREE); + const double longitudeOfCenter = parameterValueNumeric( + "Longitude_Of_Center", common::UnitOfMeasure::DEGREE); + const double latitudeOfCenter = parameterValueNumeric( + "Latitude_Of_Center", common::UnitOfMeasure::DEGREE); + const double XYPlaneRotation = parameterValueNumeric( + "XY_Plane_Rotation", common::UnitOfMeasure::DEGREE); + if (scaleFactor != 1.0 || azimuth != 0.0 || latitudeOfCenter != 0.0 || + XYPlaneRotation != 0.0) { + throw io::FormattingException("Unsupported value for one or " + "several parameters of " + "Adams_Square_II"); + } + formatter->addStep("adams_ws2"); + formatter->addParam("lon_0", longitudeOfCenter); + formatter->addParam("x_0", falseEasting); + formatter->addParam("y_0", falseNorthing); + bConversionDone = true; + } else if (formatter->convention() == + io::PROJStringFormatter::Convention::PROJ_5 && + isZUnitConversion) { + double convFactor = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR); + auto uom = common::UnitOfMeasure(std::string(), convFactor, + common::UnitOfMeasure::Type::LINEAR) + .exportToPROJString(); + auto reverse_uom = + convFactor == 0.0 + ? std::string() + : common::UnitOfMeasure(std::string(), 1.0 / convFactor, + common::UnitOfMeasure::Type::LINEAR) + .exportToPROJString(); + if (uom == "m") { + // do nothing + } else if (!uom.empty()) { + formatter->addStep("unitconvert"); + formatter->addParam("z_in", uom); + formatter->addParam("z_out", "m"); + } else if (!reverse_uom.empty()) { + formatter->addStep("unitconvert"); + formatter->addParam("z_in", "m"); + formatter->addParam("z_out", reverse_uom); + } else { + formatter->addStep("affine"); + formatter->addParam("s33", convFactor); + } + bConversionDone = true; + bEllipsoidParametersDone = true; + } + + auto l_targetCRS = targetCRS(); + + bool bAxisSpecFound = false; + if (!bConversionDone) { + const MethodMapping *mapping = getMapping(l_method.get()); + if (mapping && mapping->proj_name_main) { + formatter->addStep(mapping->proj_name_main); + if (useApprox) { + formatter->addParam("approx"); + } + if (mapping->proj_name_aux) { + bool addAux = true; + if (internal::starts_with(mapping->proj_name_aux, "axis=")) { + if (mapping->epsg_code == EPSG_CODE_METHOD_KROVAK) { + auto projCRS = dynamic_cast( + l_targetCRS.get()); + if (projCRS) { + const auto &axisList = + projCRS->coordinateSystem()->axisList(); + if (axisList[0]->direction() == + cs::AxisDirection::WEST && + axisList[1]->direction() == + cs::AxisDirection::SOUTH) { + formatter->addParam("czech"); + addAux = false; + } + } + } + bAxisSpecFound = true; + } + + // No need to add explicit f=0 if the ellipsoid is a sphere + if (strcmp(mapping->proj_name_aux, "f=0") == 0) { + crs::CRS *horiz = l_sourceCRS.get(); + const auto compound = + dynamic_cast(horiz); + if (compound) { + const auto &components = + compound->componentReferenceSystems(); + if (!components.empty()) { + horiz = components.front().get(); + } + } + + auto geogCRS = + dynamic_cast(horiz); + if (geogCRS && geogCRS->ellipsoid()->isSphere()) { + addAux = false; + } + } + + if (addAux) { + auto kv = split(mapping->proj_name_aux, '='); + if (kv.size() == 2) { + formatter->addParam(kv[0], kv[1]); + } else { + formatter->addParam(mapping->proj_name_aux); + } + } + } + + if (mapping->epsg_code == + EPSG_CODE_METHOD_POLAR_STEREOGRAPHIC_VARIANT_B) { + double latitudeStdParallel = parameterValueNumeric( + EPSG_CODE_PARAMETER_LATITUDE_STD_PARALLEL, + common::UnitOfMeasure::DEGREE); + formatter->addParam("lat_0", + (latitudeStdParallel >= 0) ? 90.0 : -90.0); + } + + for (int i = 0; mapping->params[i] != nullptr; i++) { + const auto *param = mapping->params[i]; + if (!param->proj_name) { + continue; + } + const auto &value = + parameterValueMeasure(param->wkt2_name, param->epsg_code); + double valueConverted = 0; + if (value == nullMeasure) { + // Deal with missing values. In an ideal world, this would + // not happen + if (param->epsg_code == + EPSG_CODE_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN) { + valueConverted = 1.0; + } + } else if (param->unit_type == + common::UnitOfMeasure::Type::ANGULAR) { + valueConverted = + value.convertToUnit(common::UnitOfMeasure::DEGREE); + } else { + valueConverted = value.getSIValue(); + } + + if (mapping->epsg_code == + EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP && + strcmp(param->proj_name, "lat_1") == 0) { + formatter->addParam(param->proj_name, valueConverted); + formatter->addParam("lat_0", valueConverted); + } else { + formatter->addParam(param->proj_name, valueConverted); + } + } + + } else { + if (!exportToPROJStringGeneric(formatter)) { + throw io::FormattingException( + concat("Unsupported conversion method: ", methodName)); + } + } + } + + if (l_targetCRS && applyTargetCRSModifiers) { + crs::CRS *horiz = l_targetCRS.get(); + const auto compound = dynamic_cast(horiz); + if (compound) { + const auto &components = compound->componentReferenceSystems(); + if (!components.empty()) { + horiz = components.front().get(); + } + } + + if (!bEllipsoidParametersDone) { + auto targetGeogCRS = horiz->extractGeographicCRS(); + if (targetGeogCRS) { + if (formatter->getCRSExport()) { + targetGeogCRS->addDatumInfoToPROJString(formatter); + } else { + targetGeogCRS->ellipsoid()->_exportToPROJString(formatter); + targetGeogCRS->primeMeridian()->_exportToPROJString( + formatter); + } + } + } + + auto projCRS = dynamic_cast(horiz); + if (projCRS) { + formatter->pushOmitZUnitConversion(); + projCRS->addUnitConvertAndAxisSwap(formatter, bAxisSpecFound); + formatter->popOmitZUnitConversion(); + } + + auto derivedGeographicCRS = + dynamic_cast(horiz); + if (!formatter->getCRSExport() && derivedGeographicCRS) { + formatter->setOmitProjLongLatIfPossible(true); + derivedGeographicCRS->addAngularUnitConvertAndAxisSwap(formatter); + formatter->setOmitProjLongLatIfPossible(false); + } + } +} +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Return whether a conversion is a [Universal Transverse Mercator] + * (https://proj.org/operations/projections/utm.html) conversion. + * + * @param[out] zone UTM zone number between 1 and 60. + * @param[out] north true for UTM northern hemisphere, false for UTM southern + * hemisphere. + * @return true if it is a UTM conversion. + */ +bool Conversion::isUTM(int &zone, bool &north) const { + zone = 0; + north = true; + + if (method()->getEPSGCode() == EPSG_CODE_METHOD_TRANSVERSE_MERCATOR) { + // Check for UTM + + bool bLatitudeNatOriginUTM = false; + bool bScaleFactorUTM = false; + bool bFalseEastingUTM = false; + bool bFalseNorthingUTM = false; + for (const auto &genOpParamvalue : parameterValues()) { + auto opParamvalue = dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue) { + const auto epsg_code = opParamvalue->parameter()->getEPSGCode(); + const auto &l_parameterValue = opParamvalue->parameterValue(); + if (l_parameterValue->type() == ParameterValue::Type::MEASURE) { + const auto &measure = l_parameterValue->value(); + if (epsg_code == + EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN && + std::fabs(measure.value() - + UTM_LATITUDE_OF_NATURAL_ORIGIN) < 1e-10) { + bLatitudeNatOriginUTM = true; + } else if ( + (epsg_code == + EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN || + epsg_code == + EPSG_CODE_PARAMETER_LONGITUDE_OF_ORIGIN) && + measure.unit()._isEquivalentTo( + common::UnitOfMeasure::DEGREE, + util::IComparable::Criterion::EQUIVALENT)) { + double dfZone = (measure.value() + 183.0) / 6.0; + if (dfZone > 0.9 && dfZone < 60.1 && + std::abs(dfZone - std::round(dfZone)) < 1e-10) { + zone = static_cast(std::lround(dfZone)); + } + } else if ( + epsg_code == + EPSG_CODE_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN && + measure.unit()._isEquivalentTo( + common::UnitOfMeasure::SCALE_UNITY, + util::IComparable::Criterion::EQUIVALENT) && + std::fabs(measure.value() - UTM_SCALE_FACTOR) < 1e-10) { + bScaleFactorUTM = true; + } else if (epsg_code == EPSG_CODE_PARAMETER_FALSE_EASTING && + measure.value() == UTM_FALSE_EASTING && + measure.unit()._isEquivalentTo( + common::UnitOfMeasure::METRE, + util::IComparable::Criterion::EQUIVALENT)) { + bFalseEastingUTM = true; + } else if (epsg_code == + EPSG_CODE_PARAMETER_FALSE_NORTHING && + measure.unit()._isEquivalentTo( + common::UnitOfMeasure::METRE, + util::IComparable::Criterion::EQUIVALENT)) { + if (std::fabs(measure.value() - + UTM_NORTH_FALSE_NORTHING) < 1e-10) { + bFalseNorthingUTM = true; + north = true; + } else if (std::fabs(measure.value() - + UTM_SOUTH_FALSE_NORTHING) < + 1e-10) { + bFalseNorthingUTM = true; + north = false; + } + } + } + } + } + if (bLatitudeNatOriginUTM && zone > 0 && bScaleFactorUTM && + bFalseEastingUTM && bFalseNorthingUTM) { + return true; + } + } + return false; +} + +// --------------------------------------------------------------------------- + +/** \brief Return a Conversion object where some parameters are better + * identified. + * + * @return a new Conversion. + */ +ConversionNNPtr Conversion::identify() const { + auto newConversion = Conversion::nn_make_shared(*this); + newConversion->assignSelf(newConversion); + + if (method()->getEPSGCode() == EPSG_CODE_METHOD_TRANSVERSE_MERCATOR) { + // Check for UTM + int zone = 0; + bool north = true; + if (isUTM(zone, north)) { + newConversion->setProperties( + getUTMConversionProperty(util::PropertyMap(), zone, north)); + } + } + + return newConversion; +} + +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END diff --git a/include/proj/internal/coordinateoperation_internal.hpp b/src/iso19111/operation/coordinateoperation_internal.hpp similarity index 87% rename from include/proj/internal/coordinateoperation_internal.hpp rename to src/iso19111/operation/coordinateoperation_internal.hpp index 322d71b730..d890f71052 100644 --- a/include/proj/internal/coordinateoperation_internal.hpp +++ b/src/iso19111/operation/coordinateoperation_internal.hpp @@ -43,53 +43,6 @@ NS_PROJ_START namespace operation { -struct ParamMapping { - const char *wkt2_name; - const int epsg_code; - const char *wkt1_name; - const common::UnitOfMeasure::Type unit_type; - const char *proj_name; -}; - -struct MethodMapping { - const char *wkt2_name; - const int epsg_code; - const char *wkt1_name; - const char *proj_name_main; - const char *proj_name_aux; - const ParamMapping *const *params; -}; - -const MethodMapping *getMapping(int epsg_code) noexcept; -const MethodMapping *getMappingFromWKT1(const std::string &wkt1_name) noexcept; -const MethodMapping *getMapping(const char *wkt2_name) noexcept; -const MethodMapping *getMapping(const OperationMethod *method) noexcept; -std::vector -getMappingsFromPROJName(const std::string &projName); -const ParamMapping *getMappingFromWKT1(const MethodMapping *mapping, - const std::string &wkt1_name); -bool areEquivalentParameters(const std::string &a, const std::string &b); - -// --------------------------------------------------------------------------- - -struct ESRIParamMapping { - const char *esri_name; - const char *wkt2_name; - int epsg_code; - const char *fixed_value; - bool is_fixed_value; -}; - -struct ESRIMethodMapping { - const char *esri_name; - const char *wkt2_name; - int epsg_code; - const ESRIParamMapping *const params; -}; - -std::vector -getMappingsFromESRI(const std::string &esri_name); - // --------------------------------------------------------------------------- bool isAxisOrderReversal(int methodEPSGCode); @@ -303,6 +256,15 @@ class PROJBasedOperation : public SingleOperation { bool inverse_ = false; }; +// --------------------------------------------------------------------------- + +class InvalidOperationEmptyIntersection : public InvalidOperation { + public: + explicit InvalidOperationEmptyIntersection(const std::string &message); + InvalidOperationEmptyIntersection( + const InvalidOperationEmptyIntersection &other); + ~InvalidOperationEmptyIntersection() override; +}; } // namespace operation NS_PROJ_END diff --git a/src/iso19111/operation/coordinateoperation_private.hpp b/src/iso19111/operation/coordinateoperation_private.hpp new file mode 100644 index 0000000000..42bedd4b6e --- /dev/null +++ b/src/iso19111/operation/coordinateoperation_private.hpp @@ -0,0 +1,88 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef COORDINATEROPERATION_PRIVATE_HPP +#define COORDINATEROPERATION_PRIVATE_HPP + +#include "proj/coordinateoperation.hpp" +#include "proj/util.hpp" + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct CoordinateOperation::Private { + util::optional operationVersion_{}; + std::vector + coordinateOperationAccuracies_{}; + std::weak_ptr sourceCRSWeak_{}; + std::weak_ptr targetCRSWeak_{}; + crs::CRSPtr interpolationCRS_{}; + util::optional sourceCoordinateEpoch_{}; + util::optional targetCoordinateEpoch_{}; + bool hasBallparkTransformation_ = false; + bool use3DHelmert_ = false; + + // do not set this for a ProjectedCRS.definingConversion + struct CRSStrongRef { + crs::CRSNNPtr sourceCRS_; + crs::CRSNNPtr targetCRS_; + CRSStrongRef(const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn) + : sourceCRS_(sourceCRSIn), targetCRS_(targetCRSIn) {} + }; + std::unique_ptr strongRef_{}; + + Private() = default; + Private(const Private &other) + : operationVersion_(other.operationVersion_), + coordinateOperationAccuracies_(other.coordinateOperationAccuracies_), + sourceCRSWeak_(other.sourceCRSWeak_), + targetCRSWeak_(other.targetCRSWeak_), + interpolationCRS_(other.interpolationCRS_), + sourceCoordinateEpoch_(other.sourceCoordinateEpoch_), + targetCoordinateEpoch_(other.targetCoordinateEpoch_), + hasBallparkTransformation_(other.hasBallparkTransformation_), + strongRef_(other.strongRef_ ? internal::make_unique( + *(other.strongRef_)) + : nullptr) {} + + Private &operator=(const Private &) = delete; +}; +//! @endcond + +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END + +#endif // COORDINATEROPERATION_PRIVATE_HPP diff --git a/src/iso19111/operation/coordinateoperationfactory.cpp b/src/iso19111/operation/coordinateoperationfactory.cpp new file mode 100644 index 0000000000..bde654d24f --- /dev/null +++ b/src/iso19111/operation/coordinateoperationfactory.cpp @@ -0,0 +1,5257 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP +#endif + +#include "proj/coordinateoperation.hpp" +#include "proj/common.hpp" +#include "proj/crs.hpp" +#include "proj/io.hpp" +#include "proj/metadata.hpp" +#include "proj/util.hpp" + +#include "proj/internal/internal.hpp" +#include "proj/internal/io_internal.hpp" +#include "proj/internal/tracing.hpp" + +#include "coordinateoperation_internal.hpp" +#include "coordinateoperation_private.hpp" +#include "oputils.hpp" + +// PROJ include order is sensitive +// clang-format off +#include "proj.h" +#include "proj_internal.h" // M_PI +// clang-format on + +#include +#include +#include +#include +#include +#include +#include +#include + +// #define TRACE_CREATE_OPERATIONS +// #define DEBUG_SORT +// #define DEBUG_CONCATENATED_OPERATION +#if defined(DEBUG_SORT) || defined(DEBUG_CONCATENATED_OPERATION) +#include + +void dumpWKT(const NS_PROJ::crs::CRS *crs); +void dumpWKT(const NS_PROJ::crs::CRS *crs) { + auto f(NS_PROJ::io::WKTFormatter::create( + NS_PROJ::io::WKTFormatter::Convention::WKT2_2019)); + std::cerr << crs->exportToWKT(f.get()) << std::endl; +} + +void dumpWKT(const NS_PROJ::crs::CRSPtr &crs); +void dumpWKT(const NS_PROJ::crs::CRSPtr &crs) { dumpWKT(crs.get()); } + +void dumpWKT(const NS_PROJ::crs::CRSNNPtr &crs); +void dumpWKT(const NS_PROJ::crs::CRSNNPtr &crs) { + dumpWKT(crs.as_nullable().get()); +} + +void dumpWKT(const NS_PROJ::crs::GeographicCRSPtr &crs); +void dumpWKT(const NS_PROJ::crs::GeographicCRSPtr &crs) { dumpWKT(crs.get()); } + +void dumpWKT(const NS_PROJ::crs::GeographicCRSNNPtr &crs); +void dumpWKT(const NS_PROJ::crs::GeographicCRSNNPtr &crs) { + dumpWKT(crs.as_nullable().get()); +} + +#endif + +using namespace NS_PROJ::internal; + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +// --------------------------------------------------------------------------- + +#ifdef TRACE_CREATE_OPERATIONS + +//! @cond Doxygen_Suppress + +static std::string objectAsStr(const common::IdentifiedObject *obj) { + std::string ret(obj->nameStr()); + const auto &ids = obj->identifiers(); + if (!ids.empty()) { + ret += " ("; + ret += (*ids[0]->codeSpace()) + ":" + ids[0]->code(); + ret += ")"; + } + return ret; +} +//! @endcond + +#endif + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +static double getPseudoArea(const metadata::ExtentPtr &extent) { + if (!extent) + return 0.0; + const auto &geographicElements = extent->geographicElements(); + if (geographicElements.empty()) + return 0.0; + auto bbox = dynamic_cast( + geographicElements[0].get()); + if (!bbox) + return 0; + double w = bbox->westBoundLongitude(); + double s = bbox->southBoundLatitude(); + double e = bbox->eastBoundLongitude(); + double n = bbox->northBoundLatitude(); + if (w > e) { + e += 360.0; + } + // Integrate cos(lat) between south_lat and north_lat + return (e - w) * (std::sin(common::Angle(n).getSIValue()) - + std::sin(common::Angle(s).getSIValue())); +} + +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct CoordinateOperationContext::Private { + io::AuthorityFactoryPtr authorityFactory_{}; + metadata::ExtentPtr extent_{}; + double accuracy_ = 0.0; + SourceTargetCRSExtentUse sourceAndTargetCRSExtentUse_ = + CoordinateOperationContext::SourceTargetCRSExtentUse::SMALLEST; + SpatialCriterion spatialCriterion_ = + CoordinateOperationContext::SpatialCriterion::STRICT_CONTAINMENT; + bool usePROJNames_ = true; + GridAvailabilityUse gridAvailabilityUse_ = + GridAvailabilityUse::USE_FOR_SORTING; + IntermediateCRSUse allowUseIntermediateCRS_ = CoordinateOperationContext:: + IntermediateCRSUse::IF_NO_DIRECT_TRANSFORMATION; + std::vector> + intermediateCRSAuthCodes_{}; + bool discardSuperseded_ = true; + bool allowBallpark_ = true; +}; +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +CoordinateOperationContext::~CoordinateOperationContext() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +CoordinateOperationContext::CoordinateOperationContext() + : d(internal::make_unique()) {} + +// --------------------------------------------------------------------------- + +/** \brief Return the authority factory, or null */ +const io::AuthorityFactoryPtr & +CoordinateOperationContext::getAuthorityFactory() const { + return d->authorityFactory_; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the desired area of interest, or null */ +const metadata::ExtentPtr & +CoordinateOperationContext::getAreaOfInterest() const { + return d->extent_; +} + +// --------------------------------------------------------------------------- + +/** \brief Set the desired area of interest, or null */ +void CoordinateOperationContext::setAreaOfInterest( + const metadata::ExtentPtr &extent) { + d->extent_ = extent; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the desired accuracy (in metre), or 0 */ +double CoordinateOperationContext::getDesiredAccuracy() const { + return d->accuracy_; +} + +// --------------------------------------------------------------------------- + +/** \brief Set the desired accuracy (in metre), or 0 */ +void CoordinateOperationContext::setDesiredAccuracy(double accuracy) { + d->accuracy_ = accuracy; +} + +// --------------------------------------------------------------------------- + +/** \brief Return whether ballpark transformations are allowed */ +bool CoordinateOperationContext::getAllowBallparkTransformations() const { + return d->allowBallpark_; +} + +// --------------------------------------------------------------------------- + +/** \brief Set whether ballpark transformations are allowed */ +void CoordinateOperationContext::setAllowBallparkTransformations(bool allow) { + d->allowBallpark_ = allow; +} + +// --------------------------------------------------------------------------- + +/** \brief Set how source and target CRS extent should be used + * when considering if a transformation can be used (only takes effect if + * no area of interest is explicitly defined). + * + * The default is + * CoordinateOperationContext::SourceTargetCRSExtentUse::SMALLEST. + */ +void CoordinateOperationContext::setSourceAndTargetCRSExtentUse( + SourceTargetCRSExtentUse use) { + d->sourceAndTargetCRSExtentUse_ = use; +} + +// --------------------------------------------------------------------------- + +/** \brief Return how source and target CRS extent should be used + * when considering if a transformation can be used (only takes effect if + * no area of interest is explicitly defined). + * + * The default is + * CoordinateOperationContext::SourceTargetCRSExtentUse::SMALLEST. + */ +CoordinateOperationContext::SourceTargetCRSExtentUse +CoordinateOperationContext::getSourceAndTargetCRSExtentUse() const { + return d->sourceAndTargetCRSExtentUse_; +} + +// --------------------------------------------------------------------------- + +/** \brief Set the spatial criterion to use when comparing the area of + * validity + * of coordinate operations with the area of interest / area of validity of + * source and target CRS. + * + * The default is STRICT_CONTAINMENT. + */ +void CoordinateOperationContext::setSpatialCriterion( + SpatialCriterion criterion) { + d->spatialCriterion_ = criterion; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the spatial criterion to use when comparing the area of + * validity + * of coordinate operations with the area of interest / area of validity of + * source and target CRS. + * + * The default is STRICT_CONTAINMENT. + */ +CoordinateOperationContext::SpatialCriterion +CoordinateOperationContext::getSpatialCriterion() const { + return d->spatialCriterion_; +} + +// --------------------------------------------------------------------------- + +/** \brief Set whether PROJ alternative grid names should be substituted to + * the official authority names. + * + * This only has effect is an authority factory with a non-null database context + * has been attached to this context. + * + * If set to false, it is still possible to + * obtain later the substitution by using io::PROJStringFormatter::create() + * with a non-null database context. + * + * The default is true. + */ +void CoordinateOperationContext::setUsePROJAlternativeGridNames( + bool usePROJNames) { + d->usePROJNames_ = usePROJNames; +} + +// --------------------------------------------------------------------------- + +/** \brief Return whether PROJ alternative grid names should be substituted to + * the official authority names. + * + * The default is true. + */ +bool CoordinateOperationContext::getUsePROJAlternativeGridNames() const { + return d->usePROJNames_; +} + +// --------------------------------------------------------------------------- + +/** \brief Return whether transformations that are superseded (but not + * deprecated) + * should be discarded. + * + * The default is true. + */ +bool CoordinateOperationContext::getDiscardSuperseded() const { + return d->discardSuperseded_; +} + +// --------------------------------------------------------------------------- + +/** \brief Set whether transformations that are superseded (but not deprecated) + * should be discarded. + * + * The default is true. + */ +void CoordinateOperationContext::setDiscardSuperseded(bool discard) { + d->discardSuperseded_ = discard; +} + +// --------------------------------------------------------------------------- + +/** \brief Set how grid availability is used. + * + * The default is USE_FOR_SORTING. + */ +void CoordinateOperationContext::setGridAvailabilityUse( + GridAvailabilityUse use) { + d->gridAvailabilityUse_ = use; +} + +// --------------------------------------------------------------------------- + +/** \brief Return how grid availability is used. + * + * The default is USE_FOR_SORTING. + */ +CoordinateOperationContext::GridAvailabilityUse +CoordinateOperationContext::getGridAvailabilityUse() const { + return d->gridAvailabilityUse_; +} + +// --------------------------------------------------------------------------- + +/** \brief Set whether an intermediate pivot CRS can be used for researching + * coordinate operations between a source and target CRS. + * + * Concretely if in the database there is an operation from A to C + * (or C to A), and another one from C to B (or B to C), but no direct + * operation between A and B, setting this parameter to + * ALWAYS/IF_NO_DIRECT_TRANSFORMATION, allow chaining both operations. + * + * The current implementation is limited to researching one intermediate + * step. + * + * By default, with the IF_NO_DIRECT_TRANSFORMATION stratgey, all potential + * C candidates will be used if there is no direct transformation. + */ +void CoordinateOperationContext::setAllowUseIntermediateCRS( + IntermediateCRSUse use) { + d->allowUseIntermediateCRS_ = use; +} + +// --------------------------------------------------------------------------- + +/** \brief Return whether an intermediate pivot CRS can be used for researching + * coordinate operations between a source and target CRS. + * + * Concretely if in the database there is an operation from A to C + * (or C to A), and another one from C to B (or B to C), but no direct + * operation between A and B, setting this parameter to + * ALWAYS/IF_NO_DIRECT_TRANSFORMATION, allow chaining both operations. + * + * The default is IF_NO_DIRECT_TRANSFORMATION. + */ +CoordinateOperationContext::IntermediateCRSUse +CoordinateOperationContext::getAllowUseIntermediateCRS() const { + return d->allowUseIntermediateCRS_; +} + +// --------------------------------------------------------------------------- + +/** \brief Restrict the potential pivot CRSs that can be used when trying to + * build a coordinate operation between two CRS that have no direct operation. + * + * @param intermediateCRSAuthCodes a vector of (auth_name, code) that can be + * used as potential pivot RS + */ +void CoordinateOperationContext::setIntermediateCRS( + const std::vector> + &intermediateCRSAuthCodes) { + d->intermediateCRSAuthCodes_ = intermediateCRSAuthCodes; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the potential pivot CRSs that can be used when trying to + * build a coordinate operation between two CRS that have no direct operation. + * + */ +const std::vector> & +CoordinateOperationContext::getIntermediateCRS() const { + return d->intermediateCRSAuthCodes_; +} + +// --------------------------------------------------------------------------- + +/** \brief Creates a context for a coordinate operation. + * + * If a non null authorityFactory is provided, the resulting context should + * not be used simultaneously by more than one thread. + * + * If authorityFactory->getAuthority() is the empty string, then coordinate + * operations from any authority will be searched, with the restrictions set + * in the authority_to_authority_preference database table. + * If authorityFactory->getAuthority() is set to "any", then coordinate + * operations from any authority will be searched + * If authorityFactory->getAuthority() is a non-empty string different of "any", + * then coordinate operatiosn will be searched only in that authority namespace. + * + * @param authorityFactory Authority factory, or null if no database lookup + * is allowed. + * Use io::authorityFactory::create(context, std::string()) to allow all + * authorities to be used. + * @param extent Area of interest, or null if none is known. + * @param accuracy Maximum allowed accuracy in metre, as specified in or + * 0 to get best accuracy. + * @return a new context. + */ +CoordinateOperationContextNNPtr CoordinateOperationContext::create( + const io::AuthorityFactoryPtr &authorityFactory, + const metadata::ExtentPtr &extent, double accuracy) { + auto ctxt = NN_NO_CHECK( + CoordinateOperationContext::make_unique()); + ctxt->d->authorityFactory_ = authorityFactory; + ctxt->d->extent_ = extent; + ctxt->d->accuracy_ = accuracy; + return ctxt; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct CoordinateOperationFactory::Private { + + struct Context { + // This is the extent of the source CRS and target CRS of the initial + // CoordinateOperationFactory::createOperations() public call, not + // necessarily the ones of intermediate + // CoordinateOperationFactory::Private::createOperations() calls. + // This is used to compare transformations area of use against the + // area of use of the source & target CRS. + const metadata::ExtentPtr &extent1; + const metadata::ExtentPtr &extent2; + const CoordinateOperationContextNNPtr &context; + bool inCreateOperationsWithDatumPivotAntiRecursion = false; + bool inCreateOperationsGeogToVertWithAlternativeGeog = false; + bool inCreateOperationsGeogToVertWithIntermediateVert = false; + bool skipHorizontalTransformation = false; + std::map, + std::list>> + cacheNameToCRS{}; + + Context(const metadata::ExtentPtr &extent1In, + const metadata::ExtentPtr &extent2In, + const CoordinateOperationContextNNPtr &contextIn) + : extent1(extent1In), extent2(extent2In), context(contextIn) {} + }; + + static std::vector + createOperations(const crs::CRSNNPtr &sourceCRS, + const crs::CRSNNPtr &targetCRS, Context &context); + + private: + static constexpr bool disallowEmptyIntersection = true; + + static void + buildCRSIds(const crs::CRSNNPtr &crs, Private::Context &context, + std::list> &ids); + + static std::vector findOpsInRegistryDirect( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, bool &resNonEmptyBeforeFiltering); + + static std::vector + findOpsInRegistryDirectTo(const crs::CRSNNPtr &targetCRS, + Private::Context &context); + + static std::vector + findsOpsInRegistryWithIntermediate( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, + bool useCreateBetweenGeodeticCRSWithDatumBasedIntermediates); + + static void createOperationsFromProj4Ext( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + const crs::BoundCRS *boundSrc, const crs::BoundCRS *boundDst, + std::vector &res); + + static bool createOperationsFromDatabase( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::GeodeticCRS *geodSrc, + const crs::GeodeticCRS *geodDst, const crs::GeographicCRS *geogSrc, + const crs::GeographicCRS *geogDst, const crs::VerticalCRS *vertSrc, + const crs::VerticalCRS *vertDst, + std::vector &res); + + static std::vector + createOperationsGeogToVertFromGeoid(const crs::CRSNNPtr &sourceCRS, + const crs::CRSNNPtr &targetCRS, + const crs::VerticalCRS *vertDst, + Context &context); + + static std::vector + createOperationsGeogToVertWithIntermediateVert( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + const crs::VerticalCRS *vertDst, Context &context); + + static std::vector + createOperationsGeogToVertWithAlternativeGeog( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Context &context); + + static void createOperationsFromDatabaseWithVertCRS( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::GeographicCRS *geogSrc, + const crs::GeographicCRS *geogDst, const crs::VerticalCRS *vertSrc, + const crs::VerticalCRS *vertDst, + std::vector &res); + + static void createOperationsGeodToGeod( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::GeodeticCRS *geodSrc, + const crs::GeodeticCRS *geodDst, + std::vector &res); + + static void createOperationsDerivedTo( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::DerivedCRS *derivedSrc, + std::vector &res); + + static void createOperationsBoundToGeog( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::BoundCRS *boundSrc, + const crs::GeographicCRS *geogDst, + std::vector &res); + + static void createOperationsBoundToVert( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::BoundCRS *boundSrc, + const crs::VerticalCRS *vertDst, + std::vector &res); + + static void createOperationsVertToVert( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::VerticalCRS *vertSrc, + const crs::VerticalCRS *vertDst, + std::vector &res); + + static void createOperationsVertToGeog( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::VerticalCRS *vertSrc, + const crs::GeographicCRS *geogDst, + std::vector &res); + + static void createOperationsVertToGeogBallpark( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::VerticalCRS *vertSrc, + const crs::GeographicCRS *geogDst, + std::vector &res); + + static void createOperationsBoundToBound( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::BoundCRS *boundSrc, + const crs::BoundCRS *boundDst, + std::vector &res); + + static void createOperationsCompoundToGeog( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::CompoundCRS *compoundSrc, + const crs::GeographicCRS *geogDst, + std::vector &res); + + static void createOperationsToGeod( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::GeodeticCRS *geodDst, + std::vector &res); + + static void createOperationsCompoundToCompound( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::CompoundCRS *compoundSrc, + const crs::CompoundCRS *compoundDst, + std::vector &res); + + static void createOperationsBoundToCompound( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::BoundCRS *boundSrc, + const crs::CompoundCRS *compoundDst, + std::vector &res); + + static std::vector createOperationsGeogToGeog( + std::vector &res, + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::GeographicCRS *geogSrc, + const crs::GeographicCRS *geogDst); + + static void createOperationsWithDatumPivot( + std::vector &res, + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + const crs::GeodeticCRS *geodSrc, const crs::GeodeticCRS *geodDst, + Context &context); + + static bool + hasPerfectAccuracyResult(const std::vector &res, + const Context &context); + + static void setCRSs(CoordinateOperation *co, const crs::CRSNNPtr &sourceCRS, + const crs::CRSNNPtr &targetCRS); +}; +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +CoordinateOperationFactory::~CoordinateOperationFactory() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +CoordinateOperationFactory::CoordinateOperationFactory() : d(nullptr) {} + +// --------------------------------------------------------------------------- + +/** \brief Find a CoordinateOperation from sourceCRS to targetCRS. + * + * This is a helper of createOperations(), using a coordinate operation + * context + * with no authority factory (so no catalog searching is done), no desired + * accuracy and no area of interest. + * This returns the first operation of the result set of createOperations(), + * or null if none found. + * + * @param sourceCRS source CRS. + * @param targetCRS source CRS. + * @return a CoordinateOperation or nullptr. + */ +CoordinateOperationPtr CoordinateOperationFactory::createOperation( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS) const { + auto res = createOperations( + sourceCRS, targetCRS, + CoordinateOperationContext::create(nullptr, nullptr, 0.0)); + if (!res.empty()) { + return res[0]; + } + return nullptr; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +// --------------------------------------------------------------------------- + +struct PrecomputedOpCharacteristics { + double area_{}; + double accuracy_{}; + bool isPROJExportable_ = false; + bool hasGrids_ = false; + bool gridsAvailable_ = false; + bool gridsKnown_ = false; + size_t stepCount_ = 0; + bool isApprox_ = false; + bool hasBallparkVertical_ = false; + bool isNullTransformation_ = false; + + PrecomputedOpCharacteristics() = default; + PrecomputedOpCharacteristics(double area, double accuracy, + bool isPROJExportable, bool hasGrids, + bool gridsAvailable, bool gridsKnown, + size_t stepCount, bool isApprox, + bool hasBallparkVertical, + bool isNullTransformation) + : area_(area), accuracy_(accuracy), isPROJExportable_(isPROJExportable), + hasGrids_(hasGrids), gridsAvailable_(gridsAvailable), + gridsKnown_(gridsKnown), stepCount_(stepCount), isApprox_(isApprox), + hasBallparkVertical_(hasBallparkVertical), + isNullTransformation_(isNullTransformation) {} +}; + +// --------------------------------------------------------------------------- + +// We could have used a lambda instead of this old-school way, but +// filterAndSort() is already huge. +struct SortFunction { + + const std::map ↦ + + explicit SortFunction(const std::map &mapIn) + : map(mapIn) {} + + // Sorting function + // Return true if a < b + bool compare(const CoordinateOperationNNPtr &a, + const CoordinateOperationNNPtr &b) const { + auto iterA = map.find(a.get()); + assert(iterA != map.end()); + auto iterB = map.find(b.get()); + assert(iterB != map.end()); + + // CAUTION: the order of the comparisons is extremely important + // to get the intended result. + + if (iterA->second.isPROJExportable_ && + !iterB->second.isPROJExportable_) { + return true; + } + if (!iterA->second.isPROJExportable_ && + iterB->second.isPROJExportable_) { + return false; + } + + if (!iterA->second.isApprox_ && iterB->second.isApprox_) { + return true; + } + if (iterA->second.isApprox_ && !iterB->second.isApprox_) { + return false; + } + + if (!iterA->second.hasBallparkVertical_ && + iterB->second.hasBallparkVertical_) { + return true; + } + if (iterA->second.hasBallparkVertical_ && + !iterB->second.hasBallparkVertical_) { + return false; + } + + if (!iterA->second.isNullTransformation_ && + iterB->second.isNullTransformation_) { + return true; + } + if (iterA->second.isNullTransformation_ && + !iterB->second.isNullTransformation_) { + return false; + } + + // Operations where grids are all available go before other + if (iterA->second.gridsAvailable_ && !iterB->second.gridsAvailable_) { + return true; + } + if (iterB->second.gridsAvailable_ && !iterA->second.gridsAvailable_) { + return false; + } + + // Operations where grids are all known in our DB go before other + if (iterA->second.gridsKnown_ && !iterB->second.gridsKnown_) { + return true; + } + if (iterB->second.gridsKnown_ && !iterA->second.gridsKnown_) { + return false; + } + + // Operations with known accuracy go before those with unknown accuracy + const double accuracyA = iterA->second.accuracy_; + const double accuracyB = iterB->second.accuracy_; + if (accuracyA >= 0 && accuracyB < 0) { + return true; + } + if (accuracyB >= 0 && accuracyA < 0) { + return false; + } + + if (accuracyA < 0 && accuracyB < 0) { + // unknown accuracy ? then prefer operations with grids, which + // are likely to have best practical accuracy + if (iterA->second.hasGrids_ && !iterB->second.hasGrids_) { + return true; + } + if (!iterA->second.hasGrids_ && iterB->second.hasGrids_) { + return false; + } + } + + // Operations with larger non-zero area of use go before those with + // lower one + const double areaA = iterA->second.area_; + const double areaB = iterB->second.area_; + if (areaA > 0) { + if (areaA > areaB) { + return true; + } + if (areaA < areaB) { + return false; + } + } else if (areaB > 0) { + return false; + } + + // Operations with better accuracy go before those with worse one + if (accuracyA >= 0 && accuracyA < accuracyB) { + return true; + } + if (accuracyB >= 0 && accuracyB < accuracyA) { + return false; + } + + if (accuracyA >= 0 && accuracyA == accuracyB) { + // same accuracy ? then prefer operations without grids + if (!iterA->second.hasGrids_ && iterB->second.hasGrids_) { + return true; + } + if (iterA->second.hasGrids_ && !iterB->second.hasGrids_) { + return false; + } + } + + // The less intermediate steps, the better + if (iterA->second.stepCount_ < iterB->second.stepCount_) { + return true; + } + if (iterB->second.stepCount_ < iterA->second.stepCount_) { + return false; + } + + const auto &a_name = a->nameStr(); + const auto &b_name = b->nameStr(); + // The shorter name, the better ? + if (a_name.size() < b_name.size()) { + return true; + } + if (b_name.size() < a_name.size()) { + return false; + } + + // Arbitrary final criterion. We actually return the greater element + // first, so that "Amersfoort to WGS 84 (4)" is presented before + // "Amersfoort to WGS 84 (3)", which is probably a better guess. + + // Except for French NTF (Paris) to NTF, where the (1) conversion + // should be preferred because in the remarks of (2), it is mentioned + // OGP prefers value from IGN Paris (code 1467)... + if (a_name.find("NTF (Paris) to NTF (1)") != std::string::npos && + b_name.find("NTF (Paris) to NTF (2)") != std::string::npos) { + return true; + } + if (a_name.find("NTF (Paris) to NTF (2)") != std::string::npos && + b_name.find("NTF (Paris) to NTF (1)") != std::string::npos) { + return false; + } + if (a_name.find("NTF (Paris) to RGF93 (1)") != std::string::npos && + b_name.find("NTF (Paris) to RGF93 (2)") != std::string::npos) { + return true; + } + if (a_name.find("NTF (Paris) to RGF93 (2)") != std::string::npos && + b_name.find("NTF (Paris) to RGF93 (1)") != std::string::npos) { + return false; + } + + return a_name > b_name; + } + + bool operator()(const CoordinateOperationNNPtr &a, + const CoordinateOperationNNPtr &b) const { + const bool ret = compare(a, b); +#if 0 + std::cerr << a->nameStr() << " < " << b->nameStr() << " : " << ret << std::endl; +#endif + return ret; + } +}; + +// --------------------------------------------------------------------------- + +static size_t getStepCount(const CoordinateOperationNNPtr &op) { + auto concat = dynamic_cast(op.get()); + size_t stepCount = 1; + if (concat) { + stepCount = concat->operations().size(); + } + return stepCount; +} + +// --------------------------------------------------------------------------- + +// Return number of steps that are transformations (and not conversions) +static size_t getTransformationStepCount(const CoordinateOperationNNPtr &op) { + auto concat = dynamic_cast(op.get()); + size_t stepCount = 1; + if (concat) { + stepCount = 0; + for (const auto &subOp : concat->operations()) { + if (dynamic_cast(subOp.get()) == nullptr) { + stepCount++; + } + } + } + return stepCount; +} + +// --------------------------------------------------------------------------- + +static bool isNullTransformation(const std::string &name) { + if (name.find(" + ") != std::string::npos) + return false; + return starts_with(name, BALLPARK_GEOCENTRIC_TRANSLATION) || + starts_with(name, BALLPARK_GEOGRAPHIC_OFFSET) || + starts_with(name, NULL_GEOGRAPHIC_OFFSET) || + starts_with(name, NULL_GEOCENTRIC_TRANSLATION); +} + +// --------------------------------------------------------------------------- + +struct FilterResults { + + FilterResults(const std::vector &sourceListIn, + const CoordinateOperationContextNNPtr &contextIn, + const metadata::ExtentPtr &extent1In, + const metadata::ExtentPtr &extent2In, + bool forceStrictContainmentTest) + : sourceList(sourceListIn), context(contextIn), extent1(extent1In), + extent2(extent2In), areaOfInterest(context->getAreaOfInterest()), + desiredAccuracy(context->getDesiredAccuracy()), + sourceAndTargetCRSExtentUse( + context->getSourceAndTargetCRSExtentUse()) { + + computeAreaOfInterest(); + filterOut(forceStrictContainmentTest); + } + + FilterResults &andSort() { + sort(); + + // And now that we have a sorted list, we can remove uninteresting + // results + // ... + removeSyntheticNullTransforms(); + removeUninterestingOps(); + removeDuplicateOps(); + removeSyntheticNullTransforms(); + return *this; + } + + // ---------------------------------------------------------------------- + + // cppcheck-suppress functionStatic + const std::vector &getRes() { return res; } + + // ---------------------------------------------------------------------- + private: + const std::vector &sourceList; + const CoordinateOperationContextNNPtr &context; + const metadata::ExtentPtr &extent1; + const metadata::ExtentPtr &extent2; + metadata::ExtentPtr areaOfInterest; + const double desiredAccuracy = context->getDesiredAccuracy(); + const CoordinateOperationContext::SourceTargetCRSExtentUse + sourceAndTargetCRSExtentUse; + + bool hasOpThatContainsAreaOfInterestAndNoGrid = false; + std::vector res{}; + + // ---------------------------------------------------------------------- + void computeAreaOfInterest() { + + // Compute an area of interest from the CRS extent if the user did + // not specify one + if (!areaOfInterest) { + if (sourceAndTargetCRSExtentUse == + CoordinateOperationContext::SourceTargetCRSExtentUse:: + INTERSECTION) { + if (extent1 && extent2) { + areaOfInterest = + extent1->intersection(NN_NO_CHECK(extent2)); + } + } else if (sourceAndTargetCRSExtentUse == + CoordinateOperationContext::SourceTargetCRSExtentUse:: + SMALLEST) { + if (extent1 && extent2) { + if (getPseudoArea(extent1) < getPseudoArea(extent2)) { + areaOfInterest = extent1; + } else { + areaOfInterest = extent2; + } + } else if (extent1) { + areaOfInterest = extent1; + } else { + areaOfInterest = extent2; + } + } + } + } + + // --------------------------------------------------------------------------- + + void filterOut(bool forceStrictContainmentTest) { + + // Filter out operations that do not match the expected accuracy + // and area of use. + const auto spatialCriterion = + forceStrictContainmentTest + ? CoordinateOperationContext::SpatialCriterion:: + STRICT_CONTAINMENT + : context->getSpatialCriterion(); + bool hasOnlyBallpark = true; + bool hasNonBallparkWithoutExtent = false; + bool hasNonBallparkOpWithExtent = false; + const bool allowBallpark = context->getAllowBallparkTransformations(); + for (const auto &op : sourceList) { + if (desiredAccuracy != 0) { + const double accuracy = getAccuracy(op); + if (accuracy < 0 || accuracy > desiredAccuracy) { + continue; + } + } + if (!allowBallpark && op->hasBallparkTransformation()) { + continue; + } + if (areaOfInterest) { + bool emptyIntersection = false; + auto extent = getExtent(op, true, emptyIntersection); + if (!extent) { + if (!op->hasBallparkTransformation()) { + hasNonBallparkWithoutExtent = true; + } + continue; + } + if (!op->hasBallparkTransformation()) { + hasNonBallparkOpWithExtent = true; + } + bool extentContains = + extent->contains(NN_NO_CHECK(areaOfInterest)); + if (!hasOpThatContainsAreaOfInterestAndNoGrid && + extentContains) { + if (!op->hasBallparkTransformation() && + op->gridsNeeded(nullptr, true).empty()) { + hasOpThatContainsAreaOfInterestAndNoGrid = true; + } + } + if (spatialCriterion == + CoordinateOperationContext::SpatialCriterion:: + STRICT_CONTAINMENT && + !extentContains) { + continue; + } + if (spatialCriterion == + CoordinateOperationContext::SpatialCriterion:: + PARTIAL_INTERSECTION && + !extent->intersects(NN_NO_CHECK(areaOfInterest))) { + continue; + } + } else if (sourceAndTargetCRSExtentUse == + CoordinateOperationContext::SourceTargetCRSExtentUse:: + BOTH) { + bool emptyIntersection = false; + auto extent = getExtent(op, true, emptyIntersection); + if (!extent) { + if (!op->hasBallparkTransformation()) { + hasNonBallparkWithoutExtent = true; + } + continue; + } + if (!op->hasBallparkTransformation()) { + hasNonBallparkOpWithExtent = true; + } + bool extentContainsExtent1 = + !extent1 || extent->contains(NN_NO_CHECK(extent1)); + bool extentContainsExtent2 = + !extent2 || extent->contains(NN_NO_CHECK(extent2)); + if (!hasOpThatContainsAreaOfInterestAndNoGrid && + extentContainsExtent1 && extentContainsExtent2) { + if (!op->hasBallparkTransformation() && + op->gridsNeeded(nullptr, true).empty()) { + hasOpThatContainsAreaOfInterestAndNoGrid = true; + } + } + if (spatialCriterion == + CoordinateOperationContext::SpatialCriterion:: + STRICT_CONTAINMENT) { + if (!extentContainsExtent1 || !extentContainsExtent2) { + continue; + } + } else if (spatialCriterion == + CoordinateOperationContext::SpatialCriterion:: + PARTIAL_INTERSECTION) { + bool extentIntersectsExtent1 = + !extent1 || extent->intersects(NN_NO_CHECK(extent1)); + bool extentIntersectsExtent2 = + extent2 && extent->intersects(NN_NO_CHECK(extent2)); + if (!extentIntersectsExtent1 || !extentIntersectsExtent2) { + continue; + } + } + } + if (!op->hasBallparkTransformation()) { + hasOnlyBallpark = false; + } + res.emplace_back(op); + } + + // In case no operation has an extent and no result is found, + // retain all initial operations that match accuracy criterion. + if ((res.empty() && !hasNonBallparkOpWithExtent) || + (hasOnlyBallpark && hasNonBallparkWithoutExtent)) { + for (const auto &op : sourceList) { + if (desiredAccuracy != 0) { + const double accuracy = getAccuracy(op); + if (accuracy < 0 || accuracy > desiredAccuracy) { + continue; + } + } + if (!allowBallpark && op->hasBallparkTransformation()) { + continue; + } + res.emplace_back(op); + } + } + } + + // ---------------------------------------------------------------------- + + void sort() { + + // Precompute a number of parameters for each operation that will be + // useful for the sorting. + std::map map; + const auto gridAvailabilityUse = context->getGridAvailabilityUse(); + for (const auto &op : res) { + bool dummy = false; + auto extentOp = getExtent(op, true, dummy); + double area = 0.0; + if (extentOp) { + if (areaOfInterest) { + area = getPseudoArea( + extentOp->intersection(NN_NO_CHECK(areaOfInterest))); + } else if (extent1 && extent2) { + auto x = extentOp->intersection(NN_NO_CHECK(extent1)); + auto y = extentOp->intersection(NN_NO_CHECK(extent2)); + area = getPseudoArea(x) + getPseudoArea(y) - + ((x && y) + ? getPseudoArea(x->intersection(NN_NO_CHECK(y))) + : 0.0); + } else if (extent1) { + area = getPseudoArea( + extentOp->intersection(NN_NO_CHECK(extent1))); + } else if (extent2) { + area = getPseudoArea( + extentOp->intersection(NN_NO_CHECK(extent2))); + } else { + area = getPseudoArea(extentOp); + } + } + + bool hasGrids = false; + bool gridsAvailable = true; + bool gridsKnown = true; + if (context->getAuthorityFactory()) { + const auto gridsNeeded = op->gridsNeeded( + context->getAuthorityFactory()->databaseContext(), + gridAvailabilityUse == + CoordinateOperationContext::GridAvailabilityUse:: + KNOWN_AVAILABLE); + for (const auto &gridDesc : gridsNeeded) { + hasGrids = true; + if (gridAvailabilityUse == + CoordinateOperationContext::GridAvailabilityUse:: + USE_FOR_SORTING && + !gridDesc.available) { + gridsAvailable = false; + } + if (gridDesc.packageName.empty() && + !(!gridDesc.url.empty() && gridDesc.openLicense) && + !gridDesc.available) { + gridsKnown = false; + } + } + } + + const auto stepCount = getStepCount(op); + + bool isPROJExportable = false; + auto formatter = io::PROJStringFormatter::create(); + try { + op->exportToPROJString(formatter.get()); + // Grids might be missing, but at least this is something + // PROJ could potentially process + isPROJExportable = true; + } catch (const std::exception &) { + } + +#if 0 + std::cerr << op->nameStr() << " "; + std::cerr << area << " "; + std::cerr << getAccuracy(op) << " "; + std::cerr << isPROJExportable << " "; + std::cerr << hasGrids << " "; + std::cerr << gridsAvailable << " "; + std::cerr << gridsKnown << " "; + std::cerr << stepCount << " "; + std::cerr << op->hasBallparkTransformation() << " "; + std::cerr << isNullTransformation(op->nameStr()) << " "; + std::cerr << std::endl; +#endif + map[op.get()] = PrecomputedOpCharacteristics( + area, getAccuracy(op), isPROJExportable, hasGrids, + gridsAvailable, gridsKnown, stepCount, + op->hasBallparkTransformation(), + op->nameStr().find("ballpark vertical transformation") != + std::string::npos, + isNullTransformation(op->nameStr())); + } + + // Sort ! + SortFunction sortFunc(map); + std::sort(res.begin(), res.end(), sortFunc); + +// Debug code to check consistency of the sort function +#ifdef DEBUG_SORT + constexpr bool debugSort = true; +#elif !defined(NDEBUG) + const bool debugSort = getenv("PROJ_DEBUG_SORT_FUNCT") != nullptr; +#endif +#if defined(DEBUG_SORT) || !defined(NDEBUG) + if (debugSort) { + const bool assertIfIssue = + !(getenv("PROJ_DEBUG_SORT_FUNCT_ASSERT") != nullptr); + for (size_t i = 0; i < res.size(); ++i) { + for (size_t j = i + 1; j < res.size(); ++j) { + if (sortFunc(res[j], res[i])) { +#ifdef DEBUG_SORT + std::cerr << "Sorting issue with entry " << i << "(" + << res[i]->nameStr() << ") and " << j << "(" + << res[j]->nameStr() << ")" << std::endl; +#endif + if (assertIfIssue) { + assert(false); + } + } + } + } + } +#endif + } + + // ---------------------------------------------------------------------- + + void removeSyntheticNullTransforms() { + + // If we have more than one result, and than the last result is the + // default "Ballpark geographic offset" or "Ballpark geocentric + // translation" operations we have synthetized, and that at least one + // operation has the desired area of interest and does not require the + // use of grids, remove it as all previous results are necessarily + // better + if (hasOpThatContainsAreaOfInterestAndNoGrid && res.size() > 1) { + const auto &opLast = res.back(); + if (opLast->hasBallparkTransformation() || + isNullTransformation(opLast->nameStr())) { + std::vector resTemp; + for (size_t i = 0; i < res.size() - 1; i++) { + resTemp.emplace_back(res[i]); + } + res = std::move(resTemp); + } + } + } + + // ---------------------------------------------------------------------- + + void removeUninterestingOps() { + + // Eliminate operations that bring nothing, ie for a given area of use, + // do not keep operations that have similar or worse accuracy, but + // involve more (non conversion) steps + std::vector resTemp; + metadata::ExtentPtr lastExtent; + double lastAccuracy = -1; + size_t lastStepCount = 0; + CoordinateOperationPtr lastOp; + + bool first = true; + for (const auto &op : res) { + const auto curAccuracy = getAccuracy(op); + bool dummy = false; + const auto curExtent = getExtent(op, true, dummy); + const auto curStepCount = getTransformationStepCount(op); + + if (first) { + resTemp.emplace_back(op); + first = false; + } else { + if (lastOp->_isEquivalentTo(op.get())) { + continue; + } + const bool sameExtent = + ((!curExtent && !lastExtent) || + (curExtent && lastExtent && + curExtent->contains(NN_NO_CHECK(lastExtent)) && + lastExtent->contains(NN_NO_CHECK(curExtent)))); + if (((curAccuracy >= lastAccuracy && lastAccuracy >= 0) || + (curAccuracy < 0 && lastAccuracy >= 0)) && + sameExtent && curStepCount > lastStepCount) { + continue; + } + + resTemp.emplace_back(op); + } + + lastOp = op.as_nullable(); + lastStepCount = curStepCount; + lastExtent = curExtent; + lastAccuracy = curAccuracy; + } + res = std::move(resTemp); + } + + // ---------------------------------------------------------------------- + + // cppcheck-suppress functionStatic + void removeDuplicateOps() { + + if (res.size() <= 1) { + return; + } + + // When going from EPSG:4807 (NTF Paris) to EPSG:4171 (RGC93), we get + // EPSG:7811, NTF (Paris) to RGF93 (2), 1 m + // and unknown id, NTF (Paris) to NTF (1) + Inverse of RGF93 to NTF (2), + // 1 m + // both have same PROJ string and extent + // Do not keep the later (that has more steps) as it adds no value. + + std::set setPROJPlusExtent; + std::vector resTemp; + for (const auto &op : res) { + auto formatter = io::PROJStringFormatter::create(); + try { + std::string key(op->exportToPROJString(formatter.get())); + bool dummy = false; + auto extentOp = getExtent(op, true, dummy); + if (extentOp) { + const auto &geogElts = extentOp->geographicElements(); + if (geogElts.size() == 1) { + auto bbox = dynamic_cast< + const metadata::GeographicBoundingBox *>( + geogElts[0].get()); + if (bbox) { + double w = bbox->westBoundLongitude(); + double s = bbox->southBoundLatitude(); + double e = bbox->eastBoundLongitude(); + double n = bbox->northBoundLatitude(); + key += "-"; + key += toString(w); + key += "-"; + key += toString(s); + key += "-"; + key += toString(e); + key += "-"; + key += toString(n); + } + } + } + + if (setPROJPlusExtent.find(key) == setPROJPlusExtent.end()) { + resTemp.emplace_back(op); + setPROJPlusExtent.insert(key); + } + } catch (const std::exception &) { + resTemp.emplace_back(op); + } + } + res = std::move(resTemp); + } +}; + +// --------------------------------------------------------------------------- + +/** \brief Filter operations and sort them given context. + * + * If a desired accuracy is specified, only keep operations whose accuracy + * is at least the desired one. + * If an area of interest is specified, only keep operations whose area of + * use include the area of interest. + * Then sort remaining operations by descending area of use, and increasing + * accuracy. + */ +static std::vector +filterAndSort(const std::vector &sourceList, + const CoordinateOperationContextNNPtr &context, + const metadata::ExtentPtr &extent1, + const metadata::ExtentPtr &extent2) { +#ifdef TRACE_CREATE_OPERATIONS + ENTER_FUNCTION(); + logTrace("number of results before filter and sort: " + + toString(static_cast(sourceList.size()))); +#endif + auto resFiltered = + FilterResults(sourceList, context, extent1, extent2, false) + .andSort() + .getRes(); +#ifdef TRACE_CREATE_OPERATIONS + logTrace("number of results after filter and sort: " + + toString(static_cast(resFiltered.size()))); +#endif + return resFiltered; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +// Apply the inverse() method on all elements of the input list +static std::vector +applyInverse(const std::vector &list) { + auto res = list; + for (auto &op : res) { +#ifdef DEBUG + auto opNew = op->inverse(); + assert(opNew->targetCRS()->isEquivalentTo(op->sourceCRS().get())); + assert(opNew->sourceCRS()->isEquivalentTo(op->targetCRS().get())); + op = opNew; +#else + op = op->inverse(); +#endif + } + return res; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +void CoordinateOperationFactory::Private::buildCRSIds( + const crs::CRSNNPtr &crs, Private::Context &context, + std::list> &ids) { + const auto &authFactory = context.context->getAuthorityFactory(); + assert(authFactory); + for (const auto &id : crs->identifiers()) { + const auto &authName = *(id->codeSpace()); + const auto &code = id->code(); + if (!authName.empty()) { + const auto tmpAuthFactory = io::AuthorityFactory::create( + authFactory->databaseContext(), authName); + try { + // Consistency check for the ID attached to the object. + // See https://github.com/OSGeo/PROJ/issues/1982 where EPSG:4656 + // is attached to a GeographicCRS whereas it is a ProjectedCRS + if (tmpAuthFactory->createCoordinateReferenceSystem(code) + ->_isEquivalentTo( + crs.get(), + util::IComparable::Criterion:: + EQUIVALENT_EXCEPT_AXIS_ORDER_GEOGCRS)) { + ids.emplace_back(authName, code); + } else { + // TODO? log this inconsistency + } + } catch (const std::exception &) { + // TODO? log this inconsistency + } + } + } + if (ids.empty()) { + std::vector allowedObjects; + auto geogCRS = dynamic_cast(crs.get()); + if (geogCRS) { + allowedObjects.push_back( + geogCRS->coordinateSystem()->axisList().size() == 2 + ? io::AuthorityFactory::ObjectType::GEOGRAPHIC_2D_CRS + : io::AuthorityFactory::ObjectType::GEOGRAPHIC_3D_CRS); + } else if (dynamic_cast(crs.get())) { + allowedObjects.push_back( + io::AuthorityFactory::ObjectType::PROJECTED_CRS); + } else if (dynamic_cast(crs.get())) { + allowedObjects.push_back( + io::AuthorityFactory::ObjectType::VERTICAL_CRS); + } + if (!allowedObjects.empty()) { + + const std::pair key( + allowedObjects[0], crs->nameStr()); + auto iter = context.cacheNameToCRS.find(key); + if (iter != context.cacheNameToCRS.end()) { + ids = iter->second; + return; + } + + const auto &authFactoryName = authFactory->getAuthority(); + try { + const auto tmpAuthFactory = io::AuthorityFactory::create( + authFactory->databaseContext(), + (authFactoryName.empty() || authFactoryName == "any") + ? std::string() + : authFactoryName); + + auto matches = tmpAuthFactory->createObjectsFromName( + crs->nameStr(), allowedObjects, false, 2); + if (matches.size() == 1 && + crs->_isEquivalentTo( + matches.front().get(), + util::IComparable::Criterion::EQUIVALENT) && + !matches.front()->identifiers().empty()) { + const auto &tmpIds = matches.front()->identifiers(); + ids.emplace_back(*(tmpIds[0]->codeSpace()), + tmpIds[0]->code()); + } + } catch (const std::exception &) { + } + context.cacheNameToCRS[key] = ids; + } + } +} + +// --------------------------------------------------------------------------- + +static std::vector +getCandidateAuthorities(const io::AuthorityFactoryPtr &authFactory, + const std::string &srcAuthName, + const std::string &targetAuthName) { + const auto &authFactoryName = authFactory->getAuthority(); + std::vector authorities; + if (authFactoryName == "any") { + authorities.emplace_back(); + } + if (authFactoryName.empty()) { + authorities = authFactory->databaseContext()->getAllowedAuthorities( + srcAuthName, targetAuthName); + if (authorities.empty()) { + authorities.emplace_back(); + } + } else { + authorities.emplace_back(authFactoryName); + } + return authorities; +} + +// --------------------------------------------------------------------------- + +// Look in the authority registry for operations from sourceCRS to targetCRS +std::vector +CoordinateOperationFactory::Private::findOpsInRegistryDirect( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, bool &resNonEmptyBeforeFiltering) { + const auto &authFactory = context.context->getAuthorityFactory(); + assert(authFactory); + +#ifdef TRACE_CREATE_OPERATIONS + ENTER_BLOCK("findOpsInRegistryDirect(" + objectAsStr(sourceCRS.get()) + + " --> " + objectAsStr(targetCRS.get()) + ")"); +#endif + + resNonEmptyBeforeFiltering = false; + std::list> sourceIds; + std::list> targetIds; + buildCRSIds(sourceCRS, context, sourceIds); + buildCRSIds(targetCRS, context, targetIds); + + const auto gridAvailabilityUse = context.context->getGridAvailabilityUse(); + for (const auto &idSrc : sourceIds) { + const auto &srcAuthName = idSrc.first; + const auto &srcCode = idSrc.second; + for (const auto &idTarget : targetIds) { + const auto &targetAuthName = idTarget.first; + const auto &targetCode = idTarget.second; + + const auto authorities(getCandidateAuthorities( + authFactory, srcAuthName, targetAuthName)); + std::vector res; + for (const auto &authority : authorities) { + const auto authName = + authority == "any" ? std::string() : authority; + const auto tmpAuthFactory = io::AuthorityFactory::create( + authFactory->databaseContext(), authName); + auto resTmp = + tmpAuthFactory->createFromCoordinateReferenceSystemCodes( + srcAuthName, srcCode, targetAuthName, targetCode, + context.context->getUsePROJAlternativeGridNames(), + gridAvailabilityUse == + CoordinateOperationContext:: + GridAvailabilityUse:: + DISCARD_OPERATION_IF_MISSING_GRID || + gridAvailabilityUse == + CoordinateOperationContext:: + GridAvailabilityUse::KNOWN_AVAILABLE, + gridAvailabilityUse == + CoordinateOperationContext::GridAvailabilityUse:: + KNOWN_AVAILABLE, + context.context->getDiscardSuperseded(), true, false, + context.extent1, context.extent2); + res.insert(res.end(), resTmp.begin(), resTmp.end()); + if (authName == "PROJ") { + continue; + } + if (!res.empty()) { + resNonEmptyBeforeFiltering = true; + auto resFiltered = + FilterResults(res, context.context, context.extent1, + context.extent2, false) + .getRes(); +#ifdef TRACE_CREATE_OPERATIONS + logTrace("filtering reduced from " + + toString(static_cast(res.size())) + " to " + + toString(static_cast(resFiltered.size()))); +#endif + return resFiltered; + } + } + } + } + return std::vector(); +} + +// --------------------------------------------------------------------------- + +// Look in the authority registry for operations to targetCRS +std::vector +CoordinateOperationFactory::Private::findOpsInRegistryDirectTo( + const crs::CRSNNPtr &targetCRS, Private::Context &context) { +#ifdef TRACE_CREATE_OPERATIONS + ENTER_BLOCK("findOpsInRegistryDirectTo({any} -->" + + objectAsStr(targetCRS.get()) + ")"); +#endif + + const auto &authFactory = context.context->getAuthorityFactory(); + assert(authFactory); + + std::list> ids; + buildCRSIds(targetCRS, context, ids); + + const auto gridAvailabilityUse = context.context->getGridAvailabilityUse(); + for (const auto &id : ids) { + const auto &targetAuthName = id.first; + const auto &targetCode = id.second; + + const auto authorities(getCandidateAuthorities( + authFactory, targetAuthName, targetAuthName)); + for (const auto &authority : authorities) { + const auto tmpAuthFactory = io::AuthorityFactory::create( + authFactory->databaseContext(), + authority == "any" ? std::string() : authority); + auto res = tmpAuthFactory->createFromCoordinateReferenceSystemCodes( + std::string(), std::string(), targetAuthName, targetCode, + context.context->getUsePROJAlternativeGridNames(), + + gridAvailabilityUse == + CoordinateOperationContext::GridAvailabilityUse:: + DISCARD_OPERATION_IF_MISSING_GRID || + gridAvailabilityUse == + CoordinateOperationContext::GridAvailabilityUse:: + KNOWN_AVAILABLE, + gridAvailabilityUse == CoordinateOperationContext:: + GridAvailabilityUse::KNOWN_AVAILABLE, + context.context->getDiscardSuperseded(), true, true, + context.extent1, context.extent2); + if (!res.empty()) { + auto resFiltered = + FilterResults(res, context.context, context.extent1, + context.extent2, false) + .getRes(); +#ifdef TRACE_CREATE_OPERATIONS + logTrace("filtering reduced from " + + toString(static_cast(res.size())) + " to " + + toString(static_cast(resFiltered.size()))); +#endif + return resFiltered; + } + } + } + return std::vector(); +} + +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +// Look in the authority registry for operations from sourceCRS to targetCRS +// using an intermediate pivot +std::vector +CoordinateOperationFactory::Private::findsOpsInRegistryWithIntermediate( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, + bool useCreateBetweenGeodeticCRSWithDatumBasedIntermediates) { + +#ifdef TRACE_CREATE_OPERATIONS + ENTER_BLOCK("findsOpsInRegistryWithIntermediate(" + + objectAsStr(sourceCRS.get()) + " --> " + + objectAsStr(targetCRS.get()) + ")"); +#endif + + const auto &authFactory = context.context->getAuthorityFactory(); + assert(authFactory); + + std::list> sourceIds; + std::list> targetIds; + buildCRSIds(sourceCRS, context, sourceIds); + buildCRSIds(targetCRS, context, targetIds); + + const auto gridAvailabilityUse = context.context->getGridAvailabilityUse(); + for (const auto &idSrc : sourceIds) { + const auto &srcAuthName = idSrc.first; + const auto &srcCode = idSrc.second; + for (const auto &idTarget : targetIds) { + const auto &targetAuthName = idTarget.first; + const auto &targetCode = idTarget.second; + + const auto authorities(getCandidateAuthorities( + authFactory, srcAuthName, targetAuthName)); + assert(!authorities.empty()); + + const auto tmpAuthFactory = io::AuthorityFactory::create( + authFactory->databaseContext(), + (authFactory->getAuthority() == "any" || authorities.size() > 1) + ? std::string() + : authorities.front()); + + std::vector res; + if (useCreateBetweenGeodeticCRSWithDatumBasedIntermediates) { + res = + tmpAuthFactory + ->createBetweenGeodeticCRSWithDatumBasedIntermediates( + sourceCRS, srcAuthName, srcCode, targetCRS, + targetAuthName, targetCode, + context.context->getUsePROJAlternativeGridNames(), + gridAvailabilityUse == + CoordinateOperationContext:: + GridAvailabilityUse:: + DISCARD_OPERATION_IF_MISSING_GRID || + gridAvailabilityUse == + CoordinateOperationContext:: + GridAvailabilityUse::KNOWN_AVAILABLE, + gridAvailabilityUse == + CoordinateOperationContext:: + GridAvailabilityUse::KNOWN_AVAILABLE, + context.context->getDiscardSuperseded(), + authFactory->getAuthority() != "any" && + authorities.size() > 1 + ? authorities + : std::vector(), + context.extent1, context.extent2); + } else { + io::AuthorityFactory::ObjectType intermediateObjectType = + io::AuthorityFactory::ObjectType::CRS; + + // If doing GeogCRS --> GeogCRS, only use GeogCRS as + // intermediate CRS + // Avoid weird behavior when doing NAD83 -> NAD83(2011) + // that would go through NAVD88 otherwise. + if (context.context->getIntermediateCRS().empty() && + dynamic_cast(sourceCRS.get()) && + dynamic_cast(targetCRS.get())) { + intermediateObjectType = + io::AuthorityFactory::ObjectType::GEOGRAPHIC_CRS; + } + res = tmpAuthFactory->createFromCRSCodesWithIntermediates( + srcAuthName, srcCode, targetAuthName, targetCode, + context.context->getUsePROJAlternativeGridNames(), + gridAvailabilityUse == + CoordinateOperationContext::GridAvailabilityUse:: + DISCARD_OPERATION_IF_MISSING_GRID || + gridAvailabilityUse == + CoordinateOperationContext::GridAvailabilityUse:: + KNOWN_AVAILABLE, + gridAvailabilityUse == + CoordinateOperationContext::GridAvailabilityUse:: + KNOWN_AVAILABLE, + context.context->getDiscardSuperseded(), + context.context->getIntermediateCRS(), + intermediateObjectType, + authFactory->getAuthority() != "any" && + authorities.size() > 1 + ? authorities + : std::vector(), + context.extent1, context.extent2); + } + if (!res.empty()) { + + auto resFiltered = + FilterResults(res, context.context, context.extent1, + context.extent2, false) + .getRes(); +#ifdef TRACE_CREATE_OPERATIONS + logTrace("filtering reduced from " + + toString(static_cast(res.size())) + " to " + + toString(static_cast(resFiltered.size()))); +#endif + return resFiltered; + } + } + } + return std::vector(); +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static TransformationNNPtr +createBallparkGeographicOffset(const crs::CRSNNPtr &sourceCRS, + const crs::CRSNNPtr &targetCRS, + const io::DatabaseContextPtr &dbContext) { + + const crs::GeographicCRS *geogSrc = + dynamic_cast(sourceCRS.get()); + const crs::GeographicCRS *geogDst = + dynamic_cast(targetCRS.get()); + const bool isSameDatum = geogSrc && geogDst && + geogSrc->datumNonNull(dbContext)->_isEquivalentTo( + geogDst->datumNonNull(dbContext).get(), + util::IComparable::Criterion::EQUIVALENT); + + auto name = buildOpName(isSameDatum ? NULL_GEOGRAPHIC_OFFSET + : BALLPARK_GEOGRAPHIC_OFFSET, + sourceCRS, targetCRS); + + const auto &sourceCRSExtent = getExtent(sourceCRS); + const auto &targetCRSExtent = getExtent(targetCRS); + const bool sameExtent = + sourceCRSExtent && targetCRSExtent && + sourceCRSExtent->_isEquivalentTo( + targetCRSExtent.get(), util::IComparable::Criterion::EQUIVALENT); + + util::PropertyMap map; + map.set(common::IdentifiedObject::NAME_KEY, name) + .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + sameExtent ? NN_NO_CHECK(sourceCRSExtent) + : metadata::Extent::WORLD); + const common::Angle angle0(0); + + std::vector accuracies; + if (isSameDatum) { + accuracies.emplace_back(metadata::PositionalAccuracy::create("0")); + } + + if (dynamic_cast(sourceCRS.get()) + ->coordinateSystem() + ->axisList() + .size() == 3 || + dynamic_cast(targetCRS.get()) + ->coordinateSystem() + ->axisList() + .size() == 3) { + return Transformation::createGeographic3DOffsets( + map, sourceCRS, targetCRS, angle0, angle0, common::Length(0), + accuracies); + } else { + return Transformation::createGeographic2DOffsets( + map, sourceCRS, targetCRS, angle0, angle0, accuracies); + } +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +// --------------------------------------------------------------------------- + +struct MyPROJStringExportableGeodToGeod final + : public io::IPROJStringExportable { + crs::GeodeticCRSPtr geodSrc{}; + crs::GeodeticCRSPtr geodDst{}; + + MyPROJStringExportableGeodToGeod(const crs::GeodeticCRSPtr &geodSrcIn, + const crs::GeodeticCRSPtr &geodDstIn) + : geodSrc(geodSrcIn), geodDst(geodDstIn) {} + + ~MyPROJStringExportableGeodToGeod() override; + + void + // cppcheck-suppress functionStatic + _exportToPROJString(io::PROJStringFormatter *formatter) const override { + + formatter->startInversion(); + geodSrc->_exportToPROJString(formatter); + formatter->stopInversion(); + geodDst->_exportToPROJString(formatter); + } +}; + +MyPROJStringExportableGeodToGeod::~MyPROJStringExportableGeodToGeod() = default; + +// --------------------------------------------------------------------------- + +struct MyPROJStringExportableHorizVertical final + : public io::IPROJStringExportable { + CoordinateOperationPtr horizTransform{}; + CoordinateOperationPtr verticalTransform{}; + crs::GeographicCRSPtr geogDst{}; + + MyPROJStringExportableHorizVertical( + const CoordinateOperationPtr &horizTransformIn, + const CoordinateOperationPtr &verticalTransformIn, + const crs::GeographicCRSPtr &geogDstIn) + : horizTransform(horizTransformIn), + verticalTransform(verticalTransformIn), geogDst(geogDstIn) {} + + ~MyPROJStringExportableHorizVertical() override; + + void + // cppcheck-suppress functionStatic + _exportToPROJString(io::PROJStringFormatter *formatter) const override { + + formatter->pushOmitZUnitConversion(); + + horizTransform->_exportToPROJString(formatter); + + formatter->startInversion(); + geogDst->addAngularUnitConvertAndAxisSwap(formatter); + formatter->stopInversion(); + + formatter->popOmitZUnitConversion(); + + formatter->pushOmitHorizontalConversionInVertTransformation(); + verticalTransform->_exportToPROJString(formatter); + formatter->popOmitHorizontalConversionInVertTransformation(); + + formatter->pushOmitZUnitConversion(); + geogDst->addAngularUnitConvertAndAxisSwap(formatter); + formatter->popOmitZUnitConversion(); + } +}; + +MyPROJStringExportableHorizVertical::~MyPROJStringExportableHorizVertical() = + default; + +// --------------------------------------------------------------------------- + +struct MyPROJStringExportableHorizVerticalHorizPROJBased final + : public io::IPROJStringExportable { + CoordinateOperationPtr opSrcCRSToGeogCRS{}; + CoordinateOperationPtr verticalTransform{}; + CoordinateOperationPtr opGeogCRStoDstCRS{}; + crs::GeographicCRSPtr interpolationGeogCRS{}; + + MyPROJStringExportableHorizVerticalHorizPROJBased( + const CoordinateOperationPtr &opSrcCRSToGeogCRSIn, + const CoordinateOperationPtr &verticalTransformIn, + const CoordinateOperationPtr &opGeogCRStoDstCRSIn, + const crs::GeographicCRSPtr &interpolationGeogCRSIn) + : opSrcCRSToGeogCRS(opSrcCRSToGeogCRSIn), + verticalTransform(verticalTransformIn), + opGeogCRStoDstCRS(opGeogCRStoDstCRSIn), + interpolationGeogCRS(interpolationGeogCRSIn) {} + + ~MyPROJStringExportableHorizVerticalHorizPROJBased() override; + + void + // cppcheck-suppress functionStatic + _exportToPROJString(io::PROJStringFormatter *formatter) const override { + + formatter->pushOmitZUnitConversion(); + + opSrcCRSToGeogCRS->_exportToPROJString(formatter); + + formatter->startInversion(); + interpolationGeogCRS->addAngularUnitConvertAndAxisSwap(formatter); + formatter->stopInversion(); + + formatter->popOmitZUnitConversion(); + + formatter->pushOmitHorizontalConversionInVertTransformation(); + verticalTransform->_exportToPROJString(formatter); + formatter->popOmitHorizontalConversionInVertTransformation(); + + formatter->pushOmitZUnitConversion(); + + interpolationGeogCRS->addAngularUnitConvertAndAxisSwap(formatter); + + opGeogCRStoDstCRS->_exportToPROJString(formatter); + + formatter->popOmitZUnitConversion(); + } +}; + +MyPROJStringExportableHorizVerticalHorizPROJBased:: + ~MyPROJStringExportableHorizVerticalHorizPROJBased() = default; + +//! @endcond + +} // namespace operation +NS_PROJ_END + +#if 0 +namespace dropbox{ namespace oxygen { +template<> nn>::~nn() = default; +template<> nn>::~nn() = default; +template<> nn>::~nn() = default; +}} +#endif + +NS_PROJ_START +namespace operation { + +//! @cond Doxygen_Suppress + +// --------------------------------------------------------------------------- + +static std::string buildTransfName(const std::string &srcName, + const std::string &targetName) { + std::string name("Transformation from "); + name += srcName; + name += " to "; + name += targetName; + return name; +} + +// --------------------------------------------------------------------------- + +static SingleOperationNNPtr createPROJBased( + const util::PropertyMap &properties, + const io::IPROJStringExportableNNPtr &projExportable, + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + const crs::CRSPtr &interpolationCRS, + const std::vector &accuracies, + bool hasBallparkTransformation) { + return util::nn_static_pointer_cast( + PROJBasedOperation::create(properties, projExportable, false, sourceCRS, + targetCRS, interpolationCRS, accuracies, + hasBallparkTransformation)); +} + +// --------------------------------------------------------------------------- + +static CoordinateOperationNNPtr +createGeodToGeodPROJBased(const crs::CRSNNPtr &geodSrc, + const crs::CRSNNPtr &geodDst) { + + auto exportable = util::nn_make_shared( + util::nn_dynamic_pointer_cast(geodSrc), + util::nn_dynamic_pointer_cast(geodDst)); + + auto properties = util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, + buildTransfName(geodSrc->nameStr(), geodDst->nameStr())); + return createPROJBased(properties, exportable, geodSrc, geodDst, nullptr, + {}, false); +} + +// --------------------------------------------------------------------------- + +static std::string +getRemarks(const std::vector &ops) { + std::string remarks; + for (const auto &op : ops) { + const auto &opRemarks = op->remarks(); + if (!opRemarks.empty()) { + if (!remarks.empty()) { + remarks += '\n'; + } + + std::string opName(op->nameStr()); + if (starts_with(opName, INVERSE_OF)) { + opName = opName.substr(INVERSE_OF.size()); + } + + remarks += "For "; + remarks += opName; + + const auto &ids = op->identifiers(); + if (!ids.empty()) { + std::string authority(*ids.front()->codeSpace()); + if (starts_with(authority, "INVERSE(") && + authority.back() == ')') { + authority = authority.substr(strlen("INVERSE("), + authority.size() - 1 - + strlen("INVERSE(")); + } + if (starts_with(authority, "DERIVED_FROM(") && + authority.back() == ')') { + authority = authority.substr(strlen("DERIVED_FROM("), + authority.size() - 1 - + strlen("DERIVED_FROM(")); + } + + remarks += " ("; + remarks += authority; + remarks += ':'; + remarks += ids.front()->code(); + remarks += ')'; + } + remarks += ": "; + remarks += opRemarks; + } + } + return remarks; +} + +// --------------------------------------------------------------------------- + +static CoordinateOperationNNPtr createHorizVerticalPROJBased( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + const operation::CoordinateOperationNNPtr &horizTransform, + const operation::CoordinateOperationNNPtr &verticalTransform, + bool checkExtent) { + + auto geogDst = util::nn_dynamic_pointer_cast(targetCRS); + assert(geogDst); + + auto exportable = util::nn_make_shared( + horizTransform, verticalTransform, geogDst); + + const bool horizTransformIsNoOp = + starts_with(horizTransform->nameStr(), NULL_GEOGRAPHIC_OFFSET) && + horizTransform->nameStr().find(" + ") == std::string::npos; + if (horizTransformIsNoOp) { + auto properties = util::PropertyMap(); + properties.set(common::IdentifiedObject::NAME_KEY, + verticalTransform->nameStr()); + bool dummy = false; + auto extent = getExtent(verticalTransform, true, dummy); + if (extent) { + properties.set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + NN_NO_CHECK(extent)); + } + const auto &remarks = verticalTransform->remarks(); + if (!remarks.empty()) { + properties.set(common::IdentifiedObject::REMARKS_KEY, remarks); + } + return createPROJBased( + properties, exportable, sourceCRS, targetCRS, nullptr, + verticalTransform->coordinateOperationAccuracies(), + verticalTransform->hasBallparkTransformation()); + } else { + bool emptyIntersection = false; + auto ops = std::vector{horizTransform, + verticalTransform}; + auto extent = getExtent(ops, true, emptyIntersection); + if (checkExtent && emptyIntersection) { + std::string msg( + "empty intersection of area of validity of concatenated " + "operations"); + throw InvalidOperationEmptyIntersection(msg); + } + auto properties = util::PropertyMap(); + properties.set(common::IdentifiedObject::NAME_KEY, + computeConcatenatedName(ops)); + + if (extent) { + properties.set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + NN_NO_CHECK(extent)); + } + + const auto remarks = getRemarks(ops); + if (!remarks.empty()) { + properties.set(common::IdentifiedObject::REMARKS_KEY, remarks); + } + + std::vector accuracies; + const double accuracy = getAccuracy(ops); + if (accuracy >= 0.0) { + accuracies.emplace_back( + metadata::PositionalAccuracy::create(toString(accuracy))); + } + + return createPROJBased( + properties, exportable, sourceCRS, targetCRS, nullptr, accuracies, + horizTransform->hasBallparkTransformation() || + verticalTransform->hasBallparkTransformation()); + } +} + +// --------------------------------------------------------------------------- + +static CoordinateOperationNNPtr createHorizVerticalHorizPROJBased( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + const operation::CoordinateOperationNNPtr &opSrcCRSToGeogCRS, + const operation::CoordinateOperationNNPtr &verticalTransform, + const operation::CoordinateOperationNNPtr &opGeogCRStoDstCRS, + const crs::GeographicCRSPtr &interpolationGeogCRS, bool checkExtent) { + + auto exportable = + util::nn_make_shared( + opSrcCRSToGeogCRS, verticalTransform, opGeogCRStoDstCRS, + interpolationGeogCRS); + + std::vector ops; + if (!(starts_with(opSrcCRSToGeogCRS->nameStr(), NULL_GEOGRAPHIC_OFFSET) && + opSrcCRSToGeogCRS->nameStr().find(" + ") == std::string::npos)) { + ops.emplace_back(opSrcCRSToGeogCRS); + } + ops.emplace_back(verticalTransform); + if (!(starts_with(opGeogCRStoDstCRS->nameStr(), NULL_GEOGRAPHIC_OFFSET) && + opGeogCRStoDstCRS->nameStr().find(" + ") == std::string::npos)) { + ops.emplace_back(opGeogCRStoDstCRS); + } + + bool hasBallparkTransformation = false; + for (const auto &op : ops) { + hasBallparkTransformation |= op->hasBallparkTransformation(); + } + bool emptyIntersection = false; + auto extent = getExtent(ops, false, emptyIntersection); + if (checkExtent && emptyIntersection) { + std::string msg( + "empty intersection of area of validity of concatenated " + "operations"); + throw InvalidOperationEmptyIntersection(msg); + } + auto properties = util::PropertyMap(); + properties.set(common::IdentifiedObject::NAME_KEY, + computeConcatenatedName(ops)); + + if (extent) { + properties.set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + NN_NO_CHECK(extent)); + } + + const auto remarks = getRemarks(ops); + if (!remarks.empty()) { + properties.set(common::IdentifiedObject::REMARKS_KEY, remarks); + } + + std::vector accuracies; + const double accuracy = getAccuracy(ops); + if (accuracy >= 0.0) { + accuracies.emplace_back( + metadata::PositionalAccuracy::create(toString(accuracy))); + } + + return createPROJBased(properties, exportable, sourceCRS, targetCRS, + nullptr, accuracies, hasBallparkTransformation); +} + +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +std::vector +CoordinateOperationFactory::Private::createOperationsGeogToGeog( + std::vector &res, const crs::CRSNNPtr &sourceCRS, + const crs::CRSNNPtr &targetCRS, Private::Context &context, + const crs::GeographicCRS *geogSrc, const crs::GeographicCRS *geogDst) { + + assert(sourceCRS.get() == geogSrc); + assert(targetCRS.get() == geogDst); + + const auto &src_pm = geogSrc->primeMeridian()->longitude(); + const auto &dst_pm = geogDst->primeMeridian()->longitude(); + auto offset_pm = + (src_pm.unit() == dst_pm.unit()) + ? common::Angle(src_pm.value() - dst_pm.value(), src_pm.unit()) + : common::Angle( + src_pm.convertToUnit(common::UnitOfMeasure::DEGREE) - + dst_pm.convertToUnit(common::UnitOfMeasure::DEGREE), + common::UnitOfMeasure::DEGREE); + + double vconvSrc = 1.0; + const auto &srcCS = geogSrc->coordinateSystem(); + const auto &srcAxisList = srcCS->axisList(); + if (srcAxisList.size() == 3) { + vconvSrc = srcAxisList[2]->unit().conversionToSI(); + } + double vconvDst = 1.0; + const auto &dstCS = geogDst->coordinateSystem(); + const auto &dstAxisList = dstCS->axisList(); + if (dstAxisList.size() == 3) { + vconvDst = dstAxisList[2]->unit().conversionToSI(); + } + + std::string name(buildTransfName(geogSrc->nameStr(), geogDst->nameStr())); + + const auto &authFactory = context.context->getAuthorityFactory(); + const auto dbContext = + authFactory ? authFactory->databaseContext().as_nullable() : nullptr; + + const bool sameDatum = geogSrc->datumNonNull(dbContext)->_isEquivalentTo( + geogDst->datumNonNull(dbContext).get(), + util::IComparable::Criterion::EQUIVALENT); + + // Do the CRS differ by their axis order ? + bool axisReversal2D = false; + bool axisReversal3D = false; + if (!srcCS->_isEquivalentTo(dstCS.get(), + util::IComparable::Criterion::EQUIVALENT)) { + auto srcOrder = srcCS->axisOrder(); + auto dstOrder = dstCS->axisOrder(); + if (((srcOrder == cs::EllipsoidalCS::AxisOrder::LAT_NORTH_LONG_EAST || + srcOrder == cs::EllipsoidalCS::AxisOrder:: + LAT_NORTH_LONG_EAST_HEIGHT_UP) && + (dstOrder == cs::EllipsoidalCS::AxisOrder::LONG_EAST_LAT_NORTH || + dstOrder == cs::EllipsoidalCS::AxisOrder:: + LONG_EAST_LAT_NORTH_HEIGHT_UP)) || + ((srcOrder == cs::EllipsoidalCS::AxisOrder::LONG_EAST_LAT_NORTH || + srcOrder == cs::EllipsoidalCS::AxisOrder:: + LONG_EAST_LAT_NORTH_HEIGHT_UP) && + (dstOrder == cs::EllipsoidalCS::AxisOrder::LAT_NORTH_LONG_EAST || + dstOrder == cs::EllipsoidalCS::AxisOrder:: + LAT_NORTH_LONG_EAST_HEIGHT_UP))) { + if (srcAxisList.size() == 3 || dstAxisList.size() == 3) + axisReversal3D = true; + else + axisReversal2D = true; + } + } + + // Do they differ by vertical units ? + if (vconvSrc != vconvDst && + geogSrc->ellipsoid()->_isEquivalentTo( + geogDst->ellipsoid().get(), + util::IComparable::Criterion::EQUIVALENT)) { + if (offset_pm.value() == 0 && !axisReversal2D && !axisReversal3D) { + // If only by vertical units, use a Change of Vertical + // Unit + // transformation + const double factor = vconvSrc / vconvDst; + auto conv = Conversion::createChangeVerticalUnit( + util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, + name), + common::Scale(factor)); + conv->setCRSs(sourceCRS, targetCRS, nullptr); + conv->setHasBallparkTransformation(!sameDatum); + res.push_back(conv); + return res; + } else { + auto op = createGeodToGeodPROJBased(sourceCRS, targetCRS); + op->setHasBallparkTransformation(!sameDatum); + res.emplace_back(op); + return res; + } + } + + // Do the CRS differ only by their axis order ? + if (sameDatum && (axisReversal2D || axisReversal3D)) { + auto conv = Conversion::createAxisOrderReversal(axisReversal3D); + conv->setCRSs(sourceCRS, targetCRS, nullptr); + res.emplace_back(conv); + return res; + } + + std::vector steps; + // If both are geographic and only differ by their prime + // meridian, + // apply a longitude rotation transformation. + if (geogSrc->ellipsoid()->_isEquivalentTo( + geogDst->ellipsoid().get(), + util::IComparable::Criterion::EQUIVALENT) && + src_pm.getSIValue() != dst_pm.getSIValue()) { + + steps.emplace_back(Transformation::createLongitudeRotation( + util::PropertyMap() + .set(common::IdentifiedObject::NAME_KEY, name) + .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + metadata::Extent::WORLD), + sourceCRS, targetCRS, offset_pm)); + // If only the target has a non-zero prime meridian, chain a + // null geographic offset and then the longitude rotation + } else if (src_pm.getSIValue() == 0 && dst_pm.getSIValue() != 0) { + auto datum = datum::GeodeticReferenceFrame::create( + util::PropertyMap(), geogDst->ellipsoid(), + util::optional(), geogSrc->primeMeridian()); + std::string interm_crs_name(geogDst->nameStr()); + interm_crs_name += " altered to use prime meridian of "; + interm_crs_name += geogSrc->nameStr(); + auto interm_crs = + util::nn_static_pointer_cast(crs::GeographicCRS::create( + util::PropertyMap() + .set(common::IdentifiedObject::NAME_KEY, interm_crs_name) + .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + metadata::Extent::WORLD), + datum, dstCS)); + + steps.emplace_back( + createBallparkGeographicOffset(sourceCRS, interm_crs, dbContext)); + + steps.emplace_back(Transformation::createLongitudeRotation( + util::PropertyMap() + .set(common::IdentifiedObject::NAME_KEY, + buildTransfName(geogSrc->nameStr(), interm_crs->nameStr())) + .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + metadata::Extent::WORLD), + interm_crs, targetCRS, offset_pm)); + + } else { + // If the prime meridians are different, chain a longitude + // rotation and the null geographic offset. + if (src_pm.getSIValue() != dst_pm.getSIValue()) { + auto datum = datum::GeodeticReferenceFrame::create( + util::PropertyMap(), geogSrc->ellipsoid(), + util::optional(), geogDst->primeMeridian()); + std::string interm_crs_name(geogSrc->nameStr()); + interm_crs_name += " altered to use prime meridian of "; + interm_crs_name += geogDst->nameStr(); + auto interm_crs = util::nn_static_pointer_cast( + crs::GeographicCRS::create( + util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, + interm_crs_name), + datum, srcCS)); + + steps.emplace_back(Transformation::createLongitudeRotation( + util::PropertyMap() + .set(common::IdentifiedObject::NAME_KEY, + buildTransfName(geogSrc->nameStr(), + interm_crs->nameStr())) + .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + metadata::Extent::WORLD), + sourceCRS, interm_crs, offset_pm)); + steps.emplace_back(createBallparkGeographicOffset( + interm_crs, targetCRS, dbContext)); + } else { + steps.emplace_back(createBallparkGeographicOffset( + sourceCRS, targetCRS, dbContext)); + } + } + + auto op = ConcatenatedOperation::createComputeMetadata( + steps, disallowEmptyIntersection); + op->setHasBallparkTransformation(!sameDatum); + res.emplace_back(op); + return res; +} + +// --------------------------------------------------------------------------- + +static bool hasIdentifiers(const CoordinateOperationNNPtr &op) { + if (!op->identifiers().empty()) { + return true; + } + auto concatenated = dynamic_cast(op.get()); + if (concatenated) { + for (const auto &subOp : concatenated->operations()) { + if (hasIdentifiers(subOp)) { + return true; + } + } + } + return false; +} + +// --------------------------------------------------------------------------- + +static std::vector +findCandidateGeodCRSForDatum(const io::AuthorityFactoryPtr &authFactory, + const crs::GeodeticCRS *crs, + const datum::GeodeticReferenceFrame *datum) { + std::vector candidates; + assert(datum); + const auto &ids = datum->identifiers(); + const auto &datumName = datum->nameStr(); + if (!ids.empty()) { + for (const auto &id : ids) { + const auto &authName = *(id->codeSpace()); + const auto &code = id->code(); + if (!authName.empty()) { + const auto crsIds = crs->identifiers(); + const auto tmpFactory = + (crsIds.size() == 1 && + *(crsIds.front()->codeSpace()) == authName) + ? io::AuthorityFactory::create( + authFactory->databaseContext(), authName) + .as_nullable() + : authFactory; + auto l_candidates = tmpFactory->createGeodeticCRSFromDatum( + authName, code, std::string()); + for (const auto &candidate : l_candidates) { + candidates.emplace_back(candidate); + } + } + } + } else if (datumName != "unknown" && datumName != "unnamed") { + auto matches = authFactory->createObjectsFromName( + datumName, + {io::AuthorityFactory::ObjectType::GEODETIC_REFERENCE_FRAME}, false, + 2); + if (matches.size() == 1) { + const auto &match = matches.front(); + if (datum->_isEquivalentTo( + match.get(), util::IComparable::Criterion::EQUIVALENT) && + !match->identifiers().empty()) { + return findCandidateGeodCRSForDatum( + authFactory, crs, + dynamic_cast( + match.get())); + } + } + } + return candidates; +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::setCRSs( + CoordinateOperation *co, const crs::CRSNNPtr &sourceCRS, + const crs::CRSNNPtr &targetCRS) { + co->setCRSs(sourceCRS, targetCRS, nullptr); + + auto invCO = dynamic_cast(co); + if (invCO) { + invCO->forwardOperation()->setCRSs(targetCRS, sourceCRS, nullptr); + } + + auto transf = dynamic_cast(co); + if (transf) { + transf->inverseAsTransformation()->setCRSs(targetCRS, sourceCRS, + nullptr); + } + + auto concat = dynamic_cast(co); + if (concat) { + auto first = concat->operations().front().get(); + auto &firstTarget(first->targetCRS()); + if (firstTarget) { + setCRSs(first, sourceCRS, NN_NO_CHECK(firstTarget)); + } + auto last = concat->operations().back().get(); + auto &lastSource(last->sourceCRS()); + if (lastSource) { + setCRSs(last, NN_NO_CHECK(lastSource), targetCRS); + } + } +} + +// --------------------------------------------------------------------------- + +static bool hasResultSetOnlyResultsWithPROJStep( + const std::vector &res) { + for (const auto &op : res) { + auto concat = dynamic_cast(op.get()); + if (concat) { + bool hasPROJStep = false; + const auto &steps = concat->operations(); + for (const auto &step : steps) { + const auto &ids = step->identifiers(); + if (!ids.empty()) { + const auto &opAuthority = *(ids.front()->codeSpace()); + if (opAuthority == "PROJ" || + opAuthority == "INVERSE(PROJ)" || + opAuthority == "DERIVED_FROM(PROJ)") { + hasPROJStep = true; + break; + } + } + } + if (!hasPROJStep) { + return false; + } + } else { + return false; + } + } + return true; +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsWithDatumPivot( + std::vector &res, const crs::CRSNNPtr &sourceCRS, + const crs::CRSNNPtr &targetCRS, const crs::GeodeticCRS *geodSrc, + const crs::GeodeticCRS *geodDst, Private::Context &context) { + +#ifdef TRACE_CREATE_OPERATIONS + ENTER_BLOCK("createOperationsWithDatumPivot(" + + objectAsStr(sourceCRS.get()) + "," + + objectAsStr(targetCRS.get()) + ")"); +#endif + + struct CreateOperationsWithDatumPivotAntiRecursion { + Context &context; + + explicit CreateOperationsWithDatumPivotAntiRecursion(Context &contextIn) + : context(contextIn) { + assert(!context.inCreateOperationsWithDatumPivotAntiRecursion); + context.inCreateOperationsWithDatumPivotAntiRecursion = true; + } + + ~CreateOperationsWithDatumPivotAntiRecursion() { + context.inCreateOperationsWithDatumPivotAntiRecursion = false; + } + }; + CreateOperationsWithDatumPivotAntiRecursion guard(context); + + const auto &authFactory = context.context->getAuthorityFactory(); + const auto &dbContext = authFactory->databaseContext(); + + const auto candidatesSrcGeod(findCandidateGeodCRSForDatum( + authFactory, geodSrc, + geodSrc->datumNonNull(dbContext.as_nullable()).get())); + const auto candidatesDstGeod(findCandidateGeodCRSForDatum( + authFactory, geodDst, + geodDst->datumNonNull(dbContext.as_nullable()).get())); + + const bool sourceAndTargetAre3D = + geodSrc->coordinateSystem()->axisList().size() == 3 && + geodDst->coordinateSystem()->axisList().size() == 3; + + 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(opSecond.get()); + if (so && isAxisOrderReversal(so->method()->getEPSGCode())) { + continue; + } + + std::vector subOps; + const bool isNullThird = + isNullTransformation(opsThird[0]->nameStr()); + CoordinateOperationNNPtr opSecondCloned( + (isNullFirst || isNullThird || sourceAndTargetAre3D) + ? opSecond->shallowClone() + : opSecond); + if (isNullFirst || isNullThird) { + if (opSecondCloned->identifiers().size() == 1 && + (*opSecondCloned->identifiers()[0]->codeSpace()) + .find("DERIVED_FROM") == std::string::npos) { + { + util::PropertyMap map; + addModifiedIdentifier(map, opSecondCloned.get(), false, + true); + opSecondCloned->setProperties(map); + } + auto invCO = dynamic_cast( + opSecondCloned.get()); + if (invCO) { + auto invCOForward = invCO->forwardOperation().get(); + if (invCOForward->identifiers().size() == 1 && + (*invCOForward->identifiers()[0]->codeSpace()) + .find("DERIVED_FROM") == + std::string::npos) { + util::PropertyMap map; + addModifiedIdentifier(map, invCOForward, false, + true); + invCOForward->setProperties(map); + } + } + } + } + if (sourceAndTargetAre3D) { + opSecondCloned->getPrivate()->use3DHelmert_ = true; + auto invCO = dynamic_cast( + opSecondCloned.get()); + if (invCO) { + auto invCOForward = invCO->forwardOperation().get(); + invCOForward->getPrivate()->use3DHelmert_ = true; + } + } + if (isNullFirst) { + auto oldTarget(NN_CHECK_ASSERT(opSecondCloned->targetCRS())); + setCRSs(opSecondCloned.get(), sourceCRS, oldTarget); + } else { + subOps.emplace_back(opFirst); + } + if (isNullThird) { + auto oldSource(NN_CHECK_ASSERT(opSecondCloned->sourceCRS())); + setCRSs(opSecondCloned.get(), oldSource, targetCRS); + subOps.emplace_back(opSecondCloned); + } else { + subOps.emplace_back(opSecondCloned); + subOps.emplace_back(opsThird[0]); + } +#ifdef TRACE_CREATE_OPERATIONS + std::string debugStr; + for (const auto &op : subOps) { + if (!debugStr.empty()) { + debugStr += " + "; + } + debugStr += objectAsStr(op.get()); + debugStr += " ("; + debugStr += objectAsStr(op->sourceCRS().get()); + debugStr += "->"; + debugStr += objectAsStr(op->targetCRS().get()); + debugStr += ")"; + } + logTrace("transformation " + debugStr); +#endif + try { + res.emplace_back(ConcatenatedOperation::createComputeMetadata( + subOps, disallowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } + }; + + // Start in priority with candidates that have exactly the same name as + // the sourcCRS and targetCRS. Typically for the case of init=IGNF:XXXX + + // Transformation from IGNF:NTFP to IGNF:RGF93G, + // using + // NTF geographiques Paris (gr) vers NTF GEOGRAPHIQUES GREENWICH (DMS) + + // NOUVELLE TRIANGULATION DE LA FRANCE (NTF) vers RGF93 (ETRS89) + // that is using ntf_r93.gsb, is horribly dependent + // of IGNF:RGF93G being returned before IGNF:RGF93GEO in candidatesDstGeod. + // If RGF93GEO is returned before then we go through WGS84 and use + // instead a Helmert transformation. + // The below logic is thus quite fragile, and attempts at changing it + // result in degraded results for other use cases... + + for (const auto &candidateSrcGeod : candidatesSrcGeod) { + if (candidateSrcGeod->nameStr() == sourceCRS->nameStr()) { + for (const auto &candidateDstGeod : candidatesDstGeod) { + if (candidateDstGeod->nameStr() == targetCRS->nameStr()) { +#ifdef TRACE_CREATE_OPERATIONS + ENTER_BLOCK("try " + objectAsStr(sourceCRS.get()) + "->" + + objectAsStr(candidateSrcGeod.get()) + "->" + + objectAsStr(candidateDstGeod.get()) + "->" + + objectAsStr(targetCRS.get()) + ")"); +#endif + 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()) { + if (hasResultSetOnlyResultsWithPROJStep(res)) { + continue; + } + return; + } + } + } + } + } + + for (const auto &candidateSrcGeod : candidatesSrcGeod) { + const bool bSameSrcName = + candidateSrcGeod->nameStr() == sourceCRS->nameStr(); +#ifdef TRACE_CREATE_OPERATIONS + ENTER_BLOCK(""); +#endif + const auto opsFirst = + createOperations(sourceCRS, candidateSrcGeod, context); + assert(!opsFirst.empty()); + const bool isNullFirst = isNullTransformation(opsFirst[0]->nameStr()); + + for (const auto &candidateDstGeod : candidatesDstGeod) { + if (bSameSrcName && + candidateDstGeod->nameStr() == targetCRS->nameStr()) { + continue; + } + +#ifdef TRACE_CREATE_OPERATIONS + ENTER_BLOCK("try " + objectAsStr(sourceCRS.get()) + "->" + + objectAsStr(candidateSrcGeod.get()) + "->" + + objectAsStr(candidateDstGeod.get()) + "->" + + objectAsStr(targetCRS.get()) + ")"); +#endif + createTransformations(candidateSrcGeod, candidateDstGeod, + opsFirst[0], isNullFirst); + if (!res.empty() && !hasResultSetOnlyResultsWithPROJStep(res)) { + return; + } + } + } +} + +// --------------------------------------------------------------------------- + +static CoordinateOperationNNPtr +createBallparkGeocentricTranslation(const crs::CRSNNPtr &sourceCRS, + const crs::CRSNNPtr &targetCRS) { + std::string name(BALLPARK_GEOCENTRIC_TRANSLATION); + name += " from "; + name += sourceCRS->nameStr(); + name += " to "; + name += targetCRS->nameStr(); + + return util::nn_static_pointer_cast( + Transformation::createGeocentricTranslations( + util::PropertyMap() + .set(common::IdentifiedObject::NAME_KEY, name) + .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + metadata::Extent::WORLD), + sourceCRS, targetCRS, 0.0, 0.0, 0.0, {})); +} + +// --------------------------------------------------------------------------- + +bool CoordinateOperationFactory::Private::hasPerfectAccuracyResult( + const std::vector &res, const Context &context) { + auto resTmp = FilterResults(res, context.context, context.extent1, + context.extent2, true) + .getRes(); + for (const auto &op : resTmp) { + const double acc = getAccuracy(op); + if (acc == 0.0) { + return true; + } + } + return false; +} + +// --------------------------------------------------------------------------- + +std::vector +CoordinateOperationFactory::Private::createOperations( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context) { + +#ifdef TRACE_CREATE_OPERATIONS + ENTER_BLOCK("createOperations(" + objectAsStr(sourceCRS.get()) + " --> " + + objectAsStr(targetCRS.get()) + ")"); +#endif + + std::vector res; + + auto boundSrc = dynamic_cast(sourceCRS.get()); + auto boundDst = dynamic_cast(targetCRS.get()); + + const auto &sourceProj4Ext = boundSrc + ? boundSrc->baseCRS()->getExtensionProj4() + : sourceCRS->getExtensionProj4(); + const auto &targetProj4Ext = boundDst + ? boundDst->baseCRS()->getExtensionProj4() + : targetCRS->getExtensionProj4(); + if (!sourceProj4Ext.empty() || !targetProj4Ext.empty()) { + createOperationsFromProj4Ext(sourceCRS, targetCRS, boundSrc, boundDst, + res); + return res; + } + + auto geodSrc = dynamic_cast(sourceCRS.get()); + auto geodDst = dynamic_cast(targetCRS.get()); + auto geogSrc = dynamic_cast(sourceCRS.get()); + auto geogDst = dynamic_cast(targetCRS.get()); + auto vertSrc = dynamic_cast(sourceCRS.get()); + auto vertDst = dynamic_cast(targetCRS.get()); + + // First look-up if the registry provide us with operations. + auto derivedSrc = dynamic_cast(sourceCRS.get()); + auto derivedDst = dynamic_cast(targetCRS.get()); + const auto &authFactory = context.context->getAuthorityFactory(); + if (authFactory && + (derivedSrc == nullptr || + !derivedSrc->baseCRS()->_isEquivalentTo( + targetCRS.get(), util::IComparable::Criterion::EQUIVALENT)) && + (derivedDst == nullptr || + !derivedDst->baseCRS()->_isEquivalentTo( + sourceCRS.get(), util::IComparable::Criterion::EQUIVALENT))) { + + if (createOperationsFromDatabase(sourceCRS, targetCRS, context, geodSrc, + geodDst, geogSrc, geogDst, vertSrc, + vertDst, res)) { + return res; + } + } + + // Special case if both CRS are geodetic + if (geodSrc && geodDst && !derivedSrc && !derivedDst) { + createOperationsGeodToGeod(sourceCRS, targetCRS, context, geodSrc, + geodDst, res); + return res; + } + + // If the source is a derived CRS, then chain the inverse of its + // deriving conversion, with transforms from its baseCRS to the + // targetCRS + if (derivedSrc) { + createOperationsDerivedTo(sourceCRS, targetCRS, context, derivedSrc, + res); + return res; + } + + // reverse of previous case + if (derivedDst) { + return applyInverse(createOperations(targetCRS, sourceCRS, context)); + } + + // Order of comparison between the geogDst vs geodDst is impotant + if (boundSrc && geogDst) { + createOperationsBoundToGeog(sourceCRS, targetCRS, context, boundSrc, + geogDst, res); + return res; + } else if (boundSrc && geodDst) { + createOperationsToGeod(sourceCRS, targetCRS, context, geodDst, res); + return res; + } + + // reverse of previous case + if (geodSrc && boundDst) { + return applyInverse(createOperations(targetCRS, sourceCRS, context)); + } + + // vertCRS (as boundCRS with transformation to target vertCRS) to + // vertCRS + if (boundSrc && vertDst) { + createOperationsBoundToVert(sourceCRS, targetCRS, context, boundSrc, + vertDst, res); + return res; + } + + // reverse of previous case + if (boundDst && vertSrc) { + return applyInverse(createOperations(targetCRS, sourceCRS, context)); + } + + if (vertSrc && vertDst) { + createOperationsVertToVert(sourceCRS, targetCRS, context, vertSrc, + vertDst, res); + return res; + } + + // A bit odd case as we are comparing apples to oranges, but in case + // the vertical unit differ, do something useful. + if (vertSrc && geogDst) { + createOperationsVertToGeog(sourceCRS, targetCRS, context, vertSrc, + geogDst, res); + return res; + } + + // reverse of previous case + if (vertDst && geogSrc) { + return applyInverse(createOperations(targetCRS, sourceCRS, context)); + } + + // boundCRS to boundCRS + if (boundSrc && boundDst) { + createOperationsBoundToBound(sourceCRS, targetCRS, context, boundSrc, + boundDst, res); + return res; + } + + auto compoundSrc = dynamic_cast(sourceCRS.get()); + // Order of comparison between the geogDst vs geodDst is impotant + if (compoundSrc && geogDst) { + createOperationsCompoundToGeog(sourceCRS, targetCRS, context, + compoundSrc, geogDst, res); + return res; + } else if (compoundSrc && geodDst) { + createOperationsToGeod(sourceCRS, targetCRS, context, geodDst, res); + return res; + } + + // reverse of previous cases + auto compoundDst = dynamic_cast(targetCRS.get()); + if (geodSrc && compoundDst) { + return applyInverse(createOperations(targetCRS, sourceCRS, context)); + } + + if (compoundSrc && compoundDst) { + createOperationsCompoundToCompound(sourceCRS, targetCRS, context, + compoundSrc, compoundDst, res); + return res; + } + + // '+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +type=crs' to + // '+proj=longlat +ellps=GRS80 +nadgrids=@bar.gsb +geoidgrids=@bar.gtx + // +type=crs' + if (boundSrc && compoundDst) { + createOperationsBoundToCompound(sourceCRS, targetCRS, context, boundSrc, + compoundDst, res); + return res; + } + + // reverse of previous case + if (boundDst && compoundSrc) { + return applyInverse(createOperations(targetCRS, sourceCRS, context)); + } + + return res; +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsFromProj4Ext( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + const crs::BoundCRS *boundSrc, const crs::BoundCRS *boundDst, + std::vector &res) { + + ENTER_FUNCTION(); + + auto sourceProjExportable = dynamic_cast( + boundSrc ? boundSrc : sourceCRS.get()); + auto targetProjExportable = dynamic_cast( + boundDst ? boundDst : 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(); + projFormatter->setCRSExport(true); + projFormatter->setLegacyCRSToCRSContext(true); + projFormatter->startInversion(); + sourceProjExportable->_exportToPROJString(projFormatter.get()); + auto geogSrc = dynamic_cast(sourceCRS.get()); + if (geogSrc) { + auto tmpFormatter = io::PROJStringFormatter::create(); + geogSrc->addAngularUnitConvertAndAxisSwap(tmpFormatter.get()); + projFormatter->ingestPROJString(tmpFormatter->toString()); + } + + projFormatter->stopInversion(); + + targetProjExportable->_exportToPROJString(projFormatter.get()); + auto geogDst = dynamic_cast(targetCRS.get()); + if (geogDst) { + auto tmpFormatter = io::PROJStringFormatter::create(); + geogDst->addAngularUnitConvertAndAxisSwap(tmpFormatter.get()); + projFormatter->ingestPROJString(tmpFormatter->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, {})); +} + +// --------------------------------------------------------------------------- + +bool CoordinateOperationFactory::Private::createOperationsFromDatabase( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::GeodeticCRS *geodSrc, + const crs::GeodeticCRS *geodDst, const crs::GeographicCRS *geogSrc, + const crs::GeographicCRS *geogDst, const crs::VerticalCRS *vertSrc, + const crs::VerticalCRS *vertDst, + std::vector &res) { + + ENTER_FUNCTION(); + + if (geogSrc && vertDst) { + createOperationsFromDatabase(targetCRS, sourceCRS, context, geodDst, + geodSrc, geogDst, geogSrc, vertDst, + vertSrc, res); + res = applyInverse(res); + } else if (geogDst && vertSrc) { + res = applyInverse(createOperationsGeogToVertFromGeoid( + targetCRS, sourceCRS, vertSrc, context)); + if (!res.empty()) { + createOperationsVertToGeogBallpark(sourceCRS, targetCRS, context, + vertSrc, geogDst, res); + } + } + + if (!res.empty()) { + return true; + } + + bool resFindDirectNonEmptyBeforeFiltering = false; + res = findOpsInRegistryDirect(sourceCRS, targetCRS, context, + resFindDirectNonEmptyBeforeFiltering); + + // If we get at least a result with perfect accuracy, do not + // bother generating synthetic transforms. + if (hasPerfectAccuracyResult(res, context)) { + return true; + } + + bool doFilterAndCheckPerfectOp = false; + + bool sameGeodeticDatum = false; + + if (vertSrc || vertDst) { + if (res.empty()) { + if (geogSrc && + geogSrc->coordinateSystem()->axisList().size() == 2 && + vertDst) { + auto dbContext = + context.context->getAuthorityFactory()->databaseContext(); + auto resTmp = findOpsInRegistryDirect( + sourceCRS->promoteTo3D(std::string(), dbContext), targetCRS, + context, resFindDirectNonEmptyBeforeFiltering); + for (auto &op : resTmp) { + auto newOp = op->shallowClone(); + setCRSs(newOp.get(), sourceCRS, targetCRS); + res.emplace_back(newOp); + } + } else if (geogDst && + geogDst->coordinateSystem()->axisList().size() == 2 && + vertSrc) { + auto dbContext = + context.context->getAuthorityFactory()->databaseContext(); + auto resTmp = findOpsInRegistryDirect( + sourceCRS, targetCRS->promoteTo3D(std::string(), dbContext), + context, resFindDirectNonEmptyBeforeFiltering); + for (auto &op : resTmp) { + auto newOp = op->shallowClone(); + setCRSs(newOp.get(), sourceCRS, targetCRS); + res.emplace_back(newOp); + } + } + } + if (res.empty()) { + createOperationsFromDatabaseWithVertCRS(sourceCRS, targetCRS, + context, geogSrc, geogDst, + vertSrc, vertDst, res); + } + } else if (geodSrc && geodDst) { + + const auto &authFactory = context.context->getAuthorityFactory(); + const auto dbContext = authFactory->databaseContext().as_nullable(); + + const auto srcDatum = geodSrc->datumNonNull(dbContext); + const auto dstDatum = geodDst->datumNonNull(dbContext); + sameGeodeticDatum = srcDatum->_isEquivalentTo( + dstDatum.get(), util::IComparable::Criterion::EQUIVALENT); + + if (res.empty() && !sameGeodeticDatum && + !context.inCreateOperationsWithDatumPivotAntiRecursion) { + // If we still didn't find a transformation, and that the source + // and target are GeodeticCRS, then go through their underlying + // datum to find potential transformations between other + // GeodeticCRSs + // that are made of those datum + // The typical example is if transforming between two + // GeographicCRS, + // but transformations are only available between their + // corresponding geocentric CRS. + createOperationsWithDatumPivot(res, sourceCRS, targetCRS, geodSrc, + geodDst, context); + doFilterAndCheckPerfectOp = !res.empty(); + } + } + + bool foundInstantiableOp = false; + // FIXME: the limitation to .size() == 1 is just for the + // -s EPSG:4959+5759 -t "EPSG:4959+7839" case + // finding EPSG:7860 'NZVD2016 height to Auckland 1946 + // height (1)', which uses the EPSG:1071 'Vertical Offset by Grid + // Interpolation (NZLVD)' method which is not currently implemented by PROJ + // (cannot deal with .csv files) + // Initially the test was written to iterate over for all operations of a + // non-empty res, but this causes failures in the test suite when no grids + // are installed at all. Ideally we should tweak the test suite to be + // robust to that, or skip some tests. + if (res.size() == 1) { + try { + res.front()->exportToPROJString( + io::PROJStringFormatter::create().get()); + foundInstantiableOp = true; + } catch (const std::exception &) { + } + if (!foundInstantiableOp) { + resFindDirectNonEmptyBeforeFiltering = false; + } + } else if (res.size() > 1) { + foundInstantiableOp = true; + } + + // NAD27 to NAD83 has tens of results already. No need to look + // for a pivot + if (!sameGeodeticDatum && + (((res.empty() || !foundInstantiableOp) && + !resFindDirectNonEmptyBeforeFiltering && + context.context->getAllowUseIntermediateCRS() == + CoordinateOperationContext::IntermediateCRSUse:: + IF_NO_DIRECT_TRANSFORMATION) || + context.context->getAllowUseIntermediateCRS() == + CoordinateOperationContext::IntermediateCRSUse::ALWAYS || + getenv("PROJ_FORCE_SEARCH_PIVOT"))) { + auto resWithIntermediate = findsOpsInRegistryWithIntermediate( + sourceCRS, targetCRS, context, false); + res.insert(res.end(), resWithIntermediate.begin(), + resWithIntermediate.end()); + doFilterAndCheckPerfectOp = !res.empty(); + + } else if (!context.inCreateOperationsWithDatumPivotAntiRecursion && + !resFindDirectNonEmptyBeforeFiltering && geodSrc && geodDst && + !sameGeodeticDatum && + context.context->getIntermediateCRS().empty() && + context.context->getAllowUseIntermediateCRS() != + CoordinateOperationContext::IntermediateCRSUse::NEVER) { + + bool tryWithGeodeticDatumIntermediate = res.empty(); + if (!tryWithGeodeticDatumIntermediate) { + // This is in particular for the GDA94 to WGS 84 (G1762) case + // As we have a WGS 84 -> WGS 84 (G1762) null-transformation in the + // PROJ authority, previous steps might have use that WGS 84 + // intermediate directly. They might also have generated a path + // through ITRF2008, as there is a path + // GDA94 (geoc.) -> ITRF2008 (geoc.) -> WGS84 (G1762) (geoc.) + // But there's a better path using + // GDA94 (geog.) --> GDA2020 (geog.) and + // GDA2020 (geoc.) -> WGS84 (G1762) (geoc.) that requires to + // explore intermediates through their datum, and not directly + // trough the CRS code. + // Do that only if the number of results we got through other + // algorithms is small, or if all results we have go through an + // operation in the PROJ authority. + constexpr size_t ARBITRARY_SMALL_NUMBER = 5U; + tryWithGeodeticDatumIntermediate = + res.size() < ARBITRARY_SMALL_NUMBER || + hasResultSetOnlyResultsWithPROJStep(res); + } + if (tryWithGeodeticDatumIntermediate) { + auto resWithIntermediate = findsOpsInRegistryWithIntermediate( + sourceCRS, targetCRS, context, true); + res.insert(res.end(), resWithIntermediate.begin(), + resWithIntermediate.end()); + doFilterAndCheckPerfectOp = !res.empty(); + } + } + + if (doFilterAndCheckPerfectOp) { + // If we get at least a result with perfect accuracy, do not bother + // generating synthetic transforms. + if (hasPerfectAccuracyResult(res, context)) { + return true; + } + } + return false; +} + +// --------------------------------------------------------------------------- + +static std::vector +findCandidateVertCRSForDatum(const io::AuthorityFactoryPtr &authFactory, + const datum::VerticalReferenceFrame *datum) { + std::vector candidates; + assert(datum); + const auto &ids = datum->identifiers(); + const auto &datumName = datum->nameStr(); + if (!ids.empty()) { + for (const auto &id : ids) { + const auto &authName = *(id->codeSpace()); + const auto &code = id->code(); + if (!authName.empty()) { + auto l_candidates = + authFactory->createVerticalCRSFromDatum(authName, code); + for (const auto &candidate : l_candidates) { + candidates.emplace_back(candidate); + } + } + } + } else if (datumName != "unknown" && datumName != "unnamed") { + auto matches = authFactory->createObjectsFromName( + datumName, + {io::AuthorityFactory::ObjectType::VERTICAL_REFERENCE_FRAME}, false, + 2); + if (matches.size() == 1) { + const auto &match = matches.front(); + if (datum->_isEquivalentTo( + match.get(), util::IComparable::Criterion::EQUIVALENT) && + !match->identifiers().empty()) { + return findCandidateVertCRSForDatum( + authFactory, + dynamic_cast( + match.get())); + } + } + } + return candidates; +} + +// --------------------------------------------------------------------------- + +std::vector +CoordinateOperationFactory::Private::createOperationsGeogToVertFromGeoid( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + const crs::VerticalCRS *vertDst, Private::Context &context) { + + ENTER_FUNCTION(); + + const auto useTransf = [&targetCRS, &context, + vertDst](const CoordinateOperationNNPtr &op) { + const auto targetOp = + dynamic_cast(op->targetCRS().get()); + assert(targetOp); + if (targetOp->_isEquivalentTo( + vertDst, util::IComparable::Criterion::EQUIVALENT)) { + return op; + } + std::vector tmp; + createOperationsVertToVert(NN_NO_CHECK(op->targetCRS()), targetCRS, + context, targetOp, vertDst, tmp); + assert(!tmp.empty()); + auto ret = ConcatenatedOperation::createComputeMetadata( + {op, tmp.front()}, disallowEmptyIntersection); + return ret; + }; + + const auto getProjGeoidTransformation = [&sourceCRS, &targetCRS, &vertDst, + &context]( + const CoordinateOperationNNPtr &model, + const std::string &projFilename) { + + const auto getNameVertCRSMetre = [](const std::string &name) { + if (name.empty()) + return std::string("unnamed"); + auto ret(name); + bool haveOriginalUnit = false; + if (name.back() == ')') { + const auto pos = ret.rfind(" ("); + if (pos != std::string::npos) { + haveOriginalUnit = true; + ret = ret.substr(0, pos); + } + } + const auto pos = ret.rfind(" depth"); + if (pos != std::string::npos) { + ret = ret.substr(0, pos) + " height"; + } + if (!haveOriginalUnit) { + ret += " (metre)"; + } + return ret; + }; + + const auto &axis = vertDst->coordinateSystem()->axisList()[0]; + const auto geogSrcCRS = + dynamic_cast(model->interpolationCRS().get()) + ? NN_NO_CHECK(model->interpolationCRS()) + : sourceCRS; + const auto vertCRSMetre = + axis->unit() == common::UnitOfMeasure::METRE && + axis->direction() == cs::AxisDirection::UP + ? targetCRS + : util::nn_static_pointer_cast( + crs::VerticalCRS::create( + util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, + getNameVertCRSMetre(targetCRS->nameStr())), + vertDst->datum(), vertDst->datumEnsemble(), + cs::VerticalCS::createGravityRelatedHeight( + common::UnitOfMeasure::METRE))); + const auto properties = util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, + buildOpName("Transformation", vertCRSMetre, geogSrcCRS)); + + // Try to find a representative value for the accuracy of this grid + // from the registered transformations. + std::vector accuracies; + const auto &modelAccuracies = model->coordinateOperationAccuracies(); + if (modelAccuracies.empty()) { + const auto &authFactory = context.context->getAuthorityFactory(); + if (authFactory) { + const auto transformationsForGrid = + io::DatabaseContext::getTransformationsForGridName( + authFactory->databaseContext(), projFilename); + double accuracy = -1; + for (const auto &transf : transformationsForGrid) { + accuracy = std::max(accuracy, getAccuracy(transf)); + } + if (accuracy >= 0) { + accuracies.emplace_back( + metadata::PositionalAccuracy::create( + toString(accuracy))); + } + } + } + + return Transformation::createGravityRelatedHeightToGeographic3D( + properties, vertCRSMetre, geogSrcCRS, nullptr, projFilename, + !modelAccuracies.empty() ? modelAccuracies : accuracies); + }; + + std::vector res; + const auto &authFactory = context.context->getAuthorityFactory(); + if (authFactory) { + const auto &models = vertDst->geoidModel(); + for (const auto &model : models) { + const auto &modelName = model->nameStr(); + const auto transformations = + starts_with(modelName, "PROJ ") + ? std::vector< + CoordinateOperationNNPtr>{getProjGeoidTransformation( + model, modelName.substr(strlen("PROJ ")))} + : authFactory->getTransformationsForGeoid( + modelName, + context.context->getUsePROJAlternativeGridNames()); + for (const auto &transf : transformations) { + if (dynamic_cast( + transf->sourceCRS().get()) && + dynamic_cast( + transf->targetCRS().get())) { + res.push_back(useTransf(transf)); + } else if (dynamic_cast( + transf->targetCRS().get()) && + dynamic_cast( + transf->sourceCRS().get())) { + res.push_back(useTransf(transf->inverse())); + } + } + } + } + + return res; +} + +// --------------------------------------------------------------------------- + +std::vector CoordinateOperationFactory::Private:: + createOperationsGeogToVertWithIntermediateVert( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + const crs::VerticalCRS *vertDst, Private::Context &context) { + + ENTER_FUNCTION(); + + std::vector res; + + struct AntiRecursionGuard { + Context &context; + + explicit AntiRecursionGuard(Context &contextIn) : context(contextIn) { + assert(!context.inCreateOperationsGeogToVertWithIntermediateVert); + context.inCreateOperationsGeogToVertWithIntermediateVert = true; + } + + ~AntiRecursionGuard() { + context.inCreateOperationsGeogToVertWithIntermediateVert = false; + } + }; + AntiRecursionGuard guard(context); + const auto &authFactory = context.context->getAuthorityFactory(); + const auto dbContext = authFactory->databaseContext().as_nullable(); + + auto candidatesVert = findCandidateVertCRSForDatum( + authFactory, vertDst->datumNonNull(dbContext).get()); + for (const auto &candidateVert : candidatesVert) { + auto resTmp = createOperations(sourceCRS, candidateVert, context); + if (!resTmp.empty()) { + const auto opsSecond = + createOperations(candidateVert, targetCRS, context); + if (!opsSecond.empty()) { + // The transformation from candidateVert to targetCRS should + // be just a unit change typically, so take only the first one, + // which is likely/hopefully the only one. + for (const auto &opFirst : resTmp) { + if (hasIdentifiers(opFirst)) { + if (candidateVert->_isEquivalentTo( + targetCRS.get(), + util::IComparable::Criterion::EQUIVALENT)) { + res.emplace_back(opFirst); + } else { + res.emplace_back( + ConcatenatedOperation::createComputeMetadata( + {opFirst, opsSecond.front()}, + disallowEmptyIntersection)); + } + } + } + if (!res.empty()) + break; + } + } + } + + return res; +} + +// --------------------------------------------------------------------------- + +std::vector CoordinateOperationFactory::Private:: + createOperationsGeogToVertWithAlternativeGeog( + const crs::CRSNNPtr & /*sourceCRS*/, // geographic CRS + const crs::CRSNNPtr &targetCRS, // vertical CRS + Private::Context &context) { + + ENTER_FUNCTION(); + + std::vector res; + + struct AntiRecursionGuard { + Context &context; + + explicit AntiRecursionGuard(Context &contextIn) : context(contextIn) { + assert(!context.inCreateOperationsGeogToVertWithAlternativeGeog); + context.inCreateOperationsGeogToVertWithAlternativeGeog = true; + } + + ~AntiRecursionGuard() { + context.inCreateOperationsGeogToVertWithAlternativeGeog = false; + } + }; + AntiRecursionGuard guard(context); + + // Generally EPSG has operations from GeogCrs to VertCRS + auto ops = findOpsInRegistryDirectTo(targetCRS, context); + + for (const auto &op : ops) { + const auto tmpCRS = op->sourceCRS(); + if (tmpCRS && dynamic_cast(tmpCRS.get())) { + res.emplace_back(op); + } + } + + return res; +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private:: + createOperationsFromDatabaseWithVertCRS( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::GeographicCRS *geogSrc, + const crs::GeographicCRS *geogDst, const crs::VerticalCRS *vertSrc, + const crs::VerticalCRS *vertDst, + std::vector &res) { + + // Typically to transform from "NAVD88 height (ftUS)" to a geog CRS + // by using transformations of "NAVD88 height" (metre) to that geog CRS + if (res.empty() && + !context.inCreateOperationsGeogToVertWithIntermediateVert && geogSrc && + vertDst) { + res = createOperationsGeogToVertWithIntermediateVert( + sourceCRS, targetCRS, vertDst, context); + } else if (res.empty() && + !context.inCreateOperationsGeogToVertWithIntermediateVert && + geogDst && vertSrc) { + res = applyInverse(createOperationsGeogToVertWithIntermediateVert( + targetCRS, sourceCRS, vertSrc, context)); + } + + // NAD83 only exists in 2D version in EPSG, so if it has been + // promoted to 3D, when researching a vertical to geog + // transformation, try to down cast to 2D. + const auto geog3DToVertTryThroughGeog2D = [&res, &context]( + const crs::GeographicCRS *geogSrcIn, const crs::VerticalCRS *vertDstIn, + const crs::CRSNNPtr &targetCRSIn) { + if (res.empty() && geogSrcIn && vertDstIn && + geogSrcIn->coordinateSystem()->axisList().size() == 3) { + const auto &authFactory = context.context->getAuthorityFactory(); + const auto dbContext = + authFactory ? authFactory->databaseContext().as_nullable() + : nullptr; + const auto candidatesSrcGeod(findCandidateGeodCRSForDatum( + authFactory, geogSrcIn, + geogSrcIn->datumNonNull(dbContext).get())); + for (const auto &candidate : candidatesSrcGeod) { + auto geogCandidate = + util::nn_dynamic_pointer_cast( + candidate); + if (geogCandidate && + geogCandidate->coordinateSystem()->axisList().size() == 2) { + bool ignored; + res = + findOpsInRegistryDirect(NN_NO_CHECK(geogCandidate), + targetCRSIn, context, ignored); + break; + } + } + return true; + } + return false; + }; + + if (geog3DToVertTryThroughGeog2D(geogSrc, vertDst, targetCRS)) { + // do nothing + } else if (geog3DToVertTryThroughGeog2D(geogDst, vertSrc, sourceCRS)) { + res = applyInverse(res); + } + + // There's no direct transformation from NAVD88 height to WGS84, + // so try to research all transformations from NAVD88 to another + // intermediate GeographicCRS. + if (res.empty() && + !context.inCreateOperationsGeogToVertWithAlternativeGeog && geogSrc && + vertDst) { + res = createOperationsGeogToVertWithAlternativeGeog(sourceCRS, + targetCRS, context); + } else if (res.empty() && + !context.inCreateOperationsGeogToVertWithAlternativeGeog && + geogDst && vertSrc) { + res = applyInverse(createOperationsGeogToVertWithAlternativeGeog( + targetCRS, sourceCRS, context)); + } +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsGeodToGeod( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::GeodeticCRS *geodSrc, + const crs::GeodeticCRS *geodDst, + std::vector &res) { + + ENTER_FUNCTION(); + + if (geodSrc->ellipsoid()->celestialBody() != + geodDst->ellipsoid()->celestialBody()) { + throw util::UnsupportedOperationException( + "Source and target ellipsoid do not belong to the same " + "celestial body"); + } + + auto geogSrc = dynamic_cast(geodSrc); + auto geogDst = dynamic_cast(geodDst); + + if (geogSrc && geogDst) { + createOperationsGeogToGeog(res, sourceCRS, targetCRS, context, geogSrc, + geogDst); + return; + } + + const bool isSrcGeocentric = geodSrc->isGeocentric(); + const bool isSrcGeographic = geogSrc != nullptr; + const bool isTargetGeocentric = geodDst->isGeocentric(); + const bool isTargetGeographic = geogDst != nullptr; + + const auto IsSameDatum = [&context, &geodSrc, &geodDst]() { + const auto &authFactory = context.context->getAuthorityFactory(); + const auto dbContext = + authFactory ? authFactory->databaseContext().as_nullable() + : nullptr; + + return geodSrc->datumNonNull(dbContext)->_isEquivalentTo( + geodDst->datumNonNull(dbContext).get(), + util::IComparable::Criterion::EQUIVALENT); + }; + + if (((isSrcGeocentric && isTargetGeographic) || + (isSrcGeographic && isTargetGeocentric))) { + + // Same datum ? + if (IsSameDatum()) { + res.emplace_back( + Conversion::createGeographicGeocentric(sourceCRS, targetCRS)); + } else if (isSrcGeocentric && geogDst) { + std::string interm_crs_name(geogDst->nameStr()); + interm_crs_name += " (geocentric)"; + auto interm_crs = + util::nn_static_pointer_cast(crs::GeodeticCRS::create( + addDomains(util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, + interm_crs_name), + geogDst), + geogDst->datum(), geogDst->datumEnsemble(), + NN_CHECK_ASSERT( + util::nn_dynamic_pointer_cast( + geodSrc->coordinateSystem())))); + auto opFirst = + createBallparkGeocentricTranslation(sourceCRS, interm_crs); + auto opSecond = + Conversion::createGeographicGeocentric(interm_crs, targetCRS); + res.emplace_back(ConcatenatedOperation::createComputeMetadata( + {opFirst, opSecond}, disallowEmptyIntersection)); + } else { + // Apply previous case in reverse way + std::vector resTmp; + createOperationsGeodToGeod(targetCRS, sourceCRS, context, geodDst, + geodSrc, resTmp); + assert(resTmp.size() == 1); + res.emplace_back(resTmp.front()->inverse()); + } + + return; + } + + if (isSrcGeocentric && isTargetGeocentric) { + if (sourceCRS->_isEquivalentTo( + targetCRS.get(), util::IComparable::Criterion::EQUIVALENT) || + IsSameDatum()) { + std::string name(NULL_GEOCENTRIC_TRANSLATION); + name += " from "; + name += sourceCRS->nameStr(); + name += " to "; + name += targetCRS->nameStr(); + res.emplace_back(Transformation::createGeocentricTranslations( + util::PropertyMap() + .set(common::IdentifiedObject::NAME_KEY, name) + .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + metadata::Extent::WORLD), + sourceCRS, targetCRS, 0.0, 0.0, 0.0, + {metadata::PositionalAccuracy::create("0")})); + } else { + res.emplace_back( + createBallparkGeocentricTranslation(sourceCRS, targetCRS)); + } + return; + } + + // Transformation between two geodetic systems of unknown type + // This should normally not be triggered with "standard" CRS + res.emplace_back(createGeodToGeodPROJBased(sourceCRS, targetCRS)); +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsDerivedTo( + const crs::CRSNNPtr & /*sourceCRS*/, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::DerivedCRS *derivedSrc, + std::vector &res) { + + ENTER_FUNCTION(); + + auto opFirst = derivedSrc->derivingConversion()->inverse(); + // Small optimization if the targetCRS is the baseCRS of the source + // derivedCRS. + if (derivedSrc->baseCRS()->_isEquivalentTo( + targetCRS.get(), util::IComparable::Criterion::EQUIVALENT)) { + res.emplace_back(opFirst); + return; + } + auto opsSecond = + createOperations(derivedSrc->baseCRS(), targetCRS, context); + for (const auto &opSecond : opsSecond) { + try { + res.emplace_back(ConcatenatedOperation::createComputeMetadata( + {opFirst, opSecond}, disallowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsBoundToGeog( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::BoundCRS *boundSrc, + const crs::GeographicCRS *geogDst, + std::vector &res) { + + ENTER_FUNCTION(); + + const auto &hubSrc = boundSrc->hubCRS(); + auto hubSrcGeog = dynamic_cast(hubSrc.get()); + auto geogCRSOfBaseOfBoundSrc = boundSrc->baseCRS()->extractGeographicCRS(); + { + // If geogCRSOfBaseOfBoundSrc is a DerivedGeographicCRS, use its base + // instead (if it is a GeographicCRS) + auto derivedGeogCRS = + std::dynamic_pointer_cast( + geogCRSOfBaseOfBoundSrc); + if (derivedGeogCRS) { + auto baseCRS = std::dynamic_pointer_cast( + derivedGeogCRS->baseCRS().as_nullable()); + if (baseCRS) { + geogCRSOfBaseOfBoundSrc = baseCRS; + } + } + } + + const auto &authFactory = context.context->getAuthorityFactory(); + const auto dbContext = + authFactory ? authFactory->databaseContext().as_nullable() : nullptr; + + const auto geogDstDatum = geogDst->datumNonNull(dbContext); + + // If the underlying datum of the source is the same as the target, do + // not consider the boundCRS at all, but just its base + if (geogCRSOfBaseOfBoundSrc) { + auto geogCRSOfBaseOfBoundSrcDatum = + geogCRSOfBaseOfBoundSrc->datumNonNull(dbContext); + if (geogCRSOfBaseOfBoundSrcDatum->_isEquivalentTo( + geogDstDatum.get(), util::IComparable::Criterion::EQUIVALENT)) { + res = createOperations(boundSrc->baseCRS(), targetCRS, context); + return; + } + } + + bool triedBoundCrsToGeogCRSSameAsHubCRS = false; + // Is it: boundCRS to a geogCRS that is the same as the hubCRS ? + if (hubSrcGeog && geogCRSOfBaseOfBoundSrc && + (hubSrcGeog->_isEquivalentTo( + geogDst, util::IComparable::Criterion::EQUIVALENT) || + hubSrcGeog->is2DPartOf3D(NN_NO_CHECK(geogDst), dbContext))) { + triedBoundCrsToGeogCRSSameAsHubCRS = true; + + CoordinateOperationPtr opIntermediate; + if (!geogCRSOfBaseOfBoundSrc->_isEquivalentTo( + boundSrc->transformation()->sourceCRS().get(), + util::IComparable::Criterion::EQUIVALENT)) { + auto opsIntermediate = createOperations( + NN_NO_CHECK(geogCRSOfBaseOfBoundSrc), + boundSrc->transformation()->sourceCRS(), context); + assert(!opsIntermediate.empty()); + opIntermediate = opsIntermediate.front(); + } + + if (boundSrc->baseCRS() == geogCRSOfBaseOfBoundSrc) { + if (opIntermediate) { + try { + res.emplace_back( + ConcatenatedOperation::createComputeMetadata( + {NN_NO_CHECK(opIntermediate), + boundSrc->transformation()}, + disallowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } else { + // Optimization to avoid creating a useless concatenated + // operation + res.emplace_back(boundSrc->transformation()); + } + return; + } + auto opsFirst = createOperations( + boundSrc->baseCRS(), NN_NO_CHECK(geogCRSOfBaseOfBoundSrc), context); + if (!opsFirst.empty()) { + for (const auto &opFirst : opsFirst) { + try { + std::vector subops; + subops.emplace_back(opFirst); + if (opIntermediate) { + subops.emplace_back(NN_NO_CHECK(opIntermediate)); + } + subops.emplace_back(boundSrc->transformation()); + res.emplace_back( + ConcatenatedOperation::createComputeMetadata( + subops, disallowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } + if (!res.empty()) { + return; + } + } + // If the datum are equivalent, this is also fine + } else if (geogCRSOfBaseOfBoundSrc && hubSrcGeog && + hubSrcGeog->datumNonNull(dbContext)->_isEquivalentTo( + geogDstDatum.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()) { + CoordinateOperationPtr opIntermediate; + if (!geogCRSOfBaseOfBoundSrc->_isEquivalentTo( + boundSrc->transformation()->sourceCRS().get(), + util::IComparable::Criterion::EQUIVALENT)) { + auto opsIntermediate = createOperations( + NN_NO_CHECK(geogCRSOfBaseOfBoundSrc), + boundSrc->transformation()->sourceCRS(), context); + assert(!opsIntermediate.empty()); + opIntermediate = opsIntermediate.front(); + } + for (const auto &opFirst : opsFirst) { + for (const auto &opLast : opsLast) { + try { + std::vector subops; + subops.emplace_back(opFirst); + if (opIntermediate) { + subops.emplace_back(NN_NO_CHECK(opIntermediate)); + } + subops.emplace_back(boundSrc->transformation()); + subops.emplace_back(opLast); + res.emplace_back( + ConcatenatedOperation::createComputeMetadata( + subops, disallowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } + } + if (!res.empty()) { + return; + } + } + // 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 && + geogCRSOfBaseOfBoundSrc->ellipsoid()->_isEquivalentTo( + datum::Ellipsoid::CLARKE_1866.get(), + util::IComparable::Criterion::EQUIVALENT) && + hubSrcGeog->datumNonNull(dbContext)->_isEquivalentTo( + datum::GeodeticReferenceFrame::EPSG_6326.get(), + util::IComparable::Criterion::EQUIVALENT) && + geogDstDatum->_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; + } 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}, disallowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } + if (!res.empty()) { + return; + } + } + } + } + + if (hubSrcGeog && + hubSrcGeog->_isEquivalentTo(geogDst, + util::IComparable::Criterion::EQUIVALENT) && + dynamic_cast(boundSrc->baseCRS().get())) { + auto transfSrc = boundSrc->transformation()->sourceCRS(); + if (dynamic_cast(transfSrc.get()) && + !boundSrc->baseCRS()->_isEquivalentTo( + transfSrc.get(), util::IComparable::Criterion::EQUIVALENT)) { + auto opsFirst = + createOperations(boundSrc->baseCRS(), transfSrc, context); + for (const auto &opFirst : opsFirst) { + try { + res.emplace_back( + ConcatenatedOperation::createComputeMetadata( + {opFirst, boundSrc->transformation()}, + disallowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } + return; + } + + res.emplace_back(boundSrc->transformation()); + return; + } + + if (!triedBoundCrsToGeogCRSSameAsHubCRS && hubSrcGeog && + geogCRSOfBaseOfBoundSrc) { + // This one should go to the above 'Is it: boundCRS to a geogCRS + // that is the same as the hubCRS ?' case + auto opsFirst = createOperations(sourceCRS, hubSrc, context); + auto opsLast = createOperations(hubSrc, targetCRS, context); + if (!opsFirst.empty() && !opsLast.empty()) { + for (const auto &opFirst : opsFirst) { + for (const auto &opLast : opsLast) { + // Exclude artificial transformations from the hub + // to the target CRS, if it is the only one. + if (opsLast.size() > 1 || + !opLast->hasBallparkTransformation()) { + try { + res.emplace_back( + ConcatenatedOperation::createComputeMetadata( + {opFirst, opLast}, + disallowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } else { + // std::cerr << "excluded " << opLast->nameStr() << + // std::endl; + } + } + } + if (!res.empty()) { + return; + } + } + } + + auto vertCRSOfBaseOfBoundSrc = + dynamic_cast(boundSrc->baseCRS().get()); + if (vertCRSOfBaseOfBoundSrc && hubSrcGeog) { + auto opsFirst = createOperations(sourceCRS, hubSrc, context); + if (context.skipHorizontalTransformation) { + if (!opsFirst.empty()) { + const auto &hubAxisList = + hubSrcGeog->coordinateSystem()->axisList(); + const auto &targetAxisList = + geogDst->coordinateSystem()->axisList(); + if (hubAxisList.size() == 3 && targetAxisList.size() == 3 && + !hubAxisList[2]->_isEquivalentTo( + targetAxisList[2].get(), + util::IComparable::Criterion::EQUIVALENT)) { + + const auto &srcAxis = hubAxisList[2]; + const double convSrc = srcAxis->unit().conversionToSI(); + const auto &dstAxis = targetAxisList[2]; + const double convDst = dstAxis->unit().conversionToSI(); + const bool srcIsUp = + srcAxis->direction() == cs::AxisDirection::UP; + const bool srcIsDown = + srcAxis->direction() == cs::AxisDirection::DOWN; + const bool dstIsUp = + dstAxis->direction() == cs::AxisDirection::UP; + const bool dstIsDown = + dstAxis->direction() == cs::AxisDirection::DOWN; + const bool heightDepthReversal = + ((srcIsUp && dstIsDown) || (srcIsDown && dstIsUp)); + + const double factor = convSrc / convDst; + auto conv = Conversion::createChangeVerticalUnit( + util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, + "Change of vertical unit"), + common::Scale(heightDepthReversal ? -factor : factor)); + + conv->setCRSs( + hubSrc, + hubSrc->demoteTo2D(std::string(), dbContext) + ->promoteTo3D(std::string(), dbContext, dstAxis), + nullptr); + + for (const auto &op : opsFirst) { + try { + res.emplace_back( + ConcatenatedOperation::createComputeMetadata( + {op, conv}, disallowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } + } else { + res = opsFirst; + } + } + return; + } else { + auto opsSecond = createOperations(hubSrc, targetCRS, context); + if (!opsFirst.empty() && !opsSecond.empty()) { + for (const auto &opFirst : opsFirst) { + for (const auto &opLast : opsSecond) { + // Exclude artificial transformations from the hub + // to the target CRS + if (!opLast->hasBallparkTransformation()) { + try { + res.emplace_back( + ConcatenatedOperation:: + createComputeMetadata( + {opFirst, opLast}, + disallowEmptyIntersection)); + } catch ( + const InvalidOperationEmptyIntersection &) { + } + } else { + // std::cerr << "excluded " << opLast->nameStr() << + // std::endl; + } + } + } + if (!res.empty()) { + return; + } + } + } + } + + res = createOperations(boundSrc->baseCRS(), targetCRS, context); +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsBoundToVert( + const crs::CRSNNPtr & /*sourceCRS*/, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::BoundCRS *boundSrc, + const crs::VerticalCRS *vertDst, + std::vector &res) { + + ENTER_FUNCTION(); + + auto baseSrcVert = + dynamic_cast(boundSrc->baseCRS().get()); + const auto &hubSrc = boundSrc->hubCRS(); + auto hubSrcVert = dynamic_cast(hubSrc.get()); + if (baseSrcVert && hubSrcVert && + vertDst->_isEquivalentTo(hubSrcVert, + util::IComparable::Criterion::EQUIVALENT)) { + res.emplace_back(boundSrc->transformation()); + return; + } + + res = createOperations(boundSrc->baseCRS(), targetCRS, context); +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsVertToVert( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::VerticalCRS *vertSrc, + const crs::VerticalCRS *vertDst, + std::vector &res) { + + ENTER_FUNCTION(); + + const auto &authFactory = context.context->getAuthorityFactory(); + const auto dbContext = + authFactory ? authFactory->databaseContext().as_nullable() : nullptr; + + const auto srcDatum = vertSrc->datumNonNull(dbContext); + const auto dstDatum = vertDst->datumNonNull(dbContext); + const bool equivalentVDatum = srcDatum->_isEquivalentTo( + dstDatum.get(), util::IComparable::Criterion::EQUIVALENT); + + const auto &srcAxis = vertSrc->coordinateSystem()->axisList()[0]; + const double convSrc = srcAxis->unit().conversionToSI(); + const auto &dstAxis = vertDst->coordinateSystem()->axisList()[0]; + const double convDst = dstAxis->unit().conversionToSI(); + const bool srcIsUp = srcAxis->direction() == cs::AxisDirection::UP; + const bool srcIsDown = srcAxis->direction() == cs::AxisDirection::DOWN; + const bool dstIsUp = dstAxis->direction() == cs::AxisDirection::UP; + const bool dstIsDown = dstAxis->direction() == cs::AxisDirection::DOWN; + const bool heightDepthReversal = + ((srcIsUp && dstIsDown) || (srcIsDown && dstIsUp)); + + const double factor = convSrc / convDst; + auto name = buildTransfName(sourceCRS->nameStr(), targetCRS->nameStr()); + if (!equivalentVDatum) { + name += BALLPARK_VERTICAL_TRANSFORMATION; + auto conv = Transformation::createChangeVerticalUnit( + util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, name), + sourceCRS, targetCRS, + // In case of a height depth reversal, we should probably have + // 2 steps instead of putting a negative factor... + common::Scale(heightDepthReversal ? -factor : factor), {}); + conv->setHasBallparkTransformation(true); + res.push_back(conv); + } else if (convSrc != convDst || !heightDepthReversal) { + auto conv = Conversion::createChangeVerticalUnit( + util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, name), + // In case of a height depth reversal, we should probably have + // 2 steps instead of putting a negative factor... + common::Scale(heightDepthReversal ? -factor : factor)); + conv->setCRSs(sourceCRS, targetCRS, nullptr); + res.push_back(conv); + } else { + auto conv = Conversion::createHeightDepthReversal( + util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, name)); + conv->setCRSs(sourceCRS, targetCRS, nullptr); + res.push_back(conv); + } +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsVertToGeog( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::VerticalCRS *vertSrc, + const crs::GeographicCRS *geogDst, + std::vector &res) { + + ENTER_FUNCTION(); + + if (vertSrc->identifiers().empty()) { + const auto &vertSrcName = vertSrc->nameStr(); + const auto &authFactory = context.context->getAuthorityFactory(); + if (authFactory != nullptr && vertSrcName != "unnamed" && + vertSrcName != "unknown") { + auto matches = authFactory->createObjectsFromName( + vertSrcName, {io::AuthorityFactory::ObjectType::VERTICAL_CRS}, + false, 2); + if (matches.size() == 1) { + const auto &match = matches.front(); + if (vertSrc->_isEquivalentTo( + match.get(), + util::IComparable::Criterion::EQUIVALENT) && + !match->identifiers().empty()) { + auto resTmp = createOperations( + NN_NO_CHECK( + util::nn_dynamic_pointer_cast( + match)), + targetCRS, context); + res.insert(res.end(), resTmp.begin(), resTmp.end()); + return; + } + } + } + } + + createOperationsVertToGeogBallpark(sourceCRS, targetCRS, context, vertSrc, + geogDst, res); +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsVertToGeogBallpark( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &, const crs::VerticalCRS *vertSrc, + const crs::GeographicCRS *geogDst, + std::vector &res) { + + ENTER_FUNCTION(); + + const auto &srcAxis = vertSrc->coordinateSystem()->axisList()[0]; + const double convSrc = srcAxis->unit().conversionToSI(); + double convDst = 1.0; + const auto &geogAxis = geogDst->coordinateSystem()->axisList(); + bool dstIsUp = true; + bool dstIsDown = true; + if (geogAxis.size() == 3) { + const auto &dstAxis = geogAxis[2]; + convDst = dstAxis->unit().conversionToSI(); + dstIsUp = dstAxis->direction() == cs::AxisDirection::UP; + dstIsDown = dstAxis->direction() == cs::AxisDirection::DOWN; + } + const bool srcIsUp = srcAxis->direction() == cs::AxisDirection::UP; + const bool srcIsDown = srcAxis->direction() == cs::AxisDirection::DOWN; + const bool heightDepthReversal = + ((srcIsUp && dstIsDown) || (srcIsDown && dstIsUp)); + + const double factor = convSrc / convDst; + + const auto &sourceCRSExtent = getExtent(sourceCRS); + const auto &targetCRSExtent = getExtent(targetCRS); + const bool sameExtent = + sourceCRSExtent && targetCRSExtent && + sourceCRSExtent->_isEquivalentTo( + targetCRSExtent.get(), util::IComparable::Criterion::EQUIVALENT); + + util::PropertyMap map; + map.set(common::IdentifiedObject::NAME_KEY, + buildTransfName(sourceCRS->nameStr(), targetCRS->nameStr()) + + BALLPARK_VERTICAL_TRANSFORMATION_NO_ELLIPSOID_VERT_HEIGHT) + .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + sameExtent ? NN_NO_CHECK(sourceCRSExtent) + : metadata::Extent::WORLD); + + auto conv = Transformation::createChangeVerticalUnit( + map, sourceCRS, targetCRS, + common::Scale(heightDepthReversal ? -factor : factor), {}); + conv->setHasBallparkTransformation(true); + res.push_back(conv); +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsBoundToBound( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::BoundCRS *boundSrc, + const crs::BoundCRS *boundDst, std::vector &res) { + + ENTER_FUNCTION(); + + // BoundCRS to BoundCRS of horizontal CRS using the same (geographic) hub + const auto &hubSrc = boundSrc->hubCRS(); + auto hubSrcGeog = dynamic_cast(hubSrc.get()); + const auto &hubDst = boundDst->hubCRS(); + auto hubDstGeog = dynamic_cast(hubDst.get()); + auto geogCRSOfBaseOfBoundSrc = boundSrc->baseCRS()->extractGeographicCRS(); + auto geogCRSOfBaseOfBoundDst = boundDst->baseCRS()->extractGeographicCRS(); + if (hubSrcGeog && hubDstGeog && + hubSrcGeog->_isEquivalentTo(hubDstGeog, + util::IComparable::Criterion::EQUIVALENT) && + geogCRSOfBaseOfBoundSrc && geogCRSOfBaseOfBoundDst) { + const bool firstIsNoOp = geogCRSOfBaseOfBoundSrc->_isEquivalentTo( + boundSrc->baseCRS().get(), + util::IComparable::Criterion::EQUIVALENT); + const bool lastIsNoOp = geogCRSOfBaseOfBoundDst->_isEquivalentTo( + boundDst->baseCRS().get(), + util::IComparable::Criterion::EQUIVALENT); + auto opsFirst = createOperations( + boundSrc->baseCRS(), NN_NO_CHECK(geogCRSOfBaseOfBoundSrc), context); + auto opsLast = createOperations(NN_NO_CHECK(geogCRSOfBaseOfBoundDst), + boundDst->baseCRS(), context); + if (!opsFirst.empty() && !opsLast.empty()) { + const auto &opSecond = boundSrc->transformation(); + auto opThird = boundDst->transformation()->inverse(); + for (const auto &opFirst : opsFirst) { + for (const auto &opLast : opsLast) { + try { + std::vector ops; + if (!firstIsNoOp) { + ops.push_back(opFirst); + } + ops.push_back(opSecond); + ops.push_back(opThird); + if (!lastIsNoOp) { + ops.push_back(opLast); + } + res.emplace_back( + ConcatenatedOperation::createComputeMetadata( + ops, disallowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } + } + if (!res.empty()) { + return; + } + } + } + + // BoundCRS to BoundCRS of vertical CRS using the same vertical datum + // ==> ignore the bound transformation + auto baseOfBoundSrcAsVertCRS = + dynamic_cast(boundSrc->baseCRS().get()); + auto baseOfBoundDstAsVertCRS = + dynamic_cast(boundDst->baseCRS().get()); + if (baseOfBoundSrcAsVertCRS && baseOfBoundDstAsVertCRS) { + + const auto &authFactory = context.context->getAuthorityFactory(); + const auto dbContext = + authFactory ? authFactory->databaseContext().as_nullable() + : nullptr; + + const auto datumSrc = baseOfBoundSrcAsVertCRS->datumNonNull(dbContext); + const auto datumDst = baseOfBoundDstAsVertCRS->datumNonNull(dbContext); + if (datumSrc->nameStr() == datumDst->nameStr() && + (datumSrc->nameStr() != "unknown" || + boundSrc->transformation()->_isEquivalentTo( + boundDst->transformation().get(), + util::IComparable::Criterion::EQUIVALENT))) { + res = createOperations(boundSrc->baseCRS(), boundDst->baseCRS(), + context); + return; + } + } + + // BoundCRS to BoundCRS of vertical CRS + auto vertCRSOfBaseOfBoundSrc = boundSrc->baseCRS()->extractVerticalCRS(); + auto vertCRSOfBaseOfBoundDst = boundDst->baseCRS()->extractVerticalCRS(); + if (hubSrcGeog && hubDstGeog && + hubSrcGeog->_isEquivalentTo(hubDstGeog, + util::IComparable::Criterion::EQUIVALENT) && + vertCRSOfBaseOfBoundSrc && vertCRSOfBaseOfBoundDst) { + auto opsFirst = createOperations(sourceCRS, hubSrc, 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, opLast}, disallowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } + } + if (!res.empty()) { + return; + } + } + } + + res = createOperations(boundSrc->baseCRS(), boundDst->baseCRS(), context); +} + +// --------------------------------------------------------------------------- + +static std::vector +getOps(const CoordinateOperationNNPtr &op) { + auto concatenated = dynamic_cast(op.get()); + if (concatenated) + return concatenated->operations(); + return {op}; +} + +// --------------------------------------------------------------------------- + +static bool useDifferentTransformationsForSameSourceTarget( + const CoordinateOperationNNPtr &opA, const CoordinateOperationNNPtr &opB) { + auto subOpsA = getOps(opA); + auto subOpsB = getOps(opB); + for (const auto &subOpA : subOpsA) { + if (!dynamic_cast(subOpA.get())) + continue; + if (subOpA->sourceCRS()->nameStr() == "unknown" || + subOpA->targetCRS()->nameStr() == "unknown") + continue; + for (const auto &subOpB : subOpsB) { + if (!dynamic_cast(subOpB.get())) + continue; + if (subOpB->sourceCRS()->nameStr() == "unknown" || + subOpB->targetCRS()->nameStr() == "unknown") + continue; + + if (subOpA->sourceCRS()->nameStr() == + subOpB->sourceCRS()->nameStr() && + subOpA->targetCRS()->nameStr() == + subOpB->targetCRS()->nameStr()) { + if (starts_with(subOpA->nameStr(), NULL_GEOGRAPHIC_OFFSET) && + starts_with(subOpB->nameStr(), NULL_GEOGRAPHIC_OFFSET)) { + continue; + } + + if (!subOpA->isEquivalentTo(subOpB.get())) { + return true; + } + } else if (subOpA->sourceCRS()->nameStr() == + subOpB->targetCRS()->nameStr() && + subOpA->targetCRS()->nameStr() == + subOpB->sourceCRS()->nameStr()) { + if (starts_with(subOpA->nameStr(), NULL_GEOGRAPHIC_OFFSET) && + starts_with(subOpB->nameStr(), NULL_GEOGRAPHIC_OFFSET)) { + continue; + } + + if (!subOpA->isEquivalentTo(subOpB->inverse().get())) { + return true; + } + } + } + } + return false; +} + +// --------------------------------------------------------------------------- + +static crs::GeographicCRSPtr +getInterpolationGeogCRS(const CoordinateOperationNNPtr &verticalTransform, + const io::DatabaseContextPtr &dbContext) { + crs::GeographicCRSPtr interpolationGeogCRS; + auto transformationVerticalTransform = + dynamic_cast(verticalTransform.get()); + if (transformationVerticalTransform == nullptr) { + const auto concat = dynamic_cast( + verticalTransform.get()); + if (concat) { + const auto &steps = concat->operations(); + // Is this change of unit and/or height depth reversal + + // transformation ? + for (const auto &step : steps) { + const auto transf = + dynamic_cast(step.get()); + if (transf) { + // Only support a single Transformation in the steps + if (transformationVerticalTransform != nullptr) { + transformationVerticalTransform = nullptr; + break; + } + transformationVerticalTransform = transf; + } + } + } + } + if (transformationVerticalTransform && + !transformationVerticalTransform->hasBallparkTransformation()) { + auto interpTransformCRS = + transformationVerticalTransform->interpolationCRS(); + if (interpTransformCRS) { + interpolationGeogCRS = + std::dynamic_pointer_cast( + interpTransformCRS); + } else { + // If no explicit interpolation CRS, then + // this will be the geographic CRS of the + // vertical to geog transformation + interpolationGeogCRS = + std::dynamic_pointer_cast( + transformationVerticalTransform->targetCRS().as_nullable()); + } + } + + if (interpolationGeogCRS) { + if (interpolationGeogCRS->coordinateSystem()->axisList().size() == 3) { + // We need to force the interpolation CRS, which + // will + // frequently be 3D, to 2D to avoid transformations + // between source CRS and interpolation CRS to have + // 3D terms. + interpolationGeogCRS = + interpolationGeogCRS->demoteTo2D(std::string(), dbContext) + .as_nullable(); + } + } + + return interpolationGeogCRS; +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsCompoundToGeog( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::CompoundCRS *compoundSrc, + const crs::GeographicCRS *geogDst, + std::vector &res) { + + ENTER_FUNCTION(); + + const auto &authFactory = context.context->getAuthorityFactory(); + const auto &componentsSrc = compoundSrc->componentReferenceSystems(); + if (!componentsSrc.empty()) { + + if (componentsSrc.size() == 2) { + auto derivedHSrc = + dynamic_cast(componentsSrc[0].get()); + if (derivedHSrc) { + std::vector intermComponents{ + derivedHSrc->baseCRS(), componentsSrc[1]}; + auto properties = util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, + intermComponents[0]->nameStr() + " + " + + intermComponents[1]->nameStr()); + auto intermCompound = + crs::CompoundCRS::create(properties, intermComponents); + auto opsFirst = + createOperations(sourceCRS, intermCompound, context); + assert(!opsFirst.empty()); + auto opsLast = + createOperations(intermCompound, targetCRS, context); + for (const auto &opLast : opsLast) { + try { + res.emplace_back( + ConcatenatedOperation::createComputeMetadata( + {opsFirst.front(), opLast}, + disallowEmptyIntersection)); + } catch (const std::exception &) { + } + } + return; + } + } + + std::vector horizTransforms; + auto srcGeogCRS = componentsSrc[0]->extractGeographicCRS(); + if (srcGeogCRS) { + horizTransforms = + createOperations(componentsSrc[0], targetCRS, context); + } + std::vector verticalTransforms; + + const auto dbContext = + authFactory ? authFactory->databaseContext().as_nullable() + : nullptr; + if (componentsSrc.size() >= 2 && + componentsSrc[1]->extractVerticalCRS()) { + + struct SetSkipHorizontalTransform { + Context &context; + + explicit SetSkipHorizontalTransform(Context &contextIn) + : context(contextIn) { + assert(!context.skipHorizontalTransformation); + context.skipHorizontalTransformation = true; + } + + ~SetSkipHorizontalTransform() { + context.skipHorizontalTransformation = false; + } + }; + SetSkipHorizontalTransform setSkipHorizontalTransform(context); + + verticalTransforms = createOperations( + componentsSrc[1], + targetCRS->promoteTo3D(std::string(), dbContext), context); + bool foundRegisteredTransformWithAllGridsAvailable = false; + const auto gridAvailabilityUse = + context.context->getGridAvailabilityUse(); + const bool ignoreMissingGrids = + gridAvailabilityUse == + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY; + for (const auto &op : verticalTransforms) { + if (hasIdentifiers(op) && dbContext) { + bool missingGrid = false; + if (!ignoreMissingGrids) { + const auto gridsNeeded = op->gridsNeeded( + dbContext, + gridAvailabilityUse == + CoordinateOperationContext:: + GridAvailabilityUse::KNOWN_AVAILABLE); + for (const auto &gridDesc : gridsNeeded) { + if (!gridDesc.available) { + missingGrid = true; + break; + } + } + } + if (!missingGrid) { + foundRegisteredTransformWithAllGridsAvailable = true; + break; + } + } + } + if (!foundRegisteredTransformWithAllGridsAvailable && srcGeogCRS && + !srcGeogCRS->_isEquivalentTo( + geogDst, util::IComparable::Criterion::EQUIVALENT) && + !srcGeogCRS->is2DPartOf3D(NN_NO_CHECK(geogDst), dbContext)) { + auto verticalTransformsTmp = createOperations( + componentsSrc[1], + NN_NO_CHECK(srcGeogCRS) + ->promoteTo3D(std::string(), dbContext), + context); + bool foundRegisteredTransform = false; + foundRegisteredTransformWithAllGridsAvailable = false; + for (const auto &op : verticalTransformsTmp) { + if (hasIdentifiers(op) && dbContext) { + bool missingGrid = false; + if (!ignoreMissingGrids) { + const auto gridsNeeded = op->gridsNeeded( + dbContext, + gridAvailabilityUse == + CoordinateOperationContext:: + GridAvailabilityUse::KNOWN_AVAILABLE); + for (const auto &gridDesc : gridsNeeded) { + if (!gridDesc.available) { + missingGrid = true; + break; + } + } + } + foundRegisteredTransform = true; + if (!missingGrid) { + foundRegisteredTransformWithAllGridsAvailable = + true; + break; + } + } + } + if (foundRegisteredTransformWithAllGridsAvailable) { + verticalTransforms = verticalTransformsTmp; + } else if (foundRegisteredTransform) { + verticalTransforms.insert(verticalTransforms.end(), + verticalTransformsTmp.begin(), + verticalTransformsTmp.end()); + } + } + } + + if (horizTransforms.empty() || verticalTransforms.empty()) { + res = horizTransforms; + return; + } + + typedef std::pair, + std::vector> + PairOfTransforms; + std::map + cacheHorizToInterpAndInterpToTarget; + + for (const auto &verticalTransform : verticalTransforms) { +#ifdef TRACE_CREATE_OPERATIONS + ENTER_BLOCK("Considering vertical transform " + + objectAsStr(verticalTransform.get())); +#endif + crs::GeographicCRSPtr interpolationGeogCRS = + getInterpolationGeogCRS(verticalTransform, dbContext); + if (interpolationGeogCRS) { +#ifdef TRACE_CREATE_OPERATIONS + logTrace("Using " + objectAsStr(interpolationGeogCRS.get()) + + " as interpolation CRS"); +#endif + std::vector srcToInterpOps; + std::vector interpToTargetOps; + + std::string key; + const auto &ids = interpolationGeogCRS->identifiers(); + if (!ids.empty()) { + key = + (*ids.front()->codeSpace()) + ':' + ids.front()->code(); + } + + const auto computeOpsToInterp = + [&srcToInterpOps, &interpToTargetOps, &componentsSrc, + &interpolationGeogCRS, &targetCRS, &dbContext, + &context]() { + srcToInterpOps = createOperations( + componentsSrc[0], NN_NO_CHECK(interpolationGeogCRS), + context); + auto target2D = + targetCRS->demoteTo2D(std::string(), dbContext); + if (!componentsSrc[0]->isEquivalentTo( + target2D.get(), + util::IComparable::Criterion::EQUIVALENT)) { + // We do the transformation from the + // interpolationCRS + // to the target one in 3D (see #2225) + // But we don't do that between sourceCRS and + // interpolationCRS, as this would mess with an + // orthometric elevation. + auto interp3D = interpolationGeogCRS->promoteTo3D( + std::string(), dbContext); + interpToTargetOps = + createOperations(interp3D, targetCRS, context); + } + }; + + if (!key.empty()) { + auto iter = cacheHorizToInterpAndInterpToTarget.find(key); + if (iter == cacheHorizToInterpAndInterpToTarget.end()) { +#ifdef TRACE_CREATE_OPERATIONS + ENTER_BLOCK("looking for horizontal transformation " + "from source to interpCRS and interpCRS to " + "target"); +#endif + computeOpsToInterp(); + cacheHorizToInterpAndInterpToTarget[key] = + PairOfTransforms(srcToInterpOps, interpToTargetOps); + } else { + srcToInterpOps = iter->second.first; + interpToTargetOps = iter->second.second; + } + } else { +#ifdef TRACE_CREATE_OPERATIONS + ENTER_BLOCK("looking for horizontal transformation " + "from source to interpCRS and interpCRS to " + "target"); +#endif + computeOpsToInterp(); + } + +#ifdef TRACE_CREATE_OPERATIONS + ENTER_BLOCK("creating HorizVerticalHorizPROJBased operations"); +#endif + for (const auto &srcToInterp : srcToInterpOps) { + if (interpToTargetOps.empty()) { + try { + auto op = createHorizVerticalHorizPROJBased( + sourceCRS, targetCRS, srcToInterp, + verticalTransform, srcToInterp->inverse(), + interpolationGeogCRS, true); + res.emplace_back(op); + } catch (const std::exception &) { + } + } else { + for (const auto &interpToTarget : interpToTargetOps) { + + if (useDifferentTransformationsForSameSourceTarget( + srcToInterp, interpToTarget)) { + continue; + } + + try { + auto op = createHorizVerticalHorizPROJBased( + sourceCRS, targetCRS, srcToInterp, + verticalTransform, interpToTarget, + interpolationGeogCRS, true); + res.emplace_back(op); + } catch (const std::exception &) { + } + } + } + } + } else { + // This case is probably only correct if + // verticalTransform and horizTransform are independent + // and in particular that verticalTransform does not + // involve a grid, because of the rather arbitrary order + // horizontal then vertical applied + for (const auto &horizTransform : horizTransforms) { + try { + auto op = createHorizVerticalPROJBased( + sourceCRS, targetCRS, horizTransform, + verticalTransform, disallowEmptyIntersection); + res.emplace_back(op); + } catch (const std::exception &) { + } + } + } + } + } +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsToGeod( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::GeodeticCRS *geodDst, + std::vector &res) { + + auto cs = cs::EllipsoidalCS::createLatitudeLongitudeEllipsoidalHeight( + common::UnitOfMeasure::DEGREE, common::UnitOfMeasure::METRE); + auto intermGeog3DCRS = + util::nn_static_pointer_cast(crs::GeographicCRS::create( + util::PropertyMap() + .set(common::IdentifiedObject::NAME_KEY, geodDst->nameStr()) + .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + metadata::Extent::WORLD), + geodDst->datum(), geodDst->datumEnsemble(), cs)); + auto sourceToGeog3DOps = + createOperations(sourceCRS, intermGeog3DCRS, context); + auto geog3DToTargetOps = + createOperations(intermGeog3DCRS, targetCRS, context); + if (!geog3DToTargetOps.empty()) { + for (const auto &op : sourceToGeog3DOps) { + auto newOp = op->shallowClone(); + setCRSs(newOp.get(), sourceCRS, intermGeog3DCRS); + try { + res.emplace_back(ConcatenatedOperation::createComputeMetadata( + {newOp, geog3DToTargetOps.front()}, + disallowEmptyIntersection)); + } catch (const InvalidOperationEmptyIntersection &) { + } + } + } +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsCompoundToCompound( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::CompoundCRS *compoundSrc, + const crs::CompoundCRS *compoundDst, + std::vector &res) { + + const auto &componentsSrc = compoundSrc->componentReferenceSystems(); + const auto &componentsDst = compoundDst->componentReferenceSystems(); + if (componentsSrc.empty() || componentsSrc.size() != componentsDst.size()) { + return; + } + const auto srcGeog = componentsSrc[0]->extractGeographicCRS(); + const auto dstGeog = componentsDst[0]->extractGeographicCRS(); + if (srcGeog == nullptr || dstGeog == nullptr) { + return; + } + + std::vector verticalTransforms; + if (componentsSrc.size() >= 2 && componentsSrc[1]->extractVerticalCRS() && + componentsDst[1]->extractVerticalCRS()) { + if (!componentsSrc[1]->_isEquivalentTo(componentsDst[1].get())) { + verticalTransforms = + createOperations(componentsSrc[1], componentsDst[1], context); + } + } + + // If we didn't find a non-ballpark transformation between + // the 2 vertical CRS, then try through intermediate geographic CRS + // For example + // WGS 84 + EGM96 --> ETRS89 + Belfast height where + // there is a geoid model for EGM96 referenced to WGS 84 + // and a geoid model for Belfast height referenced to ETRS89 + if (verticalTransforms.size() == 1 && + verticalTransforms.front()->hasBallparkTransformation()) { + auto dbContext = + context.context->getAuthorityFactory()->databaseContext(); + const auto intermGeogSrc = + srcGeog->promoteTo3D(std::string(), dbContext); + const bool intermGeogSrcIsSameAsIntermGeogDst = + srcGeog->_isEquivalentTo(dstGeog.get()); + const auto intermGeogDst = + intermGeogSrcIsSameAsIntermGeogDst + ? intermGeogSrc + : dstGeog->promoteTo3D(std::string(), dbContext); + const auto opsSrcToGeog = + createOperations(sourceCRS, intermGeogSrc, context); + const auto opsGeogToTarget = + createOperations(intermGeogDst, targetCRS, context); + const bool hasNonTrivalSrcTransf = + !opsSrcToGeog.empty() && + !opsSrcToGeog.front()->hasBallparkTransformation(); + const bool hasNonTrivialTargetTransf = + !opsGeogToTarget.empty() && + !opsGeogToTarget.front()->hasBallparkTransformation(); + if (hasNonTrivalSrcTransf && hasNonTrivialTargetTransf) { + const auto opsGeogSrcToGeogDst = + createOperations(intermGeogSrc, intermGeogDst, context); + for (const auto &op1 : opsSrcToGeog) { + if (op1->hasBallparkTransformation()) { + // std::cerr << "excluded " << op1->nameStr() << std::endl; + continue; + } + for (const auto &op2 : opsGeogSrcToGeogDst) { + for (const auto &op3 : opsGeogToTarget) { + if (op3->hasBallparkTransformation()) { + // std::cerr << "excluded " << op3->nameStr() << + // std::endl; + continue; + } + try { + res.emplace_back( + ConcatenatedOperation::createComputeMetadata( + intermGeogSrcIsSameAsIntermGeogDst + ? std::vector< + CoordinateOperationNNPtr>{op1, + op3} + : std::vector< + CoordinateOperationNNPtr>{op1, + op2, + op3}, + disallowEmptyIntersection)); + } catch (const std::exception &) { + } + } + } + } + } + if (!res.empty()) { + return; + } + } + + for (const auto &verticalTransform : verticalTransforms) { + auto interpolationGeogCRS = NN_NO_CHECK(srcGeog); + auto interpTransformCRS = verticalTransform->interpolationCRS(); + if (interpTransformCRS) { + auto nn_interpTransformCRS = NN_NO_CHECK(interpTransformCRS); + if (dynamic_cast( + nn_interpTransformCRS.get())) { + interpolationGeogCRS = NN_NO_CHECK( + util::nn_dynamic_pointer_cast( + nn_interpTransformCRS)); + } + } else { + auto compSrc0BoundCrs = + dynamic_cast(componentsSrc[0].get()); + auto compDst0BoundCrs = + dynamic_cast(componentsDst[0].get()); + if (compSrc0BoundCrs && compDst0BoundCrs && + dynamic_cast( + compSrc0BoundCrs->hubCRS().get()) && + compSrc0BoundCrs->hubCRS()->_isEquivalentTo( + compDst0BoundCrs->hubCRS().get())) { + interpolationGeogCRS = NN_NO_CHECK( + util::nn_dynamic_pointer_cast( + compSrc0BoundCrs->hubCRS())); + } + } + auto opSrcCRSToGeogCRS = + createOperations(componentsSrc[0], interpolationGeogCRS, context); + auto opGeogCRStoDstCRS = + createOperations(interpolationGeogCRS, componentsDst[0], context); + for (const auto &opSrc : opSrcCRSToGeogCRS) { + for (const auto &opDst : opGeogCRStoDstCRS) { + + try { + auto op = createHorizVerticalHorizPROJBased( + sourceCRS, targetCRS, opSrc, verticalTransform, opDst, + interpolationGeogCRS, true); + res.emplace_back(op); + } catch (const InvalidOperationEmptyIntersection &) { + } catch (const io::FormattingException &) { + } + } + } + } + + if (verticalTransforms.empty()) { + auto resTmp = + createOperations(componentsSrc[0], componentsDst[0], context); + for (const auto &op : resTmp) { + auto opClone = op->shallowClone(); + setCRSs(opClone.get(), sourceCRS, targetCRS); + res.emplace_back(opClone); + } + } +} + +// --------------------------------------------------------------------------- + +void CoordinateOperationFactory::Private::createOperationsBoundToCompound( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + Private::Context &context, const crs::BoundCRS *boundSrc, + const crs::CompoundCRS *compoundDst, + std::vector &res) { + + const auto &authFactory = context.context->getAuthorityFactory(); + const auto dbContext = + authFactory ? authFactory->databaseContext().as_nullable() : nullptr; + + const auto &componentsDst = compoundDst->componentReferenceSystems(); + if (!componentsDst.empty()) { + auto compDst0BoundCrs = + dynamic_cast(componentsDst[0].get()); + if (compDst0BoundCrs) { + auto boundSrcHubAsGeogCRS = + dynamic_cast(boundSrc->hubCRS().get()); + auto compDst0BoundCrsHubAsGeogCRS = + dynamic_cast( + compDst0BoundCrs->hubCRS().get()); + if (boundSrcHubAsGeogCRS && compDst0BoundCrsHubAsGeogCRS) { + const auto boundSrcHubAsGeogCRSDatum = + boundSrcHubAsGeogCRS->datumNonNull(dbContext); + const auto compDst0BoundCrsHubAsGeogCRSDatum = + compDst0BoundCrsHubAsGeogCRS->datumNonNull(dbContext); + if (boundSrcHubAsGeogCRSDatum->_isEquivalentTo( + compDst0BoundCrsHubAsGeogCRSDatum.get())) { + auto cs = cs::EllipsoidalCS:: + createLatitudeLongitudeEllipsoidalHeight( + common::UnitOfMeasure::DEGREE, + common::UnitOfMeasure::METRE); + auto intermGeog3DCRS = util::nn_static_pointer_cast< + crs::CRS>(crs::GeographicCRS::create( + util::PropertyMap() + .set(common::IdentifiedObject::NAME_KEY, + boundSrcHubAsGeogCRS->nameStr()) + .set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY, + metadata::Extent::WORLD), + boundSrcHubAsGeogCRS->datum(), + boundSrcHubAsGeogCRS->datumEnsemble(), cs)); + auto sourceToGeog3DOps = + createOperations(sourceCRS, intermGeog3DCRS, context); + auto geog3DToTargetOps = + createOperations(intermGeog3DCRS, targetCRS, context); + for (const auto &opSrc : sourceToGeog3DOps) { + for (const auto &opDst : geog3DToTargetOps) { + if (opSrc->targetCRS() && opDst->sourceCRS() && + !opSrc->targetCRS()->_isEquivalentTo( + opDst->sourceCRS().get())) { + // Shouldn't happen normally, but typically + // one of them can be 2D and the other 3D + // due to above createOperations() not + // exactly setting the expected source and + // target CRS. + // So create an adapter operation... + auto intermOps = createOperations( + NN_NO_CHECK(opSrc->targetCRS()), + NN_NO_CHECK(opDst->sourceCRS()), context); + if (!intermOps.empty()) { + res.emplace_back( + ConcatenatedOperation:: + createComputeMetadata( + {opSrc, intermOps.front(), + opDst}, + disallowEmptyIntersection)); + } + } else { + res.emplace_back( + ConcatenatedOperation:: + createComputeMetadata( + {opSrc, opDst}, + disallowEmptyIntersection)); + } + } + } + return; + } + } + } + } + + // There might be better things to do, but for now just ignore the + // transformation of the bound CRS + res = createOperations(boundSrc->baseCRS(), targetCRS, context); +} +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Find a list of CoordinateOperation from sourceCRS to targetCRS. + * + * The operations are sorted with the most relevant ones first: by + * descending + * area (intersection of the transformation area with the area of interest, + * or intersection of the transformation with the area of use of the CRS), + * and + * by increasing accuracy. Operations with unknown accuracy are sorted last, + * whatever their area. + * + * When one of the source or target CRS has a vertical component but not the + * other one, the one that has no vertical component is automatically promoted + * to a 3D version, where its vertical axis is the ellipsoidal height in metres, + * using the ellipsoid of the base geodetic CRS. + * + * @param sourceCRS source CRS. + * @param targetCRS target CRS. + * @param context Search context. + * @return a list + */ +std::vector +CoordinateOperationFactory::createOperations( + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + const CoordinateOperationContextNNPtr &context) const { + +#ifdef TRACE_CREATE_OPERATIONS + ENTER_FUNCTION(); +#endif + // Look if we are called on CRS that have a link to a 'canonical' + // BoundCRS + // If so, use that one as input + const auto &srcBoundCRS = sourceCRS->canonicalBoundCRS(); + const auto &targetBoundCRS = targetCRS->canonicalBoundCRS(); + auto l_sourceCRS = srcBoundCRS ? NN_NO_CHECK(srcBoundCRS) : sourceCRS; + auto l_targetCRS = targetBoundCRS ? NN_NO_CHECK(targetBoundCRS) : targetCRS; + const auto &authFactory = context->getAuthorityFactory(); + + metadata::ExtentPtr sourceCRSExtent; + auto l_resolvedSourceCRS = + crs::CRS::getResolvedCRS(l_sourceCRS, authFactory, sourceCRSExtent); + metadata::ExtentPtr targetCRSExtent; + auto l_resolvedTargetCRS = + crs::CRS::getResolvedCRS(l_targetCRS, authFactory, targetCRSExtent); + Private::Context contextPrivate(sourceCRSExtent, targetCRSExtent, context); + + if (context->getSourceAndTargetCRSExtentUse() == + CoordinateOperationContext::SourceTargetCRSExtentUse::INTERSECTION) { + if (sourceCRSExtent && targetCRSExtent && + !sourceCRSExtent->intersects(NN_NO_CHECK(targetCRSExtent))) { + return std::vector(); + } + } + + return filterAndSort(Private::createOperations(l_resolvedSourceCRS, + l_resolvedTargetCRS, + contextPrivate), + context, sourceCRSExtent, targetCRSExtent); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a CoordinateOperationFactory. + */ +CoordinateOperationFactoryNNPtr CoordinateOperationFactory::create() { + return NN_NO_CHECK( + CoordinateOperationFactory::make_unique()); +} + +// --------------------------------------------------------------------------- + +} // namespace operation + +namespace crs { +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +crs::CRSNNPtr CRS::getResolvedCRS(const crs::CRSNNPtr &crs, + const io::AuthorityFactoryPtr &authFactory, + metadata::ExtentPtr &extentOut) { + const auto &ids = crs->identifiers(); + const auto &name = crs->nameStr(); + + bool approxExtent; + extentOut = operation::getExtentPossiblySynthetized(crs, approxExtent); + + // We try to "identify" the provided CRS with the ones of the database, + // but in a more restricted way that what identify() does. + // If we get a match from id in priority, and from name as a fallback, and + // that they are equivalent to the input CRS, then use the identified CRS. + // Even if they aren't equivalent, we update extentOut with the one of the + // identified CRS if our input one is absent/not reliable. + + const auto tryToIdentifyByName = [&crs, &name, &authFactory, approxExtent, + &extentOut]( + io::AuthorityFactory::ObjectType objectType) { + if (name != "unknown" && name != "unnamed") { + auto matches = authFactory->createObjectsFromName( + name, {objectType}, false, 2); + if (matches.size() == 1) { + const auto match = + util::nn_static_pointer_cast(matches.front()); + if (approxExtent || !extentOut) { + extentOut = operation::getExtent(match); + } + if (match->isEquivalentTo( + crs.get(), util::IComparable::Criterion::EQUIVALENT)) { + return match; + } + } + } + return crs; + }; + + auto geogCRS = dynamic_cast(crs.get()); + if (geogCRS && authFactory) { + if (!ids.empty()) { + const auto tmpAuthFactory = io::AuthorityFactory::create( + authFactory->databaseContext(), *ids.front()->codeSpace()); + try { + auto resolvedCrs( + tmpAuthFactory->createGeographicCRS(ids.front()->code())); + if (approxExtent || !extentOut) { + extentOut = operation::getExtent(resolvedCrs); + } + if (resolvedCrs->isEquivalentTo( + crs.get(), util::IComparable::Criterion::EQUIVALENT)) { + return util::nn_static_pointer_cast(resolvedCrs); + } + } catch (const std::exception &) { + } + } else { + return tryToIdentifyByName( + geogCRS->coordinateSystem()->axisList().size() == 2 + ? io::AuthorityFactory::ObjectType::GEOGRAPHIC_2D_CRS + : io::AuthorityFactory::ObjectType::GEOGRAPHIC_3D_CRS); + } + } + + auto projectedCrs = dynamic_cast(crs.get()); + if (projectedCrs && authFactory) { + if (!ids.empty()) { + const auto tmpAuthFactory = io::AuthorityFactory::create( + authFactory->databaseContext(), *ids.front()->codeSpace()); + try { + auto resolvedCrs( + tmpAuthFactory->createProjectedCRS(ids.front()->code())); + if (approxExtent || !extentOut) { + extentOut = operation::getExtent(resolvedCrs); + } + if (resolvedCrs->isEquivalentTo( + crs.get(), util::IComparable::Criterion::EQUIVALENT)) { + return util::nn_static_pointer_cast(resolvedCrs); + } + } catch (const std::exception &) { + } + } else { + return tryToIdentifyByName( + io::AuthorityFactory::ObjectType::PROJECTED_CRS); + } + } + + auto compoundCrs = dynamic_cast(crs.get()); + if (compoundCrs && authFactory) { + if (!ids.empty()) { + const auto tmpAuthFactory = io::AuthorityFactory::create( + authFactory->databaseContext(), *ids.front()->codeSpace()); + try { + auto resolvedCrs( + tmpAuthFactory->createCompoundCRS(ids.front()->code())); + if (approxExtent || !extentOut) { + extentOut = operation::getExtent(resolvedCrs); + } + if (resolvedCrs->isEquivalentTo( + crs.get(), util::IComparable::Criterion::EQUIVALENT)) { + return util::nn_static_pointer_cast(resolvedCrs); + } + } catch (const std::exception &) { + } + } else { + auto outCrs = tryToIdentifyByName( + io::AuthorityFactory::ObjectType::COMPOUND_CRS); + const auto &components = compoundCrs->componentReferenceSystems(); + if (outCrs.get() != crs.get()) { + bool hasGeoid = false; + if (components.size() == 2) { + auto vertCRS = + dynamic_cast(components[1].get()); + if (vertCRS && !vertCRS->geoidModel().empty()) { + hasGeoid = true; + } + } + if (!hasGeoid) { + return outCrs; + } + } + if (approxExtent || !extentOut) { + // If we still did not get a reliable extent, then try to + // resolve the components of the compoundCRS, and take the + // intersection of their extent. + extentOut = metadata::ExtentPtr(); + for (const auto &component : components) { + metadata::ExtentPtr componentExtent; + getResolvedCRS(component, authFactory, componentExtent); + if (extentOut && componentExtent) + extentOut = extentOut->intersection( + NN_NO_CHECK(componentExtent)); + else if (componentExtent) + extentOut = componentExtent; + } + } + } + } + return crs; +} + +//! @endcond + +} // namespace crs +NS_PROJ_END diff --git a/include/proj/internal/esri_projection_mappings.hpp b/src/iso19111/operation/esriparammappings.cpp similarity index 94% rename from include/proj/internal/esri_projection_mappings.hpp rename to src/iso19111/operation/esriparammappings.cpp index 16cda55265..44886e9578 100644 --- a/include/proj/internal/esri_projection_mappings.hpp +++ b/src/iso19111/operation/esriparammappings.cpp @@ -29,26 +29,24 @@ * DEALINGS IN THE SOFTWARE. ****************************************************************************/ -#ifndef FROM_COORDINATE_OPERATION_CPP -#error This file should only be included from coordinateoperation.cpp +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP #endif -#ifndef ESRI_PROJECTION_MAPPINGS_HH_INCLUDED -#define ESRI_PROJECTION_MAPPINGS_HH_INCLUDED +#include "esriparammappings.hpp" +#include "proj_constants.h" -#include "coordinateoperation_internal.hpp" +#include "proj/internal/internal.hpp" -//! @cond Doxygen_Suppress +NS_PROJ_START -// --------------------------------------------------------------------------- +using namespace internal; -// anonymous namespace -namespace { +namespace operation { -using namespace ::NS_PROJ; -using namespace ::NS_PROJ::operation; +//! @cond Doxygen_Suppress -static const ESRIParamMapping paramsESRI_Plate_Carree[] = { +const ESRIParamMapping paramsESRI_Plate_Carree[] = { {"False_Easting", EPSG_NAME_PARAMETER_FALSE_EASTING, EPSG_CODE_PARAMETER_FALSE_EASTING, "0.0", false}, {"False_Northing", EPSG_NAME_PARAMETER_FALSE_NORTHING, @@ -57,7 +55,7 @@ static const ESRIParamMapping paramsESRI_Plate_Carree[] = { EPSG_CODE_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN, "0.0", false}, {nullptr, nullptr, 0, "0.0", false}}; -static const ESRIParamMapping paramsESRI_Equidistant_Cylindrical[] = { +const ESRIParamMapping paramsESRI_Equidistant_Cylindrical[] = { {"False_Easting", EPSG_NAME_PARAMETER_FALSE_EASTING, EPSG_CODE_PARAMETER_FALSE_EASTING, "0.0", false}, {"False_Northing", EPSG_NAME_PARAMETER_FALSE_NORTHING, @@ -88,7 +86,7 @@ static const ESRIParamMapping paramsESRI_Mercator[] = { EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL, "0.0", false}, {nullptr, nullptr, 0, "0.0", false}}; -static const ESRIParamMapping paramsESRI_Gauss_Kruger[] = { +const ESRIParamMapping paramsESRI_Gauss_Kruger[] = { {"False_Easting", EPSG_NAME_PARAMETER_FALSE_EASTING, EPSG_CODE_PARAMETER_FALSE_EASTING, "0.0", false}, {"False_Northing", EPSG_NAME_PARAMETER_FALSE_NORTHING, @@ -101,7 +99,7 @@ static const ESRIParamMapping paramsESRI_Gauss_Kruger[] = { EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN, "0.0", false}, {nullptr, nullptr, 0, "0.0", false}}; -static const ESRIParamMapping paramsESRI_Transverse_Mercator[] = { +const ESRIParamMapping paramsESRI_Transverse_Mercator[] = { {"False_Easting", EPSG_NAME_PARAMETER_FALSE_EASTING, EPSG_CODE_PARAMETER_FALSE_EASTING, "0.0", false}, {"False_Northing", EPSG_NAME_PARAMETER_FALSE_NORTHING, @@ -492,7 +490,7 @@ static const ESRIParamMapping EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN, "0.0", false}, {nullptr, nullptr, 0, "0.0", false}}; -static const ESRIParamMapping +const ESRIParamMapping paramsESRI_Hotine_Oblique_Mercator_Azimuth_Natural_Origin[] = { {"False_Easting", EPSG_NAME_PARAMETER_FALSE_EASTING, EPSG_CODE_PARAMETER_FALSE_EASTING, "0.0", false}, @@ -508,21 +506,20 @@ static const ESRIParamMapping EPSG_CODE_PARAMETER_LATITUDE_PROJECTION_CENTRE, "0.0", false}, {nullptr, nullptr, 0, "0.0", false}}; -static const ESRIParamMapping - paramsESRI_Hotine_Oblique_Mercator_Azimuth_Center[] = { - {"False_Easting", EPSG_NAME_PARAMETER_EASTING_PROJECTION_CENTRE, - EPSG_CODE_PARAMETER_EASTING_PROJECTION_CENTRE, "0.0", false}, - {"False_Northing", EPSG_NAME_PARAMETER_NORTHING_PROJECTION_CENTRE, - EPSG_CODE_PARAMETER_NORTHING_PROJECTION_CENTRE, "0.0", false}, - {"Scale_Factor", EPSG_NAME_PARAMETER_SCALE_FACTOR_INITIAL_LINE, - EPSG_CODE_PARAMETER_SCALE_FACTOR_INITIAL_LINE, "0.0", false}, - {"Azimuth", EPSG_NAME_PARAMETER_AZIMUTH_INITIAL_LINE, - EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE, "0.0", false}, - {"Longitude_Of_Center", EPSG_NAME_PARAMETER_LONGITUDE_PROJECTION_CENTRE, - EPSG_CODE_PARAMETER_LONGITUDE_PROJECTION_CENTRE, "0.0", false}, - {"Latitude_Of_Center", EPSG_NAME_PARAMETER_LATITUDE_PROJECTION_CENTRE, - EPSG_CODE_PARAMETER_LATITUDE_PROJECTION_CENTRE, "0.0", false}, - {nullptr, nullptr, 0, "0.0", false}}; +const ESRIParamMapping paramsESRI_Hotine_Oblique_Mercator_Azimuth_Center[] = { + {"False_Easting", EPSG_NAME_PARAMETER_EASTING_PROJECTION_CENTRE, + EPSG_CODE_PARAMETER_EASTING_PROJECTION_CENTRE, "0.0", false}, + {"False_Northing", EPSG_NAME_PARAMETER_NORTHING_PROJECTION_CENTRE, + EPSG_CODE_PARAMETER_NORTHING_PROJECTION_CENTRE, "0.0", false}, + {"Scale_Factor", EPSG_NAME_PARAMETER_SCALE_FACTOR_INITIAL_LINE, + EPSG_CODE_PARAMETER_SCALE_FACTOR_INITIAL_LINE, "0.0", false}, + {"Azimuth", EPSG_NAME_PARAMETER_AZIMUTH_INITIAL_LINE, + EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE, "0.0", false}, + {"Longitude_Of_Center", EPSG_NAME_PARAMETER_LONGITUDE_PROJECTION_CENTRE, + EPSG_CODE_PARAMETER_LONGITUDE_PROJECTION_CENTRE, "0.0", false}, + {"Latitude_Of_Center", EPSG_NAME_PARAMETER_LATITUDE_PROJECTION_CENTRE, + EPSG_CODE_PARAMETER_LATITUDE_PROJECTION_CENTRE, "0.0", false}, + {nullptr, nullptr, 0, "0.0", false}}; static const ESRIParamMapping paramsESRI_Double_Stereographic[] = { {"False_Easting", EPSG_NAME_PARAMETER_FALSE_EASTING, @@ -706,29 +703,11 @@ static const ESRIParamMapping paramsESRI_Stereographic_South_Pole[] = { EPSG_CODE_PARAMETER_LATITUDE_STD_PARALLEL, "0.0", false}, {nullptr, nullptr, 0, "0.0", false}}; -static const ESRIParamMapping - paramsESRI_Rectified_Skew_Orthomorphic_Natural_Origin[] = { - {"False_Easting", EPSG_NAME_PARAMETER_FALSE_EASTING, - EPSG_CODE_PARAMETER_FALSE_EASTING, "0.0", false}, - {"False_Northing", EPSG_NAME_PARAMETER_FALSE_NORTHING, - EPSG_CODE_PARAMETER_FALSE_NORTHING, "0.0", false}, - {"Scale_Factor", EPSG_NAME_PARAMETER_SCALE_FACTOR_INITIAL_LINE, - EPSG_CODE_PARAMETER_SCALE_FACTOR_INITIAL_LINE, "0.0", false}, - {"Azimuth", EPSG_NAME_PARAMETER_AZIMUTH_INITIAL_LINE, - EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE, "0.0", false}, - {"Longitude_Of_Center", EPSG_NAME_PARAMETER_LONGITUDE_PROJECTION_CENTRE, - EPSG_CODE_PARAMETER_LONGITUDE_PROJECTION_CENTRE, "0.0", false}, - {"Latitude_Of_Center", EPSG_NAME_PARAMETER_LATITUDE_PROJECTION_CENTRE, - EPSG_CODE_PARAMETER_LATITUDE_PROJECTION_CENTRE, "0.0", false}, - {"XY_Plane_Rotation", EPSG_NAME_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID, - EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID, "0.0", false}, - {nullptr, nullptr, 0, "0.0", false}}; - -static const ESRIParamMapping paramsESRI_Rectified_Skew_Orthomorphic_Center[] = - {{"False_Easting", EPSG_NAME_PARAMETER_EASTING_PROJECTION_CENTRE, - EPSG_CODE_PARAMETER_EASTING_PROJECTION_CENTRE, "0.0", false}, - {"False_Northing", EPSG_NAME_PARAMETER_NORTHING_PROJECTION_CENTRE, - EPSG_CODE_PARAMETER_NORTHING_PROJECTION_CENTRE, "0.0", false}, +const ESRIParamMapping paramsESRI_Rectified_Skew_Orthomorphic_Natural_Origin[] = + {{"False_Easting", EPSG_NAME_PARAMETER_FALSE_EASTING, + EPSG_CODE_PARAMETER_FALSE_EASTING, "0.0", false}, + {"False_Northing", EPSG_NAME_PARAMETER_FALSE_NORTHING, + EPSG_CODE_PARAMETER_FALSE_NORTHING, "0.0", false}, {"Scale_Factor", EPSG_NAME_PARAMETER_SCALE_FACTOR_INITIAL_LINE, EPSG_CODE_PARAMETER_SCALE_FACTOR_INITIAL_LINE, "0.0", false}, {"Azimuth", EPSG_NAME_PARAMETER_AZIMUTH_INITIAL_LINE, @@ -741,6 +720,23 @@ static const ESRIParamMapping paramsESRI_Rectified_Skew_Orthomorphic_Center[] = EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID, "0.0", false}, {nullptr, nullptr, 0, "0.0", false}}; +const ESRIParamMapping paramsESRI_Rectified_Skew_Orthomorphic_Center[] = { + {"False_Easting", EPSG_NAME_PARAMETER_EASTING_PROJECTION_CENTRE, + EPSG_CODE_PARAMETER_EASTING_PROJECTION_CENTRE, "0.0", false}, + {"False_Northing", EPSG_NAME_PARAMETER_NORTHING_PROJECTION_CENTRE, + EPSG_CODE_PARAMETER_NORTHING_PROJECTION_CENTRE, "0.0", false}, + {"Scale_Factor", EPSG_NAME_PARAMETER_SCALE_FACTOR_INITIAL_LINE, + EPSG_CODE_PARAMETER_SCALE_FACTOR_INITIAL_LINE, "0.0", false}, + {"Azimuth", EPSG_NAME_PARAMETER_AZIMUTH_INITIAL_LINE, + EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE, "0.0", false}, + {"Longitude_Of_Center", EPSG_NAME_PARAMETER_LONGITUDE_PROJECTION_CENTRE, + EPSG_CODE_PARAMETER_LONGITUDE_PROJECTION_CENTRE, "0.0", false}, + {"Latitude_Of_Center", EPSG_NAME_PARAMETER_LATITUDE_PROJECTION_CENTRE, + EPSG_CODE_PARAMETER_LATITUDE_PROJECTION_CENTRE, "0.0", false}, + {"XY_Plane_Rotation", EPSG_NAME_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID, + EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID, "0.0", false}, + {nullptr, nullptr, 0, "0.0", false}}; + static const ESRIParamMapping paramsESRI_Goode_Homolosine_alt1[] = { {"False_Easting", EPSG_NAME_PARAMETER_FALSE_EASTING, EPSG_CODE_PARAMETER_FALSE_EASTING, "0.0", false}, @@ -1101,8 +1097,27 @@ static const ESRIMethodMapping esriMappings[] = { // --------------------------------------------------------------------------- -} // namespace { +const ESRIMethodMapping *getEsriMappings(size_t &nElts) { + nElts = sizeof(esriMappings) / sizeof(esriMappings[0]); + return esriMappings; +} + +// --------------------------------------------------------------------------- + +std::vector +getMappingsFromESRI(const std::string &esri_name) { + std::vector res; + for (const auto &mapping : esriMappings) { + if (ci_equal(esri_name, mapping.esri_name)) { + res.push_back(&mapping); + } + } + return res; +} //! @endcond -#endif // ESRI_PROJECTION_MAPPINGS_HH_INCLUDED +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END diff --git a/src/iso19111/operation/esriparammappings.hpp b/src/iso19111/operation/esriparammappings.hpp new file mode 100644 index 0000000000..373c1f2205 --- /dev/null +++ b/src/iso19111/operation/esriparammappings.hpp @@ -0,0 +1,86 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef ESRIPARAMMAPPINGS_HPP +#define ESRIPARAMMAPPINGS_HPP + +#include "proj/coordinateoperation.hpp" +#include "proj/util.hpp" + +#include "esriparammappings.hpp" +#include + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +struct ESRIParamMapping { + const char *esri_name; + const char *wkt2_name; + int epsg_code; + const char *fixed_value; + bool is_fixed_value; +}; + +struct ESRIMethodMapping { + const char *esri_name; + const char *wkt2_name; + int epsg_code; + const ESRIParamMapping *const params; +}; + +extern const ESRIParamMapping paramsESRI_Plate_Carree[]; +extern const ESRIParamMapping paramsESRI_Equidistant_Cylindrical[]; +extern const ESRIParamMapping paramsESRI_Gauss_Kruger[]; +extern const ESRIParamMapping paramsESRI_Transverse_Mercator[]; +extern const ESRIParamMapping + paramsESRI_Hotine_Oblique_Mercator_Azimuth_Natural_Origin[]; +extern const ESRIParamMapping + paramsESRI_Rectified_Skew_Orthomorphic_Natural_Origin[]; +extern const ESRIParamMapping + paramsESRI_Hotine_Oblique_Mercator_Azimuth_Center[]; +extern const ESRIParamMapping paramsESRI_Rectified_Skew_Orthomorphic_Center[]; + +const ESRIMethodMapping *getEsriMappings(size_t &nElts); + +std::vector +getMappingsFromESRI(const std::string &esri_name); + +//! @endcond + +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END + +#endif // ESRIPARAMMAPPINGS_HPP diff --git a/src/iso19111/operation/operationmethod_private.hpp b/src/iso19111/operation/operationmethod_private.hpp new file mode 100644 index 0000000000..83d290268e --- /dev/null +++ b/src/iso19111/operation/operationmethod_private.hpp @@ -0,0 +1,56 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef OPERATIONMETHOD_PRIVATE_HPP +#define OPERATIONMETHOD_PRIVATE_HPP + +#include "proj/coordinateoperation.hpp" +#include "proj/util.hpp" + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct OperationMethod::Private { + util::optional formula_{}; + util::optional formulaCitation_{}; + std::vector parameters_{}; + std::string projMethodOverride_{}; +}; +//! @endcond + +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END + +#endif // OPERATIONMETHOD_PRIVATE_HPP diff --git a/src/iso19111/operation/oputils.cpp b/src/iso19111/operation/oputils.cpp new file mode 100644 index 0000000000..b5834edf2c --- /dev/null +++ b/src/iso19111/operation/oputils.cpp @@ -0,0 +1,643 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP +#endif + +#include + +#include "proj/coordinateoperation.hpp" +#include "proj/crs.hpp" +#include "proj/util.hpp" + +#include "proj/internal/internal.hpp" +#include "proj/internal/io_internal.hpp" + +#include "oputils.hpp" +#include "parammappings.hpp" + +#include "proj_constants.h" + +// --------------------------------------------------------------------------- + +NS_PROJ_START + +using namespace internal; + +namespace operation { + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +const char *BALLPARK_GEOCENTRIC_TRANSLATION = "Ballpark geocentric translation"; +const char *NULL_GEOGRAPHIC_OFFSET = "Null geographic offset"; +const char *NULL_GEOCENTRIC_TRANSLATION = "Null geocentric translation"; +const char *BALLPARK_GEOGRAPHIC_OFFSET = "Ballpark geographic offset"; +const char *BALLPARK_VERTICAL_TRANSFORMATION = + " (ballpark vertical transformation)"; +const char *BALLPARK_VERTICAL_TRANSFORMATION_NO_ELLIPSOID_VERT_HEIGHT = + " (ballpark vertical transformation, without ellipsoid height to vertical " + "height correction)"; + +// --------------------------------------------------------------------------- + +OperationParameterNNPtr createOpParamNameEPSGCode(int code) { + const char *name = OperationParameter::getNameForEPSGCode(code); + assert(name); + return OperationParameter::create(createMapNameEPSGCode(name, code)); +} + +// --------------------------------------------------------------------------- + +util::PropertyMap createMethodMapNameEPSGCode(int code) { + const char *name = nullptr; + size_t nMethodNameCodes = 0; + const auto methodNameCodes = getMethodNameCodes(nMethodNameCodes); + for (size_t i = 0; i < nMethodNameCodes; ++i) { + const auto &tuple = methodNameCodes[i]; + if (tuple.epsg_code == code) { + name = tuple.name; + break; + } + } + assert(name); + return createMapNameEPSGCode(name, code); +} + +// --------------------------------------------------------------------------- + +util::PropertyMap createMapNameEPSGCode(const std::string &name, int code) { + return util::PropertyMap() + .set(common::IdentifiedObject::NAME_KEY, name) + .set(metadata::Identifier::CODESPACE_KEY, metadata::Identifier::EPSG) + .set(metadata::Identifier::CODE_KEY, code); +} + +// --------------------------------------------------------------------------- + +util::PropertyMap createMapNameEPSGCode(const char *name, int code) { + return util::PropertyMap() + .set(common::IdentifiedObject::NAME_KEY, name) + .set(metadata::Identifier::CODESPACE_KEY, metadata::Identifier::EPSG) + .set(metadata::Identifier::CODE_KEY, code); +} + +// --------------------------------------------------------------------------- + +util::PropertyMap &addDomains(util::PropertyMap &map, + const common::ObjectUsage *obj) { + + auto ar = util::ArrayOfBaseObject::create(); + for (const auto &domain : obj->domains()) { + ar->add(domain); + } + if (!ar->empty()) { + map.set(common::ObjectUsage::OBJECT_DOMAIN_KEY, ar); + } + return map; +} + +// --------------------------------------------------------------------------- + +static const char *getCRSQualifierStr(const crs::CRSPtr &crs) { + auto geod = dynamic_cast(crs.get()); + if (geod) { + if (geod->isGeocentric()) { + return " (geocentric)"; + } + auto geog = dynamic_cast(geod); + if (geog) { + if (geog->coordinateSystem()->axisList().size() == 2) { + return " (geog2D)"; + } else { + return " (geog3D)"; + } + } + } + return ""; +} + +// --------------------------------------------------------------------------- + +std::string buildOpName(const char *opType, const crs::CRSPtr &source, + const crs::CRSPtr &target) { + std::string res(opType); + const auto &srcName = source->nameStr(); + const auto &targetName = target->nameStr(); + const char *srcQualifier = ""; + const char *targetQualifier = ""; + if (srcName == targetName) { + srcQualifier = getCRSQualifierStr(source); + targetQualifier = getCRSQualifierStr(target); + if (strcmp(srcQualifier, targetQualifier) == 0) { + srcQualifier = ""; + targetQualifier = ""; + } + } + res += " from "; + res += srcName; + res += srcQualifier; + res += " to "; + res += targetName; + res += targetQualifier; + return res; +} + +// --------------------------------------------------------------------------- + +void addModifiedIdentifier(util::PropertyMap &map, + const common::IdentifiedObject *obj, bool inverse, + bool derivedFrom) { + // If original operation is AUTH:CODE, then assign INVERSE(AUTH):CODE + // as identifier. + + auto ar = util::ArrayOfBaseObject::create(); + for (const auto &idSrc : obj->identifiers()) { + auto authName = *(idSrc->codeSpace()); + const auto &srcCode = idSrc->code(); + if (derivedFrom) { + authName = concat("DERIVED_FROM(", authName, ")"); + } + if (inverse) { + if (starts_with(authName, "INVERSE(") && authName.back() == ')') { + authName = authName.substr(strlen("INVERSE(")); + authName.resize(authName.size() - 1); + } else { + authName = concat("INVERSE(", authName, ")"); + } + } + auto idsProp = util::PropertyMap().set( + metadata::Identifier::CODESPACE_KEY, authName); + ar->add(metadata::Identifier::create(srcCode, idsProp)); + } + if (!ar->empty()) { + map.set(common::IdentifiedObject::IDENTIFIERS_KEY, ar); + } +} + +// --------------------------------------------------------------------------- + +util::PropertyMap +createPropertiesForInverse(const OperationMethodNNPtr &method) { + util::PropertyMap map; + + const std::string &forwardName = method->nameStr(); + if (!forwardName.empty()) { + if (starts_with(forwardName, INVERSE_OF)) { + map.set(common::IdentifiedObject::NAME_KEY, + forwardName.substr(INVERSE_OF.size())); + } else { + map.set(common::IdentifiedObject::NAME_KEY, + INVERSE_OF + forwardName); + } + } + + addModifiedIdentifier(map, method.get(), true, false); + + return map; +} + +// --------------------------------------------------------------------------- + +util::PropertyMap createPropertiesForInverse(const CoordinateOperation *op, + bool derivedFrom, + bool approximateInversion) { + assert(op); + util::PropertyMap map; + + // The domain(s) are unchanged by the inverse operation + addDomains(map, op); + + const std::string &forwardName = op->nameStr(); + + // Forge a name for the inverse, either from the forward name, or + // from the source and target CRS names + const char *opType; + if (starts_with(forwardName, BALLPARK_GEOCENTRIC_TRANSLATION)) { + opType = BALLPARK_GEOCENTRIC_TRANSLATION; + } else if (starts_with(forwardName, BALLPARK_GEOGRAPHIC_OFFSET)) { + opType = BALLPARK_GEOGRAPHIC_OFFSET; + } else if (starts_with(forwardName, NULL_GEOGRAPHIC_OFFSET)) { + opType = NULL_GEOGRAPHIC_OFFSET; + } else if (starts_with(forwardName, NULL_GEOCENTRIC_TRANSLATION)) { + opType = NULL_GEOCENTRIC_TRANSLATION; + } else if (dynamic_cast(op) || + starts_with(forwardName, "Transformation from ")) { + opType = "Transformation"; + } else if (dynamic_cast(op)) { + opType = "Conversion"; + } else { + opType = "Operation"; + } + + auto sourceCRS = op->sourceCRS(); + auto targetCRS = op->targetCRS(); + std::string name; + if (!forwardName.empty()) { + if (dynamic_cast(op) == nullptr && + dynamic_cast(op) == nullptr && + (starts_with(forwardName, INVERSE_OF) || + forwardName.find(" + ") != std::string::npos)) { + std::vector tokens; + std::string curToken; + bool inString = false; + for (size_t i = 0; i < forwardName.size(); ++i) { + if (inString) { + curToken += forwardName[i]; + if (forwardName[i] == '\'') { + inString = false; + } + } else if (i + 3 < forwardName.size() && + memcmp(&forwardName[i], " + ", 3) == 0) { + tokens.push_back(curToken); + curToken.clear(); + i += 2; + } else if (forwardName[i] == '\'') { + inString = true; + curToken += forwardName[i]; + } else { + curToken += forwardName[i]; + } + } + if (!curToken.empty()) { + tokens.push_back(curToken); + } + for (size_t i = tokens.size(); i > 0;) { + i--; + if (!name.empty()) { + name += " + "; + } + if (starts_with(tokens[i], INVERSE_OF)) { + name += tokens[i].substr(INVERSE_OF.size()); + } else if (tokens[i] == AXIS_ORDER_CHANGE_2D_NAME || + tokens[i] == AXIS_ORDER_CHANGE_3D_NAME) { + name += tokens[i]; + } else { + name += INVERSE_OF + tokens[i]; + } + } + } else if (!sourceCRS || !targetCRS || + forwardName != buildOpName(opType, sourceCRS, targetCRS)) { + if (forwardName.find(" + ") != std::string::npos) { + name = INVERSE_OF + '\'' + forwardName + '\''; + } else { + name = INVERSE_OF + forwardName; + } + } + } + if (name.empty() && sourceCRS && targetCRS) { + name = buildOpName(opType, targetCRS, sourceCRS); + } + if (approximateInversion) { + name += " (approx. inversion)"; + } + + if (!name.empty()) { + map.set(common::IdentifiedObject::NAME_KEY, name); + } + + const std::string &remarks = op->remarks(); + if (!remarks.empty()) { + map.set(common::IdentifiedObject::REMARKS_KEY, remarks); + } + + addModifiedIdentifier(map, op, true, derivedFrom); + + const auto so = dynamic_cast(op); + if (so) { + const int soMethodEPSGCode = so->method()->getEPSGCode(); + if (soMethodEPSGCode > 0) { + map.set("OPERATION_METHOD_EPSG_CODE", soMethodEPSGCode); + } + } + + return map; +} + +// --------------------------------------------------------------------------- + +util::PropertyMap addDefaultNameIfNeeded(const util::PropertyMap &properties, + const std::string &defaultName) { + if (!properties.get(common::IdentifiedObject::NAME_KEY)) { + return util::PropertyMap(properties) + .set(common::IdentifiedObject::NAME_KEY, defaultName); + } else { + return properties; + } +} + +// --------------------------------------------------------------------------- + +static std::string createEntryEqParam(const std::string &a, + const std::string &b) { + return a < b ? a + b : b + a; +} + +static std::set buildSetEquivalentParameters() { + + std::set set; + + const char *const listOfEquivalentParameterNames[][7] = { + {"latitude_of_point_1", "Latitude_Of_1st_Point", nullptr}, + {"longitude_of_point_1", "Longitude_Of_1st_Point", nullptr}, + {"latitude_of_point_2", "Latitude_Of_2nd_Point", nullptr}, + {"longitude_of_point_2", "Longitude_Of_2nd_Point", nullptr}, + + {"satellite_height", "height", nullptr}, + + {EPSG_NAME_PARAMETER_FALSE_EASTING, + EPSG_NAME_PARAMETER_EASTING_FALSE_ORIGIN, + EPSG_NAME_PARAMETER_EASTING_PROJECTION_CENTRE, nullptr}, + + {EPSG_NAME_PARAMETER_FALSE_NORTHING, + EPSG_NAME_PARAMETER_NORTHING_FALSE_ORIGIN, + EPSG_NAME_PARAMETER_NORTHING_PROJECTION_CENTRE, nullptr}, + + {EPSG_NAME_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN, WKT1_SCALE_FACTOR, + EPSG_NAME_PARAMETER_SCALE_FACTOR_INITIAL_LINE, + EPSG_NAME_PARAMETER_SCALE_FACTOR_PSEUDO_STANDARD_PARALLEL, nullptr}, + + {WKT1_LATITUDE_OF_ORIGIN, WKT1_LATITUDE_OF_CENTER, + EPSG_NAME_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN, + EPSG_NAME_PARAMETER_LATITUDE_FALSE_ORIGIN, + EPSG_NAME_PARAMETER_LATITUDE_PROJECTION_CENTRE, "Central_Parallel", + nullptr}, + + {WKT1_CENTRAL_MERIDIAN, WKT1_LONGITUDE_OF_CENTER, + EPSG_NAME_PARAMETER_LONGITUDE_OF_NATURAL_ORIGIN, + EPSG_NAME_PARAMETER_LONGITUDE_FALSE_ORIGIN, + EPSG_NAME_PARAMETER_LONGITUDE_PROJECTION_CENTRE, + EPSG_NAME_PARAMETER_LONGITUDE_OF_ORIGIN, nullptr}, + + {"pseudo_standard_parallel_1", WKT1_STANDARD_PARALLEL_1, nullptr}, + }; + + for (const auto ¶mList : listOfEquivalentParameterNames) { + for (size_t i = 0; paramList[i]; i++) { + auto a = metadata::Identifier::canonicalizeName(paramList[i]); + for (size_t j = i + 1; paramList[j]; j++) { + auto b = metadata::Identifier::canonicalizeName(paramList[j]); + set.insert(createEntryEqParam(a, b)); + } + } + } + return set; +} + +bool areEquivalentParameters(const std::string &a, const std::string &b) { + + static const std::set setEquivalentParameters = + buildSetEquivalentParameters(); + + auto a_can = metadata::Identifier::canonicalizeName(a); + auto b_can = metadata::Identifier::canonicalizeName(b); + return setEquivalentParameters.find(createEntryEqParam(a_can, b_can)) != + setEquivalentParameters.end(); +} + +// --------------------------------------------------------------------------- + +bool isTimeDependent(const std::string &methodName) { + return ci_find(methodName, "Time dependent") != std::string::npos || + ci_find(methodName, "Time-dependent") != std::string::npos; +} + +// --------------------------------------------------------------------------- + +std::string computeConcatenatedName( + const std::vector &flattenOps) { + std::string name; + for (const auto &subOp : flattenOps) { + if (!name.empty()) { + name += " + "; + } + const auto &l_name = subOp->nameStr(); + if (l_name.empty()) { + name += "unnamed"; + } else { + name += l_name; + } + } + return name; +} + +// --------------------------------------------------------------------------- + +metadata::ExtentPtr getExtent(const CoordinateOperationNNPtr &op, + bool conversionExtentIsWorld, + bool &emptyIntersection) { + auto conv = dynamic_cast(op.get()); + if (conv) { + emptyIntersection = false; + return metadata::Extent::WORLD; + } + const auto &domains = op->domains(); + if (!domains.empty()) { + emptyIntersection = false; + return domains[0]->domainOfValidity(); + } + auto concatenated = dynamic_cast(op.get()); + if (!concatenated) { + emptyIntersection = false; + return nullptr; + } + return getExtent(concatenated->operations(), conversionExtentIsWorld, + emptyIntersection); +} + +// --------------------------------------------------------------------------- + +static const metadata::ExtentPtr nullExtent{}; + +const metadata::ExtentPtr &getExtent(const crs::CRSNNPtr &crs) { + const auto &domains = crs->domains(); + if (!domains.empty()) { + return domains[0]->domainOfValidity(); + } + const auto *boundCRS = dynamic_cast(crs.get()); + if (boundCRS) { + return getExtent(boundCRS->baseCRS()); + } + return nullExtent; +} + +const metadata::ExtentPtr getExtentPossiblySynthetized(const crs::CRSNNPtr &crs, + bool &approxOut) { + const auto &rawExtent(getExtent(crs)); + approxOut = false; + if (rawExtent) + return rawExtent; + const auto compoundCRS = dynamic_cast(crs.get()); + if (compoundCRS) { + // For a compoundCRS, take the intersection of the extent of its + // components. + const auto &components = compoundCRS->componentReferenceSystems(); + metadata::ExtentPtr extent; + approxOut = true; + for (const auto &component : components) { + const auto &componentExtent(getExtent(component)); + if (extent && componentExtent) + extent = extent->intersection(NN_NO_CHECK(componentExtent)); + else if (componentExtent) + extent = componentExtent; + } + return extent; + } + return rawExtent; +} + +// --------------------------------------------------------------------------- + +metadata::ExtentPtr getExtent(const std::vector &ops, + bool conversionExtentIsWorld, + bool &emptyIntersection) { + metadata::ExtentPtr res = nullptr; + for (const auto &subop : ops) { + + const auto &subExtent = + getExtent(subop, conversionExtentIsWorld, emptyIntersection); + if (!subExtent) { + if (emptyIntersection) { + return nullptr; + } + continue; + } + if (res == nullptr) { + res = subExtent; + } else { + res = res->intersection(NN_NO_CHECK(subExtent)); + if (!res) { + emptyIntersection = true; + return nullptr; + } + } + } + emptyIntersection = false; + return res; +} + +// --------------------------------------------------------------------------- + +// Returns the accuracy of an operation, or -1 if unknown +double getAccuracy(const CoordinateOperationNNPtr &op) { + + if (dynamic_cast(op.get())) { + // A conversion is perfectly accurate. + return 0.0; + } + + double accuracy = -1.0; + const auto &accuracies = op->coordinateOperationAccuracies(); + if (!accuracies.empty()) { + try { + accuracy = c_locale_stod(accuracies[0]->value()); + } catch (const std::exception &) { + } + } else { + auto concatenated = + dynamic_cast(op.get()); + if (concatenated) { + accuracy = getAccuracy(concatenated->operations()); + } + } + return accuracy; +} + +// --------------------------------------------------------------------------- + +// Returns the accuracy of a set of concatenated operations, or -1 if unknown +double getAccuracy(const std::vector &ops) { + double accuracy = -1.0; + for (const auto &subop : ops) { + const double subops_accuracy = getAccuracy(subop); + if (subops_accuracy < 0.0) { + return -1.0; + } + if (accuracy < 0.0) { + accuracy = 0.0; + } + accuracy += subops_accuracy; + } + return accuracy; +} + +// --------------------------------------------------------------------------- + +void exportSourceCRSAndTargetCRSToWKT(const CoordinateOperation *co, + io::WKTFormatter *formatter) { + auto l_sourceCRS = co->sourceCRS(); + assert(l_sourceCRS); + auto l_targetCRS = co->targetCRS(); + assert(l_targetCRS); + const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; + const bool canExportCRSId = + (isWKT2 && formatter->use2019Keywords() && + !(formatter->idOnTopLevelOnly() && formatter->topLevelHasId())); + + const bool hasDomains = !co->domains().empty(); + if (hasDomains) { + formatter->pushDisableUsage(); + } + + formatter->startNode(io::WKTConstants::SOURCECRS, false); + if (canExportCRSId && !l_sourceCRS->identifiers().empty()) { + // fake that top node has no id, so that the sourceCRS id is + // considered + formatter->pushHasId(false); + l_sourceCRS->_exportToWKT(formatter); + formatter->popHasId(); + } else { + l_sourceCRS->_exportToWKT(formatter); + } + formatter->endNode(); + + formatter->startNode(io::WKTConstants::TARGETCRS, false); + if (canExportCRSId && !l_targetCRS->identifiers().empty()) { + // fake that top node has no id, so that the targetCRS id is + // considered + formatter->pushHasId(false); + l_targetCRS->_exportToWKT(formatter); + formatter->popHasId(); + } else { + l_targetCRS->_exportToWKT(formatter); + } + formatter->endNode(); + + if (hasDomains) { + formatter->popDisableUsage(); + } +} + +//! @endcond + +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END diff --git a/src/iso19111/operation/oputils.hpp b/src/iso19111/operation/oputils.hpp new file mode 100644 index 0000000000..8c2ad1f022 --- /dev/null +++ b/src/iso19111/operation/oputils.hpp @@ -0,0 +1,121 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef OPUTILS_HPP +#define OPUTILS_HPP + +#include "proj/coordinateoperation.hpp" +#include "proj/io.hpp" +#include "proj/metadata.hpp" +#include "proj/util.hpp" + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +extern const common::Measure nullMeasure; + +extern const std::string INVERSE_OF; + +extern const char *BALLPARK_GEOCENTRIC_TRANSLATION; +extern const char *NULL_GEOGRAPHIC_OFFSET; +extern const char *NULL_GEOCENTRIC_TRANSLATION; +extern const char *BALLPARK_GEOGRAPHIC_OFFSET; +extern const char *BALLPARK_VERTICAL_TRANSFORMATION; +extern const char *BALLPARK_VERTICAL_TRANSFORMATION_NO_ELLIPSOID_VERT_HEIGHT; + +extern const std::string AXIS_ORDER_CHANGE_2D_NAME; +extern const std::string AXIS_ORDER_CHANGE_3D_NAME; + +OperationParameterNNPtr createOpParamNameEPSGCode(int code); + +util::PropertyMap createMethodMapNameEPSGCode(int code); + +util::PropertyMap createMapNameEPSGCode(const std::string &name, int code); + +util::PropertyMap createMapNameEPSGCode(const char *name, int code); + +util::PropertyMap &addDomains(util::PropertyMap &map, + const common::ObjectUsage *obj); + +std::string buildOpName(const char *opType, const crs::CRSPtr &source, + const crs::CRSPtr &target); + +void addModifiedIdentifier(util::PropertyMap &map, + const common::IdentifiedObject *obj, bool inverse, + bool derivedFrom); +util::PropertyMap +createPropertiesForInverse(const OperationMethodNNPtr &method); + +util::PropertyMap createPropertiesForInverse(const CoordinateOperation *op, + bool derivedFrom, + bool approximateInversion); + +util::PropertyMap addDefaultNameIfNeeded(const util::PropertyMap &properties, + const std::string &defaultName); + +bool areEquivalentParameters(const std::string &a, const std::string &b); + +bool isTimeDependent(const std::string &methodName); + +std::string computeConcatenatedName( + const std::vector &flattenOps); + +metadata::ExtentPtr getExtent(const std::vector &ops, + bool conversionExtentIsWorld, + bool &emptyIntersection); + +metadata::ExtentPtr getExtent(const CoordinateOperationNNPtr &op, + bool conversionExtentIsWorld, + bool &emptyIntersection); + +const metadata::ExtentPtr &getExtent(const crs::CRSNNPtr &crs); + +const metadata::ExtentPtr getExtentPossiblySynthetized(const crs::CRSNNPtr &crs, + bool &approxOut); + +double getAccuracy(const CoordinateOperationNNPtr &op); + +double getAccuracy(const std::vector &ops); + +void exportSourceCRSAndTargetCRSToWKT(const CoordinateOperation *co, + io::WKTFormatter *formatter); + +//! @endcond + +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END + +#endif // OPUTILS_HPP diff --git a/include/proj/internal/coordinateoperation_constants.hpp b/src/iso19111/operation/parammappings.cpp similarity index 90% rename from include/proj/internal/coordinateoperation_constants.hpp rename to src/iso19111/operation/parammappings.cpp index 592f6263d0..e992c9cc05 100644 --- a/include/proj/internal/coordinateoperation_constants.hpp +++ b/src/iso19111/operation/parammappings.cpp @@ -26,36 +26,31 @@ * DEALINGS IN THE SOFTWARE. ****************************************************************************/ -#ifndef FROM_COORDINATE_OPERATION_CPP -#error This file should only be included from coordinateoperation.cpp -#endif +#include "parammappings.hpp" +#include "oputils.hpp" +#include "proj_constants.h" -#ifndef COORDINATEOPERATION_CONSTANTS_HH_INCLUDED -#define COORDINATEOPERATION_CONSTANTS_HH_INCLUDED +#include "proj/internal/internal.hpp" -#include "coordinateoperation_internal.hpp" -#include "proj_constants.h" +NS_PROJ_START -//! @cond Doxygen_Suppress -// --------------------------------------------------------------------------- +using namespace internal; -// anonymous namespace -namespace { +namespace operation { -using namespace ::NS_PROJ; -using namespace ::NS_PROJ::operation; +//! @cond Doxygen_Suppress -static const char *WKT1_LATITUDE_OF_ORIGIN = "latitude_of_origin"; -static const char *WKT1_CENTRAL_MERIDIAN = "central_meridian"; -static const char *WKT1_SCALE_FACTOR = "scale_factor"; -static const char *WKT1_FALSE_EASTING = "false_easting"; -static const char *WKT1_FALSE_NORTHING = "false_northing"; -static const char *WKT1_STANDARD_PARALLEL_1 = "standard_parallel_1"; -static const char *WKT1_STANDARD_PARALLEL_2 = "standard_parallel_2"; -static const char *WKT1_LATITUDE_OF_CENTER = "latitude_of_center"; -static const char *WKT1_LONGITUDE_OF_CENTER = "longitude_of_center"; -static const char *WKT1_AZIMUTH = "azimuth"; -static const char *WKT1_RECTIFIED_GRID_ANGLE = "rectified_grid_angle"; +const char *WKT1_LATITUDE_OF_ORIGIN = "latitude_of_origin"; +const char *WKT1_CENTRAL_MERIDIAN = "central_meridian"; +const char *WKT1_SCALE_FACTOR = "scale_factor"; +const char *WKT1_FALSE_EASTING = "false_easting"; +const char *WKT1_FALSE_NORTHING = "false_northing"; +const char *WKT1_STANDARD_PARALLEL_1 = "standard_parallel_1"; +const char *WKT1_STANDARD_PARALLEL_2 = "standard_parallel_2"; +const char *WKT1_LATITUDE_OF_CENTER = "latitude_of_center"; +const char *WKT1_LONGITUDE_OF_CENTER = "longitude_of_center"; +const char *WKT1_AZIMUTH = "azimuth"; +const char *WKT1_RECTIFIED_GRID_ANGLE = "rectified_grid_angle"; static const char *lat_0 = "lat_0"; static const char *lat_1 = "lat_1"; @@ -73,7 +68,9 @@ static const char *x_0 = "x_0"; static const char *y_0 = "y_0"; static const char *h = "h"; -static const ParamMapping paramLatitudeNatOrigin = { +// --------------------------------------------------------------------------- + +const ParamMapping paramLatitudeNatOrigin = { EPSG_NAME_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN, EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN, WKT1_LATITUDE_OF_ORIGIN, common::UnitOfMeasure::Type::ANGULAR, lat_0}; @@ -838,13 +835,16 @@ static const MethodMapping projectionMethodMappings[] = { "col_urban", nullptr, paramsColombiaUrban}, }; +const MethodMapping *getProjectionMethodMappings(size_t &nElts) { + nElts = + sizeof(projectionMethodMappings) / sizeof(projectionMethodMappings[0]); + return projectionMethodMappings; +} + #define METHOD_NAME_CODE(method) \ { EPSG_NAME_METHOD_##method, EPSG_CODE_METHOD_##method } -static const struct MethodNameCode { - const char *name; - int epsg_code; -} methodNameCodes[] = { +const struct MethodNameCode methodNameCodes[] = { // Projection methods METHOD_NAME_CODE(TRANSVERSE_MERCATOR), METHOD_NAME_CODE(TRANSVERSE_MERCATOR_SOUTH_ORIENTATED), @@ -911,13 +911,15 @@ static const struct MethodNameCode { METHOD_NAME_CODE(GEOCENTRIC_TRANSLATION_BY_GRID_INTERPOLATION_IGN), }; +const MethodNameCode *getMethodNameCodes(size_t &nElts) { + nElts = sizeof(methodNameCodes) / sizeof(methodNameCodes[0]); + return methodNameCodes; +} + #define PARAM_NAME_CODE(method) \ { EPSG_NAME_PARAMETER_##method, EPSG_CODE_PARAMETER_##method } -static const struct ParamNameCode { - const char *name; - int epsg_code; -} paramNameCodes[] = { +const struct ParamNameCode paramNameCodes[] = { // Parameters of projection methods PARAM_NAME_CODE(COLATITUDE_CONE_AXIS), PARAM_NAME_CODE(LATITUDE_OF_NATURAL_ORIGIN), @@ -973,6 +975,11 @@ static const struct ParamNameCode { PARAM_NAME_CODE(GEOCENTRIC_TRANSLATION_FILE), }; +const ParamNameCode *getParamNameCodes(size_t &nElts) { + nElts = sizeof(paramNameCodes) / sizeof(paramNameCodes[0]); + return paramNameCodes; +} + static const ParamMapping paramUnitConversionScalar = { EPSG_NAME_PARAMETER_UNIT_CONVERSION_SCALAR, EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR, nullptr, @@ -1361,11 +1368,143 @@ static const MethodMapping otherMethodMappings[] = { nullptr, nullptr, paramsVERTCON}, }; -// end of anonymous namespace -} // namespace +const MethodMapping *getOtherMethodMappings(size_t &nElts) { + nElts = sizeof(otherMethodMappings) / sizeof(otherMethodMappings[0]); + return otherMethodMappings; +} + +// --------------------------------------------------------------------------- + +PROJ_NO_INLINE const MethodMapping *getMapping(int epsg_code) noexcept { + for (const auto &mapping : projectionMethodMappings) { + if (mapping.epsg_code == epsg_code) { + return &mapping; + } + } + return nullptr; +} + +// --------------------------------------------------------------------------- + +const MethodMapping *getMapping(const OperationMethod *method) noexcept { + const std::string &name(method->nameStr()); + const int epsg_code = method->getEPSGCode(); + for (const auto &mapping : projectionMethodMappings) { + if ((epsg_code != 0 && mapping.epsg_code == epsg_code) || + metadata::Identifier::isEquivalentName(mapping.wkt2_name, + name.c_str())) { + return &mapping; + } + } + return nullptr; +} + +// --------------------------------------------------------------------------- + +const MethodMapping *getMappingFromWKT1(const std::string &wkt1_name) noexcept { + // Unusual for a WKT1 projection name, but mentioned in OGC 12-063r5 C.4.2 + if (ci_starts_with(wkt1_name, "UTM zone")) { + return getMapping(EPSG_CODE_METHOD_TRANSVERSE_MERCATOR); + } + + for (const auto &mapping : projectionMethodMappings) { + if (mapping.wkt1_name && metadata::Identifier::isEquivalentName( + mapping.wkt1_name, wkt1_name.c_str())) { + return &mapping; + } + } + return nullptr; +} +// --------------------------------------------------------------------------- + +const MethodMapping *getMapping(const char *wkt2_name) noexcept { + for (const auto &mapping : projectionMethodMappings) { + if (metadata::Identifier::isEquivalentName(mapping.wkt2_name, + wkt2_name)) { + return &mapping; + } + } + for (const auto &mapping : otherMethodMappings) { + if (metadata::Identifier::isEquivalentName(mapping.wkt2_name, + wkt2_name)) { + return &mapping; + } + } + return nullptr; +} // --------------------------------------------------------------------------- +std::vector +getMappingsFromPROJName(const std::string &projName) { + std::vector res; + for (const auto &mapping : projectionMethodMappings) { + if (mapping.proj_name_main && projName == mapping.proj_name_main) { + res.push_back(&mapping); + } + } + return res; +} + +// --------------------------------------------------------------------------- + +const ParamMapping *getMapping(const MethodMapping *mapping, + const OperationParameterNNPtr ¶m) { + if (mapping->params == nullptr) { + return nullptr; + } + + // First try with id + const int epsg_code = param->getEPSGCode(); + if (epsg_code) { + for (int i = 0; mapping->params[i] != nullptr; ++i) { + const auto *paramMapping = mapping->params[i]; + if (paramMapping->epsg_code == epsg_code) { + return paramMapping; + } + } + } + + // then equivalent name + const std::string &name = param->nameStr(); + for (int i = 0; mapping->params[i] != nullptr; ++i) { + const auto *paramMapping = mapping->params[i]; + if (metadata::Identifier::isEquivalentName(paramMapping->wkt2_name, + name.c_str())) { + return paramMapping; + } + } + + // and finally different name, but equivalent parameter + for (int i = 0; mapping->params[i] != nullptr; ++i) { + const auto *paramMapping = mapping->params[i]; + if (areEquivalentParameters(paramMapping->wkt2_name, name)) { + return paramMapping; + } + } + + return nullptr; +} + +// --------------------------------------------------------------------------- + +const ParamMapping *getMappingFromWKT1(const MethodMapping *mapping, + const std::string &wkt1_name) { + for (int i = 0; mapping->params[i] != nullptr; ++i) { + const auto *paramMapping = mapping->params[i]; + if (paramMapping->wkt1_name && + (metadata::Identifier::isEquivalentName(paramMapping->wkt1_name, + wkt1_name.c_str()) || + areEquivalentParameters(paramMapping->wkt1_name, wkt1_name))) { + return paramMapping; + } + } + return nullptr; +} + //! @endcond -#endif // COORDINATEOPERATION_CONSTANTS_HH_INCLUDED +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END diff --git a/src/iso19111/operation/parammappings.hpp b/src/iso19111/operation/parammappings.hpp new file mode 100644 index 0000000000..05fe8a2d4e --- /dev/null +++ b/src/iso19111/operation/parammappings.hpp @@ -0,0 +1,114 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef PARAMMAPPINGS_HPP +#define PARAMMAPPINGS_HPP + +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP +#endif + +#include "proj/coordinateoperation.hpp" +#include "proj/util.hpp" + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +extern const char *WKT1_LATITUDE_OF_ORIGIN; +extern const char *WKT1_CENTRAL_MERIDIAN; +extern const char *WKT1_SCALE_FACTOR; +extern const char *WKT1_FALSE_EASTING; +extern const char *WKT1_FALSE_NORTHING; +extern const char *WKT1_STANDARD_PARALLEL_1; +extern const char *WKT1_STANDARD_PARALLEL_2; +extern const char *WKT1_LATITUDE_OF_CENTER; +extern const char *WKT1_LONGITUDE_OF_CENTER; +extern const char *WKT1_AZIMUTH; +extern const char *WKT1_RECTIFIED_GRID_ANGLE; + +struct ParamMapping { + const char *wkt2_name; + const int epsg_code; + const char *wkt1_name; + const common::UnitOfMeasure::Type unit_type; + const char *proj_name; +}; + +struct MethodMapping { + const char *wkt2_name; + const int epsg_code; + const char *wkt1_name; + const char *proj_name_main; + const char *proj_name_aux; + const ParamMapping *const *params; +}; + +extern const ParamMapping paramLatitudeNatOrigin; + +const MethodMapping *getProjectionMethodMappings(size_t &nElts); +const MethodMapping *getOtherMethodMappings(size_t &nElts); + +struct MethodNameCode { + const char *name; + int epsg_code; +}; + +const MethodNameCode *getMethodNameCodes(size_t &nElts); + +struct ParamNameCode { + const char *name; + int epsg_code; +}; + +const ParamNameCode *getParamNameCodes(size_t &nElts); + +const MethodMapping *getMapping(int epsg_code) noexcept; +const MethodMapping *getMappingFromWKT1(const std::string &wkt1_name) noexcept; +const MethodMapping *getMapping(const char *wkt2_name) noexcept; +const MethodMapping *getMapping(const OperationMethod *method) noexcept; +std::vector +getMappingsFromPROJName(const std::string &projName); +const ParamMapping *getMapping(const MethodMapping *mapping, + const OperationParameterNNPtr ¶m); +const ParamMapping *getMappingFromWKT1(const MethodMapping *mapping, + const std::string &wkt1_name); + +//! @endcond + +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END + +#endif // PARAMMAPPINGS_HPP diff --git a/src/iso19111/operation/projbasedoperation.cpp b/src/iso19111/operation/projbasedoperation.cpp new file mode 100644 index 0000000000..6e0fd10990 --- /dev/null +++ b/src/iso19111/operation/projbasedoperation.cpp @@ -0,0 +1,316 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP +#endif + +#include "proj/common.hpp" +#include "proj/coordinateoperation.hpp" +#include "proj/crs.hpp" +#include "proj/io.hpp" +#include "proj/metadata.hpp" +#include "proj/util.hpp" + +#include "proj/internal/internal.hpp" +#include "proj/internal/io_internal.hpp" + +#include "coordinateoperation_internal.hpp" +#include "oputils.hpp" + +// PROJ include order is sensitive +// clang-format off +#include "proj.h" +#include "proj_internal.h" // M_PI +// clang-format on +#include "proj_constants.h" +#include "proj_json_streaming_writer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace NS_PROJ::internal; + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +//! @cond Doxygen_Suppress + +// --------------------------------------------------------------------------- + +PROJBasedOperation::~PROJBasedOperation() = default; + +// --------------------------------------------------------------------------- + +PROJBasedOperation::PROJBasedOperation(const OperationMethodNNPtr &methodIn) + : SingleOperation(methodIn) {} + +// --------------------------------------------------------------------------- + +PROJBasedOperationNNPtr PROJBasedOperation::create( + const util::PropertyMap &properties, const std::string &PROJString, + const crs::CRSPtr &sourceCRS, const crs::CRSPtr &targetCRS, + const std::vector &accuracies) { + auto method = OperationMethod::create( + util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, + "PROJ-based operation method: " + PROJString), + std::vector{}); + auto op = PROJBasedOperation::nn_make_shared(method); + op->assignSelf(op); + op->projString_ = PROJString; + if (sourceCRS && targetCRS) { + op->setCRSs(NN_NO_CHECK(sourceCRS), NN_NO_CHECK(targetCRS), nullptr); + } + op->setProperties( + addDefaultNameIfNeeded(properties, "PROJ-based coordinate operation")); + op->setAccuracies(accuracies); + return op; +} + +// --------------------------------------------------------------------------- + +PROJBasedOperationNNPtr PROJBasedOperation::create( + const util::PropertyMap &properties, + const io::IPROJStringExportableNNPtr &projExportable, bool inverse, + const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS, + const crs::CRSPtr &interpolationCRS, + const std::vector &accuracies, + bool hasBallparkTransformation) { + + auto formatter = io::PROJStringFormatter::create(); + if (inverse) { + formatter->startInversion(); + } + projExportable->_exportToPROJString(formatter.get()); + if (inverse) { + formatter->stopInversion(); + } + auto projString = formatter->toString(); + + auto method = OperationMethod::create( + util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, + "PROJ-based operation method (approximate): " + + projString), + std::vector{}); + auto op = PROJBasedOperation::nn_make_shared(method); + op->assignSelf(op); + op->projString_ = projString; + op->setCRSs(sourceCRS, targetCRS, interpolationCRS); + op->setProperties( + addDefaultNameIfNeeded(properties, "PROJ-based coordinate operation")); + op->setAccuracies(accuracies); + op->projStringExportable_ = projExportable.as_nullable(); + op->inverse_ = inverse; + op->setHasBallparkTransformation(hasBallparkTransformation); + return op; +} + +// --------------------------------------------------------------------------- + +CoordinateOperationNNPtr PROJBasedOperation::inverse() const { + + if (projStringExportable_ && sourceCRS() && targetCRS()) { + return util::nn_static_pointer_cast( + PROJBasedOperation::create( + createPropertiesForInverse(this, false, false), + NN_NO_CHECK(projStringExportable_), !inverse_, + NN_NO_CHECK(targetCRS()), NN_NO_CHECK(sourceCRS()), + interpolationCRS(), coordinateOperationAccuracies(), + hasBallparkTransformation())); + } + + auto formatter = io::PROJStringFormatter::create(); + formatter->startInversion(); + try { + formatter->ingestPROJString(projString_); + } catch (const io::ParsingException &e) { + throw util::UnsupportedOperationException( + std::string("PROJBasedOperation::inverse() failed: ") + e.what()); + } + formatter->stopInversion(); + + auto op = PROJBasedOperation::create( + createPropertiesForInverse(this, false, false), formatter->toString(), + targetCRS(), sourceCRS(), coordinateOperationAccuracies()); + if (sourceCRS() && targetCRS()) { + op->setCRSs(NN_NO_CHECK(targetCRS()), NN_NO_CHECK(sourceCRS()), + interpolationCRS()); + } + op->setHasBallparkTransformation(hasBallparkTransformation()); + return util::nn_static_pointer_cast(op); +} + +// --------------------------------------------------------------------------- + +void PROJBasedOperation::_exportToWKT(io::WKTFormatter *formatter) const { + + if (sourceCRS() && targetCRS()) { + exportTransformationToWKT(formatter); + return; + } + + const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; + if (!isWKT2) { + throw io::FormattingException( + "PROJBasedOperation can only be exported to WKT2"); + } + + formatter->startNode(io::WKTConstants::CONVERSION, false); + formatter->addQuotedString(nameStr()); + method()->_exportToWKT(formatter); + + for (const auto ¶mValue : parameterValues()) { + paramValue->_exportToWKT(formatter); + } + formatter->endNode(); +} + +// --------------------------------------------------------------------------- + +void PROJBasedOperation::_exportToJSON( + io::JSONFormatter *formatter) const // throw(FormattingException) +{ + auto writer = formatter->writer(); + auto objectContext(formatter->MakeObjectContext( + (sourceCRS() && targetCRS()) ? "Transformation" : "Conversion", + !identifiers().empty())); + + writer->AddObjKey("name"); + auto l_name = nameStr(); + if (l_name.empty()) { + writer->Add("unnamed"); + } else { + writer->Add(l_name); + } + + if (sourceCRS() && targetCRS()) { + writer->AddObjKey("source_crs"); + formatter->setAllowIDInImmediateChild(); + sourceCRS()->_exportToJSON(formatter); + + writer->AddObjKey("target_crs"); + formatter->setAllowIDInImmediateChild(); + targetCRS()->_exportToJSON(formatter); + } + + writer->AddObjKey("method"); + formatter->setOmitTypeInImmediateChild(); + formatter->setAllowIDInImmediateChild(); + method()->_exportToJSON(formatter); + + const auto &l_parameterValues = parameterValues(); + if (!l_parameterValues.empty()) { + writer->AddObjKey("parameters"); + { + auto parametersContext(writer->MakeArrayContext(false)); + for (const auto &genOpParamvalue : l_parameterValues) { + formatter->setAllowIDInImmediateChild(); + formatter->setOmitTypeInImmediateChild(); + genOpParamvalue->_exportToJSON(formatter); + } + } + } +} + +// --------------------------------------------------------------------------- + +void PROJBasedOperation::_exportToPROJString( + io::PROJStringFormatter *formatter) const { + if (projStringExportable_) { + if (inverse_) { + formatter->startInversion(); + } + projStringExportable_->_exportToPROJString(formatter); + if (inverse_) { + formatter->stopInversion(); + } + return; + } + + try { + formatter->ingestPROJString(projString_); + } catch (const io::ParsingException &e) { + throw io::FormattingException( + std::string("PROJBasedOperation::exportToPROJString() failed: ") + + e.what()); + } +} + +// --------------------------------------------------------------------------- + +CoordinateOperationNNPtr PROJBasedOperation::_shallowClone() const { + auto op = PROJBasedOperation::nn_make_shared(*this); + op->assignSelf(op); + op->setCRSs(this, false); + return util::nn_static_pointer_cast(op); +} + +// --------------------------------------------------------------------------- + +std::set +PROJBasedOperation::gridsNeeded(const io::DatabaseContextPtr &databaseContext, + bool considerKnownGridsAsAvailable) const { + std::set res; + + try { + auto formatterOut = io::PROJStringFormatter::create(); + auto formatter = io::PROJStringFormatter::create(); + formatter->ingestPROJString(exportToPROJString(formatterOut.get())); + const auto usedGridNames = formatter->getUsedGridNames(); + for (const auto &shortName : usedGridNames) { + GridDescription desc; + desc.shortName = shortName; + if (databaseContext) { + databaseContext->lookForGridInfo( + desc.shortName, considerKnownGridsAsAvailable, + desc.fullName, desc.packageName, desc.url, + desc.directDownload, desc.openLicense, desc.available); + } + res.insert(desc); + } + } catch (const io::ParsingException &) { + } + + return res; +} + +//! @endcond + +// --------------------------------------------------------------------------- + +} // namespace operation + +NS_PROJ_END diff --git a/src/iso19111/operation/singleoperation.cpp b/src/iso19111/operation/singleoperation.cpp new file mode 100644 index 0000000000..0cd7b57a52 --- /dev/null +++ b/src/iso19111/operation/singleoperation.cpp @@ -0,0 +1,2218 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP +#endif + +#include "proj/common.hpp" +#include "proj/coordinateoperation.hpp" +#include "proj/crs.hpp" +#include "proj/io.hpp" +#include "proj/metadata.hpp" +#include "proj/util.hpp" + +#include "proj/internal/internal.hpp" +#include "proj/internal/io_internal.hpp" + +#include "coordinateoperation_internal.hpp" +#include "coordinateoperation_private.hpp" +#include "operationmethod_private.hpp" +#include "oputils.hpp" +#include "parammappings.hpp" + +// PROJ include order is sensitive +// clang-format off +#include "proj.h" +#include "proj_internal.h" // M_PI +// clang-format on +#include "proj_constants.h" +#include "proj_json_streaming_writer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace NS_PROJ::internal; + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +InvalidOperationEmptyIntersection::InvalidOperationEmptyIntersection( + const std::string &message) + : InvalidOperation(message) {} + +InvalidOperationEmptyIntersection::InvalidOperationEmptyIntersection( + const InvalidOperationEmptyIntersection &) = default; + +InvalidOperationEmptyIntersection::~InvalidOperationEmptyIntersection() = + default; + +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +// --------------------------------------------------------------------------- + +GridDescription::GridDescription() + : shortName{}, fullName{}, packageName{}, url{}, directDownload(false), + openLicense(false), available(false) {} + +GridDescription::~GridDescription() = default; + +GridDescription::GridDescription(const GridDescription &) = default; + +GridDescription::GridDescription(GridDescription &&other) noexcept + : shortName(std::move(other.shortName)), + fullName(std::move(other.fullName)), + packageName(std::move(other.packageName)), + url(std::move(other.url)), + directDownload(other.directDownload), + openLicense(other.openLicense), + available(other.available) {} + +//! @endcond + +// --------------------------------------------------------------------------- + +CoordinateOperation::CoordinateOperation() + : d(internal::make_unique()) {} + +// --------------------------------------------------------------------------- + +CoordinateOperation::CoordinateOperation(const CoordinateOperation &other) + : ObjectUsage(other), d(internal::make_unique(*other.d)) {} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +CoordinateOperation::~CoordinateOperation() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Return the version of the coordinate transformation (i.e. + * instantiation + * due to the stochastic nature of the parameters). + * + * Mandatory when describing a coordinate transformation or point motion + * operation, and should not be supplied for a coordinate conversion. + * + * @return version or empty. + */ +const util::optional & +CoordinateOperation::operationVersion() const { + return d->operationVersion_; +} + +// --------------------------------------------------------------------------- + +/** \brief Return estimate(s) of the impact of this coordinate operation on + * point accuracy. + * + * Gives position error estimates for target coordinates of this coordinate + * operation, assuming no errors in source coordinates. + * + * @return estimate(s) or empty vector. + */ +const std::vector & +CoordinateOperation::coordinateOperationAccuracies() const { + return d->coordinateOperationAccuracies_; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the source CRS of this coordinate operation. + * + * This should not be null, expect for of a derivingConversion of a DerivedCRS + * when the owning DerivedCRS has been destroyed. + * + * @return source CRS, or null. + */ +const crs::CRSPtr CoordinateOperation::sourceCRS() const { + return d->sourceCRSWeak_.lock(); +} + +// --------------------------------------------------------------------------- + +/** \brief Return the target CRS of this coordinate operation. + * + * This should not be null, expect for of a derivingConversion of a DerivedCRS + * when the owning DerivedCRS has been destroyed. + * + * @return target CRS, or null. + */ +const crs::CRSPtr CoordinateOperation::targetCRS() const { + return d->targetCRSWeak_.lock(); +} + +// --------------------------------------------------------------------------- + +/** \brief Return the interpolation CRS of this coordinate operation. + * + * @return interpolation CRS, or null. + */ +const crs::CRSPtr &CoordinateOperation::interpolationCRS() const { + return d->interpolationCRS_; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the source epoch of coordinates. + * + * @return source epoch of coordinates, or empty. + */ +const util::optional & +CoordinateOperation::sourceCoordinateEpoch() const { + return d->sourceCoordinateEpoch_; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the target epoch of coordinates. + * + * @return target epoch of coordinates, or empty. + */ +const util::optional & +CoordinateOperation::targetCoordinateEpoch() const { + return d->targetCoordinateEpoch_; +} + +// --------------------------------------------------------------------------- + +void CoordinateOperation::setWeakSourceTargetCRS( + std::weak_ptr sourceCRSIn, std::weak_ptr targetCRSIn) { + d->sourceCRSWeak_ = sourceCRSIn; + d->targetCRSWeak_ = targetCRSIn; +} + +// --------------------------------------------------------------------------- + +void CoordinateOperation::setCRSs(const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, + const crs::CRSPtr &interpolationCRSIn) { + d->strongRef_ = + internal::make_unique(sourceCRSIn, targetCRSIn); + d->sourceCRSWeak_ = sourceCRSIn.as_nullable(); + d->targetCRSWeak_ = targetCRSIn.as_nullable(); + d->interpolationCRS_ = interpolationCRSIn; +} +// --------------------------------------------------------------------------- + +void CoordinateOperation::setCRSs(const CoordinateOperation *in, + bool inverseSourceTarget) { + auto l_sourceCRS = in->sourceCRS(); + auto l_targetCRS = in->targetCRS(); + if (l_sourceCRS && l_targetCRS) { + auto nn_sourceCRS = NN_NO_CHECK(l_sourceCRS); + auto nn_targetCRS = NN_NO_CHECK(l_targetCRS); + if (inverseSourceTarget) { + setCRSs(nn_targetCRS, nn_sourceCRS, in->interpolationCRS()); + } else { + setCRSs(nn_sourceCRS, nn_targetCRS, in->interpolationCRS()); + } + } +} + +// --------------------------------------------------------------------------- + +void CoordinateOperation::setAccuracies( + const std::vector &accuracies) { + d->coordinateOperationAccuracies_ = accuracies; +} + +// --------------------------------------------------------------------------- + +/** \brief Return whether a coordinate operation can be instantiated as + * a PROJ pipeline, checking in particular that referenced grids are + * available. + */ +bool CoordinateOperation::isPROJInstantiable( + const io::DatabaseContextPtr &databaseContext, + bool considerKnownGridsAsAvailable) const { + try { + exportToPROJString(io::PROJStringFormatter::create().get()); + } catch (const std::exception &) { + return false; + } + for (const auto &gridDesc : + gridsNeeded(databaseContext, considerKnownGridsAsAvailable)) { + if (!gridDesc.available) { + return false; + } + } + return true; +} + +// --------------------------------------------------------------------------- + +/** \brief Return whether a coordinate operation has a "ballpark" + * transformation, + * that is a very approximate one, due to lack of more accurate transformations. + * + * Typically a null geographic offset between two horizontal datum, or a + * null vertical offset (or limited to unit changes) between two vertical + * datum. Errors of several tens to one hundred meters might be expected, + * compared to more accurate transformations. + */ +bool CoordinateOperation::hasBallparkTransformation() const { + return d->hasBallparkTransformation_; +} + +// --------------------------------------------------------------------------- + +void CoordinateOperation::setHasBallparkTransformation(bool b) { + d->hasBallparkTransformation_ = b; +} + +// --------------------------------------------------------------------------- + +void CoordinateOperation::setProperties( + const util::PropertyMap &properties) // throw(InvalidValueTypeException) +{ + ObjectUsage::setProperties(properties); + properties.getStringValue(OPERATION_VERSION_KEY, d->operationVersion_); +} + +// --------------------------------------------------------------------------- + +/** \brief Return a variation of the current coordinate operation whose axis + * order is the one expected for visualization purposes. + */ +CoordinateOperationNNPtr +CoordinateOperation::normalizeForVisualization() const { + auto l_sourceCRS = sourceCRS(); + auto l_targetCRS = targetCRS(); + if (!l_sourceCRS || !l_targetCRS) { + throw util::UnsupportedOperationException( + "Cannot retrieve source or target CRS"); + } + const bool swapSource = + l_sourceCRS->mustAxisOrderBeSwitchedForVisualization(); + const bool swapTarget = + l_targetCRS->mustAxisOrderBeSwitchedForVisualization(); + auto l_this = NN_NO_CHECK(std::dynamic_pointer_cast( + shared_from_this().as_nullable())); + if (!swapSource && !swapTarget) { + return l_this; + } + std::vector subOps; + if (swapSource) { + auto op = Conversion::createAxisOrderReversal(false); + op->setCRSs(l_sourceCRS->normalizeForVisualization(), + NN_NO_CHECK(l_sourceCRS), nullptr); + subOps.emplace_back(op); + } + subOps.emplace_back(l_this); + if (swapTarget) { + auto op = Conversion::createAxisOrderReversal(false); + op->setCRSs(NN_NO_CHECK(l_targetCRS), + l_targetCRS->normalizeForVisualization(), nullptr); + subOps.emplace_back(op); + } + return util::nn_static_pointer_cast( + ConcatenatedOperation::createComputeMetadata(subOps, true)); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +CoordinateOperationNNPtr CoordinateOperation::shallowClone() const { + return _shallowClone(); +} +//! @endcond + +// --------------------------------------------------------------------------- + +OperationMethod::OperationMethod() : d(internal::make_unique()) {} + +// --------------------------------------------------------------------------- + +OperationMethod::OperationMethod(const OperationMethod &other) + : IdentifiedObject(other), d(internal::make_unique(*other.d)) {} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +OperationMethod::~OperationMethod() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Return the formula(s) or procedure used by this coordinate operation + * method. + * + * This may be a reference to a publication (in which case use + * formulaCitation()). + * + * Note that the operation method may not be analytic, in which case this + * attribute references or contains the procedure, not an analytic formula. + * + * @return the formula, or empty. + */ +const util::optional &OperationMethod::formula() PROJ_PURE_DEFN { + return d->formula_; +} + +// --------------------------------------------------------------------------- + +/** \brief Return a reference to a publication giving the formula(s) or + * procedure + * used by the coordinate operation method. + * + * @return the formula citation, or empty. + */ +const util::optional & +OperationMethod::formulaCitation() PROJ_PURE_DEFN { + return d->formulaCitation_; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the parameters of this operation method. + * + * @return the parameters. + */ +const std::vector & +OperationMethod::parameters() PROJ_PURE_DEFN { + return d->parameters_; +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a operation method from a vector of + * GeneralOperationParameter. + * + * @param properties See \ref general_properties. At minimum the name should be + * defined. + * @param parameters Vector of GeneralOperationParameterNNPtr. + * @return a new OperationMethod. + */ +OperationMethodNNPtr OperationMethod::create( + const util::PropertyMap &properties, + const std::vector ¶meters) { + OperationMethodNNPtr method( + OperationMethod::nn_make_shared()); + method->assignSelf(method); + method->setProperties(properties); + method->d->parameters_ = parameters; + properties.getStringValue("proj_method", method->d->projMethodOverride_); + return method; +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a operation method from a vector of OperationParameter. + * + * @param properties See \ref general_properties. At minimum the name should be + * defined. + * @param parameters Vector of OperationParameterNNPtr. + * @return a new OperationMethod. + */ +OperationMethodNNPtr OperationMethod::create( + const util::PropertyMap &properties, + const std::vector ¶meters) { + std::vector parametersGeneral; + parametersGeneral.reserve(parameters.size()); + for (const auto &p : parameters) { + parametersGeneral.push_back(p); + } + return create(properties, parametersGeneral); +} + +// --------------------------------------------------------------------------- + +/** \brief Return the EPSG code, either directly, or through the name + * @return code, or 0 if not found + */ +int OperationMethod::getEPSGCode() PROJ_PURE_DEFN { + int epsg_code = IdentifiedObject::getEPSGCode(); + if (epsg_code == 0) { + auto l_name = nameStr(); + if (ends_with(l_name, " (3D)")) { + l_name.resize(l_name.size() - strlen(" (3D)")); + } + size_t nMethodNameCodes = 0; + const auto methodNameCodes = getMethodNameCodes(nMethodNameCodes); + for (size_t i = 0; i < nMethodNameCodes; ++i) { + const auto &tuple = methodNameCodes[i]; + if (metadata::Identifier::isEquivalentName(l_name.c_str(), + tuple.name)) { + return tuple.epsg_code; + } + } + } + return epsg_code; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void OperationMethod::_exportToWKT(io::WKTFormatter *formatter) const { + const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; + formatter->startNode(isWKT2 ? io::WKTConstants::METHOD + : io::WKTConstants::PROJECTION, + !identifiers().empty()); + std::string l_name(nameStr()); + if (!isWKT2) { + const MethodMapping *mapping = getMapping(this); + if (mapping == nullptr) { + l_name = replaceAll(l_name, " ", "_"); + } else { + if (l_name == + PROJ_WKT2_NAME_METHOD_GEOSTATIONARY_SATELLITE_SWEEP_X) { + l_name = "Geostationary_Satellite"; + } else { + if (mapping->wkt1_name == nullptr) { + throw io::FormattingException( + std::string("Unsupported conversion method: ") + + mapping->wkt2_name); + } + l_name = mapping->wkt1_name; + } + } + } + formatter->addQuotedString(l_name); + if (formatter->outputId()) { + formatID(formatter); + } + formatter->endNode(); +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void OperationMethod::_exportToJSON( + io::JSONFormatter *formatter) const // throw(FormattingException) +{ + auto writer = formatter->writer(); + auto objectContext(formatter->MakeObjectContext("OperationMethod", + !identifiers().empty())); + + writer->AddObjKey("name"); + writer->Add(nameStr()); + + if (formatter->outputId()) { + formatID(formatter); + } +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +bool OperationMethod::_isEquivalentTo( + const util::IComparable *other, util::IComparable::Criterion criterion, + const io::DatabaseContextPtr &dbContext) const { + auto otherOM = dynamic_cast(other); + if (otherOM == nullptr || + !IdentifiedObject::_isEquivalentTo(other, criterion, dbContext)) { + return false; + } + // TODO test formula and formulaCitation + const auto ¶ms = parameters(); + const auto &otherParams = otherOM->parameters(); + const auto paramsSize = params.size(); + if (paramsSize != otherParams.size()) { + return false; + } + if (criterion == util::IComparable::Criterion::STRICT) { + for (size_t i = 0; i < paramsSize; i++) { + if (!params[i]->_isEquivalentTo(otherParams[i].get(), criterion, + dbContext)) { + return false; + } + } + } else { + std::vector candidateIndices(paramsSize, true); + for (size_t i = 0; i < paramsSize; i++) { + bool found = false; + for (size_t j = 0; j < paramsSize; j++) { + if (candidateIndices[j] && + params[i]->_isEquivalentTo(otherParams[j].get(), criterion, + dbContext)) { + candidateIndices[j] = false; + found = true; + break; + } + } + if (!found) { + return false; + } + } + } + return true; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct GeneralParameterValue::Private {}; +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +GeneralParameterValue::GeneralParameterValue() : d(nullptr) {} + +// --------------------------------------------------------------------------- + +GeneralParameterValue::GeneralParameterValue(const GeneralParameterValue &) + : d(nullptr) {} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +GeneralParameterValue::~GeneralParameterValue() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct OperationParameterValue::Private { + OperationParameterNNPtr parameter; + ParameterValueNNPtr parameterValue; + + Private(const OperationParameterNNPtr ¶meterIn, + const ParameterValueNNPtr &valueIn) + : parameter(parameterIn), parameterValue(valueIn) {} +}; +//! @endcond + +// --------------------------------------------------------------------------- + +OperationParameterValue::OperationParameterValue( + const OperationParameterValue &other) + : GeneralParameterValue(other), + d(internal::make_unique(*other.d)) {} + +// --------------------------------------------------------------------------- + +OperationParameterValue::OperationParameterValue( + const OperationParameterNNPtr ¶meterIn, + const ParameterValueNNPtr &valueIn) + : GeneralParameterValue(), + d(internal::make_unique(parameterIn, valueIn)) {} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a OperationParameterValue. + * + * @param parameterIn Parameter (definition). + * @param valueIn Parameter value. + * @return a new OperationParameterValue. + */ +OperationParameterValueNNPtr +OperationParameterValue::create(const OperationParameterNNPtr ¶meterIn, + const ParameterValueNNPtr &valueIn) { + return OperationParameterValue::nn_make_shared( + parameterIn, valueIn); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +OperationParameterValue::~OperationParameterValue() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Return the parameter (definition) + * + * @return the parameter (definition). + */ +const OperationParameterNNPtr & +OperationParameterValue::parameter() PROJ_PURE_DEFN { + return d->parameter; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the parameter value. + * + * @return the parameter value. + */ +const ParameterValueNNPtr & +OperationParameterValue::parameterValue() PROJ_PURE_DEFN { + return d->parameterValue; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void OperationParameterValue::_exportToWKT( + // cppcheck-suppress passedByValue + io::WKTFormatter *formatter) const { + _exportToWKT(formatter, nullptr); +} + +void OperationParameterValue::_exportToWKT(io::WKTFormatter *formatter, + const MethodMapping *mapping) const { + const ParamMapping *paramMapping = + mapping ? getMapping(mapping, d->parameter) : nullptr; + if (paramMapping && paramMapping->wkt1_name == nullptr) { + return; + } + const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; + if (isWKT2 && parameterValue()->type() == ParameterValue::Type::FILENAME) { + formatter->startNode(io::WKTConstants::PARAMETERFILE, + !parameter()->identifiers().empty()); + } else { + formatter->startNode(io::WKTConstants::PARAMETER, + !parameter()->identifiers().empty()); + } + if (paramMapping) { + formatter->addQuotedString(paramMapping->wkt1_name); + } else { + formatter->addQuotedString(parameter()->nameStr()); + } + parameterValue()->_exportToWKT(formatter); + if (formatter->outputId()) { + parameter()->formatID(formatter); + } + formatter->endNode(); +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void OperationParameterValue::_exportToJSON( + io::JSONFormatter *formatter) const { + auto writer = formatter->writer(); + auto objectContext(formatter->MakeObjectContext( + "ParameterValue", !parameter()->identifiers().empty())); + + writer->AddObjKey("name"); + writer->Add(parameter()->nameStr()); + + const auto &l_value(parameterValue()); + if (l_value->type() == ParameterValue::Type::MEASURE) { + writer->AddObjKey("value"); + writer->Add(l_value->value().value(), 15); + writer->AddObjKey("unit"); + const auto &l_unit(l_value->value().unit()); + if (l_unit == common::UnitOfMeasure::METRE || + l_unit == common::UnitOfMeasure::DEGREE || + l_unit == common::UnitOfMeasure::SCALE_UNITY) { + writer->Add(l_unit.name()); + } else { + l_unit._exportToJSON(formatter); + } + } else if (l_value->type() == ParameterValue::Type::FILENAME) { + writer->AddObjKey("value"); + writer->Add(l_value->valueFile()); + } + + if (formatter->outputId()) { + parameter()->formatID(formatter); + } +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +/** Utility method used on WKT2 import to convert from abridged transformation + * to "normal" transformation parameters. + */ +bool OperationParameterValue::convertFromAbridged( + const std::string ¶mName, double &val, + const common::UnitOfMeasure *&unit, int ¶mEPSGCode) { + if (metadata::Identifier::isEquivalentName( + paramName.c_str(), EPSG_NAME_PARAMETER_X_AXIS_TRANSLATION) || + paramEPSGCode == EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION) { + unit = &common::UnitOfMeasure::METRE; + paramEPSGCode = EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION; + return true; + } else if (metadata::Identifier::isEquivalentName( + paramName.c_str(), EPSG_NAME_PARAMETER_Y_AXIS_TRANSLATION) || + paramEPSGCode == EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION) { + unit = &common::UnitOfMeasure::METRE; + paramEPSGCode = EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION; + return true; + } else if (metadata::Identifier::isEquivalentName( + paramName.c_str(), EPSG_NAME_PARAMETER_Z_AXIS_TRANSLATION) || + paramEPSGCode == EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION) { + unit = &common::UnitOfMeasure::METRE; + paramEPSGCode = EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION; + return true; + } else if (metadata::Identifier::isEquivalentName( + paramName.c_str(), EPSG_NAME_PARAMETER_X_AXIS_ROTATION) || + paramEPSGCode == EPSG_CODE_PARAMETER_X_AXIS_ROTATION) { + unit = &common::UnitOfMeasure::ARC_SECOND; + paramEPSGCode = EPSG_CODE_PARAMETER_X_AXIS_ROTATION; + return true; + } else if (metadata::Identifier::isEquivalentName( + paramName.c_str(), EPSG_NAME_PARAMETER_Y_AXIS_ROTATION) || + paramEPSGCode == EPSG_CODE_PARAMETER_Y_AXIS_ROTATION) { + unit = &common::UnitOfMeasure::ARC_SECOND; + paramEPSGCode = EPSG_CODE_PARAMETER_Y_AXIS_ROTATION; + return true; + + } else if (metadata::Identifier::isEquivalentName( + paramName.c_str(), EPSG_NAME_PARAMETER_Z_AXIS_ROTATION) || + paramEPSGCode == EPSG_CODE_PARAMETER_Z_AXIS_ROTATION) { + unit = &common::UnitOfMeasure::ARC_SECOND; + paramEPSGCode = EPSG_CODE_PARAMETER_Z_AXIS_ROTATION; + return true; + + } else if (metadata::Identifier::isEquivalentName( + paramName.c_str(), EPSG_NAME_PARAMETER_SCALE_DIFFERENCE) || + paramEPSGCode == EPSG_CODE_PARAMETER_SCALE_DIFFERENCE) { + val = (val - 1.0) * 1e6; + unit = &common::UnitOfMeasure::PARTS_PER_MILLION; + paramEPSGCode = EPSG_CODE_PARAMETER_SCALE_DIFFERENCE; + return true; + } + return false; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +bool OperationParameterValue::_isEquivalentTo( + const util::IComparable *other, util::IComparable::Criterion criterion, + const io::DatabaseContextPtr &dbContext) const { + auto otherOPV = dynamic_cast(other); + if (otherOPV == nullptr) { + return false; + } + if (!d->parameter->_isEquivalentTo(otherOPV->d->parameter.get(), criterion, + dbContext)) { + return false; + } + if (criterion == util::IComparable::Criterion::STRICT) { + return d->parameterValue->_isEquivalentTo( + otherOPV->d->parameterValue.get(), criterion); + } + if (d->parameterValue->_isEquivalentTo(otherOPV->d->parameterValue.get(), + criterion, dbContext)) { + return true; + } + if (d->parameter->getEPSGCode() == + EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE || + d->parameter->getEPSGCode() == + EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID) { + if (parameterValue()->type() == ParameterValue::Type::MEASURE && + otherOPV->parameterValue()->type() == + ParameterValue::Type::MEASURE) { + const double a = std::fmod(parameterValue()->value().convertToUnit( + common::UnitOfMeasure::DEGREE) + + 360.0, + 360.0); + const double b = + std::fmod(otherOPV->parameterValue()->value().convertToUnit( + common::UnitOfMeasure::DEGREE) + + 360.0, + 360.0); + return std::fabs(a - b) <= 1e-10 * std::fabs(a); + } + } + return false; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct GeneralOperationParameter::Private {}; +//! @endcond + +// --------------------------------------------------------------------------- + +GeneralOperationParameter::GeneralOperationParameter() : d(nullptr) {} + +// --------------------------------------------------------------------------- + +GeneralOperationParameter::GeneralOperationParameter( + const GeneralOperationParameter &other) + : IdentifiedObject(other), d(nullptr) {} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +GeneralOperationParameter::~GeneralOperationParameter() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct OperationParameter::Private {}; +//! @endcond + +// --------------------------------------------------------------------------- + +OperationParameter::OperationParameter() : d(nullptr) {} + +// --------------------------------------------------------------------------- + +OperationParameter::OperationParameter(const OperationParameter &other) + : GeneralOperationParameter(other), d(nullptr) {} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +OperationParameter::~OperationParameter() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a OperationParameter. + * + * @param properties See \ref general_properties. At minimum the name should be + * defined. + * @return a new OperationParameter. + */ +OperationParameterNNPtr +OperationParameter::create(const util::PropertyMap &properties) { + OperationParameterNNPtr op( + OperationParameter::nn_make_shared()); + op->assignSelf(op); + op->setProperties(properties); + return op; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +bool OperationParameter::_isEquivalentTo( + const util::IComparable *other, util::IComparable::Criterion criterion, + const io::DatabaseContextPtr &dbContext) const { + auto otherOP = dynamic_cast(other); + if (otherOP == nullptr) { + return false; + } + if (criterion == util::IComparable::Criterion::STRICT) { + return IdentifiedObject::_isEquivalentTo(other, criterion, dbContext); + } + if (IdentifiedObject::_isEquivalentTo(other, criterion, dbContext)) { + return true; + } + auto l_epsgCode = getEPSGCode(); + return l_epsgCode != 0 && l_epsgCode == otherOP->getEPSGCode(); +} +//! @endcond + +// --------------------------------------------------------------------------- + +void OperationParameter::_exportToWKT(io::WKTFormatter *) const {} + +// --------------------------------------------------------------------------- + +/** \brief Return the name of a parameter designed by its EPSG code + * @return name, or nullptr if not found + */ +const char *OperationParameter::getNameForEPSGCode(int epsg_code) noexcept { + size_t nParamNameCodes = 0; + const auto paramNameCodes = getParamNameCodes(nParamNameCodes); + for (size_t i = 0; i < nParamNameCodes; ++i) { + const auto &tuple = paramNameCodes[i]; + if (tuple.epsg_code == epsg_code) { + return tuple.name; + } + } + return nullptr; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the EPSG code, either directly, or through the name + * @return code, or 0 if not found + */ +int OperationParameter::getEPSGCode() PROJ_PURE_DEFN { + int epsg_code = IdentifiedObject::getEPSGCode(); + if (epsg_code == 0) { + const auto &l_name = nameStr(); + size_t nParamNameCodes = 0; + const auto paramNameCodes = getParamNameCodes(nParamNameCodes); + for (size_t i = 0; i < nParamNameCodes; ++i) { + const auto &tuple = paramNameCodes[i]; + if (metadata::Identifier::isEquivalentName(l_name.c_str(), + tuple.name)) { + return tuple.epsg_code; + } + } + if (metadata::Identifier::isEquivalentName(l_name.c_str(), + "Latitude of origin")) { + return EPSG_CODE_PARAMETER_LATITUDE_OF_NATURAL_ORIGIN; + } + if (metadata::Identifier::isEquivalentName(l_name.c_str(), + "Scale factor")) { + return EPSG_CODE_PARAMETER_SCALE_FACTOR_AT_NATURAL_ORIGIN; + } + } + return epsg_code; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct SingleOperation::Private { + std::vector parameterValues_{}; + OperationMethodNNPtr method_; + + explicit Private(const OperationMethodNNPtr &methodIn) + : method_(methodIn) {} +}; +//! @endcond + +// --------------------------------------------------------------------------- + +SingleOperation::SingleOperation(const OperationMethodNNPtr &methodIn) + : d(internal::make_unique(methodIn)) {} + +// --------------------------------------------------------------------------- + +SingleOperation::SingleOperation(const SingleOperation &other) + : +#if !defined(COMPILER_WARNS_ABOUT_ABSTRACT_VBASE_INIT) + CoordinateOperation(other), +#endif + d(internal::make_unique(*other.d)) { +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +SingleOperation::~SingleOperation() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Return the parameter values. + * + * @return the parameter values. + */ +const std::vector & +SingleOperation::parameterValues() PROJ_PURE_DEFN { + return d->parameterValues_; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the operation method associated to the operation. + * + * @return the operation method. + */ +const OperationMethodNNPtr &SingleOperation::method() PROJ_PURE_DEFN { + return d->method_; +} + +// --------------------------------------------------------------------------- + +void SingleOperation::setParameterValues( + const std::vector &values) { + d->parameterValues_ = values; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static const ParameterValuePtr nullParameterValue; +//! @endcond + +/** \brief Return the parameter value corresponding to a parameter name or + * EPSG code + * + * @param paramName the parameter name (or empty, in which case epsg_code + * should be non zero) + * @param epsg_code the parameter EPSG code (possibly zero) + * @return the value, or nullptr if not found. + */ +const ParameterValuePtr & +SingleOperation::parameterValue(const std::string ¶mName, + int epsg_code) const noexcept { + if (epsg_code) { + for (const auto &genOpParamvalue : parameterValues()) { + auto opParamvalue = dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue) { + const auto ¶meter = opParamvalue->parameter(); + if (parameter->getEPSGCode() == epsg_code) { + return opParamvalue->parameterValue(); + } + } + } + } + for (const auto &genOpParamvalue : parameterValues()) { + auto opParamvalue = dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue) { + const auto ¶meter = opParamvalue->parameter(); + if (metadata::Identifier::isEquivalentName( + paramName.c_str(), parameter->nameStr().c_str())) { + return opParamvalue->parameterValue(); + } + } + } + for (const auto &genOpParamvalue : parameterValues()) { + auto opParamvalue = dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue) { + const auto ¶meter = opParamvalue->parameter(); + if (areEquivalentParameters(paramName, parameter->nameStr())) { + return opParamvalue->parameterValue(); + } + } + } + return nullParameterValue; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the parameter value corresponding to a EPSG code + * + * @param epsg_code the parameter EPSG code + * @return the value, or nullptr if not found. + */ +const ParameterValuePtr &SingleOperation::parameterValue(int epsg_code) const + noexcept { + for (const auto &genOpParamvalue : parameterValues()) { + auto opParamvalue = dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue) { + const auto ¶meter = opParamvalue->parameter(); + if (parameter->getEPSGCode() == epsg_code) { + return opParamvalue->parameterValue(); + } + } + } + return nullParameterValue; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the parameter value, as a measure, corresponding to a + * parameter name or EPSG code + * + * @param paramName the parameter name (or empty, in which case epsg_code + * should be non zero) + * @param epsg_code the parameter EPSG code (possibly zero) + * @return the measure, or the empty Measure() object if not found. + */ +const common::Measure & +SingleOperation::parameterValueMeasure(const std::string ¶mName, + int epsg_code) const noexcept { + const auto &val = parameterValue(paramName, epsg_code); + if (val && val->type() == ParameterValue::Type::MEASURE) { + return val->value(); + } + return nullMeasure; +} + +/** \brief Return the parameter value, as a measure, corresponding to a + * EPSG code + * + * @param epsg_code the parameter EPSG code + * @return the measure, or the empty Measure() object if not found. + */ +const common::Measure & +SingleOperation::parameterValueMeasure(int epsg_code) const noexcept { + const auto &val = parameterValue(epsg_code); + if (val && val->type() == ParameterValue::Type::MEASURE) { + return val->value(); + } + return nullMeasure; +} + +//! @cond Doxygen_Suppress + +double SingleOperation::parameterValueNumericAsSI(int epsg_code) const + noexcept { + const auto &val = parameterValue(epsg_code); + if (val && val->type() == ParameterValue::Type::MEASURE) { + return val->value().getSIValue(); + } + return 0.0; +} + +double SingleOperation::parameterValueNumeric( + int epsg_code, const common::UnitOfMeasure &targetUnit) const noexcept { + const auto &val = parameterValue(epsg_code); + if (val && val->type() == ParameterValue::Type::MEASURE) { + return val->value().convertToUnit(targetUnit); + } + return 0.0; +} + +double SingleOperation::parameterValueNumeric( + const char *param_name, const common::UnitOfMeasure &targetUnit) const + noexcept { + const auto &val = parameterValue(param_name, 0); + if (val && val->type() == ParameterValue::Type::MEASURE) { + return val->value().convertToUnit(targetUnit); + } + return 0.0; +} + +//! @endcond +// --------------------------------------------------------------------------- + +/** \brief Instantiate a PROJ-based single operation. + * + * \note The operation might internally be a pipeline chaining several + * operations. + * The use of the SingleOperation modeling here is mostly to be able to get + * the PROJ string as a parameter. + * + * @param properties Properties + * @param PROJString the PROJ string. + * @param sourceCRS source CRS (might be null). + * @param targetCRS target CRS (might be null). + * @param accuracies Vector of positional accuracy (might be empty). + * @return the new instance + */ +SingleOperationNNPtr SingleOperation::createPROJBased( + const util::PropertyMap &properties, const std::string &PROJString, + const crs::CRSPtr &sourceCRS, const crs::CRSPtr &targetCRS, + const std::vector &accuracies) { + return util::nn_static_pointer_cast( + PROJBasedOperation::create(properties, PROJString, sourceCRS, targetCRS, + accuracies)); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +bool SingleOperation::_isEquivalentTo( + const util::IComparable *other, util::IComparable::Criterion criterion, + const io::DatabaseContextPtr &dbContext) const { + return _isEquivalentTo(other, criterion, dbContext, false); +} + +bool SingleOperation::_isEquivalentTo(const util::IComparable *other, + util::IComparable::Criterion criterion, + const io::DatabaseContextPtr &dbContext, + bool inOtherDirection) const { + + auto otherSO = dynamic_cast(other); + if (otherSO == nullptr || + (criterion == util::IComparable::Criterion::STRICT && + !ObjectUsage::_isEquivalentTo(other, criterion, dbContext))) { + return false; + } + + const int methodEPSGCode = d->method_->getEPSGCode(); + const int otherMethodEPSGCode = otherSO->d->method_->getEPSGCode(); + + bool equivalentMethods = + (criterion == util::IComparable::Criterion::EQUIVALENT && + methodEPSGCode != 0 && methodEPSGCode == otherMethodEPSGCode) || + d->method_->_isEquivalentTo(otherSO->d->method_.get(), criterion, + dbContext); + if (!equivalentMethods && + criterion == util::IComparable::Criterion::EQUIVALENT) { + if ((methodEPSGCode == EPSG_CODE_METHOD_LAMBERT_AZIMUTHAL_EQUAL_AREA && + otherMethodEPSGCode == + EPSG_CODE_METHOD_LAMBERT_AZIMUTHAL_EQUAL_AREA_SPHERICAL) || + (otherMethodEPSGCode == + EPSG_CODE_METHOD_LAMBERT_AZIMUTHAL_EQUAL_AREA && + methodEPSGCode == + EPSG_CODE_METHOD_LAMBERT_AZIMUTHAL_EQUAL_AREA_SPHERICAL) || + (methodEPSGCode == + EPSG_CODE_METHOD_LAMBERT_CYLINDRICAL_EQUAL_AREA && + otherMethodEPSGCode == + EPSG_CODE_METHOD_LAMBERT_CYLINDRICAL_EQUAL_AREA_SPHERICAL) || + (otherMethodEPSGCode == + EPSG_CODE_METHOD_LAMBERT_CYLINDRICAL_EQUAL_AREA && + methodEPSGCode == + EPSG_CODE_METHOD_LAMBERT_CYLINDRICAL_EQUAL_AREA_SPHERICAL) || + (methodEPSGCode == EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL && + otherMethodEPSGCode == + EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL_SPHERICAL) || + (otherMethodEPSGCode == EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL && + methodEPSGCode == + EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL_SPHERICAL)) { + auto geodCRS = + dynamic_cast(sourceCRS().get()); + auto otherGeodCRS = dynamic_cast( + otherSO->sourceCRS().get()); + if (geodCRS && otherGeodCRS && geodCRS->ellipsoid()->isSphere() && + otherGeodCRS->ellipsoid()->isSphere()) { + equivalentMethods = true; + } + } + } + + if (!equivalentMethods) { + if (criterion == util::IComparable::Criterion::EQUIVALENT) { + + const auto isTOWGS84Transf = [](int code) { + return code == + EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOCENTRIC || + code == EPSG_CODE_METHOD_POSITION_VECTOR_GEOCENTRIC || + code == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC || + code == + EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_2D || + code == EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_2D || + code == + EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D || + code == + EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_3D || + code == EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_3D || + code == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D; + }; + + // Translation vs (PV or CF) + // or different PV vs CF convention + if (isTOWGS84Transf(methodEPSGCode) && + isTOWGS84Transf(otherMethodEPSGCode)) { + auto transf = static_cast(this); + auto otherTransf = static_cast(otherSO); + auto params = transf->getTOWGS84Parameters(); + auto otherParams = otherTransf->getTOWGS84Parameters(); + assert(params.size() == 7); + assert(otherParams.size() == 7); + for (size_t i = 0; i < 7; i++) { + if (std::fabs(params[i] - otherParams[i]) > + 1e-10 * std::fabs(params[i])) { + return false; + } + } + return true; + } + + // _1SP methods can sometimes be equivalent to _2SP ones + // Check it by using convertToOtherMethod() + if (methodEPSGCode == + EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP && + otherMethodEPSGCode == + EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP) { + // Convert from 2SP to 1SP as the other direction has more + // degree of liberties. + return otherSO->_isEquivalentTo(this, criterion, dbContext); + } else if ((methodEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_A && + otherMethodEPSGCode == + EPSG_CODE_METHOD_MERCATOR_VARIANT_B) || + (methodEPSGCode == EPSG_CODE_METHOD_MERCATOR_VARIANT_B && + otherMethodEPSGCode == + EPSG_CODE_METHOD_MERCATOR_VARIANT_A) || + (methodEPSGCode == + EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP && + otherMethodEPSGCode == + EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP)) { + auto conv = dynamic_cast(this); + if (conv) { + auto eqConv = + conv->convertToOtherMethod(otherMethodEPSGCode); + if (eqConv) { + return eqConv->_isEquivalentTo(other, criterion, + dbContext); + } + } + } + } + + return false; + } + + const auto &values = d->parameterValues_; + const auto &otherValues = otherSO->d->parameterValues_; + const auto valuesSize = values.size(); + const auto otherValuesSize = otherValues.size(); + if (criterion == util::IComparable::Criterion::STRICT) { + if (valuesSize != otherValuesSize) { + return false; + } + for (size_t i = 0; i < valuesSize; i++) { + if (!values[i]->_isEquivalentTo(otherValues[i].get(), criterion, + dbContext)) { + return false; + } + } + return true; + } + + std::vector candidateIndices(otherValuesSize, true); + bool equivalent = true; + bool foundMissingArgs = valuesSize != otherValuesSize; + + for (size_t i = 0; equivalent && i < valuesSize; i++) { + auto opParamvalue = + dynamic_cast(values[i].get()); + if (!opParamvalue) + return false; + + equivalent = false; + bool sameNameDifferentValue = false; + for (size_t j = 0; j < otherValuesSize; j++) { + if (candidateIndices[j] && + values[i]->_isEquivalentTo(otherValues[j].get(), criterion, + dbContext)) { + candidateIndices[j] = false; + equivalent = true; + break; + } else if (candidateIndices[j]) { + auto otherOpParamvalue = + dynamic_cast( + otherValues[j].get()); + if (!otherOpParamvalue) + return false; + sameNameDifferentValue = + opParamvalue->parameter()->_isEquivalentTo( + otherOpParamvalue->parameter().get(), criterion, + dbContext); + if (sameNameDifferentValue) { + candidateIndices[j] = false; + break; + } + } + } + + if (!equivalent && + methodEPSGCode == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP) { + // For LCC_2SP, the standard parallels can be switched and + // this will result in the same result. + const int paramEPSGCode = opParamvalue->parameter()->getEPSGCode(); + if (paramEPSGCode == + EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL || + paramEPSGCode == + EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL) { + auto value_1st = parameterValue( + EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL); + auto value_2nd = parameterValue( + EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL); + if (value_1st && value_2nd) { + equivalent = + value_1st->_isEquivalentTo( + otherSO + ->parameterValue( + EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL) + .get(), + criterion, dbContext) && + value_2nd->_isEquivalentTo( + otherSO + ->parameterValue( + EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL) + .get(), + criterion, dbContext); + } + } + } + + if (equivalent) { + continue; + } + + if (sameNameDifferentValue) { + break; + } + + // If there are parameters in this method not found in the other one, + // check that they are set to a default neutral value, that is 1 + // for scale, and 0 otherwise. + foundMissingArgs = true; + const auto &value = opParamvalue->parameterValue(); + if (value->type() != ParameterValue::Type::MEASURE) { + break; + } + if (value->value().unit().type() == + common::UnitOfMeasure::Type::SCALE) { + equivalent = value->value().getSIValue() == 1.0; + } else { + equivalent = value->value().getSIValue() == 0.0; + } + } + + // In the case the arguments don't perfectly match, try the reverse + // check. + if (equivalent && foundMissingArgs && !inOtherDirection) { + return otherSO->_isEquivalentTo(this, criterion, dbContext, true); + } + + // Equivalent formulations of 2SP can have different parameters + // Then convert to 1SP and compare. + if (!equivalent && + methodEPSGCode == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP) { + auto conv = dynamic_cast(this); + auto otherConv = dynamic_cast(other); + if (conv && otherConv) { + auto thisAs1SP = conv->convertToOtherMethod( + EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP); + auto otherAs1SP = otherConv->convertToOtherMethod( + EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP); + if (thisAs1SP && otherAs1SP) { + equivalent = thisAs1SP->_isEquivalentTo(otherAs1SP.get(), + criterion, dbContext); + } + } + } + return equivalent; +} +//! @endcond + +// --------------------------------------------------------------------------- + +std::set +SingleOperation::gridsNeeded(const io::DatabaseContextPtr &databaseContext, + bool considerKnownGridsAsAvailable) const { + std::set res; + for (const auto &genOpParamvalue : parameterValues()) { + auto opParamvalue = dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue) { + const auto &value = opParamvalue->parameterValue(); + if (value->type() == ParameterValue::Type::FILENAME) { + const auto gridNames = split(value->valueFile(), ","); + for (const auto &gridName : gridNames) { + GridDescription desc; + desc.shortName = gridName; + if (databaseContext) { + databaseContext->lookForGridInfo( + desc.shortName, considerKnownGridsAsAvailable, + desc.fullName, desc.packageName, desc.url, + desc.directDownload, desc.openLicense, + desc.available); + } + res.insert(desc); + } + } + } + } + return res; +} + +// --------------------------------------------------------------------------- + +/** \brief Validate the parameters used by a coordinate operation. + * + * Return whether the method is known or not, or a list of missing or extra + * parameters for the operations recognized by this implementation. + */ +std::list SingleOperation::validateParameters() const { + std::list res; + + const auto &l_method = method(); + const auto &methodName = l_method->nameStr(); + const MethodMapping *methodMapping = nullptr; + const auto methodEPSGCode = l_method->getEPSGCode(); + size_t nProjectionMethodMappings = 0; + const auto projectionMethodMappings = + getProjectionMethodMappings(nProjectionMethodMappings); + for (size_t i = 0; i < nProjectionMethodMappings; ++i) { + const auto &mapping = projectionMethodMappings[i]; + if (metadata::Identifier::isEquivalentName(mapping.wkt2_name, + methodName.c_str()) || + (methodEPSGCode != 0 && methodEPSGCode == mapping.epsg_code)) { + methodMapping = &mapping; + } + } + if (methodMapping == nullptr) { + size_t nOtherMethodMappings = 0; + const auto otherMethodMappings = + getOtherMethodMappings(nOtherMethodMappings); + for (size_t i = 0; i < nOtherMethodMappings; ++i) { + const auto &mapping = otherMethodMappings[i]; + if (metadata::Identifier::isEquivalentName(mapping.wkt2_name, + methodName.c_str()) || + (methodEPSGCode != 0 && methodEPSGCode == mapping.epsg_code)) { + methodMapping = &mapping; + } + } + } + if (!methodMapping) { + res.emplace_back("Unknown method " + methodName); + return res; + } + if (methodMapping->wkt2_name != methodName) { + if (metadata::Identifier::isEquivalentName(methodMapping->wkt2_name, + methodName.c_str())) { + std::string msg("Method name "); + msg += methodName; + msg += " is equivalent to official "; + msg += methodMapping->wkt2_name; + msg += " but not strictly equal"; + res.emplace_back(msg); + } else { + std::string msg("Method name "); + msg += methodName; + msg += ", matched to "; + msg += methodMapping->wkt2_name; + msg += ", through its EPSG code has not an equivalent name"; + res.emplace_back(msg); + } + } + if (methodEPSGCode != 0 && methodEPSGCode != methodMapping->epsg_code) { + std::string msg("Method of EPSG code "); + msg += toString(methodEPSGCode); + msg += " does not match official code ("; + msg += toString(methodMapping->epsg_code); + msg += ')'; + res.emplace_back(msg); + } + + // Check if expected parameters are found + for (int i = 0; + methodMapping->params && methodMapping->params[i] != nullptr; ++i) { + const auto *paramMapping = methodMapping->params[i]; + + const OperationParameterValue *opv = nullptr; + for (const auto &genOpParamvalue : parameterValues()) { + auto opParamvalue = dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue) { + const auto ¶meter = opParamvalue->parameter(); + if ((paramMapping->epsg_code != 0 && + parameter->getEPSGCode() == paramMapping->epsg_code) || + ci_equal(parameter->nameStr(), paramMapping->wkt2_name)) { + opv = opParamvalue; + break; + } + } + } + + if (!opv) { + if ((methodEPSGCode == EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL || + methodEPSGCode == + EPSG_CODE_METHOD_EQUIDISTANT_CYLINDRICAL_SPHERICAL) && + paramMapping == ¶mLatitudeNatOrigin) { + // extension of EPSG used by GDAL/PROJ, so we should not + // warn on its absence. + continue; + } + std::string msg("Cannot find expected parameter "); + msg += paramMapping->wkt2_name; + res.emplace_back(msg); + continue; + } + const auto ¶meter = opv->parameter(); + if (paramMapping->wkt2_name != parameter->nameStr()) { + if (ci_equal(parameter->nameStr(), paramMapping->wkt2_name)) { + std::string msg("Parameter name "); + msg += parameter->nameStr(); + msg += " is equivalent to official "; + msg += paramMapping->wkt2_name; + msg += " but not strictly equal"; + res.emplace_back(msg); + } else { + std::string msg("Parameter name "); + msg += parameter->nameStr(); + msg += ", matched to "; + msg += paramMapping->wkt2_name; + msg += ", through its EPSG code has not an equivalent name"; + res.emplace_back(msg); + } + } + const auto paramEPSGCode = parameter->getEPSGCode(); + if (paramEPSGCode != 0 && paramEPSGCode != paramMapping->epsg_code) { + std::string msg("Parameter of EPSG code "); + msg += toString(paramEPSGCode); + msg += " does not match official code ("; + msg += toString(paramMapping->epsg_code); + msg += ')'; + res.emplace_back(msg); + } + } + + // Check if there are extra parameters + for (const auto &genOpParamvalue : parameterValues()) { + auto opParamvalue = dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue) { + const auto ¶meter = opParamvalue->parameter(); + if (!getMapping(methodMapping, parameter)) { + std::string msg("Parameter "); + msg += parameter->nameStr(); + msg += " found but not expected for this method"; + res.emplace_back(msg); + } + } + } + + return res; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct ParameterValue::Private { + ParameterValue::Type type_{ParameterValue::Type::STRING}; + std::unique_ptr measure_{}; + std::unique_ptr stringValue_{}; + int integerValue_{}; + bool booleanValue_{}; + + explicit Private(const common::Measure &valueIn) + : type_(ParameterValue::Type::MEASURE), + measure_(internal::make_unique(valueIn)) {} + + Private(const std::string &stringValueIn, ParameterValue::Type typeIn) + : type_(typeIn), + stringValue_(internal::make_unique(stringValueIn)) {} + + explicit Private(int integerValueIn) + : type_(ParameterValue::Type::INTEGER), integerValue_(integerValueIn) {} + + explicit Private(bool booleanValueIn) + : type_(ParameterValue::Type::BOOLEAN), booleanValue_(booleanValueIn) {} +}; +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +ParameterValue::~ParameterValue() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +ParameterValue::ParameterValue(const common::Measure &measureIn) + : d(internal::make_unique(measureIn)) {} + +// --------------------------------------------------------------------------- + +ParameterValue::ParameterValue(const std::string &stringValueIn, + ParameterValue::Type typeIn) + : d(internal::make_unique(stringValueIn, typeIn)) {} + +// --------------------------------------------------------------------------- + +ParameterValue::ParameterValue(int integerValueIn) + : d(internal::make_unique(integerValueIn)) {} + +// --------------------------------------------------------------------------- + +ParameterValue::ParameterValue(bool booleanValueIn) + : d(internal::make_unique(booleanValueIn)) {} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a ParameterValue from a Measure (i.e. a value associated + * with a + * unit) + * + * @return a new ParameterValue. + */ +ParameterValueNNPtr ParameterValue::create(const common::Measure &measureIn) { + return ParameterValue::nn_make_shared(measureIn); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a ParameterValue from a string value. + * + * @return a new ParameterValue. + */ +ParameterValueNNPtr ParameterValue::create(const char *stringValueIn) { + return ParameterValue::nn_make_shared( + std::string(stringValueIn), ParameterValue::Type::STRING); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a ParameterValue from a string value. + * + * @return a new ParameterValue. + */ +ParameterValueNNPtr ParameterValue::create(const std::string &stringValueIn) { + return ParameterValue::nn_make_shared( + stringValueIn, ParameterValue::Type::STRING); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a ParameterValue from a filename. + * + * @return a new ParameterValue. + */ +ParameterValueNNPtr +ParameterValue::createFilename(const std::string &stringValueIn) { + return ParameterValue::nn_make_shared( + stringValueIn, ParameterValue::Type::FILENAME); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a ParameterValue from a integer value. + * + * @return a new ParameterValue. + */ +ParameterValueNNPtr ParameterValue::create(int integerValueIn) { + return ParameterValue::nn_make_shared(integerValueIn); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a ParameterValue from a boolean value. + * + * @return a new ParameterValue. + */ +ParameterValueNNPtr ParameterValue::create(bool booleanValueIn) { + return ParameterValue::nn_make_shared(booleanValueIn); +} + +// --------------------------------------------------------------------------- + +/** \brief Returns the type of a parameter value. + * + * @return the type. + */ +const ParameterValue::Type &ParameterValue::type() PROJ_PURE_DEFN { + return d->type_; +} + +// --------------------------------------------------------------------------- + +/** \brief Returns the value as a Measure (assumes type() == Type::MEASURE) + * @return the value as a Measure. + */ +const common::Measure &ParameterValue::value() PROJ_PURE_DEFN { + return *d->measure_; +} + +// --------------------------------------------------------------------------- + +/** \brief Returns the value as a string (assumes type() == Type::STRING) + * @return the value as a string. + */ +const std::string &ParameterValue::stringValue() PROJ_PURE_DEFN { + return *d->stringValue_; +} + +// --------------------------------------------------------------------------- + +/** \brief Returns the value as a filename (assumes type() == Type::FILENAME) + * @return the value as a filename. + */ +const std::string &ParameterValue::valueFile() PROJ_PURE_DEFN { + return *d->stringValue_; +} + +// --------------------------------------------------------------------------- + +/** \brief Returns the value as a integer (assumes type() == Type::INTEGER) + * @return the value as a integer. + */ +int ParameterValue::integerValue() PROJ_PURE_DEFN { return d->integerValue_; } + +// --------------------------------------------------------------------------- + +/** \brief Returns the value as a boolean (assumes type() == Type::BOOLEAN) + * @return the value as a boolean. + */ +bool ParameterValue::booleanValue() PROJ_PURE_DEFN { return d->booleanValue_; } + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void ParameterValue::_exportToWKT(io::WKTFormatter *formatter) const { + const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; + + const auto &l_type = type(); + if (l_type == Type::MEASURE) { + const auto &l_value = value(); + if (formatter->abridgedTransformation()) { + const auto &unit = l_value.unit(); + const auto &unitType = unit.type(); + if (unitType == common::UnitOfMeasure::Type::LINEAR) { + formatter->add(l_value.getSIValue()); + } else if (unitType == common::UnitOfMeasure::Type::ANGULAR) { + formatter->add( + l_value.convertToUnit(common::UnitOfMeasure::ARC_SECOND)); + } else if (unit == common::UnitOfMeasure::PARTS_PER_MILLION) { + formatter->add(1.0 + l_value.value() * 1e-6); + } else { + formatter->add(l_value.value()); + } + } else { + const auto &unit = l_value.unit(); + if (isWKT2) { + formatter->add(l_value.value()); + } else { + // In WKT1, as we don't output the natural unit, output to the + // registered linear / angular unit. + const auto &unitType = unit.type(); + if (unitType == common::UnitOfMeasure::Type::LINEAR) { + const auto &targetUnit = *(formatter->axisLinearUnit()); + if (targetUnit.conversionToSI() == 0.0) { + throw io::FormattingException( + "cannot convert value to target linear unit"); + } + formatter->add(l_value.convertToUnit(targetUnit)); + } else if (unitType == common::UnitOfMeasure::Type::ANGULAR) { + const auto &targetUnit = *(formatter->axisAngularUnit()); + if (targetUnit.conversionToSI() == 0.0) { + throw io::FormattingException( + "cannot convert value to target angular unit"); + } + formatter->add(l_value.convertToUnit(targetUnit)); + } else { + formatter->add(l_value.getSIValue()); + } + } + if (isWKT2 && unit != common::UnitOfMeasure::NONE) { + if (!formatter + ->primeMeridianOrParameterUnitOmittedIfSameAsAxis() || + (unit != common::UnitOfMeasure::SCALE_UNITY && + unit != *(formatter->axisLinearUnit()) && + unit != *(formatter->axisAngularUnit()))) { + unit._exportToWKT(formatter); + } + } + } + } else if (l_type == Type::STRING || l_type == Type::FILENAME) { + formatter->addQuotedString(stringValue()); + } else if (l_type == Type::INTEGER) { + formatter->add(integerValue()); + } else { + throw io::FormattingException("boolean parameter value not handled"); + } +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +bool ParameterValue::_isEquivalentTo(const util::IComparable *other, + util::IComparable::Criterion criterion, + const io::DatabaseContextPtr &) const { + auto otherPV = dynamic_cast(other); + if (otherPV == nullptr) { + return false; + } + if (type() != otherPV->type()) { + return false; + } + switch (type()) { + case Type::MEASURE: { + return value()._isEquivalentTo(otherPV->value(), criterion, 2e-10); + } + + case Type::STRING: + case Type::FILENAME: { + return stringValue() == otherPV->stringValue(); + } + + case Type::INTEGER: { + return integerValue() == otherPV->integerValue(); + } + + case Type::BOOLEAN: { + return booleanValue() == otherPV->booleanValue(); + } + + default: { + assert(false); + break; + } + } + return true; +} +//! @endcond + +//! @cond Doxygen_Suppress +// --------------------------------------------------------------------------- + +InvalidOperation::InvalidOperation(const char *message) : Exception(message) {} + +// --------------------------------------------------------------------------- + +InvalidOperation::InvalidOperation(const std::string &message) + : Exception(message) {} + +// --------------------------------------------------------------------------- + +InvalidOperation::InvalidOperation(const InvalidOperation &) = default; + +// --------------------------------------------------------------------------- + +InvalidOperation::~InvalidOperation() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +void SingleOperation::exportTransformationToWKT( + io::WKTFormatter *formatter) const { + const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2; + if (!isWKT2) { + throw io::FormattingException( + "Transformation can only be exported to WKT2"); + } + + if (formatter->abridgedTransformation()) { + formatter->startNode(io::WKTConstants::ABRIDGEDTRANSFORMATION, + !identifiers().empty()); + } else { + formatter->startNode(io::WKTConstants::COORDINATEOPERATION, + !identifiers().empty()); + } + + formatter->addQuotedString(nameStr()); + + if (formatter->use2019Keywords()) { + const auto &version = operationVersion(); + if (version.has_value()) { + formatter->startNode(io::WKTConstants::VERSION, false); + formatter->addQuotedString(*version); + formatter->endNode(); + } + } + + if (!formatter->abridgedTransformation()) { + exportSourceCRSAndTargetCRSToWKT(this, formatter); + } + + method()->_exportToWKT(formatter); + + for (const auto ¶mValue : parameterValues()) { + paramValue->_exportToWKT(formatter, nullptr); + } + + if (!formatter->abridgedTransformation()) { + if (interpolationCRS()) { + formatter->startNode(io::WKTConstants::INTERPOLATIONCRS, false); + interpolationCRS()->_exportToWKT(formatter); + formatter->endNode(); + } + + if (!coordinateOperationAccuracies().empty()) { + formatter->startNode(io::WKTConstants::OPERATIONACCURACY, false); + formatter->add(coordinateOperationAccuracies()[0]->value()); + formatter->endNode(); + } + } + + ObjectUsage::baseExportToWKT(formatter); + formatter->endNode(); +} + +// --------------------------------------------------------------------------- + +bool SingleOperation::exportToPROJStringGeneric( + io::PROJStringFormatter *formatter) const { + const int methodEPSGCode = method()->getEPSGCode(); + + if (methodEPSGCode == EPSG_CODE_METHOD_AFFINE_PARAMETRIC_TRANSFORMATION) { + const double A0 = parameterValueMeasure(EPSG_CODE_PARAMETER_A0).value(); + const double A1 = parameterValueMeasure(EPSG_CODE_PARAMETER_A1).value(); + const double A2 = parameterValueMeasure(EPSG_CODE_PARAMETER_A2).value(); + const double B0 = parameterValueMeasure(EPSG_CODE_PARAMETER_B0).value(); + const double B1 = parameterValueMeasure(EPSG_CODE_PARAMETER_B1).value(); + const double B2 = parameterValueMeasure(EPSG_CODE_PARAMETER_B2).value(); + + // Do not mess with axis unit and order for that transformation + + formatter->addStep("affine"); + formatter->addParam("xoff", A0); + formatter->addParam("s11", A1); + formatter->addParam("s12", A2); + formatter->addParam("yoff", B0); + formatter->addParam("s21", B1); + formatter->addParam("s22", B2); + + return true; + } + + if (isAxisOrderReversal(methodEPSGCode)) { + formatter->addStep("axisswap"); + formatter->addParam("order", "2,1"); + auto sourceCRSGeog = + dynamic_cast(sourceCRS().get()); + auto targetCRSGeog = + dynamic_cast(targetCRS().get()); + if (sourceCRSGeog && targetCRSGeog) { + const auto &unitSrc = + sourceCRSGeog->coordinateSystem()->axisList()[0]->unit(); + const auto &unitDst = + targetCRSGeog->coordinateSystem()->axisList()[0]->unit(); + if (!unitSrc._isEquivalentTo( + unitDst, util::IComparable::Criterion::EQUIVALENT)) { + formatter->addStep("unitconvert"); + auto projUnit = unitSrc.exportToPROJString(); + if (projUnit.empty()) { + formatter->addParam("xy_in", unitSrc.conversionToSI()); + } else { + formatter->addParam("xy_in", projUnit); + } + projUnit = unitDst.exportToPROJString(); + if (projUnit.empty()) { + formatter->addParam("xy_out", unitDst.conversionToSI()); + } else { + formatter->addParam("xy_out", projUnit); + } + } + } + return true; + } + + if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC_GEOCENTRIC) { + + auto sourceCRSGeod = + dynamic_cast(sourceCRS().get()); + auto targetCRSGeod = + dynamic_cast(targetCRS().get()); + if (sourceCRSGeod && targetCRSGeod) { + auto sourceCRSGeog = + dynamic_cast(sourceCRSGeod); + auto targetCRSGeog = + dynamic_cast(targetCRSGeod); + bool isSrcGeocentric = sourceCRSGeod->isGeocentric(); + bool isSrcGeographic = sourceCRSGeog != nullptr; + bool isTargetGeocentric = targetCRSGeod->isGeocentric(); + bool isTargetGeographic = targetCRSGeog != nullptr; + if ((isSrcGeocentric && isTargetGeographic) || + (isSrcGeographic && isTargetGeocentric)) { + + formatter->startInversion(); + sourceCRSGeod->_exportToPROJString(formatter); + formatter->stopInversion(); + + targetCRSGeod->_exportToPROJString(formatter); + + return true; + } + } + + throw io::FormattingException("Invalid nature of source and/or " + "targetCRS for Geographic/Geocentric " + "conversion"); + } + + if (methodEPSGCode == EPSG_CODE_METHOD_CHANGE_VERTICAL_UNIT) { + double convFactor = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR); + auto uom = common::UnitOfMeasure(std::string(), convFactor, + common::UnitOfMeasure::Type::LINEAR) + .exportToPROJString(); + auto reverse_uom = + common::UnitOfMeasure(std::string(), 1.0 / convFactor, + common::UnitOfMeasure::Type::LINEAR) + .exportToPROJString(); + if (uom == "m") { + // do nothing + } else if (!uom.empty()) { + formatter->addStep("unitconvert"); + formatter->addParam("z_in", uom); + formatter->addParam("z_out", "m"); + } else if (!reverse_uom.empty()) { + formatter->addStep("unitconvert"); + formatter->addParam("z_in", "m"); + formatter->addParam("z_out", reverse_uom); + } else { + formatter->addStep("affine"); + formatter->addParam("s33", convFactor); + } + return true; + } + + if (methodEPSGCode == EPSG_CODE_METHOD_HEIGHT_DEPTH_REVERSAL) { + formatter->addStep("axisswap"); + formatter->addParam("order", "1,2,-3"); + return true; + } + + return false; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +InverseCoordinateOperation::~InverseCoordinateOperation() = default; + +// --------------------------------------------------------------------------- + +InverseCoordinateOperation::InverseCoordinateOperation( + const CoordinateOperationNNPtr &forwardOperationIn, + bool wktSupportsInversion) + : forwardOperation_(forwardOperationIn), + wktSupportsInversion_(wktSupportsInversion) {} + +// --------------------------------------------------------------------------- + +void InverseCoordinateOperation::setPropertiesFromForward() { + setProperties( + createPropertiesForInverse(forwardOperation_.get(), false, false)); + setAccuracies(forwardOperation_->coordinateOperationAccuracies()); + if (forwardOperation_->sourceCRS() && forwardOperation_->targetCRS()) { + setCRSs(forwardOperation_.get(), true); + } + setHasBallparkTransformation( + forwardOperation_->hasBallparkTransformation()); +} + +// --------------------------------------------------------------------------- + +CoordinateOperationNNPtr InverseCoordinateOperation::inverse() const { + return forwardOperation_; +} + +// --------------------------------------------------------------------------- + +void InverseCoordinateOperation::_exportToPROJString( + io::PROJStringFormatter *formatter) const { + formatter->startInversion(); + forwardOperation_->_exportToPROJString(formatter); + formatter->stopInversion(); +} + +// --------------------------------------------------------------------------- + +bool InverseCoordinateOperation::_isEquivalentTo( + const util::IComparable *other, util::IComparable::Criterion criterion, + const io::DatabaseContextPtr &dbContext) const { + auto otherICO = dynamic_cast(other); + if (otherICO == nullptr || + !ObjectUsage::_isEquivalentTo(other, criterion, dbContext)) { + return false; + } + return inverse()->_isEquivalentTo(otherICO->inverse().get(), criterion, + dbContext); +} + +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +PointMotionOperation::~PointMotionOperation() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +} // namespace operation + +NS_PROJ_END diff --git a/src/iso19111/operation/transformation.cpp b/src/iso19111/operation/transformation.cpp new file mode 100644 index 0000000000..fec821a15e --- /dev/null +++ b/src/iso19111/operation/transformation.cpp @@ -0,0 +1,3274 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP +#endif + +#include "proj/common.hpp" +#include "proj/coordinateoperation.hpp" +#include "proj/crs.hpp" +#include "proj/io.hpp" +#include "proj/metadata.hpp" +#include "proj/util.hpp" + +#include "proj/internal/internal.hpp" + +#include "coordinateoperation_internal.hpp" +#include "coordinateoperation_private.hpp" +#include "esriparammappings.hpp" +#include "operationmethod_private.hpp" +#include "oputils.hpp" +#include "parammappings.hpp" +#include "vectorofvaluesparams.hpp" + +// PROJ include order is sensitive +// clang-format off +#include "proj.h" +#include "proj_internal.h" // M_PI +// clang-format on +#include "proj_constants.h" + +#include "proj_json_streaming_writer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace NS_PROJ::internal; + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct Transformation::Private { + + TransformationPtr forwardOperation_{}; + + static TransformationNNPtr registerInv(const Transformation *thisIn, + TransformationNNPtr invTransform); +}; +//! @endcond + +// --------------------------------------------------------------------------- + +Transformation::Transformation( + const crs::CRSNNPtr &sourceCRSIn, const crs::CRSNNPtr &targetCRSIn, + const crs::CRSPtr &interpolationCRSIn, const OperationMethodNNPtr &methodIn, + const std::vector &values, + const std::vector &accuracies) + : SingleOperation(methodIn), d(internal::make_unique()) { + setParameterValues(values); + setCRSs(sourceCRSIn, targetCRSIn, interpolationCRSIn); + setAccuracies(accuracies); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +Transformation::~Transformation() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +Transformation::Transformation(const Transformation &other) + : CoordinateOperation(other), SingleOperation(other), + d(internal::make_unique(*other.d)) {} + +// --------------------------------------------------------------------------- + +/** \brief Return the source crs::CRS of the transformation. + * + * @return the source CRS. + */ +const crs::CRSNNPtr &Transformation::sourceCRS() PROJ_PURE_DEFN { + return CoordinateOperation::getPrivate()->strongRef_->sourceCRS_; +} + +// --------------------------------------------------------------------------- + +/** \brief Return the target crs::CRS of the transformation. + * + * @return the target CRS. + */ +const crs::CRSNNPtr &Transformation::targetCRS() PROJ_PURE_DEFN { + return CoordinateOperation::getPrivate()->strongRef_->targetCRS_; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +TransformationNNPtr Transformation::shallowClone() const { + auto transf = Transformation::nn_make_shared(*this); + transf->assignSelf(transf); + transf->setCRSs(this, false); + if (transf->d->forwardOperation_) { + transf->d->forwardOperation_ = + transf->d->forwardOperation_->shallowClone().as_nullable(); + } + return transf; +} + +CoordinateOperationNNPtr Transformation::_shallowClone() const { + return util::nn_static_pointer_cast(shallowClone()); +} + +// --------------------------------------------------------------------------- + +TransformationNNPtr +Transformation::promoteTo3D(const std::string &, + const io::DatabaseContextPtr &dbContext) const { + auto transf = shallowClone(); + transf->setCRSs(sourceCRS()->promoteTo3D(std::string(), dbContext), + targetCRS()->promoteTo3D(std::string(), dbContext), + interpolationCRS()); + return transf; +} + +// --------------------------------------------------------------------------- + +TransformationNNPtr +Transformation::demoteTo2D(const std::string &, + const io::DatabaseContextPtr &dbContext) const { + auto transf = shallowClone(); + transf->setCRSs(sourceCRS()->demoteTo2D(std::string(), dbContext), + targetCRS()->demoteTo2D(std::string(), dbContext), + interpolationCRS()); + return transf; +} + +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +/** \brief Return the TOWGS84 parameters of the transformation. + * + * If this transformation uses Coordinate Frame Rotation, Position Vector + * transformation or Geocentric translations, a vector of 7 double values + * using the Position Vector convention (EPSG:9606) is returned. Those values + * can be used as the value of the WKT1 TOWGS84 parameter or + * PROJ +towgs84 parameter. + * + * @return a vector of 7 values if valid, otherwise a io::FormattingException + * is thrown. + * @throws io::FormattingException + */ +std::vector +Transformation::getTOWGS84Parameters() const // throw(io::FormattingException) +{ + // GDAL WKT1 assumes EPSG:9606 / Position Vector convention + + bool sevenParamsTransform = false; + bool threeParamsTransform = false; + bool invertRotSigns = false; + const auto &l_method = method(); + const auto &methodName = l_method->nameStr(); + const int methodEPSGCode = l_method->getEPSGCode(); + const auto paramCount = parameterValues().size(); + if ((paramCount == 7 && + ci_find(methodName, "Coordinate Frame") != std::string::npos) || + methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC || + methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D || + methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D) { + sevenParamsTransform = true; + invertRotSigns = true; + } else if ((paramCount == 7 && + ci_find(methodName, "Position Vector") != std::string::npos) || + methodEPSGCode == EPSG_CODE_METHOD_POSITION_VECTOR_GEOCENTRIC || + methodEPSGCode == + EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_2D || + methodEPSGCode == + EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_3D) { + sevenParamsTransform = true; + invertRotSigns = false; + } else if ((paramCount == 3 && + ci_find(methodName, "Geocentric translations") != + std::string::npos) || + methodEPSGCode == + EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOCENTRIC || + methodEPSGCode == + EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_2D || + methodEPSGCode == + EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_3D) { + threeParamsTransform = true; + } + + if (threeParamsTransform || sevenParamsTransform) { + std::vector params(7, 0.0); + bool foundX = false; + bool foundY = false; + bool foundZ = false; + bool foundRotX = false; + bool foundRotY = false; + bool foundRotZ = false; + bool foundScale = false; + const double rotSign = invertRotSigns ? -1.0 : 1.0; + + const auto fixNegativeZero = [](double x) { + if (x == 0.0) + return 0.0; + return x; + }; + + for (const auto &genOpParamvalue : parameterValues()) { + auto opParamvalue = dynamic_cast( + genOpParamvalue.get()); + if (opParamvalue) { + const auto ¶meter = opParamvalue->parameter(); + const auto epsg_code = parameter->getEPSGCode(); + const auto &l_parameterValue = opParamvalue->parameterValue(); + if (l_parameterValue->type() == ParameterValue::Type::MEASURE) { + const auto &measure = l_parameterValue->value(); + if (epsg_code == EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION) { + params[0] = measure.getSIValue(); + foundX = true; + } else if (epsg_code == + EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION) { + params[1] = measure.getSIValue(); + foundY = true; + } else if (epsg_code == + EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION) { + params[2] = measure.getSIValue(); + foundZ = true; + } else if (epsg_code == + EPSG_CODE_PARAMETER_X_AXIS_ROTATION) { + params[3] = fixNegativeZero( + rotSign * + measure.convertToUnit( + common::UnitOfMeasure::ARC_SECOND)); + foundRotX = true; + } else if (epsg_code == + EPSG_CODE_PARAMETER_Y_AXIS_ROTATION) { + params[4] = fixNegativeZero( + rotSign * + measure.convertToUnit( + common::UnitOfMeasure::ARC_SECOND)); + foundRotY = true; + } else if (epsg_code == + EPSG_CODE_PARAMETER_Z_AXIS_ROTATION) { + params[5] = fixNegativeZero( + rotSign * + measure.convertToUnit( + common::UnitOfMeasure::ARC_SECOND)); + foundRotZ = true; + } else if (epsg_code == + EPSG_CODE_PARAMETER_SCALE_DIFFERENCE) { + params[6] = measure.convertToUnit( + common::UnitOfMeasure::PARTS_PER_MILLION); + foundScale = true; + } + } + } + } + if (foundX && foundY && foundZ && + (threeParamsTransform || + (foundRotX && foundRotY && foundRotZ && foundScale))) { + return params; + } else { + throw io::FormattingException( + "Missing required parameter values in transformation"); + } + } + +#if 0 + if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC2D_OFFSETS || + methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC3D_OFFSETS) { + auto offsetLat = + parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_OFFSET); + auto offsetLong = + parameterValueMeasure(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET); + + auto offsetHeight = + parameterValueMeasure(EPSG_CODE_PARAMETER_VERTICAL_OFFSET); + + if (offsetLat.getSIValue() == 0.0 && offsetLong.getSIValue() == 0.0 && + offsetHeight.getSIValue() == 0.0) { + std::vector params(7, 0.0); + return params; + } + } +#endif + + throw io::FormattingException( + "Transformation cannot be formatted as WKT1 TOWGS84 parameters"); +} +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation from a vector of GeneralParameterValue. + * + * @param properties See \ref general_properties. At minimum the name should be + * defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param interpolationCRSIn Interpolation CRS (might be null) + * @param methodIn Operation method. + * @param values Vector of GeneralOperationParameterNNPtr. + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + * @throws InvalidOperation + */ +TransformationNNPtr Transformation::create( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, const crs::CRSPtr &interpolationCRSIn, + const OperationMethodNNPtr &methodIn, + const std::vector &values, + const std::vector &accuracies) { + if (methodIn->parameters().size() != values.size()) { + throw InvalidOperation( + "Inconsistent number of parameters and parameter values"); + } + auto transf = Transformation::nn_make_shared( + sourceCRSIn, targetCRSIn, interpolationCRSIn, methodIn, values, + accuracies); + transf->assignSelf(transf); + transf->setProperties(properties); + std::string name; + if (properties.getStringValue(common::IdentifiedObject::NAME_KEY, name) && + ci_find(name, "ballpark") != std::string::npos) { + transf->setHasBallparkTransformation(true); + } + return transf; +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation ands its OperationMethod. + * + * @param propertiesTransformation The \ref general_properties of the + * Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param interpolationCRSIn Interpolation CRS (might be null) + * @param propertiesOperationMethod The \ref general_properties of the + * OperationMethod. + * At minimum the name should be defined. + * @param parameters Vector of parameters of the operation method. + * @param values Vector of ParameterValueNNPtr. Constraint: + * values.size() == parameters.size() + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + * @throws InvalidOperation + */ +TransformationNNPtr +Transformation::create(const util::PropertyMap &propertiesTransformation, + const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, + const crs::CRSPtr &interpolationCRSIn, + const util::PropertyMap &propertiesOperationMethod, + const std::vector ¶meters, + const std::vector &values, + const std::vector + &accuracies) // throw InvalidOperation +{ + OperationMethodNNPtr op( + OperationMethod::create(propertiesOperationMethod, parameters)); + + if (parameters.size() != values.size()) { + throw InvalidOperation( + "Inconsistent number of parameters and parameter values"); + } + std::vector generalParameterValues; + generalParameterValues.reserve(values.size()); + for (size_t i = 0; i < values.size(); i++) { + generalParameterValues.push_back( + OperationParameterValue::create(parameters[i], values[i])); + } + return create(propertiesTransformation, sourceCRSIn, targetCRSIn, + interpolationCRSIn, op, generalParameterValues, accuracies); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +// --------------------------------------------------------------------------- + +static TransformationNNPtr createSevenParamsTransform( + const util::PropertyMap &properties, + const util::PropertyMap &methodProperties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, double translationXMetre, + double translationYMetre, double translationZMetre, + double rotationXArcSecond, double rotationYArcSecond, + double rotationZArcSecond, double scaleDifferencePPM, + const std::vector &accuracies) { + return Transformation::create( + properties, sourceCRSIn, targetCRSIn, nullptr, methodProperties, + VectorOfParameters{ + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_X_AXIS_ROTATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Y_AXIS_ROTATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Z_AXIS_ROTATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_SCALE_DIFFERENCE), + }, + createParams(common::Length(translationXMetre), + common::Length(translationYMetre), + common::Length(translationZMetre), + common::Angle(rotationXArcSecond, + common::UnitOfMeasure::ARC_SECOND), + common::Angle(rotationYArcSecond, + common::UnitOfMeasure::ARC_SECOND), + common::Angle(rotationZArcSecond, + common::UnitOfMeasure::ARC_SECOND), + common::Scale(scaleDifferencePPM, + common::UnitOfMeasure::PARTS_PER_MILLION)), + accuracies); +} + +// --------------------------------------------------------------------------- + +static void getTransformationType(const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, + bool &isGeocentric, bool &isGeog2D, + bool &isGeog3D) { + auto sourceCRSGeod = + dynamic_cast(sourceCRSIn.get()); + auto targetCRSGeod = + dynamic_cast(targetCRSIn.get()); + isGeocentric = sourceCRSGeod && sourceCRSGeod->isGeocentric() && + targetCRSGeod && targetCRSGeod->isGeocentric(); + if (isGeocentric) { + isGeog2D = false; + isGeog3D = false; + return; + } + isGeocentric = false; + + auto sourceCRSGeog = + dynamic_cast(sourceCRSIn.get()); + auto targetCRSGeog = + dynamic_cast(targetCRSIn.get()); + if (!sourceCRSGeog || !targetCRSGeog) { + throw InvalidOperation("Inconsistent CRS type"); + } + const auto nSrcAxisCount = + sourceCRSGeog->coordinateSystem()->axisList().size(); + const auto nTargetAxisCount = + targetCRSGeog->coordinateSystem()->axisList().size(); + isGeog2D = nSrcAxisCount == 2 && nTargetAxisCount == 2; + isGeog3D = !isGeog2D && nSrcAxisCount >= 2 && nTargetAxisCount >= 2; +} + +// --------------------------------------------------------------------------- + +static int +useOperationMethodEPSGCodeIfPresent(const util::PropertyMap &properties, + int nDefaultOperationMethodEPSGCode) { + const auto *operationMethodEPSGCode = + properties.get("OPERATION_METHOD_EPSG_CODE"); + if (operationMethodEPSGCode) { + const auto boxedValue = dynamic_cast( + (*operationMethodEPSGCode).get()); + if (boxedValue && + boxedValue->type() == util::BoxedValue::Type::INTEGER) { + return boxedValue->integerValue(); + } + } + return nDefaultOperationMethodEPSGCode; +} +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation with Geocentric Translations method. + * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param translationXMetre Value of the Translation_X parameter (in metre). + * @param translationYMetre Value of the Translation_Y parameter (in metre). + * @param translationZMetre Value of the Translation_Z parameter (in metre). + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + */ +TransformationNNPtr Transformation::createGeocentricTranslations( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, double translationXMetre, + double translationYMetre, double translationZMetre, + const std::vector &accuracies) { + bool isGeocentric; + bool isGeog2D; + bool isGeog3D; + getTransformationType(sourceCRSIn, targetCRSIn, isGeocentric, isGeog2D, + isGeog3D); + return create( + properties, sourceCRSIn, targetCRSIn, nullptr, + createMethodMapNameEPSGCode(useOperationMethodEPSGCodeIfPresent( + properties, + isGeocentric + ? EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOCENTRIC + : isGeog2D + ? EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_2D + : EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_3D)), + VectorOfParameters{ + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION), + }, + createParams(common::Length(translationXMetre), + common::Length(translationYMetre), + common::Length(translationZMetre)), + accuracies); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation with Position vector transformation + * method. + * + * This is similar to createCoordinateFrameRotation(), except that the sign of + * the rotation terms is inverted. + * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param translationXMetre Value of the Translation_X parameter (in metre). + * @param translationYMetre Value of the Translation_Y parameter (in metre). + * @param translationZMetre Value of the Translation_Z parameter (in metre). + * @param rotationXArcSecond Value of the Rotation_X parameter (in + * arc-second). + * @param rotationYArcSecond Value of the Rotation_Y parameter (in + * arc-second). + * @param rotationZArcSecond Value of the Rotation_Z parameter (in + * arc-second). + * @param scaleDifferencePPM Value of the Scale_Difference parameter (in + * parts-per-million). + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + */ +TransformationNNPtr Transformation::createPositionVector( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, double translationXMetre, + double translationYMetre, double translationZMetre, + double rotationXArcSecond, double rotationYArcSecond, + double rotationZArcSecond, double scaleDifferencePPM, + const std::vector &accuracies) { + + bool isGeocentric; + bool isGeog2D; + bool isGeog3D; + getTransformationType(sourceCRSIn, targetCRSIn, isGeocentric, isGeog2D, + isGeog3D); + return createSevenParamsTransform( + properties, + createMethodMapNameEPSGCode(useOperationMethodEPSGCodeIfPresent( + properties, + isGeocentric + ? EPSG_CODE_METHOD_POSITION_VECTOR_GEOCENTRIC + : isGeog2D ? EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_2D + : EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_3D)), + sourceCRSIn, targetCRSIn, translationXMetre, translationYMetre, + translationZMetre, rotationXArcSecond, rotationYArcSecond, + rotationZArcSecond, scaleDifferencePPM, accuracies); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation with Coordinate Frame Rotation method. + * + * This is similar to createPositionVector(), except that the sign of + * the rotation terms is inverted. + * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param translationXMetre Value of the Translation_X parameter (in metre). + * @param translationYMetre Value of the Translation_Y parameter (in metre). + * @param translationZMetre Value of the Translation_Z parameter (in metre). + * @param rotationXArcSecond Value of the Rotation_X parameter (in + * arc-second). + * @param rotationYArcSecond Value of the Rotation_Y parameter (in + * arc-second). + * @param rotationZArcSecond Value of the Rotation_Z parameter (in + * arc-second). + * @param scaleDifferencePPM Value of the Scale_Difference parameter (in + * parts-per-million). + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + */ +TransformationNNPtr Transformation::createCoordinateFrameRotation( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, double translationXMetre, + double translationYMetre, double translationZMetre, + double rotationXArcSecond, double rotationYArcSecond, + double rotationZArcSecond, double scaleDifferencePPM, + const std::vector &accuracies) { + bool isGeocentric; + bool isGeog2D; + bool isGeog3D; + getTransformationType(sourceCRSIn, targetCRSIn, isGeocentric, isGeog2D, + isGeog3D); + return createSevenParamsTransform( + properties, + createMethodMapNameEPSGCode(useOperationMethodEPSGCodeIfPresent( + properties, + isGeocentric + ? EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC + : isGeog2D ? EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D + : EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D)), + sourceCRSIn, targetCRSIn, translationXMetre, translationYMetre, + translationZMetre, rotationXArcSecond, rotationYArcSecond, + rotationZArcSecond, scaleDifferencePPM, accuracies); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static TransformationNNPtr createFifteenParamsTransform( + const util::PropertyMap &properties, + const util::PropertyMap &methodProperties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, double translationXMetre, + double translationYMetre, double translationZMetre, + double rotationXArcSecond, double rotationYArcSecond, + double rotationZArcSecond, double scaleDifferencePPM, + double rateTranslationX, double rateTranslationY, double rateTranslationZ, + double rateRotationX, double rateRotationY, double rateRotationZ, + double rateScaleDifference, double referenceEpochYear, + const std::vector &accuracies) { + return Transformation::create( + properties, sourceCRSIn, targetCRSIn, nullptr, methodProperties, + VectorOfParameters{ + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_X_AXIS_ROTATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Y_AXIS_ROTATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Z_AXIS_ROTATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_SCALE_DIFFERENCE), + + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_RATE_X_AXIS_TRANSLATION), + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_RATE_Y_AXIS_TRANSLATION), + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_RATE_Z_AXIS_TRANSLATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_RATE_X_AXIS_ROTATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_RATE_Y_AXIS_ROTATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_RATE_Z_AXIS_ROTATION), + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_RATE_SCALE_DIFFERENCE), + + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_REFERENCE_EPOCH), + }, + VectorOfValues{ + common::Length(translationXMetre), + common::Length(translationYMetre), + common::Length(translationZMetre), + common::Angle(rotationXArcSecond, + common::UnitOfMeasure::ARC_SECOND), + common::Angle(rotationYArcSecond, + common::UnitOfMeasure::ARC_SECOND), + common::Angle(rotationZArcSecond, + common::UnitOfMeasure::ARC_SECOND), + common::Scale(scaleDifferencePPM, + common::UnitOfMeasure::PARTS_PER_MILLION), + common::Measure(rateTranslationX, + common::UnitOfMeasure::METRE_PER_YEAR), + common::Measure(rateTranslationY, + common::UnitOfMeasure::METRE_PER_YEAR), + common::Measure(rateTranslationZ, + common::UnitOfMeasure::METRE_PER_YEAR), + common::Measure(rateRotationX, + common::UnitOfMeasure::ARC_SECOND_PER_YEAR), + common::Measure(rateRotationY, + common::UnitOfMeasure::ARC_SECOND_PER_YEAR), + common::Measure(rateRotationZ, + common::UnitOfMeasure::ARC_SECOND_PER_YEAR), + common::Measure(rateScaleDifference, + common::UnitOfMeasure::PPM_PER_YEAR), + common::Measure(referenceEpochYear, common::UnitOfMeasure::YEAR), + }, + accuracies); +} +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation with Time Dependent position vector + * transformation method. + * + * This is similar to createTimeDependentCoordinateFrameRotation(), except that + * the sign of + * the rotation terms is inverted. + * + * This method is defined as [EPSG:1053] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1053) + * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param translationXMetre Value of the Translation_X parameter (in metre). + * @param translationYMetre Value of the Translation_Y parameter (in metre). + * @param translationZMetre Value of the Translation_Z parameter (in metre). + * @param rotationXArcSecond Value of the Rotation_X parameter (in + * arc-second). + * @param rotationYArcSecond Value of the Rotation_Y parameter (in + * arc-second). + * @param rotationZArcSecond Value of the Rotation_Z parameter (in + * arc-second). + * @param scaleDifferencePPM Value of the Scale_Difference parameter (in + * parts-per-million). + * @param rateTranslationX Value of the rate of change of X-axis translation (in + * metre/year) + * @param rateTranslationY Value of the rate of change of Y-axis translation (in + * metre/year) + * @param rateTranslationZ Value of the rate of change of Z-axis translation (in + * metre/year) + * @param rateRotationX Value of the rate of change of X-axis rotation (in + * arc-second/year) + * @param rateRotationY Value of the rate of change of Y-axis rotation (in + * arc-second/year) + * @param rateRotationZ Value of the rate of change of Z-axis rotation (in + * arc-second/year) + * @param rateScaleDifference Value of the rate of change of scale difference + * (in PPM/year) + * @param referenceEpochYear Parameter reference epoch (in decimal year) + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + */ +TransformationNNPtr Transformation::createTimeDependentPositionVector( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, double translationXMetre, + double translationYMetre, double translationZMetre, + double rotationXArcSecond, double rotationYArcSecond, + double rotationZArcSecond, double scaleDifferencePPM, + double rateTranslationX, double rateTranslationY, double rateTranslationZ, + double rateRotationX, double rateRotationY, double rateRotationZ, + double rateScaleDifference, double referenceEpochYear, + const std::vector &accuracies) { + bool isGeocentric; + bool isGeog2D; + bool isGeog3D; + getTransformationType(sourceCRSIn, targetCRSIn, isGeocentric, isGeog2D, + isGeog3D); + return createFifteenParamsTransform( + properties, + createMethodMapNameEPSGCode(useOperationMethodEPSGCodeIfPresent( + properties, + isGeocentric + ? EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOCENTRIC + : isGeog2D + ? EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOGRAPHIC_2D + : EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOGRAPHIC_3D)), + sourceCRSIn, targetCRSIn, translationXMetre, translationYMetre, + translationZMetre, rotationXArcSecond, rotationYArcSecond, + rotationZArcSecond, scaleDifferencePPM, rateTranslationX, + rateTranslationY, rateTranslationZ, rateRotationX, rateRotationY, + rateRotationZ, rateScaleDifference, referenceEpochYear, accuracies); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation with Time Dependent Position coordinate + * frame rotation transformation method. + * + * This is similar to createTimeDependentPositionVector(), except that the sign + * of + * the rotation terms is inverted. + * + * This method is defined as [EPSG:1056] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1056) + * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param translationXMetre Value of the Translation_X parameter (in metre). + * @param translationYMetre Value of the Translation_Y parameter (in metre). + * @param translationZMetre Value of the Translation_Z parameter (in metre). + * @param rotationXArcSecond Value of the Rotation_X parameter (in + * arc-second). + * @param rotationYArcSecond Value of the Rotation_Y parameter (in + * arc-second). + * @param rotationZArcSecond Value of the Rotation_Z parameter (in + * arc-second). + * @param scaleDifferencePPM Value of the Scale_Difference parameter (in + * parts-per-million). + * @param rateTranslationX Value of the rate of change of X-axis translation (in + * metre/year) + * @param rateTranslationY Value of the rate of change of Y-axis translation (in + * metre/year) + * @param rateTranslationZ Value of the rate of change of Z-axis translation (in + * metre/year) + * @param rateRotationX Value of the rate of change of X-axis rotation (in + * arc-second/year) + * @param rateRotationY Value of the rate of change of Y-axis rotation (in + * arc-second/year) + * @param rateRotationZ Value of the rate of change of Z-axis rotation (in + * arc-second/year) + * @param rateScaleDifference Value of the rate of change of scale difference + * (in PPM/year) + * @param referenceEpochYear Parameter reference epoch (in decimal year) + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + * @throws InvalidOperation + */ +TransformationNNPtr Transformation::createTimeDependentCoordinateFrameRotation( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, double translationXMetre, + double translationYMetre, double translationZMetre, + double rotationXArcSecond, double rotationYArcSecond, + double rotationZArcSecond, double scaleDifferencePPM, + double rateTranslationX, double rateTranslationY, double rateTranslationZ, + double rateRotationX, double rateRotationY, double rateRotationZ, + double rateScaleDifference, double referenceEpochYear, + const std::vector &accuracies) { + + bool isGeocentric; + bool isGeog2D; + bool isGeog3D; + getTransformationType(sourceCRSIn, targetCRSIn, isGeocentric, isGeog2D, + isGeog3D); + return createFifteenParamsTransform( + properties, + createMethodMapNameEPSGCode(useOperationMethodEPSGCodeIfPresent( + properties, + isGeocentric + ? EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOCENTRIC + : isGeog2D + ? EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOGRAPHIC_2D + : EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOGRAPHIC_3D)), + sourceCRSIn, targetCRSIn, translationXMetre, translationYMetre, + translationZMetre, rotationXArcSecond, rotationYArcSecond, + rotationZArcSecond, scaleDifferencePPM, rateTranslationX, + rateTranslationY, rateTranslationZ, rateRotationX, rateRotationY, + rateRotationZ, rateScaleDifference, referenceEpochYear, accuracies); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static TransformationNNPtr _createMolodensky( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, int methodEPSGCode, + double translationXMetre, double translationYMetre, + double translationZMetre, double semiMajorAxisDifferenceMetre, + double flattingDifference, + const std::vector &accuracies) { + return Transformation::create( + properties, sourceCRSIn, targetCRSIn, nullptr, + createMethodMapNameEPSGCode(methodEPSGCode), + VectorOfParameters{ + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION), + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_SEMI_MAJOR_AXIS_DIFFERENCE), + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_FLATTENING_DIFFERENCE), + }, + createParams( + common::Length(translationXMetre), + common::Length(translationYMetre), + common::Length(translationZMetre), + common::Length(semiMajorAxisDifferenceMetre), + common::Measure(flattingDifference, common::UnitOfMeasure::NONE)), + accuracies); +} +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation with Molodensky method. + * + * @see createAbridgedMolodensky() for a related method. + * + * This method is defined as [EPSG:9604] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9604) + * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param translationXMetre Value of the Translation_X parameter (in metre). + * @param translationYMetre Value of the Translation_Y parameter (in metre). + * @param translationZMetre Value of the Translation_Z parameter (in metre). + * @param semiMajorAxisDifferenceMetre The difference between the semi-major + * axis values of the ellipsoids used in the target and source CRS (in metre). + * @param flattingDifference The difference between the flattening values of + * the ellipsoids used in the target and source CRS. + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + * @throws InvalidOperation + */ +TransformationNNPtr Transformation::createMolodensky( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, double translationXMetre, + double translationYMetre, double translationZMetre, + double semiMajorAxisDifferenceMetre, double flattingDifference, + const std::vector &accuracies) { + return _createMolodensky( + properties, sourceCRSIn, targetCRSIn, EPSG_CODE_METHOD_MOLODENSKY, + translationXMetre, translationYMetre, translationZMetre, + semiMajorAxisDifferenceMetre, flattingDifference, accuracies); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation with Abridged Molodensky method. + * + * @see createdMolodensky() for a related method. + * + * This method is defined as [EPSG:9605] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9605) + * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param translationXMetre Value of the Translation_X parameter (in metre). + * @param translationYMetre Value of the Translation_Y parameter (in metre). + * @param translationZMetre Value of the Translation_Z parameter (in metre). + * @param semiMajorAxisDifferenceMetre The difference between the semi-major + * axis values of the ellipsoids used in the target and source CRS (in metre). + * @param flattingDifference The difference between the flattening values of + * the ellipsoids used in the target and source CRS. + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + * @throws InvalidOperation + */ +TransformationNNPtr Transformation::createAbridgedMolodensky( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, double translationXMetre, + double translationYMetre, double translationZMetre, + double semiMajorAxisDifferenceMetre, double flattingDifference, + const std::vector &accuracies) { + return _createMolodensky(properties, sourceCRSIn, targetCRSIn, + EPSG_CODE_METHOD_ABRIDGED_MOLODENSKY, + translationXMetre, translationYMetre, + translationZMetre, semiMajorAxisDifferenceMetre, + flattingDifference, accuracies); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation from TOWGS84 parameters. + * + * This is a helper of createPositionVector() with the source CRS being the + * GeographicCRS of sourceCRSIn, and the target CRS being EPSG:4326 + * + * @param sourceCRSIn Source CRS. + * @param TOWGS84Parameters The vector of 3 double values (Translation_X,_Y,_Z) + * or 7 double values (Translation_X,_Y,_Z, Rotation_X,_Y,_Z, Scale_Difference) + * passed to createPositionVector() + * @return new Transformation. + * @throws InvalidOperation + */ +TransformationNNPtr Transformation::createTOWGS84( + const crs::CRSNNPtr &sourceCRSIn, + const std::vector &TOWGS84Parameters) // throw InvalidOperation +{ + if (TOWGS84Parameters.size() != 3 && TOWGS84Parameters.size() != 7) { + throw InvalidOperation( + "Invalid number of elements in TOWGS84Parameters"); + } + + crs::CRSPtr transformSourceCRS = sourceCRSIn->extractGeodeticCRS(); + if (!transformSourceCRS) { + throw InvalidOperation( + "Cannot find GeodeticCRS in sourceCRS of TOWGS84 transformation"); + } + + util::PropertyMap properties; + properties.set(common::IdentifiedObject::NAME_KEY, + concat("Transformation from ", transformSourceCRS->nameStr(), + " to WGS84")); + + auto targetCRS = + dynamic_cast(transformSourceCRS.get()) + ? util::nn_static_pointer_cast( + crs::GeographicCRS::EPSG_4326) + : util::nn_static_pointer_cast( + crs::GeodeticCRS::EPSG_4978); + + if (TOWGS84Parameters.size() == 3) { + return createGeocentricTranslations( + properties, NN_NO_CHECK(transformSourceCRS), targetCRS, + TOWGS84Parameters[0], TOWGS84Parameters[1], TOWGS84Parameters[2], + {}); + } + + return createPositionVector(properties, NN_NO_CHECK(transformSourceCRS), + targetCRS, TOWGS84Parameters[0], + TOWGS84Parameters[1], TOWGS84Parameters[2], + TOWGS84Parameters[3], TOWGS84Parameters[4], + TOWGS84Parameters[5], TOWGS84Parameters[6], {}); +} + +// --------------------------------------------------------------------------- +/** \brief Instantiate a transformation with NTv2 method. + * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param filename NTv2 filename. + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + */ +TransformationNNPtr Transformation::createNTv2( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, const std::string &filename, + const std::vector &accuracies) { + + return create(properties, sourceCRSIn, targetCRSIn, nullptr, + createMethodMapNameEPSGCode(EPSG_CODE_METHOD_NTV2), + VectorOfParameters{createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE)}, + VectorOfValues{ParameterValue::createFilename(filename)}, + accuracies); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static TransformationNNPtr _createGravityRelatedHeightToGeographic3D( + const util::PropertyMap &properties, bool inverse, + const crs::CRSNNPtr &sourceCRSIn, const crs::CRSNNPtr &targetCRSIn, + const crs::CRSPtr &interpolationCRSIn, const std::string &filename, + const std::vector &accuracies) { + + return Transformation::create( + properties, sourceCRSIn, targetCRSIn, interpolationCRSIn, + util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, + inverse ? INVERSE_OF + PROJ_WKT2_NAME_METHOD_HEIGHT_TO_GEOG3D + : PROJ_WKT2_NAME_METHOD_HEIGHT_TO_GEOG3D), + VectorOfParameters{createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_GEOID_CORRECTION_FILENAME)}, + VectorOfValues{ParameterValue::createFilename(filename)}, accuracies); +} +//! @endcond + +// --------------------------------------------------------------------------- +/** \brief Instantiate a transformation from GravityRelatedHeight to + * Geographic3D + * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param interpolationCRSIn Interpolation CRS. (might be null) + * @param filename GRID filename. + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + */ +TransformationNNPtr Transformation::createGravityRelatedHeightToGeographic3D( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, const crs::CRSPtr &interpolationCRSIn, + const std::string &filename, + const std::vector &accuracies) { + + return _createGravityRelatedHeightToGeographic3D( + properties, false, sourceCRSIn, targetCRSIn, interpolationCRSIn, + filename, accuracies); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation with method VERTCON + * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param filename GRID filename. + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + */ +TransformationNNPtr Transformation::createVERTCON( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, const std::string &filename, + const std::vector &accuracies) { + + return create(properties, sourceCRSIn, targetCRSIn, nullptr, + createMethodMapNameEPSGCode(EPSG_CODE_METHOD_VERTCON), + VectorOfParameters{createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_VERTICAL_OFFSET_FILE)}, + VectorOfValues{ParameterValue::createFilename(filename)}, + accuracies); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static inline std::vector +buildAccuracyZero() { + return std::vector{ + metadata::PositionalAccuracy::create("0")}; +} + +// --------------------------------------------------------------------------- + +//! @endcond + +/** \brief Instantiate a transformation with method Longitude rotation + * + * This method is defined as [EPSG:9601] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9601) + * * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param offset Longitude offset to add. + * @return new Transformation. + */ +TransformationNNPtr Transformation::createLongitudeRotation( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, const common::Angle &offset) { + + return create( + properties, sourceCRSIn, targetCRSIn, nullptr, + createMethodMapNameEPSGCode(EPSG_CODE_METHOD_LONGITUDE_ROTATION), + VectorOfParameters{ + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET)}, + VectorOfValues{ParameterValue::create(offset)}, buildAccuracyZero()); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +bool Transformation::isLongitudeRotation() const { + return method()->getEPSGCode() == EPSG_CODE_METHOD_LONGITUDE_ROTATION; +} + +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation with method Geographic 2D offsets + * + * This method is defined as [EPSG:9619] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9619) + * * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param offsetLat Latitude offset to add. + * @param offsetLon Longitude offset to add. + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + */ +TransformationNNPtr Transformation::createGeographic2DOffsets( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, const common::Angle &offsetLat, + const common::Angle &offsetLon, + const std::vector &accuracies) { + return create( + properties, sourceCRSIn, targetCRSIn, nullptr, + createMethodMapNameEPSGCode(EPSG_CODE_METHOD_GEOGRAPHIC2D_OFFSETS), + VectorOfParameters{ + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LATITUDE_OFFSET), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET)}, + VectorOfValues{offsetLat, offsetLon}, accuracies); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation with method Geographic 3D offsets + * + * This method is defined as [EPSG:9660] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9660) + * * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param offsetLat Latitude offset to add. + * @param offsetLon Longitude offset to add. + * @param offsetHeight Height offset to add. + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + */ +TransformationNNPtr Transformation::createGeographic3DOffsets( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, const common::Angle &offsetLat, + const common::Angle &offsetLon, const common::Length &offsetHeight, + const std::vector &accuracies) { + return create( + properties, sourceCRSIn, targetCRSIn, nullptr, + createMethodMapNameEPSGCode(EPSG_CODE_METHOD_GEOGRAPHIC3D_OFFSETS), + VectorOfParameters{ + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LATITUDE_OFFSET), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_VERTICAL_OFFSET)}, + VectorOfValues{offsetLat, offsetLon, offsetHeight}, accuracies); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation with method Geographic 2D with + * height + * offsets + * + * This method is defined as [EPSG:9618] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9618) + * * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param offsetLat Latitude offset to add. + * @param offsetLon Longitude offset to add. + * @param offsetHeight Geoid undulation to add. + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + */ +TransformationNNPtr Transformation::createGeographic2DWithHeightOffsets( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, const common::Angle &offsetLat, + const common::Angle &offsetLon, const common::Length &offsetHeight, + const std::vector &accuracies) { + return create( + properties, sourceCRSIn, targetCRSIn, nullptr, + createMethodMapNameEPSGCode( + EPSG_CODE_METHOD_GEOGRAPHIC2D_WITH_HEIGHT_OFFSETS), + VectorOfParameters{ + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LATITUDE_OFFSET), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET), + createOpParamNameEPSGCode(EPSG_CODE_PARAMETER_GEOID_UNDULATION)}, + VectorOfValues{offsetLat, offsetLon, offsetHeight}, accuracies); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation with method Vertical Offset. + * + * This method is defined as [EPSG:9616] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::9616) + * * + * @param properties See \ref general_properties of the Transformation. + * At minimum the name should be defined. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param offsetHeight Geoid undulation to add. + * @param accuracies Vector of positional accuracy (might be empty). + * @return new Transformation. + */ +TransformationNNPtr Transformation::createVerticalOffset( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, const common::Length &offsetHeight, + const std::vector &accuracies) { + return create(properties, sourceCRSIn, targetCRSIn, nullptr, + createMethodMapNameEPSGCode(EPSG_CODE_METHOD_VERTICAL_OFFSET), + VectorOfParameters{createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_VERTICAL_OFFSET)}, + VectorOfValues{offsetHeight}, accuracies); +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a transformation based on the Change of Vertical Unit + * method. + * + * This method is defined as [EPSG:1069] + * (https://www.epsg-registry.org/export.htm?gml=urn:ogc:def:method:EPSG::1069) + * + * @param properties See \ref general_properties of the conversion. If the name + * is not provided, it is automatically set. + * @param sourceCRSIn Source CRS. + * @param targetCRSIn Target CRS. + * @param factor Conversion factor + * @param accuracies Vector of positional accuracy (might be empty). + * @return a new Transformation. + */ +TransformationNNPtr Transformation::createChangeVerticalUnit( + const util::PropertyMap &properties, const crs::CRSNNPtr &sourceCRSIn, + const crs::CRSNNPtr &targetCRSIn, const common::Scale &factor, + const std::vector &accuracies) { + return create( + properties, sourceCRSIn, targetCRSIn, nullptr, + createMethodMapNameEPSGCode(EPSG_CODE_METHOD_CHANGE_VERTICAL_UNIT), + VectorOfParameters{ + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR), + }, + VectorOfValues{ + factor, + }, + accuracies); +} + +// --------------------------------------------------------------------------- + +// to avoid -0... +static double negate(double val) { + if (val != 0) { + return -val; + } + return 0.0; +} + +// --------------------------------------------------------------------------- + +static CoordinateOperationPtr +createApproximateInverseIfPossible(const Transformation *op) { + bool sevenParamsTransform = false; + bool fifteenParamsTransform = false; + const auto &method = op->method(); + const auto &methodName = method->nameStr(); + const int methodEPSGCode = method->getEPSGCode(); + const auto paramCount = op->parameterValues().size(); + const bool isPositionVector = + ci_find(methodName, "Position Vector") != std::string::npos; + const bool isCoordinateFrame = + ci_find(methodName, "Coordinate Frame") != std::string::npos; + + // See end of "2.4.3.3 Helmert 7-parameter transformations" + // in EPSG 7-2 guidance + // For practical purposes, the inverse of 7- or 15-parameters Helmert + // can be obtained by using the forward method with all parameters + // negated + // (except reference epoch!) + // So for WKT export use that. But for PROJ string, we use the +inv flag + // so as to get "perfect" round-tripability. + if ((paramCount == 7 && isCoordinateFrame && + !isTimeDependent(methodName)) || + methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC || + methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D || + methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D) { + sevenParamsTransform = true; + } else if ( + (paramCount == 15 && isCoordinateFrame && + isTimeDependent(methodName)) || + methodEPSGCode == + EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOCENTRIC || + methodEPSGCode == + EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOGRAPHIC_2D || + methodEPSGCode == + EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOGRAPHIC_3D) { + fifteenParamsTransform = true; + } else if ((paramCount == 7 && isPositionVector && + !isTimeDependent(methodName)) || + methodEPSGCode == EPSG_CODE_METHOD_POSITION_VECTOR_GEOCENTRIC || + methodEPSGCode == + EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_2D || + methodEPSGCode == + EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_3D) { + sevenParamsTransform = true; + } else if ( + (paramCount == 15 && isPositionVector && isTimeDependent(methodName)) || + methodEPSGCode == + EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOCENTRIC || + methodEPSGCode == + EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOGRAPHIC_2D || + methodEPSGCode == + EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOGRAPHIC_3D) { + fifteenParamsTransform = true; + } + if (sevenParamsTransform || fifteenParamsTransform) { + double neg_x = negate(op->parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION)); + double neg_y = negate(op->parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION)); + double neg_z = negate(op->parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION)); + double neg_rx = negate( + op->parameterValueNumeric(EPSG_CODE_PARAMETER_X_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND)); + double neg_ry = negate( + op->parameterValueNumeric(EPSG_CODE_PARAMETER_Y_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND)); + double neg_rz = negate( + op->parameterValueNumeric(EPSG_CODE_PARAMETER_Z_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND)); + double neg_scaleDiff = negate(op->parameterValueNumeric( + EPSG_CODE_PARAMETER_SCALE_DIFFERENCE, + common::UnitOfMeasure::PARTS_PER_MILLION)); + auto methodProperties = util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, methodName); + int method_epsg_code = method->getEPSGCode(); + if (method_epsg_code) { + methodProperties + .set(metadata::Identifier::CODESPACE_KEY, + metadata::Identifier::EPSG) + .set(metadata::Identifier::CODE_KEY, method_epsg_code); + } + if (fifteenParamsTransform) { + double neg_rate_x = negate(op->parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_X_AXIS_TRANSLATION, + common::UnitOfMeasure::METRE_PER_YEAR)); + double neg_rate_y = negate(op->parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_Y_AXIS_TRANSLATION, + common::UnitOfMeasure::METRE_PER_YEAR)); + double neg_rate_z = negate(op->parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_Z_AXIS_TRANSLATION, + common::UnitOfMeasure::METRE_PER_YEAR)); + double neg_rate_rx = negate(op->parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_X_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND_PER_YEAR)); + double neg_rate_ry = negate(op->parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_Y_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND_PER_YEAR)); + double neg_rate_rz = negate(op->parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_Z_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND_PER_YEAR)); + double neg_rate_scaleDiff = negate(op->parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_SCALE_DIFFERENCE, + common::UnitOfMeasure::PPM_PER_YEAR)); + double referenceEpochYear = + op->parameterValueNumeric(EPSG_CODE_PARAMETER_REFERENCE_EPOCH, + common::UnitOfMeasure::YEAR); + return util::nn_static_pointer_cast( + createFifteenParamsTransform( + createPropertiesForInverse(op, false, true), + methodProperties, op->targetCRS(), op->sourceCRS(), + neg_x, neg_y, neg_z, neg_rx, neg_ry, neg_rz, + neg_scaleDiff, neg_rate_x, neg_rate_y, neg_rate_z, + neg_rate_rx, neg_rate_ry, neg_rate_rz, + neg_rate_scaleDiff, referenceEpochYear, + op->coordinateOperationAccuracies())) + .as_nullable(); + } else { + return util::nn_static_pointer_cast( + createSevenParamsTransform( + createPropertiesForInverse(op, false, true), + methodProperties, op->targetCRS(), op->sourceCRS(), + neg_x, neg_y, neg_z, neg_rx, neg_ry, neg_rz, + neg_scaleDiff, op->coordinateOperationAccuracies())) + .as_nullable(); + } + } + + return nullptr; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +TransformationNNPtr +Transformation::Private::registerInv(const Transformation *thisIn, + TransformationNNPtr invTransform) { + invTransform->d->forwardOperation_ = thisIn->shallowClone().as_nullable(); + invTransform->setHasBallparkTransformation( + thisIn->hasBallparkTransformation()); + return invTransform; +} +//! @endcond + +// --------------------------------------------------------------------------- + +CoordinateOperationNNPtr Transformation::inverse() const { + return inverseAsTransformation(); +} + +// --------------------------------------------------------------------------- + +TransformationNNPtr Transformation::inverseAsTransformation() const { + + if (d->forwardOperation_) { + return NN_NO_CHECK(d->forwardOperation_); + } + const auto &l_method = method(); + const auto &methodName = l_method->nameStr(); + const int methodEPSGCode = l_method->getEPSGCode(); + const auto &l_sourceCRS = sourceCRS(); + const auto &l_targetCRS = targetCRS(); + + // For geocentric translation, the inverse is exactly the negation of + // the parameters. + if (ci_find(methodName, "Geocentric translations") != std::string::npos || + methodEPSGCode == EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOCENTRIC || + methodEPSGCode == + EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_2D || + methodEPSGCode == + EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_3D) { + double x = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION); + double y = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION); + double z = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION); + auto properties = createPropertiesForInverse(this, false, false); + return Private::registerInv( + this, create(properties, l_targetCRS, l_sourceCRS, nullptr, + createMethodMapNameEPSGCode( + useOperationMethodEPSGCodeIfPresent( + properties, methodEPSGCode)), + VectorOfParameters{ + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION), + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION), + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION), + }, + createParams(common::Length(negate(x)), + common::Length(negate(y)), + common::Length(negate(z))), + coordinateOperationAccuracies())); + } + + if (methodEPSGCode == EPSG_CODE_METHOD_MOLODENSKY || + methodEPSGCode == EPSG_CODE_METHOD_ABRIDGED_MOLODENSKY) { + double x = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION); + double y = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION); + double z = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION); + double da = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_SEMI_MAJOR_AXIS_DIFFERENCE); + double df = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_FLATTENING_DIFFERENCE); + + if (methodEPSGCode == EPSG_CODE_METHOD_ABRIDGED_MOLODENSKY) { + return Private::registerInv( + this, + createAbridgedMolodensky( + createPropertiesForInverse(this, false, false), l_targetCRS, + l_sourceCRS, negate(x), negate(y), negate(z), negate(da), + negate(df), coordinateOperationAccuracies())); + } else { + return Private::registerInv( + this, + createMolodensky(createPropertiesForInverse(this, false, false), + l_targetCRS, l_sourceCRS, negate(x), negate(y), + negate(z), negate(da), negate(df), + coordinateOperationAccuracies())); + } + } + + if (isLongitudeRotation()) { + auto offset = + parameterValueMeasure(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET); + const common::Angle newOffset(negate(offset.value()), offset.unit()); + return Private::registerInv( + this, createLongitudeRotation( + createPropertiesForInverse(this, false, false), + l_targetCRS, l_sourceCRS, newOffset)); + } + + if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC2D_OFFSETS) { + auto offsetLat = + parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_OFFSET); + const common::Angle newOffsetLat(negate(offsetLat.value()), + offsetLat.unit()); + + auto offsetLong = + parameterValueMeasure(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET); + const common::Angle newOffsetLong(negate(offsetLong.value()), + offsetLong.unit()); + + return Private::registerInv( + this, createGeographic2DOffsets( + createPropertiesForInverse(this, false, false), + l_targetCRS, l_sourceCRS, newOffsetLat, newOffsetLong, + coordinateOperationAccuracies())); + } + + if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC3D_OFFSETS) { + auto offsetLat = + parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_OFFSET); + const common::Angle newOffsetLat(negate(offsetLat.value()), + offsetLat.unit()); + + auto offsetLong = + parameterValueMeasure(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET); + const common::Angle newOffsetLong(negate(offsetLong.value()), + offsetLong.unit()); + + auto offsetHeight = + parameterValueMeasure(EPSG_CODE_PARAMETER_VERTICAL_OFFSET); + const common::Length newOffsetHeight(negate(offsetHeight.value()), + offsetHeight.unit()); + + return Private::registerInv( + this, createGeographic3DOffsets( + createPropertiesForInverse(this, false, false), + l_targetCRS, l_sourceCRS, newOffsetLat, newOffsetLong, + newOffsetHeight, coordinateOperationAccuracies())); + } + + if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC2D_WITH_HEIGHT_OFFSETS) { + auto offsetLat = + parameterValueMeasure(EPSG_CODE_PARAMETER_LATITUDE_OFFSET); + const common::Angle newOffsetLat(negate(offsetLat.value()), + offsetLat.unit()); + + auto offsetLong = + parameterValueMeasure(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET); + const common::Angle newOffsetLong(negate(offsetLong.value()), + offsetLong.unit()); + + auto offsetHeight = + parameterValueMeasure(EPSG_CODE_PARAMETER_GEOID_UNDULATION); + const common::Length newOffsetHeight(negate(offsetHeight.value()), + offsetHeight.unit()); + + return Private::registerInv( + this, createGeographic2DWithHeightOffsets( + createPropertiesForInverse(this, false, false), + l_targetCRS, l_sourceCRS, newOffsetLat, newOffsetLong, + newOffsetHeight, coordinateOperationAccuracies())); + } + + if (methodEPSGCode == EPSG_CODE_METHOD_VERTICAL_OFFSET) { + + auto offsetHeight = + parameterValueMeasure(EPSG_CODE_PARAMETER_VERTICAL_OFFSET); + const common::Length newOffsetHeight(negate(offsetHeight.value()), + offsetHeight.unit()); + + return Private::registerInv( + this, + createVerticalOffset(createPropertiesForInverse(this, false, false), + l_targetCRS, l_sourceCRS, newOffsetHeight, + coordinateOperationAccuracies())); + } + + if (methodEPSGCode == EPSG_CODE_METHOD_CHANGE_VERTICAL_UNIT) { + const double convFactor = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_UNIT_CONVERSION_SCALAR); + return Private::registerInv( + this, createChangeVerticalUnit( + createPropertiesForInverse(this, false, false), + l_targetCRS, l_sourceCRS, common::Scale(1.0 / convFactor), + coordinateOperationAccuracies())); + } + +#ifdef notdef + // We don't need that currently, but we might... + if (methodEPSGCode == EPSG_CODE_METHOD_HEIGHT_DEPTH_REVERSAL) { + return Private::registerInv( + this, + createHeightDepthReversal( + createPropertiesForInverse(this, false, false), l_targetCRS, + l_sourceCRS, coordinateOperationAccuracies())); + } +#endif + + return InverseTransformation::create(NN_NO_CHECK( + util::nn_dynamic_pointer_cast(shared_from_this()))); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +// --------------------------------------------------------------------------- + +InverseTransformation::InverseTransformation(const TransformationNNPtr &forward) + : Transformation( + forward->targetCRS(), forward->sourceCRS(), + forward->interpolationCRS(), + OperationMethod::create(createPropertiesForInverse(forward->method()), + forward->method()->parameters()), + forward->parameterValues(), forward->coordinateOperationAccuracies()), + InverseCoordinateOperation(forward, true) { + setPropertiesFromForward(); +} + +// --------------------------------------------------------------------------- + +InverseTransformation::~InverseTransformation() = default; + +// --------------------------------------------------------------------------- + +TransformationNNPtr +InverseTransformation::create(const TransformationNNPtr &forward) { + auto conv = util::nn_make_shared(forward); + conv->assignSelf(conv); + return conv; +} + +// --------------------------------------------------------------------------- + +TransformationNNPtr InverseTransformation::inverseAsTransformation() const { + return NN_NO_CHECK( + util::nn_dynamic_pointer_cast(forwardOperation_)); +} + +// --------------------------------------------------------------------------- + +void InverseTransformation::_exportToWKT(io::WKTFormatter *formatter) const { + + auto approxInverse = createApproximateInverseIfPossible( + util::nn_dynamic_pointer_cast(forwardOperation_).get()); + if (approxInverse) { + approxInverse->_exportToWKT(formatter); + } else { + Transformation::_exportToWKT(formatter); + } +} + +// --------------------------------------------------------------------------- + +CoordinateOperationNNPtr InverseTransformation::_shallowClone() const { + auto op = InverseTransformation::nn_make_shared( + inverseAsTransformation()->shallowClone()); + op->assignSelf(op); + op->setCRSs(this, false); + return util::nn_static_pointer_cast(op); +} + +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void Transformation::_exportToWKT(io::WKTFormatter *formatter) const { + exportTransformationToWKT(formatter); +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void Transformation::_exportToJSON( + io::JSONFormatter *formatter) const // throw(FormattingException) +{ + auto writer = formatter->writer(); + auto objectContext(formatter->MakeObjectContext( + formatter->abridgedTransformation() ? "AbridgedTransformation" + : "Transformation", + !identifiers().empty())); + + writer->AddObjKey("name"); + auto l_name = nameStr(); + if (l_name.empty()) { + writer->Add("unnamed"); + } else { + writer->Add(l_name); + } + + if (!formatter->abridgedTransformation()) { + writer->AddObjKey("source_crs"); + formatter->setAllowIDInImmediateChild(); + sourceCRS()->_exportToJSON(formatter); + + writer->AddObjKey("target_crs"); + formatter->setAllowIDInImmediateChild(); + targetCRS()->_exportToJSON(formatter); + + const auto &l_interpolationCRS = interpolationCRS(); + if (l_interpolationCRS) { + writer->AddObjKey("interpolation_crs"); + formatter->setAllowIDInImmediateChild(); + l_interpolationCRS->_exportToJSON(formatter); + } + } + + writer->AddObjKey("method"); + formatter->setOmitTypeInImmediateChild(); + formatter->setAllowIDInImmediateChild(); + method()->_exportToJSON(formatter); + + writer->AddObjKey("parameters"); + { + auto parametersContext(writer->MakeArrayContext(false)); + for (const auto &genOpParamvalue : parameterValues()) { + formatter->setAllowIDInImmediateChild(); + formatter->setOmitTypeInImmediateChild(); + genOpParamvalue->_exportToJSON(formatter); + } + } + + if (!formatter->abridgedTransformation()) { + if (!coordinateOperationAccuracies().empty()) { + writer->AddObjKey("accuracy"); + writer->Add(coordinateOperationAccuracies()[0]->value()); + } + } + + if (formatter->abridgedTransformation()) { + if (formatter->outputId()) { + formatID(formatter); + } + } else { + ObjectUsage::baseExportToJSON(formatter); + } +} + +//! @endcond + +//! @cond Doxygen_Suppress +static const std::string nullString; + +static const std::string &_getNTv2Filename(const Transformation *op, + bool allowInverse) { + + const auto &l_method = op->method(); + if (l_method->getEPSGCode() == EPSG_CODE_METHOD_NTV2 || + (allowInverse && + ci_equal(l_method->nameStr(), INVERSE_OF + EPSG_NAME_METHOD_NTV2))) { + const auto &fileParameter = op->parameterValue( + EPSG_NAME_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE, + EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE); + if (fileParameter && + fileParameter->type() == ParameterValue::Type::FILENAME) { + return fileParameter->valueFile(); + } + } + return nullString; +} +//! @endcond + +// --------------------------------------------------------------------------- +//! @cond Doxygen_Suppress +const std::string &Transformation::getNTv2Filename() const { + + return _getNTv2Filename(this, false); +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static const std::string &_getNTv1Filename(const Transformation *op, + bool allowInverse) { + + const auto &l_method = op->method(); + const auto &methodName = l_method->nameStr(); + if (l_method->getEPSGCode() == EPSG_CODE_METHOD_NTV1 || + (allowInverse && + ci_equal(methodName, INVERSE_OF + EPSG_NAME_METHOD_NTV1))) { + const auto &fileParameter = op->parameterValue( + EPSG_NAME_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE, + EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE); + if (fileParameter && + fileParameter->type() == ParameterValue::Type::FILENAME) { + return fileParameter->valueFile(); + } + } + return nullString; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static const std::string &_getCTABLE2Filename(const Transformation *op, + bool allowInverse) { + const auto &l_method = op->method(); + const auto &methodName = l_method->nameStr(); + if (ci_equal(methodName, PROJ_WKT2_NAME_METHOD_CTABLE2) || + (allowInverse && + ci_equal(methodName, INVERSE_OF + PROJ_WKT2_NAME_METHOD_CTABLE2))) { + const auto &fileParameter = op->parameterValue( + EPSG_NAME_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE, + EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE); + if (fileParameter && + fileParameter->type() == ParameterValue::Type::FILENAME) { + return fileParameter->valueFile(); + } + } + return nullString; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static const std::string & +_getHorizontalShiftGTIFFFilename(const Transformation *op, bool allowInverse) { + const auto &l_method = op->method(); + const auto &methodName = l_method->nameStr(); + if (ci_equal(methodName, PROJ_WKT2_NAME_METHOD_HORIZONTAL_SHIFT_GTIFF) || + (allowInverse && + ci_equal(methodName, + INVERSE_OF + PROJ_WKT2_NAME_METHOD_HORIZONTAL_SHIFT_GTIFF))) { + const auto &fileParameter = op->parameterValue( + EPSG_NAME_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE, + EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE); + if (fileParameter && + fileParameter->type() == ParameterValue::Type::FILENAME) { + return fileParameter->valueFile(); + } + } + return nullString; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static const std::string & +_getGeocentricTranslationFilename(const Transformation *op, bool allowInverse) { + + const auto &l_method = op->method(); + const auto &methodName = l_method->nameStr(); + if (l_method->getEPSGCode() == + EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_BY_GRID_INTERPOLATION_IGN || + (allowInverse && + ci_equal( + methodName, + INVERSE_OF + + EPSG_NAME_METHOD_GEOCENTRIC_TRANSLATION_BY_GRID_INTERPOLATION_IGN))) { + const auto &fileParameter = + op->parameterValue(EPSG_NAME_PARAMETER_GEOCENTRIC_TRANSLATION_FILE, + EPSG_CODE_PARAMETER_GEOCENTRIC_TRANSLATION_FILE); + if (fileParameter && + fileParameter->type() == ParameterValue::Type::FILENAME) { + return fileParameter->valueFile(); + } + } + return nullString; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static const std::string & +_getHeightToGeographic3DFilename(const Transformation *op, bool allowInverse) { + + const auto &methodName = op->method()->nameStr(); + + if (ci_equal(methodName, PROJ_WKT2_NAME_METHOD_HEIGHT_TO_GEOG3D) || + (allowInverse && + ci_equal(methodName, + INVERSE_OF + PROJ_WKT2_NAME_METHOD_HEIGHT_TO_GEOG3D))) { + const auto &fileParameter = + op->parameterValue(EPSG_NAME_PARAMETER_GEOID_CORRECTION_FILENAME, + EPSG_CODE_PARAMETER_GEOID_CORRECTION_FILENAME); + if (fileParameter && + fileParameter->type() == ParameterValue::Type::FILENAME) { + return fileParameter->valueFile(); + } + } + return nullString; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static bool +isGeographic3DToGravityRelatedHeight(const OperationMethodNNPtr &method, + bool allowInverse) { + const auto &methodName = method->nameStr(); + static const char *const methodCodes[] = { + "1025", // Geographic3D to GravityRelatedHeight (EGM2008) + "1030", // Geographic3D to GravityRelatedHeight (NZgeoid) + "1045", // Geographic3D to GravityRelatedHeight (OSGM02-Ire) + "1047", // Geographic3D to GravityRelatedHeight (Gravsoft) + "1048", // Geographic3D to GravityRelatedHeight (Ausgeoid v2) + "1050", // Geographic3D to GravityRelatedHeight (CI) + "1059", // Geographic3D to GravityRelatedHeight (PNG) + "1060", // Geographic3D to GravityRelatedHeight (CGG2013) + "1072", // Geographic3D to GravityRelatedHeight (OSGM15-Ire) + "1073", // Geographic3D to GravityRelatedHeight (IGN2009) + "1081", // Geographic3D to GravityRelatedHeight (BEV AT) + "1083", // Geog3D to Geog2D+Vertical (AUSGeoid v2) + "9661", // Geographic3D to GravityRelatedHeight (EGM) + "9662", // Geographic3D to GravityRelatedHeight (Ausgeoid98) + "9663", // Geographic3D to GravityRelatedHeight (OSGM-GB) + "9664", // Geographic3D to GravityRelatedHeight (IGN1997) + "9665", // Geographic3D to GravityRelatedHeight (US .gtx) + "9635", // Geog3D to Geog2D+GravityRelatedHeight (US .gtx) + }; + + if (ci_find(methodName, "Geographic3D to GravityRelatedHeight") == 0) { + return true; + } + if (allowInverse && + ci_find(methodName, + INVERSE_OF + "Geographic3D to GravityRelatedHeight") == 0) { + return true; + } + + for (const auto &code : methodCodes) { + for (const auto &idSrc : method->identifiers()) { + const auto &srcAuthName = *(idSrc->codeSpace()); + const auto &srcCode = idSrc->code(); + if (ci_equal(srcAuthName, "EPSG") && srcCode == code) { + return true; + } + if (allowInverse && ci_equal(srcAuthName, "INVERSE(EPSG)") && + srcCode == code) { + return true; + } + } + } + return false; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +const std::string &Transformation::getHeightToGeographic3DFilename() const { + + const std::string &ret = _getHeightToGeographic3DFilename(this, false); + if (!ret.empty()) + return ret; + if (isGeographic3DToGravityRelatedHeight(method(), false)) { + const auto &fileParameter = + parameterValue(EPSG_NAME_PARAMETER_GEOID_CORRECTION_FILENAME, + EPSG_CODE_PARAMETER_GEOID_CORRECTION_FILENAME); + if (fileParameter && + fileParameter->type() == ParameterValue::Type::FILENAME) { + return fileParameter->valueFile(); + } + } + return nullString; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static util::PropertyMap +createSimilarPropertiesMethod(common::IdentifiedObjectNNPtr obj) { + util::PropertyMap map; + + const std::string &forwardName = obj->nameStr(); + if (!forwardName.empty()) { + map.set(common::IdentifiedObject::NAME_KEY, forwardName); + } + + { + auto ar = util::ArrayOfBaseObject::create(); + for (const auto &idSrc : obj->identifiers()) { + const auto &srcAuthName = *(idSrc->codeSpace()); + const auto &srcCode = idSrc->code(); + auto idsProp = util::PropertyMap().set( + metadata::Identifier::CODESPACE_KEY, srcAuthName); + ar->add(metadata::Identifier::create(srcCode, idsProp)); + } + if (!ar->empty()) { + map.set(common::IdentifiedObject::IDENTIFIERS_KEY, ar); + } + } + + return map; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static util::PropertyMap +createSimilarPropertiesTransformation(TransformationNNPtr obj) { + util::PropertyMap map; + + // The domain(s) are unchanged + addDomains(map, obj.get()); + + const std::string &forwardName = obj->nameStr(); + if (!forwardName.empty()) { + map.set(common::IdentifiedObject::NAME_KEY, forwardName); + } + + const std::string &remarks = obj->remarks(); + if (!remarks.empty()) { + map.set(common::IdentifiedObject::REMARKS_KEY, remarks); + } + + addModifiedIdentifier(map, obj.get(), false, true); + + return map; +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +static TransformationNNPtr +createNTv1(const util::PropertyMap &properties, + const crs::CRSNNPtr &sourceCRSIn, const crs::CRSNNPtr &targetCRSIn, + const std::string &filename, + const std::vector &accuracies) { + return Transformation::create( + properties, sourceCRSIn, targetCRSIn, nullptr, + createMethodMapNameEPSGCode(EPSG_CODE_METHOD_NTV1), + {OperationParameter::create( + util::PropertyMap() + .set(common::IdentifiedObject::NAME_KEY, + EPSG_NAME_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE) + .set(metadata::Identifier::CODESPACE_KEY, + metadata::Identifier::EPSG) + .set(metadata::Identifier::CODE_KEY, + EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE))}, + {ParameterValue::createFilename(filename)}, accuracies); +} +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Return an equivalent transformation to the current one, but using + * PROJ alternative grid names. + */ +TransformationNNPtr Transformation::substitutePROJAlternativeGridNames( + io::DatabaseContextNNPtr databaseContext) const { + auto self = NN_NO_CHECK(std::dynamic_pointer_cast( + shared_from_this().as_nullable())); + + const auto &l_method = method(); + const int methodEPSGCode = l_method->getEPSGCode(); + + std::string projFilename; + std::string projGridFormat; + bool inverseDirection = false; + + const auto &NTv1Filename = _getNTv1Filename(this, false); + const auto &NTv2Filename = _getNTv2Filename(this, false); + std::string lasFilename; + if (methodEPSGCode == EPSG_CODE_METHOD_NADCON) { + const auto &latitudeFileParameter = + parameterValue(EPSG_NAME_PARAMETER_LATITUDE_DIFFERENCE_FILE, + EPSG_CODE_PARAMETER_LATITUDE_DIFFERENCE_FILE); + const auto &longitudeFileParameter = + parameterValue(EPSG_NAME_PARAMETER_LONGITUDE_DIFFERENCE_FILE, + EPSG_CODE_PARAMETER_LONGITUDE_DIFFERENCE_FILE); + if (latitudeFileParameter && + latitudeFileParameter->type() == ParameterValue::Type::FILENAME && + longitudeFileParameter && + longitudeFileParameter->type() == ParameterValue::Type::FILENAME) { + lasFilename = latitudeFileParameter->valueFile(); + } + } + const auto &horizontalGridName = + !NTv1Filename.empty() ? NTv1Filename : !NTv2Filename.empty() + ? NTv2Filename + : lasFilename; + + if (!horizontalGridName.empty() && + databaseContext->lookForGridAlternative(horizontalGridName, + projFilename, projGridFormat, + inverseDirection)) { + + if (horizontalGridName == projFilename) { + if (inverseDirection) { + throw util::UnsupportedOperationException( + "Inverse direction for " + projFilename + " not supported"); + } + return self; + } + + const auto &l_sourceCRS = sourceCRS(); + const auto &l_targetCRS = targetCRS(); + const auto &l_accuracies = coordinateOperationAccuracies(); + if (projGridFormat == "GTiff") { + auto parameters = + std::vector{createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE)}; + auto methodProperties = util::PropertyMap().set( + common::IdentifiedObject::NAME_KEY, + PROJ_WKT2_NAME_METHOD_HORIZONTAL_SHIFT_GTIFF); + auto values = std::vector{ + ParameterValue::createFilename(projFilename)}; + if (inverseDirection) { + return create(createPropertiesForInverse( + self.as_nullable().get(), true, false), + l_targetCRS, l_sourceCRS, nullptr, + methodProperties, parameters, values, + l_accuracies) + ->inverseAsTransformation(); + + } else { + return create(createSimilarPropertiesTransformation(self), + l_sourceCRS, l_targetCRS, nullptr, + methodProperties, parameters, values, + l_accuracies); + } + } else if (projGridFormat == "NTv1") { + if (inverseDirection) { + return createNTv1(createPropertiesForInverse( + self.as_nullable().get(), true, false), + l_targetCRS, l_sourceCRS, projFilename, + l_accuracies) + ->inverseAsTransformation(); + } else { + return createNTv1(createSimilarPropertiesTransformation(self), + l_sourceCRS, l_targetCRS, projFilename, + l_accuracies); + } + } else if (projGridFormat == "NTv2") { + if (inverseDirection) { + return createNTv2(createPropertiesForInverse( + self.as_nullable().get(), true, false), + l_targetCRS, l_sourceCRS, projFilename, + l_accuracies) + ->inverseAsTransformation(); + } else { + return createNTv2(createSimilarPropertiesTransformation(self), + l_sourceCRS, l_targetCRS, projFilename, + l_accuracies); + } + } else if (projGridFormat == "CTable2") { + auto parameters = + std::vector{createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_LATITUDE_LONGITUDE_DIFFERENCE_FILE)}; + auto methodProperties = + util::PropertyMap().set(common::IdentifiedObject::NAME_KEY, + PROJ_WKT2_NAME_METHOD_CTABLE2); + auto values = std::vector{ + ParameterValue::createFilename(projFilename)}; + if (inverseDirection) { + return create(createPropertiesForInverse( + self.as_nullable().get(), true, false), + l_targetCRS, l_sourceCRS, nullptr, + methodProperties, parameters, values, + l_accuracies) + ->inverseAsTransformation(); + + } else { + return create(createSimilarPropertiesTransformation(self), + l_sourceCRS, l_targetCRS, nullptr, + methodProperties, parameters, values, + l_accuracies); + } + } + } + + if (isGeographic3DToGravityRelatedHeight(method(), false)) { + const auto &fileParameter = + parameterValue(EPSG_NAME_PARAMETER_GEOID_CORRECTION_FILENAME, + EPSG_CODE_PARAMETER_GEOID_CORRECTION_FILENAME); + if (fileParameter && + fileParameter->type() == ParameterValue::Type::FILENAME) { + auto filename = fileParameter->valueFile(); + if (databaseContext->lookForGridAlternative( + filename, projFilename, projGridFormat, inverseDirection)) { + + if (inverseDirection) { + throw util::UnsupportedOperationException( + "Inverse direction for " + "Geographic3DToGravityRelatedHeight not supported"); + } + + if (filename == projFilename) { + return self; + } + + auto parameters = std::vector{ + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_GEOID_CORRECTION_FILENAME)}; +#ifdef disabled_for_now + if (inverseDirection) { + return create(createPropertiesForInverse( + self.as_nullable().get(), true, false), + targetCRS(), sourceCRS(), nullptr, + createSimilarPropertiesMethod(method()), + parameters, {ParameterValue::createFilename( + projFilename)}, + coordinateOperationAccuracies()) + ->inverseAsTransformation(); + } else +#endif + { + return create( + createSimilarPropertiesTransformation(self), + sourceCRS(), targetCRS(), nullptr, + createSimilarPropertiesMethod(method()), parameters, + {ParameterValue::createFilename(projFilename)}, + coordinateOperationAccuracies()); + } + } + } + } + + const auto &geocentricTranslationFilename = + _getGeocentricTranslationFilename(this, false); + if (!geocentricTranslationFilename.empty()) { + if (databaseContext->lookForGridAlternative( + geocentricTranslationFilename, projFilename, projGridFormat, + inverseDirection)) { + + if (inverseDirection) { + throw util::UnsupportedOperationException( + "Inverse direction for " + "GeocentricTranslation not supported"); + } + + if (geocentricTranslationFilename == projFilename) { + return self; + } + + auto parameters = + std::vector{createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_GEOCENTRIC_TRANSLATION_FILE)}; + return create(createSimilarPropertiesTransformation(self), + sourceCRS(), targetCRS(), interpolationCRS(), + createSimilarPropertiesMethod(method()), parameters, + {ParameterValue::createFilename(projFilename)}, + coordinateOperationAccuracies()); + } + } + + if (methodEPSGCode == EPSG_CODE_METHOD_VERTCON || + methodEPSGCode == EPSG_CODE_METHOD_VERTICALGRID_NZLVD || + methodEPSGCode == EPSG_CODE_METHOD_VERTICALGRID_BEV_AT || + methodEPSGCode == EPSG_CODE_METHOD_VERTICALGRID_GTX) { + auto fileParameter = + parameterValue(EPSG_NAME_PARAMETER_VERTICAL_OFFSET_FILE, + EPSG_CODE_PARAMETER_VERTICAL_OFFSET_FILE); + if (fileParameter && + fileParameter->type() == ParameterValue::Type::FILENAME) { + + auto filename = fileParameter->valueFile(); + if (databaseContext->lookForGridAlternative( + filename, projFilename, projGridFormat, inverseDirection)) { + + if (filename == projFilename) { + if (inverseDirection) { + throw util::UnsupportedOperationException( + "Inverse direction for " + projFilename + + " not supported"); + } + return self; + } + + auto parameters = std::vector{ + createOpParamNameEPSGCode( + EPSG_CODE_PARAMETER_VERTICAL_OFFSET_FILE)}; + if (inverseDirection) { + return create(createPropertiesForInverse( + self.as_nullable().get(), true, false), + targetCRS(), sourceCRS(), nullptr, + createSimilarPropertiesMethod(method()), + parameters, {ParameterValue::createFilename( + projFilename)}, + coordinateOperationAccuracies()) + ->inverseAsTransformation(); + } else { + return create( + createSimilarPropertiesTransformation(self), + sourceCRS(), targetCRS(), nullptr, + createSimilarPropertiesMethod(method()), parameters, + {ParameterValue::createFilename(projFilename)}, + coordinateOperationAccuracies()); + } + } + } + } + + return self; +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +static void ThrowExceptionNotGeodeticGeographic(const char *trfrm_name) { + throw io::FormattingException(concat("Can apply ", std::string(trfrm_name), + " only to GeodeticCRS / " + "GeographicCRS")); +} + +// --------------------------------------------------------------------------- + +// If crs is a geographic CRS, or a compound CRS of a geographic CRS, +// or a compoundCRS of a bound CRS of a geographic CRS, return that +// geographic CRS +static crs::GeographicCRSPtr +extractGeographicCRSIfGeographicCRSOrEquivalent(const crs::CRSNNPtr &crs) { + auto geogCRS = util::nn_dynamic_pointer_cast(crs); + if (!geogCRS) { + auto compoundCRS = util::nn_dynamic_pointer_cast(crs); + if (compoundCRS) { + const auto &components = compoundCRS->componentReferenceSystems(); + if (!components.empty()) { + geogCRS = util::nn_dynamic_pointer_cast( + components[0]); + if (!geogCRS) { + auto boundCRS = + util::nn_dynamic_pointer_cast( + components[0]); + if (boundCRS) { + geogCRS = + util::nn_dynamic_pointer_cast( + boundCRS->baseCRS()); + } + } + } + } else { + auto boundCRS = util::nn_dynamic_pointer_cast(crs); + if (boundCRS) { + geogCRS = util::nn_dynamic_pointer_cast( + boundCRS->baseCRS()); + } + } + } + return geogCRS; +} + +// --------------------------------------------------------------------------- + +static void setupPROJGeodeticSourceCRS(io::PROJStringFormatter *formatter, + const crs::CRSNNPtr &crs, bool addPushV3, + const char *trfrm_name) { + auto sourceCRSGeog = extractGeographicCRSIfGeographicCRSOrEquivalent(crs); + if (sourceCRSGeog) { + formatter->startInversion(); + sourceCRSGeog->_exportToPROJString(formatter); + formatter->stopInversion(); + if (util::isOfExactType( + *(sourceCRSGeog.get()))) { + // The export of a DerivedGeographicCRS in non-CRS mode adds + // unit conversion and axis swapping. We must compensate for that + formatter->startInversion(); + sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + formatter->stopInversion(); + } + + if (addPushV3) { + formatter->addStep("push"); + formatter->addParam("v_3"); + } + + formatter->addStep("cart"); + sourceCRSGeog->ellipsoid()->_exportToPROJString(formatter); + } else { + auto sourceCRSGeod = dynamic_cast(crs.get()); + if (!sourceCRSGeod) { + ThrowExceptionNotGeodeticGeographic(trfrm_name); + } + formatter->startInversion(); + sourceCRSGeod->addGeocentricUnitConversionIntoPROJString(formatter); + formatter->stopInversion(); + } +} +// --------------------------------------------------------------------------- + +static void setupPROJGeodeticTargetCRS(io::PROJStringFormatter *formatter, + const crs::CRSNNPtr &crs, bool addPopV3, + const char *trfrm_name) { + auto targetCRSGeog = extractGeographicCRSIfGeographicCRSOrEquivalent(crs); + if (targetCRSGeog) { + formatter->addStep("cart"); + formatter->setCurrentStepInverted(true); + targetCRSGeog->ellipsoid()->_exportToPROJString(formatter); + + if (addPopV3) { + formatter->addStep("pop"); + formatter->addParam("v_3"); + } + if (util::isOfExactType( + *(targetCRSGeog.get()))) { + // The export of a DerivedGeographicCRS in non-CRS mode adds + // unit conversion and axis swapping. We must compensate for that + targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + } + targetCRSGeog->_exportToPROJString(formatter); + } else { + auto targetCRSGeod = dynamic_cast(crs.get()); + if (!targetCRSGeod) { + ThrowExceptionNotGeodeticGeographic(trfrm_name); + } + targetCRSGeod->addGeocentricUnitConversionIntoPROJString(formatter); + } +} + +//! @endcond + +// --------------------------------------------------------------------------- + +void Transformation::_exportToPROJString( + io::PROJStringFormatter *formatter) const // throw(FormattingException) +{ + if (formatter->convention() == + io::PROJStringFormatter::Convention::PROJ_4) { + throw io::FormattingException( + "Transformation cannot be exported as a PROJ.4 string"); + } + + formatter->setCoordinateOperationOptimizations(true); + + bool positionVectorConvention = true; + bool sevenParamsTransform = false; + bool threeParamsTransform = false; + bool fifteenParamsTransform = false; + const auto &l_method = method(); + const int methodEPSGCode = l_method->getEPSGCode(); + const auto &methodName = l_method->nameStr(); + const auto paramCount = parameterValues().size(); + const bool l_isTimeDependent = isTimeDependent(methodName); + const bool isPositionVector = + ci_find(methodName, "Position Vector") != std::string::npos || + ci_find(methodName, "PV") != std::string::npos; + const bool isCoordinateFrame = + ci_find(methodName, "Coordinate Frame") != std::string::npos || + ci_find(methodName, "CF") != std::string::npos; + if ((paramCount == 7 && isCoordinateFrame && !l_isTimeDependent) || + methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC || + methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D || + methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D) { + positionVectorConvention = false; + sevenParamsTransform = true; + } else if ( + (paramCount == 15 && isCoordinateFrame && l_isTimeDependent) || + methodEPSGCode == + EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOCENTRIC || + methodEPSGCode == + EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOGRAPHIC_2D || + methodEPSGCode == + EPSG_CODE_METHOD_TIME_DEPENDENT_COORDINATE_FRAME_GEOGRAPHIC_3D) { + positionVectorConvention = false; + fifteenParamsTransform = true; + } else if ((paramCount == 7 && isPositionVector && !l_isTimeDependent) || + methodEPSGCode == EPSG_CODE_METHOD_POSITION_VECTOR_GEOCENTRIC || + methodEPSGCode == + EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_2D || + methodEPSGCode == + EPSG_CODE_METHOD_POSITION_VECTOR_GEOGRAPHIC_3D) { + sevenParamsTransform = true; + } else if ( + (paramCount == 15 && isPositionVector && l_isTimeDependent) || + methodEPSGCode == + EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOCENTRIC || + methodEPSGCode == + EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOGRAPHIC_2D || + methodEPSGCode == + EPSG_CODE_METHOD_TIME_DEPENDENT_POSITION_VECTOR_GEOGRAPHIC_3D) { + fifteenParamsTransform = true; + } else if ((paramCount == 3 && + ci_find(methodName, "Geocentric translations") != + std::string::npos) || + methodEPSGCode == + EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOCENTRIC || + methodEPSGCode == + EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_2D || + methodEPSGCode == + EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOGRAPHIC_3D) { + threeParamsTransform = true; + } + if (threeParamsTransform || sevenParamsTransform || + fifteenParamsTransform) { + double x = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION); + double y = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION); + double z = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION); + + auto sourceCRSGeog = + dynamic_cast(sourceCRS().get()); + auto targetCRSGeog = + dynamic_cast(targetCRS().get()); + const bool addPushPopV3 = + !CoordinateOperation::getPrivate()->use3DHelmert_ && + ((sourceCRSGeog && + sourceCRSGeog->coordinateSystem()->axisList().size() == 2) || + (targetCRSGeog && + targetCRSGeog->coordinateSystem()->axisList().size() == 2)); + + setupPROJGeodeticSourceCRS(formatter, sourceCRS(), addPushPopV3, + "Helmert"); + + formatter->addStep("helmert"); + formatter->addParam("x", x); + formatter->addParam("y", y); + formatter->addParam("z", z); + if (sevenParamsTransform || fifteenParamsTransform) { + double rx = + parameterValueNumeric(EPSG_CODE_PARAMETER_X_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND); + double ry = + parameterValueNumeric(EPSG_CODE_PARAMETER_Y_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND); + double rz = + parameterValueNumeric(EPSG_CODE_PARAMETER_Z_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND); + double scaleDiff = + parameterValueNumeric(EPSG_CODE_PARAMETER_SCALE_DIFFERENCE, + common::UnitOfMeasure::PARTS_PER_MILLION); + formatter->addParam("rx", rx); + formatter->addParam("ry", ry); + formatter->addParam("rz", rz); + formatter->addParam("s", scaleDiff); + if (fifteenParamsTransform) { + double rate_x = parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_X_AXIS_TRANSLATION, + common::UnitOfMeasure::METRE_PER_YEAR); + double rate_y = parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_Y_AXIS_TRANSLATION, + common::UnitOfMeasure::METRE_PER_YEAR); + double rate_z = parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_Z_AXIS_TRANSLATION, + common::UnitOfMeasure::METRE_PER_YEAR); + double rate_rx = parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_X_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND_PER_YEAR); + double rate_ry = parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_Y_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND_PER_YEAR); + double rate_rz = parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_Z_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND_PER_YEAR); + double rate_scaleDiff = parameterValueNumeric( + EPSG_CODE_PARAMETER_RATE_SCALE_DIFFERENCE, + common::UnitOfMeasure::PPM_PER_YEAR); + double referenceEpochYear = + parameterValueNumeric(EPSG_CODE_PARAMETER_REFERENCE_EPOCH, + common::UnitOfMeasure::YEAR); + formatter->addParam("dx", rate_x); + formatter->addParam("dy", rate_y); + formatter->addParam("dz", rate_z); + formatter->addParam("drx", rate_rx); + formatter->addParam("dry", rate_ry); + formatter->addParam("drz", rate_rz); + formatter->addParam("ds", rate_scaleDiff); + formatter->addParam("t_epoch", referenceEpochYear); + } + if (positionVectorConvention) { + formatter->addParam("convention", "position_vector"); + } else { + formatter->addParam("convention", "coordinate_frame"); + } + } + + setupPROJGeodeticTargetCRS(formatter, targetCRS(), addPushPopV3, + "Helmert"); + + return; + } + + if (methodEPSGCode == EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_CF_GEOCENTRIC || + methodEPSGCode == EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOCENTRIC || + methodEPSGCode == + EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_CF_GEOGRAPHIC_3D || + methodEPSGCode == + EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOGRAPHIC_3D || + methodEPSGCode == + EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_CF_GEOGRAPHIC_2D || + methodEPSGCode == + EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOGRAPHIC_2D) { + + positionVectorConvention = + isPositionVector || + methodEPSGCode == + EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOCENTRIC || + methodEPSGCode == + EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOGRAPHIC_3D || + methodEPSGCode == + EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOGRAPHIC_2D; + + double x = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION); + double y = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION); + double z = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION); + double rx = parameterValueNumeric(EPSG_CODE_PARAMETER_X_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND); + double ry = parameterValueNumeric(EPSG_CODE_PARAMETER_Y_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND); + double rz = parameterValueNumeric(EPSG_CODE_PARAMETER_Z_AXIS_ROTATION, + common::UnitOfMeasure::ARC_SECOND); + double scaleDiff = + parameterValueNumeric(EPSG_CODE_PARAMETER_SCALE_DIFFERENCE, + common::UnitOfMeasure::PARTS_PER_MILLION); + + double px = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_ORDINATE_1_EVAL_POINT); + double py = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_ORDINATE_2_EVAL_POINT); + double pz = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_ORDINATE_3_EVAL_POINT); + + bool addPushPopV3 = + (methodEPSGCode == + EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_PV_GEOGRAPHIC_2D || + methodEPSGCode == + EPSG_CODE_METHOD_MOLODENSKY_BADEKAS_CF_GEOGRAPHIC_2D); + + setupPROJGeodeticSourceCRS(formatter, sourceCRS(), addPushPopV3, + "Molodensky-Badekas"); + + formatter->addStep("molobadekas"); + formatter->addParam("x", x); + formatter->addParam("y", y); + formatter->addParam("z", z); + formatter->addParam("rx", rx); + formatter->addParam("ry", ry); + formatter->addParam("rz", rz); + formatter->addParam("s", scaleDiff); + formatter->addParam("px", px); + formatter->addParam("py", py); + formatter->addParam("pz", pz); + if (positionVectorConvention) { + formatter->addParam("convention", "position_vector"); + } else { + formatter->addParam("convention", "coordinate_frame"); + } + + setupPROJGeodeticTargetCRS(formatter, targetCRS(), addPushPopV3, + "Molodensky-Badekas"); + + return; + } + + if (methodEPSGCode == EPSG_CODE_METHOD_MOLODENSKY || + methodEPSGCode == EPSG_CODE_METHOD_ABRIDGED_MOLODENSKY) { + double x = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_X_AXIS_TRANSLATION); + double y = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Y_AXIS_TRANSLATION); + double z = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_Z_AXIS_TRANSLATION); + double da = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_SEMI_MAJOR_AXIS_DIFFERENCE); + double df = parameterValueNumericAsSI( + EPSG_CODE_PARAMETER_FLATTENING_DIFFERENCE); + + auto sourceCRSGeog = + dynamic_cast(sourceCRS().get()); + if (!sourceCRSGeog) { + throw io::FormattingException( + "Can apply Molodensky only to GeographicCRS"); + } + + auto targetCRSGeog = + dynamic_cast(targetCRS().get()); + if (!targetCRSGeog) { + throw io::FormattingException( + "Can apply Molodensky only to GeographicCRS"); + } + + formatter->startInversion(); + sourceCRSGeog->_exportToPROJString(formatter); + formatter->stopInversion(); + + formatter->addStep("molodensky"); + sourceCRSGeog->ellipsoid()->_exportToPROJString(formatter); + formatter->addParam("dx", x); + formatter->addParam("dy", y); + formatter->addParam("dz", z); + formatter->addParam("da", da); + formatter->addParam("df", df); + + if (ci_find(methodName, "Abridged") != std::string::npos || + methodEPSGCode == EPSG_CODE_METHOD_ABRIDGED_MOLODENSKY) { + formatter->addParam("abridged"); + } + + targetCRSGeog->_exportToPROJString(formatter); + + return; + } + + if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC2D_OFFSETS) { + double offsetLat = + parameterValueNumeric(EPSG_CODE_PARAMETER_LATITUDE_OFFSET, + common::UnitOfMeasure::ARC_SECOND); + double offsetLong = + parameterValueNumeric(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET, + common::UnitOfMeasure::ARC_SECOND); + + auto sourceCRSGeog = + dynamic_cast(sourceCRS().get()); + if (!sourceCRSGeog) { + throw io::FormattingException( + "Can apply Geographic 2D offsets only to GeographicCRS"); + } + + auto targetCRSGeog = + dynamic_cast(targetCRS().get()); + if (!targetCRSGeog) { + throw io::FormattingException( + "Can apply Geographic 2D offsets only to GeographicCRS"); + } + + formatter->startInversion(); + sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + formatter->stopInversion(); + + if (offsetLat != 0.0 || offsetLong != 0.0) { + formatter->addStep("geogoffset"); + formatter->addParam("dlat", offsetLat); + formatter->addParam("dlon", offsetLong); + } + + targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + + return; + } + + if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC3D_OFFSETS) { + double offsetLat = + parameterValueNumeric(EPSG_CODE_PARAMETER_LATITUDE_OFFSET, + common::UnitOfMeasure::ARC_SECOND); + double offsetLong = + parameterValueNumeric(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET, + common::UnitOfMeasure::ARC_SECOND); + double offsetHeight = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_VERTICAL_OFFSET); + + auto sourceCRSGeog = + dynamic_cast(sourceCRS().get()); + if (!sourceCRSGeog) { + throw io::FormattingException( + "Can apply Geographic 3D offsets only to GeographicCRS"); + } + + auto targetCRSGeog = + dynamic_cast(targetCRS().get()); + if (!targetCRSGeog) { + throw io::FormattingException( + "Can apply Geographic 3D offsets only to GeographicCRS"); + } + + formatter->startInversion(); + sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + formatter->stopInversion(); + + if (offsetLat != 0.0 || offsetLong != 0.0 || offsetHeight != 0.0) { + formatter->addStep("geogoffset"); + formatter->addParam("dlat", offsetLat); + formatter->addParam("dlon", offsetLong); + formatter->addParam("dh", offsetHeight); + } + + targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + + return; + } + + if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC2D_WITH_HEIGHT_OFFSETS) { + double offsetLat = + parameterValueNumeric(EPSG_CODE_PARAMETER_LATITUDE_OFFSET, + common::UnitOfMeasure::ARC_SECOND); + double offsetLong = + parameterValueNumeric(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET, + common::UnitOfMeasure::ARC_SECOND); + double offsetHeight = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_GEOID_UNDULATION); + + auto sourceCRSGeog = + dynamic_cast(sourceCRS().get()); + if (!sourceCRSGeog) { + auto sourceCRSCompound = + dynamic_cast(sourceCRS().get()); + if (sourceCRSCompound) { + sourceCRSGeog = sourceCRSCompound->extractGeographicCRS().get(); + } + if (!sourceCRSGeog) { + throw io::FormattingException("Can apply Geographic 2D with " + "height offsets only to " + "GeographicCRS / CompoundCRS"); + } + } + + auto targetCRSGeog = + dynamic_cast(targetCRS().get()); + if (!targetCRSGeog) { + auto targetCRSCompound = + dynamic_cast(targetCRS().get()); + if (targetCRSCompound) { + targetCRSGeog = targetCRSCompound->extractGeographicCRS().get(); + } + if (!targetCRSGeog) { + throw io::FormattingException("Can apply Geographic 2D with " + "height offsets only to " + "GeographicCRS / CompoundCRS"); + } + } + + formatter->startInversion(); + sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + formatter->stopInversion(); + + if (offsetLat != 0.0 || offsetLong != 0.0 || offsetHeight != 0.0) { + formatter->addStep("geogoffset"); + formatter->addParam("dlat", offsetLat); + formatter->addParam("dlon", offsetLong); + formatter->addParam("dh", offsetHeight); + } + + targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + + return; + } + + if (methodEPSGCode == EPSG_CODE_METHOD_VERTICAL_OFFSET) { + + auto sourceCRSVert = + dynamic_cast(sourceCRS().get()); + if (!sourceCRSVert) { + throw io::FormattingException( + "Can apply Vertical offset only to VerticalCRS"); + } + + auto targetCRSVert = + dynamic_cast(targetCRS().get()); + if (!targetCRSVert) { + throw io::FormattingException( + "Can apply Vertical offset only to VerticalCRS"); + } + + auto offsetHeight = + parameterValueNumericAsSI(EPSG_CODE_PARAMETER_VERTICAL_OFFSET); + + formatter->startInversion(); + sourceCRSVert->addLinearUnitConvert(formatter); + formatter->stopInversion(); + + formatter->addStep("geogoffset"); + formatter->addParam("dh", offsetHeight); + + targetCRSVert->addLinearUnitConvert(formatter); + + return; + } + + // Substitute grid names with PROJ friendly names. + if (formatter->databaseContext()) { + auto alternate = substitutePROJAlternativeGridNames( + NN_NO_CHECK(formatter->databaseContext())); + auto self = NN_NO_CHECK(std::dynamic_pointer_cast( + shared_from_this().as_nullable())); + + if (alternate != self) { + alternate->_exportToPROJString(formatter); + return; + } + } + + const bool isMethodInverseOf = starts_with(methodName, INVERSE_OF); + + const auto &NTv1Filename = _getNTv1Filename(this, true); + const auto &NTv2Filename = _getNTv2Filename(this, true); + const auto &CTABLE2Filename = _getCTABLE2Filename(this, true); + const auto &HorizontalShiftGTIFFFilename = + _getHorizontalShiftGTIFFFilename(this, true); + const auto &hGridShiftFilename = + !HorizontalShiftGTIFFFilename.empty() + ? HorizontalShiftGTIFFFilename + : !NTv1Filename.empty() ? NTv1Filename : !NTv2Filename.empty() + ? NTv2Filename + : CTABLE2Filename; + if (!hGridShiftFilename.empty()) { + auto sourceCRSGeog = + extractGeographicCRSIfGeographicCRSOrEquivalent(sourceCRS()); + if (!sourceCRSGeog) { + throw io::FormattingException( + concat("Can apply ", methodName, " only to GeographicCRS")); + } + + auto targetCRSGeog = + extractGeographicCRSIfGeographicCRSOrEquivalent(targetCRS()); + if (!targetCRSGeog) { + throw io::FormattingException( + concat("Can apply ", methodName, " only to GeographicCRS")); + } + + formatter->startInversion(); + sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + formatter->stopInversion(); + + if (isMethodInverseOf) { + formatter->startInversion(); + } + formatter->addStep("hgridshift"); + formatter->addParam("grids", hGridShiftFilename); + if (isMethodInverseOf) { + formatter->stopInversion(); + } + + targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + + return; + } + + const auto &geocentricTranslationFilename = + _getGeocentricTranslationFilename(this, true); + if (!geocentricTranslationFilename.empty()) { + auto sourceCRSGeog = + dynamic_cast(sourceCRS().get()); + if (!sourceCRSGeog) { + throw io::FormattingException( + concat("Can apply ", methodName, " only to GeographicCRS")); + } + + auto targetCRSGeog = + dynamic_cast(targetCRS().get()); + if (!targetCRSGeog) { + throw io::FormattingException( + concat("Can apply ", methodName, " only to GeographicCRS")); + } + + const auto &interpCRS = interpolationCRS(); + if (!interpCRS) { + throw io::FormattingException( + "InterpolationCRS required " + "for" + " " EPSG_NAME_METHOD_GEOCENTRIC_TRANSLATION_BY_GRID_INTERPOLATION_IGN); + } + const bool interpIsSrc = interpCRS->_isEquivalentTo( + sourceCRS().get(), util::IComparable::Criterion::EQUIVALENT); + const bool interpIsTarget = interpCRS->_isEquivalentTo( + targetCRS().get(), util::IComparable::Criterion::EQUIVALENT); + if (!interpIsSrc && !interpIsTarget) { + throw io::FormattingException( + "For" + " " EPSG_NAME_METHOD_GEOCENTRIC_TRANSLATION_BY_GRID_INTERPOLATION_IGN + ", interpolation CRS should be the source or target CRS"); + } + + formatter->startInversion(); + sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + formatter->stopInversion(); + + if (isMethodInverseOf) { + formatter->startInversion(); + } + + formatter->addStep("push"); + formatter->addParam("v_3"); + + formatter->addStep("cart"); + sourceCRSGeog->ellipsoid()->_exportToPROJString(formatter); + + formatter->addStep("xyzgridshift"); + formatter->addParam("grids", geocentricTranslationFilename); + formatter->addParam("grid_ref", + interpIsTarget ? "output_crs" : "input_crs"); + (interpIsTarget ? targetCRSGeog : sourceCRSGeog) + ->ellipsoid() + ->_exportToPROJString(formatter); + + formatter->startInversion(); + formatter->addStep("cart"); + targetCRSGeog->ellipsoid()->_exportToPROJString(formatter); + formatter->stopInversion(); + + formatter->addStep("pop"); + formatter->addParam("v_3"); + + if (isMethodInverseOf) { + formatter->stopInversion(); + } + + targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + + return; + } + + const auto &heightFilename = _getHeightToGeographic3DFilename(this, true); + if (!heightFilename.empty()) { + auto targetCRSGeog = + extractGeographicCRSIfGeographicCRSOrEquivalent(targetCRS()); + if (!targetCRSGeog) { + throw io::FormattingException( + concat("Can apply ", methodName, " only to GeographicCRS")); + } + + if (!formatter->omitHorizontalConversionInVertTransformation()) { + formatter->startInversion(); + formatter->pushOmitZUnitConversion(); + targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + formatter->popOmitZUnitConversion(); + formatter->stopInversion(); + } + + if (isMethodInverseOf) { + formatter->startInversion(); + } + formatter->addStep("vgridshift"); + formatter->addParam("grids", heightFilename); + formatter->addParam("multiplier", 1.0); + if (isMethodInverseOf) { + formatter->stopInversion(); + } + + if (!formatter->omitHorizontalConversionInVertTransformation()) { + formatter->pushOmitZUnitConversion(); + targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + formatter->popOmitZUnitConversion(); + } + + return; + } + + if (isGeographic3DToGravityRelatedHeight(method(), true)) { + auto fileParameter = + parameterValue(EPSG_NAME_PARAMETER_GEOID_CORRECTION_FILENAME, + EPSG_CODE_PARAMETER_GEOID_CORRECTION_FILENAME); + if (fileParameter && + fileParameter->type() == ParameterValue::Type::FILENAME) { + auto filename = fileParameter->valueFile(); + + auto sourceCRSGeog = + extractGeographicCRSIfGeographicCRSOrEquivalent(sourceCRS()); + if (!sourceCRSGeog) { + throw io::FormattingException( + concat("Can apply ", methodName, " only to GeographicCRS")); + } + + if (!formatter->omitHorizontalConversionInVertTransformation()) { + formatter->startInversion(); + formatter->pushOmitZUnitConversion(); + sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + formatter->popOmitZUnitConversion(); + formatter->stopInversion(); + } + + bool doInversion = isMethodInverseOf; + // The EPSG Geog3DToHeight is the reverse convention of PROJ ! + doInversion = !doInversion; + if (doInversion) { + formatter->startInversion(); + } + formatter->addStep("vgridshift"); + formatter->addParam("grids", filename); + formatter->addParam("multiplier", 1.0); + if (doInversion) { + formatter->stopInversion(); + } + + if (!formatter->omitHorizontalConversionInVertTransformation()) { + formatter->pushOmitZUnitConversion(); + sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + formatter->popOmitZUnitConversion(); + } + + return; + } + } + + if (methodEPSGCode == EPSG_CODE_METHOD_VERTCON) { + auto fileParameter = + parameterValue(EPSG_NAME_PARAMETER_VERTICAL_OFFSET_FILE, + EPSG_CODE_PARAMETER_VERTICAL_OFFSET_FILE); + if (fileParameter && + fileParameter->type() == ParameterValue::Type::FILENAME) { + formatter->addStep("vgridshift"); + formatter->addParam("grids", fileParameter->valueFile()); + if (fileParameter->valueFile().find(".tif") != std::string::npos) { + formatter->addParam("multiplier", 1.0); + } else { + // The vertcon grids go from NGVD 29 to NAVD 88, with units + // in millimeter (see + // https://github.com/OSGeo/proj.4/issues/1071), for gtx files + formatter->addParam("multiplier", 0.001); + } + return; + } + } + + if (methodEPSGCode == EPSG_CODE_METHOD_VERTICALGRID_NZLVD || + methodEPSGCode == EPSG_CODE_METHOD_VERTICALGRID_BEV_AT || + methodEPSGCode == EPSG_CODE_METHOD_VERTICALGRID_GTX) { + auto fileParameter = + parameterValue(EPSG_NAME_PARAMETER_VERTICAL_OFFSET_FILE, + EPSG_CODE_PARAMETER_VERTICAL_OFFSET_FILE); + if (fileParameter && + fileParameter->type() == ParameterValue::Type::FILENAME) { + formatter->addStep("vgridshift"); + formatter->addParam("grids", fileParameter->valueFile()); + formatter->addParam("multiplier", 1.0); + return; + } + } + + if (isLongitudeRotation()) { + double offsetDeg = + parameterValueNumeric(EPSG_CODE_PARAMETER_LONGITUDE_OFFSET, + common::UnitOfMeasure::DEGREE); + + auto sourceCRSGeog = + dynamic_cast(sourceCRS().get()); + if (!sourceCRSGeog) { + throw io::FormattingException( + concat("Can apply ", methodName, " only to GeographicCRS")); + } + + auto targetCRSGeog = + dynamic_cast(targetCRS().get()); + if (!targetCRSGeog) { + throw io::FormattingException( + concat("Can apply ", methodName + " only to GeographicCRS")); + } + + if (!sourceCRSGeog->ellipsoid()->_isEquivalentTo( + targetCRSGeog->ellipsoid().get(), + util::IComparable::Criterion::EQUIVALENT)) { + // This is arguable if we should check this... + throw io::FormattingException("Can apply Longitude rotation " + "only to SRS with same " + "ellipsoid"); + } + + formatter->startInversion(); + sourceCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + formatter->stopInversion(); + + bool done = false; + if (offsetDeg != 0.0) { + // Optimization: as we are doing nominally a +step=inv, + // if the negation of the offset value is a well-known name, + // then use forward case with this name. + auto projPMName = datum::PrimeMeridian::getPROJStringWellKnownName( + common::Angle(-offsetDeg)); + if (!projPMName.empty()) { + done = true; + formatter->addStep("longlat"); + sourceCRSGeog->ellipsoid()->_exportToPROJString(formatter); + formatter->addParam("pm", projPMName); + } + } + if (!done) { + // To actually add the offset, we must use the reverse longlat + // operation. + formatter->startInversion(); + formatter->addStep("longlat"); + sourceCRSGeog->ellipsoid()->_exportToPROJString(formatter); + datum::PrimeMeridian::create(util::PropertyMap(), + common::Angle(offsetDeg)) + ->_exportToPROJString(formatter); + formatter->stopInversion(); + } + + targetCRSGeog->addAngularUnitConvertAndAxisSwap(formatter); + + return; + } + + if (exportToPROJStringGeneric(formatter)) { + return; + } + + throw io::FormattingException("Unimplemented"); +} + +} // namespace crs +NS_PROJ_END diff --git a/src/iso19111/operation/vectorofvaluesparams.cpp b/src/iso19111/operation/vectorofvaluesparams.cpp new file mode 100644 index 0000000000..e70ecced19 --- /dev/null +++ b/src/iso19111/operation/vectorofvaluesparams.cpp @@ -0,0 +1,116 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#include "vectorofvaluesparams.hpp" + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +static std::vector buildParameterValueFromMeasure( + const std::initializer_list &list) { + std::vector res; + for (const auto &v : list) { + res.emplace_back(ParameterValue::create(v)); + } + return res; +} + +VectorOfValues::VectorOfValues(std::initializer_list list) + : std::vector(buildParameterValueFromMeasure(list)) {} + +// This way, we disable inlining of destruction, and save a lot of space +VectorOfValues::~VectorOfValues() = default; + +VectorOfValues createParams(const common::Measure &m1, + const common::Measure &m2, + const common::Measure &m3) { + return VectorOfValues{ParameterValue::create(m1), + ParameterValue::create(m2), + ParameterValue::create(m3)}; +} + +VectorOfValues createParams(const common::Measure &m1, + const common::Measure &m2, + const common::Measure &m3, + const common::Measure &m4) { + return VectorOfValues{ + ParameterValue::create(m1), ParameterValue::create(m2), + ParameterValue::create(m3), ParameterValue::create(m4)}; +} + +VectorOfValues createParams(const common::Measure &m1, + const common::Measure &m2, + const common::Measure &m3, + const common::Measure &m4, + const common::Measure &m5) { + return VectorOfValues{ + ParameterValue::create(m1), ParameterValue::create(m2), + ParameterValue::create(m3), ParameterValue::create(m4), + ParameterValue::create(m5), + }; +} + +VectorOfValues +createParams(const common::Measure &m1, const common::Measure &m2, + const common::Measure &m3, const common::Measure &m4, + const common::Measure &m5, const common::Measure &m6) { + return VectorOfValues{ + ParameterValue::create(m1), ParameterValue::create(m2), + ParameterValue::create(m3), ParameterValue::create(m4), + ParameterValue::create(m5), ParameterValue::create(m6), + }; +} + +VectorOfValues +createParams(const common::Measure &m1, const common::Measure &m2, + const common::Measure &m3, const common::Measure &m4, + const common::Measure &m5, const common::Measure &m6, + const common::Measure &m7) { + return VectorOfValues{ + ParameterValue::create(m1), ParameterValue::create(m2), + ParameterValue::create(m3), ParameterValue::create(m4), + ParameterValue::create(m5), ParameterValue::create(m6), + ParameterValue::create(m7), + }; +} + +// This way, we disable inlining of destruction, and save a lot of space +VectorOfParameters::~VectorOfParameters() = default; + +//! @endcond + +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END diff --git a/src/iso19111/operation/vectorofvaluesparams.hpp b/src/iso19111/operation/vectorofvaluesparams.hpp new file mode 100644 index 0000000000..6f12543bb9 --- /dev/null +++ b/src/iso19111/operation/vectorofvaluesparams.hpp @@ -0,0 +1,101 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#ifndef VECTOROFVALUESPARAMS_HPP +#define VECTOROFVALUESPARAMS_HPP + +#include "proj/coordinateoperation.hpp" +#include "proj/util.hpp" + +// --------------------------------------------------------------------------- + +NS_PROJ_START +namespace operation { + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress + +struct VectorOfValues : public std::vector { + VectorOfValues() : std::vector() {} + explicit VectorOfValues(std::initializer_list list) + : std::vector(list) {} + + explicit VectorOfValues(std::initializer_list list); + VectorOfValues(const VectorOfValues &) = delete; + VectorOfValues(VectorOfValues &&) = default; + + ~VectorOfValues(); +}; + +VectorOfValues createParams(const common::Measure &m1, + const common::Measure &m2, + const common::Measure &m3); + +VectorOfValues createParams(const common::Measure &m1, + const common::Measure &m2, + const common::Measure &m3, + const common::Measure &m4); + +VectorOfValues createParams(const common::Measure &m1, + const common::Measure &m2, + const common::Measure &m3, + const common::Measure &m4, + const common::Measure &m5); + +VectorOfValues +createParams(const common::Measure &m1, const common::Measure &m2, + const common::Measure &m3, const common::Measure &m4, + const common::Measure &m5, const common::Measure &m6); + +VectorOfValues +createParams(const common::Measure &m1, const common::Measure &m2, + const common::Measure &m3, const common::Measure &m4, + const common::Measure &m5, const common::Measure &m6, + const common::Measure &m7); + +// --------------------------------------------------------------------------- + +struct VectorOfParameters : public std::vector { + VectorOfParameters() : std::vector() {} + explicit VectorOfParameters( + std::initializer_list list) + : std::vector(list) {} + VectorOfParameters(const VectorOfParameters &) = delete; + + ~VectorOfParameters(); +}; + +//! @endcond + +// --------------------------------------------------------------------------- + +} // namespace operation +NS_PROJ_END + +#endif // VECTOROFVALUESPARAMS_HPP diff --git a/src/iso19111/static.cpp b/src/iso19111/static.cpp index 11306e3504..d099cd6c10 100644 --- a/src/iso19111/static.cpp +++ b/src/iso19111/static.cpp @@ -39,6 +39,7 @@ #include "proj/metadata.hpp" #include "proj/util.hpp" +#include "operation/oputils.hpp" #include "proj/internal/coordinatesystem_internal.hpp" #include "proj/internal/io_internal.hpp" @@ -653,6 +654,17 @@ const GeographicCRSNNPtr const std::string operation::CoordinateOperation::OPERATION_VERSION_KEY("operationVersion"); +//! @cond Doxygen_Suppress +const common::Measure operation::nullMeasure{}; + +const std::string operation::INVERSE_OF = "Inverse of "; + +const std::string operation::AXIS_ORDER_CHANGE_2D_NAME = + "axis order change (2D)"; +const std::string operation::AXIS_ORDER_CHANGE_3D_NAME = + "axis order change (geographic3D horizontal)"; +//! @endcond + // --------------------------------------------------------------------------- NS_PROJ_END diff --git a/src/lib_proj.cmake b/src/lib_proj.cmake index 67bc1f4e4e..cf30734e57 100644 --- a/src/lib_proj.cmake +++ b/src/lib_proj.cmake @@ -198,11 +198,20 @@ set(SRC_LIBPROJ_ISO19111 iso19111/crs.cpp iso19111/datum.cpp iso19111/coordinatesystem.cpp - iso19111/coordinateoperation.cpp iso19111/io.cpp iso19111/internal.cpp iso19111/factory.cpp iso19111/c_api.cpp + iso19111/operation/concatenatedoperation.cpp + iso19111/operation/coordinateoperationfactory.cpp + iso19111/operation/conversion.cpp + iso19111/operation/esriparammappings.cpp + iso19111/operation/oputils.cpp + iso19111/operation/parammappings.cpp + iso19111/operation/projbasedoperation.cpp + iso19111/operation/singleoperation.cpp + iso19111/operation/transformation.cpp + iso19111/operation/vectorofvaluesparams.cpp ) set(SRC_LIBPROJ_CORE diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 2c0c19a99a..60255abf9e 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -132,6 +132,7 @@ add_executable(proj_test_cpp_api test_metadata.cpp test_io.cpp test_operation.cpp + test_operationfactory.cpp test_datum.cpp test_factory.cpp test_c_api.cpp diff --git a/test/unit/Makefile.am b/test/unit/Makefile.am index 483cb0bde1..5d6ab1d84a 100644 --- a/test/unit/Makefile.am +++ b/test/unit/Makefile.am @@ -51,7 +51,18 @@ proj_context_test_LDADD = ../../src/libproj.la @GTEST_LIBS@ proj_context_test-check: proj_context_test PROJ_SKIP_READ_USER_WRITABLE_DIRECTORY=YES ./proj_context_test -test_cpp_api_SOURCES = test_util.cpp test_common.cpp test_crs.cpp test_metadata.cpp test_io.cpp test_operation.cpp test_datum.cpp test_factory.cpp test_c_api.cpp test_grids.cpp main.cpp +test_cpp_api_SOURCES = test_util.cpp \ + test_common.cpp \ + test_crs.cpp \ + test_metadata.cpp \ + test_io.cpp \ + test_operation.cpp \ + test_operationfactory.cpp \ + test_datum.cpp \ + test_factory.cpp \ + test_c_api.cpp \ + test_grids.cpp \ + main.cpp test_cpp_api_LDADD = ../../src/libproj.la @GTEST_LIBS@ @SQLITE3_LIBS@ test_cpp_api-check: test_cpp_api diff --git a/test/unit/test_operation.cpp b/test/unit/test_operation.cpp index 9bf883d990..32f968eca5 100644 --- a/test/unit/test_operation.cpp +++ b/test/unit/test_operation.cpp @@ -28,8 +28,6 @@ #include "gtest_include.h" -#include "test_primitives.hpp" - // to be able to use internal::replaceAll #ifndef FROM_PROJ_CPP #define FROM_PROJ_CPP @@ -4285,5819 +4283,6 @@ TEST(operation, PROJ_based_with_global_parameters) { // --------------------------------------------------------------------------- -TEST(operation, geogCRS_to_geogCRS) { - - auto op = CoordinateOperationFactory::create()->createOperation( - GeographicCRS::EPSG_4807, GeographicCRS::EPSG_4326); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ( - op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv +proj=longlat " - "+ellps=clrk80ign +pm=paris +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_default) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setAllowUseIntermediateCRS( - CoordinateOperationContext::IntermediateCRSUse::NEVER); - - // Directly found in database - { - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4179"), // Pulkovo 42 - authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 - ctxt); - ASSERT_EQ(list.size(), 3U); - // Romania has a larger area than Poland (given our approx formula) - EXPECT_EQ(list[0]->getEPSGCode(), 15994); // Romania - 3m - EXPECT_EQ(list[1]->getEPSGCode(), 1644); // Poland - 1m - EXPECT_EQ(list[2]->nameStr(), - "Ballpark geographic offset from Pulkovo 1942(58) to ETRS89"); - - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 " - "+step +proj=cart +ellps=krass +step +proj=helmert +x=2.3287 " - "+y=-147.0425 +z=-92.0802 +rx=0.3092483 +ry=-0.32482185 " - "+rz=-0.49729934 +s=5.68906266 +convention=coordinate_frame +step " - "+inv +proj=cart +ellps=GRS80 +step +proj=pop +v_3 +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " - "+order=2,1"); - } - - // Reverse case - { - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4258"), - authFactory->createCoordinateReferenceSystem("4179"), ctxt); - ASSERT_EQ(list.size(), 3U); - // Romania has a larger area than Poland (given our approx formula) - EXPECT_EQ(list[0]->nameStr(), - "Inverse of Pulkovo 1942(58) to ETRS89 (4)"); // Romania - 3m - - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 " - "+step +proj=cart +ellps=GRS80 +step +inv +proj=helmert +x=2.3287 " - "+y=-147.0425 +z=-92.0802 +rx=0.3092483 +ry=-0.32482185 " - "+rz=-0.49729934 +s=5.68906266 +convention=coordinate_frame +step " - "+inv +proj=cart +ellps=krass +step +proj=pop +v_3 +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " - "+order=2,1"); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_match_by_name) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setAllowUseIntermediateCRS( - CoordinateOperationContext::IntermediateCRSUse::NEVER); - auto NAD27 = GeographicCRS::create( - PropertyMap().set(IdentifiedObject::NAME_KEY, - GeographicCRS::EPSG_4267->nameStr()), - GeographicCRS::EPSG_4267->datum(), - GeographicCRS::EPSG_4267->datumEnsemble(), - GeographicCRS::EPSG_4267->coordinateSystem()); - auto list = CoordinateOperationFactory::create()->createOperations( - NAD27, GeographicCRS::EPSG_4326, ctxt); - auto listInv = CoordinateOperationFactory::create()->createOperations( - GeographicCRS::EPSG_4326, NAD27, ctxt); - auto listRef = CoordinateOperationFactory::create()->createOperations( - GeographicCRS::EPSG_4267, GeographicCRS::EPSG_4326, ctxt); - EXPECT_EQ(list.size(), listRef.size()); - EXPECT_EQ(listInv.size(), listRef.size()); - EXPECT_GE(listRef.size(), 2U); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_filter_accuracy) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 1.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4179"), - authFactory->createCoordinateReferenceSystem("4258"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->getEPSGCode(), 1644); // Poland - 1m - } - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.9); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4179"), - authFactory->createCoordinateReferenceSystem("4258"), ctxt); - ASSERT_EQ(list.size(), 0U); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_filter_bbox) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - // INSERT INTO "area" VALUES('EPSG','1197','Romania','Romania - onshore and - // offshore.',43.44,48.27,20.26,31.41,0); - { - auto ctxt = CoordinateOperationContext::create( - authFactory, Extent::createFromBBOX(20.26, 43.44, 31.41, 48.27), - 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4179"), - authFactory->createCoordinateReferenceSystem("4258"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->getEPSGCode(), 15994); // Romania - 3m - } - { - auto ctxt = CoordinateOperationContext::create( - authFactory, Extent::createFromBBOX(20.26 + .1, 43.44 + .1, - 31.41 - .1, 48.27 - .1), - 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4179"), - authFactory->createCoordinateReferenceSystem("4258"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->getEPSGCode(), 15994); // Romania - 3m - } - { - auto ctxt = CoordinateOperationContext::create( - authFactory, Extent::createFromBBOX(20.26 - .1, 43.44 - .1, - 31.41 + .1, 48.27 + .1), - 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4179"), - authFactory->createCoordinateReferenceSystem("4258"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=noop"); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_incompatible_area) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4267"), // NAD27 - authFactory->createCoordinateReferenceSystem("4258"), // ETRS 89 - ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=noop"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_inverse_needed) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setUsePROJAlternativeGridNames(false); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4275"), // NTF - authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 - ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 " - "+step +proj=cart +ellps=clrk80ign +step +proj=helmert +x=-168 " - "+y=-60 +z=320 +step +inv +proj=cart +ellps=GRS80 +step +proj=pop " - "+v_3 +step +proj=unitconvert +xy_in=rad +xy_out=deg +step " - "+proj=axisswap +order=2,1"); - EXPECT_EQ(list[1]->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step " - "+proj=hgridshift +grids=fr_ign_ntf_r93.tif +step " - "+proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); - } - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4275"), // NTF - authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 - ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step " - "+proj=hgridshift +grids=fr_ign_ntf_r93.tif +step " - "+proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); - } - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 - authFactory->createCoordinateReferenceSystem("4275"), // NTF - ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +inv " - "+proj=hgridshift +grids=fr_ign_ntf_r93.tif +step " - "+proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_ntv1_ntv2_ctable2) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4267"), // NAD27 - authFactory->createCoordinateReferenceSystem("4269"), // NAD83 - ctxt); - ASSERT_EQ(list.size(), 10U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=hgridshift " - "+grids=ca_nrc_ntv1_can.tif +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); - EXPECT_EQ(list[1]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=hgridshift " - "+grids=ca_nrc_ntv2_0.tif +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); - EXPECT_EQ(list[2]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=hgridshift " - "+grids=us_noaa_conus.tif +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_NAD27_to_WGS84) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4267"), // NAD27 - authFactory->createCoordinateReferenceSystem("4326"), // WGS84 - ctxt); - ASSERT_EQ(list.size(), 79U); - EXPECT_EQ(list[0]->nameStr(), - "NAD27 to WGS 84 (33)"); // 1.0 m, Canada - NAD27 - EXPECT_EQ(list[1]->nameStr(), - "NAD27 to WGS 84 (3)"); // 20.0 m, Canada - NAD27 - EXPECT_EQ(list[2]->nameStr(), - "NAD27 to WGS 84 (79)"); // 5.0 m, USA - CONUS including EEZ - EXPECT_EQ(list[3]->nameStr(), - "NAD27 to WGS 84 (4)"); // 10.0 m, USA - CONUS - onshore -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_NAD27_to_WGS84_G1762) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), std::string()); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - - auto authFactoryEPSG = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto list = CoordinateOperationFactory::create()->createOperations( - // NAD27 - authFactoryEPSG->createCoordinateReferenceSystem("4267"), - // WGS84 (G1762) - authFactoryEPSG->createCoordinateReferenceSystem("9057"), ctxt); - ASSERT_GE(list.size(), 78U); - EXPECT_EQ(list[0]->nameStr(), - "NAD27 to WGS 84 (33) + WGS 84 to WGS 84 (G1762)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=hgridshift +grids=ca_nrc_ntv2_0.tif " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - EXPECT_EQ(list[1]->nameStr(), - "NAD27 to WGS 84 (3) + WGS 84 to WGS 84 (G1762)"); - EXPECT_EQ(list[2]->nameStr(), - "NAD27 to WGS 84 (79) + WGS 84 to WGS 84 (G1762)"); - EXPECT_EQ(list[3]->nameStr(), - "NAD27 to WGS 84 (4) + WGS 84 to WGS 84 (G1762)"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_WGS84_G1674_to_WGS84_G1762) { - // Check that particular behavior with WGS 84 (Gxxx) related to - // 'geodetic_datum_preferred_hub' table and custom no-op transformations - // between WGS 84 and WGS 84 (Gxxx) doesn't affect direct transformations - // to those realizations. - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), std::string()); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - - auto authFactoryEPSG = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto list = CoordinateOperationFactory::create()->createOperations( - // WGS84 (G1674) - authFactoryEPSG->createCoordinateReferenceSystem("9056"), - // WGS84 (G1762) - authFactoryEPSG->createCoordinateReferenceSystem("9057"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=cart +ellps=WGS84 " - "+step +proj=helmert +x=-0.004 +y=0.003 +z=0.004 +rx=0.00027 " - "+ry=-0.00027 +rz=0.00038 +s=-0.0069 " - "+convention=coordinate_frame " - "+step +inv +proj=cart +ellps=WGS84 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_EPSG_4240_Indian1975_to_EPSG_4326) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4240"), // Indian 1975 - authFactory->createCoordinateReferenceSystem("4326"), ctxt); - ASSERT_EQ(list.size(), 3U); - - // Indian 1975 to WGS 84 (4), 3.0 m, Thailand - onshore - EXPECT_EQ(list[0]->getEPSGCode(), 1812); - - // The following is the one we want to see. It has a lesser accuracy than - // the above one and the same bbox, but the name of its area of use is - // slightly different - // Indian 1975 to WGS 84 (2), 5.0 m, Thailand - onshore and Gulf of Thailand - EXPECT_EQ(list[1]->getEPSGCode(), 1304); - - // Indian 1975 to WGS 84 (3), 1.0 m, Thailand - Bongkot field - EXPECT_EQ(list[2]->getEPSGCode(), 1537); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_helmert_geog3D_crs) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); - - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4939"), // GDA94 3D - authFactory->createCoordinateReferenceSystem("7843"), // GDA2020 3D - ctxt); - ASSERT_EQ(list.size(), 1U); - - // Check there is no push / pop of v_3 - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +z_in=m +xy_out=rad +z_out=m " - "+step +proj=cart +ellps=GRS80 " - "+step +proj=helmert +x=0.06155 +y=-0.01087 +z=-0.04019 " - "+rx=-0.0394924 +ry=-0.0327221 +rz=-0.0328979 +s=-0.009994 " - "+convention=coordinate_frame " - "+step +inv +proj=cart +ellps=GRS80 " - "+step +proj=unitconvert +xy_in=rad +z_in=m +xy_out=deg +z_out=m " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_helmert_geocentric_3D) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); - - auto list = CoordinateOperationFactory::create()->createOperations( - // GDA94 geocentric - authFactory->createCoordinateReferenceSystem("4348"), - // GDA2020 geocentric - authFactory->createCoordinateReferenceSystem("7842"), ctxt); - ASSERT_EQ(list.size(), 1U); - - // Check there is no push / pop of v_3 - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=helmert +x=0.06155 +y=-0.01087 +z=-0.04019 " - "+rx=-0.0394924 +ry=-0.0327221 +rz=-0.0328979 +s=-0.009994 " - "+convention=coordinate_frame"); - EXPECT_EQ(list[0]->inverse()->exportToPROJString( - PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +inv +proj=helmert +x=0.06155 +y=-0.01087 +z=-0.04019 " - "+rx=-0.0394924 +ry=-0.0327221 +rz=-0.0328979 +s=-0.009994 " - "+convention=coordinate_frame"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_helmert_geog3D_to_geocentirc) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); - - auto list = CoordinateOperationFactory::create()->createOperations( - // GDA94 3D - authFactory->createCoordinateReferenceSystem("4939"), - // GDA2020 geocentric - authFactory->createCoordinateReferenceSystem("7842"), ctxt); - ASSERT_EQ(list.size(), 1U); - - // Check there is no push / pop of v_3 - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +z_in=m +xy_out=rad +z_out=m " - "+step +proj=cart +ellps=GRS80 " - "+step +proj=helmert +x=0.06155 +y=-0.01087 +z=-0.04019 " - "+rx=-0.0394924 +ry=-0.0327221 +rz=-0.0328979 +s=-0.009994 " - "+convention=coordinate_frame"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_invalid_EPSG_ID) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); - // EPSG:4656 is incorrect. Should be EPSG:8997 - auto obj = WKTParser().createFromWKT( - "GEOGCS[\"ITRF2000\"," - "DATUM[\"International_Terrestrial_Reference_Frame_2000\"," - "SPHEROID[\"GRS 1980\",6378137,298.257222101," - "AUTHORITY[\"EPSG\",\"7019\"]],AUTHORITY[\"EPSG\",\"6656\"]]," - "PRIMEM[\"Greenwich\",0],UNIT[\"Degree\",0.0174532925199433]," - "AUTHORITY[\"EPSG\",\"4656\"]]"); - auto crs = nn_dynamic_pointer_cast(obj); - ASSERT_TRUE(crs != nullptr); - - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(crs), GeographicCRS::EPSG_4326, ctxt); - ASSERT_EQ(list.size(), 1U); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_datum_ensemble) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); - - auto dst_wkt = - "GEOGCRS[\"unknown\"," - " ENSEMBLE[\"World Geodetic System 1984 ensemble\"," - " MEMBER[\"World Geodetic System 1984 (Transit)\"," - " ID[\"EPSG\",1166]]," - " MEMBER[\"World Geodetic System 1984 (G730)\"," - " ID[\"EPSG\",1152]]," - " MEMBER[\"World Geodetic System 1984 (G873)\"," - " ID[\"EPSG\",1153]]," - " MEMBER[\"World Geodetic System 1984 (G1150)\"," - " ID[\"EPSG\",1154]]," - " MEMBER[\"World Geodetic System 1984 (G1674)\"," - " ID[\"EPSG\",1155]]," - " MEMBER[\"World Geodetic System 1984 (G1762)\"," - " ID[\"EPSG\",1156]]," - " ELLIPSOID[\"WGS 84\",6378137,298.257223563," - " LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]," - " ID[\"EPSG\",7030]]," - " ENSEMBLEACCURACY[2]]," - " PRIMEM[\"Greenwich\",0," - " ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9102]]," - " ID[\"EPSG\",8901]]," - " CS[ellipsoidal,2," - " ID[\"EPSG\",6422]]," - " AXIS[\"Geodetic latitude (Lat)\",north," - " ORDER[1]]," - " AXIS[\"Geodetic longitude (Lon)\",east," - " ORDER[2]]," - " ANGLEUNIT[\"degree (supplier to define representation)\"," - "0.0174532925199433,ID[\"EPSG\",9122]]]"; - auto dstObj = WKTParser().createFromWKT(dst_wkt); - auto dstCRS = nn_dynamic_pointer_cast(dstObj); - ASSERT_TRUE(dstCRS != nullptr); - - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 - NN_NO_CHECK(dstCRS), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), "ETRS89 to WGS 84 (1)"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, vertCRS_to_geogCRS_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setUsePROJAlternativeGridNames(false); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem( - "3855"), // EGM2008 height - authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 - ctxt); - ASSERT_EQ(list.size(), 3U); - EXPECT_EQ( - list[1]->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=us_nga_egm08_25.tif +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - } - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem( - "3855"), // EGM2008 height - authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 - ctxt); - ASSERT_EQ(list.size(), 3U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=us_nga_egm08_25.tif +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - } - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 - authFactory->createCoordinateReferenceSystem( - "3855"), // EGM2008 height - ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +inv +proj=vgridshift +grids=us_nga_egm08_25.tif " - "+multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - } - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - // NGVD29 depth (ftUS) - authFactory->createCoordinateReferenceSystem("6359"), - authFactory->createCoordinateReferenceSystem("4326"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=affine +s33=-0.304800609601219"); - } - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - // NZVD2016 height - authFactory->createCoordinateReferenceSystem("7839"), - // NZGD2000 - authFactory->createCoordinateReferenceSystem("4959"), ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=nz_linz_nzgeoid2016.tif " - "+multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - } - { - // Test actually the database where we derive records using the more - // classic 'Geographic3D to GravityRelatedHeight' method from - // records using EPSG:9635 - //'Geog3D to Geog2D+GravityRelatedHeight (US .gtx)' method - auto ctxt = CoordinateOperationContext::create( - AuthorityFactory::create(DatabaseContext::create(), std::string()), - nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - // Baltic 1957 height - authFactory->createCoordinateReferenceSystem("8357"), - // ETRS89 - authFactory->createCoordinateReferenceSystem("4937"), ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift " - "+grids=sk_gku_Slovakia_ETRS89h_to_Baltic1957.tif " - "+multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, geog3DCRS_to_geog2DCRS_plus_vertCRS_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - // ETRS89 (3D) - authFactory->createCoordinateReferenceSystem("4937"), - // ETRS89 + Baltic 1957 height - authFactory->createCoordinateReferenceSystem("8360"), ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +inv +proj=vgridshift " - "+grids=sk_gku_Slovakia_ETRS89h_to_Baltic1957.tif +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - - EXPECT_EQ(list[0]->inverse()->nameStr(), - "Inverse of 'ETRS89 to ETRS89 + Baltic 1957 height (1)'"); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_noop) { - - auto op = CoordinateOperationFactory::create()->createOperation( - GeographicCRS::EPSG_4326, GeographicCRS::EPSG_4326); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->nameStr(), "Null geographic offset from WGS 84 to WGS 84"); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=noop"); - EXPECT_EQ(op->inverse()->nameStr(), op->nameStr()); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_longitude_rotation) { - - auto src = GeographicCRS::create( - PropertyMap().set(IdentifiedObject::NAME_KEY, "A"), - GeodeticReferenceFrame::create(PropertyMap(), Ellipsoid::WGS84, - optional(), - PrimeMeridian::GREENWICH), - EllipsoidalCS::createLatitudeLongitude(UnitOfMeasure::DEGREE)); - auto dest = GeographicCRS::create( - PropertyMap().set(IdentifiedObject::NAME_KEY, "B"), - GeodeticReferenceFrame::create(PropertyMap(), Ellipsoid::WGS84, - optional(), - PrimeMeridian::PARIS), - EllipsoidalCS::createLatitudeLongitude(UnitOfMeasure::DEGREE)); - - auto op = CoordinateOperationFactory::create()->createOperation(src, 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 +proj=longlat " - "+ellps=WGS84 +pm=paris +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); - EXPECT_EQ(op->inverse()->exportToWKT(WKTFormatter::create().get()), - CoordinateOperationFactory::create() - ->createOperation(dest, src) - ->exportToWKT(WKTFormatter::create().get())); - EXPECT_TRUE( - op->inverse()->isEquivalentTo(CoordinateOperationFactory::create() - ->createOperation(dest, src) - .get())); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_longitude_rotation_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4807"), // NTF(Paris) - authFactory->createCoordinateReferenceSystem("4275"), // NTF - ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ(list[0]->nameStr(), "NTF (Paris) to NTF (1)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " - "+proj=longlat +ellps=clrk80ign +pm=paris +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " - "+order=2,1"); - EXPECT_EQ(list[1]->nameStr(), "NTF (Paris) to NTF (2)"); - EXPECT_EQ(list[1]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " - "+proj=longlat +ellps=clrk80ign +pm=2.33720833333333 +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " - "+order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_concatenated_operation) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setAllowUseIntermediateCRS( - CoordinateOperationContext::IntermediateCRSUse::ALWAYS); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4807"), // NTF(Paris) - authFactory->createCoordinateReferenceSystem("4171"), // RGF93 - ctxt); - ASSERT_EQ(list.size(), 4U); - - EXPECT_EQ(list[0]->nameStr(), "NTF (Paris) to RGF93 (1)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=grad +xy_out=rad " - "+step +inv +proj=longlat +ellps=clrk80ign +pm=paris " - "+step +proj=push +v_3 " - "+step +proj=cart +ellps=clrk80ign " - "+step +proj=xyzgridshift +grids=fr_ign_gr3df97a.tif " - "+grid_ref=output_crs +ellps=GRS80 " - "+step +inv +proj=cart +ellps=GRS80 " - "+step +proj=pop +v_3 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - - EXPECT_EQ(list[1]->nameStr(), "NTF (Paris) to RGF93 (2)"); - EXPECT_EQ(list[1]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " - "+proj=longlat +ellps=clrk80ign +pm=paris +step +proj=hgridshift " - "+grids=fr_ign_ntf_r93.tif +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); - - EXPECT_TRUE(nn_dynamic_pointer_cast(list[0]) != - nullptr); - auto grids = list[0]->gridsNeeded(DatabaseContext::create(), false); - EXPECT_EQ(grids.size(), 1U); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_ED50_to_WGS72_no_NTF_intermediate) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4230"), // ED50 - authFactory->createCoordinateReferenceSystem("4322"), // WGS 72 - ctxt); - ASSERT_GE(list.size(), 2U); - // We should not use the ancient NTF as an intermediate when looking for - // ED50 -> WGS 72 operations. - for (const auto &op : list) { - EXPECT_TRUE(op->nameStr().find("NTF") == std::string::npos) - << op->nameStr(); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_context_same_grid_name) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4314"), // DHDN - authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 - ctxt); - ASSERT_TRUE(!list.empty()); - EXPECT_EQ(list[0]->nameStr(), "DHDN to ETRS89 (8)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=hgridshift " - "+grids=de_adv_BETA2007.tif +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_geographic_offset_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4120"), // NTF(Paris) - authFactory->createCoordinateReferenceSystem("4121"), // NTF - ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), "Greek to GGRS87 (1)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " - "+dlat=-5.86 +dlon=0.28 +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_CH1903_to_CH1903plus_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setAllowUseIntermediateCRS( - CoordinateOperationContext::IntermediateCRSUse::ALWAYS); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4149"), // CH1903 - authFactory->createCoordinateReferenceSystem("4150"), // CH1903+ - ctxt); - ASSERT_TRUE(list.size() == 1U); - - EXPECT_EQ(list[0]->nameStr(), "CH1903 to CH1903+ (1)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=hgridshift +grids=ch_swisstopo_CHENyx06a.tif " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -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(sourceCRS_obj); - ASSERT_TRUE(sourceCRS != nullptr); - - auto targetCRS_obj = PROJStringParser() - .attachDatabaseContext(dbContext) - .setUsePROJ4InitRules(true) - .createFromPROJString("+init=IGNF:RGF93G"); - auto targetCRS = nn_dynamic_pointer_cast(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(), 2U); - - EXPECT_EQ(list[0]->nameStr(), - "NOUVELLE TRIANGULATION DE LA FRANCE (NTF) vers RGF93 (ETRS89)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=hgridshift +grids=fr_ign_ntf_r93.tif +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geogCRS_3D) { - - auto geogcrs_m_obj = PROJStringParser().createFromPROJString( - "+proj=longlat +vunits=m +type=crs"); - auto geogcrs_m = nn_dynamic_pointer_cast(geogcrs_m_obj); - ASSERT_TRUE(geogcrs_m != nullptr); - - auto geogcrs_ft_obj = PROJStringParser().createFromPROJString( - "+proj=longlat +vunits=ft +type=crs"); - auto geogcrs_ft = nn_dynamic_pointer_cast(geogcrs_ft_obj); - ASSERT_TRUE(geogcrs_ft != nullptr); - - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(geogcrs_m), NN_CHECK_ASSERT(geogcrs_ft)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=unitconvert +z_in=m +z_out=ft"); - } - - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(geogcrs_ft), NN_CHECK_ASSERT(geogcrs_m)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=unitconvert +z_in=ft +z_out=m"); - } - - auto geogcrs_m_with_pm_obj = PROJStringParser().createFromPROJString( - "+proj=longlat +pm=paris +vunits=m +type=crs"); - auto geogcrs_m_with_pm = - nn_dynamic_pointer_cast(geogcrs_m_with_pm_obj); - ASSERT_TRUE(geogcrs_m_with_pm != nullptr); - - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(geogcrs_m_with_pm), NN_CHECK_ASSERT(geogcrs_ft)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=unitconvert +xy_in=deg +z_in=m " - "+xy_out=rad +z_out=m +step +inv +proj=longlat +ellps=WGS84 " - "+pm=paris +step +proj=unitconvert +xy_in=rad +z_in=m " - "+xy_out=deg +z_out=ft"); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_3D_lat_long_non_metre_to_geogCRS_longlat) { - - auto wkt = "GEOGCRS[\"my CRS\",\n" - " DATUM[\"World Geodetic System 1984\",\n" - " ELLIPSOID[\"WGS 84\",6378137,298.257223563],\n" - " ID[\"EPSG\",6326]],\n" - " CS[ellipsoidal,3],\n" - " AXIS[\"latitude\",north,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " AXIS[\"longitude\",east,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " AXIS[\"ellipsoidal height\",up,\n" - " LENGTHUNIT[\"my_vunit\",0.3]]]"; - auto srcCRS_obj = WKTParser().createFromWKT(wkt); - auto srcCRS = nn_dynamic_pointer_cast(srcCRS_obj); - ASSERT_TRUE(srcCRS != nullptr); - - auto dstCRS_obj = PROJStringParser().createFromPROJString( - "+proj=longlat +datum=WGS84 +type=crs"); - auto dstCRS = nn_dynamic_pointer_cast(dstCRS_obj); - ASSERT_TRUE(dstCRS != nullptr); - - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(srcCRS), NN_CHECK_ASSERT(dstCRS)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +z_in=0.3 +z_out=m"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_without_id_to_geogCRS_3D_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto src = - authFactory->createCoordinateReferenceSystem("4289"); // Amersfoort - auto dst = - authFactory->createCoordinateReferenceSystem("4937"); // ETRS89 3D - auto list = - CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); - ASSERT_GE(list.size(), 1U); - auto wkt2 = "GEOGCRS[\"unnamed\",\n" - " DATUM[\"Amersfoort\",\n" - " ELLIPSOID[\"Bessel 1841\",6377397.155,299.1528128,\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " CS[ellipsoidal,2],\n" - " AXIS[\"geodetic latitude (Lat)\",north,\n" - " ORDER[1],\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " AXIS[\"geodetic longitude (Lon)\",east,\n" - " ORDER[2],\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]]," - " USAGE[\n" - " SCOPE[\"unknown\"],\n" - " AREA[\"Netherlands - onshore\"],\n" - " BBOX[50.75,3.2,53.7,7.22]]]\n"; - - auto obj = WKTParser().createFromWKT(wkt2); - auto src_from_wkt2 = nn_dynamic_pointer_cast(obj); - ASSERT_TRUE(src_from_wkt2 != nullptr); - auto list2 = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(src_from_wkt2), dst, ctxt); - ASSERT_GE(list.size(), list2.size()); - for (size_t i = 0; i < list.size(); i++) { - const auto &op = list[i]; - const auto &op2 = list2[i]; - EXPECT_TRUE( - op->isEquivalentTo(op2.get(), IComparable::Criterion::EQUIVALENT)) - << op->nameStr() << " " << op2->nameStr(); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, geocentricCRS_to_geogCRS_same_datum) { - - auto op = CoordinateOperationFactory::create()->createOperation( - createGeocentricDatumWGS84(), GeographicCRS::EPSG_4326); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " - "+order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geocentricCRS_to_geogCRS_different_datum) { - - auto op = CoordinateOperationFactory::create()->createOperation( - createGeocentricDatumWGS84(), GeographicCRS::EPSG_4269); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->nameStr(), - "Ballpark geocentric translation from WGS 84 to NAD83 " - "(geocentric) + Conversion from NAD83 " - "(geocentric) to NAD83"); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=cart +ellps=GRS80 +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " - "+order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geocentricCRS_different_datum) { - - auto op = CoordinateOperationFactory::create()->createOperation( - GeographicCRS::EPSG_4269, createGeocentricDatumWGS84()); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->nameStr(), "Conversion from NAD83 to NAD83 (geocentric) + " - "Ballpark geocentric translation from NAD83 " - "(geocentric) to WGS 84"); - 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 +proj=cart " - "+ellps=GRS80"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geocentricCRS_to_geocentricCRS_same_noop) { - - auto op = CoordinateOperationFactory::create()->createOperation( - createGeocentricDatumWGS84(), createGeocentricDatumWGS84()); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->nameStr(), - "Null geocentric translation from WGS 84 to WGS 84"); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=noop"); - EXPECT_EQ(op->inverse()->nameStr(), op->nameStr()); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geocentricCRS_to_geocentricCRS_different_ballpark) { - - PropertyMap propertiesCRS; - propertiesCRS.set(Identifier::CODESPACE_KEY, "EPSG") - .set(Identifier::CODE_KEY, 4328) - .set(IdentifiedObject::NAME_KEY, "unknown"); - auto otherGeocentricCRS = GeodeticCRS::create( - propertiesCRS, GeodeticReferenceFrame::EPSG_6269, - CartesianCS::createGeocentric(UnitOfMeasure::METRE)); - - auto op = CoordinateOperationFactory::create()->createOperation( - createGeocentricKM(), otherGeocentricCRS); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ( - op->nameStr(), - "Ballpark geocentric translation from Based on WGS 84 to unknown"); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=unitconvert +xy_in=km +z_in=km +xy_out=m +z_out=m"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geocentricCRS_to_geogCRS_same_datum_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4326"), - // WGS84 geocentric - authFactory->createCoordinateReferenceSystem("4978"), ctxt); - ASSERT_EQ(list.size(), 1U); - - EXPECT_EQ(list[0]->nameStr(), - "Conversion from WGS 84 (geog2D) to WGS 84 (geocentric)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=cart " - "+ellps=WGS84"); - - EXPECT_EQ(list[0]->inverse()->nameStr(), - "Conversion from WGS 84 (geocentric) to WGS 84 (geog2D)"); - EXPECT_EQ(list[0]->inverse()->exportToPROJString( - PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " - "+order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geocentricCRS_to_geogCRS_same_datum_context_all_auth) { - // This is to check we don't use OGC:CRS84 as a pivot - auto authFactoryEPSG = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto authFactoryAll = - AuthorityFactory::create(DatabaseContext::create(), std::string()); - auto ctxt = - CoordinateOperationContext::create(authFactoryAll, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactoryEPSG->createCoordinateReferenceSystem("4326"), - // WGS84 geocentric - authFactoryEPSG->createCoordinateReferenceSystem("4978"), ctxt); - ASSERT_EQ(list.size(), 1U); - - EXPECT_EQ(list[0]->nameStr(), - "Conversion from WGS 84 (geog2D) to WGS 84 (geocentric)"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geocentricCRS_to_geocentricCRS_different_datum_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - // ITRF2000 (geocentric) - authFactory->createCoordinateReferenceSystem("4919"), - // ITRF2005 (geocentric) - authFactory->createCoordinateReferenceSystem("4896"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), "ITRF2000 to ITRF2005 (1)"); - EXPECT_PRED_FORMAT2( - ComparePROJString, - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=helmert +x=-0.0001 " - "+y=0.0008 +z=0.0058 +rx=0 +ry=0 +rz=0 +s=-0.0004 +dx=0.0002 " - "+dy=-0.0001 +dz=0.0018 +drx=0 +dry=0 +drz=0 +ds=-8e-05 " - "+t_epoch=2000 +convention=position_vector"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_geocentricCRS_same_datum_to_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - // WGS84 geocentric - authFactory->createCoordinateReferenceSystem("4978"), - authFactory->createCoordinateReferenceSystem("4326"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), - "Conversion from WGS 84 (geocentric) to WGS 84 (geog2D)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " - "+order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - geog2D_to_geog3D_same_datum_but_with_potential_other_pivot_context) { - // Check that when going from geog2D to geog3D of same datum, we don't - // try to go through a WGS84 pivot... - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("5365"), // CR 05 2D - authFactory->createCoordinateReferenceSystem("5364"), // CR 05 3D - ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=noop"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - geogCRS_to_geogCRS_different_datum_though_geocentric_transform_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - // ITRF2000 (geog3D) - authFactory->createCoordinateReferenceSystem("7909"), - // ITRF2005 (geog3D) - authFactory->createCoordinateReferenceSystem("7910"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), - "Conversion from ITRF2000 (geog3D) to ITRF2000 (geocentric) + " - "ITRF2000 to ITRF2005 (1) + " - "Conversion from ITRF2005 (geocentric) to ITRF2005 (geog3D)"); - EXPECT_PRED_FORMAT2( - ComparePROJString, - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +z_in=m +xy_out=rad +z_out=m " - "+step +proj=cart +ellps=GRS80 +step +proj=helmert +x=-0.0001 " - "+y=0.0008 +z=0.0058 +rx=0 +ry=0 +rz=0 +s=-0.0004 +dx=0.0002 " - "+dy=-0.0001 +dz=0.0018 +drx=0 +dry=0 +drz=0 +ds=-8e-05 " - "+t_epoch=2000 +convention=position_vector +step +inv " - "+proj=cart +ellps=GRS80 +step +proj=unitconvert +xy_in=rad " - "+z_in=m +xy_out=deg +z_out=m +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_geocentricCRS_different_datum_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - // ITRF2000 (geog3D) - authFactory->createCoordinateReferenceSystem("7909"), - // ITRF2005 (geocentric) - authFactory->createCoordinateReferenceSystem("4896"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), - "Conversion from ITRF2000 (geog3D) to ITRF2000 (geocentric) + " - "ITRF2000 to ITRF2005 (1)"); - EXPECT_PRED_FORMAT2( - ComparePROJString, - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +z_in=m +xy_out=rad +z_out=m " - "+step +proj=cart +ellps=GRS80 +step +proj=helmert +x=-0.0001 " - "+y=0.0008 +z=0.0058 +rx=0 +ry=0 +rz=0 +s=-0.0004 +dx=0.0002 " - "+dy=-0.0001 +dz=0.0018 +drx=0 +dry=0 +drz=0 +ds=-8e-05 " - "+t_epoch=2000 +convention=position_vector"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geocentricCRS_to_geogCRS_different_datum_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - // ITRF2000 (geocentric) - authFactory->createCoordinateReferenceSystem("4919"), - // ITRF2005 (geog3D) - authFactory->createCoordinateReferenceSystem("7910"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), - "ITRF2000 to ITRF2005 (1) + " - "Conversion from ITRF2005 (geocentric) to ITRF2005 (geog3D)"); - EXPECT_PRED_FORMAT2( - ComparePROJString, - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=helmert +x=-0.0001 " - "+y=0.0008 +z=0.0058 +rx=0 +ry=0 +rz=0 +s=-0.0004 +dx=0.0002 " - "+dy=-0.0001 +dz=0.0018 +drx=0 +dry=0 +drz=0 +ds=-8e-05 " - "+t_epoch=2000 +convention=position_vector +step +inv " - "+proj=cart +ellps=GRS80 +step +proj=unitconvert +xy_in=rad " - "+z_in=m +xy_out=deg +z_out=m +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, esri_projectedCRS_to_geogCRS_with_ITRF_intermediate_context) { - auto dbContext = DatabaseContext::create(); - auto authFactoryEPSG = AuthorityFactory::create(dbContext, "EPSG"); - auto authFactoryESRI = AuthorityFactory::create(dbContext, "ESRI"); - auto ctxt = - CoordinateOperationContext::create(authFactoryEPSG, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - // NAD_1983_CORS96_StatePlane_North_Carolina_FIPS_3200_Ft_US (projected) - authFactoryESRI->createCoordinateReferenceSystem("103501"), - // ITRF2005 (geog3D) - authFactoryEPSG->createCoordinateReferenceSystem("7910"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), - "Inverse of NAD_1983_CORS96_StatePlane_North_Carolina_" - "FIPS_3200_Ft_US + " - "Conversion from NAD83(CORS96) (geog2D) to NAD83(CORS96) " - "(geocentric) + Inverse of ITRF2000 to NAD83(CORS96) (1) + " - "ITRF2000 to ITRF2005 (1) + " - "Conversion from ITRF2005 (geocentric) to ITRF2005 (geog3D)"); - EXPECT_PRED_FORMAT2( - ComparePROJString, - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft " - "+xy_out=m +step +inv +proj=lcc +lat_0=33.75 +lon_0=-79 " - "+lat_1=34.3333333333333 +lat_2=36.1666666666667 " - "+x_0=609601.219202438 +y_0=0 +ellps=GRS80 +step +proj=cart " - "+ellps=GRS80 +step +inv +proj=helmert +x=0.9956 +y=-1.9013 " - "+z=-0.5215 +rx=0.025915 +ry=0.009426 +rz=0.011599 +s=0.00062 " - "+dx=0.0007 +dy=-0.0007 +dz=0.0005 +drx=6.7e-05 +dry=-0.000757 " - "+drz=-5.1e-05 +ds=-0.00018 +t_epoch=1997 " - "+convention=coordinate_frame +step +proj=helmert +x=-0.0001 " - "+y=0.0008 +z=0.0058 +rx=0 +ry=0 +rz=0 +s=-0.0004 +dx=0.0002 " - "+dy=-0.0001 +dz=0.0018 +drx=0 +dry=0 +drz=0 +ds=-8e-05 " - "+t_epoch=2000 +convention=position_vector +step +inv +proj=cart " - "+ellps=GRS80 +step +proj=unitconvert +xy_in=rad +z_in=m " - "+xy_out=deg +z_out=m +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -static ProjectedCRSNNPtr createUTM31_WGS84() { - return ProjectedCRS::create( - PropertyMap(), GeographicCRS::EPSG_4326, - Conversion::createUTM(PropertyMap(), 31, true), - CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); -} - -// --------------------------------------------------------------------------- - -static ProjectedCRSNNPtr createUTM32_WGS84() { - return ProjectedCRS::create( - PropertyMap(), GeographicCRS::EPSG_4326, - Conversion::createUTM(PropertyMap(), 32, true), - CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_projCRS) { - - auto op = CoordinateOperationFactory::create()->createOperation( - GeographicCRS::EPSG_4326, createUTM31_WGS84()); - ASSERT_TRUE(op != nullptr); - EXPECT_TRUE(std::dynamic_pointer_cast(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 +proj=utm " - "+zone=31 +ellps=WGS84"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_longlat_to_geogCS_latlong) { - - auto sourceCRS = GeographicCRS::OGC_CRS84; - auto targetCRS = GeographicCRS::EPSG_4326; - auto op = CoordinateOperationFactory::create()->createOperation(sourceCRS, - targetCRS); - ASSERT_TRUE(op != nullptr); - auto conv = std::dynamic_pointer_cast(op); - ASSERT_TRUE(conv != nullptr); - EXPECT_TRUE(op->sourceCRS() && - op->sourceCRS()->isEquivalentTo(sourceCRS.get())); - EXPECT_TRUE(op->targetCRS() && - op->targetCRS()->isEquivalentTo(targetCRS.get())); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=axisswap +order=2,1"); - auto convInverse = nn_dynamic_pointer_cast(conv->inverse()); - ASSERT_TRUE(convInverse != nullptr); - EXPECT_TRUE(convInverse->sourceCRS() && - convInverse->sourceCRS()->isEquivalentTo(targetCRS.get())); - EXPECT_TRUE(convInverse->targetCRS() && - convInverse->targetCRS()->isEquivalentTo(sourceCRS.get())); - EXPECT_EQ(conv->method()->exportToWKT(WKTFormatter::create().get()), - convInverse->method()->exportToWKT(WKTFormatter::create().get())); - EXPECT_TRUE(conv->method()->isEquivalentTo(convInverse->method().get())); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_longlat_to_geogCS_latlong_database) { - - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), std::string()); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - AuthorityFactory::create(DatabaseContext::create(), "OGC") - ->createCoordinateReferenceSystem("CRS84"), - AuthorityFactory::create(DatabaseContext::create(), "EPSG") - ->createCoordinateReferenceSystem("4326"), - ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_longlat_to_projCRS) { - - auto op = CoordinateOperationFactory::create()->createOperation( - GeographicCRS::OGC_CRS84, createUTM31_WGS84()); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=utm +zone=31 +ellps=WGS84"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_different_from_baseCRS_to_projCRS) { - - auto op = CoordinateOperationFactory::create()->createOperation( - GeographicCRS::EPSG_4807, createUTM31_WGS84()); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ( - op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv +proj=longlat " - "+ellps=clrk80ign +pm=paris +step +proj=utm +zone=31 " - "+ellps=WGS84"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - geogCRS_different_from_baseCRS_to_projCRS_context_compatible_area) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setAllowUseIntermediateCRS( - CoordinateOperationContext::IntermediateCRSUse::ALWAYS); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4807"), // NTF(Paris) - authFactory->createCoordinateReferenceSystem("32631"), // UTM31 WGS84 - ctxt); - ASSERT_EQ(list.size(), 4U); - EXPECT_EQ( - list[0]->nameStr(), - "NTF (Paris) to NTF (1) + Inverse of WGS 84 to NTF (3) + UTM zone 31N"); - ASSERT_EQ(list[0]->coordinateOperationAccuracies().size(), 1U); - EXPECT_EQ(list[0]->coordinateOperationAccuracies()[0]->value(), "1"); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " - "+proj=longlat +ellps=clrk80ign +pm=paris +step +proj=hgridshift " - "+grids=fr_ign_ntf_r93.tif +step +proj=utm +zone=31 +ellps=WGS84"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geocentricCRS_to_projCRS) { - - auto op = CoordinateOperationFactory::create()->createOperation( - createGeocentricDatumWGS84(), createUTM31_WGS84()); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 +step " - "+proj=utm +zone=31 +ellps=WGS84"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_to_geogCRS) { - - auto op = CoordinateOperationFactory::create()->createOperation( - createUTM31_WGS84(), GeographicCRS::EPSG_4326); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=utm +zone=31 +ellps=WGS84 +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " - "+order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_no_id_to_geogCRS_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto src = authFactory->createCoordinateReferenceSystem( - "28992"); // Amersfoort / RD New - auto dst = - authFactory->createCoordinateReferenceSystem("4258"); // ETRS89 2D - auto list = - CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); - ASSERT_GE(list.size(), 1U); - auto wkt2 = - "PROJCRS[\"unknown\",\n" - " BASEGEOGCRS[\"Amersfoort\",\n" - " DATUM[\"Amersfoort\",\n" - " ELLIPSOID[\"Bessel 1841\",6377397.155,299.1528128]]],\n" - " CONVERSION[\"unknown\",\n" - " METHOD[\"Oblique Stereographic\"],\n" - " PARAMETER[\"Latitude of natural origin\",52.1561605555556],\n" - " PARAMETER[\"Longitude of natural origin\",5.38763888888889],\n" - " PARAMETER[\"Scale factor at natural origin\",0.9999079],\n" - " PARAMETER[\"False easting\",155000],\n" - " PARAMETER[\"False northing\",463000]],\n" - " CS[Cartesian,2],\n" - " AXIS[\"(E)\",east],\n" - " AXIS[\"(N)\",north],\n" - " LENGTHUNIT[\"metre\",1],\n" - " ID[\"EPSG\",28992]]"; - auto obj = WKTParser().createFromWKT(wkt2); - auto src_from_wkt2 = nn_dynamic_pointer_cast(obj); - ASSERT_TRUE(src_from_wkt2 != nullptr); - auto list2 = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(src_from_wkt2), dst, ctxt); - ASSERT_GE(list.size(), list2.size() - 1); - for (size_t i = 0; i < list.size(); i++) { - const auto &op = list[i]; - const auto &op2 = list2[i]; - EXPECT_TRUE( - op->isEquivalentTo(op2.get(), IComparable::Criterion::EQUIVALENT)); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_3D_to_geogCRS_3D_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto wkt = "PROJCRS[\"NAD83(HARN) / Oregon GIC Lambert (ft)\",\n" - " BASEGEOGCRS[\"NAD83(HARN)\",\n" - " DATUM[\"NAD83 (High Accuracy Reference Network)\",\n" - " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " ID[\"EPSG\",4957]],\n" - " CONVERSION[\"unnamed\",\n" - " METHOD[\"Lambert Conic Conformal (2SP)\",\n" - " ID[\"EPSG\",9802]],\n" - " PARAMETER[\"Latitude of false origin\",41.75,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8821]],\n" - " PARAMETER[\"Longitude of false origin\",-120.5,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8822]],\n" - " PARAMETER[\"Latitude of 1st standard parallel\",43,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8823]],\n" - " PARAMETER[\"Latitude of 2nd standard parallel\",45.5,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8824]],\n" - " PARAMETER[\"Easting at false origin\",1312335.958,\n" - " LENGTHUNIT[\"foot\",0.3048],\n" - " ID[\"EPSG\",8826]],\n" - " PARAMETER[\"Northing at false origin\",0,\n" - " LENGTHUNIT[\"foot\",0.3048],\n" - " ID[\"EPSG\",8827]]],\n" - " CS[Cartesian,3],\n" - " AXIS[\"easting\",east,\n" - " ORDER[1],\n" - " LENGTHUNIT[\"foot\",0.3048]],\n" - " AXIS[\"northing\",north,\n" - " ORDER[2],\n" - " LENGTHUNIT[\"foot\",0.3048]],\n" - " AXIS[\"ellipsoidal height (h)\",up,\n" - " ORDER[3],\n" - " LENGTHUNIT[\"foot\",0.3048]]]"; - auto obj = WKTParser().createFromWKT(wkt); - auto src = NN_CHECK_ASSERT(nn_dynamic_pointer_cast(obj)); - auto dst = authFactory->createCoordinateReferenceSystem( - "4957"); // NAD83(HARN) (3D) - auto list = - CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - // Check that z ft->m conversion is done (and just once) - "+step +proj=unitconvert +xy_in=ft +z_in=ft +xy_out=m +z_out=m " - "+step +inv +proj=lcc +lat_0=41.75 +lon_0=-120.5 +lat_1=43 " - "+lat_2=45.5 +x_0=399999.9999984 +y_0=0 +ellps=GRS80 " - "+step +proj=unitconvert +xy_in=rad +z_in=m +xy_out=deg +z_out=m " - "+step +proj=axisswap +order=2,1"); -} -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_3D_to_projCRS_2D_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto wkt = - "PROJCRS[\"Projected 3d CRS\",\n" - " BASEGEOGCRS[\"JGD2000\",\n" - " DATUM[\"Japanese Geodetic Datum 2000\",\n" - " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " ID[\"EPSG\",4947]],\n" // the code is what triggered the bug - " CONVERSION[\"Japan Plane Rectangular CS zone VII\",\n" - " METHOD[\"Transverse Mercator\",\n" - " ID[\"EPSG\",9807]],\n" - " PARAMETER[\"Latitude of natural origin\",36,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8801]],\n" - " PARAMETER[\"Longitude of natural origin\",137.166666666667,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8802]],\n" - " PARAMETER[\"Scale factor at natural origin\",0.9999,\n" - " SCALEUNIT[\"unity\",1],\n" - " ID[\"EPSG\",8805]],\n" - " PARAMETER[\"False easting\",0,\n" - " LENGTHUNIT[\"metre\",1],\n" - " ID[\"EPSG\",8806]],\n" - " PARAMETER[\"False northing\",0,\n" - " LENGTHUNIT[\"metre\",1],\n" - " ID[\"EPSG\",8807]],\n" - " ID[\"EPSG\",17807]],\n" - " CS[Cartesian,3],\n" - " AXIS[\"northing (X)\",north,\n" - " ORDER[1],\n" - " LENGTHUNIT[\"metre\",1,\n" - " ID[\"EPSG\",9001]]],\n" - " AXIS[\"easting (Y)\",east,\n" - " ORDER[2],\n" - " LENGTHUNIT[\"metre\",1,\n" - " ID[\"EPSG\",9001]]],\n" - " AXIS[\"ellipsoidal height (h)\",up,\n" - " ORDER[3],\n" - " LENGTHUNIT[\"metre\",1,\n" - " ID[\"EPSG\",9001]]]]"; - auto obj = WKTParser().createFromWKT(wkt); - auto src = NN_CHECK_ASSERT(nn_dynamic_pointer_cast(obj)); - auto dst = - authFactory->createCoordinateReferenceSystem("32653"); // WGS 84 UTM 53 - // We just want to check that we don't get inconsistent chaining exception - auto list = - CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); - ASSERT_GE(list.size(), 1U); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_3D_to_projCRS_with_2D_geocentric_translation) { - - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto src = - authFactory->createCoordinateReferenceSystem("4979"); // WGS 84 3D - - // Azores Central 1948 / UTM zone 26N - auto dst = authFactory->createCoordinateReferenceSystem("2189"); - - auto list = - CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +z_in=m +xy_out=rad +z_out=m " - "+step +proj=push +v_3 " // this is what we check. Due to the - // target system being 2D only - "+step +proj=cart +ellps=WGS84 " - "+step +proj=helmert +x=104 +y=-167 +z=38 " - "+step +inv +proj=cart +ellps=intl " - "+step +proj=pop +v_3 " // this is what we check - "+step +proj=utm +zone=26 +ellps=intl"); - - auto listReverse = - CoordinateOperationFactory::create()->createOperations(dst, src, ctxt); - ASSERT_GE(listReverse.size(), 1U); - EXPECT_EQ( - listReverse[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +inv +proj=utm +zone=26 +ellps=intl " - "+step +proj=push +v_3 " // this is what we check - "+step +proj=cart +ellps=intl " - "+step +proj=helmert +x=-104 +y=167 +z=-38 " - "+step +inv +proj=cart +ellps=WGS84 " - "+step +proj=pop +v_3 " // this is what we check - "+step +proj=unitconvert +xy_in=rad +z_in=m +xy_out=deg +z_out=m " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_to_projCRS) { - - auto op = CoordinateOperationFactory::create()->createOperation( - createUTM31_WGS84(), createUTM32_WGS84()); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=utm +zone=31 +ellps=WGS84 +step " - "+proj=utm +zone=32 +ellps=WGS84"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_to_projCRS_different_baseCRS) { - - auto utm32 = ProjectedCRS::create( - PropertyMap(), GeographicCRS::EPSG_4807, - Conversion::createUTM(PropertyMap(), 32, true), - CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); - - auto op = CoordinateOperationFactory::create()->createOperation( - createUTM31_WGS84(), utm32); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=utm +zone=31 +ellps=WGS84 +step " - "+proj=utm +zone=32 +ellps=clrk80ign +pm=paris"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_to_projCRS_context_compatible_area) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("32634"), // UTM 34 - authFactory->createCoordinateReferenceSystem( - "2171"), // Pulkovo 42 Poland I - ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ(list[0]->nameStr(), - "Inverse of UTM zone 34N + Inverse of Pulkovo 1942(58) to WGS 84 " - "(1) + Poland zone I"); - ASSERT_EQ(list[0]->coordinateOperationAccuracies().size(), 1U); - EXPECT_EQ(list[0]->coordinateOperationAccuracies()[0]->value(), "1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_to_projCRS_context_compatible_area_bis) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem( - "3844"), // Pulkovo 42 Stereo 70 (Romania) - authFactory->createCoordinateReferenceSystem("32634"), // UTM 34 - ctxt); - ASSERT_EQ(list.size(), 3U); - EXPECT_EQ(list[0]->nameStr(), "Inverse of Stereo 70 + " - "Pulkovo 1942(58) to WGS 84 " - "(19) + UTM zone 34N"); - ASSERT_EQ(list[0]->coordinateOperationAccuracies().size(), 1U); - EXPECT_EQ(list[0]->coordinateOperationAccuracies()[0]->value(), "3"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_to_projCRS_context_one_incompatible_area) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("32631"), // UTM 31 - authFactory->createCoordinateReferenceSystem( - "2171"), // Pulkovo 42 Poland I - ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ(list[0]->nameStr(), - "Inverse of UTM zone 31N + Inverse of Pulkovo 1942(58) to WGS 84 " - "(1) + Poland zone I"); - ASSERT_EQ(list[0]->coordinateOperationAccuracies().size(), 1U); - EXPECT_EQ(list[0]->coordinateOperationAccuracies()[0]->value(), "1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_to_projCRS_context_incompatible_areas) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("32631"), // UTM 31 - authFactory->createCoordinateReferenceSystem("32633"), // UTM 33 - ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), "Inverse of UTM zone 31N + UTM zone 33N"); - ASSERT_EQ(list[0]->coordinateOperationAccuracies().size(), 1U); - EXPECT_EQ(list[0]->coordinateOperationAccuracies()[0]->value(), "0"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_to_projCRS_context_incompatible_areas_ballpark) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("26711"), // UTM 11 NAD27 - authFactory->createCoordinateReferenceSystem( - "3034"), // ETRS89 / LCC Europe - ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_TRUE(list[0]->hasBallparkTransformation()); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - projCRS_to_projCRS_context_incompatible_areas_crs_extent_use_intersection) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSourceAndTargetCRSExtentUse( - CoordinateOperationContext::SourceTargetCRSExtentUse::INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("26711"), // UTM 11 NAD27 - authFactory->createCoordinateReferenceSystem( - "3034"), // ETRS89 / LCC Europe - ctxt); - ASSERT_GE(list.size(), 0U); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_to_projCRS_north_pole_inverted_axis) { - - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), std::string()); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - AuthorityFactory::create(DatabaseContext::create(), "EPSG") - ->createCoordinateReferenceSystem("32661"), - AuthorityFactory::create(DatabaseContext::create(), "EPSG") - ->createCoordinateReferenceSystem("5041"), - ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_to_projCRS_south_pole_inverted_axis) { - - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), std::string()); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - AuthorityFactory::create(DatabaseContext::create(), "EPSG") - ->createCoordinateReferenceSystem("32761"), - AuthorityFactory::create(DatabaseContext::create(), "EPSG") - ->createCoordinateReferenceSystem("5042"), - ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_to_projCRS_through_geog3D) { - // Check that when going from projCRS to projCRS, using - // geog2D-->geog3D-->geog3D-->geog2D we do not have issues with - // inconsistent CRS chaining, due to how we 'hack' a bit some intermediate - // steps - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("5367"), // CR05 / CRTM05 - authFactory->createCoordinateReferenceSystem( - "8908"), // CR-SIRGAS / CRTM05 - ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 " - "+step +inv +proj=tmerc +lat_0=0 +lon_0=-84 +k=0.9999 " - "+x_0=500000 +y_0=0 +ellps=WGS84 " - "+step +proj=push +v_3 " - "+step +proj=cart +ellps=WGS84 " - "+step +proj=helmert +x=-0.16959 +y=0.35312 +z=0.51846 " - "+rx=-0.03385 +ry=0.16325 +rz=-0.03446 +s=0.03693 " - "+convention=position_vector " - "+step +inv +proj=cart +ellps=GRS80 " - "+step +proj=pop +v_3 " - "+step +proj=tmerc +lat_0=0 +lon_0=-84 +k=0.9999 +x_0=500000 " - "+y_0=0 +ellps=GRS80"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, transform_from_amersfoort_rd_new_to_epsg_4326) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("28992"), - authFactory->createCoordinateReferenceSystem("4326"), ctxt); - ASSERT_EQ(list.size(), 2U); - // The order matters: "Amersfoort to WGS 84 (4)" replaces "Amersfoort to WGS - // 84 (3)" - EXPECT_EQ(list[0]->nameStr(), - "Inverse of RD New + Amersfoort to WGS 84 (4)"); - EXPECT_EQ(list[1]->nameStr(), - "Inverse of RD New + Amersfoort to WGS 84 (3)"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_of_geogCRS_to_geogCRS) { - auto boundCRS = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto op = CoordinateOperationFactory::create()->createOperation( - boundCRS, GeographicCRS::EPSG_4326); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " - "+proj=longlat +ellps=clrk80ign +pm=paris +step +proj=push +v_3 " - "+step +proj=cart +ellps=clrk80ign +step +proj=helmert +x=1 +y=2 " - "+z=3 +rx=4 +ry=5 +rz=6 +s=7 +convention=position_vector +step " - "+inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " - "+order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_of_geogCRS_to_geodCRS) { - auto boundCRS = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto op = CoordinateOperationFactory::create()->createOperation( - boundCRS, GeodeticCRS::EPSG_4978); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step " - "+proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=grad +xy_out=rad " - "+step +inv +proj=longlat +ellps=clrk80ign +pm=paris " - "+step +proj=cart +ellps=clrk80ign " - "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 " - "+convention=position_vector"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_of_geogCRS_to_geodCRS_not_related_to_hub) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto boundCRS = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - boundCRS, - // ETRS89 geocentric - authFactory->createCoordinateReferenceSystem("4936"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step " - "+proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=grad +xy_out=rad " - "+step +inv +proj=longlat +ellps=clrk80ign +pm=paris " - "+step +proj=push +v_3 " - "+step +proj=cart +ellps=clrk80ign " - "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 " - "+convention=position_vector " - "+step +inv +proj=cart +ellps=GRS80 " - "+step +proj=pop +v_3 " - "+step +proj=cart +ellps=GRS80"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_of_geogCRS_to_geogCRS_with_area) { - auto boundCRS = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4267, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto op = CoordinateOperationFactory::create()->createOperation( - boundCRS, authFactory->createCoordinateReferenceSystem("4326")); - 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 +proj=push +v_3 " - "+step +proj=cart +ellps=clrk66 +step +proj=helmert +x=1 +y=2 " - "+z=3 +rx=4 +ry=5 +rz=6 +s=7 +convention=position_vector +step " - "+inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " - "+order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_of_geogCRS_to_unrelated_geogCRS) { - auto boundCRS = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto op = CoordinateOperationFactory::create()->createOperation( - boundCRS, GeographicCRS::EPSG_4269); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - CoordinateOperationFactory::create() - ->createOperation(GeographicCRS::EPSG_4807, - GeographicCRS::EPSG_4269) - ->exportToPROJString(PROJStringFormatter::create().get())); -} - -// --------------------------------------------------------------------------- - -TEST(operation, createOperation_boundCRS_identified_by_datum) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=longlat +datum=WGS84 +type=crs"); - auto src = nn_dynamic_pointer_cast(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 +type=crs"); - auto dest = nn_dynamic_pointer_cast(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=push +v_3 +step +proj=cart +ellps=WGS84 +step " - "+proj=helmert +x=263 +y=-6 +z=-431 +step +inv +proj=cart " - "+ellps=clrk80ign +step +proj=pop +v_3 +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 +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - - auto objDest = PROJStringParser().createFromPROJString( - "+proj=latlong +datum=NAD83 +type=crs"); - auto dest = nn_dynamic_pointer_cast(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 " - "+type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - - auto objDest = PROJStringParser().createFromPROJString( - "+proj=latlong +datum=NAD83 +type=crs"); - auto dest = nn_dynamic_pointer_cast(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, - Conversion::createUTM(PropertyMap(), 31, true), - CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); - auto boundCRS = BoundCRS::createFromTOWGS84( - utm31, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto op = CoordinateOperationFactory::create()->createOperation( - boundCRS, GeographicCRS::EPSG_4326); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=utm +zone=31 +ellps=clrk80ign " - "+pm=paris +step +proj=push +v_3 +step +proj=cart " - "+ellps=clrk80ign +step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 " - "+rz=6 +s=7 +convention=position_vector +step +inv +proj=cart " - "+ellps=WGS84 +step +proj=pop +v_3 +step +proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_of_geogCRS_to_projCRS) { - auto boundCRS = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto utm31 = ProjectedCRS::create( - PropertyMap(), GeographicCRS::EPSG_4326, - Conversion::createUTM(PropertyMap(), 31, true), - CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); - auto op = - CoordinateOperationFactory::create()->createOperation(boundCRS, utm31); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " - "+proj=longlat +ellps=clrk80ign +pm=paris +step +proj=push +v_3 " - "+step +proj=cart +ellps=clrk80ign +step +proj=helmert +x=1 +y=2 " - "+z=3 +rx=4 +ry=5 +rz=6 +s=7 +convention=position_vector +step " - "+inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step " - "+proj=utm +zone=31 +ellps=WGS84"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_of_geogCRS_to_unrelated_geogCRS_context) { - auto src = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - // ETRS89 - auto dst = authFactory->createCoordinateReferenceSystem("4258"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = - CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); - ASSERT_EQ(list.size(), 1U); - // Check with it is a concatenated operation, since it doesn't particularly - // show up in the PROJ string - EXPECT_TRUE(dynamic_cast(list[0].get()) != - nullptr); - EXPECT_EQ(list[0]->nameStr(), "Transformation from NTF (Paris) to WGS84 + " - "Inverse of ETRS89 to WGS 84 (1)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=grad +xy_out=rad " - "+step +inv +proj=longlat +ellps=clrk80ign +pm=paris " - "+step +proj=push +v_3 +step +proj=cart +ellps=clrk80ign " - "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 " - "+convention=position_vector " - "+step +inv +proj=cart +ellps=GRS80 +step +proj=pop +v_3 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geogCRS_to_boundCRS_of_geogCRS) { - auto boundCRS = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto op = CoordinateOperationFactory::create()->createOperation( - GeographicCRS::EPSG_4326, boundCRS); - 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 +proj=push +v_3 " - "+step +proj=cart +ellps=WGS84 +step +inv +proj=helmert +x=1 " - "+y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 +convention=position_vector " - "+step +inv +proj=cart +ellps=clrk80ign +step +proj=pop +v_3 " - "+step +proj=longlat +ellps=clrk80ign +pm=paris +step " - "+proj=unitconvert +xy_in=rad +xy_out=grad +step +proj=axisswap " - "+order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_to_geogCRS_same_datum_context) { - auto boundCRS = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4269, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto dbContext = DatabaseContext::create(); - auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - boundCRS, GeographicCRS::EPSG_4269, ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=noop"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_to_geogCRS_hubCRS_and_targetCRS_same_but_baseCRS_not) { - const char *wkt = - "COMPD_CS[\"NAD83 + Ellipsoid (US Feet)\",\n" - " GEOGCS[\"NAD83\",\n" - " DATUM[\"North_American_Datum_1983\",\n" - " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" - " AUTHORITY[\"EPSG\",\"7019\"]],\n" - " TOWGS84[0,0,0,0,0,0,0],\n" - " AUTHORITY[\"EPSG\",\"6269\"]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " AUTHORITY[\"EPSG\",\"8901\"]],\n" - " UNIT[\"degree\",0.0174532925199433,\n" - " AUTHORITY[\"EPSG\",\"9122\"]],\n" - " AUTHORITY[\"EPSG\",\"4269\"]],\n" - " VERT_CS[\"Ellipsoid (US Feet)\",\n" - " VERT_DATUM[\"Ellipsoid\",2002],\n" - " UNIT[\"US survey foot\",0.304800609601219,\n" - " AUTHORITY[\"EPSG\",\"9003\"]],\n" - " AXIS[\"Up\",UP]]]"; - - auto dbContext = DatabaseContext::create(); - auto obj = WKTParser().attachDatabaseContext(dbContext).createFromWKT(wkt); - auto boundCRS = nn_dynamic_pointer_cast(obj); - ASSERT_TRUE(boundCRS != nullptr); - auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(boundCRS), GeographicCRS::EPSG_4979, ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=unitconvert +z_in=us-ft +z_out=m"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_to_boundCRS) { - auto utm31 = ProjectedCRS::create( - PropertyMap(), GeographicCRS::EPSG_4807, - Conversion::createUTM(PropertyMap(), 31, true), - CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); - auto utm32 = ProjectedCRS::create( - PropertyMap(), GeographicCRS::EPSG_4269, - Conversion::createUTM(PropertyMap(), 32, true), - CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); - auto boundCRS1 = BoundCRS::createFromTOWGS84( - utm31, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto boundCRS2 = BoundCRS::createFromTOWGS84( - utm32, std::vector{8, 9, 10, 11, 12, 13, 14}); - auto op = CoordinateOperationFactory::create()->createOperation(boundCRS1, - boundCRS2); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=utm +zone=31 +ellps=clrk80ign " - "+pm=paris +step +proj=push +v_3 +step +proj=cart " - "+ellps=clrk80ign +step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 " - "+rz=6 +s=7 +convention=position_vector +step +inv +proj=helmert " - "+x=8 +y=9 +z=10 +rx=11 +ry=12 +rz=13 +s=14 " - "+convention=position_vector +step +inv +proj=cart +ellps=GRS80 " - "+step +proj=pop +v_3 +step +proj=utm +zone=32 +ellps=GRS80"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_to_boundCRS_noop_for_TOWGS84) { - auto boundCRS1 = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto boundCRS2 = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4269, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto op = CoordinateOperationFactory::create()->createOperation(boundCRS1, - boundCRS2); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " - "+proj=longlat +ellps=clrk80ign +pm=paris +step +proj=push +v_3 " - "+step +proj=cart +ellps=clrk80ign +step +inv +proj=cart " - "+ellps=GRS80 +step +proj=pop +v_3 +step +proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_to_boundCRS_unralated_hub) { - auto boundCRS1 = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto boundCRS2 = BoundCRS::create( - GeographicCRS::EPSG_4269, GeographicCRS::EPSG_4979, - Transformation::createGeocentricTranslations( - PropertyMap(), GeographicCRS::EPSG_4269, GeographicCRS::EPSG_4979, - 1.0, 2.0, 3.0, std::vector())); - auto op = CoordinateOperationFactory::create()->createOperation(boundCRS1, - boundCRS2); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - CoordinateOperationFactory::create() - ->createOperation(boundCRS1->baseCRS(), boundCRS2->baseCRS()) - ->exportToPROJString(PROJStringFormatter::create().get())); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_of_projCRS_towgs84_to_boundCRS_of_projCRS_nadgrids) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=utm +zone=15 +datum=NAD83 +units=m +no_defs +ellps=GRS80 " - "+towgs84=0,0,0 +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto objDst = PROJStringParser().createFromPROJString( - "+proj=utm +zone=15 +datum=NAD27 +units=m +no_defs +ellps=clrk66 " - "+nadgrids=@conus,@alaska,@ntv2_0.gsb,@ntv1_can.dat +type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=utm +zone=15 +ellps=GRS80 +step " - "+inv +proj=hgridshift " - "+grids=@conus,@alaska,@ntv2_0.gsb,@ntv1_can.dat +step +proj=utm " - "+zone=15 +ellps=clrk66"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_with_basecrs_with_extent_to_geogCRS) { - - auto wkt = - "BOUNDCRS[\n" - " SOURCECRS[\n" - " PROJCRS[\"NAD83 / California zone 3 (ftUS)\",\n" - " BASEGEODCRS[\"NAD83\",\n" - " DATUM[\"North American Datum 1983\",\n" - " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]]],\n" - " CONVERSION[\"SPCS83 California zone 3 (US Survey " - "feet)\",\n" - " METHOD[\"Lambert Conic Conformal (2SP)\",\n" - " ID[\"EPSG\",9802]],\n" - " PARAMETER[\"Latitude of false origin\",36.5,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8821]],\n" - " PARAMETER[\"Longitude of false origin\",-120.5,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8822]],\n" - " PARAMETER[\"Latitude of 1st standard parallel\"," - " 38.4333333333333,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8823]],\n" - " PARAMETER[\"Latitude of 2nd standard parallel\"," - " 37.0666666666667,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8824]],\n" - " PARAMETER[\"Easting at false origin\",6561666.667,\n" - " LENGTHUNIT[\"US survey foot\"," - " 0.304800609601219],\n" - " ID[\"EPSG\",8826]],\n" - " PARAMETER[\"Northing at false origin\",1640416.667,\n" - " LENGTHUNIT[\"US survey foot\"," - " 0.304800609601219],\n" - " ID[\"EPSG\",8827]]],\n" - " CS[Cartesian,2],\n" - " AXIS[\"easting (X)\",east,\n" - " ORDER[1],\n" - " LENGTHUNIT[\"US survey foot\"," - " 0.304800609601219]],\n" - " AXIS[\"northing (Y)\",north,\n" - " ORDER[2],\n" - " LENGTHUNIT[\"US survey foot\"," - " 0.304800609601219]],\n" - " SCOPE[\"unknown\"],\n" - " AREA[\"USA - California - SPCS - 3\"],\n" - " BBOX[36.73,-123.02,38.71,-117.83],\n" - " ID[\"EPSG\",2227]]],\n" - " TARGETCRS[\n" - " GEODCRS[\"WGS 84\",\n" - " DATUM[\"World Geodetic System 1984\",\n" - " ELLIPSOID[\"WGS 84\",6378137,298.257223563,\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " CS[ellipsoidal,2],\n" - " AXIS[\"latitude\",north,\n" - " ORDER[1],\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " AXIS[\"longitude\",east,\n" - " ORDER[2],\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " ID[\"EPSG\",4326]]],\n" - " ABRIDGEDTRANSFORMATION[\"NAD83 to WGS 84 (1)\",\n" - " METHOD[\"Geocentric translations (geog2D domain)\",\n" - " ID[\"EPSG\",9603]],\n" - " PARAMETER[\"X-axis translation\",0,\n" - " ID[\"EPSG\",8605]],\n" - " PARAMETER[\"Y-axis translation\",0,\n" - " ID[\"EPSG\",8606]],\n" - " PARAMETER[\"Z-axis translation\",0,\n" - " ID[\"EPSG\",8607]],\n" - " SCOPE[\"unknown\"],\n" - " AREA[\"North America - Canada and USA (CONUS, Alaska " - "mainland)\"],\n" - " BBOX[23.81,-172.54,86.46,-47.74],\n" - " ID[\"EPSG\",1188]]]"; - auto obj = WKTParser().createFromWKT(wkt); - auto boundCRS = nn_dynamic_pointer_cast(obj); - ASSERT_TRUE(boundCRS != nullptr); - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(boundCRS), GeographicCRS::EPSG_4326); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->nameStr(), "Inverse of SPCS83 California zone 3 (US Survey " - "feet) + NAD83 to WGS 84 (1)"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, ETRS89_3D_to_proj_string_with_geoidgrids_nadgrids) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - // ETRS89 3D - auto src = authFactory->createCoordinateReferenceSystem("4937"); - auto objDst = PROJStringParser().createFromPROJString( - "+proj=sterea +lat_0=52.15616055555555 +lon_0=5.38763888888889 " - "+k=0.9999079 +x_0=155000 +y_0=463000 +ellps=bessel " - "+nadgrids=rdtrans2008.gsb +geoidgrids=naptrans2008.gtx +units=m " - "+type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - src, NN_NO_CHECK(dst), ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +inv +proj=vgridshift +grids=naptrans2008.gtx " - "+multiplier=1 " - "+step +inv +proj=hgridshift +grids=rdtrans2008.gsb " - "+step +proj=sterea +lat_0=52.1561605555556 " - "+lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 " - "+y_0=463000 +ellps=bessel"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, nadgrids_with_pm) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=tmerc +lat_0=39.66666666666666 +lon_0=1 +k=1 +x_0=200000 " - "+y_0=300000 +ellps=intl +nadgrids=foo.gsb +pm=lisbon " - "+units=m +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto dst = authFactory->createCoordinateReferenceSystem("4326"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(src), dst, ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +inv +proj=tmerc +lat_0=39.6666666666667 +lon_0=1 " - "+k=1 +x_0=200000 +y_0=300000 +ellps=intl +pm=lisbon " - // Check that there is no extra +step +proj=longlat +pm=lisbon - "+step +proj=hgridshift +grids=foo.gsb " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - - // ETRS89 - dst = authFactory->createCoordinateReferenceSystem("4258"); - list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(src), dst, ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +inv +proj=tmerc +lat_0=39.6666666666667 +lon_0=1 " - "+k=1 +x_0=200000 +y_0=300000 +ellps=intl +pm=lisbon " - // Check that there is no extra +step +proj=longlat +pm=lisbon - "+step +proj=hgridshift +grids=foo.gsb " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - - // From WKT BOUNDCRS - auto formatter = WKTFormatter::create(WKTFormatter::Convention::WKT2_2019); - auto src_wkt = src->exportToWKT(formatter.get()); - auto objFromWkt = WKTParser().createFromWKT(src_wkt); - auto crsFromWkt = nn_dynamic_pointer_cast(objFromWkt); - ASSERT_TRUE(crsFromWkt); - list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(crsFromWkt), dst, ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +inv +proj=tmerc +lat_0=39.6666666666667 +lon_0=1 " - "+k=1 +x_0=200000 +y_0=300000 +ellps=intl +pm=lisbon " - // Check that there is no extra +step +proj=longlat +pm=lisbon - "+step +proj=hgridshift +grids=foo.gsb " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, WGS84_G1762_to_compoundCRS_with_bound_vertCRS) { - auto authFactoryEPSG = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - // WGS 84 (G1762) 3D - auto src = authFactoryEPSG->createCoordinateReferenceSystem("7665"); - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +datum=NAD83 +geoidgrids=@foo.gtx +type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), std::string()); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - auto list = CoordinateOperationFactory::create()->createOperations( - src, NN_NO_CHECK(dst), ctxt); - ASSERT_GE(list.size(), 53U); - EXPECT_EQ(list[0]->nameStr(), - "Inverse of WGS 84 to WGS 84 (G1762) + " - "Inverse of unknown to WGS84 ellipsoidal height + " - "Inverse of NAD83 to WGS 84 (1) + " - "axis order change (2D)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +inv +proj=vgridshift +grids=@foo.gtx +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_geogCRS) { - - auto compound = CompoundCRS::create( - PropertyMap(), - std::vector{GeographicCRS::EPSG_4326, createVerticalCRS()}); - auto op = CoordinateOperationFactory::create()->createOperation( - compound, GeographicCRS::EPSG_4807); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - CoordinateOperationFactory::create() - ->createOperation(GeographicCRS::EPSG_4326, - GeographicCRS::EPSG_4807) - ->exportToPROJString(PROJStringFormatter::create().get())); -} - -// --------------------------------------------------------------------------- - -static BoundCRSNNPtr createBoundVerticalCRS() { - auto vertCRS = createVerticalCRS(); - auto transformation = - Transformation::createGravityRelatedHeightToGeographic3D( - PropertyMap(), vertCRS, GeographicCRS::EPSG_4979, nullptr, - "us_nga_egm08_25.tif", std::vector()); - return BoundCRS::create(vertCRS, GeographicCRS::EPSG_4979, transformation); -} - -// --------------------------------------------------------------------------- - -TEST(operation, transformation_height_to_PROJ_string) { - auto transf = createBoundVerticalCRS()->transformation(); - EXPECT_EQ(transf->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=us_nga_egm08_25.tif " - "+multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - - auto grids = transf->gridsNeeded(DatabaseContext::create(), false); - ASSERT_EQ(grids.size(), 1U); - auto gridDesc = *(grids.begin()); - EXPECT_EQ(gridDesc.shortName, "us_nga_egm08_25.tif"); - EXPECT_TRUE(gridDesc.packageName.empty()); - EXPECT_EQ(gridDesc.url, "https://cdn.proj.org/us_nga_egm08_25.tif"); - if (gridDesc.available) { - EXPECT_TRUE(!gridDesc.fullName.empty()) << gridDesc.fullName; - EXPECT_TRUE(gridDesc.fullName.find(gridDesc.shortName) != - std::string::npos) - << gridDesc.fullName; - } else { - EXPECT_TRUE(gridDesc.fullName.empty()) << gridDesc.fullName; - } - EXPECT_EQ(gridDesc.directDownload, true); - EXPECT_EQ(gridDesc.openLicense, true); -} - -// --------------------------------------------------------------------------- - -TEST(operation, transformation_Geographic3D_to_GravityRelatedHeight_gtx) { - auto wkt = - "COORDINATEOPERATION[\"ETRS89 to NAP height (1)\",\n" - " VERSION[\"RDNAP-Nld 2008\"],\n" - " SOURCECRS[\n" - " GEOGCRS[\"ETRS89\",\n" - " DATUM[\"European Terrestrial Reference System 1989\",\n" - " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " CS[ellipsoidal,3],\n" - " AXIS[\"geodetic latitude (Lat)\",north,\n" - " ORDER[1],\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " AXIS[\"geodetic longitude (Lon)\",east,\n" - " ORDER[2],\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " AXIS[\"ellipsoidal height (h)\",up,\n" - " ORDER[3],\n" - " LENGTHUNIT[\"metre\",1]],\n" - " ID[\"EPSG\",4937]]],\n" - " TARGETCRS[\n" - " VERTCRS[\"NAP height\",\n" - " VDATUM[\"Normaal Amsterdams Peil\"],\n" - " CS[vertical,1],\n" - " AXIS[\"gravity-related height (H)\",up,\n" - " LENGTHUNIT[\"metre\",1]],\n" - " ID[\"EPSG\",5709]]],\n" - " METHOD[\"Geographic3D to GravityRelatedHeight (US .gtx)\",\n" - " ID[\"EPSG\",9665]],\n" - " PARAMETERFILE[\"Geoid (height correction) model " - "file\",\"naptrans2008.gtx\"],\n" - " OPERATIONACCURACY[0.01],\n" - " USAGE[\n" - " SCOPE[\"unknown\"],\n" - " AREA[\"Netherlands - onshore\"],\n" - " BBOX[50.75,3.2,53.7,7.22]],\n" - " ID[\"EPSG\",7001]]"; - ; - auto obj = WKTParser().createFromWKT(wkt); - auto transf = nn_dynamic_pointer_cast(obj); - ASSERT_TRUE(transf != nullptr); - - // Check that we correctly inverse files in the case of - // "Geographic3D to GravityRelatedHeight (US .gtx)" - EXPECT_EQ(transf->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +inv +proj=vgridshift " - "+grids=naptrans2008.gtx +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, transformation_ntv2_to_PROJ_string) { - auto transformation = Transformation::createNTv2( - PropertyMap(), GeographicCRS::EPSG_4807, GeographicCRS::EPSG_4326, - "foo.gsb", std::vector()); - EXPECT_EQ( - transformation->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=grad +xy_out=rad +step " - "+proj=hgridshift +grids=foo.gsb +step +proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, transformation_VERTCON_to_PROJ_string) { - auto verticalCRS1 = createVerticalCRS(); - - auto verticalCRS2 = VerticalCRS::create( - PropertyMap(), VerticalReferenceFrame::create(PropertyMap()), - VerticalCS::createGravityRelatedHeight(UnitOfMeasure::METRE)); - - // Use of this type of transformation is a bit of non-sense here - // since it should normally be used with NGVD29 and NAVD88 for VerticalCRS, - // and NAD27/NAD83 as horizontal CRS... - auto vtransformation = Transformation::createVERTCON( - PropertyMap(), verticalCRS1, verticalCRS2, "bla.gtx", - std::vector()); - EXPECT_EQ(vtransformation->exportToPROJString( - PROJStringFormatter::create().get()), - "+proj=vgridshift +grids=bla.gtx +multiplier=0.001"); -} -// --------------------------------------------------------------------------- - -TEST(operation, transformation_NZLVD_to_PROJ_string) { - auto dbContext = DatabaseContext::create(); - auto factory = AuthorityFactory::create(dbContext, "EPSG"); - auto op = factory->createCoordinateOperation("7860", false); - EXPECT_EQ(op->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, dbContext) - .get()), - "+proj=vgridshift +grids=nz_linz_auckht1946-nzvd2016.tif " - "+multiplier=1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, transformation_BEV_AT_to_PROJ_string) { - auto dbContext = DatabaseContext::create(); - auto factory = AuthorityFactory::create(dbContext, "EPSG"); - auto op = factory->createCoordinateOperation("9275", false); - EXPECT_EQ(op->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, dbContext) - .get()), - "+proj=vgridshift +grids=at_bev_GV_Hoehengrid_V1.tif " - "+multiplier=1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, transformation_longitude_rotation_to_PROJ_string) { - - auto src = GeographicCRS::create( - PropertyMap(), GeodeticReferenceFrame::create( - PropertyMap(), Ellipsoid::WGS84, - optional(), PrimeMeridian::GREENWICH), - EllipsoidalCS::createLatitudeLongitude(UnitOfMeasure::DEGREE)); - auto dest = GeographicCRS::create( - PropertyMap(), GeodeticReferenceFrame::create( - PropertyMap(), Ellipsoid::WGS84, - optional(), PrimeMeridian::PARIS), - EllipsoidalCS::createLatitudeLongitude(UnitOfMeasure::DEGREE)); - auto transformation = Transformation::createLongitudeRotation( - PropertyMap(), src, dest, Angle(10)); - EXPECT_TRUE(transformation->validateParameters().empty()); - EXPECT_EQ( - transformation->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 +ellps=WGS84 +pm=10 +step +proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); - EXPECT_EQ(transformation->inverse()->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 +ellps=WGS84 +pm=-10 +step +proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, transformation_Geographic2D_offsets_to_PROJ_string) { - - auto transformation = Transformation::createGeographic2DOffsets( - PropertyMap(), GeographicCRS::EPSG_4326, GeographicCRS::EPSG_4326, - Angle(0.5), Angle(-1), {}); - EXPECT_TRUE(transformation->validateParameters().empty()); - - EXPECT_EQ( - transformation->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " - "+dlat=1800 +dlon=-3600 +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); - EXPECT_EQ(transformation->inverse()->exportToPROJString( - PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " - "+dlat=-1800 +dlon=3600 +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, transformation_Geographic3D_offsets_to_PROJ_string) { - - auto transformation = Transformation::createGeographic3DOffsets( - PropertyMap(), GeographicCRS::EPSG_4326, GeographicCRS::EPSG_4326, - Angle(0.5), Angle(-1), Length(2), {}); - EXPECT_TRUE(transformation->validateParameters().empty()); - - EXPECT_EQ( - transformation->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " - "+dlat=1800 +dlon=-3600 +dh=2 +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); - EXPECT_EQ(transformation->inverse()->exportToPROJString( - PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " - "+dlat=-1800 +dlon=3600 +dh=-2 +step +proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - transformation_Geographic2D_with_height_offsets_to_PROJ_string) { - - auto transformation = Transformation::createGeographic2DWithHeightOffsets( - PropertyMap(), - CompoundCRS::create(PropertyMap(), - {GeographicCRS::EPSG_4326, createVerticalCRS()}), - GeographicCRS::EPSG_4326, Angle(0.5), Angle(-1), Length(2), {}); - EXPECT_TRUE(transformation->validateParameters().empty()); - - EXPECT_EQ( - transformation->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " - "+dlat=1800 +dlon=-3600 +dh=2 +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); - EXPECT_EQ(transformation->inverse()->exportToPROJString( - PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " - "+dlat=-1800 +dlon=3600 +dh=-2 +step +proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, transformation_vertical_offset_to_PROJ_string) { - - auto transformation = Transformation::createVerticalOffset( - PropertyMap(), createVerticalCRS(), createVerticalCRS(), Length(1), {}); - EXPECT_TRUE(transformation->validateParameters().empty()); - - EXPECT_EQ( - transformation->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=geogoffset +dh=1"); - EXPECT_EQ(transformation->inverse()->exportToPROJString( - PROJStringFormatter::create().get()), - "+proj=geogoffset +dh=-1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_with_boundVerticalCRS_to_geogCRS) { - - auto compound = CompoundCRS::create( - PropertyMap(), std::vector{GeographicCRS::EPSG_4326, - createBoundVerticalCRS()}); - auto op = CoordinateOperationFactory::create()->createOperation( - compound, GeographicCRS::EPSG_4979); - 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 +proj=vgridshift " - "+grids=us_nga_egm08_25.tif +multiplier=1 +step +proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_with_boundGeogCRS_to_geogCRS) { - - auto geogCRS = GeographicCRS::create( - PropertyMap(), GeodeticReferenceFrame::create( - PropertyMap(), Ellipsoid::WGS84, - optional(), PrimeMeridian::GREENWICH), - EllipsoidalCS::createLatitudeLongitude(UnitOfMeasure::DEGREE)); - auto horizBoundCRS = BoundCRS::createFromTOWGS84( - geogCRS, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto compound = CompoundCRS::create( - PropertyMap(), - std::vector{horizBoundCRS, createVerticalCRS()}); - auto op = CoordinateOperationFactory::create()->createOperation( - compound, GeographicCRS::EPSG_4979); - 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 +proj=push +v_3 " - "+step +proj=cart +ellps=WGS84 " - "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 " - "+convention=position_vector " - "+step +inv +proj=cart +ellps=WGS84 " - "+step +proj=pop +v_3 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_with_boundGeogCRS_and_boundVerticalCRS_to_geogCRS) { - - auto horizBoundCRS = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto compound = CompoundCRS::create( - PropertyMap(), - std::vector{horizBoundCRS, createBoundVerticalCRS()}); - auto op = CoordinateOperationFactory::create()->createOperation( - compound, GeographicCRS::EPSG_4979); - ASSERT_TRUE(op != nullptr); - // Not completely sure the order of horizontal and vertical operations - // makes sense - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=grad +xy_out=rad " - "+step +inv +proj=longlat +ellps=clrk80ign +pm=paris " - "+step +proj=push +v_3 " - "+step +proj=cart +ellps=clrk80ign " - "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 " - "+convention=position_vector " - "+step +inv +proj=cart +ellps=WGS84 " - "+step +proj=pop +v_3 " - "+step +proj=vgridshift +grids=us_nga_egm08_25.tif +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - - auto grids = op->gridsNeeded(DatabaseContext::create(), false); - EXPECT_EQ(grids.size(), 1U); - - auto opInverse = CoordinateOperationFactory::create()->createOperation( - GeographicCRS::EPSG_4979, compound); - ASSERT_TRUE(opInverse != nullptr); - EXPECT_TRUE(opInverse->inverse()->isEquivalentTo(op.get())); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_with_boundProjCRS_and_boundVerticalCRS_to_geogCRS) { - - auto horizBoundCRS = BoundCRS::createFromTOWGS84( - ProjectedCRS::create( - PropertyMap(), GeographicCRS::EPSG_4807, - Conversion::createUTM(PropertyMap(), 31, true), - CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)), - std::vector{1, 2, 3, 4, 5, 6, 7}); - auto compound = CompoundCRS::create( - PropertyMap(), - std::vector{horizBoundCRS, createBoundVerticalCRS()}); - auto op = CoordinateOperationFactory::create()->createOperation( - compound, GeographicCRS::EPSG_4979); - ASSERT_TRUE(op != nullptr); - // Not completely sure the order of horizontal and vertical operations - // makes sense - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +inv +proj=utm +zone=31 +ellps=clrk80ign +pm=paris " - "+step +proj=push +v_3 " - "+step +proj=cart +ellps=clrk80ign " - "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 " - "+convention=position_vector " - "+step +inv +proj=cart +ellps=WGS84 " - "+step +proj=pop +v_3 " - "+step +proj=vgridshift +grids=us_nga_egm08_25.tif +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - - auto opInverse = CoordinateOperationFactory::create()->createOperation( - GeographicCRS::EPSG_4979, compound); - ASSERT_TRUE(opInverse != nullptr); - EXPECT_TRUE(opInverse->inverse()->isEquivalentTo(op.get())); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - compoundCRS_with_boundVerticalCRS_from_geoidgrids_with_m_to_geogCRS) { - - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=longlat +datum=WGS84 +geoidgrids=@foo.gtx +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto op = CoordinateOperationFactory::create()->createOperation( - NN_NO_CHECK(src), GeographicCRS::EPSG_4979); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->nameStr(), "axis order change (2D) + " - "unknown to WGS84 ellipsoidal height"); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=@foo.gtx +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - compoundCRS_with_boundVerticalCRS_from_geoidgrids_with_ftus_to_geogCRS) { - - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=longlat +datum=WGS84 +geoidgrids=@foo.gtx +vunits=us-ft " - "+type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto op = CoordinateOperationFactory::create()->createOperation( - NN_NO_CHECK(src), GeographicCRS::EPSG_4979); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->nameStr(), "axis order change (2D) + " - "Transformation from unknown to unknown + " - "unknown to WGS84 ellipsoidal height"); - EXPECT_EQ( - op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=unitconvert +xy_in=deg +z_in=us-ft +xy_out=rad +z_out=m " - "+step +proj=vgridshift +grids=@foo.gtx +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - compoundCRS_with_boundProjCRS_with_ftus_and_boundVerticalCRS_to_geogCRS) { - - auto wkt = - "COMPD_CS[\"NAD_1983_StatePlane_Alabama_West_FIPS_0102_Feet + " - "NAVD88 height - Geoid12B (US Feet)\",\n" - " PROJCS[\"NAD_1983_StatePlane_Alabama_West_FIPS_0102_Feet\",\n" - " GEOGCS[\"NAD83\",\n" - " DATUM[\"North_American_Datum_1983\",\n" - " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" - " AUTHORITY[\"EPSG\",\"7019\"]],\n" - " TOWGS84[0,0,0,0,0,0,0],\n" - " AUTHORITY[\"EPSG\",\"6269\"]],\n" - " PRIMEM[\"Greenwich\",0],\n" - " UNIT[\"Degree\",0.0174532925199433]],\n" - " PROJECTION[\"Transverse_Mercator\"],\n" - " PARAMETER[\"latitude_of_origin\",30],\n" - " PARAMETER[\"central_meridian\",-87.5],\n" - " PARAMETER[\"scale_factor\",0.999933333333333],\n" - " PARAMETER[\"false_easting\",1968500],\n" - " PARAMETER[\"false_northing\",0],\n" - " UNIT[\"US survey foot\",0.304800609601219,\n" - " AUTHORITY[\"EPSG\",\"9003\"]],\n" - " AXIS[\"Easting\",EAST],\n" - " AXIS[\"Northing\",NORTH],\n" - " AUTHORITY[\"ESRI\",\"102630\"]],\n" - " VERT_CS[\"NAVD88 height (ftUS)\",\n" - " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" - " EXTENSION[\"PROJ4_GRIDS\",\"foo.gtx\"],\n" - " AUTHORITY[\"EPSG\",\"5103\"]],\n" - " UNIT[\"US survey foot\",0.304800609601219,\n" - " AUTHORITY[\"EPSG\",\"9003\"]],\n" - " AXIS[\"Gravity-related height\",UP],\n" - " AUTHORITY[\"EPSG\",\"6360\"]]]"; - - auto obj = WKTParser().createFromWKT(wkt); - auto crs = nn_dynamic_pointer_cast(obj); - ASSERT_TRUE(crs != nullptr); - auto op = CoordinateOperationFactory::create()->createOperation( - NN_NO_CHECK(crs), GeographicCRS::EPSG_4979); - ASSERT_TRUE(op != nullptr); - - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=unitconvert +xy_in=us-ft +xy_out=m " - "+step +inv +proj=tmerc +lat_0=30 +lon_0=-87.5 " - "+k=0.999933333333333 +x_0=600000 +y_0=0 +ellps=GRS80 " - "+step +proj=unitconvert +z_in=us-ft +z_out=m " - "+step +proj=vgridshift +grids=foo.gtx +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - compoundCRS_with_boundVerticalCRS_from_grids_to_geogCRS_with_ftus_ctxt) { - - auto dbContext = DatabaseContext::create(); - - const char *wktSrc = - "COMPD_CS[\"NAD83 + NAVD88 height - Geoid12B (Meters)\",\n" - " GEOGCS[\"NAD83\",\n" - " DATUM[\"North_American_Datum_1983\",\n" - " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" - " AUTHORITY[\"EPSG\",\"7019\"]],\n" - " AUTHORITY[\"EPSG\",\"6269\"]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " AUTHORITY[\"EPSG\",\"8901\"]],\n" - " UNIT[\"degree\",0.0174532925199433,\n" - " AUTHORITY[\"EPSG\",\"9122\"]],\n" - " AUTHORITY[\"EPSG\",\"4269\"]],\n" - " VERT_CS[\"NAVD88 height - Geoid12B (Meters)\",\n" - " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" - " EXTENSION[\"PROJ4_GRIDS\",\"@foo.gtx\"],\n" - " AUTHORITY[\"EPSG\",\"5103\"]],\n" - " UNIT[\"metre\",1.0,\n" - " AUTHORITY[\"EPSG\",\"9001\"]],\n" - " AXIS[\"Gravity-related height\",UP],\n" - " AUTHORITY[\"EPSG\",\"5703\"]]]"; - auto objSrc = - WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktSrc); - auto srcCRS = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(srcCRS != nullptr); - - const char *wktDst = - "COMPD_CS[\"NAD83 + Ellipsoid (US Feet)\",\n" - " GEOGCS[\"NAD83\",\n" - " DATUM[\"North_American_Datum_1983\",\n" - " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" - " AUTHORITY[\"EPSG\",\"7019\"]],\n" - " AUTHORITY[\"EPSG\",\"6269\"]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " AUTHORITY[\"EPSG\",\"8901\"]],\n" - " UNIT[\"degree\",0.0174532925199433,\n" - " AUTHORITY[\"EPSG\",\"9122\"]],\n" - " AUTHORITY[\"EPSG\",\"4269\"]],\n" - " VERT_CS[\"Ellipsoid (US Feet)\",\n" - " VERT_DATUM[\"Ellipsoid\",2002],\n" - " UNIT[\"US survey foot\",0.304800609601219,\n" - " AUTHORITY[\"EPSG\",\"9003\"]],\n" - " AXIS[\"Up\",UP]]]"; - auto objDst = - WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktDst); - auto dstCRS = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dstCRS != nullptr); - - auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcCRS), NN_NO_CHECK(dstCRS), ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=@foo.gtx +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +z_in=m " - "+xy_out=deg +z_out=us-ft " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - compoundCRS_with_boundGeogCRS_boundVerticalCRS_from_grids_to_boundGeogCRS_with_ftus_ctxt) { - - // Variant of above but with TOWGS84 in source & target CRS - - auto dbContext = DatabaseContext::create(); - - const char *wktSrc = - "COMPD_CS[\"NAD83 + NAVD88 height - Geoid12B (Meters)\",\n" - " GEOGCS[\"NAD83\",\n" - " DATUM[\"North_American_Datum_1983\",\n" - " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" - " AUTHORITY[\"EPSG\",\"7019\"]],\n" - " TOWGS84[0,0,0,0,0,0,0],\n" - " AUTHORITY[\"EPSG\",\"6269\"]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " AUTHORITY[\"EPSG\",\"8901\"]],\n" - " UNIT[\"degree\",0.0174532925199433,\n" - " AUTHORITY[\"EPSG\",\"9122\"]],\n" - " AUTHORITY[\"EPSG\",\"4269\"]],\n" - " VERT_CS[\"NAVD88 height - Geoid12B (Meters)\",\n" - " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" - " EXTENSION[\"PROJ4_GRIDS\",\"@foo.gtx\"],\n" - " AUTHORITY[\"EPSG\",\"5103\"]],\n" - " UNIT[\"metre\",1.0,\n" - " AUTHORITY[\"EPSG\",\"9001\"]],\n" - " AXIS[\"Gravity-related height\",UP],\n" - " AUTHORITY[\"EPSG\",\"5703\"]]]"; - auto objSrc = - WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktSrc); - auto srcCRS = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(srcCRS != nullptr); - - const char *wktDst = - "COMPD_CS[\"NAD83 + Ellipsoid (US Feet)\",\n" - " GEOGCS[\"NAD83\",\n" - " DATUM[\"North_American_Datum_1983\",\n" - " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" - " AUTHORITY[\"EPSG\",\"7019\"]],\n" - " TOWGS84[0,0,0,0,0,0,0],\n" - " AUTHORITY[\"EPSG\",\"6269\"]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " AUTHORITY[\"EPSG\",\"8901\"]],\n" - " UNIT[\"degree\",0.0174532925199433,\n" - " AUTHORITY[\"EPSG\",\"9122\"]],\n" - " AUTHORITY[\"EPSG\",\"4269\"]],\n" - " VERT_CS[\"Ellipsoid (US Feet)\",\n" - " VERT_DATUM[\"Ellipsoid\",2002],\n" - " UNIT[\"US survey foot\",0.304800609601219,\n" - " AUTHORITY[\"EPSG\",\"9003\"]],\n" - " AXIS[\"Up\",UP]]]"; - auto objDst = - WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktDst); - auto dstCRS = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dstCRS != nullptr); - - auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcCRS), NN_NO_CHECK(dstCRS), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=@foo.gtx +multiplier=1 " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=rad +z_in=m " - "+xy_out=deg +z_out=us-ft"); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - compoundCRS_with_boundVerticalCRS_from_grids_to_boundGeogCRS_with_ftus_ctxt) { - - // Variant of above but with TOWGS84 in target CRS only - - auto dbContext = DatabaseContext::create(); - - const char *wktSrc = - "COMPD_CS[\"NAD83 + NAVD88 height - Geoid12B (Meters)\",\n" - " GEOGCS[\"NAD83\",\n" - " DATUM[\"North_American_Datum_1983\",\n" - " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" - " AUTHORITY[\"EPSG\",\"7019\"]],\n" - " AUTHORITY[\"EPSG\",\"6269\"]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " AUTHORITY[\"EPSG\",\"8901\"]],\n" - " UNIT[\"degree\",0.0174532925199433,\n" - " AUTHORITY[\"EPSG\",\"9122\"]],\n" - " AUTHORITY[\"EPSG\",\"4269\"]],\n" - " VERT_CS[\"NAVD88 height - Geoid12B (Meters)\",\n" - " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" - " EXTENSION[\"PROJ4_GRIDS\",\"@foo.gtx\"],\n" - " AUTHORITY[\"EPSG\",\"5103\"]],\n" - " UNIT[\"metre\",1.0,\n" - " AUTHORITY[\"EPSG\",\"9001\"]],\n" - " AXIS[\"Gravity-related height\",UP],\n" - " AUTHORITY[\"EPSG\",\"5703\"]]]"; - auto objSrc = - WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktSrc); - auto srcCRS = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(srcCRS != nullptr); - - const char *wktDst = - "COMPD_CS[\"NAD83 + Ellipsoid (US Feet)\",\n" - " GEOGCS[\"NAD83\",\n" - " DATUM[\"North_American_Datum_1983\",\n" - " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" - " AUTHORITY[\"EPSG\",\"7019\"]],\n" - " TOWGS84[0,0,0,0,0,0,0],\n" - " AUTHORITY[\"EPSG\",\"6269\"]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " AUTHORITY[\"EPSG\",\"8901\"]],\n" - " UNIT[\"degree\",0.0174532925199433,\n" - " AUTHORITY[\"EPSG\",\"9122\"]],\n" - " AUTHORITY[\"EPSG\",\"4269\"]],\n" - " VERT_CS[\"Ellipsoid (US Feet)\",\n" - " VERT_DATUM[\"Ellipsoid\",2002],\n" - " UNIT[\"US survey foot\",0.304800609601219,\n" - " AUTHORITY[\"EPSG\",\"9003\"]],\n" - " AXIS[\"Up\",UP]]]"; - auto objDst = - WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktDst); - auto dstCRS = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dstCRS != nullptr); - - auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcCRS), NN_NO_CHECK(dstCRS), ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=@foo.gtx +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +z_in=m " - "+xy_out=deg +z_out=us-ft " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - compoundCRS_with_boundGeogCRS_and_geoid_to_geodCRS_NAD2011_ctxt) { - - auto dbContext = DatabaseContext::create(); - - const char *wktSrc = - "COMPD_CS[\"NAD83 / California zone 5 (ftUS) + " - "NAVD88 height - Geoid12B (ftUS)\"," - " PROJCS[\"NAD83 / California zone 5 (ftUS)\"," - " GEOGCS[\"NAD83\"," - " DATUM[\"North_American_Datum_1983\"," - " SPHEROID[\"GRS 1980\",6378137,298.257222101," - " AUTHORITY[\"EPSG\",\"7019\"]]," - " TOWGS84[0,0,0,0,0,0,0]," - " AUTHORITY[\"EPSG\",\"6269\"]]," - " PRIMEM[\"Greenwich\",0," - " AUTHORITY[\"EPSG\",\"8901\"]]," - " UNIT[\"degree\",0.0174532925199433," - " AUTHORITY[\"EPSG\",\"9122\"]]," - " AUTHORITY[\"EPSG\",\"4269\"]]," - " PROJECTION[\"Lambert_Conformal_Conic_2SP\"]," - " PARAMETER[\"standard_parallel_1\",35.46666666666667]," - " PARAMETER[\"standard_parallel_2\",34.03333333333333]," - " PARAMETER[\"latitude_of_origin\",33.5]," - " PARAMETER[\"central_meridian\",-118]," - " PARAMETER[\"false_easting\",6561666.667]," - " PARAMETER[\"false_northing\",1640416.667]," - " UNIT[\"US survey foot\",0.3048006096012192," - " AUTHORITY[\"EPSG\",\"9003\"]]," - " AXIS[\"X\",EAST]," - " AXIS[\"Y\",NORTH]," - " AUTHORITY[\"EPSG\",\"2229\"]]," - "VERT_CS[\"NAVD88 height - Geoid12B (ftUS)\"," - " VERT_DATUM[\"North American Vertical Datum 1988\",2005," - " AUTHORITY[\"EPSG\",\"5103\"]]," - " UNIT[\"US survey foot\",0.3048006096012192," - " AUTHORITY[\"EPSG\",\"9003\"]]," - " AXIS[\"Gravity-related height\",UP]," - " AUTHORITY[\"EPSG\",\"6360\"]]]"; - auto objSrc = - WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktSrc); - auto srcCRS = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(srcCRS != nullptr); - - auto authFactoryEPSG = AuthorityFactory::create(dbContext, "EPSG"); - // NAD83(2011) geocentric - auto dstCRS = authFactoryEPSG->createCoordinateReferenceSystem("6317"); - - auto authFactory = AuthorityFactory::create(dbContext, std::string()); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcCRS), dstCRS, ctxt); - bool found = false; - for (const auto &op : list) { - if (op->nameStr() == - "Inverse of unnamed + " - "Transformation from NAD83 to WGS84 + " - "Ballpark geographic offset from WGS 84 to NAD83(2011) + " - "Transformation from NAVD88 height (ftUS) to NAVD88 height + " - "Inverse of NAD83(2011) to NAVD88 height (1) + " - "Conversion from NAD83(2011) (geog3D) to NAD83(2011) " - "(geocentric)") { - found = true; - EXPECT_EQ( - op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=unitconvert +xy_in=us-ft +xy_out=m " - "+step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 " - "+lat_1=35.4666666666667 +lat_2=34.0333333333333 " - "+x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 " - "+step +proj=unitconvert +z_in=us-ft +z_out=m " - "+step +proj=vgridshift +grids=us_noaa_g2012bu0.tif " - "+multiplier=1 " - "+step +proj=cart +ellps=GRS80"); - } - } - EXPECT_TRUE(found); - if (!found) { - for (const auto &op : list) { - std::cerr << op->nameStr() << std::endl; - } - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, geocent_to_compoundCRS) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=geocent +datum=WGS84 +units=m +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " - "+type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 +step +inv " - "+proj=vgridshift +grids=@foo.gtx +multiplier=1 +step +inv " - "+proj=hgridshift +grids=@foo.gsb +step +proj=unitconvert " - "+xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, geocent_to_compoundCRS_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - // WGS84 geocentric - auto src = authFactory->createCoordinateReferenceSystem("4978"); - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " - "+type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - src, NN_CHECK_ASSERT(dst), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 +step +inv " - "+proj=vgridshift +grids=@foo.gtx +multiplier=1 +step +inv " - "+proj=hgridshift +grids=@foo.gsb +step +proj=unitconvert " - "+xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_compoundCRS) { - auto compound1 = CompoundCRS::create( - PropertyMap(), - std::vector{createUTM31_WGS84(), createVerticalCRS()}); - auto compound2 = CompoundCRS::create( - PropertyMap(), - std::vector{createUTM32_WGS84(), createVerticalCRS()}); - auto op = CoordinateOperationFactory::create()->createOperation(compound1, - compound2); - ASSERT_TRUE(op != nullptr); - auto opRef = CoordinateOperationFactory::create()->createOperation( - createUTM31_WGS84(), createUTM32_WGS84()); - ASSERT_TRUE(opRef != nullptr); - EXPECT_TRUE(op->isEquivalentTo(opRef.get())); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_compoundCRS_with_vertical_transform) { - auto verticalCRS1 = createVerticalCRS(); - - auto verticalCRS2 = VerticalCRS::create( - PropertyMap(), VerticalReferenceFrame::create(PropertyMap()), - VerticalCS::createGravityRelatedHeight(UnitOfMeasure::METRE)); - - // Use of this type of transformation is a bit of non-sense here - // since it should normally be used with NGVD29 and NAVD88 for VerticalCRS, - // and NAD27/NAD83 as horizontal CRS... - auto vtransformation = Transformation::createVERTCON( - PropertyMap(), verticalCRS1, verticalCRS2, "bla.gtx", - std::vector()); - - auto compound1 = CompoundCRS::create( - PropertyMap(), - std::vector{ - ProjectedCRS::create( - PropertyMap(), GeographicCRS::EPSG_4326, - Conversion::createTransverseMercator(PropertyMap(), Angle(1), - Angle(2), Scale(3), - Length(4), Length(5)), - CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)), - BoundCRS::create(verticalCRS1, verticalCRS2, vtransformation)}); - auto compound2 = CompoundCRS::create( - PropertyMap(), - std::vector{createUTM32_WGS84(), verticalCRS2}); - - auto op = CoordinateOperationFactory::create()->createOperation(compound1, - compound2); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=tmerc +lat_0=1 +lon_0=2 +k=3 " - "+x_0=4 +y_0=5 +ellps=WGS84 +step " - "+proj=vgridshift +grids=bla.gtx +multiplier=0.001 +step " - "+proj=utm +zone=32 " - "+ellps=WGS84"); - { - auto formatter = PROJStringFormatter::create(); - formatter->setUseApproxTMerc(true); - EXPECT_EQ( - op->exportToPROJString(formatter.get()), - "+proj=pipeline +step +inv +proj=tmerc +approx +lat_0=1 +lon_0=2 " - "+k=3 +x_0=4 +y_0=5 +ellps=WGS84 +step " - "+proj=vgridshift +grids=bla.gtx +multiplier=0.001 +step " - "+proj=utm +approx +zone=32 " - "+ellps=WGS84"); - } - { - auto formatter = PROJStringFormatter::create(); - formatter->setUseApproxTMerc(true); - EXPECT_EQ( - op->inverse()->exportToPROJString(formatter.get()), - "+proj=pipeline +step +inv +proj=utm +approx +zone=32 +ellps=WGS84 " - "+step +inv +proj=vgridshift +grids=bla.gtx " - "+multiplier=0.001 +step +proj=tmerc +approx +lat_0=1 +lon_0=2 " - "+k=3 +x_0=4 +y_0=5 +ellps=WGS84"); - } - - auto opInverse = CoordinateOperationFactory::create()->createOperation( - compound2, compound1); - ASSERT_TRUE(opInverse != nullptr); - { - auto formatter = PROJStringFormatter::create(); - auto formatter2 = PROJStringFormatter::create(); - EXPECT_EQ(opInverse->inverse()->exportToPROJString(formatter.get()), - op->exportToPROJString(formatter2.get())); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_compoundCRS_with_bound_crs_in_horiz_and_vert) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " - "+type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS80 +nadgrids=@bar.gsb +geoidgrids=@bar.gtx " - "+type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - 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=@foo.gsb " - "+step +proj=vgridshift +grids=@foo.gtx +multiplier=1 " - "+step +inv +proj=vgridshift +grids=@bar.gtx +multiplier=1 " - "+step +inv +proj=hgridshift +grids=@bar.gsb " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - compoundCRS_to_compoundCRS_with_bound_crs_in_horiz_and_vert_same_geoidgrids) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " - "+type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS80 +nadgrids=@bar.gsb +geoidgrids=@foo.gtx " - "+type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - 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=@foo.gsb " - "+step +inv +proj=hgridshift +grids=@bar.gsb " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - compoundCRS_to_compoundCRS_with_bound_crs_in_horiz_and_vert_same_geoidgrids_different_vunits) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " - "+type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS80 +nadgrids=@bar.gsb +geoidgrids=@foo.gtx " - "+vunits=us-ft +type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - 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=@foo.gsb " - "+step +proj=unitconvert +z_in=m +z_out=us-ft " - "+step +inv +proj=hgridshift +grids=@bar.gsb " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - compoundCRS_to_compoundCRS_with_bound_crs_in_horiz_and_vert_same_nadgrids_same_geoidgrids) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " - "+type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS80 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " - "+type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=noop"); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - compoundCRS_to_compoundCRS_with_bound_crs_in_horiz_and_vert_same_towgs84_same_geoidgrids) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS67 +towgs84=0,0,0 +geoidgrids=@foo.gtx " - "+type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS80 +towgs84=0,0,0 +geoidgrids=@foo.gtx " - "+type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=push +v_3 " - "+step +proj=cart +ellps=GRS67 " - "+step +inv +proj=cart +ellps=GRS80 " - "+step +proj=pop +v_3 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - compoundCRS_to_compoundCRS_with_bound_crs_in_horiz_and_vert_WKT1_same_geoidgrids_context) { - auto objSrc = WKTParser().createFromWKT( - "COMPD_CS[\"NAD83 / Alabama West + NAVD88 height - Geoid12B " - "(Meters)\",\n" - " PROJCS[\"NAD83 / Alabama West\",\n" - " GEOGCS[\"NAD83\",\n" - " DATUM[\"North_American_Datum_1983\",\n" - " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" - " AUTHORITY[\"EPSG\",\"7019\"]],\n" - " TOWGS84[0,0,0,0,0,0,0],\n" - " AUTHORITY[\"EPSG\",\"6269\"]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " AUTHORITY[\"EPSG\",\"8901\"]],\n" - " UNIT[\"degree\",0.0174532925199433,\n" - " AUTHORITY[\"EPSG\",\"9122\"]],\n" - " AUTHORITY[\"EPSG\",\"4269\"]],\n" - " PROJECTION[\"Transverse_Mercator\"],\n" - " PARAMETER[\"latitude_of_origin\",30],\n" - " PARAMETER[\"central_meridian\",-87.5],\n" - " PARAMETER[\"scale_factor\",0.999933333],\n" - " PARAMETER[\"false_easting\",600000],\n" - " PARAMETER[\"false_northing\",0],\n" - " UNIT[\"metre\",1,\n" - " AUTHORITY[\"EPSG\",\"9001\"]],\n" - " AXIS[\"X\",EAST],\n" - " AXIS[\"Y\",NORTH],\n" - " AUTHORITY[\"EPSG\",\"26930\"]],\n" - " VERT_CS[\"NAVD88 height\",\n" - " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" - " " - "EXTENSION[\"PROJ4_GRIDS\",\"g2012a_alaska.gtx,g2012a_hawaii.gtx," - "g2012a_conus.gtx\"],\n" - " AUTHORITY[\"EPSG\",\"5103\"]],\n" - " UNIT[\"metre\",1,\n" - " AUTHORITY[\"EPSG\",\"9001\"]],\n" - " AXIS[\"Gravity-related height\",UP],\n" - " AUTHORITY[\"EPSG\",\"5703\"]]]"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto objDst = WKTParser().createFromWKT( - "COMPD_CS[\"NAD_1983_StatePlane_Alabama_West_FIPS_0102_Feet + NAVD88 " - "height - Geoid12B (US Feet)\",\n" - " PROJCS[\"NAD_1983_StatePlane_Alabama_West_FIPS_0102_Feet\",\n" - " GEOGCS[\"NAD83\",\n" - " DATUM[\"North_American_Datum_1983\",\n" - " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" - " AUTHORITY[\"EPSG\",\"7019\"]],\n" - " TOWGS84[0,0,0,0,0,0,0],\n" - " AUTHORITY[\"EPSG\",\"6269\"]],\n" - " PRIMEM[\"Greenwich\",0],\n" - " UNIT[\"Degree\",0.0174532925199433]],\n" - " PROJECTION[\"Transverse_Mercator\"],\n" - " PARAMETER[\"latitude_of_origin\",30],\n" - " PARAMETER[\"central_meridian\",-87.5],\n" - " PARAMETER[\"scale_factor\",0.999933333333333],\n" - " PARAMETER[\"false_easting\",1968500],\n" - " PARAMETER[\"false_northing\",0],\n" - " UNIT[\"US survey foot\",0.304800609601219,\n" - " AUTHORITY[\"EPSG\",\"9003\"]],\n" - " AXIS[\"Easting\",EAST],\n" - " AXIS[\"Northing\",NORTH],\n" - " AUTHORITY[\"ESRI\",\"102630\"]],\n" - " VERT_CS[\"NAVD88 height (ftUS)\",\n" - " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" - " " - "EXTENSION[\"PROJ4_GRIDS\",\"g2012a_alaska.gtx,g2012a_hawaii.gtx," - "g2012a_conus.gtx\"],\n" - " AUTHORITY[\"EPSG\",\"5103\"]],\n" - " UNIT[\"US survey foot\",0.304800609601219,\n" - " AUTHORITY[\"EPSG\",\"9003\"]],\n" - " AXIS[\"Gravity-related height\",UP],\n" - " AUTHORITY[\"EPSG\",\"6360\"]]]"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - - auto dbContext = DatabaseContext::create(); - auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), - "Inverse of unnamed + " - "Transformation from NAD83 to WGS84 + " - "NAVD88 height to NAVD88 height (ftUS) + " - "Inverse of Transformation from NAD83 to WGS84 + " - "unnamed"); - auto grids = list[0]->gridsNeeded(dbContext, false); - EXPECT_TRUE(grids.empty()); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +inv +proj=tmerc +lat_0=30 +lon_0=-87.5 +k=0.999933333 " - "+x_0=600000 +y_0=0 +ellps=GRS80 " - "+step +proj=unitconvert +z_in=m +z_out=us-ft " - "+step +proj=tmerc +lat_0=30 +lon_0=-87.5 +k=0.999933333333333 " - "+x_0=600000 +y_0=0 +ellps=GRS80 " - "+step +proj=unitconvert +xy_in=m +xy_out=us-ft"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_compoundCRS_issue_2232) { - auto objSrc = WKTParser().createFromWKT( - "COMPD_CS[\"NAD83 / Alabama West + NAVD88 height - Geoid12B " - "(Meters)\",\n" - " PROJCS[\"NAD83 / Alabama West\",\n" - " GEOGCS[\"NAD83\",\n" - " DATUM[\"North_American_Datum_1983\",\n" - " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" - " AUTHORITY[\"EPSG\",\"7019\"]],\n" - " TOWGS84[0,0,0,0,0,0,0],\n" - " AUTHORITY[\"EPSG\",\"6269\"]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " AUTHORITY[\"EPSG\",\"8901\"]],\n" - " UNIT[\"degree\",0.0174532925199433,\n" - " AUTHORITY[\"EPSG\",\"9122\"]],\n" - " AUTHORITY[\"EPSG\",\"4269\"]],\n" - " PROJECTION[\"Transverse_Mercator\"],\n" - " PARAMETER[\"latitude_of_origin\",30],\n" - " PARAMETER[\"central_meridian\",-87.5],\n" - " PARAMETER[\"scale_factor\",0.999933333],\n" - " PARAMETER[\"false_easting\",600000],\n" - " PARAMETER[\"false_northing\",0],\n" - " UNIT[\"metre\",1,\n" - " AUTHORITY[\"EPSG\",\"9001\"]],\n" - " AXIS[\"X\",EAST],\n" - " AXIS[\"Y\",NORTH],\n" - " AUTHORITY[\"EPSG\",\"26930\"]],\n" - " VERT_CS[\"NAVD88 height - Geoid12B (Meters)\",\n" - " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" - " EXTENSION[\"PROJ4_GRIDS\",\"foo.gtx\"],\n" - " AUTHORITY[\"EPSG\",\"5103\"]],\n" - " UNIT[\"metre\",1.0,\n" - " AUTHORITY[\"EPSG\",\"9001\"]],\n" - " AXIS[\"Gravity-related height\",UP],\n" - " AUTHORITY[\"EPSG\",\"5703\"]]]"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - - auto objDst = WKTParser().createFromWKT( - "COMPD_CS[\"NAD83 + some CRS (US Feet)\",\n" - " GEOGCS[\"NAD83\",\n" - " DATUM[\"North_American_Datum_1983\",\n" - " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" - " AUTHORITY[\"EPSG\",\"7019\"]],\n" - " TOWGS84[0,0,0,0,0,0,0],\n" - " AUTHORITY[\"EPSG\",\"6269\"]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " AUTHORITY[\"EPSG\",\"8901\"]],\n" - " UNIT[\"degree\",0.0174532925199433,\n" - " AUTHORITY[\"EPSG\",\"9122\"]],\n" - " AUTHORITY[\"EPSG\",\"4269\"]],\n" - " VERT_CS[\"some CRS (US Feet)\",\n" - " VERT_DATUM[\"some datum\",2005],\n" - " UNIT[\"US survey foot\",0.3048006096012192,\n" - " AUTHORITY[\"EPSG\",\"9003\"]],\n" - " AXIS[\"Up\",UP]]]"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - - auto dbContext = DatabaseContext::create(); - auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - - auto list = CoordinateOperationFactory::create()->createOperations( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst), ctxt); - EXPECT_GE(list.size(), 1U); - - auto list2 = CoordinateOperationFactory::create()->createOperations( - NN_CHECK_ASSERT(dst), NN_CHECK_ASSERT(src), ctxt); - EXPECT_EQ(list2.size(), list.size()); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_compoundCRS_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - // NAD27 + NGVD29 height (ftUS) - authFactory->createCoordinateReferenceSystem("7406"), - // NAD83(NSRS2007) + NAVD88 height - authFactory->createCoordinateReferenceSystem("5500"), ctxt); - // 152 or 155 depending if the VERTCON grids are there - ASSERT_GE(list.size(), 152U); - EXPECT_FALSE(list[0]->hasBallparkTransformation()); - EXPECT_EQ(list[0]->nameStr(), "NGVD29 height (ftUS) to NAVD88 height (3) + " - "NAD27 to WGS 84 (79) + Inverse of " - "NAD83(NSRS2007) to WGS 84 (1)"); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +z_in=us-ft +xy_out=rad +z_out=m " - "+step +proj=vgridshift +grids=us_noaa_vertcone.tif +multiplier=1 " - "+step +proj=hgridshift +grids=us_noaa_conus.tif +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " - "+order=2,1"); - { - // Test that we can round-trip this through WKT and still get the same - // PROJ string. - auto wkt = list[0]->exportToWKT( - WKTFormatter::create(WKTFormatter::Convention::WKT2_2019).get()); - auto obj = WKTParser().createFromWKT(wkt); - auto co = nn_dynamic_pointer_cast(obj); - ASSERT_TRUE(co != nullptr); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - co->exportToPROJString(PROJStringFormatter::create().get())); - } - - bool foundApprox = false; - for (size_t i = 0; i < list.size(); i++) { - auto projString = - list[i]->exportToPROJString(PROJStringFormatter::create().get()); - EXPECT_TRUE( - projString.find("+proj=pipeline +step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +z_in=us-ft " - "+xy_out=rad +z_out=m") == 0) - << list[i]->nameStr(); - if (list[i]->nameStr().find("Transformation from NGVD29 height (ftUS) " - "to NAVD88 height (ballpark vertical " - "transformation)") == 0) { - EXPECT_TRUE(list[i]->hasBallparkTransformation()); - EXPECT_EQ(list[i]->nameStr(), - "Transformation from NGVD29 height (ftUS) to NAVD88 " - "height (ballpark vertical transformation) + NAD27 to " - "WGS 84 (79) + Inverse of NAD83(NSRS2007) to WGS 84 (1)"); - EXPECT_EQ( - projString, - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +z_in=us-ft +xy_out=rad " - "+z_out=m +step +proj=hgridshift +grids=us_noaa_conus.tif " - "+step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); - foundApprox = true; - break; - } - } - EXPECT_TRUE(foundApprox); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_compoundCRS_context_helmert_noop) { - auto dbContext = DatabaseContext::create(); - auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - // WGS84 + EGM96 - auto objSrc = createFromUserInput("EPSG:4326+5773", dbContext); - auto srcCrs = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(srcCrs != nullptr); - // ETRS89 + EGM96 - auto objDest = createFromUserInput("EPSG:4258+5773", dbContext); - auto destCrs = nn_dynamic_pointer_cast(objDest); - ASSERT_TRUE(destCrs != nullptr); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcCrs), NN_NO_CHECK(destCrs), ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=noop"); -} - -// --------------------------------------------------------------------------- - -// EGM96 has a geoid model referenced to WGS84, and Belfast height has a -// geoid model referenced to ETRS89 -TEST(operation, compoundCRS_to_compoundCRS_WGS84_EGM96_to_ETRS89_Belfast) { - auto dbContext = DatabaseContext::create(); - auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - // WGS84 + EGM96 - auto objSrc = createFromUserInput("EPSG:4326+5773", dbContext); - auto srcCrs = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(srcCrs != nullptr); - // ETRS89 + Belfast height - auto objDest = createFromUserInput("EPSG:4258+5732", dbContext); - auto destCrs = nn_dynamic_pointer_cast(objDest); - ASSERT_TRUE(destCrs != nullptr); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcCrs), NN_NO_CHECK(destCrs), ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), "Inverse of WGS 84 to EGM96 height (1) + " - "Inverse of ETRS89 to WGS 84 (1) + " - "ETRS89 to Belfast height (2)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=us_nga_egm96_15.tif +multiplier=1 " - "+step +inv +proj=vgridshift +grids=uk_os_OSGM15_Belfast.tif " - "+multiplier=1 +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -// Variant of above where source intermediate geog3D CRS == target intermediate -// geog3D CRS -TEST(operation, compoundCRS_to_compoundCRS_WGS84_EGM96_to_WGS84_Belfast) { - auto dbContext = DatabaseContext::create(); - auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - // WGS84 + EGM96 - auto objSrc = createFromUserInput("EPSG:4326+5773", dbContext); - auto srcCrs = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(srcCrs != nullptr); - // WGS84 + Belfast height - auto objDest = createFromUserInput("EPSG:4326+5732", dbContext); - auto destCrs = nn_dynamic_pointer_cast(objDest); - ASSERT_TRUE(destCrs != nullptr); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcCrs), NN_NO_CHECK(destCrs), ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), "Inverse of WGS 84 to EGM96 height (1) + " - "Inverse of ETRS89 to WGS 84 (1) + " - "ETRS89 to Belfast height (2) + " - "ETRS89 to WGS 84 (1)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=us_nga_egm96_15.tif +multiplier=1 " - "+step +inv +proj=vgridshift +grids=uk_os_OSGM15_Belfast.tif " - "+multiplier=1 +step " - "+proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - compoundCRS_to_compoundCRS_concatenated_operation_with_two_vert_transformation) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - // ETRS89 + Baltic 1957 height - authFactory->createCoordinateReferenceSystem("8360"), - // ETRS89 + EVRF2007 height - authFactory->createCoordinateReferenceSystem("7423"), ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift " - "+grids=sk_gku_Slovakia_ETRS89h_to_Baltic1957.tif +multiplier=1 " - "+step +inv +proj=vgridshift " - "+grids=sk_gku_Slovakia_ETRS89h_to_EVRF2007.tif +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - EXPECT_EQ( - list[0]->nameStr(), - "ETRS89 + Baltic 1957 height to ETRS89 + EVRF2007 height (1)"); - EXPECT_EQ(list[0]->inverse()->nameStr(), "Inverse of 'ETRS89 + Baltic " - "1957 height to ETRS89 + " - "EVRF2007 height (1)'"); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, vertCRS_to_vertCRS) { - - auto vertcrs_m_obj = PROJStringParser().createFromPROJString("+vunits=m"); - auto vertcrs_m = nn_dynamic_pointer_cast(vertcrs_m_obj); - ASSERT_TRUE(vertcrs_m != nullptr); - - auto vertcrs_ft_obj = PROJStringParser().createFromPROJString("+vunits=ft"); - auto vertcrs_ft = nn_dynamic_pointer_cast(vertcrs_ft_obj); - ASSERT_TRUE(vertcrs_ft != nullptr); - - auto vertcrs_us_ft_obj = - PROJStringParser().createFromPROJString("+vunits=us-ft"); - auto vertcrs_us_ft = - nn_dynamic_pointer_cast(vertcrs_us_ft_obj); - ASSERT_TRUE(vertcrs_us_ft != nullptr); - - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(vertcrs_m), NN_CHECK_ASSERT(vertcrs_ft)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=unitconvert +z_in=m +z_out=ft"); - } - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(vertcrs_m), NN_CHECK_ASSERT(vertcrs_ft)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->inverse()->exportToPROJString( - PROJStringFormatter::create().get()), - "+proj=unitconvert +z_in=ft +z_out=m"); - } - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(vertcrs_ft), NN_CHECK_ASSERT(vertcrs_m)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=unitconvert +z_in=ft +z_out=m"); - } - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(vertcrs_ft), NN_CHECK_ASSERT(vertcrs_us_ft)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=affine +s33=0.999998"); - } - - auto vertCRSMetreUp = - nn_dynamic_pointer_cast(WKTParser().createFromWKT( - "VERTCRS[\"my height\",VDATUM[\"my datum\"],CS[vertical,1]," - "AXIS[\"gravity-related height (H)\",up," - "LENGTHUNIT[\"metre\",1]]]")); - ASSERT_TRUE(vertCRSMetreUp != nullptr); - - auto vertCRSMetreDown = - nn_dynamic_pointer_cast(WKTParser().createFromWKT( - "VERTCRS[\"my depth\",VDATUM[\"my datum\"],CS[vertical,1]," - "AXIS[\"depth (D)\",down,LENGTHUNIT[\"metre\",1]]]")); - ASSERT_TRUE(vertCRSMetreDown != nullptr); - - auto vertCRSMetreDownFtUS = - nn_dynamic_pointer_cast(WKTParser().createFromWKT( - "VERTCRS[\"my depth (ftUS)\",VDATUM[\"my datum\"],CS[vertical,1]," - "AXIS[\"depth (D)\",down,LENGTHUNIT[\"US survey " - "foot\",0.304800609601219]]]")); - ASSERT_TRUE(vertCRSMetreDownFtUS != nullptr); - - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(vertCRSMetreUp), NN_CHECK_ASSERT(vertCRSMetreDown)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=axisswap +order=1,2,-3"); - } - - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(vertCRSMetreUp), - NN_CHECK_ASSERT(vertCRSMetreDownFtUS)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=affine +s33=-3.28083333333333"); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, vertCRS_to_vertCRS_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - // NGVD29 height (m) - authFactory->createCoordinateReferenceSystem("7968"), - // NAVD88 height (1) - authFactory->createCoordinateReferenceSystem("5703"), ctxt); - ASSERT_EQ(list.size(), 3U); - EXPECT_EQ(list[0]->nameStr(), "NGVD29 height (m) to NAVD88 height (3)"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=vgridshift +grids=us_noaa_vertcone.tif +multiplier=1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, vertCRS_to_vertCRS_New_Zealand_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - // NZVD2016 height - authFactory->createCoordinateReferenceSystem("7839"), - // Auckland 1946 height - authFactory->createCoordinateReferenceSystem("5759"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=vgridshift +grids=nz_linz_auckht1946-nzvd2016.tif " - "+multiplier=1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, projCRS_3D_to_geogCRS_3D) { - - auto compoundcrs_ft_obj = PROJStringParser().createFromPROJString( - "+proj=merc +vunits=ft +type=crs"); - auto proj3DCRS_ft = nn_dynamic_pointer_cast(compoundcrs_ft_obj); - ASSERT_TRUE(proj3DCRS_ft != nullptr); - - auto geogcrs_m_obj = PROJStringParser().createFromPROJString( - "+proj=longlat +vunits=m +type=crs"); - auto geogcrs_m = nn_dynamic_pointer_cast(geogcrs_m_obj); - ASSERT_TRUE(geogcrs_m != nullptr); - - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(proj3DCRS_ft), NN_CHECK_ASSERT(geogcrs_m)); - ASSERT_TRUE(op != nullptr); - EXPECT_FALSE(op->hasBallparkTransformation()); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=unitconvert +xy_in=m +z_in=ft " - "+xy_out=m +z_out=m " - "+step +inv +proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 " - "+ellps=WGS84 " - "+step +proj=unitconvert +xy_in=rad +z_in=m " - "+xy_out=deg +z_out=m"); - } - - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(geogcrs_m), NN_CHECK_ASSERT(proj3DCRS_ft)); - ASSERT_TRUE(op != nullptr); - EXPECT_FALSE(op->hasBallparkTransformation()); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=unitconvert +z_in=m +z_out=ft " - "+step +proj=unitconvert +xy_in=deg +z_in=ft " - "+xy_out=rad +z_out=m " - "+step +proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 " - "+step +proj=unitconvert +xy_in=m +z_in=m " - "+xy_out=m +z_out=ft"); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_geogCRS_3D) { - - auto compoundcrs_ft_obj = WKTParser().createFromWKT( - "COMPOUNDCRS[\"unknown\",\n" - " PROJCRS[\"unknown\",\n" - " BASEGEOGCRS[\"unknown\",\n" - " DATUM[\"World Geodetic System 1984\",\n" - " ELLIPSOID[\"WGS 84\",6378137,298.257223563,\n" - " LENGTHUNIT[\"metre\",1]],\n" - " ID[\"EPSG\",6326]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8901]]],\n" - " CONVERSION[\"unknown\",\n" - " METHOD[\"Mercator (variant A)\",\n" - " ID[\"EPSG\",9804]],\n" - " PARAMETER[\"Latitude of natural origin\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8801]],\n" - " PARAMETER[\"Longitude of natural origin\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8802]],\n" - " PARAMETER[\"Scale factor at natural origin\",1,\n" - " SCALEUNIT[\"unity\",1],\n" - " ID[\"EPSG\",8805]],\n" - " PARAMETER[\"False easting\",0,\n" - " LENGTHUNIT[\"metre\",1],\n" - " ID[\"EPSG\",8806]],\n" - " PARAMETER[\"False northing\",0,\n" - " LENGTHUNIT[\"metre\",1],\n" - " ID[\"EPSG\",8807]]],\n" - " CS[Cartesian,2],\n" - " AXIS[\"(E)\",east,\n" - " ORDER[1],\n" - " LENGTHUNIT[\"metre\",1,\n" - " ID[\"EPSG\",9001]]],\n" - " AXIS[\"(N)\",north,\n" - " ORDER[2],\n" - " LENGTHUNIT[\"metre\",1,\n" - " ID[\"EPSG\",9001]]]],\n" - " VERTCRS[\"unknown\",\n" - " VDATUM[\"unknown\"],\n" - " CS[vertical,1],\n" - " AXIS[\"gravity-related height (H)\",up,\n" - " LENGTHUNIT[\"foot\",0.3048,\n" - " ID[\"EPSG\",9002]]]]]"); - auto compoundcrs_ft = nn_dynamic_pointer_cast(compoundcrs_ft_obj); - ASSERT_TRUE(compoundcrs_ft != nullptr); - - auto geogcrs_m_obj = PROJStringParser().createFromPROJString( - "+proj=longlat +vunits=m +type=crs"); - auto geogcrs_m = nn_dynamic_pointer_cast(geogcrs_m_obj); - ASSERT_TRUE(geogcrs_m != nullptr); - - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(compoundcrs_ft), NN_CHECK_ASSERT(geogcrs_m)); - ASSERT_TRUE(op != nullptr); - EXPECT_TRUE(op->hasBallparkTransformation()); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=merc +lon_0=0 +k=1 +x_0=0 " - "+y_0=0 +ellps=WGS84 +step +proj=unitconvert +xy_in=rad " - "+z_in=ft +xy_out=deg +z_out=m"); - } - - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(geogcrs_m), NN_CHECK_ASSERT(compoundcrs_ft)); - ASSERT_TRUE(op != nullptr); - EXPECT_TRUE(op->hasBallparkTransformation()); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=unitconvert +xy_in=deg +z_in=m " - "+xy_out=rad +z_out=ft +step +proj=merc +lon_0=0 +k=1 +x_0=0 " - "+y_0=0 +ellps=WGS84"); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_geogCRS_3D_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - // CompoundCRS to Geog3DCRS, with vertical unit change, but without - // ellipsoid height <--> vertical height correction - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem( - "7406"), // NAD27 + NGVD29 height (ftUS) - authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 - ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_TRUE(list[0]->hasBallparkTransformation()); - EXPECT_EQ(list[0]->nameStr(), - "NAD27 to WGS 84 (79) + Transformation from NGVD29 height " - "(ftUS) to WGS 84 (ballpark vertical transformation, without " - "ellipsoid height to vertical height correction)"); - EXPECT_EQ(list[0]->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step " - "+proj=hgridshift +grids=us_noaa_conus.tif " - "+step +proj=unitconvert " - "+xy_in=rad +z_in=us-ft +xy_out=deg +z_out=m +step " - "+proj=axisswap +order=2,1"); - } - - // CompoundCRS to Geog3DCRS, with same vertical unit, and with - // direct ellipsoid height <--> vertical height correction and - // direct horizontal transform (no-op here) - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem( - "5500"), // NAD83(NSRS2007) + NAVD88 height - authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 - ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), - "Inverse of NAD83(NSRS2007) to NAVD88 height (1) + " - "NAD83(NSRS2007) to WGS 84 (1)"); - EXPECT_EQ(list[0]->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=us_noaa_geoid09_conus.tif " - "+multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - EXPECT_EQ(list[0]->remarks(), - "For NAD83(NSRS2007) to NAVD88 height (1) (EPSG:9173): Uses " - "Geoid09 hybrid model. Replaced by 2012 model (CT code 6326)." - "\n" - "For NAD83(NSRS2007) to WGS 84 (1) (EPSG:15931): " - "Approximation at the +/- 1m level assuming that " - "NAD83(NSRS2007) is equivalent to WGS 84 within the accuracy " - "of the transformation."); - } - - // NAD83 + NAVD88 height --> WGS 84 - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - // NAD83 + NAVD88 height - auto srcObj = createFromUserInput( - "EPSG:4269+5703", authFactory->databaseContext(), false); - auto src = nn_dynamic_pointer_cast(srcObj); - ASSERT_TRUE(src != nullptr); - auto nnSrc = NN_NO_CHECK(src); - - auto list = CoordinateOperationFactory::create()->createOperations( - nnSrc, - authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 - ctxt); - ASSERT_GE(list.size(), 2U); - - EXPECT_EQ(list[0]->nameStr(), - "NAD83 to WGS 84 (1) + " - "Inverse of NAD83(NSRS2007) to WGS 84 (1) + " - "Inverse of NAD83(NSRS2007) to NAVD88 height (1) + " - "NAD83(NSRS2007) to WGS 84 (1)"); - EXPECT_EQ(list[0]->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=us_noaa_geoid09_conus.tif " - "+multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - } - - // Another variation, but post horizontal adjustment is in two steps - { - auto ctxt = - CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - // NAD83(2011) + NAVD88 height - auto srcObj = createFromUserInput( - "EPSG:6318+5703", authFactory->databaseContext(), false); - auto src = nn_dynamic_pointer_cast(srcObj); - ASSERT_TRUE(src != nullptr); - auto nnSrc = NN_NO_CHECK(src); - - auto list = CoordinateOperationFactory::create()->createOperations( - nnSrc, - authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 - ctxt); - ASSERT_GE(list.size(), 2U); - - EXPECT_EQ(list[0]->nameStr(), - "Inverse of NAD83(2011) to NAVD88 height (3) + " - "Inverse of NAD83 to NAD83(2011) (1) + " - "NAD83 to WGS 84 (1)"); - EXPECT_EQ(list[0]->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=us_noaa_g2018u0.tif " - "+multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - - // Shows vertical step, and then horizontal step - EXPECT_EQ(list[1]->nameStr(), - "Inverse of NAD83(2011) to NAVD88 height (3) + " - "Inverse of NAD83 to NAD83(2011) (1) + " - "NAD83 to WGS 84 (18)"); - EXPECT_EQ(list[1]->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=us_noaa_g2018u0.tif " - "+multiplier=1 " - "+step +proj=hgridshift +grids=us_noaa_FL.tif " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_geogCRS_3D_with_3D_helmert_context) { - // Use case of https://github.com/OSGeo/PROJ/issues/2225 - auto dbContext = DatabaseContext::create(); - auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - // WGS84 + EGM96 height - auto srcObj = createFromUserInput("EPSG:4326+5773", dbContext, false); - auto src = nn_dynamic_pointer_cast(srcObj); - ASSERT_TRUE(src != nullptr); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(src), - // CH1903+ - authFactory->createCoordinateReferenceSystem("4150")->promoteTo3D( - std::string(), dbContext), - ctxt); - ASSERT_GE(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), "Inverse of WGS 84 to EGM96 height (1) + " - "Inverse of CH1903+ to WGS 84 (1)"); - // Check that there is no push v_3 / pop v_3 - const char *expected_proj = - "+proj=pipeline " - "+step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=vgridshift +grids=us_nga_egm96_15.tif +multiplier=1 " - "+step +proj=cart +ellps=WGS84 " - "+step +proj=helmert +x=-674.374 +y=-15.056 +z=-405.346 " - "+step +inv +proj=cart +ellps=bessel " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"; - EXPECT_EQ(list[0]->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, dbContext) - .get()), - expected_proj); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_geogCRS_2D_promote_to_3D_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - // NAD83 + NAVD88 height - auto srcObj = createFromUserInput("EPSG:4269+5703", - authFactory->databaseContext(), false); - auto src = nn_dynamic_pointer_cast(srcObj); - ASSERT_TRUE(src != nullptr); - auto nnSrc = NN_NO_CHECK(src); - auto dst = authFactory->createCoordinateReferenceSystem("4269"); // NAD83 - - auto listCompoundToGeog2D = - CoordinateOperationFactory::create()->createOperations(nnSrc, dst, - ctxt); - // The checked value is not that important, but in case this changes, - // likely due to a EPSG upgrade, worth checking - EXPECT_EQ(listCompoundToGeog2D.size(), 142U); - - auto listGeog2DToCompound = - CoordinateOperationFactory::create()->createOperations(dst, nnSrc, - ctxt); - EXPECT_EQ(listGeog2DToCompound.size(), listCompoundToGeog2D.size()); - - auto listCompoundToGeog3D = - CoordinateOperationFactory::create()->createOperations( - nnSrc, - dst->promoteTo3D(std::string(), authFactory->databaseContext()), - ctxt); - EXPECT_EQ(listCompoundToGeog3D.size(), listCompoundToGeog2D.size()); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_of_projCRS_to_geogCRS_2D_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - // SPCS83 California zone 1 (US Survey feet) + NAVD88 height (ftUS) - auto srcObj = createFromUserInput("EPSG:2225+6360", - authFactory->databaseContext(), false); - auto src = nn_dynamic_pointer_cast(srcObj); - ASSERT_TRUE(src != nullptr); - auto nnSrc = NN_NO_CHECK(src); - auto dst = authFactory->createCoordinateReferenceSystem("4269"); // NAD83 - - auto list = CoordinateOperationFactory::create()->createOperations( - nnSrc, dst, ctxt); - // The checked value is not that important, but in case this changes, - // likely due to a EPSG upgrade, worth checking - // We want to make sure that the horizontal adjustments before and after - // the vertical transformation are the reverse of each other, and there are - // not mixes with different alternative operations (like California grid - // forward and Nevada grid reverse) - ASSERT_EQ(list.size(), 14U); - - // Check that unit conversion is OK - auto op_proj = - list[0]->exportToPROJString(PROJStringFormatter::create().get()); - EXPECT_EQ(op_proj, - "+proj=pipeline " - "+step +proj=unitconvert +xy_in=us-ft +xy_out=m " - "+step +inv +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 " - "+lat_1=41.6666666666667 +lat_2=40 +x_0=2000000.0001016 " - "+y_0=500000.0001016 +ellps=GRS80 " - "+step +proj=unitconvert +z_in=us-ft +z_out=m " - "+step +proj=vgridshift +grids=us_noaa_geoid09_conus.tif " - "+multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_from_wkt_without_id_to_geogCRS) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - auto wkt = - "COMPOUNDCRS[\"NAD83(2011) + NAVD88 height\",\n" - " GEOGCRS[\"NAD83(2011)\",\n" - " DATUM[\"NAD83 (National Spatial Reference System 2011)\",\n" - " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " CS[ellipsoidal,2],\n" - " AXIS[\"geodetic latitude (Lat)\",north,\n" - " ORDER[1],\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" - " AXIS[\"geodetic longitude (Lon)\",east,\n" - " ORDER[2],\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]]],\n" - " VERTCRS[\"NAVD88 height\",\n" - " VDATUM[\"North American Vertical Datum 1988\"],\n" - " CS[vertical,1],\n" - " AXIS[\"gravity-related height (H)\",up,\n" - " LENGTHUNIT[\"metre\",1]]]]"; - auto srcObj = - createFromUserInput(wkt, authFactory->databaseContext(), false); - auto src = nn_dynamic_pointer_cast(srcObj); - ASSERT_TRUE(src != nullptr); - auto dst = - authFactory->createCoordinateReferenceSystem("6319"); // NAD83(2011) - - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(src), dst, ctxt); - // NAD83(2011) + NAVD88 height - auto srcRefObj = createFromUserInput("EPSG:6318+5703", - authFactory->databaseContext(), false); - auto srcRef = nn_dynamic_pointer_cast(srcRefObj); - ASSERT_TRUE(srcRef != nullptr); - ASSERT_TRUE( - src->isEquivalentTo(srcRef.get(), IComparable::Criterion::EQUIVALENT)); - auto listRef = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcRef), dst, ctxt); - - EXPECT_EQ(list.size(), listRef.size()); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - compoundCRS_of_projCRS_from_wkt_without_id_or_extent_to_geogCRS) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - auto wkt = - "COMPOUNDCRS[\"NAD83 / Pennsylvania South + NAVD88 height\",\n" - " PROJCRS[\"NAD83 / Pennsylvania South\",\n" - " BASEGEOGCRS[\"NAD83\",\n" - " DATUM[\"North American Datum 1983\",\n" - " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]]],\n" - " CONVERSION[\"SPCS83 Pennsylvania South zone (meters)\",\n" - " METHOD[\"Lambert Conic Conformal (2SP)\",\n" - " ID[\"EPSG\",9802]],\n" - " PARAMETER[\"Latitude of false origin\",39.3333333333333,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8821]],\n" - " PARAMETER[\"Longitude of false origin\",-77.75,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8822]],\n" - " PARAMETER[\"Latitude of 1st standard " - "parallel\",40.9666666666667,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8823]],\n" - " PARAMETER[\"Latitude of 2nd standard " - "parallel\",39.9333333333333,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8824]],\n" - " PARAMETER[\"Easting at false origin\",600000,\n" - " LENGTHUNIT[\"metre\",1],\n" - " ID[\"EPSG\",8826]],\n" - " PARAMETER[\"Northing at false origin\",0,\n" - " LENGTHUNIT[\"metre\",1],\n" - " ID[\"EPSG\",8827]]],\n" - " CS[Cartesian,2],\n" - " AXIS[\"easting (X)\",east,\n" - " ORDER[1],\n" - " LENGTHUNIT[\"metre\",1]],\n" - " AXIS[\"northing (Y)\",north,\n" - " ORDER[2],\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " VERTCRS[\"NAVD88 height\",\n" - " VDATUM[\"North American Vertical Datum 1988\"],\n" - " CS[vertical,1],\n" - " AXIS[\"gravity-related height (H)\",up,\n" - " LENGTHUNIT[\"metre\",1]]]]"; - auto srcObj = - createFromUserInput(wkt, authFactory->databaseContext(), false); - auto src = nn_dynamic_pointer_cast(srcObj); - ASSERT_TRUE(src != nullptr); - auto dst = authFactory->createCoordinateReferenceSystem("4269"); // NAD83 - - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(src), dst, ctxt); - // NAD83 / Pennsylvania South + NAVD88 height - auto srcRefObj = createFromUserInput("EPSG:32129+5703", - authFactory->databaseContext(), false); - auto srcRef = nn_dynamic_pointer_cast(srcRefObj); - ASSERT_TRUE(srcRef != nullptr); - ASSERT_TRUE( - src->isEquivalentTo(srcRef.get(), IComparable::Criterion::EQUIVALENT)); - auto listRef = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcRef), dst, ctxt); - - EXPECT_EQ(list.size(), listRef.size()); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_geogCRS_with_vertical_unit_change) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - // NAD83(2011) + NAVD88 height (ftUS) - auto srcObj = createFromUserInput("EPSG:6318+6360", - authFactory->databaseContext(), false); - auto src = nn_dynamic_pointer_cast(srcObj); - ASSERT_TRUE(src != nullptr); - auto nnSrc = NN_NO_CHECK(src); - auto dst = - authFactory->createCoordinateReferenceSystem("6319"); // NAD83(2011) 3D - - auto listCompoundToGeog = - CoordinateOperationFactory::create()->createOperations(nnSrc, dst, - ctxt); - ASSERT_TRUE(!listCompoundToGeog.empty()); - - // NAD83(2011) + NAVD88 height - auto srcObjCompoundVMetre = createFromUserInput( - "EPSG:6318+5703", authFactory->databaseContext(), false); - auto srcCompoundVMetre = nn_dynamic_pointer_cast(srcObjCompoundVMetre); - ASSERT_TRUE(srcCompoundVMetre != nullptr); - auto listCompoundMetreToGeog = - CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcCompoundVMetre), dst, ctxt); - - // Check that we get the same and similar results whether we start from - // regular NAVD88 height or its ftUs variant - ASSERT_EQ(listCompoundToGeog.size(), listCompoundMetreToGeog.size()); - - EXPECT_EQ(listCompoundToGeog[0]->nameStr(), - "Inverse of NAVD88 height to NAVD88 height (ftUS) + " + - listCompoundMetreToGeog[0]->nameStr()); - EXPECT_EQ( - listCompoundToGeog[0]->exportToPROJString( - PROJStringFormatter::create(PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - replaceAll(listCompoundMetreToGeog[0]->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - "+step +proj=unitconvert +xy_in=deg +xy_out=rad", - "+step +proj=unitconvert +xy_in=deg +z_in=us-ft +xy_out=rad " - "+z_out=m")); - - // Check reverse path - auto listGeogToCompound = - CoordinateOperationFactory::create()->createOperations(dst, nnSrc, - ctxt); - EXPECT_EQ(listGeogToCompound.size(), listCompoundToGeog.size()); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - compoundCRS_to_geogCRS_with_vertical_unit_change_and_complex_horizontal_change) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - // NAD83(2011) + NAVD88 height (ftUS) - auto srcObj = createFromUserInput("EPSG:6318+6360", - authFactory->databaseContext(), false); - auto src = nn_dynamic_pointer_cast(srcObj); - ASSERT_TRUE(src != nullptr); - auto nnSrc = NN_NO_CHECK(src); - auto dst = - authFactory->createCoordinateReferenceSystem("7665"); // WGS84(G1762) 3D - - auto listCompoundToGeog = - CoordinateOperationFactory::create()->createOperations(nnSrc, dst, - ctxt); - - // NAD83(2011) + NAVD88 height - auto srcObjCompoundVMetre = createFromUserInput( - "EPSG:6318+5703", authFactory->databaseContext(), false); - auto srcCompoundVMetre = nn_dynamic_pointer_cast(srcObjCompoundVMetre); - ASSERT_TRUE(srcCompoundVMetre != nullptr); - auto listCompoundMetreToGeog = - CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcCompoundVMetre), dst, ctxt); - - // Check that we get the same and similar results whether we start from - // regular NAVD88 height or its ftUs variant - ASSERT_EQ(listCompoundToGeog.size(), listCompoundMetreToGeog.size()); - - ASSERT_GE(listCompoundToGeog.size(), 1U); - - EXPECT_EQ(listCompoundToGeog[0]->nameStr(), - "Inverse of NAVD88 height to NAVD88 height (ftUS) + " + - listCompoundMetreToGeog[0]->nameStr()); - EXPECT_EQ( - listCompoundToGeog[0]->exportToPROJString( - PROJStringFormatter::create(PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - replaceAll(listCompoundMetreToGeog[0]->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - "+step +proj=unitconvert +xy_in=deg +xy_out=rad", - "+step +proj=unitconvert +xy_in=deg +z_in=us-ft +xy_out=rad " - "+z_out=m")); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_to_geogCRS_with_height_depth_reversal) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - // NAD83(2011) + NAVD88 depth - auto srcObj = createFromUserInput("EPSG:6318+6357", - authFactory->databaseContext(), false); - auto src = nn_dynamic_pointer_cast(srcObj); - ASSERT_TRUE(src != nullptr); - auto nnSrc = NN_NO_CHECK(src); - auto dst = - authFactory->createCoordinateReferenceSystem("6319"); // NAD83(2011) 3D - - auto listCompoundToGeog = - CoordinateOperationFactory::create()->createOperations(nnSrc, dst, - ctxt); - ASSERT_TRUE(!listCompoundToGeog.empty()); - - // NAD83(2011) + NAVD88 height - auto srcObjCompoundVMetre = createFromUserInput( - "EPSG:6318+5703", authFactory->databaseContext(), false); - auto srcCompoundVMetre = nn_dynamic_pointer_cast(srcObjCompoundVMetre); - ASSERT_TRUE(srcCompoundVMetre != nullptr); - auto listCompoundMetreToGeog = - CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcCompoundVMetre), dst, ctxt); - - // Check that we get the same and similar results whether we start from - // regular NAVD88 height or its depth variant - ASSERT_EQ(listCompoundToGeog.size(), listCompoundMetreToGeog.size()); - - EXPECT_EQ(listCompoundToGeog[0]->nameStr(), - "Inverse of NAVD88 height to NAVD88 depth + " + - listCompoundMetreToGeog[0]->nameStr()); - EXPECT_EQ( - listCompoundToGeog[0]->exportToPROJString( - PROJStringFormatter::create(PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - replaceAll(listCompoundMetreToGeog[0]->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - "+step +proj=unitconvert +xy_in=deg +xy_out=rad", - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=axisswap +order=1,2,-3")); - - // Check reverse path - auto listGeogToCompound = - CoordinateOperationFactory::create()->createOperations(dst, nnSrc, - ctxt); - EXPECT_EQ(listGeogToCompound.size(), listCompoundToGeog.size()); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - compoundCRS_to_geogCRS_with_vertical_unit_change_and_height_depth_reversal) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - // NAD83(2011) + NAVD88 depth (ftUS) - auto srcObj = createFromUserInput("EPSG:6318+6358", - authFactory->databaseContext(), false); - auto src = nn_dynamic_pointer_cast(srcObj); - ASSERT_TRUE(src != nullptr); - auto nnSrc = NN_NO_CHECK(src); - auto dst = - authFactory->createCoordinateReferenceSystem("6319"); // NAD83(2011) 3D - - auto listCompoundToGeog = - CoordinateOperationFactory::create()->createOperations(nnSrc, dst, - ctxt); - ASSERT_TRUE(!listCompoundToGeog.empty()); - - // NAD83(2011) + NAVD88 height - auto srcObjCompoundVMetre = createFromUserInput( - "EPSG:6318+5703", authFactory->databaseContext(), false); - auto srcCompoundVMetre = nn_dynamic_pointer_cast(srcObjCompoundVMetre); - ASSERT_TRUE(srcCompoundVMetre != nullptr); - auto listCompoundMetreToGeog = - CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(srcCompoundVMetre), dst, ctxt); - - // Check that we get the same and similar results whether we start from - // regular NAVD88 height or its depth (ftUS) variant - ASSERT_EQ(listCompoundToGeog.size(), listCompoundMetreToGeog.size()); - - EXPECT_EQ(listCompoundToGeog[0]->nameStr(), - "Inverse of NAVD88 height (ftUS) to NAVD88 depth (ftUS) + " - "Inverse of NAVD88 height to NAVD88 height (ftUS) + " + - listCompoundMetreToGeog[0]->nameStr()); - EXPECT_EQ( - listCompoundToGeog[0]->exportToPROJString( - PROJStringFormatter::create(PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - replaceAll(listCompoundMetreToGeog[0]->exportToPROJString( - PROJStringFormatter::create( - PROJStringFormatter::Convention::PROJ_5, - authFactory->databaseContext()) - .get()), - "+step +proj=unitconvert +xy_in=deg +xy_out=rad", - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=axisswap +order=1,2,-3 " - "+step +proj=unitconvert +z_in=us-ft +z_out=m")); - - // Check reverse path - auto listGeogToCompound = - CoordinateOperationFactory::create()->createOperations(dst, nnSrc, - ctxt); - EXPECT_EQ(listGeogToCompound.size(), listCompoundToGeog.size()); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_of_vertCRS_with_geoid_model_to_geogCRS) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - auto wkt = - "COMPOUNDCRS[\"NAD83 / Pennsylvania South + NAVD88 height\",\n" - " PROJCRS[\"NAD83 / Pennsylvania South\",\n" - " BASEGEOGCRS[\"NAD83\",\n" - " DATUM[\"North American Datum 1983\",\n" - " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433]]],\n" - " CONVERSION[\"SPCS83 Pennsylvania South zone (meters)\",\n" - " METHOD[\"Lambert Conic Conformal (2SP)\",\n" - " ID[\"EPSG\",9802]],\n" - " PARAMETER[\"Latitude of false origin\",39.3333333333333,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8821]],\n" - " PARAMETER[\"Longitude of false origin\",-77.75,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8822]],\n" - " PARAMETER[\"Latitude of 1st standard " - "parallel\",40.9666666666667,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8823]],\n" - " PARAMETER[\"Latitude of 2nd standard " - "parallel\",39.9333333333333,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8824]],\n" - " PARAMETER[\"Easting at false origin\",600000,\n" - " LENGTHUNIT[\"metre\",1],\n" - " ID[\"EPSG\",8826]],\n" - " PARAMETER[\"Northing at false origin\",0,\n" - " LENGTHUNIT[\"metre\",1],\n" - " ID[\"EPSG\",8827]]],\n" - " CS[Cartesian,2],\n" - " AXIS[\"easting (X)\",east,\n" - " ORDER[1],\n" - " LENGTHUNIT[\"metre\",1]],\n" - " AXIS[\"northing (Y)\",north,\n" - " ORDER[2],\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " VERTCRS[\"NAVD88 height\",\n" - " VDATUM[\"North American Vertical Datum 1988\"],\n" - " CS[vertical,1],\n" - " AXIS[\"gravity-related height (H)\",up,\n" - " LENGTHUNIT[\"metre\",1]],\n" - " GEOIDMODEL[\"GEOID12B\"]]]"; - auto srcObj = - createFromUserInput(wkt, authFactory->databaseContext(), false); - auto src = nn_dynamic_pointer_cast(srcObj); - ASSERT_TRUE(src != nullptr); - auto dst = authFactory->createCoordinateReferenceSystem("4269"); // NAD83 - - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(src), dst, ctxt); - ASSERT_TRUE(!list.empty()); - EXPECT_EQ(list[0]->nameStr(), - "Inverse of SPCS83 Pennsylvania South zone (meters) + " - "Ballpark geographic offset from NAD83 to NAD83(2011) + " - "Inverse of NAD83(2011) to NAVD88 height (1) + " - "Ballpark geographic offset from NAD83(2011) to NAD83"); - auto op_proj = - list[0]->exportToPROJString(PROJStringFormatter::create().get()); - EXPECT_EQ( - op_proj, - "+proj=pipeline " - "+step +inv +proj=lcc +lat_0=39.3333333333333 +lon_0=-77.75 " - "+lat_1=40.9666666666667 +lat_2=39.9333333333333 +x_0=600000 " - "+y_0=0 +ellps=GRS80 " - "+step +proj=vgridshift +grids=us_noaa_g2012bu0.tif +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_from_WKT2_to_geogCRS_3D_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto src = authFactory->createCoordinateReferenceSystem( - "7415"); // Amersfoort / RD New + NAP height - auto dst = - authFactory->createCoordinateReferenceSystem("4937"); // ETRS89 3D - auto list = - CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); - ASSERT_GE(list.size(), 1U); - auto wkt2 = src->exportToWKT( - WKTFormatter::create(WKTFormatter::Convention::WKT2_2019).get()); - auto obj = WKTParser().createFromWKT(wkt2); - auto src_from_wkt2 = nn_dynamic_pointer_cast(obj); - ASSERT_TRUE(src_from_wkt2 != nullptr); - auto list2 = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(src_from_wkt2), dst, ctxt); - ASSERT_GE(list.size(), list2.size()); - for (size_t i = 0; i < list.size(); i++) { - const auto &op = list[i]; - const auto &op2 = list2[i]; - EXPECT_TRUE( - op->isEquivalentTo(op2.get(), IComparable::Criterion::EQUIVALENT)); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_from_WKT2_no_id_to_geogCRS_3D_context) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto src = authFactory->createCoordinateReferenceSystem( - "7415"); // Amersfoort / RD New + NAP height - auto dst = - authFactory->createCoordinateReferenceSystem("4937"); // ETRS89 3D - auto list = - CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); - ASSERT_GE(list.size(), 1U); - - { - auto op_proj = - list[0]->exportToPROJString(PROJStringFormatter::create().get()); - EXPECT_EQ( - op_proj, - "+proj=pipeline +step +inv +proj=sterea +lat_0=52.1561605555556 " - "+lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 +y_0=463000 " - "+ellps=bessel " - "+step +proj=hgridshift +grids=nl_nsgi_rdtrans2018.tif " - "+step +proj=vgridshift +grids=nl_nsgi_nlgeo2018.tif +multiplier=1 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); - } - - auto wkt2 = - "COMPOUNDCRS[\"unknown\",\n" - " PROJCRS[\"unknown\",\n" - " BASEGEOGCRS[\"Amersfoort\",\n" - " DATUM[\"Amersfoort\",\n" - " ELLIPSOID[\"Bessel " - "1841\",6377397.155,299.1528128]]],\n" - " CONVERSION[\"unknown\",\n" - " METHOD[\"Oblique Stereographic\"],\n" - " PARAMETER[\"Latitude of natural origin\",52.1561605555556],\n" - " PARAMETER[\"Longitude of natural origin\",5.38763888888889],\n" - " PARAMETER[\"Scale factor at natural origin\",0.9999079],\n" - " PARAMETER[\"False easting\",155000],\n" - " PARAMETER[\"False northing\",463000]],\n" - " CS[Cartesian,2],\n" - " AXIS[\"(E)\",east],\n" - " AXIS[\"(N)\",north],\n" - " LENGTHUNIT[\"metre\",1]],\n" - " VERTCRS[\"NAP height\",\n" - " VDATUM[\"Normaal Amsterdams Peil\"],\n" - " CS[vertical,1],\n" - " AXIS[\"gravity-related height (H)\",up,\n" - " LENGTHUNIT[\"metre\",1]]],\n" - " USAGE[\n" - " SCOPE[\"unknown\"],\n" - " AREA[\"Netherlands - onshore\"],\n" - " BBOX[50.75,3.2,53.7,7.22]]]"; - - auto obj = WKTParser().createFromWKT(wkt2); - auto src_from_wkt2 = nn_dynamic_pointer_cast(obj); - ASSERT_TRUE(src_from_wkt2 != nullptr); - auto list2 = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(src_from_wkt2), dst, ctxt); - ASSERT_EQ(list.size(), list2.size()); - for (size_t i = 0; i < list.size(); i++) { - const auto &op = list[i]; - const auto &op2 = list2[i]; - auto op_proj = - op->exportToPROJString(PROJStringFormatter::create().get()); - auto op2_proj = - op2->exportToPROJString(PROJStringFormatter::create().get()); - EXPECT_EQ(op_proj, op2_proj) << "op=" << op->nameStr() - << " op2=" << op2->nameStr(); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, proj3DCRS_with_non_meter_horiz_and_vertical_to_geog) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=utm +zone=31 +datum=WGS84 +units=us-ft +vunits=us-ft +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(src), authFactory->createCoordinateReferenceSystem("4326"), - ctxt); - ASSERT_EQ(list.size(), 1U); - // Check that vertical unit conversion is done just once - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=unitconvert +xy_in=us-ft +z_in=us-ft " - "+xy_out=m +z_out=m " - "+step +inv +proj=utm +zone=31 +ellps=WGS84 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, compoundCRS_with_non_meter_horiz_and_vertical_to_geog) { - auto objSrc = WKTParser().createFromWKT( - "COMPOUNDCRS[\"unknown\",\n" - " PROJCRS[\"unknown\",\n" - " BASEGEOGCRS[\"unknown\",\n" - " DATUM[\"World Geodetic System 1984\",\n" - " ELLIPSOID[\"WGS 84\",6378137,298.257223563,\n" - " LENGTHUNIT[\"metre\",1]],\n" - " ID[\"EPSG\",6326]],\n" - " PRIMEM[\"Greenwich\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8901]]],\n" - " CONVERSION[\"UTM zone 31N\",\n" - " METHOD[\"Transverse Mercator\",\n" - " ID[\"EPSG\",9807]],\n" - " PARAMETER[\"Latitude of natural origin\",0,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8801]],\n" - " PARAMETER[\"Longitude of natural origin\",3,\n" - " ANGLEUNIT[\"degree\",0.0174532925199433],\n" - " ID[\"EPSG\",8802]],\n" - " PARAMETER[\"Scale factor at natural origin\",0.9996,\n" - " SCALEUNIT[\"unity\",1],\n" - " ID[\"EPSG\",8805]],\n" - " PARAMETER[\"False easting\",500000,\n" - " LENGTHUNIT[\"metre\",1],\n" - " ID[\"EPSG\",8806]],\n" - " PARAMETER[\"False northing\",0,\n" - " LENGTHUNIT[\"metre\",1],\n" - " ID[\"EPSG\",8807]],\n" - " ID[\"EPSG\",16031]],\n" - " CS[Cartesian,2],\n" - " AXIS[\"(E)\",east,\n" - " ORDER[1],\n" - " LENGTHUNIT[\"US survey foot\",0.304800609601219,\n" - " ID[\"EPSG\",9003]]],\n" - " AXIS[\"(N)\",north,\n" - " ORDER[2],\n" - " LENGTHUNIT[\"US survey foot\",0.304800609601219,\n" - " ID[\"EPSG\",9003]]]],\n" - " VERTCRS[\"unknown\",\n" - " VDATUM[\"unknown\"],\n" - " CS[vertical,1],\n" - " AXIS[\"gravity-related height (H)\",up,\n" - " LENGTHUNIT[\"US survey foot\",0.304800609601219,\n" - " ID[\"EPSG\",9003]]]]]" - - ); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - auto list = CoordinateOperationFactory::create()->createOperations( - NN_NO_CHECK(src), authFactory->createCoordinateReferenceSystem("4326"), - ctxt); - ASSERT_EQ(list.size(), 1U); - // Check that vertical unit conversion is done just once - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=unitconvert +xy_in=us-ft +xy_out=m " - "+step +inv +proj=utm +zone=31 +ellps=WGS84 " - "+step +proj=unitconvert +xy_in=rad +z_in=us-ft " - "+xy_out=deg +z_out=m " - "+step +proj=axisswap +order=2,1"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, boundCRS_to_compoundCRS) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +ellps=GRS80 +nadgrids=@bar.gsb +geoidgrids=@bar.gtx " - "+type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - 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=@foo.gsb " - "+step +inv +proj=vgridshift +grids=@bar.gtx +multiplier=1 " - "+step +inv +proj=hgridshift +grids=@bar.gsb " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); - - auto opInverse = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(dst), NN_CHECK_ASSERT(src)); - ASSERT_TRUE(opInverse != nullptr); - EXPECT_TRUE(opInverse->inverse()->_isEquivalentTo(op.get())); -} - -// --------------------------------------------------------------------------- - -TEST(operation, IGNF_LAMB1_TO_EPSG_4326) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), std::string()); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setGridAvailabilityUse( - CoordinateOperationContext::GridAvailabilityUse:: - IGNORE_GRID_AVAILABILITY); - ctxt->setAllowUseIntermediateCRS( - CoordinateOperationContext::IntermediateCRSUse::ALWAYS); - auto list = CoordinateOperationFactory::create()->createOperations( - AuthorityFactory::create(DatabaseContext::create(), "IGNF") - ->createCoordinateReferenceSystem("LAMB1"), - AuthorityFactory::create(DatabaseContext::create(), "EPSG") - ->createCoordinateReferenceSystem("4326"), - ctxt); - ASSERT_EQ(list.size(), 2U); - - EXPECT_FALSE(list[0]->hasBallparkTransformation()); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=lcc +lat_1=49.5 +lat_0=49.5 " - "+lon_0=0 +k_0=0.99987734 +x_0=600000 +y_0=200000 " - "+ellps=clrk80ign +pm=paris +step +proj=hgridshift " - "+grids=fr_ign_ntf_r93.tif +step +proj=unitconvert +xy_in=rad " - "+xy_out=deg +step +proj=axisswap +order=2,1"); - - EXPECT_FALSE(list[1]->hasBallparkTransformation()); - EXPECT_EQ(list[1]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=lcc +lat_1=49.5 +lat_0=49.5 " - "+lon_0=0 +k_0=0.99987734 +x_0=600000 +y_0=200000 " - "+ellps=clrk80ign +pm=paris +step +proj=push +v_3 +step " - "+proj=cart +ellps=clrk80ign +step +proj=helmert +x=-168 +y=-60 " - "+z=320 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg +step " - "+proj=axisswap +order=2,1"); - - auto list2 = CoordinateOperationFactory::create()->createOperations( - AuthorityFactory::create(DatabaseContext::create(), "EPSG") - // NTF (Paris) / Lambert Nord France equivalent to IGNF:LAMB1 - ->createCoordinateReferenceSystem("27561"), - AuthorityFactory::create(DatabaseContext::create(), "EPSG") - ->createCoordinateReferenceSystem("4326"), - ctxt); - ASSERT_GE(list2.size(), 3U); - - EXPECT_EQ(replaceAll(list2[0]->exportToPROJString( - PROJStringFormatter::create().get()), - "0.999877341", "0.99987734"), - list[0]->exportToPROJString(PROJStringFormatter::create().get())); - - // The second entry in list2 (list2[1]) uses the - // weird +pm=2.33720833333333 from "NTF (Paris) to NTF (2)" - // so skip to the 3th method - EXPECT_EQ(replaceAll(list2[2]->exportToPROJString( - PROJStringFormatter::create().get()), - "0.999877341", "0.99987734"), - list[1]->exportToPROJString(PROJStringFormatter::create().get())); -} - -// --------------------------------------------------------------------------- - -TEST(operation, NAD83_to_projeted_CRS_based_on_NAD83_2011) { - auto authFactory = - AuthorityFactory::create(DatabaseContext::create(), "EPSG"); - auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); - ctxt->setSpatialCriterion( - CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); - auto list = CoordinateOperationFactory::create()->createOperations( - // NAD83 - authFactory->createCoordinateReferenceSystem("4269"), - // NAD83(2011) / California Albers - authFactory->createCoordinateReferenceSystem("6414"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->nameStr(), "Ballpark geographic offset from NAD83 to " - "NAD83(2011) + California Albers"); - EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=aea +lat_0=0 +lon_0=-120 +lat_1=34 " - "+lat_2=40.5 +x_0=0 +y_0=-4000000 +ellps=GRS80"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, isPROJInstantiable) { - - { - auto transformation = Transformation::createGeocentricTranslations( - PropertyMap(), GeographicCRS::EPSG_4269, GeographicCRS::EPSG_4326, - 1.0, 2.0, 3.0, {}); - EXPECT_TRUE(transformation->isPROJInstantiable( - DatabaseContext::create(), false)); - } - - // Missing grid - { - auto transformation = Transformation::createNTv2( - PropertyMap(), GeographicCRS::EPSG_4807, GeographicCRS::EPSG_4326, - "foo.gsb", std::vector()); - EXPECT_FALSE(transformation->isPROJInstantiable( - DatabaseContext::create(), false)); - } - - // Unsupported method - { - auto transformation = Transformation::create( - PropertyMap(), GeographicCRS::EPSG_4269, GeographicCRS::EPSG_4326, - nullptr, OperationMethod::create( - PropertyMap(), std::vector{}), - std::vector{}, - std::vector{}); - EXPECT_FALSE(transformation->isPROJInstantiable( - DatabaseContext::create(), false)); - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, createOperation_on_crs_with_canonical_bound_crs) { - auto boundCRS = BoundCRS::createFromTOWGS84( - GeographicCRS::EPSG_4267, std::vector{1, 2, 3, 4, 5, 6, 7}); - auto crs = boundCRS->baseCRSWithCanonicalBoundCRS(); - { - auto op = CoordinateOperationFactory::create()->createOperation( - crs, GeographicCRS::EPSG_4326); - ASSERT_TRUE(op != nullptr); - EXPECT_TRUE(op->isEquivalentTo(boundCRS->transformation().get())); - { - auto wkt1 = op->exportToWKT( - WKTFormatter::create(WKTFormatter::Convention::WKT2_2019) - .get()); - auto wkt2 = boundCRS->transformation()->exportToWKT( - WKTFormatter::create(WKTFormatter::Convention::WKT2_2019) - .get()); - EXPECT_EQ(wkt1, wkt2); - } - } - { - auto op = CoordinateOperationFactory::create()->createOperation( - GeographicCRS::EPSG_4326, crs); - ASSERT_TRUE(op != nullptr); - EXPECT_TRUE( - op->isEquivalentTo(boundCRS->transformation()->inverse().get())); - { - auto wkt1 = op->exportToWKT( - WKTFormatter::create(WKTFormatter::Convention::WKT2_2019) - .get()); - auto wkt2 = boundCRS->transformation()->inverse()->exportToWKT( - WKTFormatter::create(WKTFormatter::Convention::WKT2_2019) - .get()); - EXPECT_EQ(wkt1, wkt2); - } - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, createOperation_fallback_to_proj4_strings) { - auto objDest = PROJStringParser().createFromPROJString( - "+proj=longlat +geoc +datum=WGS84 +type=crs"); - auto dest = nn_dynamic_pointer_cast(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 +proj=longlat +geoc +datum=WGS84 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - createOperation_fallback_to_proj4_strings_regular_with_datum_to_projliteral) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=utm +zone=11 +datum=NAD27 +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +datum=WGS84 +over +type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +inv +proj=utm +zone=11 +datum=NAD27 " - "+step +proj=longlat +datum=WGS84 +over " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - createOperation_fallback_to_proj4_strings_proj_NAD83_to_projliteral) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=utm +zone=11 +datum=NAD83 +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +datum=WGS84 +over +type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +inv +proj=utm +zone=11 +ellps=GRS80 " - "+step +proj=longlat +datum=WGS84 +over " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - createOperation_fallback_to_proj4_strings_geog_NAD83_to_projliteral) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=longlat +datum=NAD83 +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +datum=WGS84 +over +type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=longlat +datum=WGS84 +over " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - createOperation_fallback_to_proj4_strings_regular_with_nadgrids_to_projliteral) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=utm +zone=11 +ellps=clrk66 +nadgrids=@conus +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +datum=WGS84 +over +type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +inv +proj=utm +zone=11 +ellps=clrk66 +nadgrids=@conus " - "+step +proj=longlat +datum=WGS84 +over " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, - createOperation_fallback_to_proj4_strings_projliteral_to_projliteral) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=utm +zone=11 +datum=NAD27 +over +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +datum=WGS84 +over +type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " - "+step +inv +proj=utm +zone=11 +datum=NAD27 +over " - "+step +proj=longlat +datum=WGS84 +over " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); -} - -// --------------------------------------------------------------------------- - -TEST( - operation, - createOperation_fallback_to_proj4_strings_regular_to_projliteral_with_towgs84) { - auto objSrc = - createFromUserInput("EPSG:4326", DatabaseContext::create(), false); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - - auto objDst = PROJStringParser().createFromPROJString( - "+proj=utm +zone=31 +ellps=GRS80 +towgs84=1,2,3 +over +type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - 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 +proj=utm +zone=31 +ellps=GRS80 +towgs84=1,2,3 +over"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, createOperation_on_crs_with_bound_crs_and_wktext) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=utm +zone=55 +south +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 " - "+units=m +no_defs +nadgrids=@GDA94_GDA2020_conformal.gsb +ignored1 " - "+ignored2=val +wktext +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - - auto objDst = PROJStringParser().createFromPROJString( - "+proj=utm +zone=55 +south +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 " - "+units=m +no_defs +type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - ASSERT_TRUE(op != nullptr); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +inv +proj=utm +zone=55 +south " - "+ellps=GRS80 +step +proj=hgridshift " - "+grids=@GDA94_GDA2020_conformal.gsb +step +proj=utm +zone=55 " - "+south +ellps=GRS80"); -} - -// --------------------------------------------------------------------------- - -TEST(operation, createOperation_ossfuzz_18587) { - auto objSrc = - createFromUserInput("EPSG:4326", DatabaseContext::create(), false); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - - // Extremely weird string ! We should likely reject it - auto objDst = PROJStringParser().createFromPROJString( - "type=crs proj=pipeline step proj=merc vunits=m nadgrids=@x " - "proj=\"\nproj=pipeline step\n\""); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - - // Just check that we don't go into an infinite recursion - try { - CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - } catch (const std::exception &) { - } -} - -// --------------------------------------------------------------------------- - -TEST(operation, derivedGeographicCRS_with_to_wgs84_to_geographicCRS) { - auto objSrc = PROJStringParser().createFromPROJString( - "+proj=ob_tran +o_proj=latlon +lat_0=0 +lon_0=180 +o_lat_p=18.0 " - "+o_lon_p=-200.0 +ellps=WGS84 +towgs84=1,2,3 +type=crs"); - auto src = nn_dynamic_pointer_cast(objSrc); - ASSERT_TRUE(src != nullptr); - auto objDst = PROJStringParser().createFromPROJString( - "+proj=longlat +datum=WGS84 +type=crs"); - auto dst = nn_dynamic_pointer_cast(objDst); - ASSERT_TRUE(dst != nullptr); - - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); - ASSERT_TRUE(op != nullptr); - std::string pipeline( - "+proj=pipeline " - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +inv +proj=ob_tran +o_proj=latlon +lat_0=0 +lon_0=180 " - "+o_lat_p=18 +o_lon_p=-200 +ellps=WGS84 " - "+step +proj=push +v_3 " - "+step +proj=cart +ellps=WGS84 " - "+step +proj=helmert +x=1 +y=2 +z=3 " - "+step +inv +proj=cart +ellps=WGS84 " - "+step +proj=pop +v_3 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - pipeline); - - auto op2 = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(src), - nn_static_pointer_cast(GeographicCRS::EPSG_4326)); - ASSERT_TRUE(op2 != nullptr); - EXPECT_EQ(op2->exportToPROJString(PROJStringFormatter::create().get()), - pipeline + " +step +proj=axisswap +order=2,1"); - } - - { - auto op = CoordinateOperationFactory::create()->createOperation( - NN_CHECK_ASSERT(dst), NN_CHECK_ASSERT(src)); - ASSERT_TRUE(op != nullptr); - std::string pipeline( - "+step +proj=unitconvert +xy_in=deg +xy_out=rad " - "+step +proj=push +v_3 " - "+step +proj=cart +ellps=WGS84 " - "+step +proj=helmert +x=-1 +y=-2 +z=-3 " - "+step +inv +proj=cart +ellps=WGS84 " - "+step +proj=pop +v_3 " - "+step +proj=ob_tran +o_proj=latlon +lat_0=0 +lon_0=180 " - "+o_lat_p=18 +o_lon_p=-200 +ellps=WGS84 " - "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); - EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline " + pipeline); - - auto op2 = CoordinateOperationFactory::create()->createOperation( - nn_static_pointer_cast(GeographicCRS::EPSG_4326), - NN_CHECK_ASSERT(src)); - ASSERT_TRUE(op2 != nullptr); - EXPECT_EQ(op2->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 " + pipeline); - } -} - -// --------------------------------------------------------------------------- - TEST(operation, mercator_variant_A_to_variant_B) { auto projCRS = ProjectedCRS::create( PropertyMap(), GeographicCRS::EPSG_4326, diff --git a/test/unit/test_operationfactory.cpp b/test/unit/test_operationfactory.cpp new file mode 100644 index 0000000000..b3f6f5cbab --- /dev/null +++ b/test/unit/test_operationfactory.cpp @@ -0,0 +1,5915 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: Test ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2018, Even Rouault + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#include "gtest_include.h" + +#include "test_primitives.hpp" + +// to be able to use internal::replaceAll +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP +#endif + +#include "proj/common.hpp" +#include "proj/coordinateoperation.hpp" +#include "proj/coordinatesystem.hpp" +#include "proj/crs.hpp" +#include "proj/datum.hpp" +#include "proj/io.hpp" +#include "proj/metadata.hpp" +#include "proj/util.hpp" + +#include "proj/internal/internal.hpp" + +#include "proj_constants.h" + +#include +#include + +using namespace osgeo::proj::common; +using namespace osgeo::proj::crs; +using namespace osgeo::proj::cs; +using namespace osgeo::proj::datum; +using namespace osgeo::proj::io; +using namespace osgeo::proj::internal; +using namespace osgeo::proj::metadata; +using namespace osgeo::proj::operation; +using namespace osgeo::proj::util; + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS) { + + auto op = CoordinateOperationFactory::create()->createOperation( + GeographicCRS::EPSG_4807, GeographicCRS::EPSG_4326); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ( + op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv +proj=longlat " + "+ellps=clrk80ign +pm=paris +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_default) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setAllowUseIntermediateCRS( + CoordinateOperationContext::IntermediateCRSUse::NEVER); + + // Directly found in database + { + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4179"), // Pulkovo 42 + authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 + ctxt); + ASSERT_EQ(list.size(), 3U); + // Romania has a larger area than Poland (given our approx formula) + EXPECT_EQ(list[0]->getEPSGCode(), 15994); // Romania - 3m + EXPECT_EQ(list[1]->getEPSGCode(), 1644); // Poland - 1m + EXPECT_EQ(list[2]->nameStr(), + "Ballpark geographic offset from Pulkovo 1942(58) to ETRS89"); + + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 " + "+step +proj=cart +ellps=krass +step +proj=helmert +x=2.3287 " + "+y=-147.0425 +z=-92.0802 +rx=0.3092483 +ry=-0.32482185 " + "+rz=-0.49729934 +s=5.68906266 +convention=coordinate_frame +step " + "+inv +proj=cart +ellps=GRS80 +step +proj=pop +v_3 +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " + "+order=2,1"); + } + + // Reverse case + { + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4258"), + authFactory->createCoordinateReferenceSystem("4179"), ctxt); + ASSERT_EQ(list.size(), 3U); + // Romania has a larger area than Poland (given our approx formula) + EXPECT_EQ(list[0]->nameStr(), + "Inverse of Pulkovo 1942(58) to ETRS89 (4)"); // Romania - 3m + + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 " + "+step +proj=cart +ellps=GRS80 +step +inv +proj=helmert +x=2.3287 " + "+y=-147.0425 +z=-92.0802 +rx=0.3092483 +ry=-0.32482185 " + "+rz=-0.49729934 +s=5.68906266 +convention=coordinate_frame +step " + "+inv +proj=cart +ellps=krass +step +proj=pop +v_3 +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " + "+order=2,1"); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_match_by_name) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setAllowUseIntermediateCRS( + CoordinateOperationContext::IntermediateCRSUse::NEVER); + auto NAD27 = GeographicCRS::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, + GeographicCRS::EPSG_4267->nameStr()), + GeographicCRS::EPSG_4267->datum(), + GeographicCRS::EPSG_4267->datumEnsemble(), + GeographicCRS::EPSG_4267->coordinateSystem()); + auto list = CoordinateOperationFactory::create()->createOperations( + NAD27, GeographicCRS::EPSG_4326, ctxt); + auto listInv = CoordinateOperationFactory::create()->createOperations( + GeographicCRS::EPSG_4326, NAD27, ctxt); + auto listRef = CoordinateOperationFactory::create()->createOperations( + GeographicCRS::EPSG_4267, GeographicCRS::EPSG_4326, ctxt); + EXPECT_EQ(list.size(), listRef.size()); + EXPECT_EQ(listInv.size(), listRef.size()); + EXPECT_GE(listRef.size(), 2U); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_filter_accuracy) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 1.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4179"), + authFactory->createCoordinateReferenceSystem("4258"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->getEPSGCode(), 1644); // Poland - 1m + } + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.9); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4179"), + authFactory->createCoordinateReferenceSystem("4258"), ctxt); + ASSERT_EQ(list.size(), 0U); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_filter_bbox) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + // INSERT INTO "area" VALUES('EPSG','1197','Romania','Romania - onshore and + // offshore.',43.44,48.27,20.26,31.41,0); + { + auto ctxt = CoordinateOperationContext::create( + authFactory, Extent::createFromBBOX(20.26, 43.44, 31.41, 48.27), + 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4179"), + authFactory->createCoordinateReferenceSystem("4258"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->getEPSGCode(), 15994); // Romania - 3m + } + { + auto ctxt = CoordinateOperationContext::create( + authFactory, Extent::createFromBBOX(20.26 + .1, 43.44 + .1, + 31.41 - .1, 48.27 - .1), + 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4179"), + authFactory->createCoordinateReferenceSystem("4258"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->getEPSGCode(), 15994); // Romania - 3m + } + { + auto ctxt = CoordinateOperationContext::create( + authFactory, Extent::createFromBBOX(20.26 - .1, 43.44 - .1, + 31.41 + .1, 48.27 + .1), + 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4179"), + authFactory->createCoordinateReferenceSystem("4258"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=noop"); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_incompatible_area) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4267"), // NAD27 + authFactory->createCoordinateReferenceSystem("4258"), // ETRS 89 + ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=noop"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_inverse_needed) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setUsePROJAlternativeGridNames(false); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4275"), // NTF + authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 + ctxt); + ASSERT_EQ(list.size(), 2U); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 " + "+step +proj=cart +ellps=clrk80ign +step +proj=helmert +x=-168 " + "+y=-60 +z=320 +step +inv +proj=cart +ellps=GRS80 +step +proj=pop " + "+v_3 +step +proj=unitconvert +xy_in=rad +xy_out=deg +step " + "+proj=axisswap +order=2,1"); + EXPECT_EQ(list[1]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step " + "+proj=hgridshift +grids=fr_ign_ntf_r93.tif +step " + "+proj=unitconvert " + "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); + } + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4275"), // NTF + authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 + ctxt); + ASSERT_EQ(list.size(), 2U); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step " + "+proj=hgridshift +grids=fr_ign_ntf_r93.tif +step " + "+proj=unitconvert " + "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); + } + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 + authFactory->createCoordinateReferenceSystem("4275"), // NTF + ctxt); + ASSERT_EQ(list.size(), 2U); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +inv " + "+proj=hgridshift +grids=fr_ign_ntf_r93.tif +step " + "+proj=unitconvert " + "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_ntv1_ntv2_ctable2) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4267"), // NAD27 + authFactory->createCoordinateReferenceSystem("4269"), // NAD83 + ctxt); + ASSERT_EQ(list.size(), 10U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=hgridshift " + "+grids=ca_nrc_ntv1_can.tif +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); + EXPECT_EQ(list[1]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=hgridshift " + "+grids=ca_nrc_ntv2_0.tif +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); + EXPECT_EQ(list[2]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=hgridshift " + "+grids=us_noaa_conus.tif +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_NAD27_to_WGS84) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4267"), // NAD27 + authFactory->createCoordinateReferenceSystem("4326"), // WGS84 + ctxt); + ASSERT_EQ(list.size(), 79U); + EXPECT_EQ(list[0]->nameStr(), + "NAD27 to WGS 84 (33)"); // 1.0 m, Canada - NAD27 + EXPECT_EQ(list[1]->nameStr(), + "NAD27 to WGS 84 (3)"); // 20.0 m, Canada - NAD27 + EXPECT_EQ(list[2]->nameStr(), + "NAD27 to WGS 84 (79)"); // 5.0 m, USA - CONUS including EEZ + EXPECT_EQ(list[3]->nameStr(), + "NAD27 to WGS 84 (4)"); // 10.0 m, USA - CONUS - onshore +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_NAD27_to_WGS84_G1762) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), std::string()); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + + auto authFactoryEPSG = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto list = CoordinateOperationFactory::create()->createOperations( + // NAD27 + authFactoryEPSG->createCoordinateReferenceSystem("4267"), + // WGS84 (G1762) + authFactoryEPSG->createCoordinateReferenceSystem("9057"), ctxt); + ASSERT_GE(list.size(), 78U); + EXPECT_EQ(list[0]->nameStr(), + "NAD27 to WGS 84 (33) + WGS 84 to WGS 84 (G1762)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=hgridshift +grids=ca_nrc_ntv2_0.tif " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + EXPECT_EQ(list[1]->nameStr(), + "NAD27 to WGS 84 (3) + WGS 84 to WGS 84 (G1762)"); + EXPECT_EQ(list[2]->nameStr(), + "NAD27 to WGS 84 (79) + WGS 84 to WGS 84 (G1762)"); + EXPECT_EQ(list[3]->nameStr(), + "NAD27 to WGS 84 (4) + WGS 84 to WGS 84 (G1762)"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_WGS84_G1674_to_WGS84_G1762) { + // Check that particular behavior with WGS 84 (Gxxx) related to + // 'geodetic_datum_preferred_hub' table and custom no-op transformations + // between WGS 84 and WGS 84 (Gxxx) doesn't affect direct transformations + // to those realizations. + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), std::string()); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + + auto authFactoryEPSG = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto list = CoordinateOperationFactory::create()->createOperations( + // WGS84 (G1674) + authFactoryEPSG->createCoordinateReferenceSystem("9056"), + // WGS84 (G1762) + authFactoryEPSG->createCoordinateReferenceSystem("9057"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=cart +ellps=WGS84 " + "+step +proj=helmert +x=-0.004 +y=0.003 +z=0.004 +rx=0.00027 " + "+ry=-0.00027 +rz=0.00038 +s=-0.0069 " + "+convention=coordinate_frame " + "+step +inv +proj=cart +ellps=WGS84 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_EPSG_4240_Indian1975_to_EPSG_4326) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4240"), // Indian 1975 + authFactory->createCoordinateReferenceSystem("4326"), ctxt); + ASSERT_EQ(list.size(), 3U); + + // Indian 1975 to WGS 84 (4), 3.0 m, Thailand - onshore + EXPECT_EQ(list[0]->getEPSGCode(), 1812); + + // The following is the one we want to see. It has a lesser accuracy than + // the above one and the same bbox, but the name of its area of use is + // slightly different + // Indian 1975 to WGS 84 (2), 5.0 m, Thailand - onshore and Gulf of Thailand + EXPECT_EQ(list[1]->getEPSGCode(), 1304); + + // Indian 1975 to WGS 84 (3), 1.0 m, Thailand - Bongkot field + EXPECT_EQ(list[2]->getEPSGCode(), 1537); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_helmert_geog3D_crs) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); + + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4939"), // GDA94 3D + authFactory->createCoordinateReferenceSystem("7843"), // GDA2020 3D + ctxt); + ASSERT_EQ(list.size(), 1U); + + // Check there is no push / pop of v_3 + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +z_in=m +xy_out=rad +z_out=m " + "+step +proj=cart +ellps=GRS80 " + "+step +proj=helmert +x=0.06155 +y=-0.01087 +z=-0.04019 " + "+rx=-0.0394924 +ry=-0.0327221 +rz=-0.0328979 +s=-0.009994 " + "+convention=coordinate_frame " + "+step +inv +proj=cart +ellps=GRS80 " + "+step +proj=unitconvert +xy_in=rad +z_in=m +xy_out=deg +z_out=m " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_helmert_geocentric_3D) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); + + auto list = CoordinateOperationFactory::create()->createOperations( + // GDA94 geocentric + authFactory->createCoordinateReferenceSystem("4348"), + // GDA2020 geocentric + authFactory->createCoordinateReferenceSystem("7842"), ctxt); + ASSERT_EQ(list.size(), 1U); + + // Check there is no push / pop of v_3 + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=helmert +x=0.06155 +y=-0.01087 +z=-0.04019 " + "+rx=-0.0394924 +ry=-0.0327221 +rz=-0.0328979 +s=-0.009994 " + "+convention=coordinate_frame"); + EXPECT_EQ(list[0]->inverse()->exportToPROJString( + PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +inv +proj=helmert +x=0.06155 +y=-0.01087 +z=-0.04019 " + "+rx=-0.0394924 +ry=-0.0327221 +rz=-0.0328979 +s=-0.009994 " + "+convention=coordinate_frame"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_helmert_geog3D_to_geocentirc) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); + + auto list = CoordinateOperationFactory::create()->createOperations( + // GDA94 3D + authFactory->createCoordinateReferenceSystem("4939"), + // GDA2020 geocentric + authFactory->createCoordinateReferenceSystem("7842"), ctxt); + ASSERT_EQ(list.size(), 1U); + + // Check there is no push / pop of v_3 + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +z_in=m +xy_out=rad +z_out=m " + "+step +proj=cart +ellps=GRS80 " + "+step +proj=helmert +x=0.06155 +y=-0.01087 +z=-0.04019 " + "+rx=-0.0394924 +ry=-0.0327221 +rz=-0.0328979 +s=-0.009994 " + "+convention=coordinate_frame"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_invalid_EPSG_ID) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); + // EPSG:4656 is incorrect. Should be EPSG:8997 + auto obj = WKTParser().createFromWKT( + "GEOGCS[\"ITRF2000\"," + "DATUM[\"International_Terrestrial_Reference_Frame_2000\"," + "SPHEROID[\"GRS 1980\",6378137,298.257222101," + "AUTHORITY[\"EPSG\",\"7019\"]],AUTHORITY[\"EPSG\",\"6656\"]]," + "PRIMEM[\"Greenwich\",0],UNIT[\"Degree\",0.0174532925199433]," + "AUTHORITY[\"EPSG\",\"4656\"]]"); + auto crs = nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(crs != nullptr); + + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(crs), GeographicCRS::EPSG_4326, ctxt); + ASSERT_EQ(list.size(), 1U); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_datum_ensemble) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); + + auto dst_wkt = + "GEOGCRS[\"unknown\"," + " ENSEMBLE[\"World Geodetic System 1984 ensemble\"," + " MEMBER[\"World Geodetic System 1984 (Transit)\"," + " ID[\"EPSG\",1166]]," + " MEMBER[\"World Geodetic System 1984 (G730)\"," + " ID[\"EPSG\",1152]]," + " MEMBER[\"World Geodetic System 1984 (G873)\"," + " ID[\"EPSG\",1153]]," + " MEMBER[\"World Geodetic System 1984 (G1150)\"," + " ID[\"EPSG\",1154]]," + " MEMBER[\"World Geodetic System 1984 (G1674)\"," + " ID[\"EPSG\",1155]]," + " MEMBER[\"World Geodetic System 1984 (G1762)\"," + " ID[\"EPSG\",1156]]," + " ELLIPSOID[\"WGS 84\",6378137,298.257223563," + " LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]," + " ID[\"EPSG\",7030]]," + " ENSEMBLEACCURACY[2]]," + " PRIMEM[\"Greenwich\",0," + " ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9102]]," + " ID[\"EPSG\",8901]]," + " CS[ellipsoidal,2," + " ID[\"EPSG\",6422]]," + " AXIS[\"Geodetic latitude (Lat)\",north," + " ORDER[1]]," + " AXIS[\"Geodetic longitude (Lon)\",east," + " ORDER[2]]," + " ANGLEUNIT[\"degree (supplier to define representation)\"," + "0.0174532925199433,ID[\"EPSG\",9122]]]"; + auto dstObj = WKTParser().createFromWKT(dst_wkt); + auto dstCRS = nn_dynamic_pointer_cast(dstObj); + ASSERT_TRUE(dstCRS != nullptr); + + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 + NN_NO_CHECK(dstCRS), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), "ETRS89 to WGS 84 (1)"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, vertCRS_to_geogCRS_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setUsePROJAlternativeGridNames(false); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem( + "3855"), // EGM2008 height + authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 + ctxt); + ASSERT_EQ(list.size(), 3U); + EXPECT_EQ( + list[1]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=us_nga_egm08_25.tif +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + } + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem( + "3855"), // EGM2008 height + authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 + ctxt); + ASSERT_EQ(list.size(), 3U); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=us_nga_egm08_25.tif +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + } + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 + authFactory->createCoordinateReferenceSystem( + "3855"), // EGM2008 height + ctxt); + ASSERT_EQ(list.size(), 2U); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +inv +proj=vgridshift +grids=us_nga_egm08_25.tif " + "+multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + } + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + // NGVD29 depth (ftUS) + authFactory->createCoordinateReferenceSystem("6359"), + authFactory->createCoordinateReferenceSystem("4326"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=affine +s33=-0.304800609601219"); + } + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + // NZVD2016 height + authFactory->createCoordinateReferenceSystem("7839"), + // NZGD2000 + authFactory->createCoordinateReferenceSystem("4959"), ctxt); + ASSERT_EQ(list.size(), 2U); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=nz_linz_nzgeoid2016.tif " + "+multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + } + { + // Test actually the database where we derive records using the more + // classic 'Geographic3D to GravityRelatedHeight' method from + // records using EPSG:9635 + //'Geog3D to Geog2D+GravityRelatedHeight (US .gtx)' method + auto ctxt = CoordinateOperationContext::create( + AuthorityFactory::create(DatabaseContext::create(), std::string()), + nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + // Baltic 1957 height + authFactory->createCoordinateReferenceSystem("8357"), + // ETRS89 + authFactory->createCoordinateReferenceSystem("4937"), ctxt); + ASSERT_EQ(list.size(), 2U); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift " + "+grids=sk_gku_Slovakia_ETRS89h_to_Baltic1957.tif " + "+multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, geog3DCRS_to_geog2DCRS_plus_vertCRS_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + // ETRS89 (3D) + authFactory->createCoordinateReferenceSystem("4937"), + // ETRS89 + Baltic 1957 height + authFactory->createCoordinateReferenceSystem("8360"), ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +inv +proj=vgridshift " + "+grids=sk_gku_Slovakia_ETRS89h_to_Baltic1957.tif +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + EXPECT_EQ(list[0]->inverse()->nameStr(), + "Inverse of 'ETRS89 to ETRS89 + Baltic 1957 height (1)'"); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_noop) { + + auto op = CoordinateOperationFactory::create()->createOperation( + GeographicCRS::EPSG_4326, GeographicCRS::EPSG_4326); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->nameStr(), "Null geographic offset from WGS 84 to WGS 84"); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=noop"); + EXPECT_EQ(op->inverse()->nameStr(), op->nameStr()); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_longitude_rotation) { + + auto src = GeographicCRS::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "A"), + GeodeticReferenceFrame::create(PropertyMap(), Ellipsoid::WGS84, + optional(), + PrimeMeridian::GREENWICH), + EllipsoidalCS::createLatitudeLongitude(UnitOfMeasure::DEGREE)); + auto dest = GeographicCRS::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "B"), + GeodeticReferenceFrame::create(PropertyMap(), Ellipsoid::WGS84, + optional(), + PrimeMeridian::PARIS), + EllipsoidalCS::createLatitudeLongitude(UnitOfMeasure::DEGREE)); + + auto op = CoordinateOperationFactory::create()->createOperation(src, 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 +proj=longlat " + "+ellps=WGS84 +pm=paris +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); + EXPECT_EQ(op->inverse()->exportToWKT(WKTFormatter::create().get()), + CoordinateOperationFactory::create() + ->createOperation(dest, src) + ->exportToWKT(WKTFormatter::create().get())); + EXPECT_TRUE( + op->inverse()->isEquivalentTo(CoordinateOperationFactory::create() + ->createOperation(dest, src) + .get())); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_longitude_rotation_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4807"), // NTF(Paris) + authFactory->createCoordinateReferenceSystem("4275"), // NTF + ctxt); + ASSERT_EQ(list.size(), 2U); + EXPECT_EQ(list[0]->nameStr(), "NTF (Paris) to NTF (1)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " + "+proj=longlat +ellps=clrk80ign +pm=paris +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " + "+order=2,1"); + EXPECT_EQ(list[1]->nameStr(), "NTF (Paris) to NTF (2)"); + EXPECT_EQ(list[1]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " + "+proj=longlat +ellps=clrk80ign +pm=2.33720833333333 +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " + "+order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_concatenated_operation) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setAllowUseIntermediateCRS( + CoordinateOperationContext::IntermediateCRSUse::ALWAYS); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4807"), // NTF(Paris) + authFactory->createCoordinateReferenceSystem("4171"), // RGF93 + ctxt); + ASSERT_EQ(list.size(), 4U); + + EXPECT_EQ(list[0]->nameStr(), "NTF (Paris) to RGF93 (1)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=grad +xy_out=rad " + "+step +inv +proj=longlat +ellps=clrk80ign +pm=paris " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=clrk80ign " + "+step +proj=xyzgridshift +grids=fr_ign_gr3df97a.tif " + "+grid_ref=output_crs +ellps=GRS80 " + "+step +inv +proj=cart +ellps=GRS80 " + "+step +proj=pop +v_3 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + EXPECT_EQ(list[1]->nameStr(), "NTF (Paris) to RGF93 (2)"); + EXPECT_EQ(list[1]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " + "+proj=longlat +ellps=clrk80ign +pm=paris +step +proj=hgridshift " + "+grids=fr_ign_ntf_r93.tif +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); + + EXPECT_TRUE(nn_dynamic_pointer_cast(list[0]) != + nullptr); + auto grids = list[0]->gridsNeeded(DatabaseContext::create(), false); + EXPECT_EQ(grids.size(), 1U); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_ED50_to_WGS72_no_NTF_intermediate) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4230"), // ED50 + authFactory->createCoordinateReferenceSystem("4322"), // WGS 72 + ctxt); + ASSERT_GE(list.size(), 2U); + // We should not use the ancient NTF as an intermediate when looking for + // ED50 -> WGS 72 operations. + for (const auto &op : list) { + EXPECT_TRUE(op->nameStr().find("NTF") == std::string::npos) + << op->nameStr(); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_context_same_grid_name) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4314"), // DHDN + authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 + ctxt); + ASSERT_TRUE(!list.empty()); + EXPECT_EQ(list[0]->nameStr(), "DHDN to ETRS89 (8)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=hgridshift " + "+grids=de_adv_BETA2007.tif +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_geographic_offset_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4120"), // NTF(Paris) + authFactory->createCoordinateReferenceSystem("4121"), // NTF + ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), "Greek to GGRS87 (1)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " + "+dlat=-5.86 +dlon=0.28 +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_CH1903_to_CH1903plus_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setAllowUseIntermediateCRS( + CoordinateOperationContext::IntermediateCRSUse::ALWAYS); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4149"), // CH1903 + authFactory->createCoordinateReferenceSystem("4150"), // CH1903+ + ctxt); + ASSERT_TRUE(list.size() == 1U); + + EXPECT_EQ(list[0]->nameStr(), "CH1903 to CH1903+ (1)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=hgridshift +grids=ch_swisstopo_CHENyx06a.tif " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +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(sourceCRS_obj); + ASSERT_TRUE(sourceCRS != nullptr); + + auto targetCRS_obj = PROJStringParser() + .attachDatabaseContext(dbContext) + .setUsePROJ4InitRules(true) + .createFromPROJString("+init=IGNF:RGF93G"); + auto targetCRS = nn_dynamic_pointer_cast(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(), 2U); + + EXPECT_EQ(list[0]->nameStr(), + "NOUVELLE TRIANGULATION DE LA FRANCE (NTF) vers RGF93 (ETRS89)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=hgridshift +grids=fr_ign_ntf_r93.tif +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geogCRS_3D) { + + auto geogcrs_m_obj = PROJStringParser().createFromPROJString( + "+proj=longlat +vunits=m +type=crs"); + auto geogcrs_m = nn_dynamic_pointer_cast(geogcrs_m_obj); + ASSERT_TRUE(geogcrs_m != nullptr); + + auto geogcrs_ft_obj = PROJStringParser().createFromPROJString( + "+proj=longlat +vunits=ft +type=crs"); + auto geogcrs_ft = nn_dynamic_pointer_cast(geogcrs_ft_obj); + ASSERT_TRUE(geogcrs_ft != nullptr); + + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(geogcrs_m), NN_CHECK_ASSERT(geogcrs_ft)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=unitconvert +z_in=m +z_out=ft"); + } + + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(geogcrs_ft), NN_CHECK_ASSERT(geogcrs_m)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=unitconvert +z_in=ft +z_out=m"); + } + + auto geogcrs_m_with_pm_obj = PROJStringParser().createFromPROJString( + "+proj=longlat +pm=paris +vunits=m +type=crs"); + auto geogcrs_m_with_pm = + nn_dynamic_pointer_cast(geogcrs_m_with_pm_obj); + ASSERT_TRUE(geogcrs_m_with_pm != nullptr); + + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(geogcrs_m_with_pm), NN_CHECK_ASSERT(geogcrs_ft)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=unitconvert +xy_in=deg +z_in=m " + "+xy_out=rad +z_out=m +step +inv +proj=longlat +ellps=WGS84 " + "+pm=paris +step +proj=unitconvert +xy_in=rad +z_in=m " + "+xy_out=deg +z_out=ft"); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_3D_lat_long_non_metre_to_geogCRS_longlat) { + + auto wkt = "GEOGCRS[\"my CRS\",\n" + " DATUM[\"World Geodetic System 1984\",\n" + " ELLIPSOID[\"WGS 84\",6378137,298.257223563],\n" + " ID[\"EPSG\",6326]],\n" + " CS[ellipsoidal,3],\n" + " AXIS[\"latitude\",north,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " AXIS[\"longitude\",east,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " AXIS[\"ellipsoidal height\",up,\n" + " LENGTHUNIT[\"my_vunit\",0.3]]]"; + auto srcCRS_obj = WKTParser().createFromWKT(wkt); + auto srcCRS = nn_dynamic_pointer_cast(srcCRS_obj); + ASSERT_TRUE(srcCRS != nullptr); + + auto dstCRS_obj = PROJStringParser().createFromPROJString( + "+proj=longlat +datum=WGS84 +type=crs"); + auto dstCRS = nn_dynamic_pointer_cast(dstCRS_obj); + ASSERT_TRUE(dstCRS != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(srcCRS), NN_CHECK_ASSERT(dstCRS)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +z_in=0.3 +z_out=m"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_without_id_to_geogCRS_3D_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto src = + authFactory->createCoordinateReferenceSystem("4289"); // Amersfoort + auto dst = + authFactory->createCoordinateReferenceSystem("4937"); // ETRS89 3D + auto list = + CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); + ASSERT_GE(list.size(), 1U); + auto wkt2 = "GEOGCRS[\"unnamed\",\n" + " DATUM[\"Amersfoort\",\n" + " ELLIPSOID[\"Bessel 1841\",6377397.155,299.1528128,\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " CS[ellipsoidal,2],\n" + " AXIS[\"geodetic latitude (Lat)\",north,\n" + " ORDER[1],\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " AXIS[\"geodetic longitude (Lon)\",east,\n" + " ORDER[2],\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]]," + " USAGE[\n" + " SCOPE[\"unknown\"],\n" + " AREA[\"Netherlands - onshore\"],\n" + " BBOX[50.75,3.2,53.7,7.22]]]\n"; + + auto obj = WKTParser().createFromWKT(wkt2); + auto src_from_wkt2 = nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(src_from_wkt2 != nullptr); + auto list2 = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(src_from_wkt2), dst, ctxt); + ASSERT_GE(list.size(), list2.size()); + for (size_t i = 0; i < list.size(); i++) { + const auto &op = list[i]; + const auto &op2 = list2[i]; + EXPECT_TRUE( + op->isEquivalentTo(op2.get(), IComparable::Criterion::EQUIVALENT)) + << op->nameStr() << " " << op2->nameStr(); + } +} + +// --------------------------------------------------------------------------- + +static GeodeticCRSNNPtr createGeocentricDatumWGS84() { + PropertyMap propertiesCRS; + propertiesCRS.set(Identifier::CODESPACE_KEY, "EPSG") + .set(Identifier::CODE_KEY, 4328) + .set(IdentifiedObject::NAME_KEY, "WGS 84"); + return GeodeticCRS::create( + propertiesCRS, GeodeticReferenceFrame::EPSG_6326, + CartesianCS::createGeocentric(UnitOfMeasure::METRE)); +} + +// --------------------------------------------------------------------------- + +static GeodeticCRSNNPtr createGeocentricKM() { + PropertyMap propertiesCRS; + propertiesCRS.set(IdentifiedObject::NAME_KEY, "Based on WGS 84"); + return GeodeticCRS::create( + propertiesCRS, GeodeticReferenceFrame::EPSG_6326, + CartesianCS::createGeocentric( + UnitOfMeasure("kilometre", 1000.0, UnitOfMeasure::Type::LINEAR))); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geocentricCRS_to_geogCRS_same_datum) { + + auto op = CoordinateOperationFactory::create()->createOperation( + createGeocentricDatumWGS84(), GeographicCRS::EPSG_4326); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " + "+order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geocentricCRS_to_geogCRS_different_datum) { + + auto op = CoordinateOperationFactory::create()->createOperation( + createGeocentricDatumWGS84(), GeographicCRS::EPSG_4269); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->nameStr(), + "Ballpark geocentric translation from WGS 84 to NAD83 " + "(geocentric) + Conversion from NAD83 " + "(geocentric) to NAD83"); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=cart +ellps=GRS80 +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " + "+order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geocentricCRS_different_datum) { + + auto op = CoordinateOperationFactory::create()->createOperation( + GeographicCRS::EPSG_4269, createGeocentricDatumWGS84()); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->nameStr(), "Conversion from NAD83 to NAD83 (geocentric) + " + "Ballpark geocentric translation from NAD83 " + "(geocentric) to WGS 84"); + 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 +proj=cart " + "+ellps=GRS80"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geocentricCRS_to_geocentricCRS_same_noop) { + + auto op = CoordinateOperationFactory::create()->createOperation( + createGeocentricDatumWGS84(), createGeocentricDatumWGS84()); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->nameStr(), + "Null geocentric translation from WGS 84 to WGS 84"); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=noop"); + EXPECT_EQ(op->inverse()->nameStr(), op->nameStr()); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geocentricCRS_to_geocentricCRS_different_ballpark) { + + PropertyMap propertiesCRS; + propertiesCRS.set(Identifier::CODESPACE_KEY, "EPSG") + .set(Identifier::CODE_KEY, 4328) + .set(IdentifiedObject::NAME_KEY, "unknown"); + auto otherGeocentricCRS = GeodeticCRS::create( + propertiesCRS, GeodeticReferenceFrame::EPSG_6269, + CartesianCS::createGeocentric(UnitOfMeasure::METRE)); + + auto op = CoordinateOperationFactory::create()->createOperation( + createGeocentricKM(), otherGeocentricCRS); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ( + op->nameStr(), + "Ballpark geocentric translation from Based on WGS 84 to unknown"); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=unitconvert +xy_in=km +z_in=km +xy_out=m +z_out=m"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geocentricCRS_to_geogCRS_same_datum_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4326"), + // WGS84 geocentric + authFactory->createCoordinateReferenceSystem("4978"), ctxt); + ASSERT_EQ(list.size(), 1U); + + EXPECT_EQ(list[0]->nameStr(), + "Conversion from WGS 84 (geog2D) to WGS 84 (geocentric)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=cart " + "+ellps=WGS84"); + + EXPECT_EQ(list[0]->inverse()->nameStr(), + "Conversion from WGS 84 (geocentric) to WGS 84 (geog2D)"); + EXPECT_EQ(list[0]->inverse()->exportToPROJString( + PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " + "+order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geocentricCRS_to_geogCRS_same_datum_context_all_auth) { + // This is to check we don't use OGC:CRS84 as a pivot + auto authFactoryEPSG = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto authFactoryAll = + AuthorityFactory::create(DatabaseContext::create(), std::string()); + auto ctxt = + CoordinateOperationContext::create(authFactoryAll, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactoryEPSG->createCoordinateReferenceSystem("4326"), + // WGS84 geocentric + authFactoryEPSG->createCoordinateReferenceSystem("4978"), ctxt); + ASSERT_EQ(list.size(), 1U); + + EXPECT_EQ(list[0]->nameStr(), + "Conversion from WGS 84 (geog2D) to WGS 84 (geocentric)"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geocentricCRS_to_geocentricCRS_different_datum_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + // ITRF2000 (geocentric) + authFactory->createCoordinateReferenceSystem("4919"), + // ITRF2005 (geocentric) + authFactory->createCoordinateReferenceSystem("4896"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), "ITRF2000 to ITRF2005 (1)"); + EXPECT_PRED_FORMAT2( + ComparePROJString, + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=helmert +x=-0.0001 " + "+y=0.0008 +z=0.0058 +rx=0 +ry=0 +rz=0 +s=-0.0004 +dx=0.0002 " + "+dy=-0.0001 +dz=0.0018 +drx=0 +dry=0 +drz=0 +ds=-8e-05 " + "+t_epoch=2000 +convention=position_vector"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_geocentricCRS_same_datum_to_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + // WGS84 geocentric + authFactory->createCoordinateReferenceSystem("4978"), + authFactory->createCoordinateReferenceSystem("4326"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), + "Conversion from WGS 84 (geocentric) to WGS 84 (geog2D)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " + "+order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + geog2D_to_geog3D_same_datum_but_with_potential_other_pivot_context) { + // Check that when going from geog2D to geog3D of same datum, we don't + // try to go through a WGS84 pivot... + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("5365"), // CR 05 2D + authFactory->createCoordinateReferenceSystem("5364"), // CR 05 3D + ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=noop"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + geogCRS_to_geogCRS_different_datum_though_geocentric_transform_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + // ITRF2000 (geog3D) + authFactory->createCoordinateReferenceSystem("7909"), + // ITRF2005 (geog3D) + authFactory->createCoordinateReferenceSystem("7910"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), + "Conversion from ITRF2000 (geog3D) to ITRF2000 (geocentric) + " + "ITRF2000 to ITRF2005 (1) + " + "Conversion from ITRF2005 (geocentric) to ITRF2005 (geog3D)"); + EXPECT_PRED_FORMAT2( + ComparePROJString, + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +z_in=m +xy_out=rad +z_out=m " + "+step +proj=cart +ellps=GRS80 +step +proj=helmert +x=-0.0001 " + "+y=0.0008 +z=0.0058 +rx=0 +ry=0 +rz=0 +s=-0.0004 +dx=0.0002 " + "+dy=-0.0001 +dz=0.0018 +drx=0 +dry=0 +drz=0 +ds=-8e-05 " + "+t_epoch=2000 +convention=position_vector +step +inv " + "+proj=cart +ellps=GRS80 +step +proj=unitconvert +xy_in=rad " + "+z_in=m +xy_out=deg +z_out=m +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_geocentricCRS_different_datum_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + // ITRF2000 (geog3D) + authFactory->createCoordinateReferenceSystem("7909"), + // ITRF2005 (geocentric) + authFactory->createCoordinateReferenceSystem("4896"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), + "Conversion from ITRF2000 (geog3D) to ITRF2000 (geocentric) + " + "ITRF2000 to ITRF2005 (1)"); + EXPECT_PRED_FORMAT2( + ComparePROJString, + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +z_in=m +xy_out=rad +z_out=m " + "+step +proj=cart +ellps=GRS80 +step +proj=helmert +x=-0.0001 " + "+y=0.0008 +z=0.0058 +rx=0 +ry=0 +rz=0 +s=-0.0004 +dx=0.0002 " + "+dy=-0.0001 +dz=0.0018 +drx=0 +dry=0 +drz=0 +ds=-8e-05 " + "+t_epoch=2000 +convention=position_vector"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geocentricCRS_to_geogCRS_different_datum_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + // ITRF2000 (geocentric) + authFactory->createCoordinateReferenceSystem("4919"), + // ITRF2005 (geog3D) + authFactory->createCoordinateReferenceSystem("7910"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), + "ITRF2000 to ITRF2005 (1) + " + "Conversion from ITRF2005 (geocentric) to ITRF2005 (geog3D)"); + EXPECT_PRED_FORMAT2( + ComparePROJString, + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=helmert +x=-0.0001 " + "+y=0.0008 +z=0.0058 +rx=0 +ry=0 +rz=0 +s=-0.0004 +dx=0.0002 " + "+dy=-0.0001 +dz=0.0018 +drx=0 +dry=0 +drz=0 +ds=-8e-05 " + "+t_epoch=2000 +convention=position_vector +step +inv " + "+proj=cart +ellps=GRS80 +step +proj=unitconvert +xy_in=rad " + "+z_in=m +xy_out=deg +z_out=m +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, esri_projectedCRS_to_geogCRS_with_ITRF_intermediate_context) { + auto dbContext = DatabaseContext::create(); + auto authFactoryEPSG = AuthorityFactory::create(dbContext, "EPSG"); + auto authFactoryESRI = AuthorityFactory::create(dbContext, "ESRI"); + auto ctxt = + CoordinateOperationContext::create(authFactoryEPSG, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + // NAD_1983_CORS96_StatePlane_North_Carolina_FIPS_3200_Ft_US (projected) + authFactoryESRI->createCoordinateReferenceSystem("103501"), + // ITRF2005 (geog3D) + authFactoryEPSG->createCoordinateReferenceSystem("7910"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), + "Inverse of NAD_1983_CORS96_StatePlane_North_Carolina_" + "FIPS_3200_Ft_US + " + "Conversion from NAD83(CORS96) (geog2D) to NAD83(CORS96) " + "(geocentric) + Inverse of ITRF2000 to NAD83(CORS96) (1) + " + "ITRF2000 to ITRF2005 (1) + " + "Conversion from ITRF2005 (geocentric) to ITRF2005 (geog3D)"); + EXPECT_PRED_FORMAT2( + ComparePROJString, + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft " + "+xy_out=m +step +inv +proj=lcc +lat_0=33.75 +lon_0=-79 " + "+lat_1=34.3333333333333 +lat_2=36.1666666666667 " + "+x_0=609601.219202438 +y_0=0 +ellps=GRS80 +step +proj=cart " + "+ellps=GRS80 +step +inv +proj=helmert +x=0.9956 +y=-1.9013 " + "+z=-0.5215 +rx=0.025915 +ry=0.009426 +rz=0.011599 +s=0.00062 " + "+dx=0.0007 +dy=-0.0007 +dz=0.0005 +drx=6.7e-05 +dry=-0.000757 " + "+drz=-5.1e-05 +ds=-0.00018 +t_epoch=1997 " + "+convention=coordinate_frame +step +proj=helmert +x=-0.0001 " + "+y=0.0008 +z=0.0058 +rx=0 +ry=0 +rz=0 +s=-0.0004 +dx=0.0002 " + "+dy=-0.0001 +dz=0.0018 +drx=0 +dry=0 +drz=0 +ds=-8e-05 " + "+t_epoch=2000 +convention=position_vector +step +inv +proj=cart " + "+ellps=GRS80 +step +proj=unitconvert +xy_in=rad +z_in=m " + "+xy_out=deg +z_out=m +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +static ProjectedCRSNNPtr createUTM31_WGS84() { + return ProjectedCRS::create( + PropertyMap(), GeographicCRS::EPSG_4326, + Conversion::createUTM(PropertyMap(), 31, true), + CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); +} + +// --------------------------------------------------------------------------- + +static ProjectedCRSNNPtr createUTM32_WGS84() { + return ProjectedCRS::create( + PropertyMap(), GeographicCRS::EPSG_4326, + Conversion::createUTM(PropertyMap(), 32, true), + CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_projCRS) { + + auto op = CoordinateOperationFactory::create()->createOperation( + GeographicCRS::EPSG_4326, createUTM31_WGS84()); + ASSERT_TRUE(op != nullptr); + EXPECT_TRUE(std::dynamic_pointer_cast(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 +proj=utm " + "+zone=31 +ellps=WGS84"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_longlat_to_geogCS_latlong) { + + auto sourceCRS = GeographicCRS::OGC_CRS84; + auto targetCRS = GeographicCRS::EPSG_4326; + auto op = CoordinateOperationFactory::create()->createOperation(sourceCRS, + targetCRS); + ASSERT_TRUE(op != nullptr); + auto conv = std::dynamic_pointer_cast(op); + ASSERT_TRUE(conv != nullptr); + EXPECT_TRUE(op->sourceCRS() && + op->sourceCRS()->isEquivalentTo(sourceCRS.get())); + EXPECT_TRUE(op->targetCRS() && + op->targetCRS()->isEquivalentTo(targetCRS.get())); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=axisswap +order=2,1"); + auto convInverse = nn_dynamic_pointer_cast(conv->inverse()); + ASSERT_TRUE(convInverse != nullptr); + EXPECT_TRUE(convInverse->sourceCRS() && + convInverse->sourceCRS()->isEquivalentTo(targetCRS.get())); + EXPECT_TRUE(convInverse->targetCRS() && + convInverse->targetCRS()->isEquivalentTo(sourceCRS.get())); + EXPECT_EQ(conv->method()->exportToWKT(WKTFormatter::create().get()), + convInverse->method()->exportToWKT(WKTFormatter::create().get())); + EXPECT_TRUE(conv->method()->isEquivalentTo(convInverse->method().get())); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_longlat_to_geogCS_latlong_database) { + + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), std::string()); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + AuthorityFactory::create(DatabaseContext::create(), "OGC") + ->createCoordinateReferenceSystem("CRS84"), + AuthorityFactory::create(DatabaseContext::create(), "EPSG") + ->createCoordinateReferenceSystem("4326"), + ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_longlat_to_projCRS) { + + auto op = CoordinateOperationFactory::create()->createOperation( + GeographicCRS::OGC_CRS84, createUTM31_WGS84()); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=utm +zone=31 +ellps=WGS84"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_different_from_baseCRS_to_projCRS) { + + auto op = CoordinateOperationFactory::create()->createOperation( + GeographicCRS::EPSG_4807, createUTM31_WGS84()); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ( + op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv +proj=longlat " + "+ellps=clrk80ign +pm=paris +step +proj=utm +zone=31 " + "+ellps=WGS84"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + geogCRS_different_from_baseCRS_to_projCRS_context_compatible_area) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setAllowUseIntermediateCRS( + CoordinateOperationContext::IntermediateCRSUse::ALWAYS); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("4807"), // NTF(Paris) + authFactory->createCoordinateReferenceSystem("32631"), // UTM31 WGS84 + ctxt); + ASSERT_EQ(list.size(), 4U); + EXPECT_EQ( + list[0]->nameStr(), + "NTF (Paris) to NTF (1) + Inverse of WGS 84 to NTF (3) + UTM zone 31N"); + ASSERT_EQ(list[0]->coordinateOperationAccuracies().size(), 1U); + EXPECT_EQ(list[0]->coordinateOperationAccuracies()[0]->value(), "1"); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " + "+proj=longlat +ellps=clrk80ign +pm=paris +step +proj=hgridshift " + "+grids=fr_ign_ntf_r93.tif +step +proj=utm +zone=31 +ellps=WGS84"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geocentricCRS_to_projCRS) { + + auto op = CoordinateOperationFactory::create()->createOperation( + createGeocentricDatumWGS84(), createUTM31_WGS84()); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 +step " + "+proj=utm +zone=31 +ellps=WGS84"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_to_geogCRS) { + + auto op = CoordinateOperationFactory::create()->createOperation( + createUTM31_WGS84(), GeographicCRS::EPSG_4326); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=utm +zone=31 +ellps=WGS84 +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " + "+order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_no_id_to_geogCRS_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto src = authFactory->createCoordinateReferenceSystem( + "28992"); // Amersfoort / RD New + auto dst = + authFactory->createCoordinateReferenceSystem("4258"); // ETRS89 2D + auto list = + CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); + ASSERT_GE(list.size(), 1U); + auto wkt2 = + "PROJCRS[\"unknown\",\n" + " BASEGEOGCRS[\"Amersfoort\",\n" + " DATUM[\"Amersfoort\",\n" + " ELLIPSOID[\"Bessel 1841\",6377397.155,299.1528128]]],\n" + " CONVERSION[\"unknown\",\n" + " METHOD[\"Oblique Stereographic\"],\n" + " PARAMETER[\"Latitude of natural origin\",52.1561605555556],\n" + " PARAMETER[\"Longitude of natural origin\",5.38763888888889],\n" + " PARAMETER[\"Scale factor at natural origin\",0.9999079],\n" + " PARAMETER[\"False easting\",155000],\n" + " PARAMETER[\"False northing\",463000]],\n" + " CS[Cartesian,2],\n" + " AXIS[\"(E)\",east],\n" + " AXIS[\"(N)\",north],\n" + " LENGTHUNIT[\"metre\",1],\n" + " ID[\"EPSG\",28992]]"; + auto obj = WKTParser().createFromWKT(wkt2); + auto src_from_wkt2 = nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(src_from_wkt2 != nullptr); + auto list2 = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(src_from_wkt2), dst, ctxt); + ASSERT_GE(list.size(), list2.size() - 1); + for (size_t i = 0; i < list.size(); i++) { + const auto &op = list[i]; + const auto &op2 = list2[i]; + EXPECT_TRUE( + op->isEquivalentTo(op2.get(), IComparable::Criterion::EQUIVALENT)); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_3D_to_geogCRS_3D_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto wkt = "PROJCRS[\"NAD83(HARN) / Oregon GIC Lambert (ft)\",\n" + " BASEGEOGCRS[\"NAD83(HARN)\",\n" + " DATUM[\"NAD83 (High Accuracy Reference Network)\",\n" + " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " ID[\"EPSG\",4957]],\n" + " CONVERSION[\"unnamed\",\n" + " METHOD[\"Lambert Conic Conformal (2SP)\",\n" + " ID[\"EPSG\",9802]],\n" + " PARAMETER[\"Latitude of false origin\",41.75,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8821]],\n" + " PARAMETER[\"Longitude of false origin\",-120.5,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8822]],\n" + " PARAMETER[\"Latitude of 1st standard parallel\",43,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8823]],\n" + " PARAMETER[\"Latitude of 2nd standard parallel\",45.5,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8824]],\n" + " PARAMETER[\"Easting at false origin\",1312335.958,\n" + " LENGTHUNIT[\"foot\",0.3048],\n" + " ID[\"EPSG\",8826]],\n" + " PARAMETER[\"Northing at false origin\",0,\n" + " LENGTHUNIT[\"foot\",0.3048],\n" + " ID[\"EPSG\",8827]]],\n" + " CS[Cartesian,3],\n" + " AXIS[\"easting\",east,\n" + " ORDER[1],\n" + " LENGTHUNIT[\"foot\",0.3048]],\n" + " AXIS[\"northing\",north,\n" + " ORDER[2],\n" + " LENGTHUNIT[\"foot\",0.3048]],\n" + " AXIS[\"ellipsoidal height (h)\",up,\n" + " ORDER[3],\n" + " LENGTHUNIT[\"foot\",0.3048]]]"; + auto obj = WKTParser().createFromWKT(wkt); + auto src = NN_CHECK_ASSERT(nn_dynamic_pointer_cast(obj)); + auto dst = authFactory->createCoordinateReferenceSystem( + "4957"); // NAD83(HARN) (3D) + auto list = + CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + // Check that z ft->m conversion is done (and just once) + "+step +proj=unitconvert +xy_in=ft +z_in=ft +xy_out=m +z_out=m " + "+step +inv +proj=lcc +lat_0=41.75 +lon_0=-120.5 +lat_1=43 " + "+lat_2=45.5 +x_0=399999.9999984 +y_0=0 +ellps=GRS80 " + "+step +proj=unitconvert +xy_in=rad +z_in=m +xy_out=deg +z_out=m " + "+step +proj=axisswap +order=2,1"); +} +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_3D_to_projCRS_2D_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto wkt = + "PROJCRS[\"Projected 3d CRS\",\n" + " BASEGEOGCRS[\"JGD2000\",\n" + " DATUM[\"Japanese Geodetic Datum 2000\",\n" + " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " ID[\"EPSG\",4947]],\n" // the code is what triggered the bug + " CONVERSION[\"Japan Plane Rectangular CS zone VII\",\n" + " METHOD[\"Transverse Mercator\",\n" + " ID[\"EPSG\",9807]],\n" + " PARAMETER[\"Latitude of natural origin\",36,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8801]],\n" + " PARAMETER[\"Longitude of natural origin\",137.166666666667,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8802]],\n" + " PARAMETER[\"Scale factor at natural origin\",0.9999,\n" + " SCALEUNIT[\"unity\",1],\n" + " ID[\"EPSG\",8805]],\n" + " PARAMETER[\"False easting\",0,\n" + " LENGTHUNIT[\"metre\",1],\n" + " ID[\"EPSG\",8806]],\n" + " PARAMETER[\"False northing\",0,\n" + " LENGTHUNIT[\"metre\",1],\n" + " ID[\"EPSG\",8807]],\n" + " ID[\"EPSG\",17807]],\n" + " CS[Cartesian,3],\n" + " AXIS[\"northing (X)\",north,\n" + " ORDER[1],\n" + " LENGTHUNIT[\"metre\",1,\n" + " ID[\"EPSG\",9001]]],\n" + " AXIS[\"easting (Y)\",east,\n" + " ORDER[2],\n" + " LENGTHUNIT[\"metre\",1,\n" + " ID[\"EPSG\",9001]]],\n" + " AXIS[\"ellipsoidal height (h)\",up,\n" + " ORDER[3],\n" + " LENGTHUNIT[\"metre\",1,\n" + " ID[\"EPSG\",9001]]]]"; + auto obj = WKTParser().createFromWKT(wkt); + auto src = NN_CHECK_ASSERT(nn_dynamic_pointer_cast(obj)); + auto dst = + authFactory->createCoordinateReferenceSystem("32653"); // WGS 84 UTM 53 + // We just want to check that we don't get inconsistent chaining exception + auto list = + CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); + ASSERT_GE(list.size(), 1U); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_3D_to_projCRS_with_2D_geocentric_translation) { + + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto src = + authFactory->createCoordinateReferenceSystem("4979"); // WGS 84 3D + + // Azores Central 1948 / UTM zone 26N + auto dst = authFactory->createCoordinateReferenceSystem("2189"); + + auto list = + CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +z_in=m +xy_out=rad +z_out=m " + "+step +proj=push +v_3 " // this is what we check. Due to the + // target system being 2D only + "+step +proj=cart +ellps=WGS84 " + "+step +proj=helmert +x=104 +y=-167 +z=38 " + "+step +inv +proj=cart +ellps=intl " + "+step +proj=pop +v_3 " // this is what we check + "+step +proj=utm +zone=26 +ellps=intl"); + + auto listReverse = + CoordinateOperationFactory::create()->createOperations(dst, src, ctxt); + ASSERT_GE(listReverse.size(), 1U); + EXPECT_EQ( + listReverse[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +inv +proj=utm +zone=26 +ellps=intl " + "+step +proj=push +v_3 " // this is what we check + "+step +proj=cart +ellps=intl " + "+step +proj=helmert +x=-104 +y=167 +z=-38 " + "+step +inv +proj=cart +ellps=WGS84 " + "+step +proj=pop +v_3 " // this is what we check + "+step +proj=unitconvert +xy_in=rad +z_in=m +xy_out=deg +z_out=m " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_to_projCRS) { + + auto op = CoordinateOperationFactory::create()->createOperation( + createUTM31_WGS84(), createUTM32_WGS84()); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=utm +zone=31 +ellps=WGS84 +step " + "+proj=utm +zone=32 +ellps=WGS84"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_to_projCRS_different_baseCRS) { + + auto utm32 = ProjectedCRS::create( + PropertyMap(), GeographicCRS::EPSG_4807, + Conversion::createUTM(PropertyMap(), 32, true), + CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); + + auto op = CoordinateOperationFactory::create()->createOperation( + createUTM31_WGS84(), utm32); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=utm +zone=31 +ellps=WGS84 +step " + "+proj=utm +zone=32 +ellps=clrk80ign +pm=paris"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_to_projCRS_context_compatible_area) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("32634"), // UTM 34 + authFactory->createCoordinateReferenceSystem( + "2171"), // Pulkovo 42 Poland I + ctxt); + ASSERT_EQ(list.size(), 2U); + EXPECT_EQ(list[0]->nameStr(), + "Inverse of UTM zone 34N + Inverse of Pulkovo 1942(58) to WGS 84 " + "(1) + Poland zone I"); + ASSERT_EQ(list[0]->coordinateOperationAccuracies().size(), 1U); + EXPECT_EQ(list[0]->coordinateOperationAccuracies()[0]->value(), "1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_to_projCRS_context_compatible_area_bis) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem( + "3844"), // Pulkovo 42 Stereo 70 (Romania) + authFactory->createCoordinateReferenceSystem("32634"), // UTM 34 + ctxt); + ASSERT_EQ(list.size(), 3U); + EXPECT_EQ(list[0]->nameStr(), "Inverse of Stereo 70 + " + "Pulkovo 1942(58) to WGS 84 " + "(19) + UTM zone 34N"); + ASSERT_EQ(list[0]->coordinateOperationAccuracies().size(), 1U); + EXPECT_EQ(list[0]->coordinateOperationAccuracies()[0]->value(), "3"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_to_projCRS_context_one_incompatible_area) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("32631"), // UTM 31 + authFactory->createCoordinateReferenceSystem( + "2171"), // Pulkovo 42 Poland I + ctxt); + ASSERT_EQ(list.size(), 2U); + EXPECT_EQ(list[0]->nameStr(), + "Inverse of UTM zone 31N + Inverse of Pulkovo 1942(58) to WGS 84 " + "(1) + Poland zone I"); + ASSERT_EQ(list[0]->coordinateOperationAccuracies().size(), 1U); + EXPECT_EQ(list[0]->coordinateOperationAccuracies()[0]->value(), "1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_to_projCRS_context_incompatible_areas) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("32631"), // UTM 31 + authFactory->createCoordinateReferenceSystem("32633"), // UTM 33 + ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), "Inverse of UTM zone 31N + UTM zone 33N"); + ASSERT_EQ(list[0]->coordinateOperationAccuracies().size(), 1U); + EXPECT_EQ(list[0]->coordinateOperationAccuracies()[0]->value(), "0"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_to_projCRS_context_incompatible_areas_ballpark) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("26711"), // UTM 11 NAD27 + authFactory->createCoordinateReferenceSystem( + "3034"), // ETRS89 / LCC Europe + ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_TRUE(list[0]->hasBallparkTransformation()); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + projCRS_to_projCRS_context_incompatible_areas_crs_extent_use_intersection) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSourceAndTargetCRSExtentUse( + CoordinateOperationContext::SourceTargetCRSExtentUse::INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("26711"), // UTM 11 NAD27 + authFactory->createCoordinateReferenceSystem( + "3034"), // ETRS89 / LCC Europe + ctxt); + ASSERT_GE(list.size(), 0U); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_to_projCRS_north_pole_inverted_axis) { + + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), std::string()); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + AuthorityFactory::create(DatabaseContext::create(), "EPSG") + ->createCoordinateReferenceSystem("32661"), + AuthorityFactory::create(DatabaseContext::create(), "EPSG") + ->createCoordinateReferenceSystem("5041"), + ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_to_projCRS_south_pole_inverted_axis) { + + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), std::string()); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + AuthorityFactory::create(DatabaseContext::create(), "EPSG") + ->createCoordinateReferenceSystem("32761"), + AuthorityFactory::create(DatabaseContext::create(), "EPSG") + ->createCoordinateReferenceSystem("5042"), + ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_to_projCRS_through_geog3D) { + // Check that when going from projCRS to projCRS, using + // geog2D-->geog3D-->geog3D-->geog2D we do not have issues with + // inconsistent CRS chaining, due to how we 'hack' a bit some intermediate + // steps + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("5367"), // CR05 / CRTM05 + authFactory->createCoordinateReferenceSystem( + "8908"), // CR-SIRGAS / CRTM05 + ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 " + "+step +inv +proj=tmerc +lat_0=0 +lon_0=-84 +k=0.9999 " + "+x_0=500000 +y_0=0 +ellps=WGS84 " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=WGS84 " + "+step +proj=helmert +x=-0.16959 +y=0.35312 +z=0.51846 " + "+rx=-0.03385 +ry=0.16325 +rz=-0.03446 +s=0.03693 " + "+convention=position_vector " + "+step +inv +proj=cart +ellps=GRS80 " + "+step +proj=pop +v_3 " + "+step +proj=tmerc +lat_0=0 +lon_0=-84 +k=0.9999 +x_0=500000 " + "+y_0=0 +ellps=GRS80"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, transform_from_amersfoort_rd_new_to_epsg_4326) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem("28992"), + authFactory->createCoordinateReferenceSystem("4326"), ctxt); + ASSERT_EQ(list.size(), 2U); + // The order matters: "Amersfoort to WGS 84 (4)" replaces "Amersfoort to WGS + // 84 (3)" + EXPECT_EQ(list[0]->nameStr(), + "Inverse of RD New + Amersfoort to WGS 84 (4)"); + EXPECT_EQ(list[1]->nameStr(), + "Inverse of RD New + Amersfoort to WGS 84 (3)"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_of_geogCRS_to_geogCRS) { + auto boundCRS = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto op = CoordinateOperationFactory::create()->createOperation( + boundCRS, GeographicCRS::EPSG_4326); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " + "+proj=longlat +ellps=clrk80ign +pm=paris +step +proj=push +v_3 " + "+step +proj=cart +ellps=clrk80ign +step +proj=helmert +x=1 +y=2 " + "+z=3 +rx=4 +ry=5 +rz=6 +s=7 +convention=position_vector +step " + "+inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " + "+order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_of_geogCRS_to_geodCRS) { + auto boundCRS = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto op = CoordinateOperationFactory::create()->createOperation( + boundCRS, GeodeticCRS::EPSG_4978); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step " + "+proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=grad +xy_out=rad " + "+step +inv +proj=longlat +ellps=clrk80ign +pm=paris " + "+step +proj=cart +ellps=clrk80ign " + "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 " + "+convention=position_vector"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_of_geogCRS_to_geodCRS_not_related_to_hub) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto boundCRS = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + boundCRS, + // ETRS89 geocentric + authFactory->createCoordinateReferenceSystem("4936"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step " + "+proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=grad +xy_out=rad " + "+step +inv +proj=longlat +ellps=clrk80ign +pm=paris " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=clrk80ign " + "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 " + "+convention=position_vector " + "+step +inv +proj=cart +ellps=GRS80 " + "+step +proj=pop +v_3 " + "+step +proj=cart +ellps=GRS80"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_of_geogCRS_to_geogCRS_with_area) { + auto boundCRS = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4267, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto op = CoordinateOperationFactory::create()->createOperation( + boundCRS, authFactory->createCoordinateReferenceSystem("4326")); + 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 +proj=push +v_3 " + "+step +proj=cart +ellps=clrk66 +step +proj=helmert +x=1 +y=2 " + "+z=3 +rx=4 +ry=5 +rz=6 +s=7 +convention=position_vector +step " + "+inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " + "+order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_of_geogCRS_to_unrelated_geogCRS) { + auto boundCRS = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto op = CoordinateOperationFactory::create()->createOperation( + boundCRS, GeographicCRS::EPSG_4269); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + CoordinateOperationFactory::create() + ->createOperation(GeographicCRS::EPSG_4807, + GeographicCRS::EPSG_4269) + ->exportToPROJString(PROJStringFormatter::create().get())); +} + +// --------------------------------------------------------------------------- + +TEST(operation, createOperation_boundCRS_identified_by_datum) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=longlat +datum=WGS84 +type=crs"); + auto src = nn_dynamic_pointer_cast(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 +type=crs"); + auto dest = nn_dynamic_pointer_cast(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=push +v_3 +step +proj=cart +ellps=WGS84 +step " + "+proj=helmert +x=263 +y=-6 +z=-431 +step +inv +proj=cart " + "+ellps=clrk80ign +step +proj=pop +v_3 +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 +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDest = PROJStringParser().createFromPROJString( + "+proj=latlong +datum=NAD83 +type=crs"); + auto dest = nn_dynamic_pointer_cast(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 " + "+type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDest = PROJStringParser().createFromPROJString( + "+proj=latlong +datum=NAD83 +type=crs"); + auto dest = nn_dynamic_pointer_cast(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, + Conversion::createUTM(PropertyMap(), 31, true), + CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); + auto boundCRS = BoundCRS::createFromTOWGS84( + utm31, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto op = CoordinateOperationFactory::create()->createOperation( + boundCRS, GeographicCRS::EPSG_4326); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=utm +zone=31 +ellps=clrk80ign " + "+pm=paris +step +proj=push +v_3 +step +proj=cart " + "+ellps=clrk80ign +step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 " + "+rz=6 +s=7 +convention=position_vector +step +inv +proj=cart " + "+ellps=WGS84 +step +proj=pop +v_3 +step +proj=unitconvert " + "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_of_geogCRS_to_projCRS) { + auto boundCRS = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto utm31 = ProjectedCRS::create( + PropertyMap(), GeographicCRS::EPSG_4326, + Conversion::createUTM(PropertyMap(), 31, true), + CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); + auto op = + CoordinateOperationFactory::create()->createOperation(boundCRS, utm31); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " + "+proj=longlat +ellps=clrk80ign +pm=paris +step +proj=push +v_3 " + "+step +proj=cart +ellps=clrk80ign +step +proj=helmert +x=1 +y=2 " + "+z=3 +rx=4 +ry=5 +rz=6 +s=7 +convention=position_vector +step " + "+inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step " + "+proj=utm +zone=31 +ellps=WGS84"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_of_geogCRS_to_unrelated_geogCRS_context) { + auto src = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + // ETRS89 + auto dst = authFactory->createCoordinateReferenceSystem("4258"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = + CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); + ASSERT_EQ(list.size(), 1U); + // Check with it is a concatenated operation, since it doesn't particularly + // show up in the PROJ string + EXPECT_TRUE(dynamic_cast(list[0].get()) != + nullptr); + EXPECT_EQ(list[0]->nameStr(), "Transformation from NTF (Paris) to WGS84 + " + "Inverse of ETRS89 to WGS 84 (1)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=grad +xy_out=rad " + "+step +inv +proj=longlat +ellps=clrk80ign +pm=paris " + "+step +proj=push +v_3 +step +proj=cart +ellps=clrk80ign " + "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 " + "+convention=position_vector " + "+step +inv +proj=cart +ellps=GRS80 +step +proj=pop +v_3 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geogCRS_to_boundCRS_of_geogCRS) { + auto boundCRS = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto op = CoordinateOperationFactory::create()->createOperation( + GeographicCRS::EPSG_4326, boundCRS); + 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 +proj=push +v_3 " + "+step +proj=cart +ellps=WGS84 +step +inv +proj=helmert +x=1 " + "+y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 +convention=position_vector " + "+step +inv +proj=cart +ellps=clrk80ign +step +proj=pop +v_3 " + "+step +proj=longlat +ellps=clrk80ign +pm=paris +step " + "+proj=unitconvert +xy_in=rad +xy_out=grad +step +proj=axisswap " + "+order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_to_geogCRS_same_datum_context) { + auto boundCRS = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4269, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto dbContext = DatabaseContext::create(); + auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + boundCRS, GeographicCRS::EPSG_4269, ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=noop"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_to_geogCRS_hubCRS_and_targetCRS_same_but_baseCRS_not) { + const char *wkt = + "COMPD_CS[\"NAD83 + Ellipsoid (US Feet)\",\n" + " GEOGCS[\"NAD83\",\n" + " DATUM[\"North_American_Datum_1983\",\n" + " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" + " AUTHORITY[\"EPSG\",\"7019\"]],\n" + " TOWGS84[0,0,0,0,0,0,0],\n" + " AUTHORITY[\"EPSG\",\"6269\"]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " AUTHORITY[\"EPSG\",\"8901\"]],\n" + " UNIT[\"degree\",0.0174532925199433,\n" + " AUTHORITY[\"EPSG\",\"9122\"]],\n" + " AUTHORITY[\"EPSG\",\"4269\"]],\n" + " VERT_CS[\"Ellipsoid (US Feet)\",\n" + " VERT_DATUM[\"Ellipsoid\",2002],\n" + " UNIT[\"US survey foot\",0.304800609601219,\n" + " AUTHORITY[\"EPSG\",\"9003\"]],\n" + " AXIS[\"Up\",UP]]]"; + + auto dbContext = DatabaseContext::create(); + auto obj = WKTParser().attachDatabaseContext(dbContext).createFromWKT(wkt); + auto boundCRS = nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(boundCRS != nullptr); + auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(boundCRS), GeographicCRS::EPSG_4979, ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=unitconvert +z_in=us-ft +z_out=m"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_to_boundCRS) { + auto utm31 = ProjectedCRS::create( + PropertyMap(), GeographicCRS::EPSG_4807, + Conversion::createUTM(PropertyMap(), 31, true), + CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); + auto utm32 = ProjectedCRS::create( + PropertyMap(), GeographicCRS::EPSG_4269, + Conversion::createUTM(PropertyMap(), 32, true), + CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); + auto boundCRS1 = BoundCRS::createFromTOWGS84( + utm31, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto boundCRS2 = BoundCRS::createFromTOWGS84( + utm32, std::vector{8, 9, 10, 11, 12, 13, 14}); + auto op = CoordinateOperationFactory::create()->createOperation(boundCRS1, + boundCRS2); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=utm +zone=31 +ellps=clrk80ign " + "+pm=paris +step +proj=push +v_3 +step +proj=cart " + "+ellps=clrk80ign +step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 " + "+rz=6 +s=7 +convention=position_vector +step +inv +proj=helmert " + "+x=8 +y=9 +z=10 +rx=11 +ry=12 +rz=13 +s=14 " + "+convention=position_vector +step +inv +proj=cart +ellps=GRS80 " + "+step +proj=pop +v_3 +step +proj=utm +zone=32 +ellps=GRS80"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_to_boundCRS_noop_for_TOWGS84) { + auto boundCRS1 = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto boundCRS2 = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4269, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto op = CoordinateOperationFactory::create()->createOperation(boundCRS1, + boundCRS2); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=grad +xy_out=rad +step +inv " + "+proj=longlat +ellps=clrk80ign +pm=paris +step +proj=push +v_3 " + "+step +proj=cart +ellps=clrk80ign +step +inv +proj=cart " + "+ellps=GRS80 +step +proj=pop +v_3 +step +proj=unitconvert " + "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_to_boundCRS_unralated_hub) { + auto boundCRS1 = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto boundCRS2 = BoundCRS::create( + GeographicCRS::EPSG_4269, GeographicCRS::EPSG_4979, + Transformation::createGeocentricTranslations( + PropertyMap(), GeographicCRS::EPSG_4269, GeographicCRS::EPSG_4979, + 1.0, 2.0, 3.0, std::vector())); + auto op = CoordinateOperationFactory::create()->createOperation(boundCRS1, + boundCRS2); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + CoordinateOperationFactory::create() + ->createOperation(boundCRS1->baseCRS(), boundCRS2->baseCRS()) + ->exportToPROJString(PROJStringFormatter::create().get())); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_of_projCRS_towgs84_to_boundCRS_of_projCRS_nadgrids) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=utm +zone=15 +datum=NAD83 +units=m +no_defs +ellps=GRS80 " + "+towgs84=0,0,0 +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto objDst = PROJStringParser().createFromPROJString( + "+proj=utm +zone=15 +datum=NAD27 +units=m +no_defs +ellps=clrk66 " + "+nadgrids=@conus,@alaska,@ntv2_0.gsb,@ntv1_can.dat +type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=utm +zone=15 +ellps=GRS80 +step " + "+inv +proj=hgridshift " + "+grids=@conus,@alaska,@ntv2_0.gsb,@ntv1_can.dat +step +proj=utm " + "+zone=15 +ellps=clrk66"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_with_basecrs_with_extent_to_geogCRS) { + + auto wkt = + "BOUNDCRS[\n" + " SOURCECRS[\n" + " PROJCRS[\"NAD83 / California zone 3 (ftUS)\",\n" + " BASEGEODCRS[\"NAD83\",\n" + " DATUM[\"North American Datum 1983\",\n" + " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]]],\n" + " CONVERSION[\"SPCS83 California zone 3 (US Survey " + "feet)\",\n" + " METHOD[\"Lambert Conic Conformal (2SP)\",\n" + " ID[\"EPSG\",9802]],\n" + " PARAMETER[\"Latitude of false origin\",36.5,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8821]],\n" + " PARAMETER[\"Longitude of false origin\",-120.5,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8822]],\n" + " PARAMETER[\"Latitude of 1st standard parallel\"," + " 38.4333333333333,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8823]],\n" + " PARAMETER[\"Latitude of 2nd standard parallel\"," + " 37.0666666666667,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8824]],\n" + " PARAMETER[\"Easting at false origin\",6561666.667,\n" + " LENGTHUNIT[\"US survey foot\"," + " 0.304800609601219],\n" + " ID[\"EPSG\",8826]],\n" + " PARAMETER[\"Northing at false origin\",1640416.667,\n" + " LENGTHUNIT[\"US survey foot\"," + " 0.304800609601219],\n" + " ID[\"EPSG\",8827]]],\n" + " CS[Cartesian,2],\n" + " AXIS[\"easting (X)\",east,\n" + " ORDER[1],\n" + " LENGTHUNIT[\"US survey foot\"," + " 0.304800609601219]],\n" + " AXIS[\"northing (Y)\",north,\n" + " ORDER[2],\n" + " LENGTHUNIT[\"US survey foot\"," + " 0.304800609601219]],\n" + " SCOPE[\"unknown\"],\n" + " AREA[\"USA - California - SPCS - 3\"],\n" + " BBOX[36.73,-123.02,38.71,-117.83],\n" + " ID[\"EPSG\",2227]]],\n" + " TARGETCRS[\n" + " GEODCRS[\"WGS 84\",\n" + " DATUM[\"World Geodetic System 1984\",\n" + " ELLIPSOID[\"WGS 84\",6378137,298.257223563,\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " CS[ellipsoidal,2],\n" + " AXIS[\"latitude\",north,\n" + " ORDER[1],\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " AXIS[\"longitude\",east,\n" + " ORDER[2],\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " ID[\"EPSG\",4326]]],\n" + " ABRIDGEDTRANSFORMATION[\"NAD83 to WGS 84 (1)\",\n" + " METHOD[\"Geocentric translations (geog2D domain)\",\n" + " ID[\"EPSG\",9603]],\n" + " PARAMETER[\"X-axis translation\",0,\n" + " ID[\"EPSG\",8605]],\n" + " PARAMETER[\"Y-axis translation\",0,\n" + " ID[\"EPSG\",8606]],\n" + " PARAMETER[\"Z-axis translation\",0,\n" + " ID[\"EPSG\",8607]],\n" + " SCOPE[\"unknown\"],\n" + " AREA[\"North America - Canada and USA (CONUS, Alaska " + "mainland)\"],\n" + " BBOX[23.81,-172.54,86.46,-47.74],\n" + " ID[\"EPSG\",1188]]]"; + auto obj = WKTParser().createFromWKT(wkt); + auto boundCRS = nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(boundCRS != nullptr); + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(boundCRS), GeographicCRS::EPSG_4326); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->nameStr(), "Inverse of SPCS83 California zone 3 (US Survey " + "feet) + NAD83 to WGS 84 (1)"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, ETRS89_3D_to_proj_string_with_geoidgrids_nadgrids) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + // ETRS89 3D + auto src = authFactory->createCoordinateReferenceSystem("4937"); + auto objDst = PROJStringParser().createFromPROJString( + "+proj=sterea +lat_0=52.15616055555555 +lon_0=5.38763888888889 " + "+k=0.9999079 +x_0=155000 +y_0=463000 +ellps=bessel " + "+nadgrids=rdtrans2008.gsb +geoidgrids=naptrans2008.gtx +units=m " + "+type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + src, NN_NO_CHECK(dst), ctxt); + ASSERT_EQ(list.size(), 2U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +inv +proj=vgridshift +grids=naptrans2008.gtx " + "+multiplier=1 " + "+step +inv +proj=hgridshift +grids=rdtrans2008.gsb " + "+step +proj=sterea +lat_0=52.1561605555556 " + "+lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 " + "+y_0=463000 +ellps=bessel"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, nadgrids_with_pm) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=tmerc +lat_0=39.66666666666666 +lon_0=1 +k=1 +x_0=200000 " + "+y_0=300000 +ellps=intl +nadgrids=foo.gsb +pm=lisbon " + "+units=m +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto dst = authFactory->createCoordinateReferenceSystem("4326"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(src), dst, ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +inv +proj=tmerc +lat_0=39.6666666666667 +lon_0=1 " + "+k=1 +x_0=200000 +y_0=300000 +ellps=intl +pm=lisbon " + // Check that there is no extra +step +proj=longlat +pm=lisbon + "+step +proj=hgridshift +grids=foo.gsb " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + // ETRS89 + dst = authFactory->createCoordinateReferenceSystem("4258"); + list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(src), dst, ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +inv +proj=tmerc +lat_0=39.6666666666667 +lon_0=1 " + "+k=1 +x_0=200000 +y_0=300000 +ellps=intl +pm=lisbon " + // Check that there is no extra +step +proj=longlat +pm=lisbon + "+step +proj=hgridshift +grids=foo.gsb " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + // From WKT BOUNDCRS + auto formatter = WKTFormatter::create(WKTFormatter::Convention::WKT2_2019); + auto src_wkt = src->exportToWKT(formatter.get()); + auto objFromWkt = WKTParser().createFromWKT(src_wkt); + auto crsFromWkt = nn_dynamic_pointer_cast(objFromWkt); + ASSERT_TRUE(crsFromWkt); + list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(crsFromWkt), dst, ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +inv +proj=tmerc +lat_0=39.6666666666667 +lon_0=1 " + "+k=1 +x_0=200000 +y_0=300000 +ellps=intl +pm=lisbon " + // Check that there is no extra +step +proj=longlat +pm=lisbon + "+step +proj=hgridshift +grids=foo.gsb " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, WGS84_G1762_to_compoundCRS_with_bound_vertCRS) { + auto authFactoryEPSG = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + // WGS 84 (G1762) 3D + auto src = authFactoryEPSG->createCoordinateReferenceSystem("7665"); + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +datum=NAD83 +geoidgrids=@foo.gtx +type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), std::string()); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + auto list = CoordinateOperationFactory::create()->createOperations( + src, NN_NO_CHECK(dst), ctxt); + ASSERT_GE(list.size(), 53U); + EXPECT_EQ(list[0]->nameStr(), + "Inverse of WGS 84 to WGS 84 (G1762) + " + "Inverse of unknown to WGS84 ellipsoidal height + " + "Inverse of NAD83 to WGS 84 (1) + " + "axis order change (2D)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +inv +proj=vgridshift +grids=@foo.gtx +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +static VerticalCRSNNPtr createVerticalCRS() { + PropertyMap propertiesVDatum; + propertiesVDatum.set(Identifier::CODESPACE_KEY, "EPSG") + .set(Identifier::CODE_KEY, 5101) + .set(IdentifiedObject::NAME_KEY, "Ordnance Datum Newlyn"); + auto vdatum = VerticalReferenceFrame::create(propertiesVDatum); + PropertyMap propertiesCRS; + propertiesCRS.set(Identifier::CODESPACE_KEY, "EPSG") + .set(Identifier::CODE_KEY, 5701) + .set(IdentifiedObject::NAME_KEY, "ODN height"); + return VerticalCRS::create( + propertiesCRS, vdatum, + VerticalCS::createGravityRelatedHeight(UnitOfMeasure::METRE)); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_geogCRS) { + + auto compound = CompoundCRS::create( + PropertyMap(), + std::vector{GeographicCRS::EPSG_4326, createVerticalCRS()}); + auto op = CoordinateOperationFactory::create()->createOperation( + compound, GeographicCRS::EPSG_4807); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + CoordinateOperationFactory::create() + ->createOperation(GeographicCRS::EPSG_4326, + GeographicCRS::EPSG_4807) + ->exportToPROJString(PROJStringFormatter::create().get())); +} + +// --------------------------------------------------------------------------- + +static BoundCRSNNPtr createBoundVerticalCRS() { + auto vertCRS = createVerticalCRS(); + auto transformation = + Transformation::createGravityRelatedHeightToGeographic3D( + PropertyMap(), vertCRS, GeographicCRS::EPSG_4979, nullptr, + "us_nga_egm08_25.tif", std::vector()); + return BoundCRS::create(vertCRS, GeographicCRS::EPSG_4979, transformation); +} + +// --------------------------------------------------------------------------- + +TEST(operation, transformation_height_to_PROJ_string) { + auto transf = createBoundVerticalCRS()->transformation(); + EXPECT_EQ(transf->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=us_nga_egm08_25.tif " + "+multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + auto grids = transf->gridsNeeded(DatabaseContext::create(), false); + ASSERT_EQ(grids.size(), 1U); + auto gridDesc = *(grids.begin()); + EXPECT_EQ(gridDesc.shortName, "us_nga_egm08_25.tif"); + EXPECT_TRUE(gridDesc.packageName.empty()); + EXPECT_EQ(gridDesc.url, "https://cdn.proj.org/us_nga_egm08_25.tif"); + if (gridDesc.available) { + EXPECT_TRUE(!gridDesc.fullName.empty()) << gridDesc.fullName; + EXPECT_TRUE(gridDesc.fullName.find(gridDesc.shortName) != + std::string::npos) + << gridDesc.fullName; + } else { + EXPECT_TRUE(gridDesc.fullName.empty()) << gridDesc.fullName; + } + EXPECT_EQ(gridDesc.directDownload, true); + EXPECT_EQ(gridDesc.openLicense, true); +} + +// --------------------------------------------------------------------------- + +TEST(operation, transformation_Geographic3D_to_GravityRelatedHeight_gtx) { + auto wkt = + "COORDINATEOPERATION[\"ETRS89 to NAP height (1)\",\n" + " VERSION[\"RDNAP-Nld 2008\"],\n" + " SOURCECRS[\n" + " GEOGCRS[\"ETRS89\",\n" + " DATUM[\"European Terrestrial Reference System 1989\",\n" + " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " CS[ellipsoidal,3],\n" + " AXIS[\"geodetic latitude (Lat)\",north,\n" + " ORDER[1],\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " AXIS[\"geodetic longitude (Lon)\",east,\n" + " ORDER[2],\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " AXIS[\"ellipsoidal height (h)\",up,\n" + " ORDER[3],\n" + " LENGTHUNIT[\"metre\",1]],\n" + " ID[\"EPSG\",4937]]],\n" + " TARGETCRS[\n" + " VERTCRS[\"NAP height\",\n" + " VDATUM[\"Normaal Amsterdams Peil\"],\n" + " CS[vertical,1],\n" + " AXIS[\"gravity-related height (H)\",up,\n" + " LENGTHUNIT[\"metre\",1]],\n" + " ID[\"EPSG\",5709]]],\n" + " METHOD[\"Geographic3D to GravityRelatedHeight (US .gtx)\",\n" + " ID[\"EPSG\",9665]],\n" + " PARAMETERFILE[\"Geoid (height correction) model " + "file\",\"naptrans2008.gtx\"],\n" + " OPERATIONACCURACY[0.01],\n" + " USAGE[\n" + " SCOPE[\"unknown\"],\n" + " AREA[\"Netherlands - onshore\"],\n" + " BBOX[50.75,3.2,53.7,7.22]],\n" + " ID[\"EPSG\",7001]]"; + ; + auto obj = WKTParser().createFromWKT(wkt); + auto transf = nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(transf != nullptr); + + // Check that we correctly inverse files in the case of + // "Geographic3D to GravityRelatedHeight (US .gtx)" + EXPECT_EQ(transf->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +inv +proj=vgridshift " + "+grids=naptrans2008.gtx +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, transformation_ntv2_to_PROJ_string) { + auto transformation = Transformation::createNTv2( + PropertyMap(), GeographicCRS::EPSG_4807, GeographicCRS::EPSG_4326, + "foo.gsb", std::vector()); + EXPECT_EQ( + transformation->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=grad +xy_out=rad +step " + "+proj=hgridshift +grids=foo.gsb +step +proj=unitconvert " + "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, transformation_VERTCON_to_PROJ_string) { + auto verticalCRS1 = createVerticalCRS(); + + auto verticalCRS2 = VerticalCRS::create( + PropertyMap(), VerticalReferenceFrame::create(PropertyMap()), + VerticalCS::createGravityRelatedHeight(UnitOfMeasure::METRE)); + + // Use of this type of transformation is a bit of non-sense here + // since it should normally be used with NGVD29 and NAVD88 for VerticalCRS, + // and NAD27/NAD83 as horizontal CRS... + auto vtransformation = Transformation::createVERTCON( + PropertyMap(), verticalCRS1, verticalCRS2, "bla.gtx", + std::vector()); + EXPECT_EQ(vtransformation->exportToPROJString( + PROJStringFormatter::create().get()), + "+proj=vgridshift +grids=bla.gtx +multiplier=0.001"); +} +// --------------------------------------------------------------------------- + +TEST(operation, transformation_NZLVD_to_PROJ_string) { + auto dbContext = DatabaseContext::create(); + auto factory = AuthorityFactory::create(dbContext, "EPSG"); + auto op = factory->createCoordinateOperation("7860", false); + EXPECT_EQ(op->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, dbContext) + .get()), + "+proj=vgridshift +grids=nz_linz_auckht1946-nzvd2016.tif " + "+multiplier=1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, transformation_BEV_AT_to_PROJ_string) { + auto dbContext = DatabaseContext::create(); + auto factory = AuthorityFactory::create(dbContext, "EPSG"); + auto op = factory->createCoordinateOperation("9275", false); + EXPECT_EQ(op->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, dbContext) + .get()), + "+proj=vgridshift +grids=at_bev_GV_Hoehengrid_V1.tif " + "+multiplier=1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, transformation_longitude_rotation_to_PROJ_string) { + + auto src = GeographicCRS::create( + PropertyMap(), GeodeticReferenceFrame::create( + PropertyMap(), Ellipsoid::WGS84, + optional(), PrimeMeridian::GREENWICH), + EllipsoidalCS::createLatitudeLongitude(UnitOfMeasure::DEGREE)); + auto dest = GeographicCRS::create( + PropertyMap(), GeodeticReferenceFrame::create( + PropertyMap(), Ellipsoid::WGS84, + optional(), PrimeMeridian::PARIS), + EllipsoidalCS::createLatitudeLongitude(UnitOfMeasure::DEGREE)); + auto transformation = Transformation::createLongitudeRotation( + PropertyMap(), src, dest, Angle(10)); + EXPECT_TRUE(transformation->validateParameters().empty()); + EXPECT_EQ( + transformation->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 +ellps=WGS84 +pm=10 +step +proj=unitconvert " + "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); + EXPECT_EQ(transformation->inverse()->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 +ellps=WGS84 +pm=-10 +step +proj=unitconvert " + "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, transformation_Geographic2D_offsets_to_PROJ_string) { + + auto transformation = Transformation::createGeographic2DOffsets( + PropertyMap(), GeographicCRS::EPSG_4326, GeographicCRS::EPSG_4326, + Angle(0.5), Angle(-1), {}); + EXPECT_TRUE(transformation->validateParameters().empty()); + + EXPECT_EQ( + transformation->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " + "+dlat=1800 +dlon=-3600 +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); + EXPECT_EQ(transformation->inverse()->exportToPROJString( + PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " + "+dlat=-1800 +dlon=3600 +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, transformation_Geographic3D_offsets_to_PROJ_string) { + + auto transformation = Transformation::createGeographic3DOffsets( + PropertyMap(), GeographicCRS::EPSG_4326, GeographicCRS::EPSG_4326, + Angle(0.5), Angle(-1), Length(2), {}); + EXPECT_TRUE(transformation->validateParameters().empty()); + + EXPECT_EQ( + transformation->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " + "+dlat=1800 +dlon=-3600 +dh=2 +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); + EXPECT_EQ(transformation->inverse()->exportToPROJString( + PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " + "+dlat=-1800 +dlon=3600 +dh=-2 +step +proj=unitconvert " + "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + transformation_Geographic2D_with_height_offsets_to_PROJ_string) { + + auto transformation = Transformation::createGeographic2DWithHeightOffsets( + PropertyMap(), + CompoundCRS::create(PropertyMap(), + {GeographicCRS::EPSG_4326, createVerticalCRS()}), + GeographicCRS::EPSG_4326, Angle(0.5), Angle(-1), Length(2), {}); + EXPECT_TRUE(transformation->validateParameters().empty()); + + EXPECT_EQ( + transformation->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " + "+dlat=1800 +dlon=-3600 +dh=2 +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); + EXPECT_EQ(transformation->inverse()->exportToPROJString( + PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=geogoffset " + "+dlat=-1800 +dlon=3600 +dh=-2 +step +proj=unitconvert " + "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, transformation_vertical_offset_to_PROJ_string) { + + auto transformation = Transformation::createVerticalOffset( + PropertyMap(), createVerticalCRS(), createVerticalCRS(), Length(1), {}); + EXPECT_TRUE(transformation->validateParameters().empty()); + + EXPECT_EQ( + transformation->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=geogoffset +dh=1"); + EXPECT_EQ(transformation->inverse()->exportToPROJString( + PROJStringFormatter::create().get()), + "+proj=geogoffset +dh=-1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_with_boundVerticalCRS_to_geogCRS) { + + auto compound = CompoundCRS::create( + PropertyMap(), std::vector{GeographicCRS::EPSG_4326, + createBoundVerticalCRS()}); + auto op = CoordinateOperationFactory::create()->createOperation( + compound, GeographicCRS::EPSG_4979); + 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 +proj=vgridshift " + "+grids=us_nga_egm08_25.tif +multiplier=1 +step +proj=unitconvert " + "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_with_boundGeogCRS_to_geogCRS) { + + auto geogCRS = GeographicCRS::create( + PropertyMap(), GeodeticReferenceFrame::create( + PropertyMap(), Ellipsoid::WGS84, + optional(), PrimeMeridian::GREENWICH), + EllipsoidalCS::createLatitudeLongitude(UnitOfMeasure::DEGREE)); + auto horizBoundCRS = BoundCRS::createFromTOWGS84( + geogCRS, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto compound = CompoundCRS::create( + PropertyMap(), + std::vector{horizBoundCRS, createVerticalCRS()}); + auto op = CoordinateOperationFactory::create()->createOperation( + compound, GeographicCRS::EPSG_4979); + 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 +proj=push +v_3 " + "+step +proj=cart +ellps=WGS84 " + "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 " + "+convention=position_vector " + "+step +inv +proj=cart +ellps=WGS84 " + "+step +proj=pop +v_3 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_with_boundGeogCRS_and_boundVerticalCRS_to_geogCRS) { + + auto horizBoundCRS = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4807, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto compound = CompoundCRS::create( + PropertyMap(), + std::vector{horizBoundCRS, createBoundVerticalCRS()}); + auto op = CoordinateOperationFactory::create()->createOperation( + compound, GeographicCRS::EPSG_4979); + ASSERT_TRUE(op != nullptr); + // Not completely sure the order of horizontal and vertical operations + // makes sense + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=grad +xy_out=rad " + "+step +inv +proj=longlat +ellps=clrk80ign +pm=paris " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=clrk80ign " + "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 " + "+convention=position_vector " + "+step +inv +proj=cart +ellps=WGS84 " + "+step +proj=pop +v_3 " + "+step +proj=vgridshift +grids=us_nga_egm08_25.tif +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + auto grids = op->gridsNeeded(DatabaseContext::create(), false); + EXPECT_EQ(grids.size(), 1U); + + auto opInverse = CoordinateOperationFactory::create()->createOperation( + GeographicCRS::EPSG_4979, compound); + ASSERT_TRUE(opInverse != nullptr); + EXPECT_TRUE(opInverse->inverse()->isEquivalentTo(op.get())); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_with_boundProjCRS_and_boundVerticalCRS_to_geogCRS) { + + auto horizBoundCRS = BoundCRS::createFromTOWGS84( + ProjectedCRS::create( + PropertyMap(), GeographicCRS::EPSG_4807, + Conversion::createUTM(PropertyMap(), 31, true), + CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)), + std::vector{1, 2, 3, 4, 5, 6, 7}); + auto compound = CompoundCRS::create( + PropertyMap(), + std::vector{horizBoundCRS, createBoundVerticalCRS()}); + auto op = CoordinateOperationFactory::create()->createOperation( + compound, GeographicCRS::EPSG_4979); + ASSERT_TRUE(op != nullptr); + // Not completely sure the order of horizontal and vertical operations + // makes sense + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +inv +proj=utm +zone=31 +ellps=clrk80ign +pm=paris " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=clrk80ign " + "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 +ry=5 +rz=6 +s=7 " + "+convention=position_vector " + "+step +inv +proj=cart +ellps=WGS84 " + "+step +proj=pop +v_3 " + "+step +proj=vgridshift +grids=us_nga_egm08_25.tif +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + auto opInverse = CoordinateOperationFactory::create()->createOperation( + GeographicCRS::EPSG_4979, compound); + ASSERT_TRUE(opInverse != nullptr); + EXPECT_TRUE(opInverse->inverse()->isEquivalentTo(op.get())); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + compoundCRS_with_boundVerticalCRS_from_geoidgrids_with_m_to_geogCRS) { + + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=longlat +datum=WGS84 +geoidgrids=@foo.gtx +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto op = CoordinateOperationFactory::create()->createOperation( + NN_NO_CHECK(src), GeographicCRS::EPSG_4979); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->nameStr(), "axis order change (2D) + " + "unknown to WGS84 ellipsoidal height"); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=@foo.gtx +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + compoundCRS_with_boundVerticalCRS_from_geoidgrids_with_ftus_to_geogCRS) { + + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=longlat +datum=WGS84 +geoidgrids=@foo.gtx +vunits=us-ft " + "+type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto op = CoordinateOperationFactory::create()->createOperation( + NN_NO_CHECK(src), GeographicCRS::EPSG_4979); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->nameStr(), "axis order change (2D) + " + "Transformation from unknown to unknown + " + "unknown to WGS84 ellipsoidal height"); + EXPECT_EQ( + op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=deg +z_in=us-ft +xy_out=rad +z_out=m " + "+step +proj=vgridshift +grids=@foo.gtx +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + compoundCRS_with_boundProjCRS_with_ftus_and_boundVerticalCRS_to_geogCRS) { + + auto wkt = + "COMPD_CS[\"NAD_1983_StatePlane_Alabama_West_FIPS_0102_Feet + " + "NAVD88 height - Geoid12B (US Feet)\",\n" + " PROJCS[\"NAD_1983_StatePlane_Alabama_West_FIPS_0102_Feet\",\n" + " GEOGCS[\"NAD83\",\n" + " DATUM[\"North_American_Datum_1983\",\n" + " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" + " AUTHORITY[\"EPSG\",\"7019\"]],\n" + " TOWGS84[0,0,0,0,0,0,0],\n" + " AUTHORITY[\"EPSG\",\"6269\"]],\n" + " PRIMEM[\"Greenwich\",0],\n" + " UNIT[\"Degree\",0.0174532925199433]],\n" + " PROJECTION[\"Transverse_Mercator\"],\n" + " PARAMETER[\"latitude_of_origin\",30],\n" + " PARAMETER[\"central_meridian\",-87.5],\n" + " PARAMETER[\"scale_factor\",0.999933333333333],\n" + " PARAMETER[\"false_easting\",1968500],\n" + " PARAMETER[\"false_northing\",0],\n" + " UNIT[\"US survey foot\",0.304800609601219,\n" + " AUTHORITY[\"EPSG\",\"9003\"]],\n" + " AXIS[\"Easting\",EAST],\n" + " AXIS[\"Northing\",NORTH],\n" + " AUTHORITY[\"ESRI\",\"102630\"]],\n" + " VERT_CS[\"NAVD88 height (ftUS)\",\n" + " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" + " EXTENSION[\"PROJ4_GRIDS\",\"foo.gtx\"],\n" + " AUTHORITY[\"EPSG\",\"5103\"]],\n" + " UNIT[\"US survey foot\",0.304800609601219,\n" + " AUTHORITY[\"EPSG\",\"9003\"]],\n" + " AXIS[\"Gravity-related height\",UP],\n" + " AUTHORITY[\"EPSG\",\"6360\"]]]"; + + auto obj = WKTParser().createFromWKT(wkt); + auto crs = nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(crs != nullptr); + auto op = CoordinateOperationFactory::create()->createOperation( + NN_NO_CHECK(crs), GeographicCRS::EPSG_4979); + ASSERT_TRUE(op != nullptr); + + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=us-ft +xy_out=m " + "+step +inv +proj=tmerc +lat_0=30 +lon_0=-87.5 " + "+k=0.999933333333333 +x_0=600000 +y_0=0 +ellps=GRS80 " + "+step +proj=unitconvert +z_in=us-ft +z_out=m " + "+step +proj=vgridshift +grids=foo.gtx +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + compoundCRS_with_boundVerticalCRS_from_grids_to_geogCRS_with_ftus_ctxt) { + + auto dbContext = DatabaseContext::create(); + + const char *wktSrc = + "COMPD_CS[\"NAD83 + NAVD88 height - Geoid12B (Meters)\",\n" + " GEOGCS[\"NAD83\",\n" + " DATUM[\"North_American_Datum_1983\",\n" + " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" + " AUTHORITY[\"EPSG\",\"7019\"]],\n" + " AUTHORITY[\"EPSG\",\"6269\"]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " AUTHORITY[\"EPSG\",\"8901\"]],\n" + " UNIT[\"degree\",0.0174532925199433,\n" + " AUTHORITY[\"EPSG\",\"9122\"]],\n" + " AUTHORITY[\"EPSG\",\"4269\"]],\n" + " VERT_CS[\"NAVD88 height - Geoid12B (Meters)\",\n" + " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" + " EXTENSION[\"PROJ4_GRIDS\",\"@foo.gtx\"],\n" + " AUTHORITY[\"EPSG\",\"5103\"]],\n" + " UNIT[\"metre\",1.0,\n" + " AUTHORITY[\"EPSG\",\"9001\"]],\n" + " AXIS[\"Gravity-related height\",UP],\n" + " AUTHORITY[\"EPSG\",\"5703\"]]]"; + auto objSrc = + WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktSrc); + auto srcCRS = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(srcCRS != nullptr); + + const char *wktDst = + "COMPD_CS[\"NAD83 + Ellipsoid (US Feet)\",\n" + " GEOGCS[\"NAD83\",\n" + " DATUM[\"North_American_Datum_1983\",\n" + " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" + " AUTHORITY[\"EPSG\",\"7019\"]],\n" + " AUTHORITY[\"EPSG\",\"6269\"]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " AUTHORITY[\"EPSG\",\"8901\"]],\n" + " UNIT[\"degree\",0.0174532925199433,\n" + " AUTHORITY[\"EPSG\",\"9122\"]],\n" + " AUTHORITY[\"EPSG\",\"4269\"]],\n" + " VERT_CS[\"Ellipsoid (US Feet)\",\n" + " VERT_DATUM[\"Ellipsoid\",2002],\n" + " UNIT[\"US survey foot\",0.304800609601219,\n" + " AUTHORITY[\"EPSG\",\"9003\"]],\n" + " AXIS[\"Up\",UP]]]"; + auto objDst = + WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktDst); + auto dstCRS = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dstCRS != nullptr); + + auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcCRS), NN_NO_CHECK(dstCRS), ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=@foo.gtx +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +z_in=m " + "+xy_out=deg +z_out=us-ft " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + compoundCRS_with_boundGeogCRS_boundVerticalCRS_from_grids_to_boundGeogCRS_with_ftus_ctxt) { + + // Variant of above but with TOWGS84 in source & target CRS + + auto dbContext = DatabaseContext::create(); + + const char *wktSrc = + "COMPD_CS[\"NAD83 + NAVD88 height - Geoid12B (Meters)\",\n" + " GEOGCS[\"NAD83\",\n" + " DATUM[\"North_American_Datum_1983\",\n" + " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" + " AUTHORITY[\"EPSG\",\"7019\"]],\n" + " TOWGS84[0,0,0,0,0,0,0],\n" + " AUTHORITY[\"EPSG\",\"6269\"]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " AUTHORITY[\"EPSG\",\"8901\"]],\n" + " UNIT[\"degree\",0.0174532925199433,\n" + " AUTHORITY[\"EPSG\",\"9122\"]],\n" + " AUTHORITY[\"EPSG\",\"4269\"]],\n" + " VERT_CS[\"NAVD88 height - Geoid12B (Meters)\",\n" + " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" + " EXTENSION[\"PROJ4_GRIDS\",\"@foo.gtx\"],\n" + " AUTHORITY[\"EPSG\",\"5103\"]],\n" + " UNIT[\"metre\",1.0,\n" + " AUTHORITY[\"EPSG\",\"9001\"]],\n" + " AXIS[\"Gravity-related height\",UP],\n" + " AUTHORITY[\"EPSG\",\"5703\"]]]"; + auto objSrc = + WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktSrc); + auto srcCRS = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(srcCRS != nullptr); + + const char *wktDst = + "COMPD_CS[\"NAD83 + Ellipsoid (US Feet)\",\n" + " GEOGCS[\"NAD83\",\n" + " DATUM[\"North_American_Datum_1983\",\n" + " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" + " AUTHORITY[\"EPSG\",\"7019\"]],\n" + " TOWGS84[0,0,0,0,0,0,0],\n" + " AUTHORITY[\"EPSG\",\"6269\"]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " AUTHORITY[\"EPSG\",\"8901\"]],\n" + " UNIT[\"degree\",0.0174532925199433,\n" + " AUTHORITY[\"EPSG\",\"9122\"]],\n" + " AUTHORITY[\"EPSG\",\"4269\"]],\n" + " VERT_CS[\"Ellipsoid (US Feet)\",\n" + " VERT_DATUM[\"Ellipsoid\",2002],\n" + " UNIT[\"US survey foot\",0.304800609601219,\n" + " AUTHORITY[\"EPSG\",\"9003\"]],\n" + " AXIS[\"Up\",UP]]]"; + auto objDst = + WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktDst); + auto dstCRS = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dstCRS != nullptr); + + auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcCRS), NN_NO_CHECK(dstCRS), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=@foo.gtx +multiplier=1 " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=rad +z_in=m " + "+xy_out=deg +z_out=us-ft"); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + compoundCRS_with_boundVerticalCRS_from_grids_to_boundGeogCRS_with_ftus_ctxt) { + + // Variant of above but with TOWGS84 in target CRS only + + auto dbContext = DatabaseContext::create(); + + const char *wktSrc = + "COMPD_CS[\"NAD83 + NAVD88 height - Geoid12B (Meters)\",\n" + " GEOGCS[\"NAD83\",\n" + " DATUM[\"North_American_Datum_1983\",\n" + " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" + " AUTHORITY[\"EPSG\",\"7019\"]],\n" + " AUTHORITY[\"EPSG\",\"6269\"]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " AUTHORITY[\"EPSG\",\"8901\"]],\n" + " UNIT[\"degree\",0.0174532925199433,\n" + " AUTHORITY[\"EPSG\",\"9122\"]],\n" + " AUTHORITY[\"EPSG\",\"4269\"]],\n" + " VERT_CS[\"NAVD88 height - Geoid12B (Meters)\",\n" + " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" + " EXTENSION[\"PROJ4_GRIDS\",\"@foo.gtx\"],\n" + " AUTHORITY[\"EPSG\",\"5103\"]],\n" + " UNIT[\"metre\",1.0,\n" + " AUTHORITY[\"EPSG\",\"9001\"]],\n" + " AXIS[\"Gravity-related height\",UP],\n" + " AUTHORITY[\"EPSG\",\"5703\"]]]"; + auto objSrc = + WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktSrc); + auto srcCRS = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(srcCRS != nullptr); + + const char *wktDst = + "COMPD_CS[\"NAD83 + Ellipsoid (US Feet)\",\n" + " GEOGCS[\"NAD83\",\n" + " DATUM[\"North_American_Datum_1983\",\n" + " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" + " AUTHORITY[\"EPSG\",\"7019\"]],\n" + " TOWGS84[0,0,0,0,0,0,0],\n" + " AUTHORITY[\"EPSG\",\"6269\"]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " AUTHORITY[\"EPSG\",\"8901\"]],\n" + " UNIT[\"degree\",0.0174532925199433,\n" + " AUTHORITY[\"EPSG\",\"9122\"]],\n" + " AUTHORITY[\"EPSG\",\"4269\"]],\n" + " VERT_CS[\"Ellipsoid (US Feet)\",\n" + " VERT_DATUM[\"Ellipsoid\",2002],\n" + " UNIT[\"US survey foot\",0.304800609601219,\n" + " AUTHORITY[\"EPSG\",\"9003\"]],\n" + " AXIS[\"Up\",UP]]]"; + auto objDst = + WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktDst); + auto dstCRS = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dstCRS != nullptr); + + auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcCRS), NN_NO_CHECK(dstCRS), ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=@foo.gtx +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +z_in=m " + "+xy_out=deg +z_out=us-ft " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + compoundCRS_with_boundGeogCRS_and_geoid_to_geodCRS_NAD2011_ctxt) { + + auto dbContext = DatabaseContext::create(); + + const char *wktSrc = + "COMPD_CS[\"NAD83 / California zone 5 (ftUS) + " + "NAVD88 height - Geoid12B (ftUS)\"," + " PROJCS[\"NAD83 / California zone 5 (ftUS)\"," + " GEOGCS[\"NAD83\"," + " DATUM[\"North_American_Datum_1983\"," + " SPHEROID[\"GRS 1980\",6378137,298.257222101," + " AUTHORITY[\"EPSG\",\"7019\"]]," + " TOWGS84[0,0,0,0,0,0,0]," + " AUTHORITY[\"EPSG\",\"6269\"]]," + " PRIMEM[\"Greenwich\",0," + " AUTHORITY[\"EPSG\",\"8901\"]]," + " UNIT[\"degree\",0.0174532925199433," + " AUTHORITY[\"EPSG\",\"9122\"]]," + " AUTHORITY[\"EPSG\",\"4269\"]]," + " PROJECTION[\"Lambert_Conformal_Conic_2SP\"]," + " PARAMETER[\"standard_parallel_1\",35.46666666666667]," + " PARAMETER[\"standard_parallel_2\",34.03333333333333]," + " PARAMETER[\"latitude_of_origin\",33.5]," + " PARAMETER[\"central_meridian\",-118]," + " PARAMETER[\"false_easting\",6561666.667]," + " PARAMETER[\"false_northing\",1640416.667]," + " UNIT[\"US survey foot\",0.3048006096012192," + " AUTHORITY[\"EPSG\",\"9003\"]]," + " AXIS[\"X\",EAST]," + " AXIS[\"Y\",NORTH]," + " AUTHORITY[\"EPSG\",\"2229\"]]," + "VERT_CS[\"NAVD88 height - Geoid12B (ftUS)\"," + " VERT_DATUM[\"North American Vertical Datum 1988\",2005," + " AUTHORITY[\"EPSG\",\"5103\"]]," + " UNIT[\"US survey foot\",0.3048006096012192," + " AUTHORITY[\"EPSG\",\"9003\"]]," + " AXIS[\"Gravity-related height\",UP]," + " AUTHORITY[\"EPSG\",\"6360\"]]]"; + auto objSrc = + WKTParser().attachDatabaseContext(dbContext).createFromWKT(wktSrc); + auto srcCRS = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(srcCRS != nullptr); + + auto authFactoryEPSG = AuthorityFactory::create(dbContext, "EPSG"); + // NAD83(2011) geocentric + auto dstCRS = authFactoryEPSG->createCoordinateReferenceSystem("6317"); + + auto authFactory = AuthorityFactory::create(dbContext, std::string()); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcCRS), dstCRS, ctxt); + bool found = false; + for (const auto &op : list) { + if (op->nameStr() == + "Inverse of unnamed + " + "Transformation from NAD83 to WGS84 + " + "Ballpark geographic offset from WGS 84 to NAD83(2011) + " + "Transformation from NAVD88 height (ftUS) to NAVD88 height + " + "Inverse of NAD83(2011) to NAVD88 height (1) + " + "Conversion from NAD83(2011) (geog3D) to NAD83(2011) " + "(geocentric)") { + found = true; + EXPECT_EQ( + op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=us-ft +xy_out=m " + "+step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 " + "+lat_1=35.4666666666667 +lat_2=34.0333333333333 " + "+x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 " + "+step +proj=unitconvert +z_in=us-ft +z_out=m " + "+step +proj=vgridshift +grids=us_noaa_g2012bu0.tif " + "+multiplier=1 " + "+step +proj=cart +ellps=GRS80"); + } + } + EXPECT_TRUE(found); + if (!found) { + for (const auto &op : list) { + std::cerr << op->nameStr() << std::endl; + } + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, geocent_to_compoundCRS) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=geocent +datum=WGS84 +units=m +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " + "+type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 +step +inv " + "+proj=vgridshift +grids=@foo.gtx +multiplier=1 +step +inv " + "+proj=hgridshift +grids=@foo.gsb +step +proj=unitconvert " + "+xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, geocent_to_compoundCRS_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + // WGS84 geocentric + auto src = authFactory->createCoordinateReferenceSystem("4978"); + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " + "+type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + src, NN_CHECK_ASSERT(dst), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 +step +inv " + "+proj=vgridshift +grids=@foo.gtx +multiplier=1 +step +inv " + "+proj=hgridshift +grids=@foo.gsb +step +proj=unitconvert " + "+xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_compoundCRS) { + auto compound1 = CompoundCRS::create( + PropertyMap(), + std::vector{createUTM31_WGS84(), createVerticalCRS()}); + auto compound2 = CompoundCRS::create( + PropertyMap(), + std::vector{createUTM32_WGS84(), createVerticalCRS()}); + auto op = CoordinateOperationFactory::create()->createOperation(compound1, + compound2); + ASSERT_TRUE(op != nullptr); + auto opRef = CoordinateOperationFactory::create()->createOperation( + createUTM31_WGS84(), createUTM32_WGS84()); + ASSERT_TRUE(opRef != nullptr); + EXPECT_TRUE(op->isEquivalentTo(opRef.get())); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_compoundCRS_with_vertical_transform) { + auto verticalCRS1 = createVerticalCRS(); + + auto verticalCRS2 = VerticalCRS::create( + PropertyMap(), VerticalReferenceFrame::create(PropertyMap()), + VerticalCS::createGravityRelatedHeight(UnitOfMeasure::METRE)); + + // Use of this type of transformation is a bit of non-sense here + // since it should normally be used with NGVD29 and NAVD88 for VerticalCRS, + // and NAD27/NAD83 as horizontal CRS... + auto vtransformation = Transformation::createVERTCON( + PropertyMap(), verticalCRS1, verticalCRS2, "bla.gtx", + std::vector()); + + auto compound1 = CompoundCRS::create( + PropertyMap(), + std::vector{ + ProjectedCRS::create( + PropertyMap(), GeographicCRS::EPSG_4326, + Conversion::createTransverseMercator(PropertyMap(), Angle(1), + Angle(2), Scale(3), + Length(4), Length(5)), + CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)), + BoundCRS::create(verticalCRS1, verticalCRS2, vtransformation)}); + auto compound2 = CompoundCRS::create( + PropertyMap(), + std::vector{createUTM32_WGS84(), verticalCRS2}); + + auto op = CoordinateOperationFactory::create()->createOperation(compound1, + compound2); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=tmerc +lat_0=1 +lon_0=2 +k=3 " + "+x_0=4 +y_0=5 +ellps=WGS84 +step " + "+proj=vgridshift +grids=bla.gtx +multiplier=0.001 +step " + "+proj=utm +zone=32 " + "+ellps=WGS84"); + { + auto formatter = PROJStringFormatter::create(); + formatter->setUseApproxTMerc(true); + EXPECT_EQ( + op->exportToPROJString(formatter.get()), + "+proj=pipeline +step +inv +proj=tmerc +approx +lat_0=1 +lon_0=2 " + "+k=3 +x_0=4 +y_0=5 +ellps=WGS84 +step " + "+proj=vgridshift +grids=bla.gtx +multiplier=0.001 +step " + "+proj=utm +approx +zone=32 " + "+ellps=WGS84"); + } + { + auto formatter = PROJStringFormatter::create(); + formatter->setUseApproxTMerc(true); + EXPECT_EQ( + op->inverse()->exportToPROJString(formatter.get()), + "+proj=pipeline +step +inv +proj=utm +approx +zone=32 +ellps=WGS84 " + "+step +inv +proj=vgridshift +grids=bla.gtx " + "+multiplier=0.001 +step +proj=tmerc +approx +lat_0=1 +lon_0=2 " + "+k=3 +x_0=4 +y_0=5 +ellps=WGS84"); + } + + auto opInverse = CoordinateOperationFactory::create()->createOperation( + compound2, compound1); + ASSERT_TRUE(opInverse != nullptr); + { + auto formatter = PROJStringFormatter::create(); + auto formatter2 = PROJStringFormatter::create(); + EXPECT_EQ(opInverse->inverse()->exportToPROJString(formatter.get()), + op->exportToPROJString(formatter2.get())); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_compoundCRS_with_bound_crs_in_horiz_and_vert) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " + "+type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS80 +nadgrids=@bar.gsb +geoidgrids=@bar.gtx " + "+type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + 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=@foo.gsb " + "+step +proj=vgridshift +grids=@foo.gtx +multiplier=1 " + "+step +inv +proj=vgridshift +grids=@bar.gtx +multiplier=1 " + "+step +inv +proj=hgridshift +grids=@bar.gsb " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + compoundCRS_to_compoundCRS_with_bound_crs_in_horiz_and_vert_same_geoidgrids) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " + "+type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS80 +nadgrids=@bar.gsb +geoidgrids=@foo.gtx " + "+type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + 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=@foo.gsb " + "+step +inv +proj=hgridshift +grids=@bar.gsb " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + compoundCRS_to_compoundCRS_with_bound_crs_in_horiz_and_vert_same_geoidgrids_different_vunits) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " + "+type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS80 +nadgrids=@bar.gsb +geoidgrids=@foo.gtx " + "+vunits=us-ft +type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + 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=@foo.gsb " + "+step +proj=unitconvert +z_in=m +z_out=us-ft " + "+step +inv +proj=hgridshift +grids=@bar.gsb " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + compoundCRS_to_compoundCRS_with_bound_crs_in_horiz_and_vert_same_nadgrids_same_geoidgrids) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " + "+type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS80 +nadgrids=@foo.gsb +geoidgrids=@foo.gtx " + "+type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=noop"); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + compoundCRS_to_compoundCRS_with_bound_crs_in_horiz_and_vert_same_towgs84_same_geoidgrids) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS67 +towgs84=0,0,0 +geoidgrids=@foo.gtx " + "+type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS80 +towgs84=0,0,0 +geoidgrids=@foo.gtx " + "+type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=GRS67 " + "+step +inv +proj=cart +ellps=GRS80 " + "+step +proj=pop +v_3 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + compoundCRS_to_compoundCRS_with_bound_crs_in_horiz_and_vert_WKT1_same_geoidgrids_context) { + auto objSrc = WKTParser().createFromWKT( + "COMPD_CS[\"NAD83 / Alabama West + NAVD88 height - Geoid12B " + "(Meters)\",\n" + " PROJCS[\"NAD83 / Alabama West\",\n" + " GEOGCS[\"NAD83\",\n" + " DATUM[\"North_American_Datum_1983\",\n" + " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" + " AUTHORITY[\"EPSG\",\"7019\"]],\n" + " TOWGS84[0,0,0,0,0,0,0],\n" + " AUTHORITY[\"EPSG\",\"6269\"]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " AUTHORITY[\"EPSG\",\"8901\"]],\n" + " UNIT[\"degree\",0.0174532925199433,\n" + " AUTHORITY[\"EPSG\",\"9122\"]],\n" + " AUTHORITY[\"EPSG\",\"4269\"]],\n" + " PROJECTION[\"Transverse_Mercator\"],\n" + " PARAMETER[\"latitude_of_origin\",30],\n" + " PARAMETER[\"central_meridian\",-87.5],\n" + " PARAMETER[\"scale_factor\",0.999933333],\n" + " PARAMETER[\"false_easting\",600000],\n" + " PARAMETER[\"false_northing\",0],\n" + " UNIT[\"metre\",1,\n" + " AUTHORITY[\"EPSG\",\"9001\"]],\n" + " AXIS[\"X\",EAST],\n" + " AXIS[\"Y\",NORTH],\n" + " AUTHORITY[\"EPSG\",\"26930\"]],\n" + " VERT_CS[\"NAVD88 height\",\n" + " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" + " " + "EXTENSION[\"PROJ4_GRIDS\",\"g2012a_alaska.gtx,g2012a_hawaii.gtx," + "g2012a_conus.gtx\"],\n" + " AUTHORITY[\"EPSG\",\"5103\"]],\n" + " UNIT[\"metre\",1,\n" + " AUTHORITY[\"EPSG\",\"9001\"]],\n" + " AXIS[\"Gravity-related height\",UP],\n" + " AUTHORITY[\"EPSG\",\"5703\"]]]"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto objDst = WKTParser().createFromWKT( + "COMPD_CS[\"NAD_1983_StatePlane_Alabama_West_FIPS_0102_Feet + NAVD88 " + "height - Geoid12B (US Feet)\",\n" + " PROJCS[\"NAD_1983_StatePlane_Alabama_West_FIPS_0102_Feet\",\n" + " GEOGCS[\"NAD83\",\n" + " DATUM[\"North_American_Datum_1983\",\n" + " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" + " AUTHORITY[\"EPSG\",\"7019\"]],\n" + " TOWGS84[0,0,0,0,0,0,0],\n" + " AUTHORITY[\"EPSG\",\"6269\"]],\n" + " PRIMEM[\"Greenwich\",0],\n" + " UNIT[\"Degree\",0.0174532925199433]],\n" + " PROJECTION[\"Transverse_Mercator\"],\n" + " PARAMETER[\"latitude_of_origin\",30],\n" + " PARAMETER[\"central_meridian\",-87.5],\n" + " PARAMETER[\"scale_factor\",0.999933333333333],\n" + " PARAMETER[\"false_easting\",1968500],\n" + " PARAMETER[\"false_northing\",0],\n" + " UNIT[\"US survey foot\",0.304800609601219,\n" + " AUTHORITY[\"EPSG\",\"9003\"]],\n" + " AXIS[\"Easting\",EAST],\n" + " AXIS[\"Northing\",NORTH],\n" + " AUTHORITY[\"ESRI\",\"102630\"]],\n" + " VERT_CS[\"NAVD88 height (ftUS)\",\n" + " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" + " " + "EXTENSION[\"PROJ4_GRIDS\",\"g2012a_alaska.gtx,g2012a_hawaii.gtx," + "g2012a_conus.gtx\"],\n" + " AUTHORITY[\"EPSG\",\"5103\"]],\n" + " UNIT[\"US survey foot\",0.304800609601219,\n" + " AUTHORITY[\"EPSG\",\"9003\"]],\n" + " AXIS[\"Gravity-related height\",UP],\n" + " AUTHORITY[\"EPSG\",\"6360\"]]]"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + + auto dbContext = DatabaseContext::create(); + auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), + "Inverse of unnamed + " + "Transformation from NAD83 to WGS84 + " + "NAVD88 height to NAVD88 height (ftUS) + " + "Inverse of Transformation from NAD83 to WGS84 + " + "unnamed"); + auto grids = list[0]->gridsNeeded(dbContext, false); + EXPECT_TRUE(grids.empty()); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +inv +proj=tmerc +lat_0=30 +lon_0=-87.5 +k=0.999933333 " + "+x_0=600000 +y_0=0 +ellps=GRS80 " + "+step +proj=unitconvert +z_in=m +z_out=us-ft " + "+step +proj=tmerc +lat_0=30 +lon_0=-87.5 +k=0.999933333333333 " + "+x_0=600000 +y_0=0 +ellps=GRS80 " + "+step +proj=unitconvert +xy_in=m +xy_out=us-ft"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_compoundCRS_issue_2232) { + auto objSrc = WKTParser().createFromWKT( + "COMPD_CS[\"NAD83 / Alabama West + NAVD88 height - Geoid12B " + "(Meters)\",\n" + " PROJCS[\"NAD83 / Alabama West\",\n" + " GEOGCS[\"NAD83\",\n" + " DATUM[\"North_American_Datum_1983\",\n" + " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" + " AUTHORITY[\"EPSG\",\"7019\"]],\n" + " TOWGS84[0,0,0,0,0,0,0],\n" + " AUTHORITY[\"EPSG\",\"6269\"]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " AUTHORITY[\"EPSG\",\"8901\"]],\n" + " UNIT[\"degree\",0.0174532925199433,\n" + " AUTHORITY[\"EPSG\",\"9122\"]],\n" + " AUTHORITY[\"EPSG\",\"4269\"]],\n" + " PROJECTION[\"Transverse_Mercator\"],\n" + " PARAMETER[\"latitude_of_origin\",30],\n" + " PARAMETER[\"central_meridian\",-87.5],\n" + " PARAMETER[\"scale_factor\",0.999933333],\n" + " PARAMETER[\"false_easting\",600000],\n" + " PARAMETER[\"false_northing\",0],\n" + " UNIT[\"metre\",1,\n" + " AUTHORITY[\"EPSG\",\"9001\"]],\n" + " AXIS[\"X\",EAST],\n" + " AXIS[\"Y\",NORTH],\n" + " AUTHORITY[\"EPSG\",\"26930\"]],\n" + " VERT_CS[\"NAVD88 height - Geoid12B (Meters)\",\n" + " VERT_DATUM[\"North American Vertical Datum 1988\",2005,\n" + " EXTENSION[\"PROJ4_GRIDS\",\"foo.gtx\"],\n" + " AUTHORITY[\"EPSG\",\"5103\"]],\n" + " UNIT[\"metre\",1.0,\n" + " AUTHORITY[\"EPSG\",\"9001\"]],\n" + " AXIS[\"Gravity-related height\",UP],\n" + " AUTHORITY[\"EPSG\",\"5703\"]]]"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDst = WKTParser().createFromWKT( + "COMPD_CS[\"NAD83 + some CRS (US Feet)\",\n" + " GEOGCS[\"NAD83\",\n" + " DATUM[\"North_American_Datum_1983\",\n" + " SPHEROID[\"GRS 1980\",6378137,298.257222101,\n" + " AUTHORITY[\"EPSG\",\"7019\"]],\n" + " TOWGS84[0,0,0,0,0,0,0],\n" + " AUTHORITY[\"EPSG\",\"6269\"]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " AUTHORITY[\"EPSG\",\"8901\"]],\n" + " UNIT[\"degree\",0.0174532925199433,\n" + " AUTHORITY[\"EPSG\",\"9122\"]],\n" + " AUTHORITY[\"EPSG\",\"4269\"]],\n" + " VERT_CS[\"some CRS (US Feet)\",\n" + " VERT_DATUM[\"some datum\",2005],\n" + " UNIT[\"US survey foot\",0.3048006096012192,\n" + " AUTHORITY[\"EPSG\",\"9003\"]],\n" + " AXIS[\"Up\",UP]]]"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + + auto dbContext = DatabaseContext::create(); + auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + + auto list = CoordinateOperationFactory::create()->createOperations( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst), ctxt); + EXPECT_GE(list.size(), 1U); + + auto list2 = CoordinateOperationFactory::create()->createOperations( + NN_CHECK_ASSERT(dst), NN_CHECK_ASSERT(src), ctxt); + EXPECT_EQ(list2.size(), list.size()); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_compoundCRS_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + // NAD27 + NGVD29 height (ftUS) + authFactory->createCoordinateReferenceSystem("7406"), + // NAD83(NSRS2007) + NAVD88 height + authFactory->createCoordinateReferenceSystem("5500"), ctxt); + // 152 or 155 depending if the VERTCON grids are there + ASSERT_GE(list.size(), 152U); + EXPECT_FALSE(list[0]->hasBallparkTransformation()); + EXPECT_EQ(list[0]->nameStr(), "NGVD29 height (ftUS) to NAVD88 height (3) + " + "NAD27 to WGS 84 (79) + Inverse of " + "NAD83(NSRS2007) to WGS 84 (1)"); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +z_in=us-ft +xy_out=rad +z_out=m " + "+step +proj=vgridshift +grids=us_noaa_vertcone.tif +multiplier=1 " + "+step +proj=hgridshift +grids=us_noaa_conus.tif +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " + "+order=2,1"); + { + // Test that we can round-trip this through WKT and still get the same + // PROJ string. + auto wkt = list[0]->exportToWKT( + WKTFormatter::create(WKTFormatter::Convention::WKT2_2019).get()); + auto obj = WKTParser().createFromWKT(wkt); + auto co = nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(co != nullptr); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + co->exportToPROJString(PROJStringFormatter::create().get())); + } + + bool foundApprox = false; + for (size_t i = 0; i < list.size(); i++) { + auto projString = + list[i]->exportToPROJString(PROJStringFormatter::create().get()); + EXPECT_TRUE( + projString.find("+proj=pipeline +step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +z_in=us-ft " + "+xy_out=rad +z_out=m") == 0) + << list[i]->nameStr(); + if (list[i]->nameStr().find("Transformation from NGVD29 height (ftUS) " + "to NAVD88 height (ballpark vertical " + "transformation)") == 0) { + EXPECT_TRUE(list[i]->hasBallparkTransformation()); + EXPECT_EQ(list[i]->nameStr(), + "Transformation from NGVD29 height (ftUS) to NAVD88 " + "height (ballpark vertical transformation) + NAD27 to " + "WGS 84 (79) + Inverse of NAD83(NSRS2007) to WGS 84 (1)"); + EXPECT_EQ( + projString, + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +z_in=us-ft +xy_out=rad " + "+z_out=m +step +proj=hgridshift +grids=us_noaa_conus.tif " + "+step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); + foundApprox = true; + break; + } + } + EXPECT_TRUE(foundApprox); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_compoundCRS_context_helmert_noop) { + auto dbContext = DatabaseContext::create(); + auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + // WGS84 + EGM96 + auto objSrc = createFromUserInput("EPSG:4326+5773", dbContext); + auto srcCrs = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(srcCrs != nullptr); + // ETRS89 + EGM96 + auto objDest = createFromUserInput("EPSG:4258+5773", dbContext); + auto destCrs = nn_dynamic_pointer_cast(objDest); + ASSERT_TRUE(destCrs != nullptr); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcCrs), NN_NO_CHECK(destCrs), ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=noop"); +} + +// --------------------------------------------------------------------------- + +// EGM96 has a geoid model referenced to WGS84, and Belfast height has a +// geoid model referenced to ETRS89 +TEST(operation, compoundCRS_to_compoundCRS_WGS84_EGM96_to_ETRS89_Belfast) { + auto dbContext = DatabaseContext::create(); + auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + // WGS84 + EGM96 + auto objSrc = createFromUserInput("EPSG:4326+5773", dbContext); + auto srcCrs = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(srcCrs != nullptr); + // ETRS89 + Belfast height + auto objDest = createFromUserInput("EPSG:4258+5732", dbContext); + auto destCrs = nn_dynamic_pointer_cast(objDest); + ASSERT_TRUE(destCrs != nullptr); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcCrs), NN_NO_CHECK(destCrs), ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), "Inverse of WGS 84 to EGM96 height (1) + " + "Inverse of ETRS89 to WGS 84 (1) + " + "ETRS89 to Belfast height (2)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=us_nga_egm96_15.tif +multiplier=1 " + "+step +inv +proj=vgridshift +grids=uk_os_OSGM15_Belfast.tif " + "+multiplier=1 +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +// Variant of above where source intermediate geog3D CRS == target intermediate +// geog3D CRS +TEST(operation, compoundCRS_to_compoundCRS_WGS84_EGM96_to_WGS84_Belfast) { + auto dbContext = DatabaseContext::create(); + auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + // WGS84 + EGM96 + auto objSrc = createFromUserInput("EPSG:4326+5773", dbContext); + auto srcCrs = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(srcCrs != nullptr); + // WGS84 + Belfast height + auto objDest = createFromUserInput("EPSG:4326+5732", dbContext); + auto destCrs = nn_dynamic_pointer_cast(objDest); + ASSERT_TRUE(destCrs != nullptr); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcCrs), NN_NO_CHECK(destCrs), ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), "Inverse of WGS 84 to EGM96 height (1) + " + "Inverse of ETRS89 to WGS 84 (1) + " + "ETRS89 to Belfast height (2) + " + "ETRS89 to WGS 84 (1)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=us_nga_egm96_15.tif +multiplier=1 " + "+step +inv +proj=vgridshift +grids=uk_os_OSGM15_Belfast.tif " + "+multiplier=1 +step " + "+proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + compoundCRS_to_compoundCRS_concatenated_operation_with_two_vert_transformation) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + // ETRS89 + Baltic 1957 height + authFactory->createCoordinateReferenceSystem("8360"), + // ETRS89 + EVRF2007 height + authFactory->createCoordinateReferenceSystem("7423"), ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_EQ( + list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift " + "+grids=sk_gku_Slovakia_ETRS89h_to_Baltic1957.tif +multiplier=1 " + "+step +inv +proj=vgridshift " + "+grids=sk_gku_Slovakia_ETRS89h_to_EVRF2007.tif +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + EXPECT_EQ( + list[0]->nameStr(), + "ETRS89 + Baltic 1957 height to ETRS89 + EVRF2007 height (1)"); + EXPECT_EQ(list[0]->inverse()->nameStr(), "Inverse of 'ETRS89 + Baltic " + "1957 height to ETRS89 + " + "EVRF2007 height (1)'"); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, vertCRS_to_vertCRS) { + + auto vertcrs_m_obj = PROJStringParser().createFromPROJString("+vunits=m"); + auto vertcrs_m = nn_dynamic_pointer_cast(vertcrs_m_obj); + ASSERT_TRUE(vertcrs_m != nullptr); + + auto vertcrs_ft_obj = PROJStringParser().createFromPROJString("+vunits=ft"); + auto vertcrs_ft = nn_dynamic_pointer_cast(vertcrs_ft_obj); + ASSERT_TRUE(vertcrs_ft != nullptr); + + auto vertcrs_us_ft_obj = + PROJStringParser().createFromPROJString("+vunits=us-ft"); + auto vertcrs_us_ft = + nn_dynamic_pointer_cast(vertcrs_us_ft_obj); + ASSERT_TRUE(vertcrs_us_ft != nullptr); + + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(vertcrs_m), NN_CHECK_ASSERT(vertcrs_ft)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=unitconvert +z_in=m +z_out=ft"); + } + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(vertcrs_m), NN_CHECK_ASSERT(vertcrs_ft)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->inverse()->exportToPROJString( + PROJStringFormatter::create().get()), + "+proj=unitconvert +z_in=ft +z_out=m"); + } + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(vertcrs_ft), NN_CHECK_ASSERT(vertcrs_m)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=unitconvert +z_in=ft +z_out=m"); + } + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(vertcrs_ft), NN_CHECK_ASSERT(vertcrs_us_ft)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=affine +s33=0.999998"); + } + + auto vertCRSMetreUp = + nn_dynamic_pointer_cast(WKTParser().createFromWKT( + "VERTCRS[\"my height\",VDATUM[\"my datum\"],CS[vertical,1]," + "AXIS[\"gravity-related height (H)\",up," + "LENGTHUNIT[\"metre\",1]]]")); + ASSERT_TRUE(vertCRSMetreUp != nullptr); + + auto vertCRSMetreDown = + nn_dynamic_pointer_cast(WKTParser().createFromWKT( + "VERTCRS[\"my depth\",VDATUM[\"my datum\"],CS[vertical,1]," + "AXIS[\"depth (D)\",down,LENGTHUNIT[\"metre\",1]]]")); + ASSERT_TRUE(vertCRSMetreDown != nullptr); + + auto vertCRSMetreDownFtUS = + nn_dynamic_pointer_cast(WKTParser().createFromWKT( + "VERTCRS[\"my depth (ftUS)\",VDATUM[\"my datum\"],CS[vertical,1]," + "AXIS[\"depth (D)\",down,LENGTHUNIT[\"US survey " + "foot\",0.304800609601219]]]")); + ASSERT_TRUE(vertCRSMetreDownFtUS != nullptr); + + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(vertCRSMetreUp), NN_CHECK_ASSERT(vertCRSMetreDown)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=axisswap +order=1,2,-3"); + } + + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(vertCRSMetreUp), + NN_CHECK_ASSERT(vertCRSMetreDownFtUS)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=affine +s33=-3.28083333333333"); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, vertCRS_to_vertCRS_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + // NGVD29 height (m) + authFactory->createCoordinateReferenceSystem("7968"), + // NAVD88 height (1) + authFactory->createCoordinateReferenceSystem("5703"), ctxt); + ASSERT_EQ(list.size(), 3U); + EXPECT_EQ(list[0]->nameStr(), "NGVD29 height (m) to NAVD88 height (3)"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=vgridshift +grids=us_noaa_vertcone.tif +multiplier=1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, vertCRS_to_vertCRS_New_Zealand_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + // NZVD2016 height + authFactory->createCoordinateReferenceSystem("7839"), + // Auckland 1946 height + authFactory->createCoordinateReferenceSystem("5759"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=vgridshift +grids=nz_linz_auckht1946-nzvd2016.tif " + "+multiplier=1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, projCRS_3D_to_geogCRS_3D) { + + auto compoundcrs_ft_obj = PROJStringParser().createFromPROJString( + "+proj=merc +vunits=ft +type=crs"); + auto proj3DCRS_ft = nn_dynamic_pointer_cast(compoundcrs_ft_obj); + ASSERT_TRUE(proj3DCRS_ft != nullptr); + + auto geogcrs_m_obj = PROJStringParser().createFromPROJString( + "+proj=longlat +vunits=m +type=crs"); + auto geogcrs_m = nn_dynamic_pointer_cast(geogcrs_m_obj); + ASSERT_TRUE(geogcrs_m != nullptr); + + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(proj3DCRS_ft), NN_CHECK_ASSERT(geogcrs_m)); + ASSERT_TRUE(op != nullptr); + EXPECT_FALSE(op->hasBallparkTransformation()); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=m +z_in=ft " + "+xy_out=m +z_out=m " + "+step +inv +proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 " + "+ellps=WGS84 " + "+step +proj=unitconvert +xy_in=rad +z_in=m " + "+xy_out=deg +z_out=m"); + } + + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(geogcrs_m), NN_CHECK_ASSERT(proj3DCRS_ft)); + ASSERT_TRUE(op != nullptr); + EXPECT_FALSE(op->hasBallparkTransformation()); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=unitconvert +z_in=m +z_out=ft " + "+step +proj=unitconvert +xy_in=deg +z_in=ft " + "+xy_out=rad +z_out=m " + "+step +proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 " + "+step +proj=unitconvert +xy_in=m +z_in=m " + "+xy_out=m +z_out=ft"); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_geogCRS_3D) { + + auto compoundcrs_ft_obj = WKTParser().createFromWKT( + "COMPOUNDCRS[\"unknown\",\n" + " PROJCRS[\"unknown\",\n" + " BASEGEOGCRS[\"unknown\",\n" + " DATUM[\"World Geodetic System 1984\",\n" + " ELLIPSOID[\"WGS 84\",6378137,298.257223563,\n" + " LENGTHUNIT[\"metre\",1]],\n" + " ID[\"EPSG\",6326]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8901]]],\n" + " CONVERSION[\"unknown\",\n" + " METHOD[\"Mercator (variant A)\",\n" + " ID[\"EPSG\",9804]],\n" + " PARAMETER[\"Latitude of natural origin\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8801]],\n" + " PARAMETER[\"Longitude of natural origin\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8802]],\n" + " PARAMETER[\"Scale factor at natural origin\",1,\n" + " SCALEUNIT[\"unity\",1],\n" + " ID[\"EPSG\",8805]],\n" + " PARAMETER[\"False easting\",0,\n" + " LENGTHUNIT[\"metre\",1],\n" + " ID[\"EPSG\",8806]],\n" + " PARAMETER[\"False northing\",0,\n" + " LENGTHUNIT[\"metre\",1],\n" + " ID[\"EPSG\",8807]]],\n" + " CS[Cartesian,2],\n" + " AXIS[\"(E)\",east,\n" + " ORDER[1],\n" + " LENGTHUNIT[\"metre\",1,\n" + " ID[\"EPSG\",9001]]],\n" + " AXIS[\"(N)\",north,\n" + " ORDER[2],\n" + " LENGTHUNIT[\"metre\",1,\n" + " ID[\"EPSG\",9001]]]],\n" + " VERTCRS[\"unknown\",\n" + " VDATUM[\"unknown\"],\n" + " CS[vertical,1],\n" + " AXIS[\"gravity-related height (H)\",up,\n" + " LENGTHUNIT[\"foot\",0.3048,\n" + " ID[\"EPSG\",9002]]]]]"); + auto compoundcrs_ft = nn_dynamic_pointer_cast(compoundcrs_ft_obj); + ASSERT_TRUE(compoundcrs_ft != nullptr); + + auto geogcrs_m_obj = PROJStringParser().createFromPROJString( + "+proj=longlat +vunits=m +type=crs"); + auto geogcrs_m = nn_dynamic_pointer_cast(geogcrs_m_obj); + ASSERT_TRUE(geogcrs_m != nullptr); + + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(compoundcrs_ft), NN_CHECK_ASSERT(geogcrs_m)); + ASSERT_TRUE(op != nullptr); + EXPECT_TRUE(op->hasBallparkTransformation()); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=merc +lon_0=0 +k=1 +x_0=0 " + "+y_0=0 +ellps=WGS84 +step +proj=unitconvert +xy_in=rad " + "+z_in=ft +xy_out=deg +z_out=m"); + } + + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(geogcrs_m), NN_CHECK_ASSERT(compoundcrs_ft)); + ASSERT_TRUE(op != nullptr); + EXPECT_TRUE(op->hasBallparkTransformation()); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=unitconvert +xy_in=deg +z_in=m " + "+xy_out=rad +z_out=ft +step +proj=merc +lon_0=0 +k=1 +x_0=0 " + "+y_0=0 +ellps=WGS84"); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_geogCRS_3D_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + // CompoundCRS to Geog3DCRS, with vertical unit change, but without + // ellipsoid height <--> vertical height correction + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem( + "7406"), // NAD27 + NGVD29 height (ftUS) + authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 + ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_TRUE(list[0]->hasBallparkTransformation()); + EXPECT_EQ(list[0]->nameStr(), + "NAD27 to WGS 84 (79) + Transformation from NGVD29 height " + "(ftUS) to WGS 84 (ballpark vertical transformation, without " + "ellipsoid height to vertical height correction)"); + EXPECT_EQ(list[0]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 +step " + "+proj=unitconvert +xy_in=deg +xy_out=rad +step " + "+proj=hgridshift +grids=us_noaa_conus.tif " + "+step +proj=unitconvert " + "+xy_in=rad +z_in=us-ft +xy_out=deg +z_out=m +step " + "+proj=axisswap +order=2,1"); + } + + // CompoundCRS to Geog3DCRS, with same vertical unit, and with + // direct ellipsoid height <--> vertical height correction and + // direct horizontal transform (no-op here) + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + auto list = CoordinateOperationFactory::create()->createOperations( + authFactory->createCoordinateReferenceSystem( + "5500"), // NAD83(NSRS2007) + NAVD88 height + authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 + ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), + "Inverse of NAD83(NSRS2007) to NAVD88 height (1) + " + "NAD83(NSRS2007) to WGS 84 (1)"); + EXPECT_EQ(list[0]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=us_noaa_geoid09_conus.tif " + "+multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + EXPECT_EQ(list[0]->remarks(), + "For NAD83(NSRS2007) to NAVD88 height (1) (EPSG:9173): Uses " + "Geoid09 hybrid model. Replaced by 2012 model (CT code 6326)." + "\n" + "For NAD83(NSRS2007) to WGS 84 (1) (EPSG:15931): " + "Approximation at the +/- 1m level assuming that " + "NAD83(NSRS2007) is equivalent to WGS 84 within the accuracy " + "of the transformation."); + } + + // NAD83 + NAVD88 height --> WGS 84 + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + // NAD83 + NAVD88 height + auto srcObj = createFromUserInput( + "EPSG:4269+5703", authFactory->databaseContext(), false); + auto src = nn_dynamic_pointer_cast(srcObj); + ASSERT_TRUE(src != nullptr); + auto nnSrc = NN_NO_CHECK(src); + + auto list = CoordinateOperationFactory::create()->createOperations( + nnSrc, + authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 + ctxt); + ASSERT_GE(list.size(), 2U); + + EXPECT_EQ(list[0]->nameStr(), + "NAD83 to WGS 84 (1) + " + "Inverse of NAD83(NSRS2007) to WGS 84 (1) + " + "Inverse of NAD83(NSRS2007) to NAVD88 height (1) + " + "NAD83(NSRS2007) to WGS 84 (1)"); + EXPECT_EQ(list[0]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=us_noaa_geoid09_conus.tif " + "+multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + } + + // Another variation, but post horizontal adjustment is in two steps + { + auto ctxt = + CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + // NAD83(2011) + NAVD88 height + auto srcObj = createFromUserInput( + "EPSG:6318+5703", authFactory->databaseContext(), false); + auto src = nn_dynamic_pointer_cast(srcObj); + ASSERT_TRUE(src != nullptr); + auto nnSrc = NN_NO_CHECK(src); + + auto list = CoordinateOperationFactory::create()->createOperations( + nnSrc, + authFactory->createCoordinateReferenceSystem("4979"), // WGS 84 + ctxt); + ASSERT_GE(list.size(), 2U); + + EXPECT_EQ(list[0]->nameStr(), + "Inverse of NAD83(2011) to NAVD88 height (3) + " + "Inverse of NAD83 to NAD83(2011) (1) + " + "NAD83 to WGS 84 (1)"); + EXPECT_EQ(list[0]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=us_noaa_g2018u0.tif " + "+multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + // Shows vertical step, and then horizontal step + EXPECT_EQ(list[1]->nameStr(), + "Inverse of NAD83(2011) to NAVD88 height (3) + " + "Inverse of NAD83 to NAD83(2011) (1) + " + "NAD83 to WGS 84 (18)"); + EXPECT_EQ(list[1]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=us_noaa_g2018u0.tif " + "+multiplier=1 " + "+step +proj=hgridshift +grids=us_noaa_FL.tif " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_geogCRS_3D_with_3D_helmert_context) { + // Use case of https://github.com/OSGeo/PROJ/issues/2225 + auto dbContext = DatabaseContext::create(); + auto authFactory = AuthorityFactory::create(dbContext, "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + // WGS84 + EGM96 height + auto srcObj = createFromUserInput("EPSG:4326+5773", dbContext, false); + auto src = nn_dynamic_pointer_cast(srcObj); + ASSERT_TRUE(src != nullptr); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(src), + // CH1903+ + authFactory->createCoordinateReferenceSystem("4150")->promoteTo3D( + std::string(), dbContext), + ctxt); + ASSERT_GE(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), "Inverse of WGS 84 to EGM96 height (1) + " + "Inverse of CH1903+ to WGS 84 (1)"); + // Check that there is no push v_3 / pop v_3 + const char *expected_proj = + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=vgridshift +grids=us_nga_egm96_15.tif +multiplier=1 " + "+step +proj=cart +ellps=WGS84 " + "+step +proj=helmert +x=-674.374 +y=-15.056 +z=-405.346 " + "+step +inv +proj=cart +ellps=bessel " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"; + EXPECT_EQ(list[0]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, dbContext) + .get()), + expected_proj); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_geogCRS_2D_promote_to_3D_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + // NAD83 + NAVD88 height + auto srcObj = createFromUserInput("EPSG:4269+5703", + authFactory->databaseContext(), false); + auto src = nn_dynamic_pointer_cast(srcObj); + ASSERT_TRUE(src != nullptr); + auto nnSrc = NN_NO_CHECK(src); + auto dst = authFactory->createCoordinateReferenceSystem("4269"); // NAD83 + + auto listCompoundToGeog2D = + CoordinateOperationFactory::create()->createOperations(nnSrc, dst, + ctxt); + // The checked value is not that important, but in case this changes, + // likely due to a EPSG upgrade, worth checking + EXPECT_EQ(listCompoundToGeog2D.size(), 142U); + + auto listGeog2DToCompound = + CoordinateOperationFactory::create()->createOperations(dst, nnSrc, + ctxt); + EXPECT_EQ(listGeog2DToCompound.size(), listCompoundToGeog2D.size()); + + auto listCompoundToGeog3D = + CoordinateOperationFactory::create()->createOperations( + nnSrc, + dst->promoteTo3D(std::string(), authFactory->databaseContext()), + ctxt); + EXPECT_EQ(listCompoundToGeog3D.size(), listCompoundToGeog2D.size()); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_of_projCRS_to_geogCRS_2D_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + // SPCS83 California zone 1 (US Survey feet) + NAVD88 height (ftUS) + auto srcObj = createFromUserInput("EPSG:2225+6360", + authFactory->databaseContext(), false); + auto src = nn_dynamic_pointer_cast(srcObj); + ASSERT_TRUE(src != nullptr); + auto nnSrc = NN_NO_CHECK(src); + auto dst = authFactory->createCoordinateReferenceSystem("4269"); // NAD83 + + auto list = CoordinateOperationFactory::create()->createOperations( + nnSrc, dst, ctxt); + // The checked value is not that important, but in case this changes, + // likely due to a EPSG upgrade, worth checking + // We want to make sure that the horizontal adjustments before and after + // the vertical transformation are the reverse of each other, and there are + // not mixes with different alternative operations (like California grid + // forward and Nevada grid reverse) + ASSERT_EQ(list.size(), 14U); + + // Check that unit conversion is OK + auto op_proj = + list[0]->exportToPROJString(PROJStringFormatter::create().get()); + EXPECT_EQ(op_proj, + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=us-ft +xy_out=m " + "+step +inv +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 " + "+lat_1=41.6666666666667 +lat_2=40 +x_0=2000000.0001016 " + "+y_0=500000.0001016 +ellps=GRS80 " + "+step +proj=unitconvert +z_in=us-ft +z_out=m " + "+step +proj=vgridshift +grids=us_noaa_geoid09_conus.tif " + "+multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_from_wkt_without_id_to_geogCRS) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + auto wkt = + "COMPOUNDCRS[\"NAD83(2011) + NAVD88 height\",\n" + " GEOGCRS[\"NAD83(2011)\",\n" + " DATUM[\"NAD83 (National Spatial Reference System 2011)\",\n" + " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " CS[ellipsoidal,2],\n" + " AXIS[\"geodetic latitude (Lat)\",north,\n" + " ORDER[1],\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " AXIS[\"geodetic longitude (Lon)\",east,\n" + " ORDER[2],\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]]],\n" + " VERTCRS[\"NAVD88 height\",\n" + " VDATUM[\"North American Vertical Datum 1988\"],\n" + " CS[vertical,1],\n" + " AXIS[\"gravity-related height (H)\",up,\n" + " LENGTHUNIT[\"metre\",1]]]]"; + auto srcObj = + createFromUserInput(wkt, authFactory->databaseContext(), false); + auto src = nn_dynamic_pointer_cast(srcObj); + ASSERT_TRUE(src != nullptr); + auto dst = + authFactory->createCoordinateReferenceSystem("6319"); // NAD83(2011) + + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(src), dst, ctxt); + // NAD83(2011) + NAVD88 height + auto srcRefObj = createFromUserInput("EPSG:6318+5703", + authFactory->databaseContext(), false); + auto srcRef = nn_dynamic_pointer_cast(srcRefObj); + ASSERT_TRUE(srcRef != nullptr); + ASSERT_TRUE( + src->isEquivalentTo(srcRef.get(), IComparable::Criterion::EQUIVALENT)); + auto listRef = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcRef), dst, ctxt); + + EXPECT_EQ(list.size(), listRef.size()); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + compoundCRS_of_projCRS_from_wkt_without_id_or_extent_to_geogCRS) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + auto wkt = + "COMPOUNDCRS[\"NAD83 / Pennsylvania South + NAVD88 height\",\n" + " PROJCRS[\"NAD83 / Pennsylvania South\",\n" + " BASEGEOGCRS[\"NAD83\",\n" + " DATUM[\"North American Datum 1983\",\n" + " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]]],\n" + " CONVERSION[\"SPCS83 Pennsylvania South zone (meters)\",\n" + " METHOD[\"Lambert Conic Conformal (2SP)\",\n" + " ID[\"EPSG\",9802]],\n" + " PARAMETER[\"Latitude of false origin\",39.3333333333333,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8821]],\n" + " PARAMETER[\"Longitude of false origin\",-77.75,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8822]],\n" + " PARAMETER[\"Latitude of 1st standard " + "parallel\",40.9666666666667,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8823]],\n" + " PARAMETER[\"Latitude of 2nd standard " + "parallel\",39.9333333333333,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8824]],\n" + " PARAMETER[\"Easting at false origin\",600000,\n" + " LENGTHUNIT[\"metre\",1],\n" + " ID[\"EPSG\",8826]],\n" + " PARAMETER[\"Northing at false origin\",0,\n" + " LENGTHUNIT[\"metre\",1],\n" + " ID[\"EPSG\",8827]]],\n" + " CS[Cartesian,2],\n" + " AXIS[\"easting (X)\",east,\n" + " ORDER[1],\n" + " LENGTHUNIT[\"metre\",1]],\n" + " AXIS[\"northing (Y)\",north,\n" + " ORDER[2],\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " VERTCRS[\"NAVD88 height\",\n" + " VDATUM[\"North American Vertical Datum 1988\"],\n" + " CS[vertical,1],\n" + " AXIS[\"gravity-related height (H)\",up,\n" + " LENGTHUNIT[\"metre\",1]]]]"; + auto srcObj = + createFromUserInput(wkt, authFactory->databaseContext(), false); + auto src = nn_dynamic_pointer_cast(srcObj); + ASSERT_TRUE(src != nullptr); + auto dst = authFactory->createCoordinateReferenceSystem("4269"); // NAD83 + + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(src), dst, ctxt); + // NAD83 / Pennsylvania South + NAVD88 height + auto srcRefObj = createFromUserInput("EPSG:32129+5703", + authFactory->databaseContext(), false); + auto srcRef = nn_dynamic_pointer_cast(srcRefObj); + ASSERT_TRUE(srcRef != nullptr); + ASSERT_TRUE( + src->isEquivalentTo(srcRef.get(), IComparable::Criterion::EQUIVALENT)); + auto listRef = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcRef), dst, ctxt); + + EXPECT_EQ(list.size(), listRef.size()); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_geogCRS_with_vertical_unit_change) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + // NAD83(2011) + NAVD88 height (ftUS) + auto srcObj = createFromUserInput("EPSG:6318+6360", + authFactory->databaseContext(), false); + auto src = nn_dynamic_pointer_cast(srcObj); + ASSERT_TRUE(src != nullptr); + auto nnSrc = NN_NO_CHECK(src); + auto dst = + authFactory->createCoordinateReferenceSystem("6319"); // NAD83(2011) 3D + + auto listCompoundToGeog = + CoordinateOperationFactory::create()->createOperations(nnSrc, dst, + ctxt); + ASSERT_TRUE(!listCompoundToGeog.empty()); + + // NAD83(2011) + NAVD88 height + auto srcObjCompoundVMetre = createFromUserInput( + "EPSG:6318+5703", authFactory->databaseContext(), false); + auto srcCompoundVMetre = nn_dynamic_pointer_cast(srcObjCompoundVMetre); + ASSERT_TRUE(srcCompoundVMetre != nullptr); + auto listCompoundMetreToGeog = + CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcCompoundVMetre), dst, ctxt); + + // Check that we get the same and similar results whether we start from + // regular NAVD88 height or its ftUs variant + ASSERT_EQ(listCompoundToGeog.size(), listCompoundMetreToGeog.size()); + + EXPECT_EQ(listCompoundToGeog[0]->nameStr(), + "Inverse of NAVD88 height to NAVD88 height (ftUS) + " + + listCompoundMetreToGeog[0]->nameStr()); + EXPECT_EQ( + listCompoundToGeog[0]->exportToPROJString( + PROJStringFormatter::create(PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + replaceAll(listCompoundMetreToGeog[0]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+step +proj=unitconvert +xy_in=deg +xy_out=rad", + "+step +proj=unitconvert +xy_in=deg +z_in=us-ft +xy_out=rad " + "+z_out=m")); + + // Check reverse path + auto listGeogToCompound = + CoordinateOperationFactory::create()->createOperations(dst, nnSrc, + ctxt); + EXPECT_EQ(listGeogToCompound.size(), listCompoundToGeog.size()); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + compoundCRS_to_geogCRS_with_vertical_unit_change_and_complex_horizontal_change) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + // NAD83(2011) + NAVD88 height (ftUS) + auto srcObj = createFromUserInput("EPSG:6318+6360", + authFactory->databaseContext(), false); + auto src = nn_dynamic_pointer_cast(srcObj); + ASSERT_TRUE(src != nullptr); + auto nnSrc = NN_NO_CHECK(src); + auto dst = + authFactory->createCoordinateReferenceSystem("7665"); // WGS84(G1762) 3D + + auto listCompoundToGeog = + CoordinateOperationFactory::create()->createOperations(nnSrc, dst, + ctxt); + + // NAD83(2011) + NAVD88 height + auto srcObjCompoundVMetre = createFromUserInput( + "EPSG:6318+5703", authFactory->databaseContext(), false); + auto srcCompoundVMetre = nn_dynamic_pointer_cast(srcObjCompoundVMetre); + ASSERT_TRUE(srcCompoundVMetre != nullptr); + auto listCompoundMetreToGeog = + CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcCompoundVMetre), dst, ctxt); + + // Check that we get the same and similar results whether we start from + // regular NAVD88 height or its ftUs variant + ASSERT_EQ(listCompoundToGeog.size(), listCompoundMetreToGeog.size()); + + ASSERT_GE(listCompoundToGeog.size(), 1U); + + EXPECT_EQ(listCompoundToGeog[0]->nameStr(), + "Inverse of NAVD88 height to NAVD88 height (ftUS) + " + + listCompoundMetreToGeog[0]->nameStr()); + EXPECT_EQ( + listCompoundToGeog[0]->exportToPROJString( + PROJStringFormatter::create(PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + replaceAll(listCompoundMetreToGeog[0]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+step +proj=unitconvert +xy_in=deg +xy_out=rad", + "+step +proj=unitconvert +xy_in=deg +z_in=us-ft +xy_out=rad " + "+z_out=m")); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_to_geogCRS_with_height_depth_reversal) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + // NAD83(2011) + NAVD88 depth + auto srcObj = createFromUserInput("EPSG:6318+6357", + authFactory->databaseContext(), false); + auto src = nn_dynamic_pointer_cast(srcObj); + ASSERT_TRUE(src != nullptr); + auto nnSrc = NN_NO_CHECK(src); + auto dst = + authFactory->createCoordinateReferenceSystem("6319"); // NAD83(2011) 3D + + auto listCompoundToGeog = + CoordinateOperationFactory::create()->createOperations(nnSrc, dst, + ctxt); + ASSERT_TRUE(!listCompoundToGeog.empty()); + + // NAD83(2011) + NAVD88 height + auto srcObjCompoundVMetre = createFromUserInput( + "EPSG:6318+5703", authFactory->databaseContext(), false); + auto srcCompoundVMetre = nn_dynamic_pointer_cast(srcObjCompoundVMetre); + ASSERT_TRUE(srcCompoundVMetre != nullptr); + auto listCompoundMetreToGeog = + CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcCompoundVMetre), dst, ctxt); + + // Check that we get the same and similar results whether we start from + // regular NAVD88 height or its depth variant + ASSERT_EQ(listCompoundToGeog.size(), listCompoundMetreToGeog.size()); + + EXPECT_EQ(listCompoundToGeog[0]->nameStr(), + "Inverse of NAVD88 height to NAVD88 depth + " + + listCompoundMetreToGeog[0]->nameStr()); + EXPECT_EQ( + listCompoundToGeog[0]->exportToPROJString( + PROJStringFormatter::create(PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + replaceAll(listCompoundMetreToGeog[0]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+step +proj=unitconvert +xy_in=deg +xy_out=rad", + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=axisswap +order=1,2,-3")); + + // Check reverse path + auto listGeogToCompound = + CoordinateOperationFactory::create()->createOperations(dst, nnSrc, + ctxt); + EXPECT_EQ(listGeogToCompound.size(), listCompoundToGeog.size()); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + compoundCRS_to_geogCRS_with_vertical_unit_change_and_height_depth_reversal) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + // NAD83(2011) + NAVD88 depth (ftUS) + auto srcObj = createFromUserInput("EPSG:6318+6358", + authFactory->databaseContext(), false); + auto src = nn_dynamic_pointer_cast(srcObj); + ASSERT_TRUE(src != nullptr); + auto nnSrc = NN_NO_CHECK(src); + auto dst = + authFactory->createCoordinateReferenceSystem("6319"); // NAD83(2011) 3D + + auto listCompoundToGeog = + CoordinateOperationFactory::create()->createOperations(nnSrc, dst, + ctxt); + ASSERT_TRUE(!listCompoundToGeog.empty()); + + // NAD83(2011) + NAVD88 height + auto srcObjCompoundVMetre = createFromUserInput( + "EPSG:6318+5703", authFactory->databaseContext(), false); + auto srcCompoundVMetre = nn_dynamic_pointer_cast(srcObjCompoundVMetre); + ASSERT_TRUE(srcCompoundVMetre != nullptr); + auto listCompoundMetreToGeog = + CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(srcCompoundVMetre), dst, ctxt); + + // Check that we get the same and similar results whether we start from + // regular NAVD88 height or its depth (ftUS) variant + ASSERT_EQ(listCompoundToGeog.size(), listCompoundMetreToGeog.size()); + + EXPECT_EQ(listCompoundToGeog[0]->nameStr(), + "Inverse of NAVD88 height (ftUS) to NAVD88 depth (ftUS) + " + "Inverse of NAVD88 height to NAVD88 height (ftUS) + " + + listCompoundMetreToGeog[0]->nameStr()); + EXPECT_EQ( + listCompoundToGeog[0]->exportToPROJString( + PROJStringFormatter::create(PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + replaceAll(listCompoundMetreToGeog[0]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+step +proj=unitconvert +xy_in=deg +xy_out=rad", + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=axisswap +order=1,2,-3 " + "+step +proj=unitconvert +z_in=us-ft +z_out=m")); + + // Check reverse path + auto listGeogToCompound = + CoordinateOperationFactory::create()->createOperations(dst, nnSrc, + ctxt); + EXPECT_EQ(listGeogToCompound.size(), listCompoundToGeog.size()); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_of_vertCRS_with_geoid_model_to_geogCRS) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + auto wkt = + "COMPOUNDCRS[\"NAD83 / Pennsylvania South + NAVD88 height\",\n" + " PROJCRS[\"NAD83 / Pennsylvania South\",\n" + " BASEGEOGCRS[\"NAD83\",\n" + " DATUM[\"North American Datum 1983\",\n" + " ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]]],\n" + " CONVERSION[\"SPCS83 Pennsylvania South zone (meters)\",\n" + " METHOD[\"Lambert Conic Conformal (2SP)\",\n" + " ID[\"EPSG\",9802]],\n" + " PARAMETER[\"Latitude of false origin\",39.3333333333333,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8821]],\n" + " PARAMETER[\"Longitude of false origin\",-77.75,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8822]],\n" + " PARAMETER[\"Latitude of 1st standard " + "parallel\",40.9666666666667,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8823]],\n" + " PARAMETER[\"Latitude of 2nd standard " + "parallel\",39.9333333333333,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8824]],\n" + " PARAMETER[\"Easting at false origin\",600000,\n" + " LENGTHUNIT[\"metre\",1],\n" + " ID[\"EPSG\",8826]],\n" + " PARAMETER[\"Northing at false origin\",0,\n" + " LENGTHUNIT[\"metre\",1],\n" + " ID[\"EPSG\",8827]]],\n" + " CS[Cartesian,2],\n" + " AXIS[\"easting (X)\",east,\n" + " ORDER[1],\n" + " LENGTHUNIT[\"metre\",1]],\n" + " AXIS[\"northing (Y)\",north,\n" + " ORDER[2],\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " VERTCRS[\"NAVD88 height\",\n" + " VDATUM[\"North American Vertical Datum 1988\"],\n" + " CS[vertical,1],\n" + " AXIS[\"gravity-related height (H)\",up,\n" + " LENGTHUNIT[\"metre\",1]],\n" + " GEOIDMODEL[\"GEOID12B\"]]]"; + auto srcObj = + createFromUserInput(wkt, authFactory->databaseContext(), false); + auto src = nn_dynamic_pointer_cast(srcObj); + ASSERT_TRUE(src != nullptr); + auto dst = authFactory->createCoordinateReferenceSystem("4269"); // NAD83 + + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(src), dst, ctxt); + ASSERT_TRUE(!list.empty()); + EXPECT_EQ(list[0]->nameStr(), + "Inverse of SPCS83 Pennsylvania South zone (meters) + " + "Ballpark geographic offset from NAD83 to NAD83(2011) + " + "Inverse of NAD83(2011) to NAVD88 height (1) + " + "Ballpark geographic offset from NAD83(2011) to NAD83"); + auto op_proj = + list[0]->exportToPROJString(PROJStringFormatter::create().get()); + EXPECT_EQ( + op_proj, + "+proj=pipeline " + "+step +inv +proj=lcc +lat_0=39.3333333333333 +lon_0=-77.75 " + "+lat_1=40.9666666666667 +lat_2=39.9333333333333 +x_0=600000 " + "+y_0=0 +ellps=GRS80 " + "+step +proj=vgridshift +grids=us_noaa_g2012bu0.tif +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_from_WKT2_to_geogCRS_3D_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto src = authFactory->createCoordinateReferenceSystem( + "7415"); // Amersfoort / RD New + NAP height + auto dst = + authFactory->createCoordinateReferenceSystem("4937"); // ETRS89 3D + auto list = + CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); + ASSERT_GE(list.size(), 1U); + auto wkt2 = src->exportToWKT( + WKTFormatter::create(WKTFormatter::Convention::WKT2_2019).get()); + auto obj = WKTParser().createFromWKT(wkt2); + auto src_from_wkt2 = nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(src_from_wkt2 != nullptr); + auto list2 = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(src_from_wkt2), dst, ctxt); + ASSERT_GE(list.size(), list2.size()); + for (size_t i = 0; i < list.size(); i++) { + const auto &op = list[i]; + const auto &op2 = list2[i]; + EXPECT_TRUE( + op->isEquivalentTo(op2.get(), IComparable::Criterion::EQUIVALENT)); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_from_WKT2_no_id_to_geogCRS_3D_context) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto src = authFactory->createCoordinateReferenceSystem( + "7415"); // Amersfoort / RD New + NAP height + auto dst = + authFactory->createCoordinateReferenceSystem("4937"); // ETRS89 3D + auto list = + CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); + ASSERT_GE(list.size(), 1U); + + { + auto op_proj = + list[0]->exportToPROJString(PROJStringFormatter::create().get()); + EXPECT_EQ( + op_proj, + "+proj=pipeline +step +inv +proj=sterea +lat_0=52.1561605555556 " + "+lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 +y_0=463000 " + "+ellps=bessel " + "+step +proj=hgridshift +grids=nl_nsgi_rdtrans2018.tif " + "+step +proj=vgridshift +grids=nl_nsgi_nlgeo2018.tif +multiplier=1 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + } + + auto wkt2 = + "COMPOUNDCRS[\"unknown\",\n" + " PROJCRS[\"unknown\",\n" + " BASEGEOGCRS[\"Amersfoort\",\n" + " DATUM[\"Amersfoort\",\n" + " ELLIPSOID[\"Bessel " + "1841\",6377397.155,299.1528128]]],\n" + " CONVERSION[\"unknown\",\n" + " METHOD[\"Oblique Stereographic\"],\n" + " PARAMETER[\"Latitude of natural origin\",52.1561605555556],\n" + " PARAMETER[\"Longitude of natural origin\",5.38763888888889],\n" + " PARAMETER[\"Scale factor at natural origin\",0.9999079],\n" + " PARAMETER[\"False easting\",155000],\n" + " PARAMETER[\"False northing\",463000]],\n" + " CS[Cartesian,2],\n" + " AXIS[\"(E)\",east],\n" + " AXIS[\"(N)\",north],\n" + " LENGTHUNIT[\"metre\",1]],\n" + " VERTCRS[\"NAP height\",\n" + " VDATUM[\"Normaal Amsterdams Peil\"],\n" + " CS[vertical,1],\n" + " AXIS[\"gravity-related height (H)\",up,\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " USAGE[\n" + " SCOPE[\"unknown\"],\n" + " AREA[\"Netherlands - onshore\"],\n" + " BBOX[50.75,3.2,53.7,7.22]]]"; + + auto obj = WKTParser().createFromWKT(wkt2); + auto src_from_wkt2 = nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(src_from_wkt2 != nullptr); + auto list2 = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(src_from_wkt2), dst, ctxt); + ASSERT_EQ(list.size(), list2.size()); + for (size_t i = 0; i < list.size(); i++) { + const auto &op = list[i]; + const auto &op2 = list2[i]; + auto op_proj = + op->exportToPROJString(PROJStringFormatter::create().get()); + auto op2_proj = + op2->exportToPROJString(PROJStringFormatter::create().get()); + EXPECT_EQ(op_proj, op2_proj) << "op=" << op->nameStr() + << " op2=" << op2->nameStr(); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, proj3DCRS_with_non_meter_horiz_and_vertical_to_geog) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=utm +zone=31 +datum=WGS84 +units=us-ft +vunits=us-ft +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(src), authFactory->createCoordinateReferenceSystem("4326"), + ctxt); + ASSERT_EQ(list.size(), 1U); + // Check that vertical unit conversion is done just once + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=us-ft +z_in=us-ft " + "+xy_out=m +z_out=m " + "+step +inv +proj=utm +zone=31 +ellps=WGS84 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, compoundCRS_with_non_meter_horiz_and_vertical_to_geog) { + auto objSrc = WKTParser().createFromWKT( + "COMPOUNDCRS[\"unknown\",\n" + " PROJCRS[\"unknown\",\n" + " BASEGEOGCRS[\"unknown\",\n" + " DATUM[\"World Geodetic System 1984\",\n" + " ELLIPSOID[\"WGS 84\",6378137,298.257223563,\n" + " LENGTHUNIT[\"metre\",1]],\n" + " ID[\"EPSG\",6326]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8901]]],\n" + " CONVERSION[\"UTM zone 31N\",\n" + " METHOD[\"Transverse Mercator\",\n" + " ID[\"EPSG\",9807]],\n" + " PARAMETER[\"Latitude of natural origin\",0,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8801]],\n" + " PARAMETER[\"Longitude of natural origin\",3,\n" + " ANGLEUNIT[\"degree\",0.0174532925199433],\n" + " ID[\"EPSG\",8802]],\n" + " PARAMETER[\"Scale factor at natural origin\",0.9996,\n" + " SCALEUNIT[\"unity\",1],\n" + " ID[\"EPSG\",8805]],\n" + " PARAMETER[\"False easting\",500000,\n" + " LENGTHUNIT[\"metre\",1],\n" + " ID[\"EPSG\",8806]],\n" + " PARAMETER[\"False northing\",0,\n" + " LENGTHUNIT[\"metre\",1],\n" + " ID[\"EPSG\",8807]],\n" + " ID[\"EPSG\",16031]],\n" + " CS[Cartesian,2],\n" + " AXIS[\"(E)\",east,\n" + " ORDER[1],\n" + " LENGTHUNIT[\"US survey foot\",0.304800609601219,\n" + " ID[\"EPSG\",9003]]],\n" + " AXIS[\"(N)\",north,\n" + " ORDER[2],\n" + " LENGTHUNIT[\"US survey foot\",0.304800609601219,\n" + " ID[\"EPSG\",9003]]]],\n" + " VERTCRS[\"unknown\",\n" + " VDATUM[\"unknown\"],\n" + " CS[vertical,1],\n" + " AXIS[\"gravity-related height (H)\",up,\n" + " LENGTHUNIT[\"US survey foot\",0.304800609601219,\n" + " ID[\"EPSG\",9003]]]]]" + + ); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + auto list = CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(src), authFactory->createCoordinateReferenceSystem("4326"), + ctxt); + ASSERT_EQ(list.size(), 1U); + // Check that vertical unit conversion is done just once + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=us-ft +xy_out=m " + "+step +inv +proj=utm +zone=31 +ellps=WGS84 " + "+step +proj=unitconvert +xy_in=rad +z_in=us-ft " + "+xy_out=deg +z_out=m " + "+step +proj=axisswap +order=2,1"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, boundCRS_to_compoundCRS) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS67 +nadgrids=@foo.gsb +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +ellps=GRS80 +nadgrids=@bar.gsb +geoidgrids=@bar.gtx " + "+type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + 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=@foo.gsb " + "+step +inv +proj=vgridshift +grids=@bar.gtx +multiplier=1 " + "+step +inv +proj=hgridshift +grids=@bar.gsb " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); + + auto opInverse = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(dst), NN_CHECK_ASSERT(src)); + ASSERT_TRUE(opInverse != nullptr); + EXPECT_TRUE(opInverse->inverse()->_isEquivalentTo(op.get())); +} + +// --------------------------------------------------------------------------- + +TEST(operation, IGNF_LAMB1_TO_EPSG_4326) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), std::string()); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setGridAvailabilityUse( + CoordinateOperationContext::GridAvailabilityUse:: + IGNORE_GRID_AVAILABILITY); + ctxt->setAllowUseIntermediateCRS( + CoordinateOperationContext::IntermediateCRSUse::ALWAYS); + auto list = CoordinateOperationFactory::create()->createOperations( + AuthorityFactory::create(DatabaseContext::create(), "IGNF") + ->createCoordinateReferenceSystem("LAMB1"), + AuthorityFactory::create(DatabaseContext::create(), "EPSG") + ->createCoordinateReferenceSystem("4326"), + ctxt); + ASSERT_EQ(list.size(), 2U); + + EXPECT_FALSE(list[0]->hasBallparkTransformation()); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=lcc +lat_1=49.5 +lat_0=49.5 " + "+lon_0=0 +k_0=0.99987734 +x_0=600000 +y_0=200000 " + "+ellps=clrk80ign +pm=paris +step +proj=hgridshift " + "+grids=fr_ign_ntf_r93.tif +step +proj=unitconvert +xy_in=rad " + "+xy_out=deg +step +proj=axisswap +order=2,1"); + + EXPECT_FALSE(list[1]->hasBallparkTransformation()); + EXPECT_EQ(list[1]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=lcc +lat_1=49.5 +lat_0=49.5 " + "+lon_0=0 +k_0=0.99987734 +x_0=600000 +y_0=200000 " + "+ellps=clrk80ign +pm=paris +step +proj=push +v_3 +step " + "+proj=cart +ellps=clrk80ign +step +proj=helmert +x=-168 +y=-60 " + "+z=320 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg +step " + "+proj=axisswap +order=2,1"); + + auto list2 = CoordinateOperationFactory::create()->createOperations( + AuthorityFactory::create(DatabaseContext::create(), "EPSG") + // NTF (Paris) / Lambert Nord France equivalent to IGNF:LAMB1 + ->createCoordinateReferenceSystem("27561"), + AuthorityFactory::create(DatabaseContext::create(), "EPSG") + ->createCoordinateReferenceSystem("4326"), + ctxt); + ASSERT_GE(list2.size(), 3U); + + EXPECT_EQ(replaceAll(list2[0]->exportToPROJString( + PROJStringFormatter::create().get()), + "0.999877341", "0.99987734"), + list[0]->exportToPROJString(PROJStringFormatter::create().get())); + + // The second entry in list2 (list2[1]) uses the + // weird +pm=2.33720833333333 from "NTF (Paris) to NTF (2)" + // so skip to the 3th method + EXPECT_EQ(replaceAll(list2[2]->exportToPROJString( + PROJStringFormatter::create().get()), + "0.999877341", "0.99987734"), + list[1]->exportToPROJString(PROJStringFormatter::create().get())); +} + +// --------------------------------------------------------------------------- + +TEST(operation, NAD83_to_projeted_CRS_based_on_NAD83_2011) { + auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0); + ctxt->setSpatialCriterion( + CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); + auto list = CoordinateOperationFactory::create()->createOperations( + // NAD83 + authFactory->createCoordinateReferenceSystem("4269"), + // NAD83(2011) / California Albers + authFactory->createCoordinateReferenceSystem("6414"), ctxt); + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->nameStr(), "Ballpark geographic offset from NAD83 to " + "NAD83(2011) + California Albers"); + EXPECT_EQ(list[0]->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=aea +lat_0=0 +lon_0=-120 +lat_1=34 " + "+lat_2=40.5 +x_0=0 +y_0=-4000000 +ellps=GRS80"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, isPROJInstantiable) { + + { + auto transformation = Transformation::createGeocentricTranslations( + PropertyMap(), GeographicCRS::EPSG_4269, GeographicCRS::EPSG_4326, + 1.0, 2.0, 3.0, {}); + EXPECT_TRUE(transformation->isPROJInstantiable( + DatabaseContext::create(), false)); + } + + // Missing grid + { + auto transformation = Transformation::createNTv2( + PropertyMap(), GeographicCRS::EPSG_4807, GeographicCRS::EPSG_4326, + "foo.gsb", std::vector()); + EXPECT_FALSE(transformation->isPROJInstantiable( + DatabaseContext::create(), false)); + } + + // Unsupported method + { + auto transformation = Transformation::create( + PropertyMap(), GeographicCRS::EPSG_4269, GeographicCRS::EPSG_4326, + nullptr, OperationMethod::create( + PropertyMap(), std::vector{}), + std::vector{}, + std::vector{}); + EXPECT_FALSE(transformation->isPROJInstantiable( + DatabaseContext::create(), false)); + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, createOperation_on_crs_with_canonical_bound_crs) { + auto boundCRS = BoundCRS::createFromTOWGS84( + GeographicCRS::EPSG_4267, std::vector{1, 2, 3, 4, 5, 6, 7}); + auto crs = boundCRS->baseCRSWithCanonicalBoundCRS(); + { + auto op = CoordinateOperationFactory::create()->createOperation( + crs, GeographicCRS::EPSG_4326); + ASSERT_TRUE(op != nullptr); + EXPECT_TRUE(op->isEquivalentTo(boundCRS->transformation().get())); + { + auto wkt1 = op->exportToWKT( + WKTFormatter::create(WKTFormatter::Convention::WKT2_2019) + .get()); + auto wkt2 = boundCRS->transformation()->exportToWKT( + WKTFormatter::create(WKTFormatter::Convention::WKT2_2019) + .get()); + EXPECT_EQ(wkt1, wkt2); + } + } + { + auto op = CoordinateOperationFactory::create()->createOperation( + GeographicCRS::EPSG_4326, crs); + ASSERT_TRUE(op != nullptr); + EXPECT_TRUE( + op->isEquivalentTo(boundCRS->transformation()->inverse().get())); + { + auto wkt1 = op->exportToWKT( + WKTFormatter::create(WKTFormatter::Convention::WKT2_2019) + .get()); + auto wkt2 = boundCRS->transformation()->inverse()->exportToWKT( + WKTFormatter::create(WKTFormatter::Convention::WKT2_2019) + .get()); + EXPECT_EQ(wkt1, wkt2); + } + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, createOperation_fallback_to_proj4_strings) { + auto objDest = PROJStringParser().createFromPROJString( + "+proj=longlat +geoc +datum=WGS84 +type=crs"); + auto dest = nn_dynamic_pointer_cast(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 +proj=longlat +geoc +datum=WGS84 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + createOperation_fallback_to_proj4_strings_regular_with_datum_to_projliteral) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=utm +zone=11 +datum=NAD27 +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +datum=WGS84 +over +type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +inv +proj=utm +zone=11 +datum=NAD27 " + "+step +proj=longlat +datum=WGS84 +over " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + createOperation_fallback_to_proj4_strings_proj_NAD83_to_projliteral) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=utm +zone=11 +datum=NAD83 +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +datum=WGS84 +over +type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +inv +proj=utm +zone=11 +ellps=GRS80 " + "+step +proj=longlat +datum=WGS84 +over " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + createOperation_fallback_to_proj4_strings_geog_NAD83_to_projliteral) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=longlat +datum=NAD83 +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +datum=WGS84 +over +type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=longlat +datum=WGS84 +over " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + createOperation_fallback_to_proj4_strings_regular_with_nadgrids_to_projliteral) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=utm +zone=11 +ellps=clrk66 +nadgrids=@conus +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +datum=WGS84 +over +type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +inv +proj=utm +zone=11 +ellps=clrk66 +nadgrids=@conus " + "+step +proj=longlat +datum=WGS84 +over " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + createOperation_fallback_to_proj4_strings_projliteral_to_projliteral) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=utm +zone=11 +datum=NAD27 +over +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +datum=WGS84 +over +type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + "+step +inv +proj=utm +zone=11 +datum=NAD27 +over " + "+step +proj=longlat +datum=WGS84 +over " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); +} + +// --------------------------------------------------------------------------- + +TEST( + operation, + createOperation_fallback_to_proj4_strings_regular_to_projliteral_with_towgs84) { + auto objSrc = + createFromUserInput("EPSG:4326", DatabaseContext::create(), false); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDst = PROJStringParser().createFromPROJString( + "+proj=utm +zone=31 +ellps=GRS80 +towgs84=1,2,3 +over +type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + 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 +proj=utm +zone=31 +ellps=GRS80 +towgs84=1,2,3 +over"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, createOperation_on_crs_with_bound_crs_and_wktext) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=utm +zone=55 +south +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 " + "+units=m +no_defs +nadgrids=@GDA94_GDA2020_conformal.gsb +ignored1 " + "+ignored2=val +wktext +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + + auto objDst = PROJStringParser().createFromPROJString( + "+proj=utm +zone=55 +south +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 " + "+units=m +no_defs +type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + ASSERT_TRUE(op != nullptr); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +inv +proj=utm +zone=55 +south " + "+ellps=GRS80 +step +proj=hgridshift " + "+grids=@GDA94_GDA2020_conformal.gsb +step +proj=utm +zone=55 " + "+south +ellps=GRS80"); +} + +// --------------------------------------------------------------------------- + +TEST(operation, createOperation_ossfuzz_18587) { + auto objSrc = + createFromUserInput("EPSG:4326", DatabaseContext::create(), false); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + + // Extremely weird string ! We should likely reject it + auto objDst = PROJStringParser().createFromPROJString( + "type=crs proj=pipeline step proj=merc vunits=m nadgrids=@x " + "proj=\"\nproj=pipeline step\n\""); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + + // Just check that we don't go into an infinite recursion + try { + CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + } catch (const std::exception &) { + } +} + +// --------------------------------------------------------------------------- + +TEST(operation, derivedGeographicCRS_with_to_wgs84_to_geographicCRS) { + auto objSrc = PROJStringParser().createFromPROJString( + "+proj=ob_tran +o_proj=latlon +lat_0=0 +lon_0=180 +o_lat_p=18.0 " + "+o_lon_p=-200.0 +ellps=WGS84 +towgs84=1,2,3 +type=crs"); + auto src = nn_dynamic_pointer_cast(objSrc); + ASSERT_TRUE(src != nullptr); + auto objDst = PROJStringParser().createFromPROJString( + "+proj=longlat +datum=WGS84 +type=crs"); + auto dst = nn_dynamic_pointer_cast(objDst); + ASSERT_TRUE(dst != nullptr); + + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), NN_CHECK_ASSERT(dst)); + ASSERT_TRUE(op != nullptr); + std::string pipeline( + "+proj=pipeline " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +inv +proj=ob_tran +o_proj=latlon +lat_0=0 +lon_0=180 " + "+o_lat_p=18 +o_lon_p=-200 +ellps=WGS84 " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=WGS84 " + "+step +proj=helmert +x=1 +y=2 +z=3 " + "+step +inv +proj=cart +ellps=WGS84 " + "+step +proj=pop +v_3 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + pipeline); + + auto op2 = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(src), + nn_static_pointer_cast(GeographicCRS::EPSG_4326)); + ASSERT_TRUE(op2 != nullptr); + EXPECT_EQ(op2->exportToPROJString(PROJStringFormatter::create().get()), + pipeline + " +step +proj=axisswap +order=2,1"); + } + + { + auto op = CoordinateOperationFactory::create()->createOperation( + NN_CHECK_ASSERT(dst), NN_CHECK_ASSERT(src)); + ASSERT_TRUE(op != nullptr); + std::string pipeline( + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=WGS84 " + "+step +proj=helmert +x=-1 +y=-2 +z=-3 " + "+step +inv +proj=cart +ellps=WGS84 " + "+step +proj=pop +v_3 " + "+step +proj=ob_tran +o_proj=latlon +lat_0=0 +lon_0=180 " + "+o_lat_p=18 +o_lon_p=-200 +ellps=WGS84 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg"); + EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline " + pipeline); + + auto op2 = CoordinateOperationFactory::create()->createOperation( + nn_static_pointer_cast(GeographicCRS::EPSG_4326), + NN_CHECK_ASSERT(src)); + ASSERT_TRUE(op2 != nullptr); + EXPECT_EQ(op2->exportToPROJString(PROJStringFormatter::create().get()), + "+proj=pipeline +step +proj=axisswap +order=2,1 " + pipeline); + } +}