From 20f10da961a17c03207094fdffd96b3a137dc9c9 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 8 May 2026 19:58:24 +0200 Subject: [PATCH 01/10] Revert "Adjust previous commits" This reverts commit a818559c4241b5d12bffef081633d1ff8ee01166. --- scripts/build_db.py | 3 --- src/iso19111/operation/coordinateoperationfactory.cpp | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/build_db.py b/scripts/build_db.py index 69b10a5462..777e2350ba 100755 --- a/scripts/build_db.py +++ b/scripts/build_db.py @@ -1172,9 +1172,6 @@ def fill_alias(proj_db_cursor): proj_db_cursor.execute("SELECT DISTINCT object_code, alias, coord_op_name FROM epsg.epsg_alias, epsg.epsg_coordoperation ON coord_op_code = object_code WHERE object_table_name = 'epsg_coordoperation' AND epsg_coordoperation.deprecated = 0") for row in proj_db_cursor.fetchall(): code, alt_name, new_name = row - # We could potentially ingest all records, but the only use of them for - # now is to workaround effects of the creation of national ETRS89-XXX - # datums. See https://github.com/OSGeo/PROJ/pull/4736 for more details if "ETRS89" not in alt_name: # print('Ignoring alias %s for coordinate operation %s %s' % (alt_name, code, new_name)) continue diff --git a/src/iso19111/operation/coordinateoperationfactory.cpp b/src/iso19111/operation/coordinateoperationfactory.cpp index 9e4f427f33..9ea42853e8 100644 --- a/src/iso19111/operation/coordinateoperationfactory.cpp +++ b/src/iso19111/operation/coordinateoperationfactory.cpp @@ -4280,7 +4280,8 @@ bool CoordinateOperationFactory::Private::createOperationsFromDatabase( // "MGI to ETRS89 (8)" operation, that is now "MGI to ETRS89-AUT [2002] (8)" const bool srcIsETRS89 = sourceCRS->nameStr() == "ETRS89"; const bool dstIsETRS89 = targetCRS->nameStr() == "ETRS89"; - if (geodSrc && geodDst && (srcIsETRS89 != dstIsETRS89)) { + if (geodSrc && geodDst && (srcIsETRS89 || dstIsETRS89) && + !(srcIsETRS89 && dstIsETRS89)) { const auto &authFactory = context.context->getAuthorityFactory(); const auto gridAvailabilityUse = context.context->getGridAvailabilityUse(); From 135834b2a785907f74e9ad4566ac4b4c7950b67f Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 8 May 2026 19:58:39 +0200 Subject: [PATCH 02/10] Revert "Yet another MEGA (Make ETRS89 Great Again) fix!" This reverts commit 4982d17e7e555f3e2002976aecf6fb08b55e374e. --- include/proj/io.hpp | 8 -- src/iso19111/factory.cpp | 56 -------------- .../operation/coordinateoperationfactory.cpp | 74 ------------------- test/unit/gie_self_tests.cpp | 12 +-- test/unit/test_network.cpp | 9 +-- test/unit/test_operationfactory.cpp | 53 ++++++------- 6 files changed, 32 insertions(+), 180 deletions(-) diff --git a/include/proj/io.hpp b/include/proj/io.hpp index cbe184ffdc..fbbb3649bc 100644 --- a/include/proj/io.hpp +++ b/include/proj/io.hpp @@ -1339,14 +1339,6 @@ class PROJ_GCC_DLL AuthorityFactory { const metadata::ExtentPtr &intersectingExtent2, bool skipIntermediateExtentIntersection = false) const; - PROJ_INTERNAL std::vector - getOperationsFromAlias(const std::string &crs1Name, - const std::string &crs2Name, - bool usePROJAlternativeGridNames, - bool discardIfMissingGrid, - bool considerKnownGridsAsAvailable, - bool discardSuperseded) const; - typedef std::pair PairObjectName; PROJ_INTERNAL std::list diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp index a12d945ba2..2ddd4dd6aa 100644 --- a/src/iso19111/factory.cpp +++ b/src/iso19111/factory.cpp @@ -10596,62 +10596,6 @@ AuthorityFactory::getPointMotionOperationsFor( return res; } -// --------------------------------------------------------------------------- - -std::vector -AuthorityFactory::getOperationsFromAlias(const std::string &crs1Name, - const std::string &crs2Name, - bool usePROJAlternativeGridNames, - bool discardIfMissingGrid, - bool considerKnownGridsAsAvailable, - bool discardSuperseded) const { - std::string sql("SELECT alias.auth_name, alias.code FROM alias_name alias " - "JOIN coordinate_operation_view cov " - "ON alias.table_name = cov.table_name " - "AND alias.auth_name = cov.auth_name " - "AND alias.code = cov.code " - "WHERE alias.table_name IN ('grid_transformation', " - "'helmert_transformation', 'other_transformation', " - "'concatenated_operation') " - "AND (alt_name LIKE ? OR alt_name LIKE ?) " - "AND NOT (alt_name LIKE ? OR alt_name LIKE ? OR alt_name " - "LIKE ? OR alt_name LIKE ?) " - "AND cov.deprecated = 0"); - if (discardSuperseded) { - sql += " AND NOT EXISTS (SELECT 1 FROM supersession ss WHERE " - "ss.superseded_table_name = cov.table_name AND " - "ss.superseded_auth_name = cov.auth_name AND " - "ss.superseded_code = cov.code AND " - "ss.superseded_table_name = ss.replacement_table_name AND " - "ss.same_source_target_crs = 1)"; - } - ListOfParams params{ - std::string(crs1Name).append(" to ").append(crs2Name).append("%"), - std::string(crs2Name).append(" to ").append(crs1Name).append("%"), - // If looking for "MGI to ETRS89", don't match "ETRS89 to ETRS89-XXX" - std::string(crs1Name).append(" to ").append(crs1Name).append("-%"), - std::string(crs2Name).append(" to ").append(crs2Name).append("-%"), - // If looking for "MGI to ETRS89", don't match "MGI to ETRS89-XXX" (not - // an actual example, but just in case) - std::string(crs1Name).append(" to ").append(crs2Name).append("-%"), - std::string(crs2Name).append(" to ").append(crs1Name).append("-%"), - }; - - std::vector res; - auto sqlRes = d->run(sql, params); - for (const auto &row : sqlRes) { - const auto &auth_name = row[0]; - const auto &code = row[1]; - auto op = d->createFactory(auth_name)->createCoordinateOperation( - code, usePROJAlternativeGridNames); - if (!discardIfMissingGrid || - !d->rejectOpDueToMissingGrid(op, considerKnownGridsAsAvailable)) { - res.emplace_back(op); - } - } - return res; -} - //! @endcond // --------------------------------------------------------------------------- diff --git a/src/iso19111/operation/coordinateoperationfactory.cpp b/src/iso19111/operation/coordinateoperationfactory.cpp index 9ea42853e8..d14c8bd694 100644 --- a/src/iso19111/operation/coordinateoperationfactory.cpp +++ b/src/iso19111/operation/coordinateoperationfactory.cpp @@ -4272,80 +4272,6 @@ bool CoordinateOperationFactory::Private::createOperationsFromDatabase( } } - // ETRS89 specific hack to deal with the fact that a lot of ETRS89-XXX - // datum/CRS have been created, with older transformations going from/to - // generic ETRS89 being re-attributed to that specific ETRS89-XXX datum/CRS - // which breaks backwards compatibility - // e.g we want ETRS89 to MGI to be able to use the previously named - // "MGI to ETRS89 (8)" operation, that is now "MGI to ETRS89-AUT [2002] (8)" - const bool srcIsETRS89 = sourceCRS->nameStr() == "ETRS89"; - const bool dstIsETRS89 = targetCRS->nameStr() == "ETRS89"; - if (geodSrc && geodDst && (srcIsETRS89 || dstIsETRS89) && - !(srcIsETRS89 && dstIsETRS89)) { - const auto &authFactory = context.context->getAuthorityFactory(); - const auto gridAvailabilityUse = - context.context->getGridAvailabilityUse(); - const auto opFromAlias = authFactory->getOperationsFromAlias( - sourceCRS->nameStr(), targetCRS->nameStr(), - context.context->getUsePROJAlternativeGridNames(), - /* discardIfMissingGrid = */ - gridAvailabilityUse == - CoordinateOperationContext::GridAvailabilityUse:: - DISCARD_OPERATION_IF_MISSING_GRID || - gridAvailabilityUse == CoordinateOperationContext:: - GridAvailabilityUse::KNOWN_AVAILABLE, - /* considerKnownGridsAsAvailable = */ - gridAvailabilityUse == CoordinateOperationContext:: - GridAvailabilityUse::KNOWN_AVAILABLE, - context.context->getDiscardSuperseded()); - for (const auto &aliasOp : opFromAlias) { - const auto aliasOpSourceCRS = aliasOp->sourceCRS(); - const auto aliasOpTargetCRS = aliasOp->targetCRS(); - if (aliasOpSourceCRS && aliasOpTargetCRS && - (aliasOpSourceCRS->nameStr().find("ETRS89-") != - std::string::npos || - aliasOpTargetCRS->nameStr().find("ETRS89-") != - std::string::npos)) { - const bool bSourceAliasIsEquivalentToSourceCRS = starts_with( - aliasOpSourceCRS->nameStr(), sourceCRS->nameStr()); - auto newOp = aliasOp->shallowClone(); - // Patch the transformation to modify its source/target CRS - // to the expected one - if (bSourceAliasIsEquivalentToSourceCRS) { - setCRSs(newOp.get(), sourceCRS, targetCRS); - } else { - setCRSs(newOp.get(), targetCRS, sourceCRS); - newOp = newOp->inverse(); - } - - const auto getNatureOf = - [](const crs::GeodeticCRS &crs) -> std::string { - if (dynamic_cast(&crs) != - nullptr) { - if (crs.coordinateSystem()->axisList().size() == 2) - return "geographic2D"; - else - return "geographic3D"; - } else { - return "geodetic"; - } - }; - // TODO? Ideally we'd create 2D<-->3D<-->geocentric conversions - // when needed - const auto newOpSrc = dynamic_cast( - newOp->sourceCRS().get()); - const auto newOpDst = dynamic_cast( - newOp->targetCRS().get()); - if (newOpSrc && newOpDst && - getNatureOf(*newOpSrc) == getNatureOf(*geodSrc) && - getNatureOf(*newOpDst) == getNatureOf(*geodDst)) { - res.emplace_back(newOp); - doFilterAndCheckPerfectOp = true; - } - } - } - } - if (doFilterAndCheckPerfectOp) { // If we get at least a result with perfect accuracy, do not bother // generating synthetic transforms. diff --git a/test/unit/gie_self_tests.cpp b/test/unit/gie_self_tests.cpp index 2db5366ce3..81b01c58d9 100644 --- a/test/unit/gie_self_tests.cpp +++ b/test/unit/gie_self_tests.cpp @@ -1297,15 +1297,15 @@ TEST(gie, proj_create_crs_to_crs_PULKOVO42_ETRS89) { c.xyzt.z = 0; c.xyzt.t = HUGE_VAL; c = proj_trans(P, PJ_FWD, c); - EXPECT_NEAR(c.xy.x, 44.999701238, 1e-9); - EXPECT_NEAR(c.xy.y, 24.998474948, 1e-9); + EXPECT_NEAR(c.xy.x, 45.000346961, 1e-9); + EXPECT_NEAR(c.xy.y, 25.001524075, 1e-9); EXPECT_EQ(std::string(proj_pj_info(P).definition), "proj=pipeline step proj=axisswap order=2,1 " "step proj=unitconvert xy_in=deg xy_out=rad " "step proj=push v_3 " "step proj=cart " - "ellps=krass step proj=helmert x=2.3287 y=-147.0425 z=-92.0802 " - "rx=0.3092483 ry=-0.32482185 rz=-0.49729934 s=5.68906266 " + "ellps=krass step proj=helmert x=68.1564 y=32.7756 z=80.2249 " + "rx=2.20333014 ry=2.19256447 rz=-2.54166911 s=-0.14155333 " "convention=coordinate_frame step inv proj=cart ellps=GRS80 " "step proj=pop v_3 " "step proj=unitconvert xy_in=rad xy_out=deg step proj=axisswap " @@ -1322,8 +1322,8 @@ TEST(gie, proj_create_crs_to_crs_PULKOVO42_ETRS89) { proj_trans_generic(P, PJ_FWD, &(c.xyz.x), sizeof(double), 1, &(c.xyz.y), sizeof(double), 1, &(c.xyz.z), sizeof(double), 1, nullptr, 0, 0); - EXPECT_NEAR(c.xy.x, 44.999701238, 1e-9); - EXPECT_NEAR(c.xy.y, 24.998474948, 1e-9); + EXPECT_NEAR(c.xy.x, 45.000346961, 1e-9); + EXPECT_NEAR(c.xy.y, 25.001524075, 1e-9); // Poland c.xyz.x = 52; // Lat diff --git a/test/unit/test_network.cpp b/test/unit/test_network.cpp index c3b664ffaf..ecced83351 100644 --- a/test/unit/test_network.cpp +++ b/test/unit/test_network.cpp @@ -1386,8 +1386,7 @@ TEST(networking, network_endpoint_api_and_not_reachable_hgridshift) { EXPECT_EQ(c2.xyz.x, HUGE_VAL); EXPECT_EQ(proj_errno(P), PROJ_ERR_OTHER_NETWORK_ERROR); PJ *last_op = proj_trans_get_last_used_operation(P); - EXPECT_STREQ(proj_pj_info(last_op).description, - "MGI to ETRS89-AUT [2002] (8)"); + EXPECT_STREQ(proj_pj_info(last_op).description, "MGI to ETRS89 (5)"); proj_destroy(last_op); } @@ -1399,8 +1398,7 @@ TEST(networking, network_endpoint_api_and_not_reachable_hgridshift) { EXPECT_EQ(c2.xyz.x, HUGE_VAL); EXPECT_EQ(proj_errno(P), PROJ_ERR_OTHER_NETWORK_ERROR); PJ *last_op = proj_trans_get_last_used_operation(P); - EXPECT_STREQ(proj_pj_info(last_op).description, - "MGI to ETRS89-AUT [2002] (8)"); + EXPECT_STREQ(proj_pj_info(last_op).description, "MGI to ETRS89 (5)"); proj_destroy(last_op); } @@ -1412,8 +1410,7 @@ TEST(networking, network_endpoint_api_and_not_reachable_hgridshift) { EXPECT_EQ(c2.xyz.x, HUGE_VAL); EXPECT_EQ(proj_errno(P), PROJ_ERR_OTHER_NETWORK_ERROR); PJ *last_op = proj_trans_get_last_used_operation(P); - EXPECT_STREQ(proj_pj_info(last_op).description, - "MGI to ETRS89-AUT [2002] (8)"); + EXPECT_STREQ(proj_pj_info(last_op).description, "MGI to ETRS89 (5)"); proj_destroy(last_op); } diff --git a/test/unit/test_operationfactory.cpp b/test/unit/test_operationfactory.cpp index b13afda4ec..97c22c7026 100644 --- a/test/unit/test_operationfactory.cpp +++ b/test/unit/test_operationfactory.cpp @@ -95,24 +95,20 @@ TEST(operation, geogCRS_to_geogCRS_context_default) { authFactory->createCoordinateReferenceSystem("4179"), // Pulkovo 42 authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 ctxt); - ASSERT_EQ(list.size(), 4U); + ASSERT_EQ(list.size(), 3U); // Romania has a larger area than Poland (given our approx formula) - EXPECT_EQ( - list[0]->nameStr(), - "Pulkovo 1942(58) to ETRS89-ROU [ETRF2000] (4)"); // Romania - 10m - EXPECT_EQ(list[0]->getEPSGCode(), 15994); // Romania - 3m - EXPECT_EQ(list[1]->getEPSGCode(), 15993); // Romania - 10m - EXPECT_EQ(list[2]->getEPSGCode(), 1644); // Poland - 1m - EXPECT_EQ(list[3]->nameStr(), + EXPECT_EQ(list[0]->getEPSGCode(), 15993); // Romania - 10m + EXPECT_EQ(list[1]->getEPSGCode(), 1644); // Poland - 1m + EXPECT_EQ(list[2]->nameStr(), "Ballpark geographic offset from Pulkovo 1942(58) to ETRS89"); EXPECT_EQ( list[0]->exportToPROJString(PROJStringFormatter::create().get()), "+proj=pipeline +step +proj=axisswap +order=2,1 +step " "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 " - "+step +proj=cart +ellps=krass +step +proj=helmert +x=2.3287 " - "+y=-147.0425 +z=-92.0802 +rx=0.3092483 +ry=-0.32482185 " - "+rz=-0.49729934 +s=5.68906266 +convention=coordinate_frame +step " + "+step +proj=cart +ellps=krass +step +proj=helmert +x=68.1564 " + "+y=32.7756 +z=80.2249 +rx=2.20333014 +ry=2.19256447 " + "+rz=-2.54166911 +s=-0.14155333 +convention=coordinate_frame +step " "+inv +proj=cart +ellps=GRS80 +step +proj=pop +v_3 +step " "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " "+order=2,1"); @@ -123,21 +119,18 @@ TEST(operation, geogCRS_to_geogCRS_context_default) { auto list = CoordinateOperationFactory::create()->createOperations( authFactory->createCoordinateReferenceSystem("4258"), authFactory->createCoordinateReferenceSystem("4179"), ctxt); - ASSERT_EQ(list.size(), 4U); + ASSERT_EQ(list.size(), 3U); // Romania has a larger area than Poland (given our approx formula) - EXPECT_EQ( - list[0]->nameStr(), - "Inverse of Pulkovo 1942(58) to ETRS89-ROU [ETRF2000] (4)"); // Romania - // - - // 10m + EXPECT_EQ(list[0]->nameStr(), + "Inverse of Pulkovo 1942(58) to ETRS89 (3)"); // Romania - 10m EXPECT_EQ( list[0]->exportToPROJString(PROJStringFormatter::create().get()), "+proj=pipeline +step +proj=axisswap +order=2,1 +step " "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 " - "+step +proj=cart +ellps=GRS80 +step +inv +proj=helmert +x=2.3287 " - "+y=-147.0425 +z=-92.0802 +rx=0.3092483 +ry=-0.32482185 " - "+rz=-0.49729934 +s=5.68906266 +convention=coordinate_frame +step " + "+step +proj=cart +ellps=GRS80 +step +inv +proj=helmert +x=68.1564 " + "+y=32.7756 +z=80.2249 +rx=2.20333014 +ry=2.19256447 " + "+rz=-2.54166911 +s=-0.14155333 +convention=coordinate_frame +step " "+inv +proj=cart +ellps=krass +step +proj=pop +v_3 +step " "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " "+order=2,1"); @@ -215,9 +208,8 @@ TEST(operation, geogCRS_to_geogCRS_context_filter_bbox) { auto list = CoordinateOperationFactory::create()->createOperations( authFactory->createCoordinateReferenceSystem("4179"), authFactory->createCoordinateReferenceSystem("4258"), ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ(list[0]->getEPSGCode(), 15994); // Romania - 3m - EXPECT_EQ(list[1]->getEPSGCode(), 15993); // Romania - 10m + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->getEPSGCode(), 15993); // Romania - 10m } { auto ctxt = CoordinateOperationContext::create( @@ -228,9 +220,8 @@ TEST(operation, geogCRS_to_geogCRS_context_filter_bbox) { auto list = CoordinateOperationFactory::create()->createOperations( authFactory->createCoordinateReferenceSystem("4179"), authFactory->createCoordinateReferenceSystem("4258"), ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ(list[0]->getEPSGCode(), 15994); // Romania - 3m - EXPECT_EQ(list[1]->getEPSGCode(), 15993); // Romania - 10m + ASSERT_EQ(list.size(), 1U); + EXPECT_EQ(list[0]->getEPSGCode(), 15993); // Romania - 10m } { auto ctxt = CoordinateOperationContext::create( @@ -1519,7 +1510,7 @@ TEST(operation, geogCRS_without_id_to_geogCRS_3D_context) { auto list = CoordinateOperationFactory::create()->createOperations(src, dst, ctxt); ASSERT_GE(list.size(), 1U); - auto wkt2 = "GEOGCRS[\"Amersfoort\",\n" + auto wkt2 = "GEOGCRS[\"unnamed\",\n" " DATUM[\"Amersfoort\",\n" " ELLIPSOID[\"Bessel 1841\",6377397.155,299.1528128,\n" " LENGTHUNIT[\"metre\",1]]],\n" @@ -12363,9 +12354,10 @@ TEST(operation, createOperation_ETRS89_to_Amersfoort) { // Amersfoort authFactoryEPSG->createCoordinateReferenceSystem("4289"), ctxt); ASSERT_GE(list.size(), 1U); - // We check that we use the most + // We check that we go through ETRS89-NLD [AGRS2010] to use the most // precise "Amersfoort to ETRS89-NLD [AGRS2010] (9)" operation. EXPECT_EQ(list[0]->nameStr(), + "ETRS89 to ETRS89-NLD [AGRS2010] + " "Inverse of Amersfoort to ETRS89-NLD [AGRS2010] (9)"); } { @@ -12375,9 +12367,10 @@ TEST(operation, createOperation_ETRS89_to_Amersfoort) { // ETRS89 authFactoryEPSG->createCoordinateReferenceSystem("4258"), ctxt); ASSERT_GE(list.size(), 1U); - // We check that we use the most + // We check that we go through ETRS89-NLD [AGRS2010] to use the most // precise "Amersfoort to ETRS89-NLD [AGRS2010] (9)" operation. EXPECT_EQ(list[0]->nameStr(), - "Amersfoort to ETRS89-NLD [AGRS2010] (9)"); + "Amersfoort to ETRS89-NLD [AGRS2010] (9) + " + "Inverse of ETRS89 to ETRS89-NLD [AGRS2010]"); } } From e79c5c5a3a18a7b219d2a63d3ab9e907fec7f1b7 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 8 May 2026 20:03:42 +0200 Subject: [PATCH 03/10] Revert "Database: import aliases for coordinate operations involving ETRS89" This reverts commit 94be2a358f924aac3dee384088c15ee3b8da3b19. --- data/sql/alias_name.sql | 269 ------------------------------ data/sql/proj.db.sql.expected.md5 | 2 +- scripts/build_db.py | 17 -- 3 files changed, 1 insertion(+), 287 deletions(-) diff --git a/data/sql/alias_name.sql b/data/sql/alias_name.sql index 0f4f85315b..ce13f25a47 100644 --- a/data/sql/alias_name.sql +++ b/data/sql/alias_name.sql @@ -8560,272 +8560,3 @@ INSERT INTO "alias_name" VALUES('compound_crs','EPSG','6697','JGD2011 + JGD2011 INSERT INTO "alias_name" VALUES('vertical_crs','EPSG','11317','JGD2024 (vertical) - OHt','EPSG'); INSERT INTO "alias_name" VALUES('vertical_crs','EPSG','11454','GLLAT ensemble depth','EPSG'); INSERT INTO "alias_name" VALUES('vertical_crs','EPSG','11455','GLMSL ensemble depth','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1619','AT_MGI to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1622','CZ_S-JTSK to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1626','DK_ED50 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1628','GI_ED50 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1630','ES_ED50 (BAL99) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1632','ES_ED50 (EST99) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1634','ES_ED50 (ZNW99) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1783','TR_ED50 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1638','FI_KKJ to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1642','LU_LUREF to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1644','PL_42/58 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1646','CH_CH1903 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1647','CH_CH1903+ to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1650','FR_ED50 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1651','FR_NTF to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1653','NO_NGO1948 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1657','PT_D73 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1655','PT_DLX(HAY) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1652','BE_BD72 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1895','RT90 to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1953','(IE_Ireland65 to ETRS89 - see alias remarks)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1992','D73 to ETRS89 (3)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1655','Lisbon 1937 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1657','D73 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1655','DLx to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1997','DLx to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1997','Lisbon 1937 to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1663','Rome 1940 to ETRS89 (3)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1661','Rome 1940 to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1659','Rome 1940 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1642','Luxembourg 1930 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1078','Luxembourg 1930 to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10003','ETRS89 to IGN78 Corsica height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10001','ETRS89 to NGF IGN69 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','10098','(FI_KKJ to ETRS89)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1779','DE_DHDN (Middle) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1780','DE_DHDN (North) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1776','DE_DHDN (whole country, 2001) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1778','DE_DHDN (South) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1309','DE_DHDN (whole country, 1995) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1775','DE_42/83 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1314','GB_OSGB36 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1659','IT_ROMA40 (peninsular part) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1661','IT_ROMA40 (Sardinia) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1663','IT_ROMA40 (Sicily) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1751','NL_RD to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1992','PT_D73 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1997','PT_DLX(HAY) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1895','SE_RT90 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','15868','DE_RD/83 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','15867','DE_PD/83 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','15948','DE_DHDN (BeTA, 2007) to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10021','ETRS89 to Newlyn height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10029','ETRS89 to Newlyn (Orkney) height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','15993','S-42 to ETRS89 (3)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','15994','S-42 to ETRS89 (4)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1644','S-42 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1674','S-42 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1775','S-42 to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','3963','HR1901 to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','8367','S-JTSK [JTSK03] to ETRS89 [ETRF2000] (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','8365','ETRS89 [ETRF2000] to S-JTSK [JTSK03] (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','8361','ETRS89 [ETRF2000] to ETRS89 [ETRF2000] + Baltic 1957 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','8361','ETRS89 [ETRF2000] to ETRS89 [ETRF2000] + Bpv (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','8362','ETRS89 [ETRF2000] to ETRS89 [ETRF2000] + EVRF2007 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('concatenated_operation','EPSG','8442','ETRS89 [ETRF2000] to S-JTSK [JTSK]','EPSG'); -INSERT INTO "alias_name" VALUES('concatenated_operation','EPSG','8443','S-JTSK [JTSK] to ETRS89 [ETRF2000]','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','8680','HR_HDKS to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','3963','HR_HDKS to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','3914','SI_D48 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','3914','D48 to ETRS89 (3)','EPSG'); -INSERT INTO "alias_name" VALUES('other_transformation','EPSG','9041','ISN2004 / LAEA EU to ETRS89 / LAEA EU (1)','EPSG'); -INSERT INTO "alias_name" VALUES('other_transformation','EPSG','9042','ISN2004 / LCC EU to ETRS89 / LCC EU (1)','EPSG'); -INSERT INTO "alias_name" VALUES('other_transformation','EPSG','9043','ISN2016 / LAEA EU to ETRS89 / LAEA EU (1)','EPSG'); -INSERT INTO "alias_name" VALUES('other_transformation','EPSG','9044','ISN2016 / LCC EU to ETRS89 / LCC EU (1)','EPSG'); -INSERT INTO "alias_name" VALUES('other_transformation','EPSG','9048','REGCAN95 / LCC EU to ETRS89 / LCC EU (1)','EPSG'); -INSERT INTO "alias_name" VALUES('other_transformation','EPSG','9045','PTRA08 / LAEA EU to ETRS89 / LAEA EU (1)','EPSG'); -INSERT INTO "alias_name" VALUES('other_transformation','EPSG','9049','TUREF / LAEA EU to ETRS89 / LAEA EU (1)','EPSG'); -INSERT INTO "alias_name" VALUES('other_transformation','EPSG','9046','PTRA08 / LCC EU to ETRS89 / LCC EU (1)','EPSG'); -INSERT INTO "alias_name" VALUES('other_transformation','EPSG','9050','TUREF / LCC EU to ETRS89 / LCC EU (1)','EPSG'); -INSERT INTO "alias_name" VALUES('other_transformation','EPSG','9047','REGCAN95 / LAEA EU to ETRS89 / LAEA EU (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','7675','MGI 1901 to SRB_ETRS89 (6)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9282','RD Bessel to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','9281','RD Bessel to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','4829','SK_S-JTSK to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','4827','SK_S-JTSK to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5036','D73 to ETRS89(4)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5037','D73 to ETRS89(5)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5039','B DLx to ETRS89(1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5038','DLx to ETRS89(3)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5038','Lisbon 1937 to ETRS89 (3)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','5334','ETRS89 to Belfast Lough height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5483','Luxembourg 1930 to ETRS89 (4)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5485','Luxembourg 1930 to ETRS89 (3)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','6188','Lisbon 1937 to ETRS89 (4)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','6188','DLx to ETRS89 (4)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','6189','D73 to ETRS89(6)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','6205','MSCS to ETRS89 (5)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7958','ETRS89 to Belfast Lough height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7713','ETRS89 to Newlyn (Offshore) height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7711','ETRS89 to Newlyn height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7712','ETRS89 to Newlyn (Orkney) height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7709','OSGB 1936 to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','5338','OSGB 1936 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9908','ETRS89 to Oostende height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9909','ETRS89 to ETRS89 + Oostende height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5826','DB_REF2003 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5826','DB_REF2016 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','10098','KKJ to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('concatenated_operation','EPSG','10778','KKJ to ETRS89 (3)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10106','ETRS89 to SVD2006 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10107','ETRS89 to ETRS89 + SVD2006 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1653','NGO 1948 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10854','ETRS89 to NGO 1948 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9484','ETRS89 to NN54 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9594','ETRS89 to ETRS89 + NN54 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9485','ETRS89 to NN2000 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9593','ETRS89 to ETRS89 + NN2000 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9884','ETRS89 to CD Norway depth (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9885','ETRS89 to ETRS89 + CD Norway depth (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10130','ETRS89 to CD Norway depth (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10133','ETRS89 to ETRS89 + CD Norway depth (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10504','ETRS89 to CD Norway depth (3)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10505','ETRS89 to ETRS89 + CD Norway depth (3)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10509','ETRS89 to CD Norway depth (4)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10510','ETRS89 to ETRS89 + CD Norway depth (4)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7714','ETRS89 to Lerwick height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9589','ETRS89 to ETRS89 + Lerwick height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7711','ETRS89 to ODN height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9587','ETRS89 to ETRS89 + ODN height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7712','ETRS89 to ODN Orkney height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9586','ETRS89 to ETRS89 + ODN Orkney height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7716','ETRS89 to St. Marys height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9585','ETRS89 to ETRS89 + St. Marys height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7715','ETRS89 to Stornoway height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9584','ETRS89 to ETRS89 + Stornoway height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7709','OSGB36 to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9283','ETRS89 to NAP height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9597','ETRS89 to ETRS89 + NAP height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','9281','Amersfoort to ETRS89 (8)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9282','Amersfoort to ETRS89 (9)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','7833','Albanian 1987 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9276','ETRS89 to EVRF2000 Austria height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9600','ETRS89 to ETRS89 + EVRF2000 Austria height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9910','MGI to ETRS89 (8)','EPSG'); -INSERT INTO "alias_name" VALUES('concatenated_operation','EPSG','9499','ETRS89 to GHA height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1652','BD72 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','15928','BD72 to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','8369','BD72 to ETRS89 (3)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9908','ETRS89 to Ostend height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9909','ETRS89 to ETRS89 + Ostend height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10567','ETRS89 to Baltic 1957 height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10568','ETRS89 to ETRS89 + Baltic 1957 height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5226','S-JTSK/05 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','8365','ETRS89 to S-JTSK [JTSK03] (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','8367','S-JTSK [JTSK03] to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('concatenated_operation','EPSG','8442','ETRS89 to S-JTSK (5)','EPSG'); -INSERT INTO "alias_name" VALUES('concatenated_operation','EPSG','8443','S-JTSK to ETRS89 (6)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','6205','MGI 1901 to ETRS89 (5)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10544','ETRS89 to Cascais height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10546','ETRS89 to ETRS89 + Cascais height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5036','Datum 73 to ETRS89 (4)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5037','Datum 73 to ETRS89 (5)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','6189','Datum 73 to ETRS89 (6)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','5038','Lisbon to ETRS89 (3)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','6188','Lisbon to ETRS89 (4)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','11122','ETRS89 to ETRS89 + FVR09 height','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','11121','ETRS89 to FVR09 height','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','15994','Pulkovo 1942(58) to ETRS89 (4)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10360','ETRS89 to Alboran height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10361','ETRS89 to ETRS89 + Alboran height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9414','ETRS89 to Ceuta 2 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9606','ETRS89 to ETRS89 + Ceuta 2 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9410','ETRS89 to Alicante height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9605','ETRS89 to ETRS89 + Alicante height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10358','ETRS89 to Formentera height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10359','ETRS89 to ETRS89 + Formentera height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9413','ETRS89 to Ibiza height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9607','ETRS89 to ETRS89 + Ibiza height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9411','ETRS89 to Mallorca height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9608','ETRS89 to ETRS89 + Mallorca height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10363','ETRS89 to ETRS89 + Melilla height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10362','ETRS89 to Melilla height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9412','ETRS89 to Menorca height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9609','ETRS89 to ETRS89 + Menorca height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','5661','ED50 to ETRS89 (14)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9408','ED50 to ETRS89 (16)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9409','ED50 to ETRS89 (17)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1671','ETRS89-FRA [RGF93 v1] to WGS 84 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','11010','OSNet v2009 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','9791','ETRS89-FRA [RGF93 v2] to WGS 84 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','9792','ETRS89-FRA [RGF93 v2b] to WGS 84 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9727','ETRS89 to Genoa 1942 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9728','ETRS89 to Cagliari 1956 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9729','ETRS89 to ETRS89 + Genoa 1942 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9730','ETRS89 to ETRS89 + Cagliari 1956 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','10333','BH_ETRS89 to WGS 84 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','8680','MGI 1901 to ETRS89 (7)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','3963','MGI 1901 to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','10292','ETRS89/DREF91/2016 to ETRF2000 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10294','ETRS89/DREF91/2016 to DHHN2016 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10855','ETRS89/DREF91/2016 to GNTRANS2016 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','10905','ETRS89/DREF91/2016 to Asse 2025 + Asse 2025 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','7675','MGI 1901 to ETRS89 (6)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','9495','MGI 1901 to SRB-ETRS89 (8)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9496','MGI 1901 to SRB-ETRS89 (9)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10247','ETRS89-SVN [D96-17] to SVS2010 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10248','ETRS89-SVN [D96-17] to ETRS89-SVN [D96-17] + SVS2010 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','15976','ETRS89-SVN [D96-17] to WGS 84 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','8689','MGI 1901 to ETRS89-SVN [D96-17] (12)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','3916','MGI 1901 to ETRS89-SVN [D96-17] (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10295','ETRS89/DREF91/2016 to ETRS89/DREF91/2016 + DHHN2016 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7958','ETRS89 to Belfast height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9591','ETRS89 to ETRS89 + Malin Head height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9592','ETRS89 to ETRS89 + Belfast height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7959','ETRS89 to Malin Head height (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10351','ETRS89 to ETRS89 + LAT NL depth (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10466','ETRS89 to MSL NL depth (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10467','ETRS89 to ETRS89 + MSL NL depth (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10350','ETRS89 to LAT NL depth (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','11153','OSNet v2009 to ETRS89','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','10996','ETRS89 to Xrail84 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10990','ETRS89 to LSG height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','11212','ITRF2020 to ETRS89-SVN [D96-17] (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','11152','ETRS89-SVN [D96-17] to ETRF2000','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1647','CH1903+ to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','1646','CH1903 to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','7674','CH1903 to ETRS89 (2)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9365','ETRS89 to TPEN11-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9369','ETRS89 to MML07-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9386','ETRS89 to AbInvA96_2020-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9454','ETRS89 to GBK19-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9740','ETRS89 to EOS21-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9759','ETRS89 to ECML14_NB-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9764','ETRS89 to EWR2-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9867','ETRS89 to MRH21-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9878','ETRS89 to MOLDOR11-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9941','ETRS89 to EBBWV14-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9965','ETRS89 to HULLEE13-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9970','ETRS89 to SCM22-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9975','ETRS89 to FNL22-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10108','ETRS89 to MWC18-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10181','ETRS89 to DoPw22-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10186','ETRS89 to ShAb07-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10192','ETRS89 to CNH22-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10197','ETRS89 to CWS13-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10205','ETRS89 to DIBA15-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10210','ETRS89 to GWPBS22-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10215','ETRS89 to GWWAB22-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10220','ETRS89 to GWWWA22-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10225','ETRS89 to MALS09-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10230','ETRS89 to OxWo08-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10238','ETRS89 to SYC20-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10273','ETRS89 to SMITB20-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10278','ETRS89 to RBEPP12-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10469','ETRS89 to COV23-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10624','ETRS89 to ECML14-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10629','ETRS89 to WC05-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10850','ETRS89 to EWR3-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','10861','ETRS89 to WSPG-IRF (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9302','HS2-IRF to ETRS89 (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9304','ETRS89 to HS2-VRF height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','8361','ETRS89 to ETRS89 + Baltic 1957 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('concatenated_operation','EPSG','8363','ETRS89 + Baltic 1957 height to ETRS89 + EVRF2007 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','8362','ETRS89 to ETRS89 + EVRF2007 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','9937','LUREF to ETRS89 (7)','EPSG'); -INSERT INTO "alias_name" VALUES('helmert_transformation','EPSG','9938','LUREF to ETRS89 (8)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9485','ETRS89-NOR [EUREF89] to NN2000 height (1)','EPSG'); -INSERT INTO "alias_name" VALUES('grid_transformation','EPSG','9593',' ETRS89-NOR [EUREF89] to ETRS89-NOR [EUREF89] + NN2000 height (1)','EPSG'); diff --git a/data/sql/proj.db.sql.expected.md5 b/data/sql/proj.db.sql.expected.md5 index 7dcd24da7c..445dbc9754 100644 --- a/data/sql/proj.db.sql.expected.md5 +++ b/data/sql/proj.db.sql.expected.md5 @@ -1 +1 @@ -398415c72838764acfc012b2182c5db9 +ea8ccedcd3ea286c626239942a64115c diff --git a/scripts/build_db.py b/scripts/build_db.py index 777e2350ba..f14dfb55d0 100755 --- a/scripts/build_db.py +++ b/scripts/build_db.py @@ -1169,23 +1169,6 @@ def fill_alias(proj_db_cursor): if not match: print('Cannot find CRS %s in geodetic_crs, projected_crs, vertical_crs or compound_crs' % (code)) - proj_db_cursor.execute("SELECT DISTINCT object_code, alias, coord_op_name FROM epsg.epsg_alias, epsg.epsg_coordoperation ON coord_op_code = object_code WHERE object_table_name = 'epsg_coordoperation' AND epsg_coordoperation.deprecated = 0") - for row in proj_db_cursor.fetchall(): - code, alt_name, new_name = row - if "ETRS89" not in alt_name: - # print('Ignoring alias %s for coordinate operation %s %s' % (alt_name, code, new_name)) - continue - - proj_db_cursor.execute('SELECT table_name FROM coordinate_operation_view WHERE code = ?', (code,)) - row = proj_db_cursor.fetchone() - if row is not None: - table_name = row[0] - if table_name != "conversion": - proj_db_cursor.execute("INSERT INTO alias_name VALUES (?,'EPSG',?,?,'EPSG')", (table_name, code, alt_name)) - continue - - print('Cannot find coordinate operation %s for alias %s' % (code, alt_name)) - def find_table(proj_db_cursor, code): for table_name in ('helmert_transformation', 'grid_transformation', 'concatenated_operation', 'geodetic_crs', 'projected_crs', 'vertical_crs', 'compound_crs'): From 72ea363903d915646cc610e1f33d85422d409af3 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 9 May 2026 00:30:33 +0200 Subject: [PATCH 04/10] createOperations(): tune geod_crs_of_datum_ensemble <--> geod_crs_of_another_datum typically ETRS89 to some national CRS (e.g. Pulkovo 42(58), etc.) If the existing lookup paths have not tried to go through an intermediate datum already (which can be the case if direct transformations exists), then force trying with the members of the datum ensemble. e.g. ETRS89 to Pulkovo 42(58) has direct transformations (EPSG:15993 Romania 10m and EPSG:1644 Poland 1m), but there's also a ETRS89-ROU to Pulkovo 42(58), EPSG:15994, for Romania 3m. --- .../operation/coordinateoperationfactory.cpp | 246 +++++++++++++++++- test/unit/gie_self_tests.cpp | 12 +- test/unit/test_network.cpp | 9 +- test/unit/test_operationfactory.cpp | 76 +++--- 4 files changed, 299 insertions(+), 44 deletions(-) diff --git a/src/iso19111/operation/coordinateoperationfactory.cpp b/src/iso19111/operation/coordinateoperationfactory.cpp index d14c8bd694..415b4e0b45 100644 --- a/src/iso19111/operation/coordinateoperationfactory.cpp +++ b/src/iso19111/operation/coordinateoperationfactory.cpp @@ -558,6 +558,8 @@ struct CoordinateOperationFactory::Private { const metadata::ExtentPtr &extent2; const CoordinateOperationContextNNPtr &context; bool inCreateOperationsWithDatumPivotAntiRecursion = false; + bool inCreateOperationsWithIntermediate = false; + bool inCreateOperationsGeogToGeogWithAlternativeGeog = false; bool inCreateOperationsGeogToVertWithAlternativeGeog = false; bool inCreateOperationsGeogToVertWithIntermediateVert = false; bool inCreateOperationsVertToVertWithIntermediateVert = false; @@ -2056,6 +2058,19 @@ CoordinateOperationFactory::Private::findsOpsInRegistryWithIntermediate( objectAsStr(sourceCRS.get()) + " --> " + objectAsStr(targetCRS.get()) + ")"); #endif + struct AntiRecursionGuard { + Context &context; + + explicit AntiRecursionGuard(Context &contextIn) : context(contextIn) { + assert(!context.inCreateOperationsWithIntermediate); + context.inCreateOperationsWithIntermediate = true; + } + + ~AntiRecursionGuard() { + context.inCreateOperationsWithIntermediate = false; + } + }; + const auto &authFactory = context.context->getAuthorityFactory(); assert(authFactory); @@ -2120,6 +2135,8 @@ CoordinateOperationFactory::Private::findsOpsInRegistryWithIntermediate( candidateSrcGeod, targetCRS, context, useCreateBetweenGeodeticCRSWithDatumBasedIntermediates); if (!opsWithIntermediate.empty()) { + AntiRecursionGuard guard(context); + const auto opsFirst = createOperations( sourceCRS, util::optional(), candidateSrcGeod, @@ -4203,7 +4220,7 @@ bool CoordinateOperationFactory::Private::createOperationsFromDatabase( // NAD27 to NAD83 has tens of results already. No need to look // for a pivot - if (!sameGeodeticDatum && + if (!sameGeodeticDatum && !context.inCreateOperationsWithIntermediate && (((res.empty() || !foundInstantiableOp) && !resFindDirectNonEmptyBeforeFiltering && context.context->getAllowUseIntermediateCRS() == @@ -4219,10 +4236,11 @@ bool CoordinateOperationFactory::Private::createOperationsFromDatabase( doFilterAndCheckPerfectOp = !res.empty(); } + bool bTriedThroughIntermediateCRS = false; bool bUseOnlyReplacedOperations = false; - if (!context.inCreateOperationsWithDatumPivotAntiRecursion && geodSrc && - geodDst && !sameGeodeticDatum && - context.context->getIntermediateCRS().empty() && + if (!context.inCreateOperationsWithDatumPivotAntiRecursion && + !context.inCreateOperationsWithIntermediate && geodSrc && geodDst && + !sameGeodeticDatum && context.context->getIntermediateCRS().empty() && context.context->getAllowUseIntermediateCRS() != CoordinateOperationContext::IntermediateCRSUse::NEVER && (!resFindDirectNonEmptyBeforeFiltering || @@ -4255,6 +4273,7 @@ bool CoordinateOperationFactory::Private::createOperationsFromDatabase( } } + bTriedThroughIntermediateCRS = true; auto resWithIntermediate = findsOpsInRegistryWithIntermediate( sourceCRS, targetCRS, context, true); if (tooSmallAreas && !res.empty()) { @@ -4272,6 +4291,225 @@ bool CoordinateOperationFactory::Private::createOperationsFromDatabase( } } + // Below helps for example when doing ETRS89 to Pulkovo 1942(58) which has + // direct but imprecise operations. By trying to use members of the ETRS89 + // datum ensemble, we can get precise operations using ETRS89-ROU [ETRF2000] + // for example. + if (!bTriedThroughIntermediateCRS && + !context.inCreateOperationsGeogToGeogWithAlternativeGeog && + !context.inCreateOperationsWithIntermediate && + !context.inCreateOperationsWithDatumPivotAntiRecursion && geodSrc && + geodDst && !sameGeodeticDatum && + // Do not use that heuristics/algorithm for ETRS89 to WGS 84 + ((geodSrc->datumEnsemble() != nullptr) != + (geodDst->datumEnsemble() != nullptr)) && + // Nor when going from a datum ensemble to one of its members... + !(geodSrc->datumEnsemble() && + isDatumInEnsemble(NN_CHECK_ASSERT(geodDst->datum()), + NN_CHECK_ASSERT(geodSrc->datumEnsemble()))) && + !(geodDst->datumEnsemble() && + isDatumInEnsemble(NN_CHECK_ASSERT(geodSrc->datum()), + NN_CHECK_ASSERT(geodDst->datumEnsemble()))) && + // We don't want to "improve" the result set for those transformations + // where heuristics have been painfully tuned over the years to give + // expected results... + !(sourceCRS->nameStr() == "NTF" && targetCRS->nameStr() == "ETRS89") && + !(sourceCRS->nameStr() == "ETRS89" && targetCRS->nameStr() == "NTF") && + !((sourceCRS->nameStr() == "GDA94" || + sourceCRS->nameStr() == "GDA2020") && + targetCRS->nameStr() == "WGS 84") && + !(sourceCRS->nameStr() == "WGS 84" && + (targetCRS->nameStr() == "GDA94" || + targetCRS->nameStr() == "GDA2020"))) { + struct AntiRecursionGuard { + Context &context; + + explicit AntiRecursionGuard(Context &contextIn) + : context(contextIn) { + assert( + !context.inCreateOperationsGeogToGeogWithAlternativeGeog); + context.inCreateOperationsGeogToGeogWithAlternativeGeog = true; + + assert(!context.inCreateOperationsWithIntermediate); + context.inCreateOperationsWithIntermediate = true; + + // This one does really help for performance otherwise + // "projinfo -s EPSG:7789 -t EPSG:4936 --hide-ballpark + // --summary" would be really slow. hopefully that does not + // discard candidate operations... + assert(!context.inCreateOperationsWithDatumPivotAntiRecursion); + context.inCreateOperationsWithDatumPivotAntiRecursion = true; + } + + ~AntiRecursionGuard() { + context.inCreateOperationsGeogToGeogWithAlternativeGeog = false; + context.inCreateOperationsWithDatumPivotAntiRecursion = false; + context.inCreateOperationsWithIntermediate = false; + } + }; + AntiRecursionGuard guard(context); + + const auto getNatureOf = + [](const crs::GeodeticCRS &crs) -> std::string { + if (dynamic_cast(&crs) != nullptr) { + if (crs.coordinateSystem()->axisList().size() == 2) + return "geographic2D"; + else + return "geographic3D"; + } else { + return "geodetic"; + } + }; + + const auto &authFactory = context.context->getAuthorityFactory(); + const auto srcEnsemble = geodSrc->datumEnsemble(); + const auto &srcDomains = geodSrc->domains(); + const auto dstEnsemble = geodDst->datumEnsemble(); + const auto &dstDomains = geodDst->domains(); + const auto &areaOfInterest = context.context->getAreaOfInterest(); + const bool is3D = geodSrc->coordinateSystem()->axisList().size() == 3 && + geodDst->coordinateSystem()->axisList().size() == 3; + const char *intermediateType = is3D ? "geographic 3D" : "geographic 2D"; + if (srcEnsemble && !dstDomains.empty()) { + const auto &dstExtent = dstDomains[0]->domainOfValidity(); + if (dstExtent) { + for (const auto &srcDatumCandidate : srcEnsemble->datums()) { + const auto &srcDatumDomains = srcDatumCandidate->domains(); + if (srcDatumDomains.empty()) + continue; + + const auto &srcDatumExtent = + srcDatumDomains[0]->domainOfValidity(); + if (!srcDatumExtent || + !srcDatumExtent->intersects(NN_NO_CHECK(dstExtent))) { + continue; + } + + if (areaOfInterest && !srcDatumExtent->intersects( + NN_NO_CHECK(areaOfInterest))) { + continue; + } + + const auto srcDatumGeogCRSList = + authFactory->createGeodeticCRSFromDatum( + NN_NO_CHECK(std::static_pointer_cast< + datum::GeodeticReferenceFrame>( + srcDatumCandidate.as_nullable())), + std::string(), intermediateType); + for (const auto &srcDatumGeogCRS : srcDatumGeogCRSList) { + auto ops = + createOperations(srcDatumGeogCRS, sourceEpoch, + targetCRS, targetEpoch, context); + if (getNatureOf(*(srcDatumGeogCRS.get())) == + getNatureOf(*geodSrc) && + srcDatumGeogCRS->coordinateSystem() + ->_isEquivalentTo( + geodSrc->coordinateSystem().get(), + util::IComparable::Criterion::EQUIVALENT)) { + for (const auto &op : ops) { + if (!op->hasBallparkTransformation()) { + auto newOp = op->shallowClone(); + setCRSs(newOp.get(), sourceCRS, targetCRS); + res.push_back(newOp); + } + } + } else { + // Expected to get only one as this is a null + // transformation between an ensemble and one + // of its member + const auto opsFirst = createOperations( + sourceCRS, sourceEpoch, srcDatumGeogCRS, + targetEpoch, context); + if (!opsFirst.empty() && + !opsFirst.front() + ->hasBallparkTransformation()) { + const auto &opFirst = opsFirst.front(); + for (const auto &op : ops) { + if (!op->hasBallparkTransformation()) { + res.push_back( + ConcatenatedOperation::createComputeMetadata( + {opFirst, op}, + context + .disallowEmptyIntersection())); + } + } + } + } + } + } + } + } else if (dstEnsemble && !srcDomains.empty()) { + // This is the symmetric case of the previous one + + const auto &srcExtent = srcDomains[0]->domainOfValidity(); + if (srcExtent) { + for (const auto &dstDatumCandidate : dstEnsemble->datums()) { + const auto &dstDatumDomains = dstDatumCandidate->domains(); + if (dstDatumDomains.empty()) + continue; + + const auto &dstDatumExtent = + dstDatumDomains[0]->domainOfValidity(); + if (!dstDatumExtent || + !dstDatumExtent->intersects(NN_NO_CHECK(srcExtent))) { + continue; + } + + if (areaOfInterest && !dstDatumExtent->intersects( + NN_NO_CHECK(areaOfInterest))) { + continue; + } + + const auto dstDatumGeogCRSList = + authFactory->createGeodeticCRSFromDatum( + NN_NO_CHECK(std::static_pointer_cast< + datum::GeodeticReferenceFrame>( + dstDatumCandidate.as_nullable())), + std::string(), intermediateType); + for (const auto &dstDatumGeogCRS : dstDatumGeogCRSList) { + const auto ops = createOperations( + sourceCRS, sourceEpoch, dstDatumGeogCRS, + targetEpoch, context); + if (getNatureOf(*(dstDatumGeogCRS.get())) == + getNatureOf(*geodDst) && + dstDatumGeogCRS->coordinateSystem() + ->_isEquivalentTo( + geodDst->coordinateSystem().get(), + util::IComparable::Criterion::EQUIVALENT)) { + for (const auto &op : ops) { + if (!op->hasBallparkTransformation()) { + auto newOp = op->shallowClone(); + setCRSs(newOp.get(), sourceCRS, targetCRS); + res.push_back(newOp); + } + } + } else { + // Expected to get only one as this is a null + // transformation between an ensemble and one + // of its member + const auto opsLast = createOperations( + dstDatumGeogCRS, targetEpoch, targetCRS, + targetEpoch, context); + if (!opsLast.empty() && + !opsLast.front()->hasBallparkTransformation()) { + const auto &opLast = opsLast.front(); + for (const auto &op : ops) { + if (!op->hasBallparkTransformation()) { + res.push_back( + ConcatenatedOperation::createComputeMetadata( + {op, opLast}, + context + .disallowEmptyIntersection())); + } + } + } + } + } + } + } + } + } + if (doFilterAndCheckPerfectOp) { // If we get at least a result with perfect accuracy, do not bother // generating synthetic transforms. diff --git a/test/unit/gie_self_tests.cpp b/test/unit/gie_self_tests.cpp index 81b01c58d9..2db5366ce3 100644 --- a/test/unit/gie_self_tests.cpp +++ b/test/unit/gie_self_tests.cpp @@ -1297,15 +1297,15 @@ TEST(gie, proj_create_crs_to_crs_PULKOVO42_ETRS89) { c.xyzt.z = 0; c.xyzt.t = HUGE_VAL; c = proj_trans(P, PJ_FWD, c); - EXPECT_NEAR(c.xy.x, 45.000346961, 1e-9); - EXPECT_NEAR(c.xy.y, 25.001524075, 1e-9); + EXPECT_NEAR(c.xy.x, 44.999701238, 1e-9); + EXPECT_NEAR(c.xy.y, 24.998474948, 1e-9); EXPECT_EQ(std::string(proj_pj_info(P).definition), "proj=pipeline step proj=axisswap order=2,1 " "step proj=unitconvert xy_in=deg xy_out=rad " "step proj=push v_3 " "step proj=cart " - "ellps=krass step proj=helmert x=68.1564 y=32.7756 z=80.2249 " - "rx=2.20333014 ry=2.19256447 rz=-2.54166911 s=-0.14155333 " + "ellps=krass step proj=helmert x=2.3287 y=-147.0425 z=-92.0802 " + "rx=0.3092483 ry=-0.32482185 rz=-0.49729934 s=5.68906266 " "convention=coordinate_frame step inv proj=cart ellps=GRS80 " "step proj=pop v_3 " "step proj=unitconvert xy_in=rad xy_out=deg step proj=axisswap " @@ -1322,8 +1322,8 @@ TEST(gie, proj_create_crs_to_crs_PULKOVO42_ETRS89) { proj_trans_generic(P, PJ_FWD, &(c.xyz.x), sizeof(double), 1, &(c.xyz.y), sizeof(double), 1, &(c.xyz.z), sizeof(double), 1, nullptr, 0, 0); - EXPECT_NEAR(c.xy.x, 45.000346961, 1e-9); - EXPECT_NEAR(c.xy.y, 25.001524075, 1e-9); + EXPECT_NEAR(c.xy.x, 44.999701238, 1e-9); + EXPECT_NEAR(c.xy.y, 24.998474948, 1e-9); // Poland c.xyz.x = 52; // Lat diff --git a/test/unit/test_network.cpp b/test/unit/test_network.cpp index ecced83351..c3b664ffaf 100644 --- a/test/unit/test_network.cpp +++ b/test/unit/test_network.cpp @@ -1386,7 +1386,8 @@ TEST(networking, network_endpoint_api_and_not_reachable_hgridshift) { EXPECT_EQ(c2.xyz.x, HUGE_VAL); EXPECT_EQ(proj_errno(P), PROJ_ERR_OTHER_NETWORK_ERROR); PJ *last_op = proj_trans_get_last_used_operation(P); - EXPECT_STREQ(proj_pj_info(last_op).description, "MGI to ETRS89 (5)"); + EXPECT_STREQ(proj_pj_info(last_op).description, + "MGI to ETRS89-AUT [2002] (8)"); proj_destroy(last_op); } @@ -1398,7 +1399,8 @@ TEST(networking, network_endpoint_api_and_not_reachable_hgridshift) { EXPECT_EQ(c2.xyz.x, HUGE_VAL); EXPECT_EQ(proj_errno(P), PROJ_ERR_OTHER_NETWORK_ERROR); PJ *last_op = proj_trans_get_last_used_operation(P); - EXPECT_STREQ(proj_pj_info(last_op).description, "MGI to ETRS89 (5)"); + EXPECT_STREQ(proj_pj_info(last_op).description, + "MGI to ETRS89-AUT [2002] (8)"); proj_destroy(last_op); } @@ -1410,7 +1412,8 @@ TEST(networking, network_endpoint_api_and_not_reachable_hgridshift) { EXPECT_EQ(c2.xyz.x, HUGE_VAL); EXPECT_EQ(proj_errno(P), PROJ_ERR_OTHER_NETWORK_ERROR); PJ *last_op = proj_trans_get_last_used_operation(P); - EXPECT_STREQ(proj_pj_info(last_op).description, "MGI to ETRS89 (5)"); + EXPECT_STREQ(proj_pj_info(last_op).description, + "MGI to ETRS89-AUT [2002] (8)"); proj_destroy(last_op); } diff --git a/test/unit/test_operationfactory.cpp b/test/unit/test_operationfactory.cpp index 97c22c7026..c7d33dc167 100644 --- a/test/unit/test_operationfactory.cpp +++ b/test/unit/test_operationfactory.cpp @@ -82,6 +82,8 @@ TEST(operation, geogCRS_to_geogCRS) { TEST(operation, geogCRS_to_geogCRS_context_default) { auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), std::string()); + auto authFactoryEPSG = AuthorityFactory::create(DatabaseContext::create(), "EPSG"); auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0); ctxt->setSpatialCriterion( @@ -92,23 +94,28 @@ TEST(operation, geogCRS_to_geogCRS_context_default) { // Directly found in database { auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4179"), // Pulkovo 42 - authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 + authFactoryEPSG->createCoordinateReferenceSystem( + "4179"), // Pulkovo 42 + authFactoryEPSG->createCoordinateReferenceSystem("4258"), // ETRS89 ctxt); - ASSERT_EQ(list.size(), 3U); + ASSERT_EQ(list.size(), 4U); // Romania has a larger area than Poland (given our approx formula) - EXPECT_EQ(list[0]->getEPSGCode(), 15993); // Romania - 10m - EXPECT_EQ(list[1]->getEPSGCode(), 1644); // Poland - 1m - EXPECT_EQ(list[2]->nameStr(), + EXPECT_EQ( + list[0]->nameStr(), + "Pulkovo 1942(58) to ETRS89-ROU [ETRF2000] (4)"); // Romania - 3m + EXPECT_EQ(list[0]->getEPSGCode(), 15994); // Romania - 3m + EXPECT_EQ(list[1]->getEPSGCode(), 15993); // Romania - 10m + EXPECT_EQ(list[2]->getEPSGCode(), 1644); // Poland - 1m + EXPECT_EQ(list[3]->nameStr(), "Ballpark geographic offset from Pulkovo 1942(58) to ETRS89"); EXPECT_EQ( list[0]->exportToPROJString(PROJStringFormatter::create().get()), "+proj=pipeline +step +proj=axisswap +order=2,1 +step " "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 " - "+step +proj=cart +ellps=krass +step +proj=helmert +x=68.1564 " - "+y=32.7756 +z=80.2249 +rx=2.20333014 +ry=2.19256447 " - "+rz=-2.54166911 +s=-0.14155333 +convention=coordinate_frame +step " + "+step +proj=cart +ellps=krass +step +proj=helmert +x=2.3287 " + "+y=-147.0425 +z=-92.0802 +rx=0.3092483 +ry=-0.32482185 " + "+rz=-0.49729934 +s=5.68906266 +convention=coordinate_frame +step " "+inv +proj=cart +ellps=GRS80 +step +proj=pop +v_3 +step " "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " "+order=2,1"); @@ -117,20 +124,19 @@ TEST(operation, geogCRS_to_geogCRS_context_default) { // Reverse case { auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4258"), - authFactory->createCoordinateReferenceSystem("4179"), ctxt); - ASSERT_EQ(list.size(), 3U); + authFactoryEPSG->createCoordinateReferenceSystem("4258"), + authFactoryEPSG->createCoordinateReferenceSystem("4179"), ctxt); + ASSERT_EQ(list.size(), 4U); // Romania has a larger area than Poland (given our approx formula) EXPECT_EQ(list[0]->nameStr(), - "Inverse of Pulkovo 1942(58) to ETRS89 (3)"); // Romania - 10m - + "Inverse of Pulkovo 1942(58) to ETRS89-ROU [ETRF2000] (4)"); EXPECT_EQ( list[0]->exportToPROJString(PROJStringFormatter::create().get()), "+proj=pipeline +step +proj=axisswap +order=2,1 +step " "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 " - "+step +proj=cart +ellps=GRS80 +step +inv +proj=helmert +x=68.1564 " - "+y=32.7756 +z=80.2249 +rx=2.20333014 +ry=2.19256447 " - "+rz=-2.54166911 +s=-0.14155333 +convention=coordinate_frame +step " + "+step +proj=cart +ellps=GRS80 +step +inv +proj=helmert +x=2.3287 " + "+y=-147.0425 +z=-92.0802 +rx=0.3092483 +ry=-0.32482185 " + "+rz=-0.49729934 +s=5.68906266 +convention=coordinate_frame +step " "+inv +proj=cart +ellps=krass +step +proj=pop +v_3 +step " "+proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap " "+order=2,1"); @@ -168,7 +174,10 @@ TEST(operation, geogCRS_to_geogCRS_context_match_by_name) { TEST(operation, geogCRS_to_geogCRS_context_filter_accuracy) { auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), std::string()); + auto authFactoryEPSG = AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + { auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 1.0); @@ -176,8 +185,8 @@ TEST(operation, geogCRS_to_geogCRS_context_filter_accuracy) { CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4179"), - authFactory->createCoordinateReferenceSystem("4258"), ctxt); + authFactoryEPSG->createCoordinateReferenceSystem("4179"), + authFactoryEPSG->createCoordinateReferenceSystem("4258"), ctxt); ASSERT_EQ(list.size(), 1U); EXPECT_EQ(list[0]->getEPSGCode(), 1644); // Poland - 1m } @@ -188,8 +197,8 @@ TEST(operation, geogCRS_to_geogCRS_context_filter_accuracy) { CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION); auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4179"), - authFactory->createCoordinateReferenceSystem("4258"), ctxt); + authFactoryEPSG->createCoordinateReferenceSystem("4179"), + authFactoryEPSG->createCoordinateReferenceSystem("4258"), ctxt); ASSERT_EQ(list.size(), 0U); } } @@ -198,7 +207,10 @@ TEST(operation, geogCRS_to_geogCRS_context_filter_accuracy) { TEST(operation, geogCRS_to_geogCRS_context_filter_bbox) { auto authFactory = + AuthorityFactory::create(DatabaseContext::create(), std::string()); + auto authFactoryEPSG = AuthorityFactory::create(DatabaseContext::create(), "EPSG"); + // INSERT INTO "area" VALUES('EPSG','1197','Romania','Romania - onshore and // offshore.',43.44,48.27,20.26,31.41,0); { @@ -206,10 +218,11 @@ TEST(operation, geogCRS_to_geogCRS_context_filter_bbox) { authFactory, Extent::createFromBBOX(20.26, 43.44, 31.41, 48.27), 0.0); auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4179"), - authFactory->createCoordinateReferenceSystem("4258"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->getEPSGCode(), 15993); // Romania - 10m + authFactoryEPSG->createCoordinateReferenceSystem("4179"), + authFactoryEPSG->createCoordinateReferenceSystem("4258"), ctxt); + ASSERT_EQ(list.size(), 2U); + EXPECT_EQ(list[0]->getEPSGCode(), 15994); // Romania - 3m + EXPECT_EQ(list[1]->getEPSGCode(), 15993); // Romania - 10m } { auto ctxt = CoordinateOperationContext::create( @@ -218,10 +231,11 @@ TEST(operation, geogCRS_to_geogCRS_context_filter_bbox) { 48.27 - .1), 0.0); auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4179"), - authFactory->createCoordinateReferenceSystem("4258"), ctxt); - ASSERT_EQ(list.size(), 1U); - EXPECT_EQ(list[0]->getEPSGCode(), 15993); // Romania - 10m + authFactoryEPSG->createCoordinateReferenceSystem("4179"), + authFactoryEPSG->createCoordinateReferenceSystem("4258"), ctxt); + ASSERT_EQ(list.size(), 2U); + EXPECT_EQ(list[0]->getEPSGCode(), 15994); // Romania - 3m + EXPECT_EQ(list[1]->getEPSGCode(), 15993); // Romania - 10m } { auto ctxt = CoordinateOperationContext::create( @@ -230,8 +244,8 @@ TEST(operation, geogCRS_to_geogCRS_context_filter_bbox) { 48.27 + .1), 0.0); auto list = CoordinateOperationFactory::create()->createOperations( - authFactory->createCoordinateReferenceSystem("4179"), - authFactory->createCoordinateReferenceSystem("4258"), ctxt); + authFactoryEPSG->createCoordinateReferenceSystem("4179"), + authFactoryEPSG->createCoordinateReferenceSystem("4258"), ctxt); ASSERT_EQ(list.size(), 1U); EXPECT_EQ( list[0]->exportToPROJString(PROJStringFormatter::create().get()), From 0771155344c815a844022407a9376c8d1b8aedda Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 9 May 2026 01:30:54 +0200 Subject: [PATCH 05/10] Partial revert of c0fc1fa6b8838f0e30229fa7d497463546d7e602: we no longer need a custom ED50_TO_ETRS89_CATALONIA --- data/sql/final_consistency_checks.sql | 8 ++------ data/sql/grid_transformation_custom.sql | 6 ------ data/sql/proj.db.sql.expected.md5 | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/data/sql/final_consistency_checks.sql b/data/sql/final_consistency_checks.sql index a44d45fabc..1256545218 100644 --- a/data/sql/final_consistency_checks.sql +++ b/data/sql/final_consistency_checks.sql @@ -66,12 +66,8 @@ FOR EACH ROW BEGIN AND g1.source_crs_code = g2.source_crs_code AND g1.target_crs_auth_name = g2.target_crs_auth_name AND g1.target_crs_code = g2.target_crs_code - WHERE g1.auth_name = 'PROJ' - AND g1.code NOT LIKE '%_RESTRICTED_TO_VERTCRS%' - AND g1.code != 'ED50_TO_ETRS89_CATALONIA' - AND g2.auth_name = 'EPSG' - AND g2.deprecated = 0 AND - ((g1.interpolation_crs_auth_name IS NULL AND g2.interpolation_crs_auth_name IS NULL) OR + WHERE g1.auth_name = 'PROJ' AND g1.code NOT LIKE '%_RESTRICTED_TO_VERTCRS%' AND g2.auth_name = 'EPSG' AND g2.deprecated = 0 AND ( + (g1.interpolation_crs_auth_name IS NULL AND g2.interpolation_crs_auth_name IS NULL) OR (g1.interpolation_crs_auth_name IS NOT NULL AND g2.interpolation_crs_auth_name IS NOT NULL AND g1.interpolation_crs_auth_name = g2.interpolation_crs_auth_name AND g1.interpolation_crs_code = g2.interpolation_crs_code))) diff --git a/data/sql/grid_transformation_custom.sql b/data/sql/grid_transformation_custom.sql index 6746516777..721c04c46f 100644 --- a/data/sql/grid_transformation_custom.sql +++ b/data/sql/grid_transformation_custom.sql @@ -6,12 +6,6 @@ INSERT INTO "grid_transformation" VALUES('PROJ','BD72_TO_BEREF2002','BD72 to ETRS89-BEL [BEREF2002] (3)','Copy of BD72 to ETRS89-BEL [BEREF2011] (3) EPSG:8369','EPSG','9615','NTv2','EPSG','4313','EPSG','11063',0.01,'EPSG','8656','Latitude and longitude difference file','bd72lb72_etrs89lb08.gsb',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'IGN-Bel 0.01m',0); INSERT INTO "usage" VALUES('PROJ','BD72_TO_BEREF2002_USAGE','grid_transformation','PROJ','BD72_TO_BEREF2002','EPSG','1347','EPSG','1150'); --- Catalonia - --- FIXME: remove this record when EPSG has fixed that issue -INSERT INTO "grid_transformation" VALUES('PROJ','ED50_TO_ETRS89_CATALONIA','ED50 to ETRS89 (14)','Copy of ED50 to ETRS89-ESP [REGENTE] (14) EPSG:5661','EPSG','9615','NTv2','EPSG','4230','EPSG','4258',0.05,'EPSG','8656','Latitude and longitude difference file','100800401.gsb',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'ICC-Esp Cat',0); -INSERT INTO "usage" VALUES('PROJ','ED50_TO_ETRS89_CATALONIA_USAGE','grid_transformation','PROJ','ED50_TO_ETRS89_CATALONIA','EPSG','3732','EPSG','1079'); - -- Denmark INSERT INTO "grid_transformation" VALUES( diff --git a/data/sql/proj.db.sql.expected.md5 b/data/sql/proj.db.sql.expected.md5 index 445dbc9754..8f35012779 100644 --- a/data/sql/proj.db.sql.expected.md5 +++ b/data/sql/proj.db.sql.expected.md5 @@ -1 +1 @@ -ea8ccedcd3ea286c626239942a64115c +49ee5cee7798112c25b15bc17eafe6f2 From 97b3d02f0fe758c5c6282eb43b0f8267983e34bf Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 9 May 2026 15:59:50 +0200 Subject: [PATCH 06/10] createOperations(): remove recently added exception for NTF to ETRS89 --- .../operation/coordinateoperationfactory.cpp | 39 ++++- test/unit/test_operationfactory.cpp | 161 ++++++++++++++---- 2 files changed, 166 insertions(+), 34 deletions(-) diff --git a/src/iso19111/operation/coordinateoperationfactory.cpp b/src/iso19111/operation/coordinateoperationfactory.cpp index 415b4e0b45..c244a543a7 100644 --- a/src/iso19111/operation/coordinateoperationfactory.cpp +++ b/src/iso19111/operation/coordinateoperationfactory.cpp @@ -865,6 +865,7 @@ struct PrecomputedOpCharacteristics { bool isApprox_ = false; bool hasBallparkVertical_ = false; bool isNullTransformation_ = false; + bool hasXYZGridshift_ = false; // 3 below tests are for ETRS89-XXX to ETRS89-YYY following // recommendations of IOGP 373-07-7 to use ETRF2000 hub @@ -877,7 +878,7 @@ struct PrecomputedOpCharacteristics { double area, bool isPROJExportable, bool hasGrids, bool gridsAvailable, bool gridsKnown, size_t stepCount, - size_t projStepCount) + size_t projStepCount, bool hasXYZGridshift) : area_(area), accuracy_(getAccuracy(op)), isPROJExportable_(isPROJExportable), hasGrids_(hasGrids), gridsAvailable_(gridsAvailable), gridsKnown_(gridsKnown), @@ -887,6 +888,7 @@ struct PrecomputedOpCharacteristics { op->nameStr().find(BALLPARK_VERTICAL_TRANSFORMATION) != std::string::npos), isNullTransformation_(isNullTransformation(op->nameStr())), + hasXYZGridshift_(hasXYZGridshift), is_ETRS89_XXX_to_ETRS89_YYY_( starts_with(op->sourceCRS()->nameStr().c_str(), "ETRS89-") && starts_with(op->targetCRS()->nameStr().c_str(), "ETRS89-")), @@ -1022,6 +1024,13 @@ struct SortFunction { } } + if (iterA->second.hasXYZGridshift_ && !iterB->second.hasXYZGridshift_) { + return true; + } + if (!iterA->second.hasXYZGridshift_ && iterB->second.hasXYZGridshift_) { + return false; + } + // Follow recommendations of IOGP 373-07-7 to use ETRF2000 hub if // no direct Transformation if (iterA->second.is_ETRS89_XXX_to_ETRS89_YYY_ && @@ -1485,6 +1494,7 @@ struct FilterResults { bool isPROJExportable = false; auto formatter = io::PROJStringFormatter::create(); size_t projStepCount = 0; + bool hasXYZGridshift = false; try { const auto str = op->exportToPROJString(formatter.get()); // Grids might be missing, but at least this is something @@ -1497,6 +1507,8 @@ struct FilterResults { auto formatter2 = io::PROJStringFormatter::create(); formatter2->ingestPROJString(str); projStepCount = formatter2->getStepCount(); + } else { + hasXYZGridshift = true; } } catch (const std::exception &) { } @@ -1519,11 +1531,12 @@ struct FilterResults { << " "; std::cerr << "isNull=" << isNullTransformation(op->nameStr()) << " "; + std::cerr << "hasXYZGridshift=" << hasXYZGridshift << " "; std::cerr << std::endl; #endif map[op.get()] = PrecomputedOpCharacteristics( op, area, isPROJExportable, hasGrids, gridsAvailable, - gridsKnown, stepCount, projStepCount); + gridsKnown, stepCount, projStepCount, hasXYZGridshift); } // Sort ! @@ -4313,8 +4326,6 @@ bool CoordinateOperationFactory::Private::createOperationsFromDatabase( // We don't want to "improve" the result set for those transformations // where heuristics have been painfully tuned over the years to give // expected results... - !(sourceCRS->nameStr() == "NTF" && targetCRS->nameStr() == "ETRS89") && - !(sourceCRS->nameStr() == "ETRS89" && targetCRS->nameStr() == "NTF") && !((sourceCRS->nameStr() == "GDA94" || sourceCRS->nameStr() == "GDA2020") && targetCRS->nameStr() == "WGS 84") && @@ -4409,6 +4420,15 @@ bool CoordinateOperationFactory::Private::createOperationsFromDatabase( for (const auto &op : ops) { if (!op->hasBallparkTransformation()) { auto newOp = op->shallowClone(); + auto interpolationCRS = + newOp->interpolationCRS(); + if (interpolationCRS && + interpolationCRS->_isEquivalentTo( + srcDatumGeogCRS.get(), + util::IComparable::Criterion:: + EQUIVALENT)) { + newOp->setInterpolationCRS(sourceCRS); + } setCRSs(newOp.get(), sourceCRS, targetCRS); res.push_back(newOp); } @@ -4479,6 +4499,15 @@ bool CoordinateOperationFactory::Private::createOperationsFromDatabase( for (const auto &op : ops) { if (!op->hasBallparkTransformation()) { auto newOp = op->shallowClone(); + auto interpolationCRS = + newOp->interpolationCRS(); + if (interpolationCRS && + interpolationCRS->_isEquivalentTo( + dstDatumGeogCRS.get(), + util::IComparable::Criterion:: + EQUIVALENT)) { + newOp->setInterpolationCRS(targetCRS); + } setCRSs(newOp.get(), sourceCRS, targetCRS); res.push_back(newOp); } @@ -4508,6 +4537,8 @@ bool CoordinateOperationFactory::Private::createOperationsFromDatabase( } } } + + doFilterAndCheckPerfectOp = !res.empty(); } if (doFilterAndCheckPerfectOp) { diff --git a/test/unit/test_operationfactory.cpp b/test/unit/test_operationfactory.cpp index c7d33dc167..a140908a05 100644 --- a/test/unit/test_operationfactory.cpp +++ b/test/unit/test_operationfactory.cpp @@ -284,25 +284,52 @@ TEST(operation, geogCRS_to_geogCRS_context_inverse_needed) { authFactory->createCoordinateReferenceSystem("4275"), // NTF authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 " - "+step +proj=cart +ellps=clrk80ign +step +proj=helmert +x=-168 " - "+y=-60 +z=320 +step +inv +proj=cart +ellps=GRS80 +step +proj=pop " - "+v_3 +step +proj=unitconvert +xy_in=rad +xy_out=deg +step " - "+proj=axisswap +order=2,1"); + ASSERT_EQ(list.size(), 3U); + + EXPECT_EQ(list[0]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=clrk80ign " + "+step +proj=helmert +x=-168 +y=-60 +z=320 " + "+step +inv +proj=cart +ellps=GRS80 " + "+step +proj=pop +v_3 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + EXPECT_EQ(list[1]->exportToPROJString( PROJStringFormatter::create( PROJStringFormatter::Convention::PROJ_5, authFactory->databaseContext()) .get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step " - "+proj=hgridshift +grids=fr_ign_ntf_r93.tif +step " - "+proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=clrk80ign " + "+step +proj=xyzgridshift +grids=fr_ign_gr3df97a.tif " + "+grid_ref=output_crs +ellps=GRS80 " + "+step +inv +proj=cart +ellps=GRS80 " + "+step +proj=pop +v_3 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + EXPECT_EQ(list[2]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=hgridshift +grids=fr_ign_ntf_r93.tif " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); } { auto ctxt = @@ -314,14 +341,51 @@ TEST(operation, geogCRS_to_geogCRS_context_inverse_needed) { authFactory->createCoordinateReferenceSystem("4275"), // NTF authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step " - "+proj=hgridshift +grids=fr_ign_ntf_r93.tif +step " - "+proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); + ASSERT_EQ(list.size(), 3U); + EXPECT_EQ(list[0]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=clrk80ign " + "+step +proj=xyzgridshift +grids=fr_ign_gr3df97a.tif " + "+grid_ref=output_crs +ellps=GRS80 " + "+step +inv +proj=cart +ellps=GRS80 " + "+step +proj=pop +v_3 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + EXPECT_EQ(list[1]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=hgridshift +grids=fr_ign_ntf_r93.tif " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + EXPECT_EQ(list[2]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=clrk80ign " + "+step +proj=helmert +x=-168 +y=-60 +z=320 " + "+step +inv +proj=cart +ellps=GRS80 " + "+step +proj=pop +v_3 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); } { auto ctxt = @@ -333,14 +397,51 @@ TEST(operation, geogCRS_to_geogCRS_context_inverse_needed) { authFactory->createCoordinateReferenceSystem("4258"), // ETRS89 authFactory->createCoordinateReferenceSystem("4275"), // NTF ctxt); - ASSERT_EQ(list.size(), 2U); - EXPECT_EQ( - list[0]->exportToPROJString(PROJStringFormatter::create().get()), - "+proj=pipeline +step +proj=axisswap +order=2,1 +step " - "+proj=unitconvert +xy_in=deg +xy_out=rad +step +inv " - "+proj=hgridshift +grids=fr_ign_ntf_r93.tif +step " - "+proj=unitconvert " - "+xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1"); + ASSERT_EQ(list.size(), 3U); + EXPECT_EQ(list[0]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=GRS80 " + "+step +inv +proj=xyzgridshift +grids=fr_ign_gr3df97a.tif " + "+grid_ref=output_crs +ellps=GRS80 " + "+step +inv +proj=cart +ellps=clrk80ign " + "+step +proj=pop +v_3 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + EXPECT_EQ(list[1]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +inv +proj=hgridshift +grids=fr_ign_ntf_r93.tif " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); + + EXPECT_EQ(list[2]->exportToPROJString( + PROJStringFormatter::create( + PROJStringFormatter::Convention::PROJ_5, + authFactory->databaseContext()) + .get()), + "+proj=pipeline " + "+step +proj=axisswap +order=2,1 " + "+step +proj=unitconvert +xy_in=deg +xy_out=rad " + "+step +proj=push +v_3 " + "+step +proj=cart +ellps=GRS80 " + "+step +proj=helmert +x=168 +y=60 +z=-320 " + "+step +inv +proj=cart +ellps=clrk80ign " + "+step +proj=pop +v_3 " + "+step +proj=unitconvert +xy_in=rad +xy_out=deg " + "+step +proj=axisswap +order=2,1"); } } From 9881dd284ca14a92855902c1c62706def517535c Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 9 May 2026 16:14:19 +0200 Subject: [PATCH 07/10] Factor code --- .../operation/coordinateoperationfactory.cpp | 361 ++++++++---------- 1 file changed, 162 insertions(+), 199 deletions(-) diff --git a/src/iso19111/operation/coordinateoperationfactory.cpp b/src/iso19111/operation/coordinateoperationfactory.cpp index c244a543a7..38a9d36dcf 100644 --- a/src/iso19111/operation/coordinateoperationfactory.cpp +++ b/src/iso19111/operation/coordinateoperationfactory.cpp @@ -626,6 +626,15 @@ struct CoordinateOperationFactory::Private { const crs::BoundCRS *boundSrc, const crs::BoundCRS *boundDst, std::vector &res); + static std::vector + createOperationsEnsembleCRSToOtherGeodCRS( + const crs::CRSNNPtr &sourceCRS, + const util::optional &sourceEpoch, + const crs::CRSNNPtr &targetCRS, + const util::optional &targetEpoch, + Private::Context &context, const crs::GeodeticCRS *geodSrc, + const crs::GeodeticCRS *geodDst); + static bool createOperationsFromDatabase( const crs::CRSNNPtr &sourceCRS, const util::optional &sourceEpoch, @@ -4031,6 +4040,143 @@ useOnlyReplacedOperations(const std::vector &ops) { // --------------------------------------------------------------------------- +// Below helps for example when doing ETRS89 to Pulkovo 1942(58) which has +// direct but imprecise operations. By trying to use members of the ETRS89 +// datum ensemble, we can get precise operations using ETRS89-ROU [ETRF2000] +// for example. +std::vector +CoordinateOperationFactory::Private::createOperationsEnsembleCRSToOtherGeodCRS( + const crs::CRSNNPtr &sourceCRS, + const util::optional &sourceEpoch, + const crs::CRSNNPtr &targetCRS, + const util::optional &targetEpoch, + Private::Context &context, const crs::GeodeticCRS *geodSrc, + const crs::GeodeticCRS *geodDst) { + + struct AntiRecursionGuard { + Context &context; + + explicit AntiRecursionGuard(Context &contextIn) : context(contextIn) { + assert(!context.inCreateOperationsGeogToGeogWithAlternativeGeog); + context.inCreateOperationsGeogToGeogWithAlternativeGeog = true; + + assert(!context.inCreateOperationsWithIntermediate); + context.inCreateOperationsWithIntermediate = true; + + // This one does really help for performance otherwise + // "projinfo -s EPSG:7789 -t EPSG:4936 --hide-ballpark + // --summary" would be really slow. hopefully that does not + // discard candidate operations... + assert(!context.inCreateOperationsWithDatumPivotAntiRecursion); + context.inCreateOperationsWithDatumPivotAntiRecursion = true; + } + + ~AntiRecursionGuard() { + context.inCreateOperationsGeogToGeogWithAlternativeGeog = false; + context.inCreateOperationsWithDatumPivotAntiRecursion = false; + context.inCreateOperationsWithIntermediate = false; + } + }; + AntiRecursionGuard guard(context); + + const auto getNatureOf = [](const crs::GeodeticCRS &crs) -> std::string { + if (dynamic_cast(&crs) != nullptr) { + if (crs.coordinateSystem()->axisList().size() == 2) + return "geographic2D"; + else + return "geographic3D"; + } else { + return "geodetic"; + } + }; + + const auto &authFactory = context.context->getAuthorityFactory(); + const auto srcEnsemble = geodSrc->datumEnsemble(); + assert(srcEnsemble); + const auto &dstDomains = geodDst->domains(); + const auto &areaOfInterest = context.context->getAreaOfInterest(); + const bool is3D = geodSrc->coordinateSystem()->axisList().size() == 3 && + geodDst->coordinateSystem()->axisList().size() == 3; + const char *intermediateType = is3D ? "geographic 3D" : "geographic 2D"; + + std::vector newOps; + const auto &dstExtent = dstDomains[0]->domainOfValidity(); + if (dstExtent) { + for (const auto &srcDatumCandidate : srcEnsemble->datums()) { + const auto &srcDatumDomains = srcDatumCandidate->domains(); + if (srcDatumDomains.empty()) + continue; + + const auto &srcDatumExtent = srcDatumDomains[0]->domainOfValidity(); + if (!srcDatumExtent || + !srcDatumExtent->intersects(NN_NO_CHECK(dstExtent))) { + continue; + } + + if (areaOfInterest && + !srcDatumExtent->intersects(NN_NO_CHECK(areaOfInterest))) { + continue; + } + + const auto srcDatumGeogCRSList = + authFactory->createGeodeticCRSFromDatum( + NN_NO_CHECK( + std::static_pointer_cast( + srcDatumCandidate.as_nullable())), + std::string(), intermediateType); + for (const auto &srcDatumGeogCRS : srcDatumGeogCRSList) { + auto ops = createOperations(srcDatumGeogCRS, sourceEpoch, + targetCRS, targetEpoch, context); + if (getNatureOf(*(srcDatumGeogCRS.get())) == + getNatureOf(*geodSrc) && + srcDatumGeogCRS->coordinateSystem()->_isEquivalentTo( + geodSrc->coordinateSystem().get(), + util::IComparable::Criterion::EQUIVALENT)) { + for (const auto &op : ops) { + if (!op->hasBallparkTransformation()) { + auto newOp = op->shallowClone(); + auto interpolationCRS = newOp->interpolationCRS(); + if (interpolationCRS && + interpolationCRS->_isEquivalentTo( + srcDatumGeogCRS.get(), + util::IComparable::Criterion::EQUIVALENT)) { + newOp->setInterpolationCRS(sourceCRS); + } + setCRSs(newOp.get(), sourceCRS, targetCRS); + newOps.push_back(newOp); + } + } + } else { + // Expected to get only one as this is a null + // transformation between an ensemble and one + // of its member + const auto opsFirst = + createOperations(sourceCRS, sourceEpoch, + srcDatumGeogCRS, sourceEpoch, context); + if (!opsFirst.empty() && + !opsFirst.front()->hasBallparkTransformation()) { + const auto &opFirst = opsFirst.front(); + for (const auto &op : ops) { + if (!op->hasBallparkTransformation()) { + newOps.push_back( + ConcatenatedOperation:: + createComputeMetadata( + {opFirst, op}, + context + .disallowEmptyIntersection())); + } + } + } + } + } + } + } + + return newOps; +} + +// --------------------------------------------------------------------------- + bool CoordinateOperationFactory::Private::createOperationsFromDatabase( const crs::CRSNNPtr &sourceCRS, const util::optional &sourceEpoch, @@ -4332,209 +4478,26 @@ bool CoordinateOperationFactory::Private::createOperationsFromDatabase( !(sourceCRS->nameStr() == "WGS 84" && (targetCRS->nameStr() == "GDA94" || targetCRS->nameStr() == "GDA2020"))) { - struct AntiRecursionGuard { - Context &context; - - explicit AntiRecursionGuard(Context &contextIn) - : context(contextIn) { - assert( - !context.inCreateOperationsGeogToGeogWithAlternativeGeog); - context.inCreateOperationsGeogToGeogWithAlternativeGeog = true; - assert(!context.inCreateOperationsWithIntermediate); - context.inCreateOperationsWithIntermediate = true; - - // This one does really help for performance otherwise - // "projinfo -s EPSG:7789 -t EPSG:4936 --hide-ballpark - // --summary" would be really slow. hopefully that does not - // discard candidate operations... - assert(!context.inCreateOperationsWithDatumPivotAntiRecursion); - context.inCreateOperationsWithDatumPivotAntiRecursion = true; - } - - ~AntiRecursionGuard() { - context.inCreateOperationsGeogToGeogWithAlternativeGeog = false; - context.inCreateOperationsWithDatumPivotAntiRecursion = false; - context.inCreateOperationsWithIntermediate = false; - } - }; - AntiRecursionGuard guard(context); - - const auto getNatureOf = - [](const crs::GeodeticCRS &crs) -> std::string { - if (dynamic_cast(&crs) != nullptr) { - if (crs.coordinateSystem()->axisList().size() == 2) - return "geographic2D"; - else - return "geographic3D"; - } else { - return "geodetic"; - } - }; - - const auto &authFactory = context.context->getAuthorityFactory(); const auto srcEnsemble = geodSrc->datumEnsemble(); - const auto &srcDomains = geodSrc->domains(); - const auto dstEnsemble = geodDst->datumEnsemble(); const auto &dstDomains = geodDst->domains(); - const auto &areaOfInterest = context.context->getAreaOfInterest(); - const bool is3D = geodSrc->coordinateSystem()->axisList().size() == 3 && - geodDst->coordinateSystem()->axisList().size() == 3; - const char *intermediateType = is3D ? "geographic 3D" : "geographic 2D"; if (srcEnsemble && !dstDomains.empty()) { - const auto &dstExtent = dstDomains[0]->domainOfValidity(); - if (dstExtent) { - for (const auto &srcDatumCandidate : srcEnsemble->datums()) { - const auto &srcDatumDomains = srcDatumCandidate->domains(); - if (srcDatumDomains.empty()) - continue; - - const auto &srcDatumExtent = - srcDatumDomains[0]->domainOfValidity(); - if (!srcDatumExtent || - !srcDatumExtent->intersects(NN_NO_CHECK(dstExtent))) { - continue; - } - - if (areaOfInterest && !srcDatumExtent->intersects( - NN_NO_CHECK(areaOfInterest))) { - continue; - } - - const auto srcDatumGeogCRSList = - authFactory->createGeodeticCRSFromDatum( - NN_NO_CHECK(std::static_pointer_cast< - datum::GeodeticReferenceFrame>( - srcDatumCandidate.as_nullable())), - std::string(), intermediateType); - for (const auto &srcDatumGeogCRS : srcDatumGeogCRSList) { - auto ops = - createOperations(srcDatumGeogCRS, sourceEpoch, - targetCRS, targetEpoch, context); - if (getNatureOf(*(srcDatumGeogCRS.get())) == - getNatureOf(*geodSrc) && - srcDatumGeogCRS->coordinateSystem() - ->_isEquivalentTo( - geodSrc->coordinateSystem().get(), - util::IComparable::Criterion::EQUIVALENT)) { - for (const auto &op : ops) { - if (!op->hasBallparkTransformation()) { - auto newOp = op->shallowClone(); - auto interpolationCRS = - newOp->interpolationCRS(); - if (interpolationCRS && - interpolationCRS->_isEquivalentTo( - srcDatumGeogCRS.get(), - util::IComparable::Criterion:: - EQUIVALENT)) { - newOp->setInterpolationCRS(sourceCRS); - } - setCRSs(newOp.get(), sourceCRS, targetCRS); - res.push_back(newOp); - } - } - } else { - // Expected to get only one as this is a null - // transformation between an ensemble and one - // of its member - const auto opsFirst = createOperations( - sourceCRS, sourceEpoch, srcDatumGeogCRS, - targetEpoch, context); - if (!opsFirst.empty() && - !opsFirst.front() - ->hasBallparkTransformation()) { - const auto &opFirst = opsFirst.front(); - for (const auto &op : ops) { - if (!op->hasBallparkTransformation()) { - res.push_back( - ConcatenatedOperation::createComputeMetadata( - {opFirst, op}, - context - .disallowEmptyIntersection())); - } - } - } - } - } - } - } - } else if (dstEnsemble && !srcDomains.empty()) { - // This is the symmetric case of the previous one - - const auto &srcExtent = srcDomains[0]->domainOfValidity(); - if (srcExtent) { - for (const auto &dstDatumCandidate : dstEnsemble->datums()) { - const auto &dstDatumDomains = dstDatumCandidate->domains(); - if (dstDatumDomains.empty()) - continue; - - const auto &dstDatumExtent = - dstDatumDomains[0]->domainOfValidity(); - if (!dstDatumExtent || - !dstDatumExtent->intersects(NN_NO_CHECK(srcExtent))) { - continue; - } - - if (areaOfInterest && !dstDatumExtent->intersects( - NN_NO_CHECK(areaOfInterest))) { - continue; - } - - const auto dstDatumGeogCRSList = - authFactory->createGeodeticCRSFromDatum( - NN_NO_CHECK(std::static_pointer_cast< - datum::GeodeticReferenceFrame>( - dstDatumCandidate.as_nullable())), - std::string(), intermediateType); - for (const auto &dstDatumGeogCRS : dstDatumGeogCRSList) { - const auto ops = createOperations( - sourceCRS, sourceEpoch, dstDatumGeogCRS, - targetEpoch, context); - if (getNatureOf(*(dstDatumGeogCRS.get())) == - getNatureOf(*geodDst) && - dstDatumGeogCRS->coordinateSystem() - ->_isEquivalentTo( - geodDst->coordinateSystem().get(), - util::IComparable::Criterion::EQUIVALENT)) { - for (const auto &op : ops) { - if (!op->hasBallparkTransformation()) { - auto newOp = op->shallowClone(); - auto interpolationCRS = - newOp->interpolationCRS(); - if (interpolationCRS && - interpolationCRS->_isEquivalentTo( - dstDatumGeogCRS.get(), - util::IComparable::Criterion:: - EQUIVALENT)) { - newOp->setInterpolationCRS(targetCRS); - } - setCRSs(newOp.get(), sourceCRS, targetCRS); - res.push_back(newOp); - } - } - } else { - // Expected to get only one as this is a null - // transformation between an ensemble and one - // of its member - const auto opsLast = createOperations( - dstDatumGeogCRS, targetEpoch, targetCRS, - targetEpoch, context); - if (!opsLast.empty() && - !opsLast.front()->hasBallparkTransformation()) { - const auto &opLast = opsLast.front(); - for (const auto &op : ops) { - if (!op->hasBallparkTransformation()) { - res.push_back( - ConcatenatedOperation::createComputeMetadata( - {op, opLast}, - context - .disallowEmptyIntersection())); - } - } - } - } - } - } + auto newOps = createOperationsEnsembleCRSToOtherGeodCRS( + sourceCRS, sourceEpoch, targetCRS, targetEpoch, context, + geodSrc, geodDst); + res.insert(res.end(), std::make_move_iterator(newOps.begin()), + std::make_move_iterator(newOps.end())); + } else { + // Symmetrical case + const auto dstEnsemble = geodDst->datumEnsemble(); + const auto &srcDomains = geodSrc->domains(); + if (dstEnsemble && !srcDomains.empty()) { + auto newOps = + applyInverse(createOperationsEnsembleCRSToOtherGeodCRS( + targetCRS, targetEpoch, sourceCRS, sourceEpoch, context, + geodDst, geodSrc)); + res.insert(res.end(), std::make_move_iterator(newOps.begin()), + std::make_move_iterator(newOps.end())); } } From 2c8891e4721ce0f729d897ed8ebe6735468bf7ae Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 9 May 2026 17:54:52 +0200 Subject: [PATCH 08/10] Move CRS subtype constants in private header --- include/proj/internal/io_internal.hpp | 11 +++++ src/iso19111/factory.cpp | 71 +++++++++++++-------------- 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/include/proj/internal/io_internal.hpp b/include/proj/internal/io_internal.hpp index 4ae681f2fa..63d4c358b6 100644 --- a/include/proj/internal/io_internal.hpp +++ b/include/proj/internal/io_internal.hpp @@ -45,6 +45,17 @@ NS_PROJ_START namespace io { +// CRS subtypes +#define CRS_SUBTYPE_GEOG_2D "geographic 2D" +#define CRS_SUBTYPE_GEOG_3D "geographic 3D" +#define CRS_SUBTYPE_GEOCENTRIC "geocentric" +#define CRS_SUBTYPE_OTHER "other" +#define CRS_SUBTYPE_PROJECTED "projected" +#define CRS_SUBTYPE_DERIVED_PROJECTED "derived projected" +#define CRS_SUBTYPE_ENGINEERING "engineering" +#define CRS_SUBTYPE_VERTICAL "vertical" +#define CRS_SUBTYPE_COMPOUND "compound" + // --------------------------------------------------------------------------- class WKTConstants { diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp index 2ddd4dd6aa..8802b53ad5 100644 --- a/src/iso19111/factory.cpp +++ b/src/iso19111/factory.cpp @@ -106,17 +106,6 @@ namespace io { //! @cond Doxygen_Suppress -// CRS subtypes -#define GEOG_2D "geographic 2D" -#define GEOG_3D "geographic 3D" -#define GEOCENTRIC "geocentric" -#define OTHER "other" -#define PROJECTED "projected" -#define DERIVED_PROJECTED "derived projected" -#define ENGINEERING "engineering" -#define VERTICAL "vertical" -#define COMPOUND "compound" - #define GEOG_2D_SINGLE_QUOTED "'geographic 2D'" #define GEOG_3D_SINGLE_QUOTED "'geographic 3D'" #define GEOCENTRIC_SINGLE_QUOTED "'geocentric'" @@ -2520,12 +2509,12 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( identifyOrInsert(self, coordinateSystem, "GEODETIC_CRS", authName, code, csAuthName, csCode, sqlStatements); - const char *type = GEOG_2D; + const char *type = CRS_SUBTYPE_GEOG_2D; if (coordinateSystem->axisList().size() == 3) { if (dynamic_cast(crs.get())) { - type = GEOG_3D; + type = CRS_SUBTYPE_GEOG_3D; } else { - type = GEOCENTRIC; + type = CRS_SUBTYPE_GEOCENTRIC; } } @@ -5538,7 +5527,8 @@ AuthorityFactory::createGeodeticCRS(const std::string &code, auto ellipsoidalCS = util::nn_dynamic_pointer_cast(cs); - if ((type == GEOG_2D || type == GEOG_3D) && ellipsoidalCS) { + if ((type == CRS_SUBTYPE_GEOG_2D || type == CRS_SUBTYPE_GEOG_3D) && + ellipsoidalCS) { auto crsRet = crs::GeographicCRS::create( props, datum, datumEnsemble, NN_NO_CHECK(ellipsoidalCS)); d->context()->d->cache(cacheKey, crsRet); @@ -5546,7 +5536,7 @@ AuthorityFactory::createGeodeticCRS(const std::string &code, } auto geocentricCS = util::nn_dynamic_pointer_cast(cs); - if (type == GEOCENTRIC && geocentricCS) { + if (type == CRS_SUBTYPE_GEOCENTRIC && geocentricCS) { auto crsRet = crs::GeodeticCRS::create(props, datum, datumEnsemble, NN_NO_CHECK(geocentricCS)); d->context()->d->cache(cacheKey, crsRet); @@ -5554,7 +5544,7 @@ AuthorityFactory::createGeodeticCRS(const std::string &code, } auto sphericalCS = util::nn_dynamic_pointer_cast(cs); - if (type == OTHER && sphericalCS) { + if (type == CRS_SUBTYPE_OTHER && sphericalCS) { auto crsRet = crs::GeodeticCRS::create(props, datum, datumEnsemble, NN_NO_CHECK(sphericalCS)); d->context()->d->cache(cacheKey, crsRet); @@ -6214,23 +6204,23 @@ AuthorityFactory::createCoordinateReferenceSystem(const std::string &code, code); } const auto &type = res.front()[0]; - if (type == GEOG_2D || type == GEOG_3D || type == GEOCENTRIC || - type == OTHER) { + if (type == CRS_SUBTYPE_GEOG_2D || type == CRS_SUBTYPE_GEOG_3D || + type == CRS_SUBTYPE_GEOCENTRIC || type == CRS_SUBTYPE_OTHER) { return createGeodeticCRS(code); } - if (type == VERTICAL) { + if (type == CRS_SUBTYPE_VERTICAL) { return createVerticalCRS(code); } - if (type == PROJECTED) { + if (type == CRS_SUBTYPE_PROJECTED) { return createProjectedCRS(code); } - if (type == DERIVED_PROJECTED) { + if (type == CRS_SUBTYPE_DERIVED_PROJECTED) { return createDerivedProjectedCRS(code); } - if (type == ENGINEERING) { + if (type == CRS_SUBTYPE_ENGINEERING) { return createEngineeringCRS(code); } - if (allowCompound && type == COMPOUND) { + if (allowCompound && type == CRS_SUBTYPE_COMPOUND) { return createCompoundCRS(code); } throw FactoryException("unhandled CRS type: " + type); @@ -9060,23 +9050,23 @@ std::list AuthorityFactory::getCRSInfoList() const { info.code = row[1]; info.name = row[2]; const auto &type = row[3]; - if (type == GEOG_2D) { + if (type == CRS_SUBTYPE_GEOG_2D) { info.type = AuthorityFactory::ObjectType::GEOGRAPHIC_2D_CRS; - } else if (type == GEOG_3D) { + } else if (type == CRS_SUBTYPE_GEOG_3D) { info.type = AuthorityFactory::ObjectType::GEOGRAPHIC_3D_CRS; - } else if (type == GEOCENTRIC) { + } else if (type == CRS_SUBTYPE_GEOCENTRIC) { info.type = AuthorityFactory::ObjectType::GEOCENTRIC_CRS; - } else if (type == OTHER) { + } else if (type == CRS_SUBTYPE_OTHER) { info.type = AuthorityFactory::ObjectType::GEODETIC_CRS; - } else if (type == PROJECTED) { + } else if (type == CRS_SUBTYPE_PROJECTED) { info.type = AuthorityFactory::ObjectType::PROJECTED_CRS; - } else if (type == VERTICAL) { + } else if (type == CRS_SUBTYPE_VERTICAL) { info.type = AuthorityFactory::ObjectType::VERTICAL_CRS; - } else if (type == COMPOUND) { + } else if (type == CRS_SUBTYPE_COMPOUND) { info.type = AuthorityFactory::ObjectType::COMPOUND_CRS; - } else if (type == ENGINEERING) { + } else if (type == CRS_SUBTYPE_ENGINEERING) { info.type = AuthorityFactory::ObjectType::ENGINEERING_CRS; - } else if (type == DERIVED_PROJECTED) { + } else if (type == CRS_SUBTYPE_DERIVED_PROJECTED) { info.type = AuthorityFactory::ObjectType::DERIVED_PROJECTED_CRS; } info.deprecated = row[4] == "1"; @@ -9450,17 +9440,22 @@ AuthorityFactory::createObjectsFromNameEx( res.emplace_back(TableType("geodetic_crs", std::string())); break; case ObjectType::GEOCENTRIC_CRS: - res.emplace_back(TableType("geodetic_crs", GEOCENTRIC)); + res.emplace_back( + TableType("geodetic_crs", CRS_SUBTYPE_GEOCENTRIC)); break; case ObjectType::GEOGRAPHIC_CRS: - res.emplace_back(TableType("geodetic_crs", GEOG_2D)); - res.emplace_back(TableType("geodetic_crs", GEOG_3D)); + res.emplace_back( + TableType("geodetic_crs", CRS_SUBTYPE_GEOG_2D)); + res.emplace_back( + TableType("geodetic_crs", CRS_SUBTYPE_GEOG_3D)); break; case ObjectType::GEOGRAPHIC_2D_CRS: - res.emplace_back(TableType("geodetic_crs", GEOG_2D)); + res.emplace_back( + TableType("geodetic_crs", CRS_SUBTYPE_GEOG_2D)); break; case ObjectType::GEOGRAPHIC_3D_CRS: - res.emplace_back(TableType("geodetic_crs", GEOG_3D)); + res.emplace_back( + TableType("geodetic_crs", CRS_SUBTYPE_GEOG_3D)); break; case ObjectType::PROJECTED_CRS: res.emplace_back(TableType("projected_crs", std::string())); From aad992172c53903e6ac59c1bc90d1970aeb7e149 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 9 May 2026 17:56:10 +0200 Subject: [PATCH 09/10] createPropertiesForInverse(): fix 'Inverse of Inverse of concatenated_op_without_plus'^ --- src/iso19111/operation/oputils.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/iso19111/operation/oputils.cpp b/src/iso19111/operation/oputils.cpp index 642db1cd83..879be6eb20 100644 --- a/src/iso19111/operation/oputils.cpp +++ b/src/iso19111/operation/oputils.cpp @@ -309,6 +309,11 @@ util::PropertyMap createPropertiesForInverse(const CoordinateOperation *op, forwardName != buildOpName(opType, sourceCRS, targetCRS)) { if (forwardName.find(" + ") != std::string::npos) { name = INVERSE_OF + '\'' + forwardName + '\''; + } else if (starts_with(forwardName, INVERSE_OF)) { + name = forwardName.substr(INVERSE_OF.size()); + if (!name.empty() && name.front() == '\'' && + name.back() == '\'') + name = name.substr(1, name.size() - 2); } else { name = INVERSE_OF + forwardName; } From 00d0f25b08e22930bb62864d4947bf01c6706890 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 9 May 2026 17:56:37 +0200 Subject: [PATCH 10/10] createOperationsEnsembleCRSToOtherGeodCRS(): better deal with geographic 3D / geocentric CRS --- .../operation/coordinateoperationfactory.cpp | 103 +++++++++++++++--- test/cli/test_projinfo.yaml | 3 +- 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/src/iso19111/operation/coordinateoperationfactory.cpp b/src/iso19111/operation/coordinateoperationfactory.cpp index 38a9d36dcf..83328baec7 100644 --- a/src/iso19111/operation/coordinateoperationfactory.cpp +++ b/src/iso19111/operation/coordinateoperationfactory.cpp @@ -4082,11 +4082,11 @@ CoordinateOperationFactory::Private::createOperationsEnsembleCRSToOtherGeodCRS( const auto getNatureOf = [](const crs::GeodeticCRS &crs) -> std::string { if (dynamic_cast(&crs) != nullptr) { if (crs.coordinateSystem()->axisList().size() == 2) - return "geographic2D"; + return CRS_SUBTYPE_GEOG_2D; else - return "geographic3D"; + return CRS_SUBTYPE_GEOG_3D; } else { - return "geodetic"; + return CRS_SUBTYPE_GEOCENTRIC; } }; @@ -4094,14 +4094,19 @@ CoordinateOperationFactory::Private::createOperationsEnsembleCRSToOtherGeodCRS( const auto srcEnsemble = geodSrc->datumEnsemble(); assert(srcEnsemble); const auto &dstDomains = geodDst->domains(); + const auto &dstExtent = dstDomains[0]->domainOfValidity(); + if (!dstExtent) { + return {}; + } + const auto &areaOfInterest = context.context->getAreaOfInterest(); const bool is3D = geodSrc->coordinateSystem()->axisList().size() == 3 && geodDst->coordinateSystem()->axisList().size() == 3; - const char *intermediateType = is3D ? "geographic 3D" : "geographic 2D"; - std::vector newOps; - const auto &dstExtent = dstDomains[0]->domainOfValidity(); - if (dstExtent) { + for (int iter = 0; iter < (is3D ? 2 : 1); ++iter) { + const char *intermediateType = + is3D ? (iter == 0 ? CRS_SUBTYPE_GEOG_3D : CRS_SUBTYPE_GEOCENTRIC) + : CRS_SUBTYPE_GEOG_2D; for (const auto &srcDatumCandidate : srcEnsemble->datums()) { const auto &srcDatumDomains = srcDatumCandidate->domains(); if (srcDatumDomains.empty()) @@ -4125,8 +4130,29 @@ CoordinateOperationFactory::Private::createOperationsEnsembleCRSToOtherGeodCRS( srcDatumCandidate.as_nullable())), std::string(), intermediateType); for (const auto &srcDatumGeogCRS : srcDatumGeogCRSList) { - auto ops = createOperations(srcDatumGeogCRS, sourceEpoch, - targetCRS, targetEpoch, context); + + std::vector ops; + std::vector opsLastConversion; + if (getNatureOf(*(srcDatumGeogCRS.get())) == + CRS_SUBTYPE_GEOCENTRIC && + getNatureOf(*geodDst) == CRS_SUBTYPE_GEOG_3D) { + auto tmpList = authFactory->createGeodeticCRSFromDatum( + NN_CHECK_ASSERT(geodDst->datum()), std::string(), + CRS_SUBTYPE_GEOCENTRIC); + if (!tmpList.empty()) { + ops = createOperations(srcDatumGeogCRS, sourceEpoch, + tmpList.front(), targetEpoch, + context); + opsLastConversion = + createOperations(tmpList.front(), targetEpoch, + targetCRS, targetEpoch, context); + } + } + if (ops.empty()) { + opsLastConversion.clear(); + ops = createOperations(srcDatumGeogCRS, sourceEpoch, + targetCRS, targetEpoch, context); + } if (getNatureOf(*(srcDatumGeogCRS.get())) == getNatureOf(*geodSrc) && srcDatumGeogCRS->coordinateSystem()->_isEquivalentTo( @@ -4142,6 +4168,12 @@ CoordinateOperationFactory::Private::createOperationsEnsembleCRSToOtherGeodCRS( util::IComparable::Criterion::EQUIVALENT)) { newOp->setInterpolationCRS(sourceCRS); } + if (!opsLastConversion.empty()) { + newOp = ConcatenatedOperation:: + createComputeMetadata( + {newOp, opsLastConversion.front()}, + context.disallowEmptyIntersection()); + } setCRSs(newOp.get(), sourceCRS, targetCRS); newOps.push_back(newOp); } @@ -4150,20 +4182,55 @@ CoordinateOperationFactory::Private::createOperationsEnsembleCRSToOtherGeodCRS( // Expected to get only one as this is a null // transformation between an ensemble and one // of its member - const auto opsFirst = - createOperations(sourceCRS, sourceEpoch, - srcDatumGeogCRS, sourceEpoch, context); + std::vector opsFirstConversion; + std::vector opsFirst; + if (getNatureOf(*(srcDatumGeogCRS.get())) == + CRS_SUBTYPE_GEOCENTRIC && + getNatureOf(*geodSrc) == CRS_SUBTYPE_GEOG_3D) { + auto tmpList = authFactory->createGeodeticCRSFromDatum( + geodSrc->datumNonNull( + authFactory->databaseContext()), + std::string(), CRS_SUBTYPE_GEOCENTRIC); + if (!tmpList.empty()) { + opsFirstConversion = createOperations( + sourceCRS, sourceEpoch, tmpList.front(), + sourceEpoch, context); + context + .inCreateOperationsWithDatumPivotAntiRecursion = + false; + opsFirst = createOperations( + tmpList.front(), sourceEpoch, srcDatumGeogCRS, + sourceEpoch, context); + context + .inCreateOperationsWithDatumPivotAntiRecursion = + true; + } + } + if (opsFirst.empty()) { + opsFirstConversion.clear(); + opsFirst = createOperations(sourceCRS, sourceEpoch, + srcDatumGeogCRS, + sourceEpoch, context); + } if (!opsFirst.empty() && !opsFirst.front()->hasBallparkTransformation()) { const auto &opFirst = opsFirst.front(); for (const auto &op : ops) { if (!op->hasBallparkTransformation()) { - newOps.push_back( - ConcatenatedOperation:: - createComputeMetadata( - {opFirst, op}, - context - .disallowEmptyIntersection())); + std::vector tmp; + if (!opsFirstConversion.empty()) { + tmp.push_back(opsFirstConversion.front()); + } + tmp.push_back(opFirst); + tmp.push_back(op); + if (!opsLastConversion.empty()) { + tmp.push_back(opsLastConversion.front()); + } + auto newOp = ConcatenatedOperation:: + createComputeMetadata( + tmp, + context.disallowEmptyIntersection()); + newOps.push_back(newOp); } } } diff --git a/test/cli/test_projinfo.yaml b/test/cli/test_projinfo.yaml index 28ec769426..eac2dedb2f 100644 --- a/test/cli/test_projinfo.yaml +++ b/test/cli/test_projinfo.yaml @@ -1718,7 +1718,8 @@ tests: - comment: Quick check of NKG transformations args: -s EPSG:7789 -t EPSG:4936 --area EPSG:1080 --summary --hide-ballpark out: | - Candidate operations found: 1 + Candidate operations found: 2 + EPSG:10894, ITRF2014 to ETRS89-DNK (1), 0.006 m, Denmark - onshore and offshore., time-dependent operation NKG:ITRF2014_TO_DK, ITRF2014 to ETRS89(DK), 0.01 m, Denmark - onshore and offshore., time-dependent operation - args: --dump-db-structure head: 5