diff --git a/include/proj/internal/datum_internal.hpp b/include/proj/internal/datum_internal.hpp new file mode 100644 index 0000000000..899679f669 --- /dev/null +++ b/include/proj/internal/datum_internal.hpp @@ -0,0 +1,38 @@ +/****************************************************************************** + * + * 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 +#error This file should only be included from a PROJ cpp file +#endif + +#ifndef DATUM_INTERNAL_HH_INCLUDED +#define DATUM_INTERNAL_HH_INCLUDED + +#define UNKNOWN_ENGINEERING_DATUM "Unknown engineering datum" + +#endif // DATUM_INTERNAL_HH_INCLUDED diff --git a/src/iso19111/c_api.cpp b/src/iso19111/c_api.cpp index a1a5f2d8f2..243c4b9e76 100644 --- a/src/iso19111/c_api.cpp +++ b/src/iso19111/c_api.cpp @@ -51,6 +51,7 @@ #include "proj/metadata.hpp" #include "proj/util.hpp" +#include "proj/internal/datum_internal.hpp" #include "proj/internal/internal.hpp" #include "proj/internal/io_internal.hpp" @@ -4332,7 +4333,8 @@ PJ *proj_create_engineering_crs(PJ_CONTEXT *ctx, const char *crs_name) { return pj_obj_create( ctx, EngineeringCRS::create( createPropertyMapName(crs_name), - EngineeringDatum::create(PropertyMap()), + EngineeringDatum::create( + createPropertyMapName(UNKNOWN_ENGINEERING_DATUM)), CartesianCS::createEastingNorthing(UnitOfMeasure::METRE))); } catch (const std::exception &e) { proj_log_error(ctx, __FUNCTION__, e.what()); diff --git a/src/iso19111/crs.cpp b/src/iso19111/crs.cpp index c29ae581d0..fc42bc29ab 100644 --- a/src/iso19111/crs.cpp +++ b/src/iso19111/crs.cpp @@ -44,6 +44,7 @@ #include "proj/internal/coordinatesystem_internal.hpp" #include "proj/internal/crs_internal.hpp" +#include "proj/internal/datum_internal.hpp" #include "proj/internal/internal.hpp" #include "proj/internal/io_internal.hpp" @@ -6953,7 +6954,7 @@ void EngineeringCRS::_exportToWKT(io::WKTFormatter *formatter) const { formatter->addQuotedString(nameStr()); const auto &datumName = datum()->nameStr(); if (isWKT2 || - (!datumName.empty() && datumName != "Unknown engineering datum")) { + (!datumName.empty() && datumName != UNKNOWN_ENGINEERING_DATUM)) { datum()->_exportToWKT(formatter); } if (!isWKT2) { @@ -7006,8 +7007,54 @@ bool EngineeringCRS::_isEquivalentTo( const util::IComparable *other, util::IComparable::Criterion criterion, const io::DatabaseContextPtr &dbContext) const { auto otherEngineeringCRS = dynamic_cast(other); - return otherEngineeringCRS != nullptr && - SingleCRS::baseIsEquivalentTo(other, criterion, dbContext); + if (otherEngineeringCRS == nullptr || + (criterion == util::IComparable::Criterion::STRICT && + !ObjectUsage::_isEquivalentTo(other, criterion, dbContext))) { + return false; + } + + // Check datum + const auto &thisDatum = datum(); + const auto &otherDatum = otherEngineeringCRS->datum(); + if (!thisDatum->_isEquivalentTo(otherDatum.get(), criterion, dbContext)) { + return false; + } + + // Check coordinate system + const auto &thisCS = coordinateSystem(); + const auto &otherCS = otherEngineeringCRS->coordinateSystem(); + if (!(thisCS->_isEquivalentTo(otherCS.get(), criterion, dbContext))) { + const auto thisCartCS = dynamic_cast(thisCS.get()); + const auto otherCartCS = dynamic_cast(otherCS.get()); + const auto &thisAxisList = thisCS->axisList(); + const auto &otherAxisList = otherCS->axisList(); + // Check particular case of + // https://github.com/r-spatial/sf/issues/2049#issuecomment-1486600723 + if (criterion != util::IComparable::Criterion::STRICT && thisCartCS && + otherCartCS && thisAxisList.size() == 2 && + otherAxisList.size() == 2 && + ((&thisAxisList[0]->direction() == + &cs::AxisDirection::UNSPECIFIED && + &thisAxisList[1]->direction() == + &cs::AxisDirection::UNSPECIFIED) || + (&otherAxisList[0]->direction() == + &cs::AxisDirection::UNSPECIFIED && + &otherAxisList[1]->direction() == + &cs::AxisDirection::UNSPECIFIED)) && + ((thisAxisList[0]->nameStr() == "X" && + otherAxisList[0]->nameStr() == "Easting" && + thisAxisList[1]->nameStr() == "Y" && + otherAxisList[1]->nameStr() == "Northing") || + (otherAxisList[0]->nameStr() == "X" && + thisAxisList[0]->nameStr() == "Easting" && + otherAxisList[1]->nameStr() == "Y" && + thisAxisList[1]->nameStr() == "Northing"))) { + return true; + } + return false; + } + + return true; } // --------------------------------------------------------------------------- diff --git a/src/iso19111/datum.cpp b/src/iso19111/datum.cpp index 191ccfb85e..777addd22c 100644 --- a/src/iso19111/datum.cpp +++ b/src/iso19111/datum.cpp @@ -36,6 +36,7 @@ #include "proj/metadata.hpp" #include "proj/util.hpp" +#include "proj/internal/datum_internal.hpp" #include "proj/internal/internal.hpp" #include "proj/internal/io_internal.hpp" @@ -2654,12 +2655,17 @@ void EngineeringDatum::_exportToJSON( bool EngineeringDatum::_isEquivalentTo( const util::IComparable *other, util::IComparable::Criterion criterion, const io::DatabaseContextPtr &dbContext) const { - auto otherTD = dynamic_cast(other); - if (otherTD == nullptr || - !Datum::_isEquivalentTo(other, criterion, dbContext)) { + auto otherDatum = dynamic_cast(other); + if (otherDatum == nullptr) { return false; } - return true; + if (criterion != util::IComparable::Criterion::STRICT && + (nameStr().empty() || nameStr() == UNKNOWN_ENGINEERING_DATUM) && + (otherDatum->nameStr().empty() || + otherDatum->nameStr() == UNKNOWN_ENGINEERING_DATUM)) { + return true; + } + return Datum::_isEquivalentTo(other, criterion, dbContext); } //! @endcond diff --git a/src/iso19111/io.cpp b/src/iso19111/io.cpp index 02974b1a41..78e842b830 100644 --- a/src/iso19111/io.cpp +++ b/src/iso19111/io.cpp @@ -61,6 +61,7 @@ #include "operation/parammappings.hpp" #include "proj/internal/coordinatesystem_internal.hpp" +#include "proj/internal/datum_internal.hpp" #include "proj/internal/internal.hpp" #include "proj/internal/io_internal.hpp" @@ -5180,8 +5181,7 @@ WKTParser::Private::buildEngineeringCRSFromLocalCS(const WKTNodeNNPtr &node) { // has a tradition of emitting just LOCAL_CS["foo"] []() { PropertyMap map; - map.set(IdentifiedObject::NAME_KEY, - "Unknown engineering datum"); + map.set(IdentifiedObject::NAME_KEY, UNKNOWN_ENGINEERING_DATUM); return map; }()); return EngineeringCRS::create(buildProperties(node), datum, cs); diff --git a/test/unit/test_crs.cpp b/test/unit/test_crs.cpp index 40df78d814..480f9d6948 100644 --- a/test/unit/test_crs.cpp +++ b/test/unit/test_crs.cpp @@ -5968,6 +5968,51 @@ TEST(crs, engineeringCRS_WKT1) { // --------------------------------------------------------------------------- +TEST(crs, engineeringCRS_unknown_equivalence) { + + // Test equivalent of CRS definition got from GPKG (wkt2) and its equivalent + // from GeoTIFF (wkt1) + // Cf https://github.com/r-spatial/sf/issues/2049#issuecomment-1486600723 + auto wkt1 = "ENGCRS[\"Undefined Cartesian SRS with unknown unit\",\n" + " EDATUM[\"\"],\n" + " CS[Cartesian,2],\n" + " AXIS[\"(E)\",east,\n" + " ORDER[1],\n" + " LENGTHUNIT[\"metre\",1,\n" + " ID[\"EPSG\",9001]]],\n" + " AXIS[\"(N)\",north,\n" + " ORDER[2],\n" + " LENGTHUNIT[\"metre\",1,\n" + " ID[\"EPSG\",9001]]]]"; + + auto wkt2 = "ENGCRS[\"Undefined Cartesian SRS with unknown unit\",\n" + " EDATUM[\"Unknown engineering datum\"],\n" + " CS[Cartesian,2],\n" + " AXIS[\"x\",unspecified,\n" + " ORDER[1],\n" + " LENGTHUNIT[\"metre\",1,\n" + " ID[\"EPSG\",9001]]],\n" + " AXIS[\"y\",unspecified,\n" + " ORDER[2],\n" + " LENGTHUNIT[\"metre\",1,\n" + " ID[\"EPSG\",9001]]]]"; + + auto obj1 = WKTParser().createFromWKT(wkt1); + auto crs1 = nn_dynamic_pointer_cast(obj1); + ASSERT_TRUE(crs1 != nullptr); + + auto obj2 = WKTParser().createFromWKT(wkt2); + auto crs2 = nn_dynamic_pointer_cast(obj2); + ASSERT_TRUE(crs2 != nullptr); + + EXPECT_TRUE( + crs1->isEquivalentTo(crs2.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_TRUE( + crs2->isEquivalentTo(crs1.get(), IComparable::Criterion::EQUIVALENT)); +} + +// --------------------------------------------------------------------------- + static ParametricCSNNPtr createParametricCS() { return ParametricCS::create(