aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/proj/coordinateoperation.hpp4
-rw-r--r--src/coordinateoperation.cpp201
-rw-r--r--test/unit/test_operation.cpp250
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));
+}