diff options
| -rw-r--r-- | include/proj/coordinateoperation.hpp | 4 | ||||
| -rw-r--r-- | src/coordinateoperation.cpp | 201 | ||||
| -rw-r--r-- | test/unit/test_operation.cpp | 250 |
3 files changed, 399 insertions, 56 deletions
diff --git a/include/proj/coordinateoperation.hpp b/include/proj/coordinateoperation.hpp index 4ab25d86..5f0f834f 100644 --- a/include/proj/coordinateoperation.hpp +++ b/include/proj/coordinateoperation.hpp @@ -596,6 +596,10 @@ class PROJ_GCC_DLL SingleOperation : virtual public CoordinateOperation { PROJ_INTERNAL bool exportToPROJStringGeneric(io::PROJStringFormatter *formatter) const; + PROJ_INTERNAL bool _isEquivalentTo(const util::IComparable *other, + util::IComparable::Criterion criterion, + bool inOtherDirection) const; + private: PROJ_OPAQUE_PRIVATE_DATA SingleOperation &operator=(const SingleOperation &other) = delete; diff --git a/src/coordinateoperation.cpp b/src/coordinateoperation.cpp index ef03edf8..90854d2e 100644 --- a/src/coordinateoperation.cpp +++ b/src/coordinateoperation.cpp @@ -1113,10 +1113,38 @@ bool OperationParameterValue::_isEquivalentTo( if (otherOPV == nullptr) { return false; } - return d->parameter->_isEquivalentTo(otherOPV->d->parameter.get(), - criterion) && - d->parameterValue->_isEquivalentTo(otherOPV->d->parameterValue.get(), - criterion); + if (!d->parameter->_isEquivalentTo(otherOPV->d->parameter.get(), + criterion)) { + return false; + } + if (criterion == util::IComparable::Criterion::STRICT) { + return d->parameterValue->_isEquivalentTo( + otherOPV->d->parameterValue.get(), criterion); + } + if (d->parameterValue->_isEquivalentTo(otherOPV->d->parameterValue.get(), + criterion)) { + return true; + } + if (d->parameter->getEPSGCode() == + EPSG_CODE_PARAMETER_AZIMUTH_INITIAL_LINE || + d->parameter->getEPSGCode() == + EPSG_CODE_PARAMETER_ANGLE_RECTIFIED_TO_SKEW_GRID) { + if (parameterValue()->type() == ParameterValue::Type::MEASURE && + otherOPV->parameterValue()->type() == + ParameterValue::Type::MEASURE) { + const double a = std::fmod(parameterValue()->value().convertToUnit( + common::UnitOfMeasure::DEGREE) + + 360.0, + 360.0); + const double b = + std::fmod(otherOPV->parameterValue()->value().convertToUnit( + common::UnitOfMeasure::DEGREE) + + 360.0, + 360.0); + return std::fabs(a - b) <= 1e-10 * std::fabs(a); + } + } + return false; } //! @endcond @@ -1187,8 +1215,17 @@ bool OperationParameter::_isEquivalentTo( const util::IComparable *other, util::IComparable::Criterion criterion) const { auto otherOP = dynamic_cast<const OperationParameter *>(other); - return otherOP != nullptr && - IdentifiedObject::_isEquivalentTo(other, criterion); + if (otherOP == nullptr) { + return false; + } + if (criterion == util::IComparable::Criterion::STRICT) { + return IdentifiedObject::_isEquivalentTo(other, criterion); + } + if (IdentifiedObject::_isEquivalentTo(other, criterion)) { + return true; + } + auto l_epsgCode = getEPSGCode(); + return l_epsgCode != 0 && l_epsgCode == otherOP->getEPSGCode(); } //! @endcond @@ -1449,6 +1486,13 @@ static SingleOperationNNPtr createPROJBased( bool SingleOperation::_isEquivalentTo( const util::IComparable *other, util::IComparable::Criterion criterion) const { + return _isEquivalentTo(other, criterion, false); +} + +bool SingleOperation::_isEquivalentTo(const util::IComparable *other, + util::IComparable::Criterion criterion, + bool inOtherDirection) const { + auto otherSO = dynamic_cast<const SingleOperation *>(other); if (otherSO == nullptr || (criterion == util::IComparable::Criterion::STRICT && @@ -1457,13 +1501,18 @@ bool SingleOperation::_isEquivalentTo( } const int methodEPSGCode = d->method_->getEPSGCode(); - if (!d->method_->_isEquivalentTo(otherSO->d->method_.get(), criterion)) { + const int otherMethodEPSGCode = otherSO->d->method_->getEPSGCode(); + + const bool equivalentMethods = + (criterion == util::IComparable::Criterion::EQUIVALENT && + methodEPSGCode != 0 && methodEPSGCode == otherMethodEPSGCode) || + d->method_->_isEquivalentTo(otherSO->d->method_.get(), criterion); + + if (!equivalentMethods) { if (criterion == util::IComparable::Criterion::EQUIVALENT) { // _1SP methods can sometimes be equivalent to _2SP ones // Check it by using convertToOtherMethod() - const int otherMethodEPSGCode = otherSO->d->method_->getEPSGCode(); - if ((methodEPSGCode == EPSG_CODE_METHOD_GEOCENTRIC_TRANSLATION_GEOCENTRIC && (otherMethodEPSGCode == @@ -1548,10 +1597,11 @@ bool SingleOperation::_isEquivalentTo( const auto &values = d->parameterValues_; const auto &otherValues = otherSO->d->parameterValues_; const auto valuesSize = values.size(); - if (valuesSize != otherValues.size()) { - return false; - } + const auto otherValuesSize = otherValues.size(); if (criterion == util::IComparable::Criterion::STRICT) { + if (valuesSize != otherValuesSize) { + return false; + } for (size_t i = 0; i < valuesSize; i++) { if (!values[i]->_isEquivalentTo(otherValues[i].get(), criterion)) { return false; @@ -1560,60 +1610,99 @@ bool SingleOperation::_isEquivalentTo( return true; } - std::vector<bool> candidateIndices(valuesSize, true); + std::vector<bool> candidateIndices(otherValuesSize, true); bool equivalent = true; - for (size_t i = 0; i < valuesSize; i++) { - bool found = false; - for (size_t j = 0; j < valuesSize; j++) { + bool foundMissingArgs = valuesSize != otherValuesSize; + + for (size_t i = 0; equivalent && i < valuesSize; i++) { + auto opParamvalue = + dynamic_cast<const OperationParameterValue *>(values[i].get()); + if (!opParamvalue) + return false; + + equivalent = false; + bool sameNameDifferentValue = false; + for (size_t j = 0; j < otherValuesSize; j++) { if (candidateIndices[j] && values[i]->_isEquivalentTo(otherValues[j].get(), criterion)) { candidateIndices[j] = false; - found = true; + equivalent = true; break; - } - } - if (!found) { - equivalent = false; - if (methodEPSGCode == - EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP) { - // For LCC_2SP, the standard parallels can be switched and - // this will result in the same result. - auto opParamvalue = + } else if (candidateIndices[j]) { + auto otherOpParamvalue = dynamic_cast<const OperationParameterValue *>( - values[i].get()); - if (opParamvalue) { - const int paramEPSGCode = - opParamvalue->parameter()->getEPSGCode(); - if (paramEPSGCode == - EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL || - paramEPSGCode == - EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL) { - auto value_1st = parameterValue( - EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL); - auto value_2nd = parameterValue( - EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL); - if (value_1st && value_2nd) { - equivalent = - value_1st->_isEquivalentTo( - otherSO - ->parameterValue( - EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL) - .get(), - criterion) && - value_2nd->_isEquivalentTo( - otherSO - ->parameterValue( - EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL) - .get(), - criterion); - } - } + otherValues[j].get()); + if (!otherOpParamvalue) + return false; + sameNameDifferentValue = + opParamvalue->parameter()->_isEquivalentTo( + otherOpParamvalue->parameter().get(), criterion); + if (sameNameDifferentValue) { + candidateIndices[j] = false; + break; } } - if (!equivalent) { - break; + } + + if (!equivalent && + methodEPSGCode == EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP) { + // For LCC_2SP, the standard parallels can be switched and + // this will result in the same result. + const int paramEPSGCode = opParamvalue->parameter()->getEPSGCode(); + if (paramEPSGCode == + EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL || + paramEPSGCode == + EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL) { + auto value_1st = parameterValue( + EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL); + auto value_2nd = parameterValue( + EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL); + if (value_1st && value_2nd) { + equivalent = + value_1st->_isEquivalentTo( + otherSO + ->parameterValue( + EPSG_CODE_PARAMETER_LATITUDE_2ND_STD_PARALLEL) + .get(), + criterion) && + value_2nd->_isEquivalentTo( + otherSO + ->parameterValue( + EPSG_CODE_PARAMETER_LATITUDE_1ST_STD_PARALLEL) + .get(), + criterion); + } } } + + if (equivalent) { + continue; + } + + if (sameNameDifferentValue) { + break; + } + + // If there are parameters in this method not found in the other one, + // check that they are set to a default neutral value, that is 1 + // for scale, and 0 otherwise. + foundMissingArgs = true; + const auto &value = opParamvalue->parameterValue(); + if (value->type() != ParameterValue::Type::MEASURE) { + break; + } + if (value->value().unit().type() == + common::UnitOfMeasure::Type::SCALE) { + equivalent = value->value().getSIValue() == 1.0; + } else { + equivalent = value->value().getSIValue() == 0.0; + } + } + + // In the case the arguments don't perfectly match, try the reverse + // check. + if (equivalent && foundMissingArgs && !inOtherDirection) { + return otherSO->_isEquivalentTo(this, criterion, true); } // Equivalent formulations of 2SP can have different parameters diff --git a/test/unit/test_operation.cpp b/test/unit/test_operation.cpp index b5bd188c..66284b59 100644 --- a/test/unit/test_operation.cpp +++ b/test/unit/test_operation.cpp @@ -6690,3 +6690,253 @@ TEST(operation, three_param_equivalent_to_seven_param) { EXPECT_FALSE(three_param->isEquivalentTo( seven_param_non_eq.get(), IComparable::Criterion::EQUIVALENT)); } + +// --------------------------------------------------------------------------- + +TEST(operation, conversion_missing_parameter) { + + auto wkt1 = "PROJCS[\"NAD83(CSRS98) / UTM zone 20N (deprecated)\"," + " GEOGCS[\"NAD83(CSRS98)\"," + " DATUM[\"NAD83_Canadian_Spatial_Reference_System\"," + " SPHEROID[\"GRS 1980\",6378137,298.257222101," + " AUTHORITY[\"EPSG\",\"7019\"]]," + " AUTHORITY[\"EPSG\",\"6140\"]]," + " PRIMEM[\"Greenwich\",0," + " AUTHORITY[\"EPSG\",\"8901\"]]," + " UNIT[\"degree\",0.0174532925199433," + " AUTHORITY[\"EPSG\",\"9108\"]]," + " AUTHORITY[\"EPSG\",\"4140\"]]," + " PROJECTION[\"Transverse_Mercator\"]," + " PARAMETER[\"latitude_of_origin\",0]," + " PARAMETER[\"central_meridian\",-63]," + " PARAMETER[\"scale_factor\",0.9996]," + " PARAMETER[\"false_easting\",500000]," + " UNIT[\"metre\",1," + " AUTHORITY[\"EPSG\",\"9001\"]]," + " AUTHORITY[\"EPSG\",\"2038\"]," + " AXIS[\"Easting\",EAST]," + " AXIS[\"Northing\",NORTH]]"; + auto obj1 = WKTParser().createFromWKT(wkt1); + auto crs1 = nn_dynamic_pointer_cast<ProjectedCRS>(obj1); + ASSERT_TRUE(crs1 != nullptr); + + // Difference with wkt1: latitude_of_origin missing, but false_northing + // added to 0 + auto wkt2 = "PROJCS[\"NAD83(CSRS98) / UTM zone 20N (deprecated)\"," + " GEOGCS[\"NAD83(CSRS98)\"," + " DATUM[\"NAD83_Canadian_Spatial_Reference_System\"," + " SPHEROID[\"GRS 1980\",6378137,298.257222101," + " AUTHORITY[\"EPSG\",\"7019\"]]," + " AUTHORITY[\"EPSG\",\"6140\"]]," + " PRIMEM[\"Greenwich\",0," + " AUTHORITY[\"EPSG\",\"8901\"]]," + " UNIT[\"degree\",0.0174532925199433," + " AUTHORITY[\"EPSG\",\"9108\"]]," + " AUTHORITY[\"EPSG\",\"4140\"]]," + " PROJECTION[\"Transverse_Mercator\"]," + " PARAMETER[\"central_meridian\",-63]," + " PARAMETER[\"scale_factor\",0.9996]," + " PARAMETER[\"false_easting\",500000]," + " PARAMETER[\"false_northing\",0]," + " UNIT[\"metre\",1," + " AUTHORITY[\"EPSG\",\"9001\"]]," + " AUTHORITY[\"EPSG\",\"2038\"]," + " AXIS[\"Easting\",EAST]," + " AXIS[\"Northing\",NORTH]]"; + auto obj2 = WKTParser().createFromWKT(wkt2); + auto crs2 = nn_dynamic_pointer_cast<ProjectedCRS>(obj2); + ASSERT_TRUE(crs2 != nullptr); + + // Difference with wkt1: false_northing added to 0 + auto wkt3 = "PROJCS[\"NAD83(CSRS98) / UTM zone 20N (deprecated)\"," + " GEOGCS[\"NAD83(CSRS98)\"," + " DATUM[\"NAD83_Canadian_Spatial_Reference_System\"," + " SPHEROID[\"GRS 1980\",6378137,298.257222101," + " AUTHORITY[\"EPSG\",\"7019\"]]," + " AUTHORITY[\"EPSG\",\"6140\"]]," + " PRIMEM[\"Greenwich\",0," + " AUTHORITY[\"EPSG\",\"8901\"]]," + " UNIT[\"degree\",0.0174532925199433," + " AUTHORITY[\"EPSG\",\"9108\"]]," + " AUTHORITY[\"EPSG\",\"4140\"]]," + " PROJECTION[\"Transverse_Mercator\"]," + " PARAMETER[\"latitude_of_origin\",0]," + " PARAMETER[\"central_meridian\",-63]," + " PARAMETER[\"scale_factor\",0.9996]," + " PARAMETER[\"false_easting\",500000]," + " PARAMETER[\"false_northing\",0]," + " UNIT[\"metre\",1," + " AUTHORITY[\"EPSG\",\"9001\"]]," + " AUTHORITY[\"EPSG\",\"2038\"]," + " AXIS[\"Easting\",EAST]," + " AXIS[\"Northing\",NORTH]]"; + auto obj3 = WKTParser().createFromWKT(wkt3); + auto crs3 = nn_dynamic_pointer_cast<ProjectedCRS>(obj3); + ASSERT_TRUE(crs3 != nullptr); + + // Difference with wkt1: UNKNOWN added to non-zero + auto wkt4 = "PROJCS[\"NAD83(CSRS98) / UTM zone 20N (deprecated)\"," + " GEOGCS[\"NAD83(CSRS98)\"," + " DATUM[\"NAD83_Canadian_Spatial_Reference_System\"," + " SPHEROID[\"GRS 1980\",6378137,298.257222101," + " AUTHORITY[\"EPSG\",\"7019\"]]," + " AUTHORITY[\"EPSG\",\"6140\"]]," + " PRIMEM[\"Greenwich\",0," + " AUTHORITY[\"EPSG\",\"8901\"]]," + " UNIT[\"degree\",0.0174532925199433," + " AUTHORITY[\"EPSG\",\"9108\"]]," + " AUTHORITY[\"EPSG\",\"4140\"]]," + " PROJECTION[\"Transverse_Mercator\"]," + " PARAMETER[\"latitude_of_origin\",0]," + " PARAMETER[\"central_meridian\",-63]," + " PARAMETER[\"scale_factor\",0.9996]," + " PARAMETER[\"false_easting\",500000]," + " PARAMETER[\"false_northing\",0]," + " PARAMETER[\"UNKNOWN\",13]," + " UNIT[\"metre\",1," + " AUTHORITY[\"EPSG\",\"9001\"]]," + " AUTHORITY[\"EPSG\",\"2038\"]," + " AXIS[\"Easting\",EAST]," + " AXIS[\"Northing\",NORTH]]"; + auto obj4 = WKTParser().createFromWKT(wkt4); + auto crs4 = nn_dynamic_pointer_cast<ProjectedCRS>(obj4); + ASSERT_TRUE(crs4 != nullptr); + + // Difference with wkt1: latitude_of_origin missing, but false_northing + // added to non-zero + auto wkt5 = "PROJCS[\"NAD83(CSRS98) / UTM zone 20N (deprecated)\"," + " GEOGCS[\"NAD83(CSRS98)\"," + " DATUM[\"NAD83_Canadian_Spatial_Reference_System\"," + " SPHEROID[\"GRS 1980\",6378137,298.257222101," + " AUTHORITY[\"EPSG\",\"7019\"]]," + " AUTHORITY[\"EPSG\",\"6140\"]]," + " PRIMEM[\"Greenwich\",0," + " AUTHORITY[\"EPSG\",\"8901\"]]," + " UNIT[\"degree\",0.0174532925199433," + " AUTHORITY[\"EPSG\",\"9108\"]]," + " AUTHORITY[\"EPSG\",\"4140\"]]," + " PROJECTION[\"Transverse_Mercator\"]," + " PARAMETER[\"central_meridian\",-63]," + " PARAMETER[\"scale_factor\",0.9996]," + " PARAMETER[\"false_easting\",500000]," + " PARAMETER[\"false_northing\",-99999]," + " UNIT[\"metre\",1," + " AUTHORITY[\"EPSG\",\"9001\"]]," + " AUTHORITY[\"EPSG\",\"2038\"]," + " AXIS[\"Easting\",EAST]," + " AXIS[\"Northing\",NORTH]]"; + auto obj5 = WKTParser().createFromWKT(wkt5); + auto crs5 = nn_dynamic_pointer_cast<ProjectedCRS>(obj5); + ASSERT_TRUE(crs5 != nullptr); + + EXPECT_TRUE( + crs1->isEquivalentTo(crs2.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_TRUE( + crs2->isEquivalentTo(crs1.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_TRUE( + crs1->isEquivalentTo(crs3.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_TRUE( + crs3->isEquivalentTo(crs1.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_TRUE( + crs2->isEquivalentTo(crs3.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_TRUE( + crs3->isEquivalentTo(crs2.get(), IComparable::Criterion::EQUIVALENT)); + + EXPECT_FALSE( + crs1->isEquivalentTo(crs4.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_FALSE( + crs4->isEquivalentTo(crs1.get(), IComparable::Criterion::EQUIVALENT)); + + EXPECT_FALSE( + crs1->isEquivalentTo(crs5.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_FALSE( + crs5->isEquivalentTo(crs1.get(), IComparable::Criterion::EQUIVALENT)); +} + +// --------------------------------------------------------------------------- + +TEST(operation, conversion_missing_parameter_scale) { + + auto wkt1 = "PROJCS[\"test\",\n" + " GEOGCS[\"WGS 84\",\n" + " DATUM[\"WGS 1984\",\n" + " SPHEROID[\"WGS 84\",6378137,298.257223563]],\n" + " PRIMEM[\"Greenwich\",0],\n" + " UNIT[\"degree\",0.0174532925199433]],\n" + " PROJECTION[\"Mercator_1SP\"],\n" + " PARAMETER[\"latitude_of_origin\",-1],\n" + " PARAMETER[\"central_meridian\",2],\n" + " PARAMETER[\"scale_factor\",1],\n" + " PARAMETER[\"false_easting\",3],\n" + " PARAMETER[\"false_northing\",4],\n" + " UNIT[\"metre\",1]]"; + + auto obj1 = WKTParser().createFromWKT(wkt1); + auto crs1 = nn_dynamic_pointer_cast<ProjectedCRS>(obj1); + ASSERT_TRUE(crs1 != nullptr); + + // Difference with wkt1: scale_factor missing + auto wkt2 = "PROJCS[\"test\",\n" + " GEOGCS[\"WGS 84\",\n" + " DATUM[\"WGS 1984\",\n" + " SPHEROID[\"WGS 84\",6378137,298.257223563]],\n" + " PRIMEM[\"Greenwich\",0],\n" + " UNIT[\"degree\",0.0174532925199433]],\n" + " PROJECTION[\"Mercator_1SP\"],\n" + " PARAMETER[\"latitude_of_origin\",-1],\n" + " PARAMETER[\"central_meridian\",2],\n" + " PARAMETER[\"false_easting\",3],\n" + " PARAMETER[\"false_northing\",4],\n" + " UNIT[\"metre\",1]]"; + + auto obj2 = WKTParser().createFromWKT(wkt2); + auto crs2 = nn_dynamic_pointer_cast<ProjectedCRS>(obj2); + ASSERT_TRUE(crs2 != nullptr); + + // Difference with wkt1: scale_factor set to non-1 + auto wkt3 = "PROJCS[\"test\",\n" + " GEOGCS[\"WGS 84\",\n" + " DATUM[\"WGS 1984\",\n" + " SPHEROID[\"WGS 84\",6378137,298.257223563]],\n" + " PRIMEM[\"Greenwich\",0],\n" + " UNIT[\"degree\",0.0174532925199433]],\n" + " PROJECTION[\"Mercator_1SP\"],\n" + " PARAMETER[\"latitude_of_origin\",-1],\n" + " PARAMETER[\"central_meridian\",2],\n" + " PARAMETER[\"scale_factor\",-1],\n" + " PARAMETER[\"false_easting\",3],\n" + " PARAMETER[\"false_northing\",4],\n" + " UNIT[\"metre\",1]]"; + auto obj3 = WKTParser().createFromWKT(wkt3); + auto crs3 = nn_dynamic_pointer_cast<ProjectedCRS>(obj3); + ASSERT_TRUE(crs3 != nullptr); + + EXPECT_TRUE( + crs1->isEquivalentTo(crs2.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_TRUE( + crs2->isEquivalentTo(crs1.get(), IComparable::Criterion::EQUIVALENT)); + + EXPECT_FALSE( + crs1->isEquivalentTo(crs3.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_FALSE( + crs3->isEquivalentTo(crs1.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_FALSE( + crs2->isEquivalentTo(crs3.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_FALSE( + crs3->isEquivalentTo(crs2.get(), IComparable::Criterion::EQUIVALENT)); +} + +// --------------------------------------------------------------------------- + +TEST(operation, + hotine_oblique_mercator_variant_A_export_equivalent_modulo_360) { + auto conv1 = Conversion::createHotineObliqueMercatorVariantA( + PropertyMap(), Angle(1), Angle(2), Angle(-3), Angle(-4), Scale(5), + Length(6), Length(7)); + auto conv2 = Conversion::createHotineObliqueMercatorVariantA( + PropertyMap(), Angle(1), Angle(2), Angle(-3 + 360), Angle(-4 + 360), + Scale(5), Length(6), Length(7)); + + EXPECT_TRUE( + conv1->isEquivalentTo(conv2.get(), IComparable::Criterion::EQUIVALENT)); +} |
