Skip to content

Commit

Permalink
WKT ESRI export: use LINUNIT node for geographic 3D CRS, unless WKTFo…
Browse files Browse the repository at this point in the history
…rmatter::setAllowLINUNITNode(false) or proj_as_wkt(..., options = ['ALLOW_LINUNIT_NODE=NO]) is specified
  • Loading branch information
rouault committed Jul 9, 2022
1 parent da21da6 commit 8719202
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 29 deletions.
3 changes: 3 additions & 0 deletions include/proj/io.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ class PROJ_GCC_DLL WKTFormatter {
setAllowEllipsoidalHeightAsVerticalCRS(bool allow) noexcept;
PROJ_DLL bool isAllowedEllipsoidalHeightAsVerticalCRS() const noexcept;

PROJ_DLL WKTFormatter &setAllowLINUNITNode(bool allow) noexcept;
PROJ_DLL bool isAllowedLINUNITNode() const noexcept;

PROJ_DLL const std::string &toString() const;

PROJ_PRIVATE :
Expand Down
5 changes: 5 additions & 0 deletions src/iso19111/c_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,9 @@ const char *proj_get_id_code(const PJ *obj, int index) {
* to YES and type == PJ_WKT1_GDAL, a Geographic 3D CRS or a Projected 3D CRS
* will be exported as a compound CRS whose vertical part represents an
* ellipsoidal height (for example for use with LAS 1.4 WKT1).</li>
* <li>ALLOW_LINUNIT_NODE=YES/NO. Default is YES starting with PROJ 9.1.
* Only taken into account with type == PJ_WKT1_ESRI on a Geographic 3D
* CRS.</li>
* </ul>
* @return a string, or NULL in case of error.
*/
Expand Down Expand Up @@ -1580,6 +1583,8 @@ const char *proj_as_wkt(PJ_CONTEXT *ctx, const PJ *obj, PJ_WKT_TYPE type,
"ALLOW_ELLIPSOIDAL_HEIGHT_AS_VERTICAL_CRS="))) {
formatter->setAllowEllipsoidalHeightAsVerticalCRS(
ci_equal(value, "YES"));
} else if ((value = getOptionValue(*iter, "ALLOW_LINUNIT_NODE="))) {
formatter->setAllowLINUNITNode(ci_equal(value, "YES"));
} else {
std::string msg("Unknown option :");
msg += *iter;
Expand Down
6 changes: 3 additions & 3 deletions src/iso19111/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,10 @@ void UnitOfMeasure::_exportToWKT(
const bool isWKT2 = formatter->version() == WKTFormatter::Version::WKT2;

const auto l_type = type();
if (formatter->forceUNITKeyword() && l_type != Type::PARAMETRIC) {
formatter->startNode(WKTConstants::UNIT, !codeSpace().empty());
} else if (!unitType.empty()) {
if (!unitType.empty()) {
formatter->startNode(unitType, !codeSpace().empty());
} else if (formatter->forceUNITKeyword() && l_type != Type::PARAMETRIC) {
formatter->startNode(WKTConstants::UNIT, !codeSpace().empty());
} else {
if (isWKT2 && l_type == Type::LINEAR) {
formatter->startNode(WKTConstants::LENGTHUNIT,
Expand Down
45 changes: 26 additions & 19 deletions src/iso19111/crs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1982,28 +1982,34 @@ void GeodeticCRS::_exportToWKT(io::WKTFormatter *formatter) const {

const auto &cs = coordinateSystem();
const auto &axisList = cs->axisList();
const bool isGeographic3D = isGeographic && axisList.size() == 3;
const auto oldAxisOutputRule = formatter->outputAxis();
auto l_name = nameStr();
const auto &dbContext = formatter->databaseContext();

if (!isWKT2 && formatter->useESRIDialect() && axisList.size() == 3) {
const bool isESRIExport = !isWKT2 && formatter->useESRIDialect();
const auto &l_identifiers = identifiers();

if (isESRIExport && axisList.size() == 3) {
if (!isGeographic) {
io::FormattingException::Throw(
"Geocentric CRS not supported in WKT1_ESRI");
}
// Try to format the Geographic 3D CRS as a GEOGCS[],VERTCS[...,DATUM[]]
// if we find corresponding objects
if (dbContext) {
if (exportAsESRIWktCompoundCRSWithEllipsoidalHeight(this, this,
formatter)) {
return;
if (!formatter->isAllowedLINUNITNode()) {
// Try to format the Geographic 3D CRS as a
// GEOGCS[],VERTCS[...,DATUM[]] if we find corresponding objects
if (dbContext) {
if (exportAsESRIWktCompoundCRSWithEllipsoidalHeight(
this, this, formatter)) {
return;
}
}
io::FormattingException::Throw(
"Cannot export this Geographic 3D CRS in WKT1_ESRI");
}
io::FormattingException::Throw(
"Cannot export this Geographic 3D CRS in WKT1_ESRI");
}

if (!isWKT2 && formatter->isStrict() && isGeographic &&
if (!isWKT2 && !isESRIExport && formatter->isStrict() && isGeographic &&
axisList.size() == 3 &&
oldAxisOutputRule != io::WKTFormatter::OutputAxisRule::NO) {

Expand Down Expand Up @@ -2048,7 +2054,6 @@ void GeodeticCRS::_exportToWKT(io::WKTFormatter *formatter) const {
"WKT1 does not support Geographic 3D CRS.");
}

const auto &l_identifiers = identifiers();
formatter->startNode(isWKT2
? ((formatter->use2019Keywords() && isGeographic)
? io::WKTConstants::GEOGCRS
Expand All @@ -2057,15 +2062,14 @@ void GeodeticCRS::_exportToWKT(io::WKTFormatter *formatter) const {
: io::WKTConstants::GEOGCS,
!l_identifiers.empty());

if (formatter->useESRIDialect()) {
if (isESRIExport) {
std::string l_esri_name;
if (l_name == "WGS 84") {
l_name = "GCS_WGS_1984";
l_esri_name = isGeographic3D ? "WGS_1984_3D" : "GCS_WGS_1984";
} else {
std::string l_esri_name;
if (dbContext) {
const auto tableName = isGeographic && axisList.size() == 3
? "geographic_3D_crs"
: "geodetic_crs";
const auto tableName =
isGeographic3D ? "geographic_3D_crs" : "geodetic_crs";
if (!l_identifiers.empty()) {
// Try to find the ESRI alias from the CRS identified by its
// id
Expand Down Expand Up @@ -2105,8 +2109,8 @@ void GeodeticCRS::_exportToWKT(io::WKTFormatter *formatter) const {
l_esri_name = "GCS_" + l_esri_name;
}
}
l_name = l_esri_name;
}
l_name = l_esri_name;
} else if (!isWKT2 && isDeprecated()) {
l_name += " (deprecated)";
}
Expand All @@ -2120,6 +2124,9 @@ void GeodeticCRS::_exportToWKT(io::WKTFormatter *formatter) const {
if (!isWKT2) {
unit._exportToWKT(formatter);
}
if (isGeographic3D && isESRIExport) {
axisList[2]->unit()._exportToWKT(formatter, io::WKTConstants::LINUNIT);
}

if (oldAxisOutputRule ==
io::WKTFormatter::OutputAxisRule::WKT1_GDAL_EPSG_STYLE &&
Expand All @@ -2131,7 +2138,7 @@ void GeodeticCRS::_exportToWKT(io::WKTFormatter *formatter) const {

ObjectUsage::baseExportToWKT(formatter);

if (!isWKT2 && !formatter->useESRIDialect()) {
if (!isWKT2 && !isESRIExport) {
const auto &extensionProj4 = CRS::getPrivate()->extensionProj4_;
if (!extensionProj4.empty()) {
formatter->startNode(io::WKTConstants::EXTENSION, false);
Expand Down
25 changes: 25 additions & 0 deletions src/iso19111/io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ struct WKTFormatter::Private {
bool use2019Keywords_ = false;
bool useESRIDialect_ = false;
bool allowEllipsoidalHeightAsVerticalCRS_ = false;
bool allowLINUNITNode_ = false;
OutputAxisRule outputAxis_ = WKTFormatter::OutputAxisRule::YES;
};
Params params_{};
Expand Down Expand Up @@ -298,6 +299,29 @@ bool WKTFormatter::isAllowedEllipsoidalHeightAsVerticalCRS() const noexcept {

// ---------------------------------------------------------------------------

/** \brief Set whether the formatter should export, in WKT1_ESRI, a Geographic
* 3D CRS with the relatively new (ArcGIS Pro >= 2.7) LINUNIT node.
* Defaults to true.
* @since PROJ 9.1
*/
WKTFormatter &WKTFormatter::setAllowLINUNITNode(bool allow) noexcept {
d->params_.allowLINUNITNode_ = allow;
return *this;
}

// ---------------------------------------------------------------------------

/** \brief Return whether the formatter should export, in WKT1_ESRI, a
* Geographic 3D CRS with the relatively new (ArcGIS Pro >= 2.7) LINUNIT node.
* Defaults to true.
* @since PROJ 9.1
*/
bool WKTFormatter::isAllowedLINUNITNode() const noexcept {
return d->params_.allowLINUNITNode_;
}

// ---------------------------------------------------------------------------

/** Returns the WKT string from the formatter. */
const std::string &WKTFormatter::toString() const {
if (d->indentLevel_ > 0 || d->level_ > 0) {
Expand Down Expand Up @@ -372,6 +396,7 @@ WKTFormatter::WKTFormatter(Convention convention)
d->params_.useESRIDialect_ = true;
d->params_.multiLine_ = false;
d->params_.outputAxis_ = WKTFormatter::OutputAxisRule::NO;
d->params_.allowLINUNITNode_ = true;
break;

default:
Expand Down
17 changes: 17 additions & 0 deletions test/unit/test_c_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,23 @@ TEST_F(CApi, proj_as_wkt) {
<< wkt;
}

// ALLOW_LINUNIT_NODE default value
{
auto wkt = proj_as_wkt(m_ctxt, crs4979, PJ_WKT1_ESRI, nullptr);
ASSERT_NE(wkt, nullptr);
EXPECT_TRUE(std::string(wkt).find("LINUNIT") != std::string::npos)
<< wkt;
}

// ALLOW_LINUNIT_NODE=NO
{
const char *const options[] = {"ALLOW_LINUNIT_NODE=NO", nullptr};
auto wkt = proj_as_wkt(m_ctxt, crs4979, PJ_WKT1_ESRI, options);
ASSERT_NE(wkt, nullptr);
EXPECT_TRUE(std::string(wkt).find("LINUNIT") == std::string::npos)
<< wkt;
}

// unsupported option
{
const char *const options[] = {"unsupported=yes", nullptr};
Expand Down
28 changes: 27 additions & 1 deletion test/unit/test_crs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,31 @@ TEST(crs, EPSG_4979_as_WKT1_ESRI) {
auto crs = GeographicCRS::EPSG_4979;
WKTFormatterNNPtr f(
WKTFormatter::create(WKTFormatter::Convention::WKT1_ESRI));
EXPECT_THROW(crs->exportToWKT(f.get()), FormattingException);
const auto wkt = "GEOGCS[\"WGS_1984_3D\",DATUM[\"D_WGS_1984\","
"SPHEROID[\"WGS_1984\",6378137.0,298.257223563]],"
"PRIMEM[\"Greenwich\",0.0],"
"UNIT[\"Degree\",0.0174532925199433],"
"LINUNIT[\"Meter\",1.0]]";

EXPECT_EQ(crs->exportToWKT(f.get()), wkt);
}

// ---------------------------------------------------------------------------

TEST(crs, geographic3D_crs_as_WKT1_ESRI_database) {
auto dbContext = DatabaseContext::create();
auto factory = AuthorityFactory::create(dbContext, "EPSG");
auto crs = factory->createCoordinateReferenceSystem("7087");
WKTFormatterNNPtr f(
WKTFormatter::create(WKTFormatter::Convention::WKT1_ESRI, dbContext));
const auto wkt = "GEOGCS[\"RGTAAF07_(lon-lat)_3D\","
"DATUM[\"D_Reseau_Geodesique_des_Terres_Australes_et_"
"Antarctiques_Francaises_2007\","
"SPHEROID[\"GRS_1980\",6378137.0,298.257222101]],"
"PRIMEM[\"Greenwich\",0.0],"
"UNIT[\"Degree\",0.0174532925199433],"
"LINUNIT[\"Meter\",1.0]]";
EXPECT_EQ(crs->exportToWKT(f.get()), wkt);
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -772,6 +796,7 @@ TEST(crs,
EXPECT_EQ(crs->coordinateSystem()->axisList().size(), 3U);
WKTFormatterNNPtr f(WKTFormatter::create(
WKTFormatter::Convention::WKT1_ESRI, DatabaseContext::create()));
f->setAllowLINUNITNode(false);
// Situation where there is no EPSG official name
EXPECT_EQ(crs->exportToWKT(f.get()),
"GEOGCS[\"California_SRS_Epoch_2017.50_(NAD83)\","
Expand All @@ -794,6 +819,7 @@ TEST(crs, implicit_compound_ESRI_104971_to_3D_as_WKT1_ESRI_with_database) {
std::string(), dbContext);
WKTFormatterNNPtr f(WKTFormatter::create(
WKTFormatter::Convention::WKT1_ESRI, DatabaseContext::create()));
f->setAllowLINUNITNode(false);
// Situation where there is no ESRI vertical CRS, but the GEOGCS does exist
// This will be only partly recognized by ESRI software.
// See /~https://github.com/OSGeo/PROJ/issues/2757
Expand Down
10 changes: 4 additions & 6 deletions test/unit/test_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3113,12 +3113,10 @@ TEST(wkt_parse, implicit_compound_CRS_geographic_with_ellipsoidal_height_ESRI) {
auto crs = nn_dynamic_pointer_cast<GeographicCRS>(obj);
ASSERT_TRUE(crs != nullptr);
EXPECT_EQ(crs->coordinateSystem()->axisList().size(), 3U);

EXPECT_EQ(
crs->exportToWKT(
WKTFormatter::create(WKTFormatter::Convention::WKT1_ESRI, dbContext)
.get()),
wkt);
WKTFormatterNNPtr f(
WKTFormatter::create(WKTFormatter::Convention::WKT1_ESRI, dbContext));
f->setAllowLINUNITNode(false);
EXPECT_EQ(crs->exportToWKT(f.get()), wkt);
}

// ---------------------------------------------------------------------------
Expand Down

0 comments on commit 8719202

Please sign in to comment.