From d434551c9d3b4a61e940f2574783e0d7f4417949 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 14 Jan 2023 15:18:42 +0100 Subject: [PATCH 1/8] CoordinateMetadata: add C++ class, WKT and PROJJSON support --- data/projjson.schema.json | 15 +- docs/source/specifications/projjson.rst | 2 +- include/proj/CMakeLists.txt | 2 +- include/proj/coordinates.hpp | 108 +++++++++++++ include/proj/internal/io_internal.hpp | 4 +- schemas/v0.6/projjson.schema.json | 15 +- scripts/reference_exported_symbols.txt | 6 + src/iso19111/coordinates.cpp | 197 ++++++++++++++++++++++++ src/iso19111/io.cpp | 64 ++++++++ src/iso19111/static.cpp | 2 + src/lib_proj.cmake | 1 + test/unit/CMakeLists.txt | 1 + test/unit/test_coordinates.cpp | 124 +++++++++++++++ 13 files changed, 536 insertions(+), 5 deletions(-) create mode 100644 include/proj/coordinates.hpp create mode 100644 src/iso19111/coordinates.cpp create mode 100644 test/unit/test_coordinates.cpp diff --git a/data/projjson.schema.json b/data/projjson.schema.json index cb96129700..e6125fe742 100644 --- a/data/projjson.schema.json +++ b/data/projjson.schema.json @@ -11,7 +11,8 @@ { "$ref": "#/definitions/ellipsoid" }, { "$ref": "#/definitions/prime_meridian" }, { "$ref": "#/definitions/single_operation" }, - { "$ref": "#/definitions/concatenated_operation" } + { "$ref": "#/definitions/concatenated_operation" }, + { "$ref": "#/definitions/coordinate_metadata" } ], "definitions": { @@ -208,6 +209,18 @@ "additionalProperties": false }, + "coordinate_metadata": { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["CoordinateMetadata"] }, + "crs": { "$ref": "#/definitions/crs" }, + "coordinateEpoch": { "type": "number" } + }, + "required" : [ "crs" ], + "additionalProperties": false + }, + "coordinate_system": { "type": "object", "properties": { diff --git a/docs/source/specifications/projjson.rst b/docs/source/specifications/projjson.rst index 15d25e679e..2ac511f572 100644 --- a/docs/source/specifications/projjson.rst +++ b/docs/source/specifications/projjson.rst @@ -51,7 +51,7 @@ in the WKT2:2019 specification also apply, as supplement to the JSON schema cons History of the schema --------------------- -* v0.6: additional optional "source_crs" property in "abridged_transformation". Implemented in PROJ 9.2 +* v0.6: additional optional "source_crs" property in "abridged_transformation". Added CoordinateMeta. Implemented in PROJ 9.2 * v0.5: - Implemented in PROJ 9.1: + add "meridian" member in Axis object type. diff --git a/include/proj/CMakeLists.txt b/include/proj/CMakeLists.txt index f9c727a028..bc95b916b8 100644 --- a/include/proj/CMakeLists.txt +++ b/include/proj/CMakeLists.txt @@ -1,5 +1,5 @@ install( - FILES util.hpp metadata.hpp common.hpp crs.hpp datum.hpp + FILES util.hpp metadata.hpp common.hpp coordinates.hpp crs.hpp datum.hpp coordinatesystem.hpp coordinateoperation.hpp io.hpp nn.hpp DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/proj ) diff --git a/include/proj/coordinates.hpp b/include/proj/coordinates.hpp new file mode 100644 index 0000000000..87ca479e3a --- /dev/null +++ b/include/proj/coordinates.hpp @@ -0,0 +1,108 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2023, 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 COORDINATES_HH_INCLUDED +#define COORDINATES_HH_INCLUDED + +#include + +#include "common.hpp" +#include "crs.hpp" +#include "io.hpp" +#include "util.hpp" + +NS_PROJ_START + +/** osgeo.proj.coordinates namespace + + \brief Coordinates package +*/ +namespace coordinates { + +class CoordinateMetadata; +/** Shared pointer of CoordinateMetadata */ +using CoordinateMetadataPtr = std::shared_ptr; +/** Non-null shared pointer of CoordinateMetadata */ +using CoordinateMetadataNNPtr = util::nn; + +// --------------------------------------------------------------------------- + +/** \brief Associates a CRS with a coordinate epoch. + * + * \remark Implements CoordinateMetadata from \ref ISO_19111_2019 + * \since 9.2 + */ + +class PROJ_GCC_DLL CoordinateMetadata : public util::BaseObject, + public io::IWKTExportable, + public io::IJSONExportable { + public: + //! @cond Doxygen_Suppress + PROJ_DLL ~CoordinateMetadata() override; + //! @endcond + + PROJ_DLL const crs::CRSNNPtr &crs() PROJ_PURE_DECL; + PROJ_DLL const util::optional & + coordinateEpoch() PROJ_PURE_DECL; + PROJ_DLL double coordinateEpochAsDecimalYear() PROJ_PURE_DECL; + + PROJ_DLL static CoordinateMetadataNNPtr create(const crs::CRSNNPtr &crsIn); + PROJ_DLL static CoordinateMetadataNNPtr + create(const crs::CRSNNPtr &crsIn, double coordinateEpochAsDecimalYear); + + PROJ_PRIVATE : + //! @cond Doxygen_Suppress + + PROJ_INTERNAL void + _exportToWKT(io::WKTFormatter *formatter) + const override; // throw(io::FormattingException) + + PROJ_INTERNAL void _exportToJSON(io::JSONFormatter *formatter) + const override; // throw(FormattingException) + + //! @endcond + + protected: + PROJ_INTERNAL explicit CoordinateMetadata(const crs::CRSNNPtr &crsIn); + PROJ_INTERNAL CoordinateMetadata(const crs::CRSNNPtr &crsIn, + double coordinateEpochAsDecimalYear); + + INLINED_MAKE_SHARED + + private: + PROJ_OPAQUE_PRIVATE_DATA + CoordinateMetadata &operator=(const CoordinateMetadata &other) = delete; +}; + +// --------------------------------------------------------------------------- + +} // namespace coordinates + +NS_PROJ_END + +#endif // COORDINATES_HH_INCLUDED diff --git a/include/proj/internal/io_internal.hpp b/include/proj/internal/io_internal.hpp index 2c18add0f4..0ec3f47b3a 100644 --- a/include/proj/internal/io_internal.hpp +++ b/include/proj/internal/io_internal.hpp @@ -136,7 +136,9 @@ class WKTConstants { static const std::string BASEPARAMCRS; static const std::string BASETIMECRS; static const std::string VERSION; - static const std::string GEOIDMODEL; // WKT2-2019 + static const std::string GEOIDMODEL; // WKT2-2019 + static const std::string COORDINATEMETADATA; // WKT2-2019 + static const std::string EPOCH; // WKT2-2019 // WKT2 alternate (longer or shorter) static const std::string GEODETICCRS; diff --git a/schemas/v0.6/projjson.schema.json b/schemas/v0.6/projjson.schema.json index cb96129700..e6125fe742 100644 --- a/schemas/v0.6/projjson.schema.json +++ b/schemas/v0.6/projjson.schema.json @@ -11,7 +11,8 @@ { "$ref": "#/definitions/ellipsoid" }, { "$ref": "#/definitions/prime_meridian" }, { "$ref": "#/definitions/single_operation" }, - { "$ref": "#/definitions/concatenated_operation" } + { "$ref": "#/definitions/concatenated_operation" }, + { "$ref": "#/definitions/coordinate_metadata" } ], "definitions": { @@ -208,6 +209,18 @@ "additionalProperties": false }, + "coordinate_metadata": { + "type": "object", + "properties": { + "$schema" : { "type": "string" }, + "type": { "type": "string", "enum": ["CoordinateMetadata"] }, + "crs": { "$ref": "#/definitions/crs" }, + "coordinateEpoch": { "type": "number" } + }, + "required" : [ "crs" ], + "additionalProperties": false + }, + "coordinate_system": { "type": "object", "properties": { diff --git a/scripts/reference_exported_symbols.txt b/scripts/reference_exported_symbols.txt index 37d9f65d39..db3515d073 100644 --- a/scripts/reference_exported_symbols.txt +++ b/scripts/reference_exported_symbols.txt @@ -92,6 +92,12 @@ osgeo::proj::common::UnitOfMeasure::type() const osgeo::proj::common::UnitOfMeasure::~UnitOfMeasure() osgeo::proj::common::UnitOfMeasure::UnitOfMeasure(osgeo::proj::common::UnitOfMeasure const&) osgeo::proj::common::UnitOfMeasure::UnitOfMeasure(std::string const&, double, osgeo::proj::common::UnitOfMeasure::Type, std::string const&, std::string const&) +osgeo::proj::coordinates::CoordinateMetadata::coordinateEpochAsDecimalYear() const +osgeo::proj::coordinates::CoordinateMetadata::coordinateEpoch() const +osgeo::proj::coordinates::CoordinateMetadata::~CoordinateMetadata() +osgeo::proj::coordinates::CoordinateMetadata::create(dropbox::oxygen::nn > const&) +osgeo::proj::coordinates::CoordinateMetadata::create(dropbox::oxygen::nn > const&, double) +osgeo::proj::coordinates::CoordinateMetadata::crs() const osgeo::proj::crs::BoundCRS::baseCRS() const osgeo::proj::crs::BoundCRS::baseCRSWithCanonicalBoundCRS() const osgeo::proj::crs::BoundCRS::~BoundCRS() diff --git a/src/iso19111/coordinates.cpp b/src/iso19111/coordinates.cpp new file mode 100644 index 0000000000..7c81cac14f --- /dev/null +++ b/src/iso19111/coordinates.cpp @@ -0,0 +1,197 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2023, 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/coordinates.hpp" +#include "proj/common.hpp" +#include "proj/crs.hpp" +#include "proj/io.hpp" + +#include "proj/internal/internal.hpp" +#include "proj/internal/io_internal.hpp" + +#include "proj_json_streaming_writer.hpp" + +#include + +using namespace NS_PROJ::internal; + +NS_PROJ_START + +namespace coordinates { + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +struct CoordinateMetadata::Private { + crs::CRSNNPtr crs_; + util::optional coordinateEpoch_{}; + + explicit Private(const crs::CRSNNPtr &crs) : crs_(crs) {} + Private(const crs::CRSNNPtr &crs, const common::DataEpoch &coordinateEpoch) + : crs_(crs), coordinateEpoch_(coordinateEpoch) {} +}; +//! @endcond + +// --------------------------------------------------------------------------- + +CoordinateMetadata::CoordinateMetadata(const crs::CRSNNPtr &crsIn) + : d(internal::make_unique(crsIn)) {} + +// --------------------------------------------------------------------------- + +CoordinateMetadata::CoordinateMetadata(const crs::CRSNNPtr &crsIn, + double coordinateEpochAsDecimalYearIn) + : d(internal::make_unique( + crsIn, + common::DataEpoch(common::Measure(coordinateEpochAsDecimalYearIn, + common::UnitOfMeasure::YEAR)))) {} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +CoordinateMetadata::~CoordinateMetadata() = default; +//! @endcond + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a CoordinateMetadata from a static CRS. + * @param crsIn a static CRS + * @return new CoordinateMetadata. + */ +CoordinateMetadataNNPtr CoordinateMetadata::create(const crs::CRSNNPtr &crsIn) { + auto coordinateMetadata( + CoordinateMetadata::nn_make_shared(crsIn)); + coordinateMetadata->assignSelf(coordinateMetadata); + return coordinateMetadata; +} + +// --------------------------------------------------------------------------- + +/** \brief Instantiate a CoordinateMetadata from a dyanmic CRS and an associated + * coordinate epoch. + * + * @param crsIn a dynamic CRS + * @param coordinateEpochIn coordinate epoch expressed in decimal year. + * @return new CoordinateMetadata. + */ +CoordinateMetadataNNPtr CoordinateMetadata::create(const crs::CRSNNPtr &crsIn, + double coordinateEpochIn) { + auto coordinateMetadata( + CoordinateMetadata::nn_make_shared( + crsIn, coordinateEpochIn)); + coordinateMetadata->assignSelf(coordinateMetadata); + return coordinateMetadata; +} + +// --------------------------------------------------------------------------- + +/** \brief Get the CRS associated with this CoordinateMetadata object. + */ +const crs::CRSNNPtr &CoordinateMetadata::crs() PROJ_PURE_DEFN { + return d->crs_; +} + +// --------------------------------------------------------------------------- + +/** \brief Get the coordinate epoch associated with this CoordinateMetadata + * object. + * + * The coordinate epoch is mandatory for a dynamic CRS, + * and forbidden for a static CRS. + */ +const util::optional & +CoordinateMetadata::coordinateEpoch() PROJ_PURE_DEFN { + return d->coordinateEpoch_; +} + +// --------------------------------------------------------------------------- + +/** \brief Get the coordinate epoch associated with this CoordinateMetadata + * object, as decimal year. + * + * The coordinate epoch is mandatory for a dynamic CRS, + * and forbidden for a static CRS. + */ +double CoordinateMetadata::coordinateEpochAsDecimalYear() PROJ_PURE_DEFN { + if (d->coordinateEpoch_.has_value()) { + return d->coordinateEpoch_->coordinateEpoch().convertToUnit( + common::UnitOfMeasure::YEAR); + } + return std::numeric_limits::quiet_NaN(); +} + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void CoordinateMetadata::_exportToWKT(io::WKTFormatter *formatter) const { + if (formatter->version() != io::WKTFormatter::Version::WKT2 || + !formatter->use2019Keywords()) { + io::FormattingException::Throw( + "CoordinateMetadata can only be exported since WKT2:2019"); + } + formatter->startNode(io::WKTConstants::COORDINATEMETADATA, false); + + crs()->_exportToWKT(formatter); + + if (d->coordinateEpoch_.has_value()) { + formatter->startNode(io::WKTConstants::EPOCH, false); + formatter->add(coordinateEpochAsDecimalYear()); + formatter->endNode(); + } + + formatter->endNode(); +} +//! @endcond + +// --------------------------------------------------------------------------- + +//! @cond Doxygen_Suppress +void CoordinateMetadata::_exportToJSON( + io::JSONFormatter *formatter) const // throw(io::FormattingException) +{ + auto writer = formatter->writer(); + auto objectContext( + formatter->MakeObjectContext("CoordinateMetadata", false)); + + writer->AddObjKey("crs"); + crs()->_exportToJSON(formatter); + + if (d->coordinateEpoch_.has_value()) { + writer->AddObjKey("coordinateEpoch"); + writer->Add(coordinateEpochAsDecimalYear()); + } +} +//! @endcond + +} // namespace coordinates + +NS_PROJ_END diff --git a/src/iso19111/io.cpp b/src/iso19111/io.cpp index 466c6fc6c2..49c5f68fe2 100644 --- a/src/iso19111/io.cpp +++ b/src/iso19111/io.cpp @@ -47,6 +47,7 @@ #include "proj/common.hpp" #include "proj/coordinateoperation.hpp" +#include "proj/coordinates.hpp" #include "proj/coordinatesystem.hpp" #include "proj/crs.hpp" #include "proj/datum.hpp" @@ -78,6 +79,7 @@ // clang-format on using namespace NS_PROJ::common; +using namespace NS_PROJ::coordinates; using namespace NS_PROJ::crs; using namespace NS_PROJ::cs; using namespace NS_PROJ::datum; @@ -1476,6 +1478,8 @@ struct WKTParser::Private { ConcatenatedOperationNNPtr buildConcatenatedOperation(const WKTNodeNNPtr &node); + + CoordinateMetadataNNPtr buildCoordinateMetadata(const WKTNodeNNPtr &node); }; //! @endcond @@ -5214,6 +5218,40 @@ WKTParser::Private::buildDerivedProjectedCRS(const WKTNodeNNPtr &node) { // --------------------------------------------------------------------------- +CoordinateMetadataNNPtr +WKTParser::Private::buildCoordinateMetadata(const WKTNodeNNPtr &node) { + const auto *nodeP = node->GP(); + + const auto &l_children = nodeP->children(); + if (l_children.empty()) { + ThrowNotEnoughChildren(WKTConstants::COORDINATEMETADATA); + } + + auto crs = buildCRS(l_children[0]); + if (!crs) { + throw ParsingException("Invalid content in CRS node"); + } + + auto &epochNode = nodeP->lookForChild(WKTConstants::EPOCH); + if (!isNull(epochNode)) { + const auto &epochChildren = epochNode->GP()->children(); + if (epochChildren.empty()) { + ThrowMissing(WKTConstants::EPOCH); + } + try { + const double coordinateEpoch = asDouble(epochChildren[0]); + return CoordinateMetadata::create(NN_NO_CHECK(crs), + coordinateEpoch); + } catch (const std::exception &) { + throw ParsingException("Invalid EPOCH node"); + } + } + + return CoordinateMetadata::create(NN_NO_CHECK(crs)); +} + +// --------------------------------------------------------------------------- + static bool isGeodeticCRS(const std::string &name) { return ci_equal(name, WKTConstants::GEODCRS) || // WKT2 ci_equal(name, WKTConstants::GEODETICCRS) || // WKT2 @@ -5444,6 +5482,11 @@ BaseObjectNNPtr WKTParser::Private::build(const WKTNodeNNPtr &node) { NN_NO_CHECK(buildId(node, false, false))); } + if (ci_equal(name, WKTConstants::COORDINATEMETADATA)) { + return util::nn_static_pointer_cast( + buildCoordinateMetadata(node)); + } + throw ParsingException(concat("unhandled keyword: ", name)); } @@ -5490,6 +5533,7 @@ class JSONParser { BoundCRSNNPtr buildBoundCRS(const json &j); TransformationNNPtr buildTransformation(const json &j); ConcatenatedOperationNNPtr buildConcatenatedOperation(const json &j); + CoordinateMetadataNNPtr buildCoordinateMetadata(const json &j); void buildGeodeticDatumOrDatumEnsemble(const json &j, GeodeticReferenceFramePtr &datum, @@ -6021,6 +6065,9 @@ BaseObjectNNPtr JSONParser::create(const json &j) if (type == "ConcatenatedOperation") { return buildConcatenatedOperation(j); } + if (type == "CoordinateMetadata") { + return buildCoordinateMetadata(j); + } if (type == "Axis") { return buildAxis(j); } @@ -6397,6 +6444,23 @@ JSONParser::buildConcatenatedOperation(const json &j) { // --------------------------------------------------------------------------- +CoordinateMetadataNNPtr JSONParser::buildCoordinateMetadata(const json &j) { + + auto crs = buildCRS(getObject(j, "crs")); + if (j.contains("coordinateEpoch")) { + auto jCoordinateEpoch = j["coordinateEpoch"]; + if (jCoordinateEpoch.is_number()) { + return CoordinateMetadata::create(crs, + jCoordinateEpoch.get()); + } + throw ParsingException( + "Unexpected type for value of \"coordinateEpoch\""); + } + return CoordinateMetadata::create(crs); +} + +// --------------------------------------------------------------------------- + MeridianNNPtr JSONParser::buildMeridian(const json &j) { if (!j.contains("longitude")) { throw ParsingException("Missing \"longitude\" key"); diff --git a/src/iso19111/static.cpp b/src/iso19111/static.cpp index cad8486aea..2957880d3b 100644 --- a/src/iso19111/static.cpp +++ b/src/iso19111/static.cpp @@ -278,6 +278,8 @@ DEFINE_WKT_CONSTANT(BASEPARAMCRS); DEFINE_WKT_CONSTANT(BASETIMECRS); DEFINE_WKT_CONSTANT(VERSION); DEFINE_WKT_CONSTANT(GEOIDMODEL); +DEFINE_WKT_CONSTANT(COORDINATEMETADATA); +DEFINE_WKT_CONSTANT(EPOCH); DEFINE_WKT_CONSTANT(GEODETICCRS); DEFINE_WKT_CONSTANT(GEODETICDATUM); diff --git a/src/lib_proj.cmake b/src/lib_proj.cmake index da23462f01..49992ae781 100644 --- a/src/lib_proj.cmake +++ b/src/lib_proj.cmake @@ -173,6 +173,7 @@ set(SRC_LIBPROJ_ISO19111 iso19111/util.cpp iso19111/metadata.cpp iso19111/common.cpp + iso19111/coordinates.cpp iso19111/crs.cpp iso19111/datum.cpp iso19111/coordinatesystem.cpp diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 6d9e56985a..e1221ef4eb 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -129,6 +129,7 @@ add_executable(proj_test_cpp_api main.cpp test_util.cpp test_common.cpp + test_coordinates.cpp test_crs.cpp test_metadata.cpp test_io.cpp diff --git a/test/unit/test_coordinates.cpp b/test/unit/test_coordinates.cpp new file mode 100644 index 0000000000..4bb363b423 --- /dev/null +++ b/test/unit/test_coordinates.cpp @@ -0,0 +1,124 @@ +/****************************************************************************** + * + * Project: PROJ + * Purpose: Test ISO19111:2019 implementation + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2023, 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" + +// to be able to use internal::replaceAll +#ifndef FROM_PROJ_CPP +#define FROM_PROJ_CPP +#endif + +#include "proj/common.hpp" +#include "proj/coordinates.hpp" +#include "proj/coordinatesystem.hpp" +#include "proj/crs.hpp" +#include "proj/datum.hpp" +#include "proj/io.hpp" +#include "proj/util.hpp" + +#include +#include + +using namespace osgeo::proj::common; +using namespace osgeo::proj::coordinates; +using namespace osgeo::proj::crs; +using namespace osgeo::proj::cs; +using namespace osgeo::proj::datum; +using namespace osgeo::proj::io; +using namespace osgeo::proj::util; + +// --------------------------------------------------------------------------- + +TEST(coordinateMetadata, static_crs) { + auto coordinateMetadata = + CoordinateMetadata::create(GeographicCRS::EPSG_4326); + EXPECT_TRUE(coordinateMetadata->crs()->isEquivalentTo( + GeographicCRS::EPSG_4326.get())); + EXPECT_FALSE(coordinateMetadata->coordinateEpoch().has_value()); + + WKTFormatterNNPtr f( + WKTFormatter::create(WKTFormatter::Convention::WKT2_2019)); + auto wkt = coordinateMetadata->exportToWKT(f.get()); + auto obj = WKTParser().createFromWKT(wkt); + auto coordinateMetadataFromWkt = + nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(coordinateMetadataFromWkt != nullptr); + EXPECT_TRUE(coordinateMetadataFromWkt->crs()->isEquivalentTo( + GeographicCRS::EPSG_4326.get())); + EXPECT_FALSE(coordinateMetadataFromWkt->coordinateEpoch().has_value()); + + auto projjson = + coordinateMetadata->exportToJSON(JSONFormatter::create(nullptr).get()); + auto obj2 = createFromUserInput(projjson, nullptr); + auto coordinateMetadataFromJson = + nn_dynamic_pointer_cast(obj2); + ASSERT_TRUE(coordinateMetadataFromJson != nullptr); + EXPECT_TRUE(coordinateMetadataFromJson->crs()->isEquivalentTo( + GeographicCRS::EPSG_4326.get())); + EXPECT_FALSE(coordinateMetadataFromJson->coordinateEpoch().has_value()); +} + +// --------------------------------------------------------------------------- + +TEST(coordinateMetadata, dynamic_crs) { + auto drf = DynamicGeodeticReferenceFrame::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "test"), Ellipsoid::WGS84, + optional("My anchor"), PrimeMeridian::GREENWICH, + Measure(2018.5, UnitOfMeasure::YEAR), + optional("My model")); + auto crs = GeographicCRS::create( + PropertyMap(), drf, + EllipsoidalCS::createLatitudeLongitude(UnitOfMeasure::DEGREE)); + auto coordinateMetadata = CoordinateMetadata::create(crs, 2023.5); + EXPECT_TRUE(coordinateMetadata->crs()->isEquivalentTo(crs.get())); + EXPECT_TRUE(coordinateMetadata->coordinateEpoch().has_value()); + EXPECT_NEAR(coordinateMetadata->coordinateEpochAsDecimalYear(), 2023.5, + 1e-10); + + WKTFormatterNNPtr f( + WKTFormatter::create(WKTFormatter::Convention::WKT2_2019)); + auto wkt = coordinateMetadata->exportToWKT(f.get()); + auto obj = WKTParser().createFromWKT(wkt); + auto coordinateMetadataFromWkt = + nn_dynamic_pointer_cast(obj); + EXPECT_TRUE(coordinateMetadataFromWkt->crs()->isEquivalentTo(crs.get())); + EXPECT_TRUE(coordinateMetadataFromWkt->coordinateEpoch().has_value()); + EXPECT_NEAR(coordinateMetadataFromWkt->coordinateEpochAsDecimalYear(), + 2023.5, 1e-10); + + auto projjson = + coordinateMetadata->exportToJSON(JSONFormatter::create(nullptr).get()); + auto obj2 = createFromUserInput(projjson, nullptr); + auto coordinateMetadataFromJson = + nn_dynamic_pointer_cast(obj2); + EXPECT_TRUE(coordinateMetadataFromJson->crs()->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_TRUE(coordinateMetadataFromJson->coordinateEpoch().has_value()); + EXPECT_NEAR(coordinateMetadataFromJson->coordinateEpochAsDecimalYear(), + 2023.5, 1e-10); +} From 540effbc2f579c06b3fb4c396d8d91491d354597 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 14 Jan 2023 18:43:23 +0100 Subject: [PATCH 2/8] JSON/WKT import: test invalid CoordinateMetadata --- test/unit/test_io.cpp | 134 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/test/unit/test_io.cpp b/test/unit/test_io.cpp index 59ad3d9386..48b4503571 100644 --- a/test/unit/test_io.cpp +++ b/test/unit/test_io.cpp @@ -8495,6 +8495,73 @@ TEST(wkt_parse, invalid_DerivedTemporalCRS) { // --------------------------------------------------------------------------- +TEST(wkt_parse, invalid_CoordinateMetadata) { + EXPECT_THROW(WKTParser().createFromWKT("COORDINATEMETADATA[]"), + ParsingException); + + EXPECT_THROW(WKTParser().createFromWKT("COORDINATEMETADATA[ELLIPSOID[\"GRS " + "1980\",6378137,298.257222101]]"), + ParsingException); + + // Empty epoch + EXPECT_THROW( + WKTParser().createFromWKT( + "COORDINATEMETADATA[\n" + " GEOGCRS[\"ITRF2014\",\n" + " DYNAMIC[\n" + " FRAMEEPOCH[2010]],\n" + " DATUM[\"International Terrestrial Reference Frame " + "2014\",\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" + " USAGE[\n" + " SCOPE[\"Geodesy.\"],\n" + " AREA[\"World.\"],\n" + " BBOX[-90,-180,90,180]],\n" + " ID[\"EPSG\",9000]],\n" + " EPOCH[]]"), + ParsingException); + + // Invalid epoch + EXPECT_THROW( + WKTParser().createFromWKT( + "COORDINATEMETADATA[\n" + " GEOGCRS[\"ITRF2014\",\n" + " DYNAMIC[\n" + " FRAMEEPOCH[2010]],\n" + " DATUM[\"International Terrestrial Reference Frame " + "2014\",\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" + " USAGE[\n" + " SCOPE[\"Geodesy.\"],\n" + " AREA[\"World.\"],\n" + " BBOX[-90,-180,90,180]],\n" + " ID[\"EPSG\",9000]],\n" + " EPOCH[invalid]]"), + ParsingException); +} + +// --------------------------------------------------------------------------- + TEST(io, projstringformatter) { { @@ -15814,6 +15881,73 @@ TEST(json_export, coordinate_system_id) { // --------------------------------------------------------------------------- +TEST(json_import, invalid_CoordinateMetadata) { + { + auto json = "{\n" + " \"$schema\": \"foo\",\n" + " \"type\": \"CoordinateMetadata\"\n" + "}"; + EXPECT_THROW(createFromUserInput(json, nullptr), ParsingException); + } + + { + auto json = "{\n" + " \"$schema\": \"foo\",\n" + " \"type\": \"CoordinateMetadata\",\n" + " \"crs\": \"not quite a CRS...\"\n" + "}"; + EXPECT_THROW(createFromUserInput(json, nullptr), ParsingException); + } + + { + auto json = "{\n" + " \"$schema\": " + "\"https://proj.org/schemas/v0.6/projjson.schema.json\",\n" + " \"type\": \"CoordinateMetadata\",\n" + " \"crs\": {\n" + " \"type\": \"GeographicCRS\",\n" + " \"name\": \"ITRF2014\",\n" + " \"datum\": {\n" + " \"type\": \"DynamicGeodeticReferenceFrame\",\n" + " \"name\": \"International Terrestrial Reference " + "Frame 2014\",\n" + " \"frame_reference_epoch\": 2010,\n" + " \"ellipsoid\": {\n" + " \"name\": \"GRS 1980\",\n" + " \"semi_major_axis\": 6378137,\n" + " \"inverse_flattening\": 298.257222101\n" + " }\n" + " },\n" + " \"coordinate_system\": {\n" + " \"subtype\": \"ellipsoidal\",\n" + " \"axis\": [\n" + " {\n" + " \"name\": \"Geodetic latitude\",\n" + " \"abbreviation\": \"Lat\",\n" + " \"direction\": \"north\",\n" + " \"unit\": \"degree\"\n" + " },\n" + " {\n" + " \"name\": \"Geodetic longitude\",\n" + " \"abbreviation\": \"Lon\",\n" + " \"direction\": \"east\",\n" + " \"unit\": \"degree\"\n" + " }\n" + " ]\n" + " },\n" + " \"id\": {\n" + " \"authority\": \"EPSG\",\n" + " \"code\": 9000\n" + " }\n" + " },\n" + " \"coordinateEpoch\": \"this should be a number\"\n" + "}"; + EXPECT_THROW(createFromUserInput(json, nullptr), ParsingException); + } +} + +// --------------------------------------------------------------------------- + TEST(io, EXTENSION_PROJ4) { // Check that the PROJ string is preserved in the remarks auto obj = PROJStringParser().createFromPROJString( From a62cd93ae0f9bc1d0686030c9f0b7c5c3c83e3c2 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 14 Jan 2023 15:44:19 +0100 Subject: [PATCH 3/8] C API: add PJ_TYPE_COORDINATE_METADATA, proj_coordinate_metadata_get_epoch(), support if in proj_create() and proj_get_source_crs() --- scripts/reference_exported_symbols.txt | 1 + src/4D_api.cpp | 8 +- src/iso19111/c_api.cpp | 117 ++++++++++++++++++++----- src/iso19111/operation/conversion.cpp | 7 +- src/proj.h | 4 + src/proj_internal.h | 2 +- src/proj_symbol_rename.h | 1 + test/unit/test_coordinates.cpp | 44 ++++++++++ 8 files changed, 154 insertions(+), 30 deletions(-) diff --git a/scripts/reference_exported_symbols.txt b/scripts/reference_exported_symbols.txt index db3515d073..0d20db1d21 100644 --- a/scripts/reference_exported_symbols.txt +++ b/scripts/reference_exported_symbols.txt @@ -843,6 +843,7 @@ proj_context_use_proj4_init_rules proj_convert_conversion_to_other_method proj_coord proj_coord_error() +proj_coordinate_metadata_get_epoch proj_coordoperation_create_inverse proj_coordoperation_get_accuracy proj_coordoperation_get_grid_used diff --git a/src/4D_api.cpp b/src/4D_api.cpp index 8d9a8512c7..0980316a97 100644 --- a/src/4D_api.cpp +++ b/src/4D_api.cpp @@ -2369,10 +2369,12 @@ PJ_PROJ_INFO proj_pj_info(PJ *P) { if (pj_param(P->ctx, P->params, "tproj").i) pjinfo.id = pj_param(P->ctx, P->params, "sproj").s; + pjinfo.description = P->descr; if( P->iso_obj ) { - pjinfo.description = P->iso_obj->nameStr().c_str(); - } else { - pjinfo.description = P->descr; + auto identifiedObj = dynamic_cast(P->iso_obj.get()); + if( identifiedObj ) { + pjinfo.description = identifiedObj->nameStr().c_str(); + } } // accuracy diff --git a/src/iso19111/c_api.cpp b/src/iso19111/c_api.cpp index 2f408cb2c1..56e21487b5 100644 --- a/src/iso19111/c_api.cpp +++ b/src/iso19111/c_api.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,7 @@ #include "proj/common.hpp" #include "proj/coordinateoperation.hpp" +#include "proj/coordinates.hpp" #include "proj/coordinatesystem.hpp" #include "proj/crs.hpp" #include "proj/datum.hpp" @@ -62,6 +64,7 @@ #include "geodesic.h" using namespace NS_PROJ::common; +using namespace NS_PROJ::coordinates; using namespace NS_PROJ::crs; using namespace NS_PROJ::cs; using namespace NS_PROJ::datum; @@ -187,7 +190,7 @@ getDBcontextNoException(PJ_CONTEXT *ctx, const char *function) { } // --------------------------------------------------------------------------- -static PJ *pj_obj_create(PJ_CONTEXT *ctx, const IdentifiedObjectNNPtr &objIn) { +static PJ *pj_obj_create(PJ_CONTEXT *ctx, const BaseObjectNNPtr &objIn) { auto coordop = dynamic_cast(objIn.get()); if (coordop) { auto dbContext = getDBcontextNoException(ctx, __FUNCTION__); @@ -572,10 +575,10 @@ PJ *proj_create(PJ_CONTEXT *ctx, const char *text) { getDBcontextNoException(ctx, __FUNCTION__); } try { - auto identifiedObject = nn_dynamic_pointer_cast( - createFromUserInput(text, ctx)); - if (identifiedObject) { - return pj_obj_create(ctx, NN_NO_CHECK(identifiedObject)); + auto obj = + nn_dynamic_pointer_cast(createFromUserInput(text, ctx)); + if (obj) { + return pj_obj_create(ctx, NN_NO_CHECK(obj)); } } catch (const io::ParsingException &e) { if (proj_context_errno(ctx) == 0) { @@ -1100,6 +1103,10 @@ convertPJObjectTypeToObjectType(PJ_TYPE type, bool &valid) { case PJ_TYPE_UNKNOWN: valid = false; break; + + case PJ_TYPE_COORDINATE_METADATA: + valid = false; + break; } return cppType; } @@ -1264,6 +1271,10 @@ PJ_TYPE proj_get_type(const PJ *obj) { return PJ_TYPE_OTHER_COORDINATE_OPERATION; } + if (dynamic_cast(ptr)) { + return PJ_TYPE_COORDINATE_METADATA; + } + return PJ_TYPE_UNKNOWN; }; @@ -1279,10 +1290,14 @@ PJ_TYPE proj_get_type(const PJ *obj) { * @return TRUE if it is deprecated, FALSE otherwise */ int proj_is_deprecated(const PJ *obj) { - if (!obj || !obj->iso_obj) { + if (!obj) { return false; } - return obj->iso_obj->isDeprecated(); + auto identifiedObj = dynamic_cast(obj->iso_obj.get()); + if (!identifiedObj) { + return false; + } + return identifiedObj->isDeprecated(); } // --------------------------------------------------------------------------- @@ -1347,7 +1362,13 @@ static int proj_is_equivalent_to_internal(PJ_CONTEXT *ctx, const PJ *obj, return true; } - if (!obj->iso_obj || !other->iso_obj) { + auto identifiedObj = dynamic_cast(obj->iso_obj.get()); + if (!identifiedObj) { + return false; + } + auto otherIdentifiedObj = + dynamic_cast(other->iso_obj.get()); + if (!otherIdentifiedObj) { return false; } const auto cppCriterion = ([](PJ_COMPARISON_CRITERION l_criterion) { @@ -1362,8 +1383,8 @@ static int proj_is_equivalent_to_internal(PJ_CONTEXT *ctx, const PJ *obj, return IComparable::Criterion::EQUIVALENT_EXCEPT_AXIS_ORDER_GEOGCRS; })(criterion); - int res = obj->iso_obj->isEquivalentTo( - other->iso_obj.get(), cppCriterion, + int res = identifiedObj->isEquivalentTo( + otherIdentifiedObj, cppCriterion, ctx ? getDBcontextNoException(ctx, "proj_is_equivalent_to_with_ctx") : nullptr); return res; @@ -1428,10 +1449,14 @@ int proj_is_crs(const PJ *obj) { * @return a string, or NULL in case of error or missing name. */ const char *proj_get_name(const PJ *obj) { - if (!obj || !obj->iso_obj) { + if (!obj) { return nullptr; } - const auto &desc = obj->iso_obj->name()->description(); + auto identifiedObj = dynamic_cast(obj->iso_obj.get()); + if (!identifiedObj) { + return nullptr; + } + const auto &desc = identifiedObj->name()->description(); if (!desc.has_value()) { return nullptr; } @@ -1450,12 +1475,16 @@ const char *proj_get_name(const PJ *obj) { * @return a string, or NULL in case of error. */ const char *proj_get_remarks(const PJ *obj) { - if (!obj || !obj->iso_obj) { + if (!obj) { + return nullptr; + } + auto identifiedObj = dynamic_cast(obj->iso_obj.get()); + if (!identifiedObj) { return nullptr; } // The object will still be alive after the function call. // cppcheck-suppress stlcstr - return obj->iso_obj->remarks().c_str(); + return identifiedObj->remarks().c_str(); } // --------------------------------------------------------------------------- @@ -1469,10 +1498,14 @@ const char *proj_get_remarks(const PJ *obj) { * @return a string, or NULL in case of error or missing name. */ const char *proj_get_id_auth_name(const PJ *obj, int index) { - if (!obj || !obj->iso_obj) { + if (!obj) { + return nullptr; + } + auto identifiedObj = dynamic_cast(obj->iso_obj.get()); + if (!identifiedObj) { return nullptr; } - const auto &ids = obj->iso_obj->identifiers(); + const auto &ids = identifiedObj->identifiers(); if (static_cast(index) >= ids.size()) { return nullptr; } @@ -1496,10 +1529,14 @@ const char *proj_get_id_auth_name(const PJ *obj, int index) { * @return a string, or NULL in case of error or missing name. */ const char *proj_get_id_code(const PJ *obj, int index) { - if (!obj || !obj->iso_obj) { + if (!obj) { return nullptr; } - const auto &ids = obj->iso_obj->identifiers(); + auto identifiedObj = dynamic_cast(obj->iso_obj.get()); + if (!identifiedObj) { + return nullptr; + } + const auto &ids = identifiedObj->identifiers(); if (static_cast(index) >= ids.size()) { return nullptr; } @@ -1552,7 +1589,8 @@ const char *proj_as_wkt(PJ_CONTEXT *ctx, const PJ *obj, PJ_WKT_TYPE type, proj_log_error(ctx, __FUNCTION__, "missing required input"); return nullptr; } - if (!obj->iso_obj) { + auto iWKTExportable = dynamic_cast(obj->iso_obj.get()); + if (!iWKTExportable) { return nullptr; } @@ -1606,7 +1644,7 @@ const char *proj_as_wkt(PJ_CONTEXT *ctx, const PJ *obj, PJ_WKT_TYPE type, return nullptr; } } - obj->lastWKT = obj->iso_obj->exportToWKT(formatter.get()); + obj->lastWKT = iWKTExportable->exportToWKT(formatter.get()); return obj->lastWKT.c_str(); } catch (const std::exception &e) { proj_log_error(ctx, __FUNCTION__, e.what()); @@ -2217,7 +2255,7 @@ PJ *proj_get_ellipsoid(PJ_CONTEXT *ctx, const PJ *obj) { */ const char *proj_get_celestial_body_name(PJ_CONTEXT *ctx, const PJ *obj) { SANITIZE_CTX(ctx); - const IdentifiedObject *ptr = obj->iso_obj.get(); + const BaseObject *ptr = obj->iso_obj.get(); if (dynamic_cast(ptr)) { const auto geodCRS = extractGeodeticCRS(ctx, obj, __FUNCTION__); if (!geodCRS) { @@ -2421,7 +2459,7 @@ int proj_prime_meridian_get_parameters(PJ_CONTEXT *ctx, // --------------------------------------------------------------------------- /** \brief Return the base CRS of a BoundCRS or a DerivedCRS/ProjectedCRS, or - * the source CRS of a CoordinateOperation. + * the source CRS of a CoordinateOperation, or the CRS of a CoordinateMetadata. * * The returned object must be unreferenced with proj_destroy() after * use. @@ -2458,8 +2496,14 @@ PJ *proj_get_source_crs(PJ_CONTEXT *ctx, const PJ *obj) { return proj_get_source_crs(ctx, obj->alternativeCoordinateOperations[0].pj); } + auto coordinateMetadata = dynamic_cast(ptr); + if (coordinateMetadata) { + return pj_obj_create(ctx, coordinateMetadata->crs()); + } + proj_log_error(ctx, __FUNCTION__, - "Object is not a BoundCRS or a CoordinateOperation"); + "Object is not a BoundCRS, a CoordinateOperation or a " + "CoordinateMetadata"); return nullptr; } @@ -9295,3 +9339,30 @@ proj_get_geoid_models_from_database(PJ_CONTEXT *ctx, const char *auth_name, } // --------------------------------------------------------------------------- + +/** \brief Return the coordinate epoch associated with a CoordinateMetadata. + * + * It may return a NaN value if there is no associated coordinate epoch. + * + * @since 9.2 + */ +double proj_coordinate_metadata_get_epoch(PJ_CONTEXT *ctx, const PJ *obj) { + SANITIZE_CTX(ctx); + if (!obj) { + proj_context_errno_set(ctx, PROJ_ERR_OTHER_API_MISUSE); + proj_log_error(ctx, __FUNCTION__, "missing required input"); + return std::numeric_limits::quiet_NaN(); + } + auto ptr = obj->iso_obj.get(); + auto coordinateMetadata = dynamic_cast(ptr); + if (coordinateMetadata) { + if (coordinateMetadata->coordinateEpoch().has_value()) { + return coordinateMetadata->coordinateEpochAsDecimalYear(); + } + return std::numeric_limits::quiet_NaN(); + } + proj_log_error(ctx, __FUNCTION__, "Object is not a CoordinateMetadata"); + return std::numeric_limits::quiet_NaN(); +} + +// --------------------------------------------------------------------------- diff --git a/src/iso19111/operation/conversion.cpp b/src/iso19111/operation/conversion.cpp index 62353923d0..62c64a6900 100644 --- a/src/iso19111/operation/conversion.cpp +++ b/src/iso19111/operation/conversion.cpp @@ -1629,9 +1629,10 @@ ConversionNNPtr Conversion::createMercatorVariantA( * * Mercator (variant B) projection method. * - * This is the B 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 is the B 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 * diff --git a/src/proj.h b/src/proj.h index 25828f9e9c..d5788abd62 100644 --- a/src/proj.h +++ b/src/proj.h @@ -805,6 +805,8 @@ typedef enum PJ_TYPE_PARAMETRIC_DATUM, PJ_TYPE_DERIVED_PROJECTED_CRS, + + PJ_TYPE_COORDINATE_METADATA, } PJ_TYPE; /** Comparison criterion. */ @@ -1515,6 +1517,8 @@ PJ PROJ_DLL *proj_concatoperation_get_step(PJ_CONTEXT *ctx, const PJ *concatoperation, int i_step); +double PROJ_DLL proj_coordinate_metadata_get_epoch(PJ_CONTEXT *ctx, const PJ *obj); + /**@}*/ #ifdef __cplusplus diff --git a/src/proj_internal.h b/src/proj_internal.h index 33253ac908..0f37496bcc 100644 --- a/src/proj_internal.h +++ b/src/proj_internal.h @@ -576,7 +576,7 @@ struct PJconsts { ISO-19111 interface **************************************************************************************/ - NS_PROJ::common::IdentifiedObjectPtr iso_obj{}; + NS_PROJ::util::BaseObjectPtr iso_obj{}; bool iso_obj_is_coordinate_operation = false; // cached results diff --git a/src/proj_symbol_rename.h b/src/proj_symbol_rename.h index 184f3b8a13..e1bf8f6fd9 100644 --- a/src/proj_symbol_rename.h +++ b/src/proj_symbol_rename.h @@ -65,6 +65,7 @@ #define proj_context_use_proj4_init_rules internal_proj_context_use_proj4_init_rules #define proj_convert_conversion_to_other_method internal_proj_convert_conversion_to_other_method #define proj_coord internal_proj_coord +#define proj_coordinate_metadata_get_epoch internal_proj_coordinate_metadata_get_epoch #define proj_coordoperation_create_inverse internal_proj_coordoperation_create_inverse #define proj_coordoperation_get_accuracy internal_proj_coordoperation_get_accuracy #define proj_coordoperation_get_grid_used internal_proj_coordoperation_get_grid_used diff --git a/test/unit/test_coordinates.cpp b/test/unit/test_coordinates.cpp index 4bb363b423..9af4ef1ce2 100644 --- a/test/unit/test_coordinates.cpp +++ b/test/unit/test_coordinates.cpp @@ -41,6 +41,7 @@ #include "proj/io.hpp" #include "proj/util.hpp" +#include #include #include @@ -52,6 +53,30 @@ using namespace osgeo::proj::datum; using namespace osgeo::proj::io; using namespace osgeo::proj::util; +namespace { +struct ObjectKeeper { + PJ *m_obj = nullptr; + explicit ObjectKeeper(PJ *obj) : m_obj(obj) {} + ~ObjectKeeper() { proj_destroy(m_obj); } + void clear() { + proj_destroy(m_obj); + m_obj = nullptr; + } + + ObjectKeeper(const ObjectKeeper &) = delete; + ObjectKeeper &operator=(const ObjectKeeper &) = delete; +}; + +struct PjContextKeeper { + PJ_CONTEXT *m_ctxt = nullptr; + explicit PjContextKeeper(PJ_CONTEXT *ctxt) : m_ctxt(ctxt) {} + ~PjContextKeeper() { proj_context_destroy(m_ctxt); } + + PjContextKeeper(const PjContextKeeper &) = delete; + PjContextKeeper &operator=(const PjContextKeeper &) = delete; +}; +} // namespace + // --------------------------------------------------------------------------- TEST(coordinateMetadata, static_crs) { @@ -72,6 +97,17 @@ TEST(coordinateMetadata, static_crs) { GeographicCRS::EPSG_4326.get())); EXPECT_FALSE(coordinateMetadataFromWkt->coordinateEpoch().has_value()); + auto ctxt = proj_context_create(); + PjContextKeeper ctxtKeeper(ctxt); + auto pjObj = proj_create(ctxt, wkt.c_str()); + ObjectKeeper objKeeper(pjObj); + ASSERT_TRUE(pjObj != nullptr); + EXPECT_EQ(proj_get_type(pjObj), PJ_TYPE_COORDINATE_METADATA); + EXPECT_TRUE(std::isnan(proj_coordinate_metadata_get_epoch(ctxt, pjObj))); + auto pjObj2 = proj_get_source_crs(ctxt, pjObj); + ObjectKeeper objKeeper2(pjObj2); + EXPECT_TRUE(pjObj2 != nullptr); + auto projjson = coordinateMetadata->exportToJSON(JSONFormatter::create(nullptr).get()); auto obj2 = createFromUserInput(projjson, nullptr); @@ -111,6 +147,14 @@ TEST(coordinateMetadata, dynamic_crs) { EXPECT_NEAR(coordinateMetadataFromWkt->coordinateEpochAsDecimalYear(), 2023.5, 1e-10); + auto ctxt = proj_context_create(); + PjContextKeeper ctxtKeeper(ctxt); + auto pjObj = proj_create(ctxt, wkt.c_str()); + ObjectKeeper objKeeper(pjObj); + ASSERT_TRUE(pjObj != nullptr); + EXPECT_EQ(proj_get_type(pjObj), PJ_TYPE_COORDINATE_METADATA); + EXPECT_NEAR(proj_coordinate_metadata_get_epoch(ctxt, pjObj), 2023.5, 1e-10); + auto projjson = coordinateMetadata->exportToJSON(JSONFormatter::create(nullptr).get()); auto obj2 = createFromUserInput(projjson, nullptr); From 6ad88abbde81e70e0d7f9d38be6e854577ff4d3f Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 14 Jan 2023 18:25:09 +0100 Subject: [PATCH 4/8] createFromUserInput(): accept 'CRS_name@decimal_year' syntax to instanciate a CoordinateMetadata --- docs/source/apps/cs2cs.rst | 1 + docs/source/apps/projinfo.rst | 1 + src/iso19111/io.cpp | 28 +++++++++++++++++++++++++++- test/unit/test_io.cpp | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/docs/source/apps/cs2cs.rst b/docs/source/apps/cs2cs.rst index 86db28c3c2..16de83e147 100644 --- a/docs/source/apps/cs2cs.rst +++ b/docs/source/apps/cs2cs.rst @@ -27,6 +27,7 @@ Synopsis "urn:ogc:def:coordinateOperation:EPSG::1671"), - an Object name. e.g "WGS 84", "WGS 84 / UTM zone 31N". In that case as uniqueness is not guaranteed, heuristics are applied to determine the appropriate best match. + - a CRS name and a coordinate epoch, separated with '@'. For example "ITRF2014@2025.0". (*added in 9.2*) - a OGC URN combining references for compound coordinate reference systems (e.g "urn:ogc:def:crs,crs:EPSG::2393,crs:EPSG::5717" or custom abbreviated syntax "EPSG:2393+5717"), diff --git a/docs/source/apps/projinfo.rst b/docs/source/apps/projinfo.rst index e68cf26e7d..a351892961 100644 --- a/docs/source/apps/projinfo.rst +++ b/docs/source/apps/projinfo.rst @@ -46,6 +46,7 @@ Synopsis "urn:ogc:def:coordinateOperation:EPSG::1671"), - an Object name. e.g "WGS 84", "WGS 84 / UTM zone 31N". In that case as uniqueness is not guaranteed, heuristics are applied to determine the appropriate best match. + - a CRS name and a coordinate epoch, separated with '@'. For example "ITRF2014@2025.0". (*added in 9.2*) - a OGC URN combining references for compound coordinate reference systems (e.g "urn:ogc:def:crs,crs:EPSG::2393,crs:EPSG::5717" or custom abbreviated syntax "EPSG:2393+5717"), diff --git a/src/iso19111/io.cpp b/src/iso19111/io.cpp index 49c5f68fe2..7fcdc37b0d 100644 --- a/src/iso19111/io.cpp +++ b/src/iso19111/io.cpp @@ -7535,6 +7535,29 @@ static BaseObjectNNPtr createFromUserInput(const std::string &text, } } + // Parse strings like "ITRF2014 @ 2025.0" + const auto posAt = text.find('@'); + if (posAt != std::string::npos) { + std::string leftPart = text.substr(0, posAt); + while (!leftPart.empty() && leftPart.back() == ' ') + leftPart.resize(leftPart.size() - 1); + const auto nonSpacePos = text.find_first_not_of(' ', posAt + 1); + if (nonSpacePos != std::string::npos) { + auto obj = createFromUserInput(leftPart, dbContext, + usePROJ4InitRules, ctx); + auto crs = nn_dynamic_pointer_cast(obj); + if (crs) { + try { + const double epoch = + c_locale_stod(text.substr(nonSpacePos)); + return CoordinateMetadata::create(NN_NO_CHECK(crs), epoch); + } catch (const std::exception &) { + throw ParsingException("non-numeric value after @"); + } + } + } + } + throw ParsingException("unrecognized format / unknown name"); } //! @endcond @@ -7566,12 +7589,15 @@ static BaseObjectNNPtr createFromUserInput(const std::string &text, * e.g. * "urn:ogc:def:coordinateOperation,coordinateOperation:EPSG::3895,coordinateOperation:EPSG::1618" *
  • OGC URL for a single CRS. e.g. - * "http://www.opengis.net/def/crs/EPSG/0/4326
  • OGC URL for a compound + * "http://www.opengis.net/def/crs/EPSG/0/4326"
  • + *
  • OGC URL for a compound * CRS. e.g * "http://www.opengis.net/def/crs-compound?1=http://www.opengis.net/def/crs/EPSG/0/4326&2=http://www.opengis.net/def/crs/EPSG/0/3855"
  • *
  • an Object name. e.g "WGS 84", "WGS 84 / UTM zone 31N". In that case as * uniqueness is not guaranteed, the function may apply heuristics to * determine the appropriate best match.
  • + *
  • a CRS name and a coordinate epoch, separated with '@'. For example + * "ITRF2014@2025.0". (added in PROJ 9.2)
  • *
  • a compound CRS made from two object names separated with " + ". * e.g. "WGS 84 + EGM96 height"
  • *
  • PROJJSON string
  • diff --git a/test/unit/test_io.cpp b/test/unit/test_io.cpp index 48b4503571..a8c32133c2 100644 --- a/test/unit/test_io.cpp +++ b/test/unit/test_io.cpp @@ -33,6 +33,7 @@ #include "proj/common.hpp" #include "proj/coordinateoperation.hpp" +#include "proj/coordinates.hpp" #include "proj/coordinatesystem.hpp" #include "proj/crs.hpp" #include "proj/datum.hpp" @@ -47,6 +48,7 @@ #include using namespace osgeo::proj::common; +using namespace osgeo::proj::coordinates; using namespace osgeo::proj::crs; using namespace osgeo::proj::cs; using namespace osgeo::proj::datum; @@ -12105,6 +12107,39 @@ TEST(io, createFromUserInput) { // Missing space, dash: OK EXPECT_NO_THROW(createFromUserInput("WGS84 PseudoMercator", dbContext)); + + // Invalid CoordinateMetadata + EXPECT_THROW(createFromUserInput("@", dbContext), ParsingException); + + // Invalid CoordinateMetadata + EXPECT_THROW(createFromUserInput("ITRF2014@", dbContext), ParsingException); + + // Invalid CoordinateMetadata + EXPECT_THROW(createFromUserInput("ITRF2014@foo", dbContext), + ParsingException); + + // Invalid CoordinateMetadata + EXPECT_THROW(createFromUserInput("foo@2025", dbContext), ParsingException); + + // Invalid CoordinateMetadata + EXPECT_THROW(createFromUserInput("@2025", dbContext), ParsingException); + + { + auto obj = createFromUserInput("ITRF2014@2025.1", dbContext); + auto coordinateMetadata = + nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(coordinateMetadata != nullptr); + EXPECT_EQ(coordinateMetadata->coordinateEpochAsDecimalYear(), 2025.1); + } + + { + // Allow spaces before and after @ + auto obj = createFromUserInput("ITRF2014 @ 2025.1", dbContext); + auto coordinateMetadata = + nn_dynamic_pointer_cast(obj); + ASSERT_TRUE(coordinateMetadata != nullptr); + EXPECT_EQ(coordinateMetadata->coordinateEpochAsDecimalYear(), 2025.1); + } } // --------------------------------------------------------------------------- From 76959be8fb20439c606cdaf6c87c47dfbf1b8249 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 14 Jan 2023 17:53:27 +0100 Subject: [PATCH 5/8] createOperations(): support one of source/target to be a CoordinateMetadata but not both. Note also that the core of createOperations() does not take into account the source/target coordinate epoch in its logic to infer pipelines. It just keep those values in its metadata, so that PJ* objects constructed from them can later override the PJ_COORD.xyzt.t component. So basically this is currently useful only for transformations between a dynamic CRS and a static CRS (or the reverse). --- .../development/reference/functions.rst | 10 +- include/proj/coordinateoperation.hpp | 37 +++++ scripts/reference_exported_symbols.txt | 7 + src/4D_api.cpp | 24 ++++ src/apps/projinfo.cpp | 66 +++++++-- src/iso19111/c_api.cpp | 66 ++++++++- .../operation/coordinateoperation_private.hpp | 6 +- .../operation/coordinateoperationfactory.cpp | 134 +++++++++++++++++- src/iso19111/operation/singleoperation.cpp | 20 ++- src/proj_internal.h | 2 + test/unit/test_c_api.cpp | 40 ++++++ 11 files changed, 385 insertions(+), 27 deletions(-) diff --git a/docs/source/development/reference/functions.rst b/docs/source/development/reference/functions.rst index 2bce1432f2..706dbd56df 100644 --- a/docs/source/development/reference/functions.rst +++ b/docs/source/development/reference/functions.rst @@ -139,6 +139,9 @@ paragraph for more details. - more generally any string accepted by :c:func:`proj_create` representing a CRS + Starting with PROJ 9.2, source_crs or target_crs can be a CoordinateMetadata + with an associated coordinate epoch (but only one of them, not both). + An "area of use" can be specified in area. When it is supplied, the more accurate transformation between two given systems can be chosen. @@ -164,9 +167,9 @@ paragraph for more details. :param ctx: Threading context. :type ctx: :c:type:`PJ_CONTEXT` * - :param `source_crs`: Source CRS. + :param `source_crs`: Source CRS or CoordinateMetadata. :type `source_crs`: `const char*` - :param `target_crs`: Destination SRS. + :param `target_crs`: Destination SRS or CoordinateMetadata :type `target_crs`: `const char*` :param `area`: Descriptor of the desired area for the transformation. :type `area`: :c:type:`PJ_AREA` * @@ -182,6 +185,9 @@ paragraph for more details. This is the same as :c:func:`proj_create_crs_to_crs` except that the source and target CRS are passed as PJ* objects which must be of the CRS variety. + Starting with PROJ 9.2, source_crs or target_crs can be a CoordinateMetadata + with an associated coordinate epoch (but only one of them, not both). + :param `options`: a list of NUL terminated options, or NULL. The list of supported options is: diff --git a/include/proj/coordinateoperation.hpp b/include/proj/coordinateoperation.hpp index 1f23821a38..7988d41078 100644 --- a/include/proj/coordinateoperation.hpp +++ b/include/proj/coordinateoperation.hpp @@ -49,6 +49,12 @@ class DerivedCRS; class ProjectedCRS; } // namespace crs +namespace coordinates { +class CoordinateMetadata; +using CoordinateMetadataPtr = std::shared_ptr; +using CoordinateMetadataNNPtr = util::nn; +} // namespace coordinates + /** osgeo.proj.operation namespace \brief Coordinate operations (relationship between any two coordinate @@ -193,6 +199,11 @@ class PROJ_GCC_DLL CoordinateOperation : public common::ObjectUsage, const std::vector &accuracies); PROJ_INTERNAL void setHasBallparkTransformation(bool b); + PROJ_INTERNAL void + setSourceCoordinateEpoch(const util::optional &epoch); + PROJ_INTERNAL void + setTargetCoordinateEpoch(const util::optional &epoch); + PROJ_INTERNAL void setProperties(const util::PropertyMap &properties); // throw(InvalidValueTypeException) @@ -1899,12 +1910,28 @@ class PROJ_GCC_DLL CoordinateOperationContext { PROJ_DLL const std::vector> & getIntermediateCRS() const; + PROJ_DLL void + setSourceCoordinateEpoch(const util::optional &epoch); + + PROJ_DLL const util::optional & + getSourceCoordinateEpoch() const; + + PROJ_DLL void + setTargetCoordinateEpoch(const util::optional &epoch); + + PROJ_DLL const util::optional & + getTargetCoordinateEpoch() const; + PROJ_DLL static CoordinateOperationContextNNPtr create(const io::AuthorityFactoryPtr &authorityFactory, const metadata::ExtentPtr &extent, double accuracy); + PROJ_DLL CoordinateOperationContextNNPtr clone() const; + protected: PROJ_INTERNAL CoordinateOperationContext(); + PROJ_INTERNAL + CoordinateOperationContext(const CoordinateOperationContext &); INLINED_MAKE_UNIQUE private: @@ -1942,6 +1969,16 @@ class PROJ_GCC_DLL CoordinateOperationFactory { const crs::CRSNNPtr &targetCRS, const CoordinateOperationContextNNPtr &context) const; + PROJ_DLL std::vector createOperations( + const coordinates::CoordinateMetadataNNPtr &sourceCoordinateMetadata, + const crs::CRSNNPtr &targetCRS, + const CoordinateOperationContextNNPtr &context) const; + + PROJ_DLL std::vector createOperations( + const crs::CRSNNPtr &sourceCRS, + const coordinates::CoordinateMetadataNNPtr &targetCoordinateMetadata, + const CoordinateOperationContextNNPtr &context) const; + PROJ_DLL static CoordinateOperationFactoryNNPtr create(); protected: diff --git a/scripts/reference_exported_symbols.txt b/scripts/reference_exported_symbols.txt index 0d20db1d21..b2b1a9fbe4 100644 --- a/scripts/reference_exported_symbols.txt +++ b/scripts/reference_exported_symbols.txt @@ -614,6 +614,7 @@ osgeo::proj::operation::Conversion::createWagnerV(osgeo::proj::util::PropertyMap osgeo::proj::operation::Conversion::identify() const osgeo::proj::operation::Conversion::inverse() const osgeo::proj::operation::Conversion::isUTM(int&, bool&) const +osgeo::proj::operation::CoordinateOperationContext::clone() const osgeo::proj::operation::CoordinateOperationContext::~CoordinateOperationContext() osgeo::proj::operation::CoordinateOperationContext::create(std::shared_ptr const&, std::shared_ptr const&, double) osgeo::proj::operation::CoordinateOperationContext::getAllowBallparkTransformations() const @@ -625,7 +626,9 @@ osgeo::proj::operation::CoordinateOperationContext::getDiscardSuperseded() const osgeo::proj::operation::CoordinateOperationContext::getGridAvailabilityUse() const osgeo::proj::operation::CoordinateOperationContext::getIntermediateCRS() const osgeo::proj::operation::CoordinateOperationContext::getSourceAndTargetCRSExtentUse() const +osgeo::proj::operation::CoordinateOperationContext::getSourceCoordinateEpoch() const osgeo::proj::operation::CoordinateOperationContext::getSpatialCriterion() const +osgeo::proj::operation::CoordinateOperationContext::getTargetCoordinateEpoch() const osgeo::proj::operation::CoordinateOperationContext::getUsePROJAlternativeGridNames() const osgeo::proj::operation::CoordinateOperationContext::setAllowBallparkTransformations(bool) osgeo::proj::operation::CoordinateOperationContext::setAllowUseIntermediateCRS(osgeo::proj::operation::CoordinateOperationContext::IntermediateCRSUse) @@ -635,13 +638,17 @@ osgeo::proj::operation::CoordinateOperationContext::setDiscardSuperseded(bool) osgeo::proj::operation::CoordinateOperationContext::setGridAvailabilityUse(osgeo::proj::operation::CoordinateOperationContext::GridAvailabilityUse) osgeo::proj::operation::CoordinateOperationContext::setIntermediateCRS(std::vector, std::allocator > > const&) osgeo::proj::operation::CoordinateOperationContext::setSourceAndTargetCRSExtentUse(osgeo::proj::operation::CoordinateOperationContext::SourceTargetCRSExtentUse) +osgeo::proj::operation::CoordinateOperationContext::setSourceCoordinateEpoch(osgeo::proj::util::optional const&) osgeo::proj::operation::CoordinateOperationContext::setSpatialCriterion(osgeo::proj::operation::CoordinateOperationContext::SpatialCriterion) +osgeo::proj::operation::CoordinateOperationContext::setTargetCoordinateEpoch(osgeo::proj::util::optional const&) osgeo::proj::operation::CoordinateOperationContext::setUsePROJAlternativeGridNames(bool) osgeo::proj::operation::CoordinateOperation::~CoordinateOperation() osgeo::proj::operation::CoordinateOperation::coordinateOperationAccuracies() const osgeo::proj::operation::CoordinateOperationFactory::~CoordinateOperationFactory() osgeo::proj::operation::CoordinateOperationFactory::create() osgeo::proj::operation::CoordinateOperationFactory::createOperation(dropbox::oxygen::nn > const&, dropbox::oxygen::nn > const&) const +osgeo::proj::operation::CoordinateOperationFactory::createOperations(dropbox::oxygen::nn > const&, dropbox::oxygen::nn > const&, dropbox::oxygen::nn > > const&) const +osgeo::proj::operation::CoordinateOperationFactory::createOperations(dropbox::oxygen::nn > const&, dropbox::oxygen::nn > const&, dropbox::oxygen::nn > > const&) const osgeo::proj::operation::CoordinateOperationFactory::createOperations(dropbox::oxygen::nn > const&, dropbox::oxygen::nn > const&, dropbox::oxygen::nn > > const&) const osgeo::proj::operation::CoordinateOperation::hasBallparkTransformation() const osgeo::proj::operation::CoordinateOperation::interpolationCRS() const diff --git a/src/4D_api.cpp b/src/4D_api.cpp index 0980316a97..a2bdf50317 100644 --- a/src/4D_api.cpp +++ b/src/4D_api.cpp @@ -374,6 +374,8 @@ similarly, but prefers the 2D resp. 3D interfaces if available. P->iCurCoordOp = iBest; } PJ_COORD res = coord; + if( alt.pj->hasCoordinateEpoch ) + coord.xyzt.t = alt.pj->coordinateEpoch; if( direction == PJ_FWD ) pj_fwd4d( res, alt.pj ); else @@ -439,6 +441,8 @@ similarly, but prefers the 2D resp. 3D interfaces if available. } P->iCurCoordOp = 0; // dummy value, to be used by proj_trans_get_last_used_operation() + if( P->hasCoordinateEpoch ) + coord.xyzt.t = P->coordinateEpoch; if (direction == PJ_FWD) pj_fwd4d (coord, P); else @@ -1691,9 +1695,29 @@ static PJ* add_coord_op_to_list( return op; } +namespace { +struct ObjectKeeper { + PJ *m_obj = nullptr; + explicit ObjectKeeper(PJ *obj) : m_obj(obj) {} + ~ObjectKeeper() { proj_destroy(m_obj); } + ObjectKeeper(const ObjectKeeper &) = delete; + ObjectKeeper& operator=(const ObjectKeeper &) = delete; +}; +} // namespace + /*****************************************************************************/ static PJ* create_operation_to_geog_crs(PJ_CONTEXT* ctx, const PJ* crs) { /*****************************************************************************/ + + std::unique_ptr keeper; + if( proj_get_type(crs) == PJ_TYPE_COORDINATE_METADATA ) { + auto tmp = proj_get_source_crs(ctx, crs); + assert(tmp); + keeper.reset(new ObjectKeeper(tmp)); + crs = tmp; + } + (void)keeper; + // Create a geographic 2D long-lat degrees CRS that is related to the // CRS auto geodetic_crs = proj_crs_get_geodetic_crs(ctx, crs); diff --git a/src/apps/projinfo.cpp b/src/apps/projinfo.cpp index 1bfae2eaf1..d4b0ea54f1 100644 --- a/src/apps/projinfo.cpp +++ b/src/apps/projinfo.cpp @@ -41,6 +41,7 @@ #include #include +#include #include #include #include @@ -49,6 +50,7 @@ #include "proj/internal/internal.hpp" // for split using namespace NS_PROJ::common; +using namespace NS_PROJ::coordinates; using namespace NS_PROJ::crs; using namespace NS_PROJ::io; using namespace NS_PROJ::metadata; @@ -835,9 +837,20 @@ static void outputOperations( CoordinateOperationContext::IntermediateCRSUse::NEVER, promoteTo3D, normalizeAxisOrder, outputOpt.quiet); auto sourceCRS = nn_dynamic_pointer_cast(sourceObj); + CoordinateMetadataPtr sourceCoordinateMetadata; if (!sourceCRS) { - std::cerr << "source CRS string is not a CRS" << std::endl; - std::exit(1); + sourceCoordinateMetadata = + nn_dynamic_pointer_cast(sourceObj); + if (!sourceCoordinateMetadata) { + std::cerr + << "source CRS string is not a CRS or a CoordinateMetadata" + << std::endl; + std::exit(1); + } + if (!sourceCoordinateMetadata->coordinateEpoch().has_value()) { + sourceCRS = sourceCoordinateMetadata->crs().as_nullable(); + sourceCoordinateMetadata.reset(); + } } auto targetObj = @@ -845,12 +858,32 @@ static void outputOperations( CoordinateOperationContext::IntermediateCRSUse::NEVER, promoteTo3D, normalizeAxisOrder, outputOpt.quiet); auto targetCRS = nn_dynamic_pointer_cast(targetObj); + CoordinateMetadataPtr targetCoordinateMetadata; if (!targetCRS) { - std::cerr << "target CRS string is not a CRS" << std::endl; + targetCoordinateMetadata = + nn_dynamic_pointer_cast(targetObj); + if (!targetCoordinateMetadata) { + std::cerr + << "target CRS string is not a CRS or a CoordinateMetadata" + << std::endl; + std::exit(1); + } + if (!targetCoordinateMetadata->coordinateEpoch().has_value()) { + targetCRS = targetCoordinateMetadata->crs().as_nullable(); + targetCoordinateMetadata.reset(); + } + } + + if (sourceCoordinateMetadata != nullptr && + targetCoordinateMetadata != nullptr) { + std::cerr << "CoordinateMetadata with epoch to CoordinateMetadata " + "with epoch not supported currently." + << std::endl; std::exit(1); } - if (dbContext && !promoteTo3D) { + // TODO: handle promotion of CoordinateMetadata + if (sourceCRS && targetCRS && dbContext && !promoteTo3D) { // Auto-promote source/target CRS if it is specified by its name, // if it has a known 3D version of it and that the other CRS is 3D. // e.g projinfo -s "WGS 84 + EGM96 height" -t "WGS 84" @@ -875,8 +908,6 @@ static void outputOperations( } } - auto nnSourceCRS = NN_NO_CHECK(sourceCRS); - auto nnTargetCRS = NN_NO_CHECK(targetCRS); std::vector list; size_t spatialCriterionPartialIntersectionResultCount = 0; bool spatialCriterionPartialIntersectionMoreRelevant = false; @@ -888,6 +919,22 @@ static void outputOperations( : nullptr; auto ctxt = CoordinateOperationContext::create(authFactory, bboxFilter, 0); + + const auto createOperations = [&]() { + if (sourceCoordinateMetadata) { + return CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(sourceCoordinateMetadata), + NN_NO_CHECK(targetCRS), ctxt); + } else if (targetCoordinateMetadata) { + return CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(sourceCRS), + NN_NO_CHECK(targetCoordinateMetadata), ctxt); + } else { + return CoordinateOperationFactory::create()->createOperations( + NN_NO_CHECK(sourceCRS), NN_NO_CHECK(targetCRS), ctxt); + } + }; + ctxt->setSpatialCriterion(spatialCriterion); ctxt->setSourceAndTargetCRSExtentUse(crsExtentUse); ctxt->setGridAvailabilityUse(gridAvailabilityUse); @@ -899,8 +946,7 @@ static void outputOperations( if (minimumAccuracy >= 0) { ctxt->setDesiredAccuracy(minimumAccuracy); } - list = CoordinateOperationFactory::create()->createOperations( - nnSourceCRS, nnTargetCRS, ctxt); + list = createOperations(); if (!spatialCriterionExplicitlySpecified && spatialCriterion == CoordinateOperationContext::SpatialCriterion:: STRICT_CONTAINMENT) { @@ -908,9 +954,7 @@ static void outputOperations( ctxt->setSpatialCriterion( CoordinateOperationContext::SpatialCriterion:: PARTIAL_INTERSECTION); - auto list2 = - CoordinateOperationFactory::create()->createOperations( - nnSourceCRS, nnTargetCRS, ctxt); + auto list2 = createOperations(); spatialCriterionPartialIntersectionResultCount = list2.size(); if (spatialCriterionPartialIntersectionResultCount == 1 && list.size() == 1 && diff --git a/src/iso19111/c_api.cpp b/src/iso19111/c_api.cpp index 56e21487b5..3e6a0244b8 100644 --- a/src/iso19111/c_api.cpp +++ b/src/iso19111/c_api.cpp @@ -206,6 +206,21 @@ static PJ *pj_obj_create(PJ_CONTEXT *ctx, const BaseObjectNNPtr &objIn) { if (pj) { pj->iso_obj = objIn; pj->iso_obj_is_coordinate_operation = true; + auto sourceEpoch = coordop->sourceCoordinateEpoch(); + if (sourceEpoch.has_value()) { + pj->hasCoordinateEpoch = true; + pj->coordinateEpoch = + sourceEpoch->coordinateEpoch().convertToUnit( + common::UnitOfMeasure::YEAR); + } else { + auto targetEpoch = coordop->targetCoordinateEpoch(); + if (targetEpoch.has_value()) { + pj->hasCoordinateEpoch = true; + pj->coordinateEpoch = + targetEpoch->coordinateEpoch().convertToUnit( + common::UnitOfMeasure::YEAR); + } + } return pj; } } catch (const std::exception &) { @@ -8256,22 +8271,61 @@ proj_create_operations(PJ_CONTEXT *ctx, const PJ *source_crs, return nullptr; } auto sourceCRS = std::dynamic_pointer_cast(source_crs->iso_obj); + CoordinateMetadataPtr sourceCoordinateMetadata; if (!sourceCRS) { - proj_log_error(ctx, __FUNCTION__, "source_crs is not a CRS"); - return nullptr; + sourceCoordinateMetadata = + std::dynamic_pointer_cast(source_crs->iso_obj); + if (!sourceCoordinateMetadata) { + proj_log_error(ctx, __FUNCTION__, + "source_crs is not a CRS or a CoordinateMetadata"); + return nullptr; + } + if (!sourceCoordinateMetadata->coordinateEpoch().has_value()) { + sourceCRS = sourceCoordinateMetadata->crs().as_nullable(); + sourceCoordinateMetadata.reset(); + } } auto targetCRS = std::dynamic_pointer_cast(target_crs->iso_obj); + CoordinateMetadataPtr targetCoordinateMetadata; if (!targetCRS) { - proj_log_error(ctx, __FUNCTION__, "target_crs is not a CRS"); + targetCoordinateMetadata = + std::dynamic_pointer_cast(target_crs->iso_obj); + if (!targetCoordinateMetadata) { + proj_log_error(ctx, __FUNCTION__, + "target_crs is not a CRS or a CoordinateMetadata"); + return nullptr; + } + if (!targetCoordinateMetadata->coordinateEpoch().has_value()) { + targetCRS = targetCoordinateMetadata->crs().as_nullable(); + targetCoordinateMetadata.reset(); + } + } + + if (sourceCoordinateMetadata != nullptr && + targetCoordinateMetadata != nullptr) { + proj_log_error(ctx, __FUNCTION__, + "CoordinateMetadata with epoch to CoordinateMetadata " + "with epoch not supported currently"); return nullptr; } try { auto factory = CoordinateOperationFactory::create(); std::vector objects; - auto ops = factory->createOperations( - NN_NO_CHECK(sourceCRS), NN_NO_CHECK(targetCRS), - operationContext->operationContext); + auto ops = + sourceCoordinateMetadata != nullptr + ? factory->createOperations( + NN_NO_CHECK(sourceCoordinateMetadata), + NN_NO_CHECK(targetCRS), + operationContext->operationContext) + : targetCoordinateMetadata != nullptr + ? factory->createOperations( + NN_NO_CHECK(sourceCRS), + NN_NO_CHECK(targetCoordinateMetadata), + operationContext->operationContext) + : factory->createOperations( + NN_NO_CHECK(sourceCRS), NN_NO_CHECK(targetCRS), + operationContext->operationContext); for (const auto &op : ops) { objects.emplace_back(op); } diff --git a/src/iso19111/operation/coordinateoperation_private.hpp b/src/iso19111/operation/coordinateoperation_private.hpp index 2dc7041473..64eb779e52 100644 --- a/src/iso19111/operation/coordinateoperation_private.hpp +++ b/src/iso19111/operation/coordinateoperation_private.hpp @@ -47,8 +47,10 @@ struct CoordinateOperation::Private { std::weak_ptr sourceCRSWeak_{}; std::weak_ptr targetCRSWeak_{}; crs::CRSPtr interpolationCRS_{}; - util::optional sourceCoordinateEpoch_{}; - util::optional targetCoordinateEpoch_{}; + std::shared_ptr> sourceCoordinateEpoch_{ + std::make_shared>()}; + std::shared_ptr> targetCoordinateEpoch_{ + std::make_shared>()}; bool hasBallparkTransformation_ = false; // do not set this for a ProjectedCRS.definingConversion diff --git a/src/iso19111/operation/coordinateoperationfactory.cpp b/src/iso19111/operation/coordinateoperationfactory.cpp index 761c47aa33..fab0188622 100644 --- a/src/iso19111/operation/coordinateoperationfactory.cpp +++ b/src/iso19111/operation/coordinateoperationfactory.cpp @@ -32,6 +32,7 @@ #include "proj/common.hpp" #include "proj/coordinateoperation.hpp" +#include "proj/coordinates.hpp" #include "proj/crs.hpp" #include "proj/io.hpp" #include "proj/metadata.hpp" @@ -167,6 +168,13 @@ struct CoordinateOperationContext::Private { intermediateCRSAuthCodes_{}; bool discardSuperseded_ = true; bool allowBallpark_ = true; + std::shared_ptr> sourceCoordinateEpoch_{ + std::make_shared>()}; + std::shared_ptr> targetCoordinateEpoch_{ + std::make_shared>()}; + + Private() = default; + Private(const Private &) = default; }; //! @endcond @@ -183,6 +191,12 @@ CoordinateOperationContext::CoordinateOperationContext() // --------------------------------------------------------------------------- +CoordinateOperationContext::CoordinateOperationContext( + const CoordinateOperationContext &other) + : d(internal::make_unique(*(other.d))) {} + +// --------------------------------------------------------------------------- + /** \brief Return the authority factory, or null */ const io::AuthorityFactoryPtr & CoordinateOperationContext::getAuthorityFactory() const { @@ -429,6 +443,46 @@ CoordinateOperationContext::getIntermediateCRS() const { // --------------------------------------------------------------------------- +/** \brief Set the source coordinate epoch. + */ +void CoordinateOperationContext::setSourceCoordinateEpoch( + const util::optional &epoch) { + + d->sourceCoordinateEpoch_ = + std::make_shared>(epoch); +} + +// --------------------------------------------------------------------------- + +/** \brief Return the source coordinate epoch. + */ +const util::optional & +CoordinateOperationContext::getSourceCoordinateEpoch() const { + return *(d->sourceCoordinateEpoch_); +} + +// --------------------------------------------------------------------------- + +/** \brief Set the target coordinate epoch. + */ +void CoordinateOperationContext::setTargetCoordinateEpoch( + const util::optional &epoch) { + + d->targetCoordinateEpoch_ = + std::make_shared>(epoch); +} + +// --------------------------------------------------------------------------- + +/** \brief Return the target coordinate epoch. + */ +const util::optional & +CoordinateOperationContext::getTargetCoordinateEpoch() const { + return *(d->targetCoordinateEpoch_); +} + +// --------------------------------------------------------------------------- + /** \brief Creates a context for a coordinate operation. * * If a non null authorityFactory is provided, the resulting context should @@ -464,6 +518,19 @@ CoordinateOperationContextNNPtr CoordinateOperationContext::create( // --------------------------------------------------------------------------- +/** \brief Clone a coordinate operation context. + * + * @return a new context. + * @since 9.2 + */ +CoordinateOperationContextNNPtr CoordinateOperationContext::clone() const { + return NN_NO_CHECK( + CoordinateOperationContext::make_unique( + *this)); +} + +// --------------------------------------------------------------------------- + //! @cond Doxygen_Suppress struct CoordinateOperationFactory::Private { @@ -6250,10 +6317,69 @@ CoordinateOperationFactory::createOperations( } } - return filterAndSort(Private::createOperations(l_resolvedSourceCRS, - l_resolvedTargetCRS, - contextPrivate), - context, sourceCRSExtent, targetCRSExtent); + auto resFiltered = filterAndSort( + Private::createOperations(l_resolvedSourceCRS, l_resolvedTargetCRS, + contextPrivate), + context, sourceCRSExtent, targetCRSExtent); + if (context->getSourceCoordinateEpoch().has_value() || + context->getTargetCoordinateEpoch().has_value()) { + std::vector res; + res.reserve(resFiltered.size()); + for (const auto &op : resFiltered) { + auto opClone = op->shallowClone(); + opClone->setSourceCoordinateEpoch( + context->getSourceCoordinateEpoch()); + opClone->setTargetCoordinateEpoch( + context->getTargetCoordinateEpoch()); + res.emplace_back(opClone); + } + return res; + } + return resFiltered; +} + +// --------------------------------------------------------------------------- + +/** \brief Find a list of CoordinateOperation from a source coordinate metadata + * to targetCRS. + * @param sourceCoordinateMetadata source CoordinateMetadata. + * @param targetCRS target CRS. + * @param context Search context. + * @return a list + * @since 9.2 + */ +std::vector +CoordinateOperationFactory::createOperations( + const coordinates::CoordinateMetadataNNPtr &sourceCoordinateMetadata, + const crs::CRSNNPtr &targetCRS, + const CoordinateOperationContextNNPtr &context) const { + auto newContext = context->clone(); + newContext->setSourceCoordinateEpoch( + sourceCoordinateMetadata->coordinateEpoch()); + return createOperations(sourceCoordinateMetadata->crs(), targetCRS, + newContext); +} + +// --------------------------------------------------------------------------- + +/** \brief Find a list of CoordinateOperation from a source CRS to a target + * coordinate metadata. + * @param sourceCRS source CRS. + * @param targetCoordinateMetadata target CoordinateMetadata. + * @param context Search context. + * @return a list + * @since 9.2 + */ +std::vector +CoordinateOperationFactory::createOperations( + const crs::CRSNNPtr &sourceCRS, + const coordinates::CoordinateMetadataNNPtr &targetCoordinateMetadata, + const CoordinateOperationContextNNPtr &context) const { + auto newContext = context->clone(); + newContext->setTargetCoordinateEpoch( + targetCoordinateMetadata->coordinateEpoch()); + return createOperations(sourceCRS, targetCoordinateMetadata->crs(), + newContext); } // --------------------------------------------------------------------------- diff --git a/src/iso19111/operation/singleoperation.cpp b/src/iso19111/operation/singleoperation.cpp index c32c3d2a88..75672bcc05 100644 --- a/src/iso19111/operation/singleoperation.cpp +++ b/src/iso19111/operation/singleoperation.cpp @@ -200,7 +200,7 @@ const crs::CRSPtr &CoordinateOperation::interpolationCRS() const { */ const util::optional & CoordinateOperation::sourceCoordinateEpoch() const { - return d->sourceCoordinateEpoch_; + return *(d->sourceCoordinateEpoch_); } // --------------------------------------------------------------------------- @@ -211,7 +211,7 @@ CoordinateOperation::sourceCoordinateEpoch() const { */ const util::optional & CoordinateOperation::targetCoordinateEpoch() const { - return d->targetCoordinateEpoch_; + return *(d->targetCoordinateEpoch_); } // --------------------------------------------------------------------------- @@ -260,6 +260,22 @@ void CoordinateOperation::setCRSs(const CoordinateOperation *in, // --------------------------------------------------------------------------- +void CoordinateOperation::setSourceCoordinateEpoch( + const util::optional &epoch) { + d->sourceCoordinateEpoch_ = + std::make_shared>(epoch); +} + +// --------------------------------------------------------------------------- + +void CoordinateOperation::setTargetCoordinateEpoch( + const util::optional &epoch) { + d->targetCoordinateEpoch_ = + std::make_shared>(epoch); +} + +// --------------------------------------------------------------------------- + void CoordinateOperation::setAccuracies( const std::vector &accuracies) { d->coordinateOperationAccuracies_ = accuracies; diff --git a/src/proj_internal.h b/src/proj_internal.h index 0f37496bcc..57226ec254 100644 --- a/src/proj_internal.h +++ b/src/proj_internal.h @@ -578,6 +578,8 @@ struct PJconsts { NS_PROJ::util::BaseObjectPtr iso_obj{}; bool iso_obj_is_coordinate_operation = false; + double coordinateEpoch = 0; + bool hasCoordinateEpoch = false; // cached results mutable std::string lastWKT{}; diff --git a/test/unit/test_c_api.cpp b/test/unit/test_c_api.cpp index 9b53468fa6..9d50d2f4e5 100644 --- a/test/unit/test_c_api.cpp +++ b/test/unit/test_c_api.cpp @@ -4707,6 +4707,46 @@ TEST_F(CApi, proj_create_crs_to_crs_from_pj_ballpark_filter) { // --------------------------------------------------------------------------- +TEST_F(CApi, proj_create_crs_to_crs_coordinate_metadata_in_src) { + + auto P = + proj_create_crs_to_crs(m_ctxt, "ITRF2014@2025.0", "GDA2020", nullptr); + ObjectKeeper keeper_P(P); + ASSERT_NE(P, nullptr); + + PJ_COORD coord; + coord.xyzt.x = -30; + coord.xyzt.y = 130; + coord.xyzt.z = 0; + coord.xyzt.t = HUGE_VAL; + + coord = proj_trans(P, PJ_FWD, coord); + EXPECT_NEAR(coord.xyzt.x, -30.0000026655, 1e-10); + EXPECT_NEAR(coord.xyzt.y, 129.9999983712, 1e-10); +} + +// --------------------------------------------------------------------------- + +TEST_F(CApi, proj_create_crs_to_crs_coordinate_metadata_in_target) { + + auto P = + proj_create_crs_to_crs(m_ctxt, "GDA2020", "ITRF2014@2025.0", nullptr); + ObjectKeeper keeper_P(P); + ASSERT_NE(P, nullptr); + + PJ_COORD coord; + coord.xyzt.x = -30.0000026655; + coord.xyzt.y = 129.9999983712; + coord.xyzt.z = 0; + coord.xyzt.t = HUGE_VAL; + + coord = proj_trans(P, PJ_FWD, coord); + EXPECT_NEAR(coord.xyzt.x, -30, 1e-10); + EXPECT_NEAR(coord.xyzt.y, 130, 1e-10); +} + +// --------------------------------------------------------------------------- + static void check_axis_is_latitude(PJ_CONTEXT *ctx, PJ *cs, int axis_number, const char *unit_name = "degree", From 5fecb305740b10b0de670dc70693e941bdabecce Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 14 Jan 2023 21:10:19 +0100 Subject: [PATCH 6/8] install.sh: validate PROJJSON CoordinateMetadata --- travis/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/travis/install.sh b/travis/install.sh index 506fee585c..a340f97573 100755 --- a/travis/install.sh +++ b/travis/install.sh @@ -161,6 +161,7 @@ test_projjson -s EPSG:3111 -t GDA2020 test_projjson EPSG:9057 # Dynamic vertical CRS "RH2000 height" test_projjson EPSG:5613 +test_projjson "ITRF2014@2025.0" validate_json $TRAVIS_BUILD_DIR/schemas/v0.5/examples/point_motion_operation.json From 28e5849b970dbb69beb809fc496bcd690a38578e Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 14 Jan 2023 21:37:53 +0100 Subject: [PATCH 7/8] Add CRS::isDynamic() --- include/proj/crs.hpp | 1 + scripts/reference_exported_symbols.txt | 1 + src/iso19111/crs.cpp | 45 ++++++++++++++++++++++++++ test/unit/test_crs.cpp | 45 ++++++++++++++++++++++++++ 4 files changed, 92 insertions(+) diff --git a/include/proj/crs.hpp b/include/proj/crs.hpp index 57091377ca..d9c3846573 100644 --- a/include/proj/crs.hpp +++ b/include/proj/crs.hpp @@ -96,6 +96,7 @@ class PROJ_GCC_DLL CRS : public common::ObjectUsage, // Non-standard + PROJ_DLL bool isDynamic(bool considerWGS84AsDynamic = false) const; PROJ_DLL GeodeticCRSPtr extractGeodeticCRS() const; PROJ_DLL GeographicCRSPtr extractGeographicCRS() const; PROJ_DLL VerticalCRSPtr extractVerticalCRS() const; diff --git a/scripts/reference_exported_symbols.txt b/scripts/reference_exported_symbols.txt index b2b1a9fbe4..e8673f0f61 100644 --- a/scripts/reference_exported_symbols.txt +++ b/scripts/reference_exported_symbols.txt @@ -124,6 +124,7 @@ osgeo::proj::crs::CRS::extractGeographicCRS() const osgeo::proj::crs::CRS::extractVerticalCRS() const osgeo::proj::crs::CRS::getNonDeprecated(dropbox::oxygen::nn > const&) const osgeo::proj::crs::CRS::identify(std::shared_ptr const&) const +osgeo::proj::crs::CRS::isDynamic(bool) const osgeo::proj::crs::CRS::normalizeForVisualization() const osgeo::proj::crs::CRS::promoteTo3D(std::string const&, std::shared_ptr const&) const osgeo::proj::crs::CRS::shallowClone() const diff --git a/src/iso19111/crs.cpp b/src/iso19111/crs.cpp index a63d76ead5..14b534af19 100644 --- a/src/iso19111/crs.cpp +++ b/src/iso19111/crs.cpp @@ -179,6 +179,51 @@ const BoundCRSPtr &CRS::canonicalBoundCRS() PROJ_PURE_DEFN { // --------------------------------------------------------------------------- +/** \brief Return whether a CRS is a dynamic CRS. + * + * A dynamic CRS is a CRS that contains a geodetic CRS whose geodetic reference + * frame is dynamic, or a vertical CRS whose vertical reference frame is + * dynamic. + * @param considerWGS84AsDynamic set to true to consider the WGS 84 / EPSG:6326 + * datum ensemble as dynamic. + * @since 9.2 + */ +bool CRS::isDynamic(bool considerWGS84AsDynamic) const { + + if (auto raw = extractGeodeticCRSRaw()) { + const auto &l_datum = raw->datum(); + if (l_datum) { + if (dynamic_cast( + l_datum.get())) { + return true; + } + if (considerWGS84AsDynamic && + l_datum->nameStr() == "World Geodetic System 1984") { + return true; + } + } + if (considerWGS84AsDynamic) { + const auto &l_datumEnsemble = raw->datumEnsemble(); + if (l_datumEnsemble && l_datumEnsemble->nameStr() == + "World Geodetic System 1984 ensemble") { + return true; + } + } + } + + if (auto vertCRS = extractVerticalCRS()) { + const auto &l_datum = vertCRS->datum(); + if (l_datum && dynamic_cast( + l_datum.get())) { + return true; + } + } + + return false; +} + +// --------------------------------------------------------------------------- + /** \brief Return the GeodeticCRS of the CRS. * * Returns the GeodeticCRS contained in a CRS. This works currently with diff --git a/test/unit/test_crs.cpp b/test/unit/test_crs.cpp index 9257902273..787338b569 100644 --- a/test/unit/test_crs.cpp +++ b/test/unit/test_crs.cpp @@ -7173,3 +7173,48 @@ TEST(crs, projected_is_equivalent_to_with_proj4_extension) { EXPECT_FALSE(crs1->isEquivalentTo(crsDifferent.get(), IComparable::Criterion::EQUIVALENT)); } + +// --------------------------------------------------------------------------- + +TEST(crs, is_dynamic) { + + EXPECT_FALSE(GeographicCRS::EPSG_4326->isDynamic()); + EXPECT_TRUE( + GeographicCRS::EPSG_4326->isDynamic(/*considerWGS84AsDynamic=*/true)); + + { + auto factory = + AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + auto crs = factory->createCoordinateReferenceSystem("4326"); + EXPECT_FALSE(crs->isDynamic()); + EXPECT_TRUE(crs->isDynamic(/*considerWGS84AsDynamic=*/true)); + } + + { + auto drf = DynamicGeodeticReferenceFrame::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "test"), + Ellipsoid::WGS84, optional("My anchor"), + PrimeMeridian::GREENWICH, Measure(2018.5, UnitOfMeasure::YEAR), + optional("My model")); + auto crs = GeographicCRS::create( + PropertyMap(), drf, + EllipsoidalCS::createLatitudeLongitude(UnitOfMeasure::DEGREE)); + EXPECT_TRUE(crs->isDynamic()); + } + + EXPECT_FALSE(createVerticalCRS()->isDynamic()); + + { + auto drf = DynamicVerticalReferenceFrame::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "test"), + optional("My anchor"), optional(), + Measure(2018.5, UnitOfMeasure::YEAR), + optional("My model")); + auto crs = VerticalCRS::create( + PropertyMap(), drf, + VerticalCS::createGravityRelatedHeight(UnitOfMeasure::METRE)); + EXPECT_TRUE(crs->isDynamic()); + } + + EXPECT_FALSE(createCompoundCRS()->isDynamic()); +} From 33f19146978e0ee3dee8d7949c88a97e874839a5 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 14 Jan 2023 21:44:45 +0100 Subject: [PATCH 8/8] CoordinateMetadata: require epoch for dynamic CRS, forbid it for static CRS --- src/iso19111/coordinates.cpp | 14 ++++++++++++++ src/iso19111/io.cpp | 13 ++++++++++--- test/unit/test_coordinates.cpp | 30 ++++++++++++++++++++++++++++++ test/unit/test_io.cpp | 4 ++++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/iso19111/coordinates.cpp b/src/iso19111/coordinates.cpp index 7c81cac14f..577afddfb9 100644 --- a/src/iso19111/coordinates.cpp +++ b/src/iso19111/coordinates.cpp @@ -86,8 +86,15 @@ CoordinateMetadata::~CoordinateMetadata() = default; /** \brief Instantiate a CoordinateMetadata from a static CRS. * @param crsIn a static CRS * @return new CoordinateMetadata. + * @throw util::Exception if crsIn is a dynamic CRS. */ CoordinateMetadataNNPtr CoordinateMetadata::create(const crs::CRSNNPtr &crsIn) { + + if (crsIn->isDynamic(/*considerWGS84AsDynamic=*/false)) { + throw util::Exception( + "Coordinate epoch should be provided for a dynamic CRS"); + } + auto coordinateMetadata( CoordinateMetadata::nn_make_shared(crsIn)); coordinateMetadata->assignSelf(coordinateMetadata); @@ -102,9 +109,16 @@ CoordinateMetadataNNPtr CoordinateMetadata::create(const crs::CRSNNPtr &crsIn) { * @param crsIn a dynamic CRS * @param coordinateEpochIn coordinate epoch expressed in decimal year. * @return new CoordinateMetadata. + * @throw util::Exception if crsIn is a static CRS. */ CoordinateMetadataNNPtr CoordinateMetadata::create(const crs::CRSNNPtr &crsIn, double coordinateEpochIn) { + + if (!crsIn->isDynamic(/*considerWGS84AsDynamic=*/true)) { + throw util::Exception( + "Coordinate epoch should not be provided for a static CRS"); + } + auto coordinateMetadata( CoordinateMetadata::nn_make_shared( crsIn, coordinateEpochIn)); diff --git a/src/iso19111/io.cpp b/src/iso19111/io.cpp index 7fcdc37b0d..87b3e9db11 100644 --- a/src/iso19111/io.cpp +++ b/src/iso19111/io.cpp @@ -7547,13 +7547,20 @@ static BaseObjectNNPtr createFromUserInput(const std::string &text, usePROJ4InitRules, ctx); auto crs = nn_dynamic_pointer_cast(obj); if (crs) { + double epoch; try { - const double epoch = - c_locale_stod(text.substr(nonSpacePos)); - return CoordinateMetadata::create(NN_NO_CHECK(crs), epoch); + epoch = c_locale_stod(text.substr(nonSpacePos)); } catch (const std::exception &) { throw ParsingException("non-numeric value after @"); } + try { + return CoordinateMetadata::create(NN_NO_CHECK(crs), epoch); + } catch (const std::exception &e) { + throw ParsingException( + std::string( + "CoordinateMetadata::create() failed with: ") + + e.what()); + } } } } diff --git a/test/unit/test_coordinates.cpp b/test/unit/test_coordinates.cpp index 9af4ef1ce2..d722fb8705 100644 --- a/test/unit/test_coordinates.cpp +++ b/test/unit/test_coordinates.cpp @@ -39,6 +39,7 @@ #include "proj/crs.hpp" #include "proj/datum.hpp" #include "proj/io.hpp" +#include "proj/metadata.hpp" #include "proj/util.hpp" #include @@ -51,6 +52,7 @@ using namespace osgeo::proj::crs; using namespace osgeo::proj::cs; using namespace osgeo::proj::datum; using namespace osgeo::proj::io; +using namespace osgeo::proj::metadata; using namespace osgeo::proj::util; namespace { @@ -79,6 +81,23 @@ struct PjContextKeeper { // --------------------------------------------------------------------------- +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(coordinateMetadata, static_crs) { auto coordinateMetadata = CoordinateMetadata::create(GeographicCRS::EPSG_4326); @@ -86,6 +105,14 @@ TEST(coordinateMetadata, static_crs) { GeographicCRS::EPSG_4326.get())); EXPECT_FALSE(coordinateMetadata->coordinateEpoch().has_value()); + // We tolerate coordinate epochs for EPSG:4326 + EXPECT_NO_THROW( + CoordinateMetadata::create(GeographicCRS::EPSG_4326, 2025.0)); + + // A coordinate epoch should NOT be provided + EXPECT_THROW(CoordinateMetadata::create(createVerticalCRS(), 2025.0), + Exception); + WKTFormatterNNPtr f( WKTFormatter::create(WKTFormatter::Convention::WKT2_2019)); auto wkt = coordinateMetadata->exportToWKT(f.get()); @@ -136,6 +163,9 @@ TEST(coordinateMetadata, dynamic_crs) { EXPECT_NEAR(coordinateMetadata->coordinateEpochAsDecimalYear(), 2023.5, 1e-10); + // A coordinate epoch should be provided + EXPECT_THROW(CoordinateMetadata::create(crs), Exception); + WKTFormatterNNPtr f( WKTFormatter::create(WKTFormatter::Convention::WKT2_2019)); auto wkt = coordinateMetadata->exportToWKT(f.get()); diff --git a/test/unit/test_io.cpp b/test/unit/test_io.cpp index a8c32133c2..74feff49c3 100644 --- a/test/unit/test_io.cpp +++ b/test/unit/test_io.cpp @@ -12124,6 +12124,10 @@ TEST(io, createFromUserInput) { // Invalid CoordinateMetadata EXPECT_THROW(createFromUserInput("@2025", dbContext), ParsingException); + // Invalid CoordinateMetadata: static CRS not allowed + EXPECT_THROW(createFromUserInput("RGF93@2025", dbContext), + ParsingException); + { auto obj = createFromUserInput("ITRF2014@2025.1", dbContext); auto coordinateMetadata =