From 5ddddfac622f40a4de920460b64e88e7b9cb5b68 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 12 Mar 2021 22:43:42 +0100 Subject: Add C/C++ API to get SQL statements to insert a CRS into database --- data/sql/customizations.sql | 3 + include/proj/io.hpp | 13 + scripts/reference_exported_symbols.txt | 9 + src/iso19111/c_api.cpp | 221 +++++ src/iso19111/crs.cpp | 16 +- src/iso19111/factory.cpp | 1514 +++++++++++++++++++++++++++++++- src/proj.h | 24 + test/unit/test_c_api.cpp | 88 ++ test/unit/test_factory.cpp | 573 ++++++++++++ 9 files changed, 2446 insertions(+), 15 deletions(-) diff --git a/data/sql/customizations.sql b/data/sql/customizations.sql index 63780e3a..b14add32 100644 --- a/data/sql/customizations.sql +++ b/data/sql/customizations.sql @@ -1,5 +1,8 @@ -- This file is hand generated. +INSERT INTO "extent" VALUES('PROJ','EXTENT_UNKNOWN','Not specified','Not specified.',-90.0,90.0,-180.0,180.0,0); +INSERT INTO "scope" VALUES('PROJ','SCOPE_UNKNOWN','Not known.',0); + -- grid_alternatives entries created from existing ones INSERT INTO grid_alternatives(original_grid_name, diff --git a/include/proj/io.hpp b/include/proj/io.hpp index 2f935a0c..5f0dd475 100644 --- a/include/proj/io.hpp +++ b/include/proj/io.hpp @@ -838,6 +838,19 @@ class PROJ_GCC_DLL DatabaseContext { PROJ_DLL std::vector getDatabaseStructure() const; + PROJ_DLL void startInsertStatementsSession(); + + PROJ_DLL std::string + suggestsCodeFor(const common::IdentifiedObjectNNPtr &object, + const std::string &authName, bool numericCode); + + PROJ_DLL std::vector + getInsertStatementsFor(const common::IdentifiedObjectNNPtr &object, + const std::string &authName, const std::string &code, + bool numericCode); + + PROJ_DLL void stopInsertStatementsSession(); + PROJ_PRIVATE : //! @cond Doxygen_Suppress PROJ_DLL void * diff --git a/scripts/reference_exported_symbols.txt b/scripts/reference_exported_symbols.txt index c6ddc03a..798f515e 100644 --- a/scripts/reference_exported_symbols.txt +++ b/scripts/reference_exported_symbols.txt @@ -359,10 +359,14 @@ osgeo::proj::io::DatabaseContext::create(void*) osgeo::proj::io::DatabaseContext::~DatabaseContext() osgeo::proj::io::DatabaseContext::getAuthorities() const osgeo::proj::io::DatabaseContext::getDatabaseStructure() const +osgeo::proj::io::DatabaseContext::getInsertStatementsFor(dropbox::oxygen::nn > const&, std::string const&, std::string const&, bool) osgeo::proj::io::DatabaseContext::getMetadata(char const*) const osgeo::proj::io::DatabaseContext::getPath() const osgeo::proj::io::DatabaseContext::getSqliteHandle() const osgeo::proj::io::DatabaseContext::lookForGridInfo(std::string const&, bool, std::string&, std::string&, std::string&, bool&, bool&, bool&) const +osgeo::proj::io::DatabaseContext::startInsertStatementsSession() +osgeo::proj::io::DatabaseContext::stopInsertStatementsSession() +osgeo::proj::io::DatabaseContext::suggestsCodeFor(dropbox::oxygen::nn > const&, std::string const&, bool) osgeo::proj::io::FactoryException::~FactoryException() osgeo::proj::io::FactoryException::FactoryException(char const*) osgeo::proj::io::FactoryException::FactoryException(osgeo::proj::io::FactoryException const&) @@ -960,6 +964,7 @@ proj_get_crs_list_parameters_destroy proj_get_ellipsoid proj_get_id_auth_name proj_get_id_code +proj_get_insert_statements proj_get_name proj_get_non_deprecated proj_get_prime_meridian @@ -980,6 +985,8 @@ proj_grid_info proj_identify proj_info proj_init_info +proj_insert_object_session_create +proj_insert_object_session_destroy proj_int_list_destroy proj_is_crs proj_is_deprecated @@ -1017,7 +1024,9 @@ proj_prime_meridian_get_parameters proj_query_geodetic_crs_from_datum proj_roundtrip proj_rtodms +proj_string_destroy proj_string_list_destroy +proj_suggests_code_for proj_todeg proj_torad proj_trans diff --git a/src/iso19111/c_api.cpp b/src/iso19111/c_api.cpp index 8f1fac70..b97e7300 100644 --- a/src/iso19111/c_api.cpp +++ b/src/iso19111/c_api.cpp @@ -8727,3 +8727,224 @@ PJ *proj_concatoperation_get_step(PJ_CONTEXT *ctx, const PJ *concatoperation, } return pj_obj_create(ctx, steps[i_step]); } +// --------------------------------------------------------------------------- + +/** \brief Opaque object representing an insertion session. */ +struct PJ_INSERT_SESSION { + //! @cond Doxygen_Suppress + PJ_CONTEXT *ctx = nullptr; + //! @endcond +}; + +// --------------------------------------------------------------------------- + +/** \brief Starts a session for proj_get_insert_statements() + * + * Starts a new session for one or several calls to + * proj_get_insert_statements(). + * + * An insertion session guarantees that the inserted objects will not create + * conflicting intermediate objects. + * + * The session must be stopped with proj_insert_object_session_destroy(). + * + * Only one session may be active at a time for a given context. + * + * @param ctx PROJ context, or NULL for default context + * @return the session, or NULL in case of error. + * + * @since 8.1 + */ +PJ_INSERT_SESSION *proj_insert_object_session_create(PJ_CONTEXT *ctx) { + SANITIZE_CTX(ctx); + try { + auto dbContext = getDBcontext(ctx); + dbContext->startInsertStatementsSession(); + PJ_INSERT_SESSION *session = new PJ_INSERT_SESSION; + session->ctx = ctx; + return session; + } catch (const std::exception &e) { + proj_log_error(ctx, __FUNCTION__, e.what()); + return nullptr; + } +} + +// --------------------------------------------------------------------------- + +/** \brief Stops an insertion session started with + * proj_insert_object_session_create() + * + * @param ctx PROJ context, or NULL for default context + * @param session The insertion session. + * @since 8.1 + */ +void proj_insert_object_session_destroy(PJ_CONTEXT *ctx, + PJ_INSERT_SESSION *session) { + SANITIZE_CTX(ctx); + if (session) { + try { + if (session->ctx != ctx) { + proj_log_error(ctx, __FUNCTION__, + "proj_insert_object_session_destroy() called " + "with a context different from the one of " + "proj_insert_object_session_create()"); + } else { + auto dbContext = getDBcontext(ctx); + dbContext->stopInsertStatementsSession(); + } + } catch (const std::exception &e) { + proj_log_error(ctx, __FUNCTION__, e.what()); + } + delete session; + } +} + +// --------------------------------------------------------------------------- + +/** \brief Suggests a database code for the passed object. + * + * Supported type of objects are PrimeMeridian, Ellipsoid, Datum, DatumEnsemble, + * GeodeticCRS, ProjectedCRS, VerticalCRS, CompoundCRS, BoundCRS, Conversion. + * + * @param ctx PROJ context, or NULL for default context + * @param object Object for which to suggest a code. + * @param authority Authority name into which the object will be inserted. + * @param numeric_code Whether the code should be numeric, or derived from the + * object name. + * @param options NULL terminated list of options, or NULL. + * No options are supported currently. + * @return the suggested code, that is guaranteed to not conflict with an + * existing one (to be freed with proj_string_destroy), + * or nullptr in case of error. + * + * @since 8.1 + */ +char *proj_suggests_code_for(PJ_CONTEXT *ctx, const PJ *object, + const char *authority, int numeric_code, + const char *const *options) { + SANITIZE_CTX(ctx); + (void)options; + + if (!object || !authority) { + proj_context_errno_set(ctx, PROJ_ERR_OTHER_API_MISUSE); + proj_log_error(ctx, __FUNCTION__, "missing required input"); + return nullptr; + } + auto identifiedObject = + std::dynamic_pointer_cast(object->iso_obj); + if (!identifiedObject) { + proj_context_errno_set(ctx, PROJ_ERR_OTHER_API_MISUSE); + proj_log_error(ctx, __FUNCTION__, "Object is not a IdentifiedObject"); + return nullptr; + } + + try { + auto dbContext = getDBcontext(ctx); + return pj_strdup(dbContext + ->suggestsCodeFor(NN_NO_CHECK(identifiedObject), + std::string(authority), + numeric_code != FALSE) + .c_str()); + } catch (const std::exception &e) { + proj_log_error(ctx, __FUNCTION__, e.what()); + } + return nullptr; +} + +// --------------------------------------------------------------------------- + +/** \brief Free a string. + * + * Only to be used with functions that document using this function. + * + * @param str String to free. + * + * @since 8.1 + */ +void proj_string_destroy(char *str) { free(str); } + +// --------------------------------------------------------------------------- + +/** \brief Returns SQL statements needed to insert the passed object into the + * database. + * + * proj_insert_object_session_create() may have been called previously. + * + * @param ctx PROJ context, or NULL for default context + * @param session The insertion session. May be NULL if a single object must be + * inserted. + * @param object The object to insert into the database. Currently only + * PrimeMeridian, Ellipsoid, Datum, GeodeticCRS, ProjectedCRS, + * VerticalCRS, CompoundCRS or BoundCRS are supported. + * @param authority Authority name into which the object will be inserted. + * Must not be NULL. + * @param code Code with which the object will be inserted.Must not be NULL. + * @param numeric_codes Whether intermediate objects that can be created should + * use numeric codes (true), or may be alphanumeric (false) + * @param options NULL terminated list of options, or NULL. + * No options are supported currently. + * + * @return a list of insert statements (to be freed with + * proj_string_list_destroy()), or NULL in case of error. + * @since 8.1 + */ +PROJ_STRING_LIST proj_get_insert_statements(PJ_CONTEXT *ctx, + PJ_INSERT_SESSION *session, + const PJ *object, + const char *authority, + const char *code, int numeric_codes, + const char *const *options) { + SANITIZE_CTX(ctx); + (void)options; + + struct TempSessionHolder { + PJ_CONTEXT *m_ctx; + PJ_INSERT_SESSION *m_tempSession = nullptr; + TempSessionHolder(const TempSessionHolder &) = delete; + TempSessionHolder &operator=(const TempSessionHolder &) = delete; + + TempSessionHolder(PJ_CONTEXT *ctx, PJ_INSERT_SESSION *session) + : m_ctx(ctx), + m_tempSession(session ? nullptr + : proj_insert_object_session_create(ctx)) {} + + ~TempSessionHolder() { + if (m_tempSession) { + proj_insert_object_session_destroy(m_ctx, m_tempSession); + } + } + }; + + try { + TempSessionHolder oHolder(ctx, session); + if (!session) { + session = oHolder.m_tempSession; + if (!session) { + return nullptr; + } + } + + if (!object || !authority || !code) { + proj_context_errno_set(ctx, PROJ_ERR_OTHER_API_MISUSE); + proj_log_error(ctx, __FUNCTION__, "missing required input"); + return nullptr; + } + auto identifiedObject = + std::dynamic_pointer_cast(object->iso_obj); + if (!identifiedObject) { + proj_context_errno_set(ctx, PROJ_ERR_OTHER_API_MISUSE); + proj_log_error(ctx, __FUNCTION__, + "Object is not a IdentifiedObject"); + return nullptr; + } + + auto dbContext = getDBcontext(ctx); + auto statements = dbContext->getInsertStatementsFor( + NN_NO_CHECK(identifiedObject), authority, code, + numeric_codes != FALSE); + return to_string_list(std::move(statements)); + } catch (const std::exception &e) { + proj_log_error(ctx, __FUNCTION__, e.what()); + } + return nullptr; +} diff --git a/src/iso19111/crs.cpp b/src/iso19111/crs.cpp index 73c61e2c..65f2087d 100644 --- a/src/iso19111/crs.cpp +++ b/src/iso19111/crs.cpp @@ -4829,7 +4829,14 @@ CompoundCRS::identify(const io::AuthorityFactoryPtr &authorityFactory) const { const auto &thisName(nameStr()); const auto &components = componentReferenceSystems(); - const bool l_implicitCS = components[0]->hasImplicitCS(); + bool l_implicitCS = components[0]->hasImplicitCS(); + if (!l_implicitCS) { + const auto projCRS = + dynamic_cast(components[0].get()); + if (projCRS) { + l_implicitCS = projCRS->baseCRS()->hasImplicitCS(); + } + } const auto crsCriterion = l_implicitCS ? util::IComparable::Criterion::EQUIVALENT_EXCEPT_AXIS_ORDER_GEOGCRS @@ -4949,6 +4956,13 @@ CompoundCRS::identify(const io::AuthorityFactoryPtr &authorityFactory) const { } res.sort(lambdaSort); + + // If there's a single candidate at 90% confidence with same name, + // then promote it to 100% + if (res.size() == 1 && res.front().second == 90 && + thisName == res.front().first->nameStr()) { + res.front().second = 100; + } } // If we didn't find a match for the CompoundCRS, check if the diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp index fbe88dda..36ef217d 100644 --- a/src/iso19111/factory.cpp +++ b/src/iso19111/factory.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -101,14 +102,19 @@ constexpr int DATABASE_LAYOUT_VERSION_MAJOR = 1; // must be incremented. constexpr int DATABASE_LAYOUT_VERSION_MINOR = 0; +constexpr size_t N_MAX_PARAMS = 7; + // --------------------------------------------------------------------------- struct SQLValues { - enum class Type { STRING, DOUBLE }; + enum class Type { STRING, INT, DOUBLE }; // cppcheck-suppress noExplicitConstructor SQLValues(const std::string &value) : type_(Type::STRING), str_(value) {} + // cppcheck-suppress noExplicitConstructor + SQLValues(int value) : type_(Type::INT), int_(value) {} + // cppcheck-suppress noExplicitConstructor SQLValues(double value) : type_(Type::DOUBLE), double_(value) {} @@ -117,12 +123,16 @@ struct SQLValues { // cppcheck-suppress functionStatic const std::string &stringValue() const { return str_; } + // cppcheck-suppress functionStatic + int intValue() const { return int_; } + // cppcheck-suppress functionStatic double doubleValue() const { return double_; } private: Type type_; std::string str_{}; + int int_ = 0; double double_ = 0.0; }; @@ -255,7 +265,12 @@ struct DatabaseContext::Private { private: friend class DatabaseContext; + // This is a manual implementation of std::enable_shared_from_this<> that + // avoids publicly deriving from it. + std::weak_ptr self_{}; + std::string databasePath_{}; + std::vector auxiliaryDatabasePaths_{}; bool close_handle_ = true; sqlite3 *sqlite_handle_{}; std::map mapSqlToStatement_{}; @@ -265,6 +280,10 @@ struct DatabaseContext::Private { std::string lastMetadataValue_{}; std::map> mapCanonicalizeGRFName_{}; + // Used by startInsertStatementsSession() and related functions + std::string memoryDbForInsertPath_{}; + sqlite3 *memoryDbHandle_ = nullptr; + using LRUCacheOfObjects = lru11::Cache; static constexpr size_t CACHE_SIZE = 128; @@ -296,9 +315,84 @@ struct DatabaseContext::Private { void closeDB() noexcept; + void clearCaches(); + // cppcheck-suppress functionStatic void registerFunctions(); + std::string findFreeCode(const std::string &tableName, + const std::string &authName, + const std::string &codePrototype); + + void identify(const DatabaseContextNNPtr &dbContext, + const cs::CoordinateSystemNNPtr &obj, std::string &authName, + std::string &code); + void identifyOrInsert(const DatabaseContextNNPtr &dbContext, + const cs::CoordinateSystemNNPtr &obj, + const std::string &ownerType, + const std::string &ownerAuthName, + const std::string &ownerCode, std::string &authName, + std::string &code, + std::vector &sqlStatements); + + void identify(const DatabaseContextNNPtr &dbContext, + const common::UnitOfMeasure &obj, std::string &authName, + std::string &code); + void identifyOrInsert(const DatabaseContextNNPtr &dbContext, + const common::UnitOfMeasure &unit, + const std::string &ownerAuthName, + std::string &authName, std::string &code, + std::vector &sqlStatements); + + void appendSql(std::vector &sqlStatements, + const std::string &sql); + + void identifyOrInsertUsages(const common::ObjectUsageNNPtr &obj, + const std::string &tableName, + const std::string &authName, + const std::string &code, + std::vector &sqlStatements); + + std::vector + getInsertStatementsFor(const datum::PrimeMeridianNNPtr &pm, + const std::string &authName, const std::string &code, + bool numericCode); + + std::vector + getInsertStatementsFor(const datum::EllipsoidNNPtr &ellipsoid, + const std::string &authName, const std::string &code, + bool numericCode); + + std::vector + getInsertStatementsFor(const datum::GeodeticReferenceFrameNNPtr &datum, + const std::string &authName, const std::string &code, + bool numericCode); + + std::vector + getInsertStatementsFor(const crs::GeodeticCRSNNPtr &crs, + const std::string &authName, const std::string &code, + bool numericCode); + + std::vector + getInsertStatementsFor(const crs::ProjectedCRSNNPtr &crs, + const std::string &authName, const std::string &code, + bool numericCode); + + std::vector + getInsertStatementsFor(const datum::VerticalReferenceFrameNNPtr &datum, + const std::string &authName, const std::string &code, + bool numericCode); + + std::vector + getInsertStatementsFor(const crs::VerticalCRSNNPtr &crs, + const std::string &authName, const std::string &code, + bool numericCode); + + std::vector + getInsertStatementsFor(const crs::CompoundCRSNNPtr &crs, + const std::string &authName, const std::string &code, + bool numericCode); + #ifdef ENABLE_CUSTOM_LOCKLESS_VFS std::unique_ptr vfs_{}; #endif @@ -355,6 +449,24 @@ void DatabaseContext::Private::closeDB() noexcept { // --------------------------------------------------------------------------- +void DatabaseContext::Private::clearCaches() { + + cacheUOM_.clear(); + cacheCRS_.clear(); + cacheEllipsoid_.clear(); + cacheGeodeticDatum_.clear(); + cacheDatumEnsemble_.clear(); + cachePrimeMeridian_.clear(); + cacheCS_.clear(); + cacheExtent_.clear(); + cacheCRSToCrsCoordOp_.clear(); + cacheGridInfo_.clear(); + cacheAllowedAuthorities_.clear(); + cacheAliasNames_.clear(); +} + +// --------------------------------------------------------------------------- + void DatabaseContext::Private::insertIntoCache(LRUCacheOfObjects &cache, const std::string &code, const util::BaseObjectPtr &obj) { @@ -684,6 +796,10 @@ void DatabaseContext::Private::attachExtraDatabases( } closeDB(); + if (auxiliaryDatabasePaths.empty()) { + open(databasePath_, pjCtxt()); + return; + } sqlite3_open_v2( ":memory:", &sqlite_handle_, @@ -850,13 +966,16 @@ SQLResultSet DatabaseContext::Private::run(const std::string &sql, int nBindField = 1; for (const auto ¶m : parameters) { - if (param.type() == SQLValues::Type::STRING) { + const auto paramType = param.type(); + if (paramType == SQLValues::Type::STRING) { auto strValue = param.stringValue(); sqlite3_bind_text(stmt, nBindField, strValue.c_str(), static_cast(strValue.size()), SQLITE_TRANSIENT); + } else if (paramType == SQLValues::Type::INT) { + sqlite3_bind_int(stmt, nBindField, param.intValue()); } else { - assert(param.type() == SQLValues::Type::DOUBLE); + assert(paramType == SQLValues::Type::DOUBLE); sqlite3_bind_double(stmt, nBindField, param.doubleValue()); } nBindField++; @@ -869,8 +988,11 @@ SQLResultSet DatabaseContext::Private::run(const std::string &sql, nPos = sqlSubst.find('?', nPos); assert(nPos != std::string::npos); std::string strValue; - if (param.type() == SQLValues::Type::STRING) { + const auto paramType = param.type(); + if (paramType == SQLValues::Type::STRING) { strValue = '\'' + param.stringValue() + '\''; + } else if (paramType == SQLValues::Type::INT) { + strValue = toString(param.intValue()); } else { strValue = toString(param.doubleValue()); } @@ -915,12 +1037,1117 @@ SQLResultSet DatabaseContext::Private::run(const std::string &sql, return result; } +// --------------------------------------------------------------------------- + +static std::string formatStatement(const char *fmt, ...) { + std::string res; + va_list args; + va_start(args, fmt); + for (int i = 0; fmt[i] != '\0'; ++i) { + if (fmt[i] == '%') { + if (fmt[i + 1] == '%') { + res += '%'; + } else if (fmt[i + 1] == 'q') { + const char *arg = va_arg(args, const char *); + for (int j = 0; arg[j] != '\0'; ++j) { + if (arg[j] == '\'') + res += arg[j]; + res += arg[j]; + } + } else if (fmt[i + 1] == 's') { + const char *arg = va_arg(args, const char *); + res += arg; + } else if (fmt[i + 1] == 'f') { + const double arg = va_arg(args, double); + res += toString(arg); + } else if (fmt[i + 1] == 'd') { + const int arg = va_arg(args, int); + res += toString(arg); + } else { + va_end(args); + throw FactoryException( + "Unsupported formatter in formatStatement()"); + } + ++i; + } else { + res += fmt[i]; + } + } + va_end(args); + return res; +} + +// --------------------------------------------------------------------------- + +void DatabaseContext::Private::appendSql( + std::vector &sqlStatements, const std::string &sql) { + sqlStatements.emplace_back(sql); + char *errMsg = nullptr; + if (sqlite3_exec(memoryDbHandle_, sql.c_str(), nullptr, nullptr, &errMsg) != + SQLITE_OK) { + std::string s("Cannot execute " + sql); + if (errMsg) { + s += " : "; + s += errMsg; + } + sqlite3_free(errMsg); + throw FactoryException(s); + } + sqlite3_free(errMsg); +} + +// --------------------------------------------------------------------------- + +static void identifyFromNameOrCode( + const DatabaseContextNNPtr &dbContext, + const AuthorityFactoryNNPtr &allAuthFactory, + const common::IdentifiedObjectNNPtr &obj, + std::function( + const AuthorityFactoryNNPtr &authFactory, const std::string &)> + instantiateFunc, + AuthorityFactory::ObjectType objType, std::string &authName, + std::string &code) { + for (const auto &id : obj->identifiers()) { + try { + const auto tmpAuthFactory = + AuthorityFactory::create(dbContext, *(id->codeSpace())); + if (instantiateFunc(tmpAuthFactory, id->code()) + ->isEquivalentTo( + obj.get(), util::IComparable::Criterion::EQUIVALENT)) { + authName = *(id->codeSpace()); + code = id->code(); + return; + } + } catch (const std::exception &) { + } + } + + const auto candidates = allAuthFactory->createObjectsFromName( + obj->nameStr(), {objType}, false, 0); + for (const auto &candidate : candidates) { + const auto &ids = candidate->identifiers(); + if (!ids.empty() && + candidate->isEquivalentTo( + obj.get(), util::IComparable::Criterion::EQUIVALENT)) { + const auto &id = ids.front(); + authName = *(id->codeSpace()); + code = id->code(); + } + } +} + +// --------------------------------------------------------------------------- + +static void identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, + const AuthorityFactoryNNPtr &allAuthFactory, + const datum::DatumEnsembleNNPtr &obj, + std::string &authName, std::string &code) { + const auto instantiateFunc = [](const AuthorityFactoryNNPtr &authFactory, + const std::string &lCode) { + return util::nn_static_pointer_cast( + authFactory->createDatumEnsemble(lCode, "geodetic_datum")); + }; + identifyFromNameOrCode(dbContext, allAuthFactory, obj, instantiateFunc, + AuthorityFactory::ObjectType::DATUM_ENSEMBLE, + authName, code); +} + +// --------------------------------------------------------------------------- + +static void +identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, + const AuthorityFactoryNNPtr &allAuthFactory, + const datum::GeodeticReferenceFrameNNPtr &obj, + std::string &authName, std::string &code) { + const auto instantiateFunc = [](const AuthorityFactoryNNPtr &authFactory, + const std::string &lCode) { + return util::nn_static_pointer_cast( + authFactory->createGeodeticDatum(lCode)); + }; + identifyFromNameOrCode( + dbContext, allAuthFactory, obj, instantiateFunc, + AuthorityFactory::ObjectType::GEODETIC_REFERENCE_FRAME, authName, code); +} + +// --------------------------------------------------------------------------- + +static void identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, + const AuthorityFactoryNNPtr &allAuthFactory, + const datum::EllipsoidNNPtr &obj, + std::string &authName, std::string &code) { + const auto instantiateFunc = [](const AuthorityFactoryNNPtr &authFactory, + const std::string &lCode) { + return util::nn_static_pointer_cast( + authFactory->createEllipsoid(lCode)); + }; + identifyFromNameOrCode(dbContext, allAuthFactory, obj, instantiateFunc, + AuthorityFactory::ObjectType::ELLIPSOID, authName, + code); +} + +// --------------------------------------------------------------------------- + +static void identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, + const AuthorityFactoryNNPtr &allAuthFactory, + const datum::PrimeMeridianNNPtr &obj, + std::string &authName, std::string &code) { + const auto instantiateFunc = [](const AuthorityFactoryNNPtr &authFactory, + const std::string &lCode) { + return util::nn_static_pointer_cast( + authFactory->createPrimeMeridian(lCode)); + }; + identifyFromNameOrCode(dbContext, allAuthFactory, obj, instantiateFunc, + AuthorityFactory::ObjectType::PRIME_MERIDIAN, + authName, code); +} + +// --------------------------------------------------------------------------- + +static void +identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, + const AuthorityFactoryNNPtr &allAuthFactory, + const datum::VerticalReferenceFrameNNPtr &obj, + std::string &authName, std::string &code) { + const auto instantiateFunc = [](const AuthorityFactoryNNPtr &authFactory, + const std::string &lCode) { + return util::nn_static_pointer_cast( + authFactory->createVerticalDatum(lCode)); + }; + identifyFromNameOrCode( + dbContext, allAuthFactory, obj, instantiateFunc, + AuthorityFactory::ObjectType::VERTICAL_REFERENCE_FRAME, authName, code); +} + +// --------------------------------------------------------------------------- + +static const char *getCSDatabaseType(const cs::CoordinateSystemNNPtr &obj) { + if (dynamic_cast(obj.get())) { + return "ellipsoidal"; + } else if (dynamic_cast(obj.get())) { + return "Cartesian"; + } else if (dynamic_cast(obj.get())) { + return "vertical"; + } + return nullptr; +} + +// --------------------------------------------------------------------------- + +std::string +DatabaseContext::Private::findFreeCode(const std::string &tableName, + const std::string &authName, + const std::string &codePrototype) { + std::string code(codePrototype); + if (run("SELECT 1 FROM " + tableName + " WHERE auth_name = ? AND code = ?", + {authName, code}) + .empty()) { + return code; + } + + for (int counter = 2; counter < 10; counter++) { + code = codePrototype + '_' + toString(counter); + if (run("SELECT 1 FROM " + tableName + + " WHERE auth_name = ? AND code = ?", + {authName, code}) + .empty()) { + return code; + } + } + + // shouldn't happen hopefully... + throw FactoryException("Cannot insert " + tableName + + ": too many similar codes"); +} + +// --------------------------------------------------------------------------- + +static const char *getUnitDatabaseType(const common::UnitOfMeasure &unit) { + switch (unit.type()) { + case common::UnitOfMeasure::Type::LINEAR: + return "length"; + + case common::UnitOfMeasure::Type::ANGULAR: + return "angle"; + + case common::UnitOfMeasure::Type::SCALE: + return "scale"; + + case common::UnitOfMeasure::Type::TIME: + return "time"; + + default: + break; + } + return nullptr; +} + +// --------------------------------------------------------------------------- + +void DatabaseContext::Private::identify(const DatabaseContextNNPtr &dbContext, + const common::UnitOfMeasure &obj, + std::string &authName, + std::string &code) { + // Identify quickly a few well-known units + const double convFactor = obj.conversionToSI(); + switch (obj.type()) { + case common::UnitOfMeasure::Type::LINEAR: { + if (convFactor == 1.0) { + authName = "EPSG"; + code = "9001"; + return; + } + break; + } + case common::UnitOfMeasure::Type::ANGULAR: { + constexpr double CONV_FACTOR_DEGREE = 1.74532925199432781271e-02; + if (std::abs(convFactor - CONV_FACTOR_DEGREE) <= + 1e-10 * CONV_FACTOR_DEGREE) { + authName = "EPSG"; + code = "9102"; + return; + } + break; + } + case common::UnitOfMeasure::Type::SCALE: { + if (convFactor == 1.0) { + authName = "EPSG"; + code = "9201"; + return; + } + break; + } + default: + break; + } + + std::string sql("SELECT auth_name, code FROM unit_of_measure " + "WHERE abs(conv_factor - ?) <= 1e-10 * conv_factor"); + ListOfParams params{convFactor}; + const char *type = getUnitDatabaseType(obj); + if (type) { + sql += " AND type = ?"; + params.emplace_back(std::string(type)); + } + sql += " ORDER BY auth_name, code"; + const auto res = run(sql, params); + for (const auto &row : res) { + const auto &rowAuthName = row[0]; + const auto &rowCode = row[1]; + const auto tmpAuthFactory = + AuthorityFactory::create(dbContext, rowAuthName); + try { + tmpAuthFactory->createUnitOfMeasure(rowCode); + authName = rowAuthName; + code = rowCode; + return; + } catch (const std::exception &) { + } + } +} + +// --------------------------------------------------------------------------- + +void DatabaseContext::Private::identifyOrInsert( + const DatabaseContextNNPtr &dbContext, const common::UnitOfMeasure &unit, + const std::string &ownerAuthName, std::string &authName, std::string &code, + std::vector &sqlStatements) { + authName = unit.codeSpace(); + code = unit.code(); + if (authName.empty()) { + identify(dbContext, unit, authName, code); + } + if (!authName.empty()) { + return; + } + const char *type = getUnitDatabaseType(unit); + if (type == nullptr) { + throw FactoryException("Cannot insert this type of UnitOfMeasure"); + } + + // Insert new record + authName = ownerAuthName; + const std::string codePrototype(replaceAll(toupper(unit.name()), " ", "_")); + code = findFreeCode("unit_of_measure", authName, codePrototype); + + const auto sql = formatStatement( + "INSERT INTO unit_of_measure VALUES('%q','%q','%q','%q',%f,NULL,0);", + authName.c_str(), code.c_str(), unit.name().c_str(), type, + unit.conversionToSI()); + appendSql(sqlStatements, sql); +} + +// --------------------------------------------------------------------------- + +void DatabaseContext::Private::identify(const DatabaseContextNNPtr &dbContext, + const cs::CoordinateSystemNNPtr &obj, + std::string &authName, + std::string &code) { + + const auto &axisList = obj->axisList(); + if (axisList.size() == 1U && + axisList[0]->unit()._isEquivalentTo(UnitOfMeasure::METRE) && + &(axisList[0]->direction()) == &cs::AxisDirection::UP && + (axisList[0]->nameStr() == "Up" || + axisList[0]->nameStr() == "Gravity-related height")) { + // preferred coordinate system for gravity-related height + authName = "EPSG"; + code = "6499"; + return; + } + + std::string sql( + "SELECT auth_name, code FROM coordinate_system WHERE dimension = ?"); + ListOfParams params{static_cast(axisList.size())}; + const char *type = getCSDatabaseType(obj); + if (type) { + sql += " AND type = ?"; + params.emplace_back(std::string(type)); + } + sql += " ORDER BY auth_name, code"; + const auto res = run(sql, params); + for (const auto &row : res) { + const auto &rowAuthName = row[0]; + const auto &rowCode = row[1]; + const auto tmpAuthFactory = + AuthorityFactory::create(dbContext, rowAuthName); + try { + const auto cs = tmpAuthFactory->createCoordinateSystem(rowCode); + if (cs->_isEquivalentTo(obj.get(), + util::IComparable::Criterion::EQUIVALENT)) { + authName = rowAuthName; + code = rowCode; + if (authName == "EPSG" && code == "4400") { + // preferred coordinate system for cartesian + // Easting, Northing + return; + } + if (authName == "EPSG" && code == "6422") { + // preferred coordinate system for geographic lat, lon + return; + } + if (authName == "EPSG" && code == "6423") { + // preferred coordinate system for geographic lat, lon, h + return; + } + } + } catch (const std::exception &) { + } + } +} + +// --------------------------------------------------------------------------- + +void DatabaseContext::Private::identifyOrInsert( + const DatabaseContextNNPtr &dbContext, const cs::CoordinateSystemNNPtr &obj, + const std::string &ownerType, const std::string &ownerAuthName, + const std::string &ownerCode, std::string &authName, std::string &code, + std::vector &sqlStatements) { + + identify(dbContext, obj, authName, code); + if (!authName.empty()) { + return; + } + + const char *type = getCSDatabaseType(obj); + if (type == nullptr) { + throw FactoryException("Cannot insert this type of CoordinateSystem"); + } + + // Insert new record in coordinate_system + authName = ownerAuthName; + const std::string codePrototype("CS_" + ownerType + '_' + ownerCode); + code = findFreeCode("coordinate_system", authName, codePrototype); + + const auto &axisList = obj->axisList(); + { + const auto sql = formatStatement( + "INSERT INTO coordinate_system VALUES('%q','%q','%q',%d);", + authName.c_str(), code.c_str(), type, + static_cast(axisList.size())); + appendSql(sqlStatements, sql); + } + + // Insert new records for the axis + for (int i = 0; i < static_cast(axisList.size()); ++i) { + const auto &axis = axisList[i]; + std::string uomAuthName; + std::string uomCode; + identifyOrInsert(dbContext, axis->unit(), ownerAuthName, uomAuthName, + uomCode, sqlStatements); + const auto sql = formatStatement( + "INSERT INTO axis VALUES(" + "'%q','%q','%q','%q','%q','%q','%q',%d,'%q','%q');", + authName.c_str(), (code + "_AXIS_" + toString(i + 1)).c_str(), + axis->nameStr().c_str(), axis->abbreviation().c_str(), + axis->direction().toString().c_str(), authName.c_str(), + code.c_str(), i + 1, uomAuthName.c_str(), uomCode.c_str()); + appendSql(sqlStatements, sql); + } +} + +// --------------------------------------------------------------------------- + +void DatabaseContext::Private::identifyOrInsertUsages( + const common::ObjectUsageNNPtr &obj, const std::string &tableName, + const std::string &authName, const std::string &code, + std::vector &sqlStatements) { + + std::string usageCode("USAGE_"); + const std::string upperTableName(toupper(tableName)); + if (!starts_with(code, upperTableName)) { + usageCode += upperTableName; + usageCode += '_'; + } + usageCode += code; + + const auto &domains = obj->domains(); + if (domains.empty()) { + const auto sql = + formatStatement("INSERT INTO usage VALUES('%q','%q','%q','%q','%q'," + "'PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN');", + authName.c_str(), usageCode.c_str(), + tableName.c_str(), authName.c_str(), code.c_str()); + appendSql(sqlStatements, sql); + return; + } + + int usageCounter = 1; + for (const auto &domain : domains) { + std::string scopeAuthName; + std::string scopeCode; + const auto &scope = domain->scope(); + if (scope.has_value()) { + const auto rows = + run("SELECT auth_name, code, " + "(CASE WHEN auth_name = 'EPSG' THEN 0 ELSE 1 END) " + "AS order_idx " + "FROM scope WHERE scope = ? AND deprecated = 0 " + "ORDER BY order_idx", + {*scope}); + if (!rows.empty()) { + const auto &row = rows.front(); + scopeAuthName = row[0]; + scopeCode = row[1]; + } else { + scopeAuthName = authName; + scopeCode = "SCOPE_" + tableName + "_" + code; + const auto sql = formatStatement( + "INSERT INTO scope VALUES('%q','%q','%q',0);", + scopeAuthName.c_str(), scopeCode.c_str(), scope->c_str()); + appendSql(sqlStatements, sql); + } + } else { + scopeAuthName = "PROJ"; + scopeCode = "SCOPE_UNKNOWN"; + } + + std::string extentAuthName("PROJ"); + std::string extentCode("EXTENT_UNKNOWN"); + const auto &extent = domain->domainOfValidity(); + if (extent) { + const auto &geogElts = extent->geographicElements(); + if (!geogElts.empty()) { + const auto bbox = + dynamic_cast( + geogElts.front().get()); + if (bbox) { + const auto rows = run( + "SELECT auth_name, code, " + "(CASE WHEN auth_name = 'EPSG' THEN 0 ELSE 1 END) " + "AS order_idx " + "FROM extent WHERE south_lat = ? AND north_lat = ? " + "AND west_lon = ? AND east_lon = ? AND deprecated = 0 " + "ORDER BY order_idx", + {bbox->southBoundLatitude(), bbox->northBoundLatitude(), + bbox->westBoundLongitude(), + bbox->eastBoundLongitude()}); + if (!rows.empty()) { + const auto &row = rows.front(); + extentAuthName = row[0]; + extentCode = row[1]; + } else { + extentAuthName = authName; + extentCode = "EXTENT_" + tableName + "_" + code; + std::string description(*(extent->description())); + if (description.empty()) { + description = "unknown"; + } + const auto sql = formatStatement( + "INSERT INTO extent " + "VALUES('%q','%q','%q','%q',%f,%f,%f,%f,0);", + extentAuthName.c_str(), extentCode.c_str(), + description.c_str(), description.c_str(), + bbox->southBoundLatitude(), + bbox->northBoundLatitude(), + bbox->westBoundLongitude(), + bbox->eastBoundLongitude()); + appendSql(sqlStatements, sql); + } + } + } + } + + if (domains.size() > 1) { + usageCode += '_'; + usageCode += toString(usageCounter); + } + const auto sql = formatStatement( + "INSERT INTO usage VALUES('%q','%q','%q','%q','%q'," + "'%q','%q','%q','%q');", + authName.c_str(), usageCode.c_str(), tableName.c_str(), + authName.c_str(), code.c_str(), extentAuthName.c_str(), + extentCode.c_str(), scopeAuthName.c_str(), scopeCode.c_str()); + appendSql(sqlStatements, sql); + + usageCounter++; + } +} + +// --------------------------------------------------------------------------- + +std::vector DatabaseContext::Private::getInsertStatementsFor( + const datum::PrimeMeridianNNPtr &pm, const std::string &authName, + const std::string &code, bool /*numericCode*/) { + + const auto self = NN_NO_CHECK(self_.lock()); + const auto allAuthFactory = AuthorityFactory::create(self, std::string()); + + // Check if the object is already known under that code + std::string pmAuthName; + std::string pmCode; + identifyFromNameOrCode(self, allAuthFactory, pm, pmAuthName, pmCode); + if (pmAuthName == authName && pmCode == code) { + return {}; + } + + std::vector sqlStatements; + + // Insert new record in prime_meridian table + std::string uomAuthName; + std::string uomCode; + identifyOrInsert(self, pm->longitude().unit(), authName, uomAuthName, + uomCode, sqlStatements); + + const auto sql = formatStatement( + "INSERT INTO prime_meridian VALUES(" + "'%q','%q','%q',%f,'%q','%q',0);", + authName.c_str(), code.c_str(), pm->nameStr().c_str(), + pm->longitude().value(), uomAuthName.c_str(), uomCode.c_str()); + appendSql(sqlStatements, sql); + + return sqlStatements; +} + +// --------------------------------------------------------------------------- + +std::vector DatabaseContext::Private::getInsertStatementsFor( + const datum::EllipsoidNNPtr &ellipsoid, const std::string &authName, + const std::string &code, bool /*numericCode*/) { + + const auto self = NN_NO_CHECK(self_.lock()); + const auto allAuthFactory = AuthorityFactory::create(self, std::string()); + + // Check if the object is already known under that code + std::string ellipsoidAuthName; + std::string ellipsoidCode; + identifyFromNameOrCode(self, allAuthFactory, ellipsoid, ellipsoidAuthName, + ellipsoidCode); + if (ellipsoidAuthName == authName && ellipsoidCode == code) { + return {}; + } + + std::vector sqlStatements; + + // Find or insert celestial body + const auto &semiMajorAxis = ellipsoid->semiMajorAxis(); + const double semiMajorAxisMetre = semiMajorAxis.getSIValue(); + constexpr double tolerance = 0.005; + std::string bodyAuthName; + std::string bodyCode; + auto res = run("SELECT auth_name, code, " + "(ABS(semi_major_axis - ?) / semi_major_axis ) " + "AS rel_error FROM celestial_body WHERE rel_error <= ?", + {semiMajorAxisMetre, tolerance}); + if (!res.empty()) { + const auto &row = res.front(); + bodyAuthName = row[0]; + bodyCode = row[1]; + } else { + bodyAuthName = authName; + bodyCode = "BODY_" + code; + const auto bodyName = "Body of " + ellipsoid->nameStr(); + const auto sql = formatStatement( + "INSERT INTO celestial_body VALUES('%q','%q','%q',%f);", + bodyAuthName.c_str(), bodyCode.c_str(), bodyName.c_str(), + semiMajorAxisMetre); + appendSql(sqlStatements, sql); + } + + // Insert new record in ellipsoid table + std::string uomAuthName; + std::string uomCode; + identifyOrInsert(self, semiMajorAxis.unit(), authName, uomAuthName, uomCode, + sqlStatements); + std::string invFlattening("NULL"); + std::string semiMinorAxis("NULL"); + if (ellipsoid->isSphere() || ellipsoid->semiMinorAxis().has_value()) { + semiMinorAxis = toString(ellipsoid->computeSemiMinorAxis().value()); + } else { + invFlattening = toString(ellipsoid->computedInverseFlattening()); + } + + const auto sql = formatStatement( + "INSERT INTO ellipsoid VALUES(" + "'%q','%q','%q','%q','%q','%q',%f,'%q','%q',%s,%s,0);", + authName.c_str(), code.c_str(), ellipsoid->nameStr().c_str(), + "", // description + bodyAuthName.c_str(), bodyCode.c_str(), semiMajorAxis.value(), + uomAuthName.c_str(), uomCode.c_str(), invFlattening.c_str(), + semiMinorAxis.c_str()); + appendSql(sqlStatements, sql); + + return sqlStatements; +} + +// --------------------------------------------------------------------------- + +std::vector DatabaseContext::Private::getInsertStatementsFor( + const datum::GeodeticReferenceFrameNNPtr &datum, + const std::string &authName, const std::string &code, bool numericCode) { + + const auto self = NN_NO_CHECK(self_.lock()); + const auto allAuthFactory = AuthorityFactory::create(self, std::string()); + + // Check if the object is already known under that code + std::string datumAuthName; + std::string datumCode; + identifyFromNameOrCode(self, allAuthFactory, datum, datumAuthName, + datumCode); + if (datumAuthName == authName && datumCode == code) { + return {}; + } + + std::vector sqlStatements; + + // Find or insert ellipsoid + std::string ellipsoidAuthName; + std::string ellipsoidCode; + const auto &ellipsoidOfDatum = datum->ellipsoid(); + identifyFromNameOrCode(self, allAuthFactory, ellipsoidOfDatum, + ellipsoidAuthName, ellipsoidCode); + if (ellipsoidAuthName.empty()) { + ellipsoidAuthName = authName; + if (numericCode) { + ellipsoidCode = self->suggestsCodeFor(ellipsoidOfDatum, + ellipsoidAuthName, true); + } else { + ellipsoidCode = "ELLPS_" + code; + } + sqlStatements = self->getInsertStatementsFor( + ellipsoidOfDatum, ellipsoidAuthName, ellipsoidCode, numericCode); + } + + // Find or insert prime meridian + std::string pmAuthName; + std::string pmCode; + const auto &pmOfDatum = datum->primeMeridian(); + identifyFromNameOrCode(self, allAuthFactory, pmOfDatum, pmAuthName, pmCode); + if (pmAuthName.empty()) { + pmAuthName = authName; + if (numericCode) { + pmCode = self->suggestsCodeFor(pmOfDatum, pmAuthName, true); + } else { + pmCode = "PM_" + code; + } + const auto sqlStatementsTmp = self->getInsertStatementsFor( + pmOfDatum, pmAuthName, pmCode, numericCode); + sqlStatements.insert(sqlStatements.end(), sqlStatementsTmp.begin(), + sqlStatementsTmp.end()); + } + + // Insert new record in geodetic_datum table + const auto sql = formatStatement( + "INSERT INTO geodetic_datum VALUES(" + "'%q','%q','%q','%q','%q','%q','%q','%q',NULL,NULL,NULL,0);", + authName.c_str(), code.c_str(), datum->nameStr().c_str(), + "", // description + ellipsoidAuthName.c_str(), ellipsoidCode.c_str(), pmAuthName.c_str(), + pmCode.c_str()); + appendSql(sqlStatements, sql); + + identifyOrInsertUsages(datum, "geodetic_datum", authName, code, + sqlStatements); + + return sqlStatements; +} + +// --------------------------------------------------------------------------- + +std::vector DatabaseContext::Private::getInsertStatementsFor( + const crs::GeodeticCRSNNPtr &crs, const std::string &authName, + const std::string &code, bool numericCode) { + + const auto self = NN_NO_CHECK(self_.lock()); + const auto allAuthFactory = AuthorityFactory::create(self, std::string()); + + std::vector sqlStatements; + + // Find or insert datum/datum ensemble + std::string datumAuthName; + std::string datumCode; + const auto &ensemble = crs->datumEnsemble(); + if (ensemble) { + identifyFromNameOrCode(self, allAuthFactory, NN_NO_CHECK(ensemble), + datumAuthName, datumCode); + if (datumAuthName.empty()) { + throw FactoryException( + "Unhandled yet: insertion of new DatumEnsemble"); + } + } else { + const auto &datum = crs->datum(); + assert(datum); + const auto datumNN = NN_NO_CHECK(datum); + identifyFromNameOrCode(self, allAuthFactory, datumNN, datumAuthName, + datumCode); + if (datumAuthName.empty()) { + datumAuthName = authName; + if (numericCode) { + datumCode = self->suggestsCodeFor(datumNN, datumAuthName, true); + } else { + datumCode = "GEODETIC_DATUM_" + code; + } + sqlStatements = self->getInsertStatementsFor( + datumNN, datumAuthName, datumCode, numericCode); + } + } + + // Find or insert coordinate system + const auto &coordinateSystem = crs->coordinateSystem(); + std::string csAuthName; + std::string csCode; + identifyOrInsert(self, coordinateSystem, "GEODETIC_CRS", authName, code, + csAuthName, csCode, sqlStatements); + + const char *type = GEOG_2D; + if (coordinateSystem->axisList().size() == 3) { + if (dynamic_cast(crs.get())) { + type = GEOG_3D; + } else { + type = GEOCENTRIC; + } + } + + // Insert new record in geodetic_crs table + const auto sql = + formatStatement("INSERT INTO geodetic_crs VALUES(" + "'%q','%q','%q','%q','%q','%q','%q','%q','%q',NULL,0);", + authName.c_str(), code.c_str(), crs->nameStr().c_str(), + "", // description + type, csAuthName.c_str(), csCode.c_str(), + datumAuthName.c_str(), datumCode.c_str()); + appendSql(sqlStatements, sql); + + identifyOrInsertUsages(crs, "geodetic_crs", authName, code, sqlStatements); + return sqlStatements; +} + +// --------------------------------------------------------------------------- + +std::vector DatabaseContext::Private::getInsertStatementsFor( + const crs::ProjectedCRSNNPtr &crs, const std::string &authName, + const std::string &code, bool numericCode) { + + const auto self = NN_NO_CHECK(self_.lock()); + const auto allAuthFactory = AuthorityFactory::create(self, std::string()); + + std::vector sqlStatements; + + // Find or insert base geodetic CRS + const auto &baseCRS = crs->baseCRS(); + std::string geodAuthName; + std::string geodCode; + const auto candidates = baseCRS->identify(allAuthFactory); + for (const auto &candidate : candidates) { + if (candidate.second == 100) { + const auto &ids = candidate.first->identifiers(); + for (const auto &id : ids) { + geodAuthName = *(id->codeSpace()); + geodCode = id->code(); + break; + } + } + } + if (geodAuthName.empty()) { + geodAuthName = authName; + geodCode = "GEODETIC_CRS_" + code; + sqlStatements = self->getInsertStatementsFor(baseCRS, geodAuthName, + geodCode, numericCode); + } + + // Insert new record in conversion table + const auto &conversion = crs->derivingConversionRef(); + std::string convAuthName(authName); + std::string convCode("CONVERSION_" + code); + if (numericCode) { + convCode = self->suggestsCodeFor(conversion, convAuthName, true); + } + { + const auto &method = conversion->method(); + const auto &methodIds = method->identifiers(); + if (methodIds.empty()) { + throw FactoryException( + "Cannot insert projection with method without identifier"); + } + const auto &methodId = methodIds.front(); + const auto &methodAuthName = *(methodId->codeSpace()); + const auto &methodCode = methodId->code(); + auto sql = formatStatement("INSERT INTO conversion VALUES(" + "'%q','%q','%q','','%q','%q','%q'", + convAuthName.c_str(), convCode.c_str(), + conversion->nameStr().c_str(), + methodAuthName.c_str(), methodCode.c_str(), + method->nameStr().c_str()); + const auto &values = conversion->parameterValues(); + if (values.size() > N_MAX_PARAMS) { + throw FactoryException("Cannot insert projection with more than " + + toString(static_cast(N_MAX_PARAMS)) + + " parameters"); + } + for (const auto &genOpParamvalue : values) { + auto opParamValue = + dynamic_cast( + genOpParamvalue.get()); + if (!opParamValue) { + throw FactoryException("Cannot insert projection with " + "non-OperationParameterValue"); + } + const auto ¶m = opParamValue->parameter(); + const auto ¶mIds = param->identifiers(); + if (paramIds.empty()) { + throw FactoryException( + "Cannot insert projection with method parameter " + "without identifier"); + } + const auto ¶mId = paramIds.front(); + const auto ¶mAuthName = *(paramId->codeSpace()); + const auto ¶mCode = paramId->code(); + const auto &value = opParamValue->parameterValue()->value(); + const auto &unit = value.unit(); + std::string uomAuthName; + std::string uomCode; + identifyOrInsert(self, unit, authName, uomAuthName, uomCode, + sqlStatements); + sql += formatStatement(",'%q','%q','%q',%f,'%q','%q'", + paramAuthName.c_str(), paramCode.c_str(), + param->nameStr().c_str(), value.value(), + uomAuthName.c_str(), uomCode.c_str()); + } + for (size_t i = values.size(); i < N_MAX_PARAMS; ++i) { + sql += ",NULL,NULL,NULL,NULL,NULL,NULL"; + } + sql += ",0);"; + appendSql(sqlStatements, sql); + identifyOrInsertUsages(crs, "conversion", convAuthName, convCode, + sqlStatements); + } + + // Find or insert coordinate system + const auto &coordinateSystem = crs->coordinateSystem(); + std::string csAuthName; + std::string csCode; + identifyOrInsert(self, coordinateSystem, "PROJECTED_CRS", authName, code, + csAuthName, csCode, sqlStatements); + + // Insert new record in projected_crs table + const auto sql = formatStatement( + "INSERT INTO projected_crs VALUES(" + "'%q','%q','%q','%q','%q','%q','%q','%q','%q','%q',NULL,0);", + authName.c_str(), code.c_str(), crs->nameStr().c_str(), + "", // description + csAuthName.c_str(), csCode.c_str(), geodAuthName.c_str(), + geodCode.c_str(), convAuthName.c_str(), convCode.c_str()); + appendSql(sqlStatements, sql); + + identifyOrInsertUsages(crs, "projected_crs", authName, code, sqlStatements); + + return sqlStatements; +} + +// --------------------------------------------------------------------------- + +std::vector DatabaseContext::Private::getInsertStatementsFor( + const datum::VerticalReferenceFrameNNPtr &datum, + const std::string &authName, const std::string &code, + bool /* numericCode */) { + + const auto self = NN_NO_CHECK(self_.lock()); + const auto allAuthFactory = AuthorityFactory::create(self, std::string()); + + std::vector sqlStatements; + + // Check if the object is already known under that code + std::string datumAuthName; + std::string datumCode; + identifyFromNameOrCode(self, allAuthFactory, datum, datumAuthName, + datumCode); + if (datumAuthName == authName && datumCode == code) { + return {}; + } + + // Insert new record in vertical_datum table + const auto sql = formatStatement("INSERT INTO vertical_datum VALUES(" + "'%q','%q','%q','%q',NULL,NULL,NULL,0);", + authName.c_str(), code.c_str(), + datum->nameStr().c_str(), + "" // description + ); + appendSql(sqlStatements, sql); + + identifyOrInsertUsages(datum, "vertical_datum", authName, code, + sqlStatements); + + return sqlStatements; +} + +// --------------------------------------------------------------------------- + +std::vector DatabaseContext::Private::getInsertStatementsFor( + const crs::VerticalCRSNNPtr &crs, const std::string &authName, + const std::string &code, bool numericCode) { + + const auto self = NN_NO_CHECK(self_.lock()); + const auto allAuthFactory = AuthorityFactory::create(self, std::string()); + + std::vector sqlStatements; + + // Find or insert datum/datum ensemble + std::string datumAuthName; + std::string datumCode; + const auto &ensemble = crs->datumEnsemble(); + if (ensemble) { + identifyFromNameOrCode(self, allAuthFactory, NN_NO_CHECK(ensemble), + datumAuthName, datumCode); + if (datumAuthName.empty()) { + throw FactoryException( + "Unhandled yet: insertion of new DatumEnsemble"); + } + } else { + const auto &datum = crs->datum(); + assert(datum); + const auto datumNN = NN_NO_CHECK(datum); + identifyFromNameOrCode(self, allAuthFactory, datumNN, datumAuthName, + datumCode); + if (datumAuthName.empty()) { + datumAuthName = authName; + if (numericCode) { + datumCode = self->suggestsCodeFor(datumNN, datumAuthName, true); + } else { + datumCode = "VERTICAL_DATUM_" + code; + } + sqlStatements = self->getInsertStatementsFor( + datumNN, datumAuthName, datumCode, numericCode); + } + } + + // Find or insert coordinate system + const auto &coordinateSystem = crs->coordinateSystem(); + std::string csAuthName; + std::string csCode; + identifyOrInsert(self, coordinateSystem, "VERTICAL_CRS", authName, code, + csAuthName, csCode, sqlStatements); + + // Insert new record in vertical_crs table + const auto sql = + formatStatement("INSERT INTO vertical_crs VALUES(" + "'%q','%q','%q','%q','%q','%q','%q','%q',0);", + authName.c_str(), code.c_str(), crs->nameStr().c_str(), + "", // description + csAuthName.c_str(), csCode.c_str(), + datumAuthName.c_str(), datumCode.c_str()); + appendSql(sqlStatements, sql); + + identifyOrInsertUsages(crs, "vertical_crs", authName, code, sqlStatements); + + return sqlStatements; +} + +// --------------------------------------------------------------------------- + +std::vector DatabaseContext::Private::getInsertStatementsFor( + const crs::CompoundCRSNNPtr &crs, const std::string &authName, + const std::string &code, bool numericCode) { + + const auto self = NN_NO_CHECK(self_.lock()); + const auto allAuthFactory = AuthorityFactory::create(self, std::string()); + + std::vector sqlStatements; + + int counter = 1; + std::vector> componentsId; + const auto &components = crs->componentReferenceSystems(); + if (components.size() != 2) { + throw FactoryException( + "Cannot insert compound CRS with number of components != 2"); + } + for (const auto &component : components) { + std::string compAuthName; + std::string compCode; + const auto candidates = component->identify(allAuthFactory); + for (const auto &candidate : candidates) { + if (candidate.second == 100) { + const auto &ids = candidate.first->identifiers(); + for (const auto &id : ids) { + compAuthName = *(id->codeSpace()); + compCode = id->code(); + break; + } + } + } + if (compAuthName.empty()) { + compAuthName = authName; + if (numericCode) { + compCode = self->suggestsCodeFor(component, compAuthName, true); + } else { + compCode = "COMPONENT_" + code + '_' + toString(counter); + } + const auto sqlStatementsTmp = self->getInsertStatementsFor( + component, compAuthName, compCode, numericCode); + sqlStatements.insert(sqlStatements.end(), sqlStatementsTmp.begin(), + sqlStatementsTmp.end()); + } + + componentsId.emplace_back( + std::pair(compAuthName, compCode)); + + ++counter; + } + + // Insert new record in compound_crs table + const auto sql = formatStatement( + "INSERT INTO compound_crs VALUES(" + "'%q','%q','%q','%q','%q','%q','%q','%q',0);", + authName.c_str(), code.c_str(), crs->nameStr().c_str(), + "", // description + componentsId[0].first.c_str(), componentsId[0].second.c_str(), + componentsId[1].first.c_str(), componentsId[1].second.c_str()); + appendSql(sqlStatements, sql); + + identifyOrInsertUsages(crs, "compound_crs", authName, code, sqlStatements); + + return sqlStatements; +} + //! @endcond // --------------------------------------------------------------------------- //! @cond Doxygen_Suppress -DatabaseContext::~DatabaseContext() = default; +DatabaseContext::~DatabaseContext() { + try { + stopInsertStatementsSession(); + } catch (const std::exception &) { + } +} //! @endcond // --------------------------------------------------------------------------- @@ -945,11 +2172,14 @@ DatabaseContext::create(const std::string &databasePath, const std::vector &auxiliaryDatabasePaths, PJ_CONTEXT *ctx) { auto dbCtx = DatabaseContext::nn_make_shared(); - dbCtx->getPrivate()->open(databasePath, ctx); + auto dbCtxPrivate = dbCtx->getPrivate(); + dbCtxPrivate->open(databasePath, ctx); if (!auxiliaryDatabasePaths.empty()) { - dbCtx->getPrivate()->attachExtraDatabases(auxiliaryDatabasePaths); + dbCtxPrivate->attachExtraDatabases(auxiliaryDatabasePaths); + dbCtxPrivate->auxiliaryDatabasePaths_ = auxiliaryDatabasePaths; } - dbCtx->getPrivate()->checkDatabaseLayout(); + dbCtxPrivate->checkDatabaseLayout(); + dbCtxPrivate->self_ = dbCtx.as_nullable(); return dbCtx; } @@ -999,6 +2229,259 @@ const char *DatabaseContext::getMetadata(const char *key) const { // --------------------------------------------------------------------------- +/** \brief Starts a session for getInsertStatementsFor() + * + * Starts a new session for one or several calls to getInsertStatementsFor(). + * An insertion session guarantees that the inserted objects will not create + * conflicting intermediate objects. + * + * The session must be stopped with stopInsertStatementsSession(). + * + * Only one session may be active at a time for a given database context. + * + * @throw FactoryException + * @since 8.1 + */ +void DatabaseContext::startInsertStatementsSession() { + if (d->memoryDbHandle_) { + throw FactoryException( + "startInsertStatementsSession() cannot be invoked until " + "stopInsertStatementsSession() is."); + } + + // Create a in-memory temporary sqlite3 database + std::ostringstream buffer; + buffer << "file:temp_db_for_insert_statements_"; + buffer << this; + buffer << ".db?mode=memory&cache=shared"; + d->memoryDbForInsertPath_ = buffer.str(); + sqlite3_open_v2( + d->memoryDbForInsertPath_.c_str(), &d->memoryDbHandle_, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI, nullptr); + if (d->memoryDbHandle_ == nullptr) { + throw FactoryException("Cannot create in-memory database"); + } + + // Fill the structure of this database + const auto sqlStatements = getDatabaseStructure(); + for (const auto &sql : sqlStatements) { + if (sqlite3_exec(d->memoryDbHandle_, sql.c_str(), nullptr, nullptr, + nullptr) != SQLITE_OK) { + throw FactoryException("Cannot execute " + sql); + } + } + + // Attach this database to the current one(s) + auto auxiliaryDatabasePaths(d->auxiliaryDatabasePaths_); + auxiliaryDatabasePaths.push_back(d->memoryDbForInsertPath_); + d->attachExtraDatabases(auxiliaryDatabasePaths); +} + +// --------------------------------------------------------------------------- + +/** \brief Suggests a database code for the passed object. + * + * Supported type of objects are PrimeMeridian, Ellipsoid, Datum, DatumEnsemble, + * GeodeticCRS, ProjectedCRS, VerticalCRS, CompoundCRS, BoundCRS, Conversion. + * + * @param object Object for which to suggest a code. + * @param authName Authority name into which the object will be inserted. + * @param numericCode Whether the code should be numeric, or derived from the + * object name. + * @return the suggested code, that is guaranteed to not conflict with an + * existing one. + * + * @throw FactoryException + * @since 8.1 + */ +std::string +DatabaseContext::suggestsCodeFor(const common::IdentifiedObjectNNPtr &object, + const std::string &authName, + bool numericCode) { + const char *tableName = ""; + if (dynamic_cast(object.get())) { + tableName = "prime_meridian"; + } else if (dynamic_cast(object.get())) { + tableName = "ellipsoid"; + } else if (dynamic_cast( + object.get())) { + tableName = "geodetic_datum"; + } else if (dynamic_cast( + object.get())) { + tableName = "vertical_datum"; + } else if (const auto ensemble = + dynamic_cast(object.get())) { + const auto &datums = ensemble->datums(); + if (!datums.empty() && + dynamic_cast( + datums[0].get())) { + tableName = "geodetic_datum"; + } else { + tableName = "vertical_datum"; + } + } else if (const auto boundCRS = + dynamic_cast(object.get())) { + return suggestsCodeFor(boundCRS->baseCRS(), authName, numericCode); + } else if (dynamic_cast(object.get())) { + tableName = "crs_view"; + } else if (dynamic_cast(object.get())) { + tableName = "conversion"; + } else { + throw FactoryException("suggestsCodeFor(): unhandled type of object"); + } + + if (numericCode) { + std::string sql("SELECT MAX(code) FROM "); + sql += tableName; + sql += " WHERE auth_name = ? AND code >= '1' AND code <= '999999999' " + "AND upper(code) = lower(code)"; + const auto res = d->run(sql, {authName}); + if (res.empty()) { + return "1"; + } + return toString(atoi(res.front()[0].c_str()) + 1); + } + + std::string code; + code.reserve(object->nameStr().size()); + bool insertUnderscore = false; + for (const auto ch : toupper(object->nameStr())) { + if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z')) { + if (insertUnderscore && code.back() != '_') + code += '_'; + code += ch; + insertUnderscore = false; + } else { + insertUnderscore = true; + } + } + return d->findFreeCode(tableName, authName, code); +} + +// --------------------------------------------------------------------------- + +/** \brief Returns SQL statements needed to insert the passed object into the + * database. + * + * startInsertStatementsSession() must have been called previously. + * + * @param object The object to insert into the database. Currently only + * PrimeMeridian, Ellipsoid, Datum, GeodeticCRS, ProjectedCRS, + * VerticalCRS, CompoundCRS or BoundCRS are supported. + * @param authName Authority name into which the object will be inserted. + * @param code Code with which the object will be inserted. + * @param numericCode Whether intermediate objects that can be created should + * use numeric codes (true), or may be alphanumeric (false) + * @throw FactoryException + * @since 8.1 + */ +std::vector DatabaseContext::getInsertStatementsFor( + const common::IdentifiedObjectNNPtr &object, const std::string &authName, + const std::string &code, bool numericCode) { + if (d->memoryDbHandle_ == nullptr) { + throw FactoryException( + "startInsertStatementsSession() should be invoked first"); + } + + const auto self = NN_NO_CHECK(d->self_.lock()); + const auto allAuthFactory = AuthorityFactory::create(self, std::string()); + const auto crs = util::nn_dynamic_pointer_cast(object); + if (crs) { + // Check if the object is already known under that code + const auto candidates = crs->identify(allAuthFactory); + for (const auto &candidate : candidates) { + if (candidate.second == 100) { + const auto &ids = candidate.first->identifiers(); + for (const auto &id : ids) { + if (*(id->codeSpace()) == authName && id->code() == code) { + return {}; + } + } + } + } + } + + if (const auto pm = + util::nn_dynamic_pointer_cast(object)) { + return d->getInsertStatementsFor(NN_NO_CHECK(pm), authName, code, + numericCode); + } + + else if (const auto ellipsoid = + util::nn_dynamic_pointer_cast(object)) { + return d->getInsertStatementsFor(NN_NO_CHECK(ellipsoid), authName, code, + numericCode); + } + + else if (const auto geodeticDatum = + util::nn_dynamic_pointer_cast( + object)) { + return d->getInsertStatementsFor(NN_NO_CHECK(geodeticDatum), authName, + code, numericCode); + } + + else if (const auto geodCRS = + std::dynamic_pointer_cast(crs)) { + return d->getInsertStatementsFor(NN_NO_CHECK(geodCRS), authName, code, + numericCode); + } + + else if (const auto projCRS = + std::dynamic_pointer_cast(crs)) { + return d->getInsertStatementsFor(NN_NO_CHECK(projCRS), authName, code, + numericCode); + } + + else if (const auto verticalDatum = + util::nn_dynamic_pointer_cast( + object)) { + return d->getInsertStatementsFor(NN_NO_CHECK(verticalDatum), authName, + code, numericCode); + } + + else if (const auto vertCRS = + std::dynamic_pointer_cast(crs)) { + return d->getInsertStatementsFor(NN_NO_CHECK(vertCRS), authName, code, + numericCode); + } + + else if (const auto compoundCRS = + std::dynamic_pointer_cast(crs)) { + return d->getInsertStatementsFor(NN_NO_CHECK(compoundCRS), authName, + code, numericCode); + } + + else if (const auto boundCRS = + std::dynamic_pointer_cast(crs)) { + return getInsertStatementsFor(boundCRS->baseCRS(), authName, code, + numericCode); + } + + else { + throw FactoryException( + "getInsertStatementsFor(): unhandled type of object"); + } +} + +// --------------------------------------------------------------------------- + +/** \brief Stops an insertion session started with + * startInsertStatementsSession() + * + * @since 8.1 + */ +void DatabaseContext::stopInsertStatementsSession() { + if (d->memoryDbHandle_) { + d->clearCaches(); + d->attachExtraDatabases(d->auxiliaryDatabasePaths_); + sqlite3_close(d->memoryDbHandle_); + d->memoryDbHandle_ = nullptr; + d->memoryDbForInsertPath_.clear(); + } +} + +// --------------------------------------------------------------------------- + //! @cond Doxygen_Suppress DatabaseContextNNPtr DatabaseContext::create(void *sqlite_handle) { @@ -1518,7 +3001,12 @@ util::PropertyMap AuthorityFactory::Private::createPropertiesSearchUsages( "JOIN scope ON usage.scope_auth_name = scope.auth_name AND " "usage.scope_code = scope.code " "WHERE object_table_name = ? AND object_auth_name = ? AND " - "object_code = ? " + "object_code = ? AND " + // We voluntary exclude extent and scope with a specific code + "NOT (usage.extent_auth_name = 'PROJ' AND " + "usage.extent_code = 'EXTENT_UNKNOWN') AND " + "NOT (usage.scope_auth_name = 'PROJ' AND " + "usage.scope_code = 'SCOPE_UNKNOWN') " "ORDER BY score, usage.auth_name, usage.code"); auto res = run(sql, {table_name, authority(), code}); std::vector usages; @@ -2802,8 +4290,7 @@ AuthorityFactory::createConversion(const std::string &code) const { const size_t base_param_idx = idx; std::vector parameters; std::vector values; - constexpr int N_MAX_PARAMS = 7; - for (int i = 0; i < N_MAX_PARAMS; ++i) { + for (size_t i = 0; i < N_MAX_PARAMS; ++i) { const auto ¶m_auth_name = row[base_param_idx + i * 6 + 0]; if (param_auth_name.empty()) { break; @@ -3523,8 +5010,7 @@ operation::CoordinateOperationNNPtr AuthorityFactory::createCoordinateOperation( "target_crs_code, " "interpolation_crs_auth_name, interpolation_crs_code, " "operation_version, accuracy, deprecated"; - constexpr int N_MAX_PARAMS = 7; - for (int i = 1; i <= N_MAX_PARAMS; ++i) { + for (size_t i = 1; i <= N_MAX_PARAMS; ++i) { buffer << ", param" << i << "_auth_name"; buffer << ", param" << i << "_code"; buffer << ", param" << i << "_name"; @@ -3563,7 +5049,7 @@ operation::CoordinateOperationNNPtr AuthorityFactory::createCoordinateOperation( const size_t base_param_idx = idx; std::vector parameters; std::vector values; - for (int i = 0; i < N_MAX_PARAMS; ++i) { + for (size_t i = 0; i < N_MAX_PARAMS; ++i) { const auto ¶m_auth_name = row[base_param_idx + i * 6 + 0]; if (param_auth_name.empty()) { break; diff --git a/src/proj.h b/src/proj.h index 08bab92b..616a4065 100644 --- a/src/proj.h +++ b/src/proj.h @@ -1180,8 +1180,32 @@ PROJ_UNIT_INFO PROJ_DLL **proj_get_units_from_database( void PROJ_DLL proj_unit_list_destroy(PROJ_UNIT_INFO** list); /* ------------------------------------------------------------------------- */ +/*! @cond Doxygen_Suppress */ +typedef struct PJ_INSERT_SESSION PJ_INSERT_SESSION; +/*! @endcond */ + +PJ_INSERT_SESSION PROJ_DLL *proj_insert_object_session_create(PJ_CONTEXT *ctx); + +void PROJ_DLL proj_insert_object_session_destroy(PJ_CONTEXT *ctx, + PJ_INSERT_SESSION *session); +PROJ_STRING_LIST PROJ_DLL proj_get_insert_statements(PJ_CONTEXT *ctx, + PJ_INSERT_SESSION *session, + const PJ *object, + const char *authority, + const char *code, + int numeric_codes, + const char *const *options); +char PROJ_DLL *proj_suggests_code_for(PJ_CONTEXT *ctx, + const PJ *object, + const char *authority, + int numeric_code, + const char *const *options); + +void PROJ_DLL proj_string_destroy(char* str); + +/* ------------------------------------------------------------------------- */ /*! @cond Doxygen_Suppress */ typedef struct PJ_OPERATION_FACTORY_CONTEXT PJ_OPERATION_FACTORY_CONTEXT; /*! @endcond */ diff --git a/test/unit/test_c_api.cpp b/test/unit/test_c_api.cpp index 1b08e884..120f8627 100644 --- a/test/unit/test_c_api.cpp +++ b/test/unit/test_c_api.cpp @@ -5227,4 +5227,92 @@ TEST_F(CApi, proj_crs_is_derived) { } } +// --------------------------------------------------------------------------- + +TEST_F(CApi, proj_suggests_code_for) { + { + auto session = proj_insert_object_session_create(nullptr); + EXPECT_NE(session, nullptr); + + EXPECT_EQ(proj_insert_object_session_create(nullptr), nullptr); + + proj_insert_object_session_destroy(nullptr, session); + } + + { proj_insert_object_session_destroy(nullptr, nullptr); } + { + auto wkt = "GEOGCRS[\"myGDA2020\",\n" + " DATUM[\"GDA2020\",\n" + " ELLIPSOID[\"GRS_1980\",6378137,298.257222101,\n" + " LENGTHUNIT[\"metre\",1]]],\n" + " PRIMEM[\"Greenwich\",0,\n" + " ANGLEUNIT[\"Degree\",0.0174532925199433]],\n" + " CS[ellipsoidal,2],\n" + " AXIS[\"geodetic latitude (Lat)\",north,\n" + " ORDER[1],\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]],\n" + " AXIS[\"geodetic longitude (Lon)\",east,\n" + " ORDER[2],\n" + " ANGLEUNIT[\"degree\",0.0174532925199433]]]"; + auto crs = proj_create_from_wkt(m_ctxt, wkt, nullptr, nullptr, nullptr); + ObjectKeeper keeper_from_wkt(crs); + EXPECT_NE(crs, nullptr); + + { + char *code = + proj_suggests_code_for(m_ctxt, crs, "HOBU", false, nullptr); + ASSERT_NE(code, nullptr); + EXPECT_EQ(std::string(code), "MYGDA2020"); + proj_string_destroy(code); + } + + { + char *code = + proj_suggests_code_for(m_ctxt, crs, "HOBU", true, nullptr); + ASSERT_NE(code, nullptr); + EXPECT_EQ(std::string(code), "1"); + proj_string_destroy(code); + } + + // No session specified: we use a temporary session + for (int i = 0; i < 2; i++) { + auto list = proj_get_insert_statements(m_ctxt, nullptr, crs, "HOBU", + "XXXX", false, nullptr); + ASSERT_NE(list, nullptr); + ASSERT_NE(list[0], nullptr); + EXPECT_EQ(std::string(list[0]), + "INSERT INTO geodetic_datum VALUES('HOBU'," + "'GEODETIC_DATUM_XXXX','GDA2020','','EPSG','7019'," + "'EPSG','8901',NULL,NULL,NULL,0);"); + proj_string_list_destroy(list); + } + + auto session = proj_insert_object_session_create(m_ctxt); + EXPECT_NE(session, nullptr); + + { + auto list = proj_get_insert_statements(m_ctxt, session, crs, "HOBU", + "XXXX", false, nullptr); + ASSERT_NE(list, nullptr); + ASSERT_NE(list[0], nullptr); + EXPECT_EQ(std::string(list[0]), + "INSERT INTO geodetic_datum VALUES('HOBU'," + "'GEODETIC_DATUM_XXXX','GDA2020','','EPSG','7019'," + "'EPSG','8901',NULL,NULL,NULL,0);"); + proj_string_list_destroy(list); + } + + // Object already inserted: return empty list + { + auto list = proj_get_insert_statements(m_ctxt, session, crs, "HOBU", + "XXXX", false, nullptr); + ASSERT_NE(list, nullptr); + ASSERT_EQ(list[0], nullptr); + proj_string_list_destroy(list); + } + + proj_insert_object_session_destroy(m_ctxt, session); + } +} + } // namespace diff --git a/test/unit/test_factory.cpp b/test/unit/test_factory.cpp index 454e4f2e..55378134 100644 --- a/test/unit/test_factory.cpp +++ b/test/unit/test_factory.cpp @@ -3434,4 +3434,577 @@ TEST(factory, getUnitList) { } } +// --------------------------------------------------------------------------- + +TEST(factory, objectInsertion) { + + // Cannot nest startInsertStatementsSession + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + EXPECT_THROW(ctxt->startInsertStatementsSession(), FactoryException); + } + + { + auto ctxt = DatabaseContext::create(); + // Tolerated withtout explicit stop + ctxt->startInsertStatementsSession(); + } + + { + auto ctxt = DatabaseContext::create(); + // Tolerated + ctxt->stopInsertStatementsSession(); + } + + // getInsertStatementsFor() must be preceded with + // startInsertStatementsSession() + { + auto ctxt = DatabaseContext::create(); + EXPECT_THROW(ctxt->getInsertStatementsFor(GeographicCRS::EPSG_4326, + "EPSG", "4326", true), + FactoryException); + } + + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + // Nothing to do + EXPECT_TRUE(ctxt->getInsertStatementsFor(GeographicCRS::EPSG_4326, + "EPSG", "4326", true) + .empty()); + ctxt->stopInsertStatementsSession(); + } + + // Geographic 2D CRS + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto crs = GeographicCRS::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my EPSG:4326"), + GeographicCRS::EPSG_4326->datum(), + GeographicCRS::EPSG_4326->datumEnsemble(), + GeographicCRS::EPSG_4326->coordinateSystem()); + + EXPECT_EQ(ctxt->suggestsCodeFor(crs, "HOBU", true), "1"); + EXPECT_EQ(ctxt->suggestsCodeFor(crs, "HOBU", false), "MY_EPSG_4326"); + + const auto sql = + ctxt->getInsertStatementsFor(crs, "HOBU", "1234", true); + + EXPECT_EQ(ctxt->suggestsCodeFor(crs, "HOBU", true), "1235"); + + ASSERT_EQ(sql.size(), 2U); + EXPECT_EQ(sql[0], "INSERT INTO geodetic_crs VALUES('HOBU','1234','my " + "EPSG:4326','','geographic " + "2D','EPSG','6422','EPSG','6326',NULL,0);"); + EXPECT_EQ( + sql[1], + "INSERT INTO usage " + "VALUES('HOBU','USAGE_GEODETIC_CRS_1234','geodetic_crs','HOBU','" + "1234','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN');"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE( + ctxt->getInsertStatementsFor(crs, "HOBU", "1234", true).empty()); + ctxt->stopInsertStatementsSession(); + AuthorityFactory::create(ctxt, std::string("EPSG")) + ->createGeographicCRS("4326"); + EXPECT_THROW(AuthorityFactory::create(ctxt, std::string("HOBU")) + ->createGeographicCRS("1234"), + NoSuchAuthorityCodeException); + } + + // Geographic 3D CRS, with known usage + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto usages = AuthorityFactory::create(ctxt, std::string("EPSG")) + ->createGeographicCRS("4979") + ->domains(); + auto array(ArrayOfBaseObject::create()); + for (const auto &usage : usages) { + array->add(usage); + } + auto props = + PropertyMap().set(IdentifiedObject::NAME_KEY, "my EPSG:4979"); + props.set(ObjectUsage::OBJECT_DOMAIN_KEY, + nn_static_pointer_cast(array)); + const auto crs = + GeographicCRS::create(props, GeographicCRS::EPSG_4979->datum(), + GeographicCRS::EPSG_4979->datumEnsemble(), + GeographicCRS::EPSG_4979->coordinateSystem()); + const auto sql = + ctxt->getInsertStatementsFor(crs, "HOBU", "4979", false); + ASSERT_EQ(sql.size(), 2U); + EXPECT_EQ(sql[0], "INSERT INTO geodetic_crs VALUES('HOBU','4979','my " + "EPSG:4979','','geographic " + "3D','EPSG','6423','EPSG','6326',NULL,0);"); + EXPECT_EQ( + sql[1], + "INSERT INTO usage " + "VALUES('HOBU','USAGE_GEODETIC_CRS_4979','geodetic_crs','HOBU','" + "4979','EPSG','1262','EPSG','1176');"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE( + ctxt->getInsertStatementsFor(crs, "HOBU", "4979", false).empty()); + ctxt->stopInsertStatementsSession(); + } + + // BoundCRS of Geocentric CRS, with new usage + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + auto props = + PropertyMap().set(IdentifiedObject::NAME_KEY, "my EPSG:4978"); + auto array(ArrayOfBaseObject::create()); + const auto extent = Extent::createFromBBOX(1, 2, 3, 4); + optional scope; + scope = "my scope"; + array->add(ObjectDomain::create(scope, extent)); + props.set(ObjectUsage::OBJECT_DOMAIN_KEY, + nn_static_pointer_cast(array)); + const auto crs = GeodeticCRS::create( + props, NN_NO_CHECK(GeodeticCRS::EPSG_4978->datum()), + NN_NO_CHECK(nn_dynamic_pointer_cast( + GeodeticCRS::EPSG_4978->coordinateSystem()))); + const auto boundCRS = BoundCRS::createFromTOWGS84( + crs, std::vector{1, 2, 3, 4, 5, 6, 7}); + const auto sql = + ctxt->getInsertStatementsFor(boundCRS, "HOBU", "4978", false); + ASSERT_EQ(sql.size(), 4U); + EXPECT_EQ( + sql[0], + "INSERT INTO geodetic_crs VALUES('HOBU','4978','my " + "EPSG:4978','','geocentric','EPSG','6500','EPSG','6326',NULL,0);"); + EXPECT_EQ(sql[1], + "INSERT INTO scope VALUES('HOBU','SCOPE_geodetic_crs_4978'," + "'my scope',0);"); + EXPECT_EQ(sql[2], + "INSERT INTO extent VALUES('HOBU','EXTENT_geodetic_crs_4978'," + "'unknown','unknown',2,4,1,3,0);"); + EXPECT_EQ( + sql[3], + "INSERT INTO usage VALUES('HOBU','USAGE_GEODETIC_CRS_4978'," + "'geodetic_crs','HOBU','4978','HOBU'," + "'EXTENT_geodetic_crs_4978','HOBU','SCOPE_geodetic_crs_4978');"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE( + ctxt->getInsertStatementsFor(boundCRS, "HOBU", "4978", false) + .empty()); + ctxt->stopInsertStatementsSession(); + } + + // Geographic 2D CRS with unknown datum, numeric code + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto datum = GeodeticReferenceFrame::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my datum"), + Ellipsoid::WGS84, optional(), + PrimeMeridian::GREENWICH); + const auto crs = GeographicCRS::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my EPSG:4326"), + datum, GeographicCRS::EPSG_4326->coordinateSystem()); + const auto sql = + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", true); + ASSERT_EQ(sql.size(), 4U); + EXPECT_EQ(sql[0], + "INSERT INTO geodetic_datum VALUES('HOBU','1','my " + "datum','','EPSG','7030','EPSG','8901',NULL,NULL,NULL,0);"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE( + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", true).empty()); + ctxt->stopInsertStatementsSession(); + } + + // Geographic 2D CRS with unknown datum, alpha code + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto datum = GeodeticReferenceFrame::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my datum"), + Ellipsoid::WGS84, optional(), + PrimeMeridian::GREENWICH); + const auto crs = GeographicCRS::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my EPSG:4326"), + datum, GeographicCRS::EPSG_4326->coordinateSystem()); + const auto sql = + ctxt->getInsertStatementsFor(crs, "HOBU", "MY_EPSG_4326", false); + + EXPECT_EQ(ctxt->suggestsCodeFor(crs, "HOBU", false), "MY_EPSG_4326_2"); + + ASSERT_EQ(sql.size(), 4U); + EXPECT_EQ(sql[0], + "INSERT INTO geodetic_datum " + "VALUES('HOBU','GEODETIC_DATUM_MY_EPSG_4326','my " + "datum','','EPSG','7030','EPSG','8901',NULL,NULL,NULL,0);"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE( + ctxt->getInsertStatementsFor(crs, "HOBU", "MY_EPSG_4326", false) + .empty()); + ctxt->stopInsertStatementsSession(); + } + + // Geographic 2D CRS with unknown ellipsoid, numeric code + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto ellipsoid = Ellipsoid::createFlattenedSphere( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my ellipsoid"), + Length(6378137), Scale(295)); + const auto datum = GeodeticReferenceFrame::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my datum"), + ellipsoid, optional(), PrimeMeridian::GREENWICH); + const auto crs = GeographicCRS::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my EPSG:4326"), + datum, GeographicCRS::EPSG_4326->coordinateSystem()); + const auto sql = + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", true); + ASSERT_EQ(sql.size(), 5U); + EXPECT_EQ( + sql[0], + "INSERT INTO ellipsoid VALUES('HOBU','1','my " + "ellipsoid','','PROJ','EARTH',6378137,'EPSG','9001',295,NULL,0);"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE( + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", true).empty()); + ctxt->stopInsertStatementsSession(); + } + + // Geographic 2D CRS with unknown ellipsoid, alpha code + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto ellipsoid = Ellipsoid::createTwoAxis( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my ellipsoid"), + Length(6378137), Length(6378136)); + const auto datum = GeodeticReferenceFrame::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my datum"), + ellipsoid, optional(), PrimeMeridian::GREENWICH); + const auto crs = GeographicCRS::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my EPSG:4326"), + datum, GeographicCRS::EPSG_4326->coordinateSystem()); + const auto sql = + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", false); + ASSERT_EQ(sql.size(), 5U); + EXPECT_EQ(sql[0], "INSERT INTO ellipsoid " + "VALUES('HOBU','ELLPS_GEODETIC_DATUM_XXXX','my " + "ellipsoid','','PROJ','EARTH',6378137,'EPSG','9001'," + "NULL,6378136,0);"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE( + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", false).empty()); + ctxt->stopInsertStatementsSession(); + } + + // Geographic 2D CRS with unknown prime meridian, numeric code + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto pm = PrimeMeridian::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "My meridian"), + Angle(10)); + const auto datum = GeodeticReferenceFrame::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my datum"), + Ellipsoid::WGS84, optional(), pm); + const auto crs = GeographicCRS::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my EPSG:4326"), + datum, GeographicCRS::EPSG_4326->coordinateSystem()); + const auto sql = + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", true); + ASSERT_EQ(sql.size(), 5U); + EXPECT_EQ(sql[0], "INSERT INTO prime_meridian VALUES('HOBU','1','My " + "meridian',10,'EPSG','9122',0);"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE( + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", true).empty()); + ctxt->stopInsertStatementsSession(); + } + + // Geographic 2D CRS with unknown prime meridian, alpha code + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto pm = PrimeMeridian::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "My meridian"), + Angle(10)); + const auto datum = GeodeticReferenceFrame::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my datum"), + Ellipsoid::WGS84, optional(), pm); + const auto crs = GeographicCRS::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my EPSG:4326"), + datum, GeographicCRS::EPSG_4326->coordinateSystem()); + const auto sql = + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", false); + ASSERT_EQ(sql.size(), 5U); + EXPECT_EQ(sql[0], "INSERT INTO prime_meridian " + "VALUES('HOBU','PM_GEODETIC_DATUM_XXXX','My " + "meridian',10,'EPSG','9122',0);"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE( + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", false).empty()); + ctxt->stopInsertStatementsSession(); + } + + // Projected CRS, numeric code + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto crs = ProjectedCRS::create( + PropertyMap().set(IdentifiedObject::NAME_KEY, "my projected CRS"), + GeographicCRS::EPSG_4807, + Conversion::createUTM(PropertyMap(), 31, true), + CartesianCS::createEastingNorthing(UnitOfMeasure::METRE)); + const auto sql = + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", true); + ASSERT_EQ(sql.size(), 4U); + EXPECT_EQ(sql[0], + "INSERT INTO conversion VALUES('HOBU','1'," + "'UTM zone 31N',''," + "'EPSG','9807','Transverse Mercator'," + "'EPSG','8801','Latitude of natural origin',0,'EPSG','9122'," + "'EPSG','8802','Longitude of natural origin',3,'EPSG','9122'," + "'EPSG','8805','Scale factor at natural origin',0.9996," + "'EPSG','9201'," + "'EPSG','8806','False easting',500000,'EPSG','9001'," + "'EPSG','8807','False northing',0,'EPSG','9001'," + "NULL,NULL,NULL,NULL,NULL,NULL," + "NULL,NULL,NULL,NULL,NULL,NULL,0);"); + EXPECT_EQ(sql[1], + "INSERT INTO usage " + "VALUES('HOBU','USAGE_CONVERSION_1','conversion','HOBU','1','" + "PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN');"); + EXPECT_EQ( + sql[2], + "INSERT INTO projected_crs VALUES('HOBU','XXXX','my projected " + "CRS','','EPSG','4400','EPSG','4807','HOBU','1',NULL,0);"); + EXPECT_EQ( + sql[3], + "INSERT INTO usage " + "VALUES('HOBU','USAGE_PROJECTED_CRS_XXXX','projected_crs','HOBU','" + "XXXX','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN');"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE( + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", true).empty()); + ctxt->stopInsertStatementsSession(); + } + + // Vertical CRS, known vertical datum, numeric code + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + PropertyMap propertiesVDatum; + propertiesVDatum.set(Identifier::CODESPACE_KEY, "EPSG") + .set(Identifier::CODE_KEY, 5101) + .set(IdentifiedObject::NAME_KEY, "Ordnance Datum Newlyn"); + auto vdatum = VerticalReferenceFrame::create(propertiesVDatum); + PropertyMap propertiesCRS; + propertiesCRS.set(IdentifiedObject::NAME_KEY, "my height"); + const auto uom = + UnitOfMeasure("my unit", 3.0, UnitOfMeasure::Type::LINEAR); + const auto crs = VerticalCRS::create( + propertiesCRS, vdatum, VerticalCS::createGravityRelatedHeight(uom)); + const auto sql = + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", true); + ASSERT_EQ(sql.size(), 5U); + EXPECT_EQ(sql[0], "INSERT INTO coordinate_system VALUES" + "('HOBU','CS_VERTICAL_CRS_XXXX','vertical',1);"); + EXPECT_EQ(sql[1], "INSERT INTO unit_of_measure VALUES" + "('HOBU','MY_UNIT','my unit','length',3,NULL,0);"); + EXPECT_EQ(sql[2], + "INSERT INTO axis VALUES('HOBU'," + "'CS_VERTICAL_CRS_XXXX_AXIS_1','Gravity-related height','H'," + "'up','HOBU','CS_VERTICAL_CRS_XXXX',1,'HOBU','MY_UNIT');"); + EXPECT_EQ(sql[3], + "INSERT INTO vertical_crs VALUES('HOBU','XXXX','my height'," + "'','HOBU','CS_VERTICAL_CRS_XXXX','EPSG','5101',0);"); + EXPECT_EQ(sql[4], + "INSERT INTO usage VALUES('HOBU','USAGE_VERTICAL_CRS_XXXX'," + "'vertical_crs','HOBU','XXXX','PROJ','EXTENT_UNKNOWN'," + "'PROJ','SCOPE_UNKNOWN');"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE( + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", true).empty()); + ctxt->stopInsertStatementsSession(); + } + + // Vertical CRS, unknown vertical datum, alpha code + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + PropertyMap propertiesVDatum; + propertiesVDatum.set(IdentifiedObject::NAME_KEY, "my datum"); + auto vdatum = VerticalReferenceFrame::create(propertiesVDatum); + PropertyMap propertiesCRS; + propertiesCRS.set(IdentifiedObject::NAME_KEY, "my height"); + const auto crs = VerticalCRS::create( + propertiesCRS, vdatum, + VerticalCS::createGravityRelatedHeight(UnitOfMeasure::METRE)); + const auto sql = + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", false); + ASSERT_EQ(sql.size(), 4U); + EXPECT_EQ(sql[0], + "INSERT INTO vertical_datum VALUES('HOBU'," + "'VERTICAL_DATUM_XXXX','my datum','',NULL,NULL,NULL,0);"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), IComparable::Criterion::EQUIVALENT)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE( + ctxt->getInsertStatementsFor(crs, "HOBU", "XXXX", false).empty()); + ctxt->stopInsertStatementsSession(); + } + + // Compound CRS + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto wkt = + "COMPD_CS[\"unknown\"," + "PROJCS[\"NAD_1983_2011_StatePlane_South_Carolina_FIPS_3900_USFT\"," + "GEOGCS[\"NAD83(2011)\"," + "DATUM[\"NAD83_National_Spatial_Reference_System_2011\"," + "SPHEROID[\"GRS 1980\",6378137,298.257222101004," + "AUTHORITY[\"EPSG\",\"7019\"]],AUTHORITY[\"EPSG\",\"1116\"]]," + "PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433," + "AUTHORITY[\"EPSG\",\"9122\"]]]," + "PROJECTION[\"Lambert_Conformal_Conic_2SP\"]," + "PARAMETER[\"latitude_of_origin\",31.8333333333333]," + "PARAMETER[\"central_meridian\",-81]," + "PARAMETER[\"standard_parallel_1\",32.5]," + "PARAMETER[\"standard_parallel_2\",34.8333333333333]," + "PARAMETER[\"false_easting\",1999996]," + "PARAMETER[\"false_northing\",0]," + "UNIT[\"US survey foot\",0.304800609601219," + "AUTHORITY[\"EPSG\",\"9003\"]]," + "AXIS[\"Easting\",EAST],AXIS[\"Northing\",NORTH]]," + "VERT_CS[\"NAVD88 height (ftUS)\"," + "VERT_DATUM[\"North American Vertical Datum 1988\",2005," + "AUTHORITY[\"EPSG\",\"5103\"]]," + "UNIT[\"US survey foot\",0.304800609601219," + "AUTHORITY[\"EPSG\",\"9003\"]]," + "AXIS[\"Up\",UP],AUTHORITY[\"EPSG\",\"6360\"]]]"; + const auto crs = + nn_dynamic_pointer_cast(WKTParser().createFromWKT(wkt)); + ASSERT_TRUE(crs != nullptr); + const auto sql = ctxt->getInsertStatementsFor(NN_NO_CHECK(crs), "HOBU", + "XXXX", false); + ASSERT_EQ(sql.size(), 6U); + EXPECT_EQ(sql[4], + "INSERT INTO compound_crs VALUES('HOBU','XXXX','unknown'," + "'','HOBU','COMPONENT_XXXX_1','EPSG','6360',0);"); + EXPECT_EQ(sql[5], + "INSERT INTO usage VALUES('HOBU','USAGE_COMPOUND_CRS_XXXX'," + "'compound_crs','HOBU','XXXX','PROJ','EXTENT_UNKNOWN'," + "'PROJ','SCOPE_UNKNOWN');"); + const auto identified = + crs->identify(AuthorityFactory::create(ctxt, std::string())); + ASSERT_EQ(identified.size(), 1U); + EXPECT_EQ( + *(identified.front().first->identifiers().front()->codeSpace()), + "HOBU"); + EXPECT_TRUE(identified.front().first->isEquivalentTo( + crs.get(), + IComparable::Criterion::EQUIVALENT_EXCEPT_AXIS_ORDER_GEOGCRS)); + EXPECT_EQ(identified.front().second, 100); + EXPECT_TRUE(ctxt->getInsertStatementsFor(NN_NO_CHECK(crs), "HOBU", + "XXXX", false) + .empty()); + ctxt->stopInsertStatementsSession(); + } +} + } // namespace -- cgit v1.2.3 From a234207957043503814a6a3eebc497d2e471dda3 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 12 Mar 2021 23:18:35 +0100 Subject: projinfo: add a '-o SQL --output-id AUTH:CODE' SQL output --- docs/source/apps/projinfo.rst | 29 ++++++++++++++++++- src/apps/projinfo.cpp | 65 +++++++++++++++++++++++++++++++++++++++--- test/cli/testprojinfo | 4 +++ test/cli/testprojinfo_out.dist | 8 ++++++ 4 files changed, 101 insertions(+), 5 deletions(-) diff --git a/docs/source/apps/projinfo.rst b/docs/source/apps/projinfo.rst index 826cd244..c7a56689 100644 --- a/docs/source/apps/projinfo.rst +++ b/docs/source/apps/projinfo.rst @@ -27,6 +27,7 @@ Synopsis | [--boundcrs-to-wgs84] | [--main-db-path path] [--aux-db-path path]* | [--identify] [--3d] + | [--output-id AUTH:CODE] | [--c-ify] [--single-line] | --searchpaths | --remote-data | {object_definition} | | {object_reference} | (-s {srs_def} -t {srs_def}) @@ -78,7 +79,7 @@ The following control parameters can appear in any order: .. option:: -o formats formats is a comma separated combination of: - ``all``, ``default``, ``PROJ``, ``WKT_ALL``, ``WKT2:2015``, ``WKT2:2019``, ``WKT1:GDAL``, ``WKT1:ESRI``, ``PROJJSON``. + ``all``, ``default``, ``PROJ``, ``WKT_ALL``, ``WKT2:2015``, ``WKT2:2019``, ``WKT1:GDAL``, ``WKT1:ESRI``, ``PROJJSON``, ``SQL``. Except ``all`` and ``default``, other formats can be preceded by ``-`` to disable them. @@ -87,6 +88,8 @@ The following control parameters can appear in any order: .. note:: Before PROJ 6.3.0, WKT1:GDAL was implicitly calling --boundcrs-to-wgs84. This is no longer the case. + .. note:: When SQL is specified, :option:`--output-id` must be specified. + .. option:: -k crs|operation|datum|ensemble|ellipsoid When used to query a single object with a AUTHORITY:CODE, determines the (k)ind of the object @@ -271,6 +274,12 @@ The following control parameters can appear in any order: automatically promoted to a 3D version, where its vertical axis is the ellipsoidal height in metres, using the ellipsoid of the base geodetic CRS. +.. option:: --output-id=AUTH:NAME + + .. versionadded:: 8.1 + + Identifier to assign to the object (for SQL output). + .. option:: --c-ify For developers only. Modify the string output of the utility so that it @@ -442,6 +451,24 @@ Output: } } +4. Exporting the SQL statements to insert a new CRS in the database. + +.. code-block:: console + + projinfo "+proj=merc +lat_ts=5 +datum=WGS84 +type=crs" --output-id HOBU:MY_CRS -o SQL -q + +Output: + +.. code-block:: sql + + INSERT INTO geodetic_crs VALUES('HOBU','GEODETIC_CRS_MY_CRS','unknown','','geographic 2D','EPSG','6424','EPSG','6326',NULL,0); + INSERT INTO usage VALUES('HOBU','USAGE_GEODETIC_CRS_MY_CRS','geodetic_crs','HOBU','GEODETIC_CRS_MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); + INSERT INTO conversion VALUES('HOBU','CONVERSION_MY_CRS','unknown','','EPSG','9805','Mercator (variant B)','EPSG','8823','Latitude of 1st standard parallel',5,'EPSG','9122','EPSG','8802','Longitude of natural origin',0,'EPSG','9122','EPSG','8806','False easting',0,'EPSG','9001','EPSG','8807','False northing',0,'EPSG','9001',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0); + INSERT INTO usage VALUES('HOBU','USAGE_CONVERSION_MY_CRS','conversion','HOBU','CONVERSION_MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); + INSERT INTO projected_crs VALUES('HOBU','MY_CRS','unknown','','EPSG','4400','HOBU','GEODETIC_CRS_MY_CRS','HOBU','CONVERSION_MY_CRS',NULL,0); + INSERT INTO usage VALUES('HOBU','USAGE_PROJECTED_CRS_MY_CRS','projected_crs','HOBU','MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); + + .. only:: man diff --git a/src/apps/projinfo.cpp b/src/apps/projinfo.cpp index 16f2a906..39d9666d 100644 --- a/src/apps/projinfo.cpp +++ b/src/apps/projinfo.cpp @@ -68,11 +68,14 @@ struct OutputOptions { bool WKT1_GDAL = false; bool WKT1_ESRI = false; bool PROJJSON = false; + bool SQL = false; bool c_ify = false; bool singleLine = false; bool strict = true; bool ballparkAllowed = true; bool allowEllipsoidalHeightAsVerticalCRS = false; + std::string outputAuthName{}; + std::string outputCode{}; }; } // anonymous namespace @@ -104,6 +107,7 @@ static void usage() { << " [--main-db-path path] [--aux-db-path path]*" << std::endl << " [--identify] [--3d]" << std::endl + << " [--output-id AUTH:CODE]" << std::endl << " [--c-ify] [--single-line]" << std::endl << " --searchpaths | --remote-data |" << std::endl << " {object_definition} | (-s {srs_def} -t {srs_def})" @@ -111,7 +115,7 @@ static void usage() { std::cerr << std::endl; std::cerr << "-o: formats is a comma separated combination of: " "all,default,PROJ,WKT_ALL,WKT2:2015,WKT2:2019,WKT1:GDAL," - "WKT1:ESRI,PROJJSON" + "WKT1:ESRI,PROJJSON,SQL" << std::endl; std::cerr << " Except 'all' and 'default', other format can be preceded " "by '-' to disable them" @@ -310,7 +314,7 @@ static void outputObject( CoordinateOperationContext::IntermediateCRSUse allowUseIntermediateCRS, const OutputOptions &outputOpt) { - auto identified = dynamic_cast(obj.get()); + auto identified = nn_dynamic_pointer_cast(obj); if (!outputOpt.quiet && identified && identified->isDeprecated()) { std::cout << "Warning: object is deprecated" << std::endl; auto crs = dynamic_cast(obj.get()); @@ -556,8 +560,31 @@ static void outputObject( std::cerr << "Error when exporting to PROJJSON: " << e.what() << std::endl; } - // alreadyOutputted = true; + alreadyOutputted = true; + } + } + + if (identified && dbContext && outputOpt.SQL) { + try { + if (alreadyOutputted) { + std::cout << std::endl; + } + if (!outputOpt.quiet) { + std::cout << "SQL:" << std::endl; + } + dbContext->startInsertStatementsSession(); + const auto statements = dbContext->getInsertStatementsFor( + NN_NO_CHECK(identified), outputOpt.outputAuthName, + outputOpt.outputCode, false); + dbContext->stopInsertStatementsSession(); + for (const auto &sql : statements) { + std::cout << sql << std::endl; + } + } catch (const std::exception &e) { + std::cerr << "Error when exporting to SQL: " << e.what() + << std::endl; } + // alreadyOutputted = true; } auto op = dynamic_cast(obj.get()); @@ -825,6 +852,7 @@ int main(int argc, char **argv) { bool showSuperseded = false; bool promoteTo3D = false; double minimumAccuracy = -1; + bool outputAll = false; for (int i = 1; i < argc; i++) { std::string arg(argv[i]); @@ -834,12 +862,14 @@ int main(int argc, char **argv) { auto formats(split(argv[i], ',')); for (auto format : formats) { if (ci_equal(format, "all")) { + outputAll = true; outputOpt.PROJ5 = true; outputOpt.WKT2_2019 = true; outputOpt.WKT2_2015 = true; outputOpt.WKT1_GDAL = true; outputOpt.WKT1_ESRI = true; outputOpt.PROJJSON = true; + outputOpt.SQL = true; } else if (ci_equal(format, "default")) { outputOpt.PROJ5 = true; outputOpt.WKT2_2019 = true; @@ -915,6 +945,10 @@ int main(int argc, char **argv) { outputOpt.PROJJSON = true; } else if (ci_equal(format, "-PROJJSON")) { outputOpt.PROJJSON = false; + } else if (ci_equal(format, "SQL")) { + outputOpt.SQL = true; + } else if (ci_equal(format, "-SQL")) { + outputOpt.SQL = false; } else { std::cerr << "Unrecognized value for option -o: " << format << std::endl; @@ -1096,6 +1130,16 @@ int main(int argc, char **argv) { outputOpt.ballparkAllowed = false; } else if (ci_equal(arg, "--3d")) { promoteTo3D = true; + } else if (arg == "--output-id" && i + 1 < argc) { + i++; + const auto tokens = split(argv[i], ':'); + if (tokens.size() != 2) { + std::cerr << "Invalid value for option --output-id" + << std::endl; + usage(); + } + outputOpt.outputAuthName = tokens[0]; + outputOpt.outputCode = tokens[1]; } else if (ci_equal(arg, "--searchpaths")) { #ifdef _WIN32 constexpr char delim = ';'; @@ -1144,6 +1188,19 @@ int main(int argc, char **argv) { std::cerr << "ERROR: --bbox and --area are exclusive" << std::endl; std::exit(1); } + if (outputOpt.SQL && outputOpt.outputAuthName.empty()) { + if (outputAll) { + outputOpt.SQL = false; + std::cerr << "WARNING: SQL output disable since " + "--output-id=AUTH:CODE has not been specified." + << std::endl; + } else { + std::cerr << "ERROR: --output-id=AUTH:CODE must be specified when " + "SQL output is enabled." + << std::endl; + std::exit(1); + } + } DatabaseContextPtr dbContext; try { @@ -1186,7 +1243,7 @@ int main(int argc, char **argv) { (outputOpt.PROJ5 + outputOpt.WKT2_2019 + outputOpt.WKT2_2019_SIMPLIFIED + outputOpt.WKT2_2015 + outputOpt.WKT2_2015_SIMPLIFIED + outputOpt.WKT1_GDAL + - outputOpt.WKT1_ESRI + outputOpt.PROJJSON) != 1) { + outputOpt.WKT1_ESRI + outputOpt.PROJJSON + outputOpt.SQL) != 1) { std::cerr << "-q can only be used with a single output format" << std::endl; usage(); diff --git a/test/cli/testprojinfo b/test/cli/testprojinfo index 1e964a5b..e300fc71 100755 --- a/test/cli/testprojinfo +++ b/test/cli/testprojinfo @@ -50,6 +50,10 @@ echo "Testing projinfo -o ALL EPSG:4326" >> ${OUT} $EXE -o ALL EPSG:4326 >>${OUT} echo "" >>${OUT} +echo "Testing projinfo \"+proj=merc +lat_ts=5 +datum=WGS84 +type=crs\" --output-id HOBU:MY_CRS -o SQL -q" >> ${OUT} +$EXE "+proj=merc +lat_ts=5 +datum=WGS84 +type=crs" --output-id HOBU:MY_CRS -o SQL -q >>${OUT} +echo "" >>${OUT} + echo "Testing projinfo -s EPSG:4326 -t EPSG:32631 --single-line" >> ${OUT} $EXE -s EPSG:4326 -t EPSG:32631 --single-line >>${OUT} echo "" >>${OUT} diff --git a/test/cli/testprojinfo_out.dist b/test/cli/testprojinfo_out.dist index 920374d9..c5bae908 100644 --- a/test/cli/testprojinfo_out.dist +++ b/test/cli/testprojinfo_out.dist @@ -251,6 +251,14 @@ PROJJSON: } } +Testing projinfo "+proj=merc +lat_ts=5 +datum=WGS84 +type=crs" --output-id HOBU:MY_CRS -o SQL -q +INSERT INTO geodetic_crs VALUES('HOBU','GEODETIC_CRS_MY_CRS','unknown','','geographic 2D','EPSG','6424','EPSG','6326',NULL,0); +INSERT INTO usage VALUES('HOBU','USAGE_GEODETIC_CRS_MY_CRS','geodetic_crs','HOBU','GEODETIC_CRS_MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); +INSERT INTO conversion VALUES('HOBU','CONVERSION_MY_CRS','unknown','','EPSG','9805','Mercator (variant B)','EPSG','8823','Latitude of 1st standard parallel',5,'EPSG','9122','EPSG','8802','Longitude of natural origin',0,'EPSG','9122','EPSG','8806','False easting',0,'EPSG','9001','EPSG','8807','False northing',0,'EPSG','9001',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0); +INSERT INTO usage VALUES('HOBU','USAGE_CONVERSION_MY_CRS','conversion','HOBU','CONVERSION_MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); +INSERT INTO projected_crs VALUES('HOBU','MY_CRS','unknown','','EPSG','4400','HOBU','GEODETIC_CRS_MY_CRS','HOBU','CONVERSION_MY_CRS',NULL,0); +INSERT INTO usage VALUES('HOBU','USAGE_PROJECTED_CRS_MY_CRS','projected_crs','HOBU','MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); + Testing projinfo -s EPSG:4326 -t EPSG:32631 --single-line Candidate operations found: 1 ------------------------------------- -- cgit v1.2.3 From e33f6b5eccdb0c66f5ff81c07a619708ebf9ec31 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 13 Mar 2021 10:52:42 +0100 Subject: CRS::identify(): take into account allowed authority to identify hard-coded WGS84-based CRSs --- src/iso19111/crs.cpp | 69 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/src/iso19111/crs.cpp b/src/iso19111/crs.cpp index 65f2087d..7c58640e 100644 --- a/src/iso19111/crs.cpp +++ b/src/iso19111/crs.cpp @@ -39,6 +39,7 @@ #include "proj/coordinateoperation.hpp" #include "proj/coordinatesystem.hpp" #include "proj/io.hpp" +#include "proj/metadata.hpp" #include "proj/util.hpp" #include "proj/internal/coordinatesystem_internal.hpp" @@ -446,7 +447,8 @@ CRSNNPtr CRS::createBoundCRSToWGS84IfPossible( crs_authority = *(l_identifiers[0]->codeSpace()); } - auto authorities = dbContext->getAllowedAuthorities(crs_authority, "EPSG"); + auto authorities = dbContext->getAllowedAuthorities( + crs_authority, metadata::Identifier::EPSG); if (authorities.empty()) { authorities.emplace_back(); } @@ -1686,8 +1688,8 @@ void GeodeticCRS::_exportToWKT(io::WKTFormatter *formatter) const { auto geogCRS2D = demoteTo2D(std::string(), dbContext); if (dbContext) { - const auto res = geogCRS2D->identify( - io::AuthorityFactory::create(NN_NO_CHECK(dbContext), "EPSG")); + const auto res = geogCRS2D->identify(io::AuthorityFactory::create( + NN_NO_CHECK(dbContext), metadata::Identifier::EPSG)); if (res.size() == 1) { const auto &front = res.front(); if (front.second == 100) { @@ -2086,26 +2088,31 @@ GeodeticCRS::identify(const io::AuthorityFactoryPtr &authorityFactory) const { ? util::IComparable::Criterion::EQUIVALENT_EXCEPT_AXIS_ORDER_GEOGCRS : util::IComparable::Criterion::EQUIVALENT; - const GeographicCRSNNPtr candidatesCRS[] = {GeographicCRS::EPSG_4326, - GeographicCRS::EPSG_4267, - GeographicCRS::EPSG_4269}; - for (const auto &crs : candidatesCRS) { - const bool nameEquivalent = metadata::Identifier::isEquivalentName( - thisName.c_str(), crs->nameStr().c_str()); - const bool nameEqual = thisName == crs->nameStr(); - const bool isEq = _isEquivalentTo(crs.get(), crsCriterion, dbContext); - if (nameEquivalent && isEq && (!authorityFactory || nameEqual)) { - res.emplace_back(util::nn_static_pointer_cast(crs), - nameEqual ? 100 : 90); - return res; - } else if (nameEqual && !isEq && !authorityFactory) { - res.emplace_back(util::nn_static_pointer_cast(crs), - 25); - return res; - } else if (isEq && !authorityFactory) { - res.emplace_back(util::nn_static_pointer_cast(crs), - 70); - return res; + if (authorityFactory == nullptr || + authorityFactory->getAuthority().empty() || + authorityFactory->getAuthority() == metadata::Identifier::EPSG) { + const GeographicCRSNNPtr candidatesCRS[] = {GeographicCRS::EPSG_4326, + GeographicCRS::EPSG_4267, + GeographicCRS::EPSG_4269}; + for (const auto &crs : candidatesCRS) { + const bool nameEquivalent = metadata::Identifier::isEquivalentName( + thisName.c_str(), crs->nameStr().c_str()); + const bool nameEqual = thisName == crs->nameStr(); + const bool isEq = + _isEquivalentTo(crs.get(), crsCriterion, dbContext); + if (nameEquivalent && isEq && (!authorityFactory || nameEqual)) { + res.emplace_back(util::nn_static_pointer_cast(crs), + nameEqual ? 100 : 90); + return res; + } else if (nameEqual && !isEq && !authorityFactory) { + res.emplace_back(util::nn_static_pointer_cast(crs), + 25); + return res; + } else if (isEq && !authorityFactory) { + res.emplace_back(util::nn_static_pointer_cast(crs), + 70); + return res; + } } } @@ -3645,8 +3652,8 @@ void ProjectedCRS::_exportToWKT(io::WKTFormatter *formatter) const { if (axisList.size() == 3 && !(isWKT2 && formatter->use2019Keywords())) { auto projCRS2D = demoteTo2D(std::string(), dbContext); if (dbContext) { - const auto res = projCRS2D->identify( - io::AuthorityFactory::create(NN_NO_CHECK(dbContext), "EPSG")); + const auto res = projCRS2D->identify(io::AuthorityFactory::create( + NN_NO_CHECK(dbContext), metadata::Identifier::EPSG)); if (res.size() == 1) { const auto &front = res.front(); if (front.second == 100) { @@ -4164,19 +4171,25 @@ ProjectedCRS::identify(const io::AuthorityFactoryPtr &authorityFactory) const { ? 90 : 70; }; - auto computeUTMCRSName = [](const char *base, int l_zone, bool l_north) { - return base + toString(l_zone) + (l_north ? "N" : "S"); - }; const auto &conv = derivingConversionRef(); const auto &cs = coordinateSystem(); if (baseRes.size() == 1 && baseRes.front().second >= 70 && + (authorityFactory == nullptr || + authorityFactory->getAuthority().empty() || + authorityFactory->getAuthority() == metadata::Identifier::EPSG) && conv->isUTM(zone, north) && cs->_isEquivalentTo( cs::CartesianCS::createEastingNorthing(common::UnitOfMeasure::METRE) .get(), util::IComparable::Criterion::EQUIVALENT, dbContext)) { + + auto computeUTMCRSName = [](const char *base, int l_zone, + bool l_north) { + return base + toString(l_zone) + (l_north ? "N" : "S"); + }; + if (baseRes.front().first->_isEquivalentTo( GeographicCRS::EPSG_4326.get(), util::IComparable::Criterion::EQUIVALENT, dbContext)) { -- cgit v1.2.3 From 8a67a3fb96ffdb29887b2954dd4bb8af92f6960d Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 13 Mar 2021 11:46:08 +0100 Subject: SQL output: add capability to restrict the authorities into which to look for intermediate objects --- docs/source/apps/projinfo.rst | 72 +++++- include/proj/io.hpp | 8 +- scripts/reference_exported_symbols.txt | 2 +- src/apps/projinfo.cpp | 10 +- src/iso19111/c_api.cpp | 28 ++- src/iso19111/factory.cpp | 431 ++++++++++++++++++++------------- src/proj.h | 1 + test/cli/testprojinfo | 4 + test/cli/testprojinfo_out.dist | 12 + test/unit/test_c_api.cpp | 57 ++++- 10 files changed, 437 insertions(+), 188 deletions(-) diff --git a/docs/source/apps/projinfo.rst b/docs/source/apps/projinfo.rst index c7a56689..34df3bca 100644 --- a/docs/source/apps/projinfo.rst +++ b/docs/source/apps/projinfo.rst @@ -25,6 +25,7 @@ Synopsis | [--show-superseded] [--hide-ballpark] [--accuracy {accuracy}] | [--allow-ellipsoidal-height-as-vertical-crs] | [--boundcrs-to-wgs84] + | [--authority name] | [--main-db-path path] [--aux-db-path path]* | [--identify] [--3d] | [--output-id AUTH:CODE] @@ -240,6 +241,16 @@ The following control parameters can appear in any order: geographic CRS, and if found, wraps those CRS into a BoundCRS object. This is mostly to be used for early-binding approaches. +.. option:: --authority name + + Specify the name of the authority into which to restrict looks up for + objects, when specifying an object by name or when coordinate operations are + computed. The default is to allow all authorities. + + When used with SQL output, this restricts the authorities to which intermediate + objects can belong to (the default is EPSG and PROJ). Note that the authority + of the :option:`--output-id` option will also be implicitly added. + .. option:: --main-db-path path Specify the name and path of the database to be used by projinfo. The @@ -451,11 +462,22 @@ Output: } } -4. Exporting the SQL statements to insert a new CRS in the database. +4. Exporting the SQL statements to insert a new CRS in an auxiliary database. .. code-block:: console - projinfo "+proj=merc +lat_ts=5 +datum=WGS84 +type=crs" --output-id HOBU:MY_CRS -o SQL -q + # Get the SQL statements for a custom CRS + projinfo "+proj=merc +lat_ts=5 +datum=WGS84 +type=crs +title=my_crs" --output-id HOBU:MY_CRS -o SQL -q > my_crs.sql + cat my_crs.sql + + # Initialize an auxiliary database with the schema of the reference database + echo ".schema" | sqlite3 /path/to/proj.db | sqlite3 aux.db + + # Append the content of the definition of HOBU:MY_CRS + sqlite3 aux.db < my_crs.db + + # Check that everything works OK + projinfo --aux-db-path aux.db HOBU:MY_CRS Output: @@ -465,9 +487,53 @@ Output: INSERT INTO usage VALUES('HOBU','USAGE_GEODETIC_CRS_MY_CRS','geodetic_crs','HOBU','GEODETIC_CRS_MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); INSERT INTO conversion VALUES('HOBU','CONVERSION_MY_CRS','unknown','','EPSG','9805','Mercator (variant B)','EPSG','8823','Latitude of 1st standard parallel',5,'EPSG','9122','EPSG','8802','Longitude of natural origin',0,'EPSG','9122','EPSG','8806','False easting',0,'EPSG','9001','EPSG','8807','False northing',0,'EPSG','9001',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0); INSERT INTO usage VALUES('HOBU','USAGE_CONVERSION_MY_CRS','conversion','HOBU','CONVERSION_MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); - INSERT INTO projected_crs VALUES('HOBU','MY_CRS','unknown','','EPSG','4400','HOBU','GEODETIC_CRS_MY_CRS','HOBU','CONVERSION_MY_CRS',NULL,0); + INSERT INTO projected_crs VALUES('HOBU','MY_CRS','my_crs','','EPSG','4400','HOBU','GEODETIC_CRS_MY_CRS','HOBU','CONVERSION_MY_CRS',NULL,0); INSERT INTO usage VALUES('HOBU','USAGE_PROJECTED_CRS_MY_CRS','projected_crs','HOBU','MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); +:: + + PROJ.4 string: + +proj=merc +lat_ts=5 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +type=crs + + WKT2:2019 string: + PROJCRS["my_crs", + BASEGEOGCRS["unknown", + ENSEMBLE["World Geodetic System 1984 ensemble", + MEMBER["World Geodetic System 1984 (Transit)"], + MEMBER["World Geodetic System 1984 (G730)"], + MEMBER["World Geodetic System 1984 (G873)"], + MEMBER["World Geodetic System 1984 (G1150)"], + MEMBER["World Geodetic System 1984 (G1674)"], + MEMBER["World Geodetic System 1984 (G1762)"], + ELLIPSOID["WGS 84",6378137,298.257223563, + LENGTHUNIT["metre",1]], + ENSEMBLEACCURACY[2.0]], + PRIMEM["Greenwich",0, + ANGLEUNIT["degree",0.0174532925199433]], + ID["HOBU","GEODETIC_CRS_MY_CRS"]], + CONVERSION["unknown", + METHOD["Mercator (variant B)", + ID["EPSG",9805]], + PARAMETER["Latitude of 1st standard parallel",5, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8823]], + PARAMETER["Longitude of natural origin",0, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8802]], + PARAMETER["False easting",0, + LENGTHUNIT["metre",1], + ID["EPSG",8806]], + PARAMETER["False northing",0, + LENGTHUNIT["metre",1], + ID["EPSG",8807]]], + CS[Cartesian,2], + AXIS["(E)",east, + ORDER[1], + LENGTHUNIT["metre",1]], + AXIS["(N)",north, + ORDER[2], + LENGTHUNIT["metre",1]], + ID["HOBU","MY_CRS"]] .. only:: man diff --git a/include/proj/io.hpp b/include/proj/io.hpp index 5f0dd475..96a97142 100644 --- a/include/proj/io.hpp +++ b/include/proj/io.hpp @@ -844,10 +844,10 @@ class PROJ_GCC_DLL DatabaseContext { suggestsCodeFor(const common::IdentifiedObjectNNPtr &object, const std::string &authName, bool numericCode); - PROJ_DLL std::vector - getInsertStatementsFor(const common::IdentifiedObjectNNPtr &object, - const std::string &authName, const std::string &code, - bool numericCode); + PROJ_DLL std::vector getInsertStatementsFor( + const common::IdentifiedObjectNNPtr &object, + const std::string &authName, const std::string &code, bool numericCode, + const std::vector &allowedAuthorities = {"EPSG", "PROJ"}); PROJ_DLL void stopInsertStatementsSession(); diff --git a/scripts/reference_exported_symbols.txt b/scripts/reference_exported_symbols.txt index 798f515e..aa148762 100644 --- a/scripts/reference_exported_symbols.txt +++ b/scripts/reference_exported_symbols.txt @@ -359,7 +359,7 @@ osgeo::proj::io::DatabaseContext::create(void*) osgeo::proj::io::DatabaseContext::~DatabaseContext() osgeo::proj::io::DatabaseContext::getAuthorities() const osgeo::proj::io::DatabaseContext::getDatabaseStructure() const -osgeo::proj::io::DatabaseContext::getInsertStatementsFor(dropbox::oxygen::nn > const&, std::string const&, std::string const&, bool) +osgeo::proj::io::DatabaseContext::getInsertStatementsFor(dropbox::oxygen::nn > const&, std::string const&, std::string const&, bool, std::vector > const&) osgeo::proj::io::DatabaseContext::getMetadata(char const*) const osgeo::proj::io::DatabaseContext::getPath() const osgeo::proj::io::DatabaseContext::getSqliteHandle() const diff --git a/src/apps/projinfo.cpp b/src/apps/projinfo.cpp index 39d9666d..2334c293 100644 --- a/src/apps/projinfo.cpp +++ b/src/apps/projinfo.cpp @@ -76,6 +76,7 @@ struct OutputOptions { bool allowEllipsoidalHeightAsVerticalCRS = false; std::string outputAuthName{}; std::string outputCode{}; + std::vector allowedAuthorities{}; }; } // anonymous namespace @@ -104,6 +105,7 @@ static void usage() { << " [--allow-ellipsoidal-height-as-vertical-crs]" << std::endl << " [--boundcrs-to-wgs84]" << std::endl + << " [--authority name]" << std::endl << " [--main-db-path path] [--aux-db-path path]*" << std::endl << " [--identify] [--3d]" << std::endl @@ -573,9 +575,14 @@ static void outputObject( std::cout << "SQL:" << std::endl; } dbContext->startInsertStatementsSession(); + auto allowedAuthorities(outputOpt.allowedAuthorities); + if (allowedAuthorities.empty()) { + allowedAuthorities.emplace_back("EPSG"); + allowedAuthorities.emplace_back("PROJ"); + } const auto statements = dbContext->getInsertStatementsFor( NN_NO_CHECK(identified), outputOpt.outputAuthName, - outputOpt.outputCode, false); + outputOpt.outputCode, false, allowedAuthorities); dbContext->stopInsertStatementsSession(); for (const auto &sql : statements) { std::cout << sql << std::endl; @@ -1118,6 +1125,7 @@ int main(int argc, char **argv) { } else if (arg == "--authority" && i + 1 < argc) { i++; authority = argv[i]; + outputOpt.allowedAuthorities = split(authority, ','); } else if (arg == "--identify") { identify = true; } else if (arg == "--show-superseded") { diff --git a/src/iso19111/c_api.cpp b/src/iso19111/c_api.cpp index b97e7300..a79b387e 100644 --- a/src/iso19111/c_api.cpp +++ b/src/iso19111/c_api.cpp @@ -8881,6 +8881,15 @@ void proj_string_destroy(char *str) { free(str); } * @param code Code with which the object will be inserted.Must not be NULL. * @param numeric_codes Whether intermediate objects that can be created should * use numeric codes (true), or may be alphanumeric (false) + * @param allowed_authorities NULL terminated list of authority names, or NULL. + * Authorities to which intermediate objects are + * allowed to refer to. "authority" will be + * implicitly added to it. Note that unit, + * coordinate systems, projection methods and + * parameters will in any case be allowed to refer + * to EPSG. + * If NULL, allowed_authorities defaults to + * {"EPSG", "PROJ", nullptr} * @param options NULL terminated list of options, or NULL. * No options are supported currently. * @@ -8888,12 +8897,10 @@ void proj_string_destroy(char *str) { free(str); } * proj_string_list_destroy()), or NULL in case of error. * @since 8.1 */ -PROJ_STRING_LIST proj_get_insert_statements(PJ_CONTEXT *ctx, - PJ_INSERT_SESSION *session, - const PJ *object, - const char *authority, - const char *code, int numeric_codes, - const char *const *options) { +PROJ_STRING_LIST proj_get_insert_statements( + PJ_CONTEXT *ctx, PJ_INSERT_SESSION *session, const PJ *object, + const char *authority, const char *code, int numeric_codes, + const char *const *allowed_authorities, const char *const *options) { SANITIZE_CTX(ctx); (void)options; @@ -8939,9 +8946,16 @@ PROJ_STRING_LIST proj_get_insert_statements(PJ_CONTEXT *ctx, } auto dbContext = getDBcontext(ctx); + std::vector allowedAuthorities{"EPSG", "PROJ"}; + if (allowed_authorities) { + allowedAuthorities.clear(); + for (auto iter = allowed_authorities; *iter; ++iter) { + allowedAuthorities.emplace_back(*iter); + } + } auto statements = dbContext->getInsertStatementsFor( NN_NO_CHECK(identifiedObject), authority, code, - numeric_codes != FALSE); + numeric_codes != FALSE, allowedAuthorities); return to_string_list(std::move(statements)); } catch (const std::exception &e) { proj_log_error(ctx, __FUNCTION__, e.what()); diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp index 36ef217d..d7050bce 100644 --- a/src/iso19111/factory.cpp +++ b/src/iso19111/factory.cpp @@ -49,6 +49,7 @@ #include "sqlite3_utils.hpp" +#include #include #include #include @@ -347,51 +348,60 @@ struct DatabaseContext::Private { void appendSql(std::vector &sqlStatements, const std::string &sql); - void identifyOrInsertUsages(const common::ObjectUsageNNPtr &obj, - const std::string &tableName, - const std::string &authName, - const std::string &code, - std::vector &sqlStatements); + void + identifyOrInsertUsages(const common::ObjectUsageNNPtr &obj, + const std::string &tableName, + const std::string &authName, const std::string &code, + const std::vector &allowedAuthorities, + std::vector &sqlStatements); std::vector getInsertStatementsFor(const datum::PrimeMeridianNNPtr &pm, const std::string &authName, const std::string &code, - bool numericCode); + bool numericCode, + const std::vector &allowedAuthorities); std::vector getInsertStatementsFor(const datum::EllipsoidNNPtr &ellipsoid, const std::string &authName, const std::string &code, - bool numericCode); + bool numericCode, + const std::vector &allowedAuthorities); std::vector getInsertStatementsFor(const datum::GeodeticReferenceFrameNNPtr &datum, const std::string &authName, const std::string &code, - bool numericCode); + bool numericCode, + const std::vector &allowedAuthorities); std::vector getInsertStatementsFor(const crs::GeodeticCRSNNPtr &crs, const std::string &authName, const std::string &code, - bool numericCode); + bool numericCode, + const std::vector &allowedAuthorities); std::vector getInsertStatementsFor(const crs::ProjectedCRSNNPtr &crs, const std::string &authName, const std::string &code, - bool numericCode); + bool numericCode, + const std::vector &allowedAuthorities); std::vector getInsertStatementsFor(const datum::VerticalReferenceFrameNNPtr &datum, const std::string &authName, const std::string &code, - bool numericCode); + bool numericCode, + const std::vector &allowedAuthorities); std::vector getInsertStatementsFor(const crs::VerticalCRSNNPtr &crs, const std::string &authName, const std::string &code, - bool numericCode); + bool numericCode, + const std::vector &allowedAuthorities); std::vector getInsertStatementsFor(const crs::CompoundCRSNNPtr &crs, const std::string &authName, const std::string &code, - bool numericCode); + bool numericCode, + const std::vector &allowedAuthorities); #ifdef ENABLE_CUSTOM_LOCKLESS_VFS std::unique_ptr vfs_{}; @@ -1100,63 +1110,81 @@ void DatabaseContext::Private::appendSql( static void identifyFromNameOrCode( const DatabaseContextNNPtr &dbContext, - const AuthorityFactoryNNPtr &allAuthFactory, - const common::IdentifiedObjectNNPtr &obj, + const std::vector &allowedAuthorities, + const std::string &authNameParent, const common::IdentifiedObjectNNPtr &obj, std::function( const AuthorityFactoryNNPtr &authFactory, const std::string &)> instantiateFunc, AuthorityFactory::ObjectType objType, std::string &authName, std::string &code) { + + auto allowedAuthoritiesTmp(allowedAuthorities); + allowedAuthoritiesTmp.emplace_back(authNameParent); + for (const auto &id : obj->identifiers()) { try { - const auto tmpAuthFactory = - AuthorityFactory::create(dbContext, *(id->codeSpace())); - if (instantiateFunc(tmpAuthFactory, id->code()) - ->isEquivalentTo( - obj.get(), util::IComparable::Criterion::EQUIVALENT)) { - authName = *(id->codeSpace()); - code = id->code(); - return; + const auto idAuthName = *(id->codeSpace()); + if (std::find(allowedAuthoritiesTmp.begin(), + allowedAuthoritiesTmp.end(), + idAuthName) != allowedAuthoritiesTmp.end()) { + const auto factory = + AuthorityFactory::create(dbContext, idAuthName); + if (instantiateFunc(factory, id->code()) + ->isEquivalentTo( + obj.get(), + util::IComparable::Criterion::EQUIVALENT)) { + authName = idAuthName; + code = id->code(); + return; + } } } catch (const std::exception &) { } } - const auto candidates = allAuthFactory->createObjectsFromName( - obj->nameStr(), {objType}, false, 0); - for (const auto &candidate : candidates) { - const auto &ids = candidate->identifiers(); - if (!ids.empty() && - candidate->isEquivalentTo( - obj.get(), util::IComparable::Criterion::EQUIVALENT)) { - const auto &id = ids.front(); - authName = *(id->codeSpace()); - code = id->code(); + for (const auto &allowedAuthority : allowedAuthoritiesTmp) { + const auto factory = + AuthorityFactory::create(dbContext, allowedAuthority); + const auto candidates = + factory->createObjectsFromName(obj->nameStr(), {objType}, false, 0); + for (const auto &candidate : candidates) { + const auto &ids = candidate->identifiers(); + if (!ids.empty() && + candidate->isEquivalentTo( + obj.get(), util::IComparable::Criterion::EQUIVALENT)) { + const auto &id = ids.front(); + authName = *(id->codeSpace()); + code = id->code(); + return; + } } } } // --------------------------------------------------------------------------- -static void identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, - const AuthorityFactoryNNPtr &allAuthFactory, - const datum::DatumEnsembleNNPtr &obj, - std::string &authName, std::string &code) { +static void +identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, + const std::vector &allowedAuthorities, + const std::string &authNameParent, + const datum::DatumEnsembleNNPtr &obj, + std::string &authName, std::string &code) { const auto instantiateFunc = [](const AuthorityFactoryNNPtr &authFactory, const std::string &lCode) { return util::nn_static_pointer_cast( authFactory->createDatumEnsemble(lCode, "geodetic_datum")); }; - identifyFromNameOrCode(dbContext, allAuthFactory, obj, instantiateFunc, - AuthorityFactory::ObjectType::DATUM_ENSEMBLE, - authName, code); + identifyFromNameOrCode( + dbContext, allowedAuthorities, authNameParent, obj, instantiateFunc, + AuthorityFactory::ObjectType::DATUM_ENSEMBLE, authName, code); } // --------------------------------------------------------------------------- static void identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, - const AuthorityFactoryNNPtr &allAuthFactory, + const std::vector &allowedAuthorities, + const std::string &authNameParent, const datum::GeodeticReferenceFrameNNPtr &obj, std::string &authName, std::string &code) { const auto instantiateFunc = [](const AuthorityFactoryNNPtr &authFactory, @@ -1165,47 +1193,52 @@ identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, authFactory->createGeodeticDatum(lCode)); }; identifyFromNameOrCode( - dbContext, allAuthFactory, obj, instantiateFunc, + dbContext, allowedAuthorities, authNameParent, obj, instantiateFunc, AuthorityFactory::ObjectType::GEODETIC_REFERENCE_FRAME, authName, code); } // --------------------------------------------------------------------------- -static void identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, - const AuthorityFactoryNNPtr &allAuthFactory, - const datum::EllipsoidNNPtr &obj, - std::string &authName, std::string &code) { +static void +identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, + const std::vector &allowedAuthorities, + const std::string &authNameParent, + const datum::EllipsoidNNPtr &obj, std::string &authName, + std::string &code) { const auto instantiateFunc = [](const AuthorityFactoryNNPtr &authFactory, const std::string &lCode) { return util::nn_static_pointer_cast( authFactory->createEllipsoid(lCode)); }; - identifyFromNameOrCode(dbContext, allAuthFactory, obj, instantiateFunc, - AuthorityFactory::ObjectType::ELLIPSOID, authName, - code); + identifyFromNameOrCode( + dbContext, allowedAuthorities, authNameParent, obj, instantiateFunc, + AuthorityFactory::ObjectType::ELLIPSOID, authName, code); } // --------------------------------------------------------------------------- -static void identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, - const AuthorityFactoryNNPtr &allAuthFactory, - const datum::PrimeMeridianNNPtr &obj, - std::string &authName, std::string &code) { +static void +identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, + const std::vector &allowedAuthorities, + const std::string &authNameParent, + const datum::PrimeMeridianNNPtr &obj, + std::string &authName, std::string &code) { const auto instantiateFunc = [](const AuthorityFactoryNNPtr &authFactory, const std::string &lCode) { return util::nn_static_pointer_cast( authFactory->createPrimeMeridian(lCode)); }; - identifyFromNameOrCode(dbContext, allAuthFactory, obj, instantiateFunc, - AuthorityFactory::ObjectType::PRIME_MERIDIAN, - authName, code); + identifyFromNameOrCode( + dbContext, allowedAuthorities, authNameParent, obj, instantiateFunc, + AuthorityFactory::ObjectType::PRIME_MERIDIAN, authName, code); } // --------------------------------------------------------------------------- static void identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, - const AuthorityFactoryNNPtr &allAuthFactory, + const std::vector &allowedAuthorities, + const std::string &authNameParent, const datum::VerticalReferenceFrameNNPtr &obj, std::string &authName, std::string &code) { const auto instantiateFunc = [](const AuthorityFactoryNNPtr &authFactory, @@ -1214,7 +1247,7 @@ identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, authFactory->createVerticalDatum(lCode)); }; identifyFromNameOrCode( - dbContext, allAuthFactory, obj, instantiateFunc, + dbContext, allowedAuthorities, authNameParent, obj, instantiateFunc, AuthorityFactory::ObjectType::VERTICAL_REFERENCE_FRAME, authName, code); } @@ -1292,7 +1325,7 @@ void DatabaseContext::Private::identify(const DatabaseContextNNPtr &dbContext, switch (obj.type()) { case common::UnitOfMeasure::Type::LINEAR: { if (convFactor == 1.0) { - authName = "EPSG"; + authName = metadata::Identifier::EPSG; code = "9001"; return; } @@ -1302,7 +1335,7 @@ void DatabaseContext::Private::identify(const DatabaseContextNNPtr &dbContext, constexpr double CONV_FACTOR_DEGREE = 1.74532925199432781271e-02; if (std::abs(convFactor - CONV_FACTOR_DEGREE) <= 1e-10 * CONV_FACTOR_DEGREE) { - authName = "EPSG"; + authName = metadata::Identifier::EPSG; code = "9102"; return; } @@ -1310,7 +1343,7 @@ void DatabaseContext::Private::identify(const DatabaseContextNNPtr &dbContext, } case common::UnitOfMeasure::Type::SCALE: { if (convFactor == 1.0) { - authName = "EPSG"; + authName = metadata::Identifier::EPSG; code = "9201"; return; } @@ -1390,7 +1423,7 @@ void DatabaseContext::Private::identify(const DatabaseContextNNPtr &dbContext, (axisList[0]->nameStr() == "Up" || axisList[0]->nameStr() == "Gravity-related height")) { // preferred coordinate system for gravity-related height - authName = "EPSG"; + authName = metadata::Identifier::EPSG; code = "6499"; return; } @@ -1416,16 +1449,16 @@ void DatabaseContext::Private::identify(const DatabaseContextNNPtr &dbContext, util::IComparable::Criterion::EQUIVALENT)) { authName = rowAuthName; code = rowCode; - if (authName == "EPSG" && code == "4400") { + if (authName == metadata::Identifier::EPSG && code == "4400") { // preferred coordinate system for cartesian // Easting, Northing return; } - if (authName == "EPSG" && code == "6422") { + if (authName == metadata::Identifier::EPSG && code == "6422") { // preferred coordinate system for geographic lat, lon return; } - if (authName == "EPSG" && code == "6423") { + if (authName == metadata::Identifier::EPSG && code == "6423") { // preferred coordinate system for geographic lat, lon, h return; } @@ -1487,9 +1520,25 @@ void DatabaseContext::Private::identifyOrInsert( // --------------------------------------------------------------------------- +static void +addAllowedAuthoritiesCond(const std::vector &allowedAuthorities, + const std::string &authName, std::string &sql, + ListOfParams ¶ms) { + sql += "auth_name IN (?"; + params.emplace_back(authName); + for (const auto &allowedAuthority : allowedAuthorities) { + sql += ",?"; + params.emplace_back(allowedAuthority); + } + sql += ')'; +} + +// --------------------------------------------------------------------------- + void DatabaseContext::Private::identifyOrInsertUsages( const common::ObjectUsageNNPtr &obj, const std::string &tableName, const std::string &authName, const std::string &code, + const std::vector &allowedAuthorities, std::vector &sqlStatements) { std::string usageCode("USAGE_"); @@ -1517,13 +1566,16 @@ void DatabaseContext::Private::identifyOrInsertUsages( std::string scopeCode; const auto &scope = domain->scope(); if (scope.has_value()) { - const auto rows = - run("SELECT auth_name, code, " - "(CASE WHEN auth_name = 'EPSG' THEN 0 ELSE 1 END) " - "AS order_idx " - "FROM scope WHERE scope = ? AND deprecated = 0 " - "ORDER BY order_idx", - {*scope}); + std::string sql = + "SELECT auth_name, code, " + "(CASE WHEN auth_name = 'EPSG' THEN 0 ELSE 1 END) " + "AS order_idx " + "FROM scope WHERE scope = ? AND deprecated = 0 AND "; + ListOfParams params{*scope}; + addAllowedAuthoritiesCond(allowedAuthorities, authName, sql, + params); + sql += " ORDER BY order_idx, auth_name, code"; + const auto rows = run(sql, params); if (!rows.empty()) { const auto &row = rows.front(); scopeAuthName = row[0]; @@ -1531,10 +1583,10 @@ void DatabaseContext::Private::identifyOrInsertUsages( } else { scopeAuthName = authName; scopeCode = "SCOPE_" + tableName + "_" + code; - const auto sql = formatStatement( + const auto sqlToInsert = formatStatement( "INSERT INTO scope VALUES('%q','%q','%q',0);", scopeAuthName.c_str(), scopeCode.c_str(), scope->c_str()); - appendSql(sqlStatements, sql); + appendSql(sqlStatements, sqlToInsert); } } else { scopeAuthName = "PROJ"; @@ -1551,16 +1603,20 @@ void DatabaseContext::Private::identifyOrInsertUsages( dynamic_cast( geogElts.front().get()); if (bbox) { - const auto rows = run( + std::string sql = "SELECT auth_name, code, " "(CASE WHEN auth_name = 'EPSG' THEN 0 ELSE 1 END) " "AS order_idx " "FROM extent WHERE south_lat = ? AND north_lat = ? " "AND west_lon = ? AND east_lon = ? AND deprecated = 0 " - "ORDER BY order_idx", - {bbox->southBoundLatitude(), bbox->northBoundLatitude(), - bbox->westBoundLongitude(), - bbox->eastBoundLongitude()}); + "AND "; + ListOfParams params{ + bbox->southBoundLatitude(), bbox->northBoundLatitude(), + bbox->westBoundLongitude(), bbox->eastBoundLongitude()}; + addAllowedAuthoritiesCond(allowedAuthorities, authName, sql, + params); + sql += " ORDER BY order_idx, auth_name, code"; + const auto rows = run(sql, params); if (!rows.empty()) { const auto &row = rows.front(); extentAuthName = row[0]; @@ -1572,7 +1628,7 @@ void DatabaseContext::Private::identifyOrInsertUsages( if (description.empty()) { description = "unknown"; } - const auto sql = formatStatement( + const auto sqlToInsert = formatStatement( "INSERT INTO extent " "VALUES('%q','%q','%q','%q',%f,%f,%f,%f,0);", extentAuthName.c_str(), extentCode.c_str(), @@ -1581,7 +1637,7 @@ void DatabaseContext::Private::identifyOrInsertUsages( bbox->northBoundLatitude(), bbox->westBoundLongitude(), bbox->eastBoundLongitude()); - appendSql(sqlStatements, sql); + appendSql(sqlStatements, sqlToInsert); } } } @@ -1607,15 +1663,16 @@ void DatabaseContext::Private::identifyOrInsertUsages( std::vector DatabaseContext::Private::getInsertStatementsFor( const datum::PrimeMeridianNNPtr &pm, const std::string &authName, - const std::string &code, bool /*numericCode*/) { + const std::string &code, bool /*numericCode*/, + const std::vector &allowedAuthorities) { const auto self = NN_NO_CHECK(self_.lock()); - const auto allAuthFactory = AuthorityFactory::create(self, std::string()); // Check if the object is already known under that code std::string pmAuthName; std::string pmCode; - identifyFromNameOrCode(self, allAuthFactory, pm, pmAuthName, pmCode); + identifyFromNameOrCode(self, allowedAuthorities, authName, pm, pmAuthName, + pmCode); if (pmAuthName == authName && pmCode == code) { return {}; } @@ -1642,16 +1699,16 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( std::vector DatabaseContext::Private::getInsertStatementsFor( const datum::EllipsoidNNPtr &ellipsoid, const std::string &authName, - const std::string &code, bool /*numericCode*/) { + const std::string &code, bool /*numericCode*/, + const std::vector &allowedAuthorities) { const auto self = NN_NO_CHECK(self_.lock()); - const auto allAuthFactory = AuthorityFactory::create(self, std::string()); // Check if the object is already known under that code std::string ellipsoidAuthName; std::string ellipsoidCode; - identifyFromNameOrCode(self, allAuthFactory, ellipsoid, ellipsoidAuthName, - ellipsoidCode); + identifyFromNameOrCode(self, allowedAuthorities, authName, ellipsoid, + ellipsoidAuthName, ellipsoidCode); if (ellipsoidAuthName == authName && ellipsoidCode == code) { return {}; } @@ -1713,16 +1770,16 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( std::vector DatabaseContext::Private::getInsertStatementsFor( const datum::GeodeticReferenceFrameNNPtr &datum, - const std::string &authName, const std::string &code, bool numericCode) { + const std::string &authName, const std::string &code, bool numericCode, + const std::vector &allowedAuthorities) { const auto self = NN_NO_CHECK(self_.lock()); - const auto allAuthFactory = AuthorityFactory::create(self, std::string()); // Check if the object is already known under that code std::string datumAuthName; std::string datumCode; - identifyFromNameOrCode(self, allAuthFactory, datum, datumAuthName, - datumCode); + identifyFromNameOrCode(self, allowedAuthorities, authName, datum, + datumAuthName, datumCode); if (datumAuthName == authName && datumCode == code) { return {}; } @@ -1733,7 +1790,7 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( std::string ellipsoidAuthName; std::string ellipsoidCode; const auto &ellipsoidOfDatum = datum->ellipsoid(); - identifyFromNameOrCode(self, allAuthFactory, ellipsoidOfDatum, + identifyFromNameOrCode(self, allowedAuthorities, authName, ellipsoidOfDatum, ellipsoidAuthName, ellipsoidCode); if (ellipsoidAuthName.empty()) { ellipsoidAuthName = authName; @@ -1744,14 +1801,16 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( ellipsoidCode = "ELLPS_" + code; } sqlStatements = self->getInsertStatementsFor( - ellipsoidOfDatum, ellipsoidAuthName, ellipsoidCode, numericCode); + ellipsoidOfDatum, ellipsoidAuthName, ellipsoidCode, numericCode, + allowedAuthorities); } // Find or insert prime meridian std::string pmAuthName; std::string pmCode; const auto &pmOfDatum = datum->primeMeridian(); - identifyFromNameOrCode(self, allAuthFactory, pmOfDatum, pmAuthName, pmCode); + identifyFromNameOrCode(self, allowedAuthorities, authName, pmOfDatum, + pmAuthName, pmCode); if (pmAuthName.empty()) { pmAuthName = authName; if (numericCode) { @@ -1760,7 +1819,7 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( pmCode = "PM_" + code; } const auto sqlStatementsTmp = self->getInsertStatementsFor( - pmOfDatum, pmAuthName, pmCode, numericCode); + pmOfDatum, pmAuthName, pmCode, numericCode, allowedAuthorities); sqlStatements.insert(sqlStatements.end(), sqlStatementsTmp.begin(), sqlStatementsTmp.end()); } @@ -1776,7 +1835,7 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( appendSql(sqlStatements, sql); identifyOrInsertUsages(datum, "geodetic_datum", authName, code, - sqlStatements); + allowedAuthorities, sqlStatements); return sqlStatements; } @@ -1785,10 +1844,10 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( std::vector DatabaseContext::Private::getInsertStatementsFor( const crs::GeodeticCRSNNPtr &crs, const std::string &authName, - const std::string &code, bool numericCode) { + const std::string &code, bool numericCode, + const std::vector &allowedAuthorities) { const auto self = NN_NO_CHECK(self_.lock()); - const auto allAuthFactory = AuthorityFactory::create(self, std::string()); std::vector sqlStatements; @@ -1797,8 +1856,8 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( std::string datumCode; const auto &ensemble = crs->datumEnsemble(); if (ensemble) { - identifyFromNameOrCode(self, allAuthFactory, NN_NO_CHECK(ensemble), - datumAuthName, datumCode); + identifyFromNameOrCode(self, allowedAuthorities, authName, + NN_NO_CHECK(ensemble), datumAuthName, datumCode); if (datumAuthName.empty()) { throw FactoryException( "Unhandled yet: insertion of new DatumEnsemble"); @@ -1807,8 +1866,8 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( const auto &datum = crs->datum(); assert(datum); const auto datumNN = NN_NO_CHECK(datum); - identifyFromNameOrCode(self, allAuthFactory, datumNN, datumAuthName, - datumCode); + identifyFromNameOrCode(self, allowedAuthorities, authName, datumNN, + datumAuthName, datumCode); if (datumAuthName.empty()) { datumAuthName = authName; if (numericCode) { @@ -1816,8 +1875,9 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( } else { datumCode = "GEODETIC_DATUM_" + code; } - sqlStatements = self->getInsertStatementsFor( - datumNN, datumAuthName, datumCode, numericCode); + sqlStatements = + self->getInsertStatementsFor(datumNN, datumAuthName, datumCode, + numericCode, allowedAuthorities); } } @@ -1847,7 +1907,8 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( datumAuthName.c_str(), datumCode.c_str()); appendSql(sqlStatements, sql); - identifyOrInsertUsages(crs, "geodetic_crs", authName, code, sqlStatements); + identifyOrInsertUsages(crs, "geodetic_crs", authName, code, + allowedAuthorities, sqlStatements); return sqlStatements; } @@ -1855,10 +1916,10 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( std::vector DatabaseContext::Private::getInsertStatementsFor( const crs::ProjectedCRSNNPtr &crs, const std::string &authName, - const std::string &code, bool numericCode) { + const std::string &code, bool numericCode, + const std::vector &allowedAuthorities) { const auto self = NN_NO_CHECK(self_.lock()); - const auto allAuthFactory = AuthorityFactory::create(self, std::string()); std::vector sqlStatements; @@ -1866,13 +1927,22 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( const auto &baseCRS = crs->baseCRS(); std::string geodAuthName; std::string geodCode; - const auto candidates = baseCRS->identify(allAuthFactory); - for (const auto &candidate : candidates) { - if (candidate.second == 100) { - const auto &ids = candidate.first->identifiers(); - for (const auto &id : ids) { - geodAuthName = *(id->codeSpace()); - geodCode = id->code(); + + auto allowedAuthoritiesTmp(allowedAuthorities); + allowedAuthoritiesTmp.emplace_back(authName); + for (const auto &allowedAuthority : allowedAuthoritiesTmp) { + const auto factory = AuthorityFactory::create(self, allowedAuthority); + const auto candidates = baseCRS->identify(factory); + for (const auto &candidate : candidates) { + if (candidate.second == 100) { + const auto &ids = candidate.first->identifiers(); + for (const auto &id : ids) { + geodAuthName = *(id->codeSpace()); + geodCode = id->code(); + break; + } + } + if (!geodAuthName.empty()) { break; } } @@ -1880,8 +1950,8 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( if (geodAuthName.empty()) { geodAuthName = authName; geodCode = "GEODETIC_CRS_" + code; - sqlStatements = self->getInsertStatementsFor(baseCRS, geodAuthName, - geodCode, numericCode); + sqlStatements = self->getInsertStatementsFor( + baseCRS, geodAuthName, geodCode, numericCode, allowedAuthorities); } // Insert new record in conversion table @@ -1948,7 +2018,7 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( sql += ",0);"; appendSql(sqlStatements, sql); identifyOrInsertUsages(crs, "conversion", convAuthName, convCode, - sqlStatements); + allowedAuthorities, sqlStatements); } // Find or insert coordinate system @@ -1968,7 +2038,8 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( geodCode.c_str(), convAuthName.c_str(), convCode.c_str()); appendSql(sqlStatements, sql); - identifyOrInsertUsages(crs, "projected_crs", authName, code, sqlStatements); + identifyOrInsertUsages(crs, "projected_crs", authName, code, + allowedAuthorities, sqlStatements); return sqlStatements; } @@ -1978,18 +2049,18 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( std::vector DatabaseContext::Private::getInsertStatementsFor( const datum::VerticalReferenceFrameNNPtr &datum, const std::string &authName, const std::string &code, - bool /* numericCode */) { + bool /* numericCode */, + const std::vector &allowedAuthorities) { const auto self = NN_NO_CHECK(self_.lock()); - const auto allAuthFactory = AuthorityFactory::create(self, std::string()); std::vector sqlStatements; // Check if the object is already known under that code std::string datumAuthName; std::string datumCode; - identifyFromNameOrCode(self, allAuthFactory, datum, datumAuthName, - datumCode); + identifyFromNameOrCode(self, allowedAuthorities, authName, datum, + datumAuthName, datumCode); if (datumAuthName == authName && datumCode == code) { return {}; } @@ -2004,7 +2075,7 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( appendSql(sqlStatements, sql); identifyOrInsertUsages(datum, "vertical_datum", authName, code, - sqlStatements); + allowedAuthorities, sqlStatements); return sqlStatements; } @@ -2013,10 +2084,10 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( std::vector DatabaseContext::Private::getInsertStatementsFor( const crs::VerticalCRSNNPtr &crs, const std::string &authName, - const std::string &code, bool numericCode) { + const std::string &code, bool numericCode, + const std::vector &allowedAuthorities) { const auto self = NN_NO_CHECK(self_.lock()); - const auto allAuthFactory = AuthorityFactory::create(self, std::string()); std::vector sqlStatements; @@ -2025,8 +2096,8 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( std::string datumCode; const auto &ensemble = crs->datumEnsemble(); if (ensemble) { - identifyFromNameOrCode(self, allAuthFactory, NN_NO_CHECK(ensemble), - datumAuthName, datumCode); + identifyFromNameOrCode(self, allowedAuthorities, authName, + NN_NO_CHECK(ensemble), datumAuthName, datumCode); if (datumAuthName.empty()) { throw FactoryException( "Unhandled yet: insertion of new DatumEnsemble"); @@ -2035,8 +2106,8 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( const auto &datum = crs->datum(); assert(datum); const auto datumNN = NN_NO_CHECK(datum); - identifyFromNameOrCode(self, allAuthFactory, datumNN, datumAuthName, - datumCode); + identifyFromNameOrCode(self, allowedAuthorities, authName, datumNN, + datumAuthName, datumCode); if (datumAuthName.empty()) { datumAuthName = authName; if (numericCode) { @@ -2044,8 +2115,9 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( } else { datumCode = "VERTICAL_DATUM_" + code; } - sqlStatements = self->getInsertStatementsFor( - datumNN, datumAuthName, datumCode, numericCode); + sqlStatements = + self->getInsertStatementsFor(datumNN, datumAuthName, datumCode, + numericCode, allowedAuthorities); } } @@ -2066,7 +2138,8 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( datumAuthName.c_str(), datumCode.c_str()); appendSql(sqlStatements, sql); - identifyOrInsertUsages(crs, "vertical_crs", authName, code, sqlStatements); + identifyOrInsertUsages(crs, "vertical_crs", authName, code, + allowedAuthorities, sqlStatements); return sqlStatements; } @@ -2075,10 +2148,10 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( std::vector DatabaseContext::Private::getInsertStatementsFor( const crs::CompoundCRSNNPtr &crs, const std::string &authName, - const std::string &code, bool numericCode) { + const std::string &code, bool numericCode, + const std::vector &allowedAuthorities) { const auto self = NN_NO_CHECK(self_.lock()); - const auto allAuthFactory = AuthorityFactory::create(self, std::string()); std::vector sqlStatements; @@ -2089,20 +2162,33 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( throw FactoryException( "Cannot insert compound CRS with number of components != 2"); } + + auto allowedAuthoritiesTmp(allowedAuthorities); + allowedAuthoritiesTmp.emplace_back(authName); + for (const auto &component : components) { std::string compAuthName; std::string compCode; - const auto candidates = component->identify(allAuthFactory); - for (const auto &candidate : candidates) { - if (candidate.second == 100) { - const auto &ids = candidate.first->identifiers(); - for (const auto &id : ids) { - compAuthName = *(id->codeSpace()); - compCode = id->code(); + + for (const auto &allowedAuthority : allowedAuthoritiesTmp) { + const auto factory = + AuthorityFactory::create(self, allowedAuthority); + const auto candidates = component->identify(factory); + for (const auto &candidate : candidates) { + if (candidate.second == 100) { + const auto &ids = candidate.first->identifiers(); + for (const auto &id : ids) { + compAuthName = *(id->codeSpace()); + compCode = id->code(); + break; + } + } + if (!compAuthName.empty()) { break; } } } + if (compAuthName.empty()) { compAuthName = authName; if (numericCode) { @@ -2110,8 +2196,9 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( } else { compCode = "COMPONENT_" + code + '_' + toString(counter); } - const auto sqlStatementsTmp = self->getInsertStatementsFor( - component, compAuthName, compCode, numericCode); + const auto sqlStatementsTmp = + self->getInsertStatementsFor(component, compAuthName, compCode, + numericCode, allowedAuthorities); sqlStatements.insert(sqlStatements.end(), sqlStatementsTmp.begin(), sqlStatementsTmp.end()); } @@ -2132,7 +2219,8 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( componentsId[1].first.c_str(), componentsId[1].second.c_str()); appendSql(sqlStatements, sql); - identifyOrInsertUsages(crs, "compound_crs", authName, code, sqlStatements); + identifyOrInsertUsages(crs, "compound_crs", authName, code, + allowedAuthorities, sqlStatements); return sqlStatements; } @@ -2372,29 +2460,41 @@ DatabaseContext::suggestsCodeFor(const common::IdentifiedObjectNNPtr &object, * @param code Code with which the object will be inserted. * @param numericCode Whether intermediate objects that can be created should * use numeric codes (true), or may be alphanumeric (false) + * @param allowedAuthorities Authorities to which intermediate objects are + * allowed to refer to. authName will be implicitly + * added to it. Note that unit, coordinate + * systems, projection methods and parameters will in + * any case be allowed to refer to EPSG. * @throw FactoryException * @since 8.1 */ std::vector DatabaseContext::getInsertStatementsFor( const common::IdentifiedObjectNNPtr &object, const std::string &authName, - const std::string &code, bool numericCode) { + const std::string &code, bool numericCode, + const std::vector &allowedAuthorities) { if (d->memoryDbHandle_ == nullptr) { throw FactoryException( "startInsertStatementsSession() should be invoked first"); } - const auto self = NN_NO_CHECK(d->self_.lock()); - const auto allAuthFactory = AuthorityFactory::create(self, std::string()); const auto crs = util::nn_dynamic_pointer_cast(object); if (crs) { // Check if the object is already known under that code - const auto candidates = crs->identify(allAuthFactory); - for (const auto &candidate : candidates) { - if (candidate.second == 100) { - const auto &ids = candidate.first->identifiers(); - for (const auto &id : ids) { - if (*(id->codeSpace()) == authName && id->code() == code) { - return {}; + const auto self = NN_NO_CHECK(d->self_.lock()); + auto allowedAuthoritiesTmp(allowedAuthorities); + allowedAuthoritiesTmp.emplace_back(authName); + for (const auto &allowedAuthority : allowedAuthoritiesTmp) { + const auto factory = + AuthorityFactory::create(self, allowedAuthority); + const auto candidates = crs->identify(factory); + for (const auto &candidate : candidates) { + if (candidate.second == 100) { + const auto &ids = candidate.first->identifiers(); + for (const auto &id : ids) { + if (*(id->codeSpace()) == authName && + id->code() == code) { + return {}; + } } } } @@ -2404,57 +2504,57 @@ std::vector DatabaseContext::getInsertStatementsFor( if (const auto pm = util::nn_dynamic_pointer_cast(object)) { return d->getInsertStatementsFor(NN_NO_CHECK(pm), authName, code, - numericCode); + numericCode, allowedAuthorities); } else if (const auto ellipsoid = util::nn_dynamic_pointer_cast(object)) { return d->getInsertStatementsFor(NN_NO_CHECK(ellipsoid), authName, code, - numericCode); + numericCode, allowedAuthorities); } else if (const auto geodeticDatum = util::nn_dynamic_pointer_cast( object)) { return d->getInsertStatementsFor(NN_NO_CHECK(geodeticDatum), authName, - code, numericCode); + code, numericCode, allowedAuthorities); } else if (const auto geodCRS = std::dynamic_pointer_cast(crs)) { return d->getInsertStatementsFor(NN_NO_CHECK(geodCRS), authName, code, - numericCode); + numericCode, allowedAuthorities); } else if (const auto projCRS = std::dynamic_pointer_cast(crs)) { return d->getInsertStatementsFor(NN_NO_CHECK(projCRS), authName, code, - numericCode); + numericCode, allowedAuthorities); } else if (const auto verticalDatum = util::nn_dynamic_pointer_cast( object)) { return d->getInsertStatementsFor(NN_NO_CHECK(verticalDatum), authName, - code, numericCode); + code, numericCode, allowedAuthorities); } else if (const auto vertCRS = std::dynamic_pointer_cast(crs)) { return d->getInsertStatementsFor(NN_NO_CHECK(vertCRS), authName, code, - numericCode); + numericCode, allowedAuthorities); } else if (const auto compoundCRS = std::dynamic_pointer_cast(crs)) { return d->getInsertStatementsFor(NN_NO_CHECK(compoundCRS), authName, - code, numericCode); + code, numericCode, allowedAuthorities); } else if (const auto boundCRS = std::dynamic_pointer_cast(crs)) { return getInsertStatementsFor(boundCRS->baseCRS(), authName, code, - numericCode); + numericCode, allowedAuthorities); } else { @@ -3112,7 +3212,8 @@ AuthorityFactoryNNPtr AuthorityFactory::create(const DatabaseContextNNPtr &context, const std::string &authorityName) { const auto getFactory = [&context, &authorityName]() { - for (const auto &knownName : {"EPSG", "ESRI", "PROJ"}) { + for (const auto &knownName : + {metadata::Identifier::EPSG.c_str(), "ESRI", "PROJ"}) { if (ci_equal(authorityName, knownName)) { return AuthorityFactory::nn_make_shared( context, knownName); diff --git a/src/proj.h b/src/proj.h index 616a4065..83975123 100644 --- a/src/proj.h +++ b/src/proj.h @@ -1195,6 +1195,7 @@ PROJ_STRING_LIST PROJ_DLL proj_get_insert_statements(PJ_CONTEXT *ctx, const char *authority, const char *code, int numeric_codes, + const char *const *allowed_authorities, const char *const *options); char PROJ_DLL *proj_suggests_code_for(PJ_CONTEXT *ctx, diff --git a/test/cli/testprojinfo b/test/cli/testprojinfo index e300fc71..a91c307a 100755 --- a/test/cli/testprojinfo +++ b/test/cli/testprojinfo @@ -54,6 +54,10 @@ echo "Testing projinfo \"+proj=merc +lat_ts=5 +datum=WGS84 +type=crs\" --output- $EXE "+proj=merc +lat_ts=5 +datum=WGS84 +type=crs" --output-id HOBU:MY_CRS -o SQL -q >>${OUT} echo "" >>${OUT} +echo "Testing projinfo \"+proj=merc +lat_ts=5 +datum=WGS84 +type=crs\" --output-id HOBU:MY_CRS --authority HOBU -o SQL -q" >> ${OUT} +$EXE "+proj=merc +lat_ts=5 +datum=WGS84 +type=crs" --output-id HOBU:MY_CRS --authority HOBU -o SQL -q >>${OUT} +echo "" >>${OUT} + echo "Testing projinfo -s EPSG:4326 -t EPSG:32631 --single-line" >> ${OUT} $EXE -s EPSG:4326 -t EPSG:32631 --single-line >>${OUT} echo "" >>${OUT} diff --git a/test/cli/testprojinfo_out.dist b/test/cli/testprojinfo_out.dist index c5bae908..89c9dbcd 100644 --- a/test/cli/testprojinfo_out.dist +++ b/test/cli/testprojinfo_out.dist @@ -259,6 +259,18 @@ INSERT INTO usage VALUES('HOBU','USAGE_CONVERSION_MY_CRS','conversion','HOBU','C INSERT INTO projected_crs VALUES('HOBU','MY_CRS','unknown','','EPSG','4400','HOBU','GEODETIC_CRS_MY_CRS','HOBU','CONVERSION_MY_CRS',NULL,0); INSERT INTO usage VALUES('HOBU','USAGE_PROJECTED_CRS_MY_CRS','projected_crs','HOBU','MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); +Testing projinfo "+proj=merc +lat_ts=5 +datum=WGS84 +type=crs" --output-id HOBU:MY_CRS --authority HOBU -o SQL -q +INSERT INTO ellipsoid VALUES('HOBU','ELLPS_GEODETIC_DATUM_GEODETIC_CRS_MY_CRS','WGS 84','','PROJ','EARTH',6378137,'EPSG','9001',298.257223563,NULL,0); +INSERT INTO prime_meridian VALUES('HOBU','PM_GEODETIC_DATUM_GEODETIC_CRS_MY_CRS','Greenwich',0,'EPSG','9122',0); +INSERT INTO geodetic_datum VALUES('HOBU','GEODETIC_DATUM_GEODETIC_CRS_MY_CRS','World Geodetic System 1984','','HOBU','ELLPS_GEODETIC_DATUM_GEODETIC_CRS_MY_CRS','HOBU','PM_GEODETIC_DATUM_GEODETIC_CRS_MY_CRS',NULL,NULL,NULL,0); +INSERT INTO usage VALUES('HOBU','USAGE_GEODETIC_DATUM_GEODETIC_CRS_MY_CRS','geodetic_datum','HOBU','GEODETIC_DATUM_GEODETIC_CRS_MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); +INSERT INTO geodetic_crs VALUES('HOBU','GEODETIC_CRS_MY_CRS','unknown','','geographic 2D','EPSG','6424','HOBU','GEODETIC_DATUM_GEODETIC_CRS_MY_CRS',NULL,0); +INSERT INTO usage VALUES('HOBU','USAGE_GEODETIC_CRS_MY_CRS','geodetic_crs','HOBU','GEODETIC_CRS_MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); +INSERT INTO conversion VALUES('HOBU','CONVERSION_MY_CRS','unknown','','EPSG','9805','Mercator (variant B)','EPSG','8823','Latitude of 1st standard parallel',5,'EPSG','9122','EPSG','8802','Longitude of natural origin',0,'EPSG','9122','EPSG','8806','False easting',0,'EPSG','9001','EPSG','8807','False northing',0,'EPSG','9001',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0); +INSERT INTO usage VALUES('HOBU','USAGE_CONVERSION_MY_CRS','conversion','HOBU','CONVERSION_MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); +INSERT INTO projected_crs VALUES('HOBU','MY_CRS','unknown','','EPSG','4400','HOBU','GEODETIC_CRS_MY_CRS','HOBU','CONVERSION_MY_CRS',NULL,0); +INSERT INTO usage VALUES('HOBU','USAGE_PROJECTED_CRS_MY_CRS','projected_crs','HOBU','MY_CRS','PROJ','EXTENT_UNKNOWN','PROJ','SCOPE_UNKNOWN'); + Testing projinfo -s EPSG:4326 -t EPSG:32631 --single-line Candidate operations found: 1 ------------------------------------- diff --git a/test/unit/test_c_api.cpp b/test/unit/test_c_api.cpp index 120f8627..696e3615 100644 --- a/test/unit/test_c_api.cpp +++ b/test/unit/test_c_api.cpp @@ -5229,7 +5229,7 @@ TEST_F(CApi, proj_crs_is_derived) { // --------------------------------------------------------------------------- -TEST_F(CApi, proj_suggests_code_for) { +TEST_F(CApi, proj_get_insert_statements) { { auto session = proj_insert_object_session_create(nullptr); EXPECT_NE(session, nullptr); @@ -5274,16 +5274,59 @@ TEST_F(CApi, proj_suggests_code_for) { proj_string_destroy(code); } + const auto sizeOfStringList = [](const char *const *list) { + if (list == nullptr) + return -1; + int size = 0; + for (auto iter = list; *iter; ++iter) { + size += 1; + } + return size; + }; + // No session specified: we use a temporary session for (int i = 0; i < 2; i++) { - auto list = proj_get_insert_statements(m_ctxt, nullptr, crs, "HOBU", - "XXXX", false, nullptr); + auto list = proj_get_insert_statements( + m_ctxt, nullptr, crs, "HOBU", "XXXX", false, nullptr, nullptr); ASSERT_NE(list, nullptr); ASSERT_NE(list[0], nullptr); EXPECT_EQ(std::string(list[0]), "INSERT INTO geodetic_datum VALUES('HOBU'," "'GEODETIC_DATUM_XXXX','GDA2020','','EPSG','7019'," "'EPSG','8901',NULL,NULL,NULL,0);"); + EXPECT_EQ(sizeOfStringList(list), 4); + proj_string_list_destroy(list); + } + + // Pass an empty list of allowed authorities + // We cannot reuse the EPSG ellipsoid and prime meridian + { + const char *const allowed_authorities[] = {nullptr}; + auto list = + proj_get_insert_statements(m_ctxt, nullptr, crs, "HOBU", "XXXX", + false, allowed_authorities, nullptr); + EXPECT_EQ(sizeOfStringList(list), 6); + proj_string_list_destroy(list); + } + + // Allow only PROJ + // We cannot reuse the EPSG ellipsoid and prime meridian + { + const char *const allowed_authorities[] = {"PROJ", nullptr}; + auto list = + proj_get_insert_statements(m_ctxt, nullptr, crs, "HOBU", "XXXX", + false, allowed_authorities, nullptr); + EXPECT_EQ(sizeOfStringList(list), 6); + proj_string_list_destroy(list); + } + + // Allow EPSG + { + const char *const allowed_authorities[] = {"EPSG", nullptr}; + auto list = + proj_get_insert_statements(m_ctxt, nullptr, crs, "HOBU", "XXXX", + false, allowed_authorities, nullptr); + EXPECT_EQ(sizeOfStringList(list), 4); proj_string_list_destroy(list); } @@ -5291,8 +5334,8 @@ TEST_F(CApi, proj_suggests_code_for) { EXPECT_NE(session, nullptr); { - auto list = proj_get_insert_statements(m_ctxt, session, crs, "HOBU", - "XXXX", false, nullptr); + auto list = proj_get_insert_statements( + m_ctxt, session, crs, "HOBU", "XXXX", false, nullptr, nullptr); ASSERT_NE(list, nullptr); ASSERT_NE(list[0], nullptr); EXPECT_EQ(std::string(list[0]), @@ -5304,8 +5347,8 @@ TEST_F(CApi, proj_suggests_code_for) { // Object already inserted: return empty list { - auto list = proj_get_insert_statements(m_ctxt, session, crs, "HOBU", - "XXXX", false, nullptr); + auto list = proj_get_insert_statements( + m_ctxt, session, crs, "HOBU", "XXXX", false, nullptr, nullptr); ASSERT_NE(list, nullptr); ASSERT_EQ(list[0], nullptr); proj_string_list_destroy(list); -- cgit v1.2.3 From d618dcc33b5a0783565e15ad36a9fa0b8839fb7b Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 13 Mar 2021 13:17:48 +0100 Subject: SQL export: add publication_date and frame_reference_epoch to datums --- src/iso19111/factory.cpp | 44 ++++++++++++++++++++++++++++++++++++-------- test/unit/test_factory.cpp | 30 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp index d7050bce..a4c5dcba 100644 --- a/src/iso19111/factory.cpp +++ b/src/iso19111/factory.cpp @@ -1825,13 +1825,27 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( } // Insert new record in geodetic_datum table + std::string publicationDate("NULL"); + if (datum->publicationDate().has_value()) { + publicationDate = '\''; + publicationDate += + replaceAll(datum->publicationDate()->toString(), "'", "''"); + publicationDate += '\''; + } + std::string frameReferenceEpoch("NULL"); + const auto dynamicDatum = + dynamic_cast(datum.get()); + if (dynamicDatum) { + frameReferenceEpoch = + toString(dynamicDatum->frameReferenceEpoch().value()); + } const auto sql = formatStatement( "INSERT INTO geodetic_datum VALUES(" - "'%q','%q','%q','%q','%q','%q','%q','%q',NULL,NULL,NULL,0);", + "'%q','%q','%q','%q','%q','%q','%q','%q',%s,%s,NULL,0);", authName.c_str(), code.c_str(), datum->nameStr().c_str(), "", // description ellipsoidAuthName.c_str(), ellipsoidCode.c_str(), pmAuthName.c_str(), - pmCode.c_str()); + pmCode.c_str(), publicationDate.c_str(), frameReferenceEpoch.c_str()); appendSql(sqlStatements, sql); identifyOrInsertUsages(datum, "geodetic_datum", authName, code, @@ -2066,12 +2080,26 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( } // Insert new record in vertical_datum table - const auto sql = formatStatement("INSERT INTO vertical_datum VALUES(" - "'%q','%q','%q','%q',NULL,NULL,NULL,0);", - authName.c_str(), code.c_str(), - datum->nameStr().c_str(), - "" // description - ); + std::string publicationDate("NULL"); + if (datum->publicationDate().has_value()) { + publicationDate = '\''; + publicationDate += + replaceAll(datum->publicationDate()->toString(), "'", "''"); + publicationDate += '\''; + } + std::string frameReferenceEpoch("NULL"); + const auto dynamicDatum = + dynamic_cast(datum.get()); + if (dynamicDatum) { + frameReferenceEpoch = + toString(dynamicDatum->frameReferenceEpoch().value()); + } + const auto sql = formatStatement( + "INSERT INTO vertical_datum VALUES(" + "'%q','%q','%q','%q',%s,%s,NULL,0);", + authName.c_str(), code.c_str(), datum->nameStr().c_str(), + "", // description + publicationDate.c_str(), frameReferenceEpoch.c_str()); appendSql(sqlStatements, sql); identifyOrInsertUsages(datum, "vertical_datum", authName, code, diff --git a/test/unit/test_factory.cpp b/test/unit/test_factory.cpp index 55378134..16aa87a3 100644 --- a/test/unit/test_factory.cpp +++ b/test/unit/test_factory.cpp @@ -4005,6 +4005,36 @@ TEST(factory, objectInsertion) { .empty()); ctxt->stopInsertStatementsSession(); } + + // DynamicGeodeticReferenceFrame + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto datum = AuthorityFactory::create(ctxt, "EPSG") + ->createDatum("1165"); // ITRF2014 + const auto sql = ctxt->getInsertStatementsFor(datum, "HOBU", "XXXX", + false, {"HOBU"}); + const auto datumNew = + AuthorityFactory::create(ctxt, "HOBU")->createDatum("XXXX"); + EXPECT_TRUE(datumNew->isEquivalentTo( + datum.get(), IComparable::Criterion::EQUIVALENT)); + ctxt->stopInsertStatementsSession(); + } + + // DynamicVerticalReferenceFrame + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto datum = AuthorityFactory::create(ctxt, "EPSG") + ->createDatum("1096"); // Norway Normal Null 2000 + const auto sql = ctxt->getInsertStatementsFor(datum, "HOBU", "XXXX", + false, {"HOBU"}); + const auto datumNew = + AuthorityFactory::create(ctxt, "HOBU")->createDatum("XXXX"); + EXPECT_TRUE(datumNew->isEquivalentTo( + datum.get(), IComparable::Criterion::EQUIVALENT)); + ctxt->stopInsertStatementsSession(); + } } } // namespace -- cgit v1.2.3 From 7293d657a658c2f2930326246d739fb7802b1115 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 13 Mar 2021 14:31:29 +0100 Subject: SQL export: add support for DatumEnsemble --- src/iso19111/factory.cpp | 194 ++++++++++++++++++++++++++++++++++++++++++--- test/unit/test_factory.cpp | 46 +++++++++++ 2 files changed, 227 insertions(+), 13 deletions(-) diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp index a4c5dcba..750e9054 100644 --- a/src/iso19111/factory.cpp +++ b/src/iso19111/factory.cpp @@ -373,6 +373,12 @@ struct DatabaseContext::Private { bool numericCode, const std::vector &allowedAuthorities); + std::vector + getInsertStatementsFor(const datum::DatumEnsembleNNPtr &ensemble, + const std::string &authName, const std::string &code, + bool numericCode, + const std::vector &allowedAuthorities); + std::vector getInsertStatementsFor(const crs::GeodeticCRSNNPtr &crs, const std::string &authName, const std::string &code, @@ -1169,11 +1175,18 @@ identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, const std::string &authNameParent, const datum::DatumEnsembleNNPtr &obj, std::string &authName, std::string &code) { - const auto instantiateFunc = [](const AuthorityFactoryNNPtr &authFactory, - const std::string &lCode) { - return util::nn_static_pointer_cast( - authFactory->createDatumEnsemble(lCode, "geodetic_datum")); - }; + const char *type = "geodetic_datum"; + if (!obj->datums().empty() && + dynamic_cast( + obj->datums().front().get())) { + type = "vertical_datum"; + } + const auto instantiateFunc = + [&type](const AuthorityFactoryNNPtr &authFactory, + const std::string &lCode) { + return util::nn_static_pointer_cast( + authFactory->createDatumEnsemble(lCode, type)); + }; identifyFromNameOrCode( dbContext, allowedAuthorities, authNameParent, obj, instantiateFunc, AuthorityFactory::ObjectType::DATUM_ENSEMBLE, authName, code); @@ -1253,6 +1266,28 @@ identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, // --------------------------------------------------------------------------- +static void +identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext, + const std::vector &allowedAuthorities, + const std::string &authNameParent, + const datum::DatumNNPtr &obj, std::string &authName, + std::string &code) { + if (const auto geodeticDatum = + util::nn_dynamic_pointer_cast(obj)) { + identifyFromNameOrCode(dbContext, allowedAuthorities, authNameParent, + NN_NO_CHECK(geodeticDatum), authName, code); + } else if (const auto verticalDatum = + util::nn_dynamic_pointer_cast( + obj)) { + identifyFromNameOrCode(dbContext, allowedAuthorities, authNameParent, + NN_NO_CHECK(verticalDatum), authName, code); + } else { + throw FactoryException("Unhandled type of datum"); + } +} + +// --------------------------------------------------------------------------- + static const char *getCSDatabaseType(const cs::CoordinateSystemNNPtr &obj) { if (dynamic_cast(obj.get())) { return "ellipsoidal"; @@ -1856,6 +1891,115 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( // --------------------------------------------------------------------------- +std::vector DatabaseContext::Private::getInsertStatementsFor( + const datum::DatumEnsembleNNPtr &ensemble, const std::string &authName, + const std::string &code, bool numericCode, + const std::vector &allowedAuthorities) { + const auto self = NN_NO_CHECK(self_.lock()); + + // Check if the object is already known under that code + std::string datumAuthName; + std::string datumCode; + identifyFromNameOrCode(self, allowedAuthorities, authName, ensemble, + datumAuthName, datumCode); + if (datumAuthName == authName && datumCode == code) { + return {}; + } + + std::vector sqlStatements; + + const auto &members = ensemble->datums(); + assert(!members.empty()); + + int counter = 1; + std::vector> membersId; + for (const auto &member : members) { + std::string memberAuthName; + std::string memberCode; + identifyFromNameOrCode(self, allowedAuthorities, authName, member, + memberAuthName, memberCode); + if (memberAuthName.empty()) { + memberAuthName = authName; + if (numericCode) { + memberCode = + self->suggestsCodeFor(member, memberAuthName, true); + } else { + memberCode = "MEMBER_" + toString(counter) + "_OF_" + code; + } + const auto sqlStatementsTmp = + self->getInsertStatementsFor(member, memberAuthName, memberCode, + numericCode, allowedAuthorities); + sqlStatements.insert(sqlStatements.end(), sqlStatementsTmp.begin(), + sqlStatementsTmp.end()); + } + + membersId.emplace_back( + std::pair(memberAuthName, memberCode)); + + ++counter; + } + + const bool isGeodetic = + util::nn_dynamic_pointer_cast( + members.front()) != nullptr; + + // Insert new record in geodetic_datum/vertical_datum table + const double accuracy = + c_locale_stod(ensemble->positionalAccuracy()->value()); + if (isGeodetic) { + const auto firstDatum = + AuthorityFactory::create(self, membersId.front().first) + ->createGeodeticDatum(membersId.front().second); + const auto &ellipsoid = firstDatum->ellipsoid(); + const auto &ellipsoidIds = ellipsoid->identifiers(); + assert(!ellipsoidIds.empty()); + const std::string &ellipsoidAuthName = + *(ellipsoidIds.front()->codeSpace()); + const std::string &ellipsoidCode = ellipsoidIds.front()->code(); + const auto &pm = firstDatum->primeMeridian(); + const auto &pmIds = pm->identifiers(); + assert(!pmIds.empty()); + const std::string &pmAuthName = *(pmIds.front()->codeSpace()); + const std::string &pmCode = pmIds.front()->code(); + const auto sql = formatStatement( + "INSERT INTO geodetic_datum VALUES(" + "'%q','%q','%q','%q','%q','%q','%q','%q',NULL,NULL,%f,0);", + authName.c_str(), code.c_str(), ensemble->nameStr().c_str(), + "", // description + ellipsoidAuthName.c_str(), ellipsoidCode.c_str(), + pmAuthName.c_str(), pmCode.c_str(), accuracy); + appendSql(sqlStatements, sql); + } else { + const auto sql = formatStatement("INSERT INTO vertical_datum VALUES(" + "'%q','%q','%q','%q',NULL,NULL,%f,0);", + authName.c_str(), code.c_str(), + ensemble->nameStr().c_str(), + "", // description + accuracy); + appendSql(sqlStatements, sql); + } + identifyOrInsertUsages(ensemble, + isGeodetic ? "geodetic_datum" : "vertical_datum", + authName, code, allowedAuthorities, sqlStatements); + + const char *tableName = isGeodetic ? "geodetic_datum_ensemble_member" + : "vertical_datum_ensemble_member"; + counter = 1; + for (const auto &authCodePair : membersId) { + const auto sql = formatStatement( + "INSERT INTO %s VALUES(" + "'%q','%q','%q','%q',%d);", + tableName, authName.c_str(), code.c_str(), + authCodePair.first.c_str(), authCodePair.second.c_str(), counter); + appendSql(sqlStatements, sql); + ++counter; + } + + return sqlStatements; +} + +// --------------------------------------------------------------------------- + std::vector DatabaseContext::Private::getInsertStatementsFor( const crs::GeodeticCRSNNPtr &crs, const std::string &authName, const std::string &code, bool numericCode, @@ -1870,11 +2014,20 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( std::string datumCode; const auto &ensemble = crs->datumEnsemble(); if (ensemble) { - identifyFromNameOrCode(self, allowedAuthorities, authName, - NN_NO_CHECK(ensemble), datumAuthName, datumCode); + const auto ensembleNN = NN_NO_CHECK(ensemble); + identifyFromNameOrCode(self, allowedAuthorities, authName, ensembleNN, + datumAuthName, datumCode); if (datumAuthName.empty()) { - throw FactoryException( - "Unhandled yet: insertion of new DatumEnsemble"); + datumAuthName = authName; + if (numericCode) { + datumCode = + self->suggestsCodeFor(ensembleNN, datumAuthName, true); + } else { + datumCode = "GEODETIC_DATUM_" + code; + } + sqlStatements = self->getInsertStatementsFor( + ensembleNN, datumAuthName, datumCode, numericCode, + allowedAuthorities); } } else { const auto &datum = crs->datum(); @@ -2124,11 +2277,20 @@ std::vector DatabaseContext::Private::getInsertStatementsFor( std::string datumCode; const auto &ensemble = crs->datumEnsemble(); if (ensemble) { - identifyFromNameOrCode(self, allowedAuthorities, authName, - NN_NO_CHECK(ensemble), datumAuthName, datumCode); + const auto ensembleNN = NN_NO_CHECK(ensemble); + identifyFromNameOrCode(self, allowedAuthorities, authName, ensembleNN, + datumAuthName, datumCode); if (datumAuthName.empty()) { - throw FactoryException( - "Unhandled yet: insertion of new DatumEnsemble"); + datumAuthName = authName; + if (numericCode) { + datumCode = + self->suggestsCodeFor(ensembleNN, datumAuthName, true); + } else { + datumCode = "VERTICAL_DATUM_" + code; + } + sqlStatements = self->getInsertStatementsFor( + ensembleNN, datumAuthName, datumCode, numericCode, + allowedAuthorities); } } else { const auto &datum = crs->datum(); @@ -2548,6 +2710,12 @@ std::vector DatabaseContext::getInsertStatementsFor( code, numericCode, allowedAuthorities); } + else if (const auto ensemble = + util::nn_dynamic_pointer_cast(object)) { + return d->getInsertStatementsFor(NN_NO_CHECK(ensemble), authName, code, + numericCode, allowedAuthorities); + } + else if (const auto geodCRS = std::dynamic_pointer_cast(crs)) { return d->getInsertStatementsFor(NN_NO_CHECK(geodCRS), authName, code, diff --git a/test/unit/test_factory.cpp b/test/unit/test_factory.cpp index 16aa87a3..5d336d0c 100644 --- a/test/unit/test_factory.cpp +++ b/test/unit/test_factory.cpp @@ -4035,6 +4035,52 @@ TEST(factory, objectInsertion) { datum.get(), IComparable::Criterion::EQUIVALENT)); ctxt->stopInsertStatementsSession(); } + + // geodetic DatumEnsemble, and add members inline + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto ensemble = AuthorityFactory::create(ctxt, "EPSG") + ->createDatumEnsemble("6326"); // WGS84 + const auto sql = ctxt->getInsertStatementsFor(ensemble, "HOBU", "XXXX", + false, {"HOBU"}); + const auto ensembleNew = + AuthorityFactory::create(ctxt, "HOBU")->createDatumEnsemble("XXXX"); + EXPECT_TRUE(ensembleNew->isEquivalentTo( + ensemble.get(), IComparable::Criterion::EQUIVALENT)); + ctxt->stopInsertStatementsSession(); + } + + // geodetic DatumEnsemble, and reference members + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + const auto ensemble = AuthorityFactory::create(ctxt, "EPSG") + ->createDatumEnsemble("6326"); // WGS84 + const auto sql = + ctxt->getInsertStatementsFor(ensemble, "HOBU", "XXXX", false); + const auto ensembleNew = + AuthorityFactory::create(ctxt, "HOBU")->createDatumEnsemble("XXXX"); + EXPECT_TRUE(ensembleNew->isEquivalentTo( + ensemble.get(), IComparable::Criterion::EQUIVALENT)); + ctxt->stopInsertStatementsSession(); + } + + // vertical DatumEnsemble + { + auto ctxt = DatabaseContext::create(); + ctxt->startInsertStatementsSession(); + // British Isles height ensemble + const auto ensemble = + AuthorityFactory::create(ctxt, "EPSG")->createDatumEnsemble("1288"); + const auto sql = ctxt->getInsertStatementsFor(ensemble, "HOBU", "XXXX", + false, {"HOBU"}); + const auto ensembleNew = + AuthorityFactory::create(ctxt, "HOBU")->createDatumEnsemble("XXXX"); + EXPECT_TRUE(ensembleNew->isEquivalentTo( + ensemble.get(), IComparable::Criterion::EQUIVALENT)); + ctxt->stopInsertStatementsSession(); + } } } // namespace -- cgit v1.2.3 From 80ef9cee87be3df8eb293e53a91992d6c19178bb Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 13 Mar 2021 16:29:44 +0100 Subject: Add proj_context_get_database_structure() to dump structure of empty valid auxiliary DB --- scripts/reference_exported_symbols.txt | 1 + src/iso19111/c_api.cpp | 73 ++++++++++++++++-------- src/iso19111/factory.cpp | 100 +++++++++++++++++++++++---------- src/proj.h | 3 + test/unit/test_c_api.cpp | 80 +++++++++++++++++++------- test/unit/test_factory.cpp | 9 ++- 6 files changed, 192 insertions(+), 74 deletions(-) diff --git a/scripts/reference_exported_symbols.txt b/scripts/reference_exported_symbols.txt index aa148762..a4716e96 100644 --- a/scripts/reference_exported_symbols.txt +++ b/scripts/reference_exported_symbols.txt @@ -792,6 +792,7 @@ proj_context_errno proj_context_errno_string proj_context_get_database_metadata proj_context_get_database_path +proj_context_get_database_structure proj_context_get_url_endpoint proj_context_get_use_proj4_init_rules proj_context_get_user_writable_directory diff --git a/src/iso19111/c_api.cpp b/src/iso19111/c_api.cpp index a79b387e..2a655e51 100644 --- a/src/iso19111/c_api.cpp +++ b/src/iso19111/c_api.cpp @@ -105,6 +105,28 @@ static void PROJ_NO_INLINE proj_log_debug(PJ_CONTEXT *ctx, const char *function, // --------------------------------------------------------------------------- +template static PROJ_STRING_LIST to_string_list(T &&set) { + auto ret = new char *[set.size() + 1]; + size_t i = 0; + for (const auto &str : set) { + try { + ret[i] = new char[str.size() + 1]; + } catch (const std::exception &) { + while (--i > 0) { + delete[] ret[i]; + } + delete[] ret; + throw; + } + std::memcpy(ret[i], str.c_str(), str.size() + 1); + i++; + } + ret[i] = nullptr; + return ret; +} + +// --------------------------------------------------------------------------- + void proj_context_delete_cpp_context(struct projCppContext *cppContext) { delete cppContext; } @@ -390,6 +412,35 @@ const char *proj_context_get_database_metadata(PJ_CONTEXT *ctx, // --------------------------------------------------------------------------- +/** \brief Return the database structure + * + * Return SQL statements to run to initiate a new valid auxiliary empty + * database. It contains definitions of tables, views and triggers, as well + * as metadata for the version of the layout of the database. + * + * @param ctx PROJ context, or NULL for default context + * @param options null-terminated list of options, or NULL. None currently. + * @return list of SQL statements (to be freed with proj_string_list_destroy()), + * or NULL in case of error. + * @since 8.1 + */ +PROJ_STRING_LIST +proj_context_get_database_structure(PJ_CONTEXT *ctx, + const char *const *options) { + SANITIZE_CTX(ctx); + (void)options; + try { + auto ret = to_string_list(getDBcontext(ctx)->getDatabaseStructure()); + ctx->safeAutoCloseDbIfNeeded(); + return ret; + } catch (const std::exception &e) { + proj_log_error(ctx, __FUNCTION__, e.what()); + return nullptr; + } +} + +// --------------------------------------------------------------------------- + /** \brief Guess the "dialect" of the WKT string. * * @param ctx PROJ context, or NULL for default context @@ -508,28 +559,6 @@ PJ *proj_create(PJ_CONTEXT *ctx, const char *text) { // --------------------------------------------------------------------------- -template static PROJ_STRING_LIST to_string_list(T &&set) { - auto ret = new char *[set.size() + 1]; - size_t i = 0; - for (const auto &str : set) { - try { - ret[i] = new char[str.size() + 1]; - } catch (const std::exception &) { - while (--i > 0) { - delete[] ret[i]; - } - delete[] ret; - throw; - } - std::memcpy(ret[i], str.c_str(), str.size() + 1); - i++; - } - ret[i] = nullptr; - return ret; -} - -// --------------------------------------------------------------------------- - /** \brief Instantiate an object from a WKT string. * * This function calls osgeo::proj::io::WKTParser::createFromWKT() diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp index 750e9054..af9736e6 100644 --- a/src/iso19111/factory.cpp +++ b/src/iso19111/factory.cpp @@ -274,6 +274,8 @@ struct DatabaseContext::Private { std::vector auxiliaryDatabasePaths_{}; bool close_handle_ = true; sqlite3 *sqlite_handle_{}; + int nLayoutVersionMajor_ = 0; + int nLayoutVersionMinor_ = 0; std::map mapSqlToStatement_{}; PJ_CONTEXT *pjCtxt_ = nullptr; int recLevel_ = 0; @@ -305,7 +307,8 @@ struct DatabaseContext::Private { lru11::Cache> cacheAliasNames_{ CACHE_SIZE}; - void checkDatabaseLayout(); + void checkDatabaseLayout(const std::string &path, + const std::string &dbNamePrefix); static void insertIntoCache(LRUCacheOfObjects &cache, const std::string &code, @@ -709,10 +712,22 @@ void DatabaseContext::Private::open(const std::string &databasePath, // --------------------------------------------------------------------------- -void DatabaseContext::Private::checkDatabaseLayout() { - auto res = run("SELECT key, value FROM metadata WHERE key IN " +void DatabaseContext::Private::checkDatabaseLayout( + const std::string &path, const std::string &dbNamePrefix) { + if (!dbNamePrefix.empty() && run("SELECT 1 FROM " + dbNamePrefix + + "sqlite_master WHERE name = 'metadata'") + .empty()) { + // Accept auxiliary databases without metadata table (sparse DBs) + return; + } + auto res = run("SELECT key, value FROM " + dbNamePrefix + + "metadata WHERE key IN " "('DATABASE.LAYOUT.VERSION.MAJOR', " "'DATABASE.LAYOUT.VERSION.MINOR')"); + if (res.empty() && !dbNamePrefix.empty()) { + // Accept auxiliary databases without layout metadata. + return; + } if (res.size() != 2) { // The database layout of PROJ 7.2 that shipped with EPSG v10.003 is // at the time of writing still compatible of the one we support. @@ -730,36 +745,47 @@ void DatabaseContext::Private::checkDatabaseLayout() { } throw FactoryException( - databasePath_ + - " lacks DATABASE.LAYOUT.VERSION.MAJOR / " - "DATABASE.LAYOUT.VERSION.MINOR " - "metadata. It comes from another PROJ installation."); + path + " lacks DATABASE.LAYOUT.VERSION.MAJOR / " + "DATABASE.LAYOUT.VERSION.MINOR " + "metadata. It comes from another PROJ installation."); } - int nMajor = 0; - int nMinor = 0; + int major = 0; + int minor = 0; for (const auto &row : res) { if (row[0] == "DATABASE.LAYOUT.VERSION.MAJOR") { - nMajor = atoi(row[1].c_str()); + major = atoi(row[1].c_str()); } else if (row[0] == "DATABASE.LAYOUT.VERSION.MINOR") { - nMinor = atoi(row[1].c_str()); + minor = atoi(row[1].c_str()); } } - if (nMajor != DATABASE_LAYOUT_VERSION_MAJOR) { + if (major != DATABASE_LAYOUT_VERSION_MAJOR) { throw FactoryException( - databasePath_ + - " contains DATABASE.LAYOUT.VERSION.MAJOR = " + toString(nMajor) + + path + + " contains DATABASE.LAYOUT.VERSION.MAJOR = " + toString(major) + " whereas " + toString(DATABASE_LAYOUT_VERSION_MAJOR) + " is expected. " "It comes from another PROJ installation."); } - if (nMinor < DATABASE_LAYOUT_VERSION_MINOR) { + if (minor < DATABASE_LAYOUT_VERSION_MINOR) { throw FactoryException( - databasePath_ + - " contains DATABASE.LAYOUT.VERSION.MINOR = " + toString(nMinor) + + path + + " contains DATABASE.LAYOUT.VERSION.MINOR = " + toString(minor) + " whereas a number >= " + toString(DATABASE_LAYOUT_VERSION_MINOR) + " is expected. " "It comes from another PROJ installation."); } + if (dbNamePrefix.empty()) { + nLayoutVersionMajor_ = major; + nLayoutVersionMinor_ = minor; + } else if (nLayoutVersionMajor_ != major || nLayoutVersionMinor_ != minor) { + throw FactoryException( + "Auxiliary database " + path + + " contains a DATABASE.LAYOUT.VERSION = " + toString(major) + '.' + + toString(minor) + + " which is different from the one from the main database " + + databasePath_ + " which is " + toString(nLayoutVersionMajor_) + + '.' + toString(nLayoutVersionMinor_)); + } } // --------------------------------------------------------------------------- @@ -777,17 +803,28 @@ void DatabaseContext::Private::setHandle(sqlite3 *sqlite_handle) { // --------------------------------------------------------------------------- std::vector DatabaseContext::Private::getDatabaseStructure() { - const char *sqls[] = { - "SELECT sql FROM sqlite_master WHERE type = 'table'", - "SELECT sql FROM sqlite_master WHERE type = 'view'", - "SELECT sql FROM sqlite_master WHERE type = 'trigger'"}; + const std::string dbNamePrefix(auxiliaryDatabasePaths_.empty() && + memoryDbForInsertPath_.empty() + ? "" + : "db_0."); + const auto sqlBegin("SELECT sql||';' FROM " + dbNamePrefix + + "sqlite_master WHERE type = "); + const char *const objectTypes[] = {"'table'", "'view'", "'trigger'"}; std::vector res; - for (const auto &sql : sqls) { - auto sqlRes = run(sql); + for (const auto &objectType : objectTypes) { + const auto sqlRes = run(sqlBegin + objectType); for (const auto &row : sqlRes) { res.emplace_back(row[0]); } } + if (nLayoutVersionMajor_ > 0) { + res.emplace_back( + "INSERT INTO metadata VALUES('DATABASE.LAYOUT.VERSION.MAJOR'," + + toString(nLayoutVersionMajor_) + ");"); + res.emplace_back( + "INSERT INTO metadata VALUES('DATABASE.LAYOUT.VERSION.MINOR'," + + toString(nLayoutVersionMinor_) + ");"); + } return res; } @@ -828,13 +865,16 @@ void DatabaseContext::Private::attachExtraDatabases( "' AS db_0"); detach_ = true; int count = 1; - for (const auto &otherDb : auxiliaryDatabasePaths) { + for (const auto &otherDbPath : auxiliaryDatabasePaths) { + const auto attachedDbName("db_" + toString(static_cast(count))); std::string sql = "ATTACH DATABASE '"; - sql += replaceAll(otherDb, "'", "''"); - sql += "' AS db_"; - sql += toString(static_cast(count)); + sql += replaceAll(otherDbPath, "'", "''"); + sql += "' AS "; + sql += attachedDbName; count++; run(sql); + + checkDatabaseLayout(otherDbPath, attachedDbName + '.'); } for (const auto &pair : tableStructure) { @@ -2452,11 +2492,11 @@ DatabaseContext::create(const std::string &databasePath, auto dbCtx = DatabaseContext::nn_make_shared(); auto dbCtxPrivate = dbCtx->getPrivate(); dbCtxPrivate->open(databasePath, ctx); + dbCtxPrivate->checkDatabaseLayout(databasePath, std::string()); if (!auxiliaryDatabasePaths.empty()) { dbCtxPrivate->attachExtraDatabases(auxiliaryDatabasePaths); dbCtxPrivate->auxiliaryDatabasePaths_ = auxiliaryDatabasePaths; } - dbCtxPrivate->checkDatabaseLayout(); dbCtxPrivate->self_ = dbCtx.as_nullable(); return dbCtx; } @@ -2527,6 +2567,9 @@ void DatabaseContext::startInsertStatementsSession() { "stopInsertStatementsSession() is."); } + d->memoryDbForInsertPath_.clear(); + const auto sqlStatements = getDatabaseStructure(); + // Create a in-memory temporary sqlite3 database std::ostringstream buffer; buffer << "file:temp_db_for_insert_statements_"; @@ -2541,7 +2584,6 @@ void DatabaseContext::startInsertStatementsSession() { } // Fill the structure of this database - const auto sqlStatements = getDatabaseStructure(); for (const auto &sql : sqlStatements) { if (sqlite3_exec(d->memoryDbHandle_, sql.c_str(), nullptr, nullptr, nullptr) != SQLITE_OK) { diff --git a/src/proj.h b/src/proj.h index 83975123..63821489 100644 --- a/src/proj.h +++ b/src/proj.h @@ -1044,6 +1044,9 @@ const char PROJ_DLL *proj_context_get_database_path(PJ_CONTEXT *ctx); const char PROJ_DLL *proj_context_get_database_metadata(PJ_CONTEXT* ctx, const char* key); +PROJ_STRING_LIST PROJ_DLL proj_context_get_database_structure( + PJ_CONTEXT* ctx, + const char* const *options); PJ_GUESSED_WKT_DIALECT PROJ_DLL proj_context_guess_wkt_dialect(PJ_CONTEXT *ctx, const char *wkt); diff --git a/test/unit/test_c_api.cpp b/test/unit/test_c_api.cpp index 696e3615..0bdb9287 100644 --- a/test/unit/test_c_api.cpp +++ b/test/unit/test_c_api.cpp @@ -1772,29 +1772,58 @@ TEST_F(CApi, proj_context_set_database_path_null) { // --------------------------------------------------------------------------- -TEST_F(CApi, proj_context_set_database_path_main_memory_one_aux) { +TEST_F(CApi, proj_context_set_database_path_aux) { + + const std::string auxDbName( + "file:proj_test_aux.db?mode=memory&cache=shared"); + + sqlite3 *dbAux = nullptr; + sqlite3_open_v2( + auxDbName.c_str(), &dbAux, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI, nullptr); + ASSERT_TRUE(dbAux != nullptr); + ASSERT_TRUE(sqlite3_exec(dbAux, "BEGIN", nullptr, nullptr, nullptr) == + SQLITE_OK); + { + auto ctxt = DatabaseContext::create(); + const auto dbStructure = ctxt->getDatabaseStructure(); + for (const auto &sql : dbStructure) { + ASSERT_TRUE(sqlite3_exec(dbAux, sql.c_str(), nullptr, nullptr, + nullptr) == SQLITE_OK); + } + } - auto c_path = proj_context_get_database_path(m_ctxt); - ASSERT_TRUE(c_path != nullptr); - std::string path(c_path); - const char *aux_db_list[] = {path.c_str(), nullptr}; - - // This is super exotic and a miracle that it works. :memory: as the - // main DB is empty. The real stuff is in the aux_db_list. No view - // is created in the ':memory:' internal DB, but as there's only one - // aux DB its tables and views can be directly queried... - // If that breaks at some point, that wouldn't be a big issue. - // Keeping that one as I had a hard time figuring out why it worked ! - // The real thing is tested by the C++ - // factory::attachExtraDatabases_auxiliary - EXPECT_TRUE(proj_context_set_database_path(m_ctxt, ":memory:", aux_db_list, - nullptr)); + ASSERT_TRUE(sqlite3_exec( + dbAux, + "INSERT INTO geodetic_crs VALUES('OTHER','OTHER_4326','WGS " + "84',NULL,'geographic 2D','EPSG','6422','EPSG','6326'," + "NULL,0);", + nullptr, nullptr, nullptr) == SQLITE_OK); + ASSERT_TRUE(sqlite3_exec(dbAux, "COMMIT", nullptr, nullptr, nullptr) == + SQLITE_OK); - auto source_crs = proj_create_from_database(m_ctxt, "EPSG", "4326", - PJ_CATEGORY_CRS, false, - nullptr); // WGS84 - ASSERT_NE(source_crs, nullptr); - ObjectKeeper keeper_source_crs(source_crs); + const char *const aux_db_list[] = {auxDbName.c_str(), nullptr}; + + EXPECT_TRUE( + proj_context_set_database_path(m_ctxt, nullptr, aux_db_list, nullptr)); + + sqlite3_close(dbAux); + + { + auto crs = proj_create_from_database(m_ctxt, "EPSG", "4326", + PJ_CATEGORY_CRS, false, + nullptr); // WGS84 + ASSERT_NE(crs, nullptr); + ObjectKeeper keeper_source_crs(crs); + } + + { + auto crs = proj_create_from_database(m_ctxt, "OTHER", "OTHER_4326", + PJ_CATEGORY_CRS, false, + nullptr); // WGS84 + ASSERT_NE(crs, nullptr); + ObjectKeeper keeper_source_crs(crs); + } } // --------------------------------------------------------------------------- @@ -2724,6 +2753,15 @@ TEST_F(CApi, proj_context_get_database_metadata) { // --------------------------------------------------------------------------- +TEST_F(CApi, proj_context_get_database_structure) { + auto list = proj_context_get_database_structure(m_ctxt, nullptr); + ASSERT_NE(list, nullptr); + ASSERT_NE(list[0], nullptr); + proj_string_list_destroy(list); +} + +// --------------------------------------------------------------------------- + TEST_F(CApi, proj_clone) { auto obj = proj_create(m_ctxt, "+proj=longlat"); ObjectKeeper keeper(obj); diff --git a/test/unit/test_factory.cpp b/test/unit/test_factory.cpp index 5d336d0c..e0616baa 100644 --- a/test/unit/test_factory.cpp +++ b/test/unit/test_factory.cpp @@ -2828,10 +2828,12 @@ TEST(factory, attachExtraDatabases_auxiliary) { ASSERT_TRUE(dbAux != nullptr); ASSERT_TRUE(sqlite3_exec(dbAux, "BEGIN", nullptr, nullptr, nullptr) == SQLITE_OK); + + std::vector tableStructureBefore; { auto ctxt = DatabaseContext::create(); - const auto dbStructure = ctxt->getDatabaseStructure(); - for (const auto &sql : dbStructure) { + tableStructureBefore = ctxt->getDatabaseStructure(); + for (const auto &sql : tableStructureBefore) { if (sql.find("CREATE TRIGGER") == std::string::npos) { ASSERT_TRUE(sqlite3_exec(dbAux, sql.c_str(), nullptr, nullptr, nullptr) == SQLITE_OK); @@ -2864,6 +2866,9 @@ TEST(factory, attachExtraDatabases_auxiliary) { auto gcrs = nn_dynamic_pointer_cast(crs); EXPECT_TRUE(gcrs != nullptr); } + + const auto dbStructure = ctxt->getDatabaseStructure(); + EXPECT_EQ(dbStructure, tableStructureBefore); } { -- cgit v1.2.3 From eda2311513a67d274d67f5ae8fb3042d78fe3b96 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 13 Mar 2021 16:48:21 +0100 Subject: projinfo: add a --dump-db-structure switch --- docs/source/apps/projinfo.rst | 25 +++++++++++++++++++++++-- src/apps/projinfo.cpp | 33 ++++++++++++++++++++++++++++++--- test/cli/testprojinfo | 8 ++++++++ test/cli/testprojinfo_out.dist | 13 +++++++++++++ 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/docs/source/apps/projinfo.rst b/docs/source/apps/projinfo.rst index 34df3bca..bb4f831d 100644 --- a/docs/source/apps/projinfo.rst +++ b/docs/source/apps/projinfo.rst @@ -27,11 +27,13 @@ Synopsis | [--boundcrs-to-wgs84] | [--authority name] | [--main-db-path path] [--aux-db-path path]* + | [--dump-db-structure] | [--identify] [--3d] | [--output-id AUTH:CODE] | [--c-ify] [--single-line] - | --searchpaths | --remote-data | {object_definition} | - | {object_reference} | (-s {srs_def} -t {srs_def}) + | --searchpaths | --remote-data | + | --dump-db-structure [{object_definition} | {object_reference}] | + | {object_definition} | {object_reference} | (-s {srs_def} -t {srs_def}) | where {object_definition} or {srs_def} is one of the possibilities accepted @@ -274,6 +276,15 @@ The following control parameters can appear in any order: For example, `+proj=utm +zone=31 +datum=WGS84 +type=crs` will be identified with a likelihood of 70% to EPSG:32631 +.. option:: --dump-db-structure + + .. versionadded:: 8.1 + + Outputs the sequence of SQL statements to create a new empty valid auxiliary + database. This option can be specified as the only switch of the utility. + If also specifying a CRS object and the :option:`--output-id` option, the + definition of the object as SQL statements will be appended. + .. option:: --3d .. versionadded:: 6.3 @@ -479,6 +490,16 @@ Output: # Check that everything works OK projinfo --aux-db-path aux.db HOBU:MY_CRS +or more simply: + +.. code-block:: console + + # Create an auxiliary database with the definition of a custom CRS. + projinfo "+proj=merc +lat_ts=5 +datum=WGS84 +type=crs +title=my_crs" --output-id HOBU:MY_CRS --dump-db-structure | sqlite3 aux.db + + # Check that everything works OK + projinfo --aux-db-path aux.db HOBU:MY_CRS + Output: .. code-block:: sql diff --git a/src/apps/projinfo.cpp b/src/apps/projinfo.cpp index 2334c293..2264a544 100644 --- a/src/apps/projinfo.cpp +++ b/src/apps/projinfo.cpp @@ -108,11 +108,16 @@ static void usage() { << " [--authority name]" << std::endl << " [--main-db-path path] [--aux-db-path path]*" << std::endl + << " [--]" << std::endl << " [--identify] [--3d]" << std::endl << " [--output-id AUTH:CODE]" << std::endl << " [--c-ify] [--single-line]" << std::endl << " --searchpaths | --remote-data |" << std::endl - << " {object_definition} | (-s {srs_def} -t {srs_def})" + << " --dump-db-structure [{object_definition} | " + "{object_reference}] |" + << std::endl + << " {object_definition} | {object_reference} | " + "(-s {srs_def} -t {srs_def})" << std::endl; std::cerr << std::endl; std::cerr << "-o: formats is a comma separated combination of: " @@ -124,7 +129,7 @@ static void usage() { << std::endl; std::cerr << std::endl; std::cerr << "{object_definition} might be a PROJ string, a WKT string, " - " a AUTHORITY:CODE, or urn:ogc:def:OBJECT_TYPE:AUTHORITY::CODE" + "a AUTHORITY:CODE, or urn:ogc:def:OBJECT_TYPE:AUTHORITY::CODE" << std::endl; std::exit(1); } @@ -860,6 +865,7 @@ int main(int argc, char **argv) { bool promoteTo3D = false; double minimumAccuracy = -1; bool outputAll = false; + bool dumpDbStructure = false; for (int i = 1; i < argc; i++) { std::string arg(argv[i]); @@ -1148,6 +1154,8 @@ int main(int argc, char **argv) { } outputOpt.outputAuthName = tokens[0]; outputOpt.outputCode = tokens[1]; + } else if (arg == "--dump-db-structure") { + dumpDbStructure = true; } else if (ci_equal(arg, "--searchpaths")) { #ifdef _WIN32 constexpr char delim = ';'; @@ -1196,6 +1204,13 @@ int main(int argc, char **argv) { std::cerr << "ERROR: --bbox and --area are exclusive" << std::endl; std::exit(1); } + + if (dumpDbStructure && user_string_specified && !outputSwitchSpecified) { + // Implicit settings in --output-db-structure mode + object + outputSwitchSpecified = true; + outputOpt.SQL = true; + outputOpt.quiet = true; + } if (outputOpt.SQL && outputOpt.outputAuthName.empty()) { if (outputAll) { outputOpt.SQL = false; @@ -1215,7 +1230,8 @@ int main(int argc, char **argv) { dbContext = DatabaseContext::create(mainDBPath, auxDBPath).as_nullable(); } catch (const std::exception &e) { - if (!mainDBPath.empty() || !auxDBPath.empty() || !area.empty()) { + if (!mainDBPath.empty() || !auxDBPath.empty() || !area.empty() || + dumpDbStructure) { std::cerr << "ERROR: Cannot create database connection: " << e.what() << std::endl; std::exit(1); @@ -1224,6 +1240,14 @@ int main(int argc, char **argv) { << std::endl; } + if (dumpDbStructure) { + assert(dbContext); + const auto structure = dbContext->getDatabaseStructure(); + for (const auto &sql : structure) { + std::cout << sql << std::endl; + } + } + if (!sourceCRSStr.empty() && targetCRSStr.empty()) { std::cerr << "Source CRS specified, but missing target CRS" << std::endl; @@ -1238,6 +1262,9 @@ int main(int argc, char **argv) { usage(); } } else if (!user_string_specified) { + if (dumpDbStructure) { + std::exit(0); + } std::cerr << "Missing user string" << std::endl; usage(); } diff --git a/test/cli/testprojinfo b/test/cli/testprojinfo index a91c307a..adbebeec 100755 --- a/test/cli/testprojinfo +++ b/test/cli/testprojinfo @@ -288,6 +288,14 @@ echo 'Testing NKG: -s EPSG:7789 -t EPSG:4936 --area EPSG:1080 --summary --hide- $EXE -s EPSG:7789 -t EPSG:4936 --area EPSG:1080 --summary --hide-ballpark >>${OUT} 2>&1 echo "" >>${OUT} +echo "Testing projinfo --dump-db-structure | head -n 5" >> ${OUT} +$EXE --dump-db-structure | head -n 5 >>${OUT} +echo "" >>${OUT} + +echo "Testing projinfo --dump-db-structure --output-id HOBU:XXXX EPSG:4326 | tail -n 4" >> ${OUT} +$EXE --dump-db-structure --output-id HOBU:XXXX EPSG:4326 | tail -n 4 >>${OUT} +echo "" >>${OUT} + # do 'diff' with distribution results echo "diff ${OUT} with testprojinfo_out.dist" diff -u ${OUT} ${TEST_CLI_DIR}/testprojinfo_out.dist diff --git a/test/cli/testprojinfo_out.dist b/test/cli/testprojinfo_out.dist index 89c9dbcd..d4ee5774 100644 --- a/test/cli/testprojinfo_out.dist +++ b/test/cli/testprojinfo_out.dist @@ -1546,3 +1546,16 @@ Candidate operations found: 1 Note: using '--spatial-test intersects' would bring more results (2) NKG:ITRF2014_TO_DK, ITRF2014 to ETRS89(DK), 0.01 m, Denmark - onshore and offshore. +Testing projinfo --dump-db-structure | head -n 5 +CREATE TABLE metadata( + key TEXT NOT NULL PRIMARY KEY CHECK (length(key) >= 1), + value TEXT NOT NULL +); +CREATE TABLE unit_of_measure( + +Testing projinfo --dump-db-structure --output-id HOBU:XXXX EPSG:4326 | tail -n 4 +INSERT INTO metadata VALUES('DATABASE.LAYOUT.VERSION.MAJOR',1); +INSERT INTO metadata VALUES('DATABASE.LAYOUT.VERSION.MINOR',0); +INSERT INTO geodetic_crs VALUES('HOBU','XXXX','WGS 84','','geographic 2D','EPSG','6422','EPSG','6326',NULL,0); +INSERT INTO usage VALUES('HOBU','USAGE_GEODETIC_CRS_XXXX','geodetic_crs','HOBU','XXXX','EPSG','1262','EPSG','1183'); + -- cgit v1.2.3 From 72f4f49bf3a2f95e4c2f4571205aa0925f20449a Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 15 Mar 2021 15:37:44 +0100 Subject: Add support for PROJ_AUX_DB environment variable to set the path to one or several auxiliary DBs --- docs/source/usage/environmentvars.rst | 12 ++++++++++++ src/ctx.cpp | 9 ++++++--- src/iso19111/factory.cpp | 23 ++++++++++++++++++++--- test/cli/testprojinfo | 9 +++++++++ test/cli/testprojinfo_out.dist | 31 +++++++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 6 deletions(-) diff --git a/docs/source/usage/environmentvars.rst b/docs/source/usage/environmentvars.rst index 0717a9fa..304b9276 100644 --- a/docs/source/usage/environmentvars.rst +++ b/docs/source/usage/environmentvars.rst @@ -51,6 +51,18 @@ done by setting the variable with no content:: :envvar:`PROJ_LIB` to allow for multiple versions of PROJ resource files on your system without conflicting. + +.. envvar:: PROJ_AUX_DB + + .. versionadded:: 8.1.0 + + To set the path to one or several auxiliary SQLite3 databases of structure + identical to the main ``proj.db`` database and that can contain additional + object (CRS, transformation, ...) definitions. If several paths are + provided, they must be separated by the colon (:) character on Unix, and + on Windows, by the semi-colon (;) character. + + .. envvar:: PROJ_DEBUG Set the debug level of PROJ. The default debug level is zero, which results diff --git a/src/ctx.cpp b/src/ctx.cpp index 097633ae..cc9df6c3 100644 --- a/src/ctx.cpp +++ b/src/ctx.cpp @@ -91,13 +91,16 @@ pj_ctx pj_ctx::createDefault() ctx.logger = pj_stderr_logger; NS_PROJ::FileManager::fillDefaultNetworkInterface(&ctx); - if( getenv("PROJ_DEBUG") != nullptr ) + const char* projDebug = getenv("PROJ_DEBUG"); + if( projDebug != nullptr ) { - if( atoi(getenv("PROJ_DEBUG")) >= -PJ_LOG_TRACE ) - ctx.debug_level = atoi(getenv("PROJ_DEBUG")); + const int debugLevel = atoi(projDebug); + if( debugLevel >= -PJ_LOG_TRACE ) + ctx.debug_level = debugLevel; else ctx.debug_level = PJ_LOG_TRACE; } + return ctx; } diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp index af9736e6..421cdb88 100644 --- a/src/iso19111/factory.cpp +++ b/src/iso19111/factory.cpp @@ -2482,6 +2482,11 @@ DatabaseContext::DatabaseContext() : d(internal::make_unique()) {} * string for the default rules to locate the default proj.db * @param auxiliaryDatabasePaths Path and filename of auxiliary databases. * Might be empty. + * Starting with PROJ 8.1, if this parameter is an empty array, + * the PROJ_AUX_DB environment variable will be used, if set. + * It must contain one or several paths. If several paths are + * provided, they must be separated by the colon (:) character on Unix, and + * on Windows, by the semi-colon (;) character. * @param ctx Context used for file search. * @throw FactoryException */ @@ -2493,9 +2498,21 @@ DatabaseContext::create(const std::string &databasePath, auto dbCtxPrivate = dbCtx->getPrivate(); dbCtxPrivate->open(databasePath, ctx); dbCtxPrivate->checkDatabaseLayout(databasePath, std::string()); - if (!auxiliaryDatabasePaths.empty()) { - dbCtxPrivate->attachExtraDatabases(auxiliaryDatabasePaths); - dbCtxPrivate->auxiliaryDatabasePaths_ = auxiliaryDatabasePaths; + auto auxDbs(auxiliaryDatabasePaths); + if (auxDbs.empty()) { + const char *auxDbStr = getenv("PROJ_AUX_DB"); + if (auxDbStr) { +#ifdef _WIN32 + const char *delim = ";"; +#else + const char *delim = ":"; +#endif + auxDbs = split(auxDbStr, delim); + } + } + if (!auxDbs.empty()) { + dbCtxPrivate->attachExtraDatabases(auxDbs); + dbCtxPrivate->auxiliaryDatabasePaths_ = auxDbs; } dbCtxPrivate->self_ = dbCtx.as_nullable(); return dbCtx; diff --git a/test/cli/testprojinfo b/test/cli/testprojinfo index adbebeec..20c14a8e 100755 --- a/test/cli/testprojinfo +++ b/test/cli/testprojinfo @@ -296,6 +296,15 @@ echo "Testing projinfo --dump-db-structure --output-id HOBU:XXXX EPSG:4326 | tai $EXE --dump-db-structure --output-id HOBU:XXXX EPSG:4326 | tail -n 4 >>${OUT} echo "" >>${OUT} +echo "Testing PROJ_AUX_DB environment variable" >> ${OUT} +rm -f tmp_projinfo_aux.db +$EXE --dump-db-structure --output-id HOBU:XXXX EPSG:4326 | sqlite3 tmp_projinfo_aux.db +export PROJ_AUX_DB=tmp_projinfo_aux.db +$EXE HOBU:XXXX >>${OUT} +unset PROJ_AUX_DB +rm -f tmp_projinfo_aux.db +echo "" >>${OUT} + # do 'diff' with distribution results echo "diff ${OUT} with testprojinfo_out.dist" diff -u ${OUT} ${TEST_CLI_DIR}/testprojinfo_out.dist diff --git a/test/cli/testprojinfo_out.dist b/test/cli/testprojinfo_out.dist index d4ee5774..8137b513 100644 --- a/test/cli/testprojinfo_out.dist +++ b/test/cli/testprojinfo_out.dist @@ -1559,3 +1559,34 @@ INSERT INTO metadata VALUES('DATABASE.LAYOUT.VERSION.MINOR',0); INSERT INTO geodetic_crs VALUES('HOBU','XXXX','WGS 84','','geographic 2D','EPSG','6422','EPSG','6326',NULL,0); INSERT INTO usage VALUES('HOBU','USAGE_GEODETIC_CRS_XXXX','geodetic_crs','HOBU','XXXX','EPSG','1262','EPSG','1183'); +Testing PROJ_AUX_DB environment variable +PROJ.4 string: ++proj=longlat +datum=WGS84 +no_defs +type=crs + +WKT2:2019 string: +GEOGCRS["WGS 84", + ENSEMBLE["World Geodetic System 1984 ensemble", + MEMBER["World Geodetic System 1984 (Transit)"], + MEMBER["World Geodetic System 1984 (G730)"], + MEMBER["World Geodetic System 1984 (G873)"], + MEMBER["World Geodetic System 1984 (G1150)"], + MEMBER["World Geodetic System 1984 (G1674)"], + MEMBER["World Geodetic System 1984 (G1762)"], + ELLIPSOID["WGS 84",6378137,298.257223563, + LENGTHUNIT["metre",1]], + ENSEMBLEACCURACY[2.0]], + PRIMEM["Greenwich",0, + ANGLEUNIT["degree",0.0174532925199433]], + CS[ellipsoidal,2], + AXIS["geodetic latitude (Lat)",north, + ORDER[1], + ANGLEUNIT["degree",0.0174532925199433]], + AXIS["geodetic longitude (Lon)",east, + ORDER[2], + ANGLEUNIT["degree",0.0174532925199433]], + USAGE[ + SCOPE["Horizontal component of 3D system."], + AREA["World."], + BBOX[-90,-180,90,180]], + ID["HOBU","XXXX"]] + -- cgit v1.2.3 From eae06c94dba10640ca2d669c4e7b356a1613f9f3 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 15 Mar 2021 16:28:37 +0100 Subject: Doc improvements regarding recomandation not to alter official registries and use auxiliary databases --- docs/source/apps/projinfo.rst | 16 ++++++++++++++++ src/iso19111/c_api.cpp | 17 +++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/docs/source/apps/projinfo.rst b/docs/source/apps/projinfo.rst index bb4f831d..6c090494 100644 --- a/docs/source/apps/projinfo.rst +++ b/docs/source/apps/projinfo.rst @@ -302,6 +302,20 @@ The following control parameters can appear in any order: Identifier to assign to the object (for SQL output). + It is strongly recommended that new objects should not be added in common + registries, such as ``EPSG``, ``ESRI``, ``IAU``, etc. + Users should use a custom authority name instead. If a new object should be + added to the official EPSG registry, users are invited to follow the + procedure explained at https://epsg.org/dataset-change-requests.html. + + Combined with :option:`--dump-db-structure`, users can create + auxiliary databases, instead of directly modifying the main proj.db database. + See the :ref:`example how to export to an auxiliary database `. + + Those auxiliary databases can be specified through + :cpp:func:`proj_context_set_database_path` or the :envvar:`PROJ_AUX_DB` + environment variable. + .. option:: --c-ify For developers only. Modify the string output of the utility so that it @@ -473,6 +487,8 @@ Output: } } +.. _projinfo_aux_db_example: + 4. Exporting the SQL statements to insert a new CRS in an auxiliary database. .. code-block:: console diff --git a/src/iso19111/c_api.cpp b/src/iso19111/c_api.cpp index 2a655e51..1462dd31 100644 --- a/src/iso19111/c_api.cpp +++ b/src/iso19111/c_api.cpp @@ -310,6 +310,12 @@ void proj_context_set_autoclose_database(PJ_CONTEXT *ctx, int autoclose) { * definition database ("proj.db"), and potentially auxiliary databases with * same structure. * + * Starting with PROJ 8.1, if the auxDbPaths parameter is an empty array, + * the PROJ_AUX_DB environment variable will be used, if set. + * It must contain one or several paths. If several paths are + * provided, they must be separated by the colon (:) character on Unix, and + * on Windows, by the semi-colon (;) character. + * * @param ctx PROJ context, or NULL for default context * @param dbPath Path to main database, or NULL for default. * @param auxDbPaths NULL-terminated list of auxiliary database filenames, or @@ -8899,6 +8905,17 @@ void proj_string_destroy(char *str) { free(str); } * * proj_insert_object_session_create() may have been called previously. * + * It is strongly recommended that new objects should not be added in common + * registries, such as "EPSG", "ESRI", "IAU", etc. Users should use a custom + * authority name instead. If a new object should be + * added to the official EPSG registry, users are invited to follow the + * procedure explainted at https://epsg.org/dataset-change-requests.html. + * + * Combined with proj_context_get_database_structure(), users can create + * auxiliary databases, instead of directly modifying the main proj.db database. + * Those auxiliary databases can be specified through proj_context_set_database_path() + * or the PROJ_AUX_DB environment variable. + * * @param ctx PROJ context, or NULL for default context * @param session The insertion session. May be NULL if a single object must be * inserted. -- cgit v1.2.3