aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEven Rouault <even.rouault@spatialys.com>2021-03-18 14:49:13 +0100
committerGitHub <noreply@github.com>2021-03-18 14:49:13 +0100
commitd4e5e1a833fa37afb5829d9eb9bfbe1472425df4 (patch)
tree1042dd9a9a6934b39a766c3b219ade22b9dd15c7
parent979e3a3ba0b270f7401a57105411375bb99ddbb5 (diff)
parenteae06c94dba10640ca2d669c4e7b356a1613f9f3 (diff)
downloadPROJ-d4e5e1a833fa37afb5829d9eb9bfbe1472425df4.tar.gz
PROJ-d4e5e1a833fa37afb5829d9eb9bfbe1472425df4.zip
Merge pull request #2577 from rouault/insert_sql
Add capability to get SQL statements to add custom CRS in the database
-rw-r--r--data/sql/customizations.sql3
-rw-r--r--docs/source/apps/projinfo.rst136
-rw-r--r--docs/source/usage/environmentvars.rst12
-rw-r--r--include/proj/io.hpp13
-rw-r--r--scripts/reference_exported_symbols.txt10
-rw-r--r--src/apps/projinfo.cpp106
-rw-r--r--src/ctx.cpp9
-rw-r--r--src/iso19111/c_api.cpp325
-rw-r--r--src/iso19111/crs.cpp85
-rw-r--r--src/iso19111/factory.cpp1928
-rw-r--r--src/proj.h28
-rwxr-xr-xtest/cli/testprojinfo25
-rw-r--r--test/cli/testprojinfo_out.dist64
-rw-r--r--test/unit/test_c_api.cpp211
-rw-r--r--test/unit/test_factory.cpp658
15 files changed, 3483 insertions, 130 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/docs/source/apps/projinfo.rst b/docs/source/apps/projinfo.rst
index 826cd244..6c090494 100644
--- a/docs/source/apps/projinfo.rst
+++ b/docs/source/apps/projinfo.rst
@@ -25,11 +25,15 @@ 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]*
+ | [--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
@@ -78,7 +82,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 +91,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
@@ -237,6 +243,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
@@ -260,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
@@ -271,6 +296,26 @@ 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).
+
+ 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 <projinfo_aux_db_example>`.
+
+ 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
@@ -442,6 +487,91 @@ Output:
}
}
+.. _projinfo_aux_db_example:
+
+4. Exporting the SQL statements to insert a new CRS in an auxiliary database.
+
+.. code-block:: console
+
+ # 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
+
+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
+
+ 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','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/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/include/proj/io.hpp b/include/proj/io.hpp
index 2f935a0c..96a97142 100644
--- a/include/proj/io.hpp
+++ b/include/proj/io.hpp
@@ -838,6 +838,19 @@ class PROJ_GCC_DLL DatabaseContext {
PROJ_DLL std::vector<std::string> 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<std::string> getInsertStatementsFor(
+ const common::IdentifiedObjectNNPtr &object,
+ const std::string &authName, const std::string &code, bool numericCode,
+ const std::vector<std::string> &allowedAuthorities = {"EPSG", "PROJ"});
+
+ 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..a4716e96 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<std::shared_ptr<osgeo::proj::common::IdentifiedObject> > const&, std::string const&, std::string const&, bool, std::vector<std::string, std::allocator<std::string> > const&)
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<std::shared_ptr<osgeo::proj::common::IdentifiedObject> > 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&)
@@ -788,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
@@ -960,6 +965,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 +986,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 +1025,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/apps/projinfo.cpp b/src/apps/projinfo.cpp
index 16f2a906..2264a544 100644
--- a/src/apps/projinfo.cpp
+++ b/src/apps/projinfo.cpp
@@ -68,11 +68,15 @@ 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{};
+ std::vector<std::string> allowedAuthorities{};
};
} // anonymous namespace
@@ -101,24 +105,31 @@ 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
+ << " [--]" << 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: "
"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"
<< 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);
}
@@ -310,7 +321,7 @@ static void outputObject(
CoordinateOperationContext::IntermediateCRSUse allowUseIntermediateCRS,
const OutputOptions &outputOpt) {
- auto identified = dynamic_cast<const IdentifiedObject *>(obj.get());
+ auto identified = nn_dynamic_pointer_cast<IdentifiedObject>(obj);
if (!outputOpt.quiet && identified && identified->isDeprecated()) {
std::cout << "Warning: object is deprecated" << std::endl;
auto crs = dynamic_cast<const CRS *>(obj.get());
@@ -556,10 +567,38 @@ 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();
+ 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, allowedAuthorities);
+ 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<CoordinateOperation *>(obj.get());
if (!outputOpt.quiet && op && dbContext &&
getenv("PROJINFO_NO_GRID_CHECK") == nullptr) {
@@ -825,6 +864,8 @@ int main(int argc, char **argv) {
bool showSuperseded = false;
bool promoteTo3D = false;
double minimumAccuracy = -1;
+ bool outputAll = false;
+ bool dumpDbStructure = false;
for (int i = 1; i < argc; i++) {
std::string arg(argv[i]);
@@ -834,12 +875,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 +958,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;
@@ -1084,6 +1131,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") {
@@ -1096,6 +1144,18 @@ 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 (arg == "--dump-db-structure") {
+ dumpDbStructure = true;
} else if (ci_equal(arg, "--searchpaths")) {
#ifdef _WIN32
constexpr char delim = ';';
@@ -1145,12 +1205,33 @@ int main(int argc, char **argv) {
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;
+ 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 {
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);
@@ -1159,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;
@@ -1173,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();
}
@@ -1186,7 +1278,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/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/c_api.cpp b/src/iso19111/c_api.cpp
index 7ffa4aac..78eb4ac5 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 <class T> 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;
}
@@ -288,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
@@ -390,6 +418,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
@@ -523,28 +580,6 @@ PJ *proj_create(PJ_CONTEXT *ctx, const char *text) {
// ---------------------------------------------------------------------------
-template <class T> 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()
@@ -8757,3 +8792,249 @@ 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<IdentifiedObject>(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.
+ *
+ * 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.
+ * @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 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.
+ *
+ * @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 *allowed_authorities, 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<IdentifiedObject>(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);
+ std::vector<std::string> 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, allowedAuthorities);
+ 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..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<GeodeticCRS>(crs),
- nameEqual ? 100 : 90);
- return res;
- } else if (nameEqual && !isEq && !authorityFactory) {
- res.emplace_back(util::nn_static_pointer_cast<GeodeticCRS>(crs),
- 25);
- return res;
- } else if (isEq && !authorityFactory) {
- res.emplace_back(util::nn_static_pointer_cast<GeodeticCRS>(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<GeodeticCRS>(crs),
+ nameEqual ? 100 : 90);
+ return res;
+ } else if (nameEqual && !isEq && !authorityFactory) {
+ res.emplace_back(util::nn_static_pointer_cast<GeodeticCRS>(crs),
+ 25);
+ return res;
+ } else if (isEq && !authorityFactory) {
+ res.emplace_back(util::nn_static_pointer_cast<GeodeticCRS>(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)) {
@@ -4829,7 +4842,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<const ProjectedCRS *>(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 +4969,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..421cdb88 100644
--- a/src/iso19111/factory.cpp
+++ b/src/iso19111/factory.cpp
@@ -49,9 +49,11 @@
#include "sqlite3_utils.hpp"
+#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
+#include <functional>
#include <iomanip>
#include <limits>
#include <locale>
@@ -101,15 +103,20 @@ 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) {}
const Type &type() const { return type_; }
@@ -118,11 +125,15 @@ struct SQLValues {
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,9 +266,16 @@ 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<DatabaseContext> self_{};
+
std::string databasePath_{};
+ std::vector<std::string> auxiliaryDatabasePaths_{};
bool close_handle_ = true;
sqlite3 *sqlite_handle_{};
+ int nLayoutVersionMajor_ = 0;
+ int nLayoutVersionMinor_ = 0;
std::map<std::string, sqlite3_stmt *> mapSqlToStatement_{};
PJ_CONTEXT *pjCtxt_ = nullptr;
int recLevel_ = 0;
@@ -265,6 +283,10 @@ struct DatabaseContext::Private {
std::string lastMetadataValue_{};
std::map<std::string, std::list<SQLRow>> mapCanonicalizeGRFName_{};
+ // Used by startInsertStatementsSession() and related functions
+ std::string memoryDbForInsertPath_{};
+ sqlite3 *memoryDbHandle_ = nullptr;
+
using LRUCacheOfObjects = lru11::Cache<std::string, util::BaseObjectPtr>;
static constexpr size_t CACHE_SIZE = 128;
@@ -285,7 +307,8 @@ struct DatabaseContext::Private {
lru11::Cache<std::string, std::list<std::string>> cacheAliasNames_{
CACHE_SIZE};
- void checkDatabaseLayout();
+ void checkDatabaseLayout(const std::string &path,
+ const std::string &dbNamePrefix);
static void insertIntoCache(LRUCacheOfObjects &cache,
const std::string &code,
@@ -296,9 +319,99 @@ 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<std::string> &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<std::string> &sqlStatements);
+
+ void appendSql(std::vector<std::string> &sqlStatements,
+ const std::string &sql);
+
+ void
+ identifyOrInsertUsages(const common::ObjectUsageNNPtr &obj,
+ const std::string &tableName,
+ const std::string &authName, const std::string &code,
+ const std::vector<std::string> &allowedAuthorities,
+ std::vector<std::string> &sqlStatements);
+
+ std::vector<std::string>
+ getInsertStatementsFor(const datum::PrimeMeridianNNPtr &pm,
+ const std::string &authName, const std::string &code,
+ bool numericCode,
+ const std::vector<std::string> &allowedAuthorities);
+
+ std::vector<std::string>
+ getInsertStatementsFor(const datum::EllipsoidNNPtr &ellipsoid,
+ const std::string &authName, const std::string &code,
+ bool numericCode,
+ const std::vector<std::string> &allowedAuthorities);
+
+ std::vector<std::string>
+ getInsertStatementsFor(const datum::GeodeticReferenceFrameNNPtr &datum,
+ const std::string &authName, const std::string &code,
+ bool numericCode,
+ const std::vector<std::string> &allowedAuthorities);
+
+ std::vector<std::string>
+ getInsertStatementsFor(const datum::DatumEnsembleNNPtr &ensemble,
+ const std::string &authName, const std::string &code,
+ bool numericCode,
+ const std::vector<std::string> &allowedAuthorities);
+
+ std::vector<std::string>
+ getInsertStatementsFor(const crs::GeodeticCRSNNPtr &crs,
+ const std::string &authName, const std::string &code,
+ bool numericCode,
+ const std::vector<std::string> &allowedAuthorities);
+
+ std::vector<std::string>
+ getInsertStatementsFor(const crs::ProjectedCRSNNPtr &crs,
+ const std::string &authName, const std::string &code,
+ bool numericCode,
+ const std::vector<std::string> &allowedAuthorities);
+
+ std::vector<std::string>
+ getInsertStatementsFor(const datum::VerticalReferenceFrameNNPtr &datum,
+ const std::string &authName, const std::string &code,
+ bool numericCode,
+ const std::vector<std::string> &allowedAuthorities);
+
+ std::vector<std::string>
+ getInsertStatementsFor(const crs::VerticalCRSNNPtr &crs,
+ const std::string &authName, const std::string &code,
+ bool numericCode,
+ const std::vector<std::string> &allowedAuthorities);
+
+ std::vector<std::string>
+ getInsertStatementsFor(const crs::CompoundCRSNNPtr &crs,
+ const std::string &authName, const std::string &code,
+ bool numericCode,
+ const std::vector<std::string> &allowedAuthorities);
+
#ifdef ENABLE_CUSTOM_LOCKLESS_VFS
std::unique_ptr<SQLite3VFS> vfs_{};
#endif
@@ -355,6 +468,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) {
@@ -581,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.
@@ -602,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_));
+ }
}
// ---------------------------------------------------------------------------
@@ -649,17 +803,28 @@ void DatabaseContext::Private::setHandle(sqlite3 *sqlite_handle) {
// ---------------------------------------------------------------------------
std::vector<std::string> 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<std::string> 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;
}
@@ -684,6 +849,10 @@ void DatabaseContext::Private::attachExtraDatabases(
}
closeDB();
+ if (auxiliaryDatabasePaths.empty()) {
+ open(databasePath_, pjCtxt());
+ return;
+ }
sqlite3_open_v2(
":memory:", &sqlite_handle_,
@@ -696,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<int>(count)));
std::string sql = "ATTACH DATABASE '";
- sql += replaceAll(otherDb, "'", "''");
- sql += "' AS db_";
- sql += toString(static_cast<int>(count));
+ sql += replaceAll(otherDbPath, "'", "''");
+ sql += "' AS ";
+ sql += attachedDbName;
count++;
run(sql);
+
+ checkDatabaseLayout(otherDbPath, attachedDbName + '.');
}
for (const auto &pair : tableStructure) {
@@ -850,13 +1022,16 @@ SQLResultSet DatabaseContext::Private::run(const std::string &sql,
int nBindField = 1;
for (const auto &param : 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<int>(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 +1044,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 +1093,1379 @@ 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<std::string> &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 std::vector<std::string> &allowedAuthorities,
+ const std::string &authNameParent, const common::IdentifiedObjectNNPtr &obj,
+ std::function<std::shared_ptr<util::IComparable>(
+ 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 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 &) {
+ }
+ }
+
+ 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 std::vector<std::string> &allowedAuthorities,
+ const std::string &authNameParent,
+ const datum::DatumEnsembleNNPtr &obj,
+ std::string &authName, std::string &code) {
+ const char *type = "geodetic_datum";
+ if (!obj->datums().empty() &&
+ dynamic_cast<const datum::VerticalReferenceFrame *>(
+ obj->datums().front().get())) {
+ type = "vertical_datum";
+ }
+ const auto instantiateFunc =
+ [&type](const AuthorityFactoryNNPtr &authFactory,
+ const std::string &lCode) {
+ return util::nn_static_pointer_cast<util::IComparable>(
+ authFactory->createDatumEnsemble(lCode, type));
+ };
+ identifyFromNameOrCode(
+ dbContext, allowedAuthorities, authNameParent, obj, instantiateFunc,
+ AuthorityFactory::ObjectType::DATUM_ENSEMBLE, authName, code);
+}
+
+// ---------------------------------------------------------------------------
+
+static void
+identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext,
+ const std::vector<std::string> &allowedAuthorities,
+ const std::string &authNameParent,
+ 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<util::IComparable>(
+ authFactory->createGeodeticDatum(lCode));
+ };
+ identifyFromNameOrCode(
+ dbContext, allowedAuthorities, authNameParent, obj, instantiateFunc,
+ AuthorityFactory::ObjectType::GEODETIC_REFERENCE_FRAME, authName, code);
+}
+
+// ---------------------------------------------------------------------------
+
+static void
+identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext,
+ const std::vector<std::string> &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<util::IComparable>(
+ authFactory->createEllipsoid(lCode));
+ };
+ identifyFromNameOrCode(
+ dbContext, allowedAuthorities, authNameParent, obj, instantiateFunc,
+ AuthorityFactory::ObjectType::ELLIPSOID, authName, code);
+}
+
+// ---------------------------------------------------------------------------
+
+static void
+identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext,
+ const std::vector<std::string> &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<util::IComparable>(
+ authFactory->createPrimeMeridian(lCode));
+ };
+ identifyFromNameOrCode(
+ dbContext, allowedAuthorities, authNameParent, obj, instantiateFunc,
+ AuthorityFactory::ObjectType::PRIME_MERIDIAN, authName, code);
+}
+
+// ---------------------------------------------------------------------------
+
+static void
+identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext,
+ const std::vector<std::string> &allowedAuthorities,
+ const std::string &authNameParent,
+ 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<util::IComparable>(
+ authFactory->createVerticalDatum(lCode));
+ };
+ identifyFromNameOrCode(
+ dbContext, allowedAuthorities, authNameParent, obj, instantiateFunc,
+ AuthorityFactory::ObjectType::VERTICAL_REFERENCE_FRAME, authName, code);
+}
+
+// ---------------------------------------------------------------------------
+
+static void
+identifyFromNameOrCode(const DatabaseContextNNPtr &dbContext,
+ const std::vector<std::string> &allowedAuthorities,
+ const std::string &authNameParent,
+ const datum::DatumNNPtr &obj, std::string &authName,
+ std::string &code) {
+ if (const auto geodeticDatum =
+ util::nn_dynamic_pointer_cast<datum::GeodeticReferenceFrame>(obj)) {
+ identifyFromNameOrCode(dbContext, allowedAuthorities, authNameParent,
+ NN_NO_CHECK(geodeticDatum), authName, code);
+ } else if (const auto verticalDatum =
+ util::nn_dynamic_pointer_cast<datum::VerticalReferenceFrame>(
+ 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<const cs::EllipsoidalCS *>(obj.get())) {
+ return "ellipsoidal";
+ } else if (dynamic_cast<const cs::CartesianCS *>(obj.get())) {
+ return "Cartesian";
+ } else if (dynamic_cast<const cs::VerticalCS *>(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 = metadata::Identifier::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 = metadata::Identifier::EPSG;
+ code = "9102";
+ return;
+ }
+ break;
+ }
+ case common::UnitOfMeasure::Type::SCALE: {
+ if (convFactor == 1.0) {
+ authName = metadata::Identifier::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<std::string> &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 = metadata::Identifier::EPSG;
+ code = "6499";
+ return;
+ }
+
+ std::string sql(
+ "SELECT auth_name, code FROM coordinate_system WHERE dimension = ?");
+ ListOfParams params{static_cast<int>(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 == metadata::Identifier::EPSG && code == "4400") {
+ // preferred coordinate system for cartesian
+ // Easting, Northing
+ return;
+ }
+ if (authName == metadata::Identifier::EPSG && code == "6422") {
+ // preferred coordinate system for geographic lat, lon
+ return;
+ }
+ if (authName == metadata::Identifier::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<std::string> &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<int>(axisList.size()));
+ appendSql(sqlStatements, sql);
+ }
+
+ // Insert new records for the axis
+ for (int i = 0; i < static_cast<int>(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);
+ }
+}
+
+// ---------------------------------------------------------------------------
+
+static void
+addAllowedAuthoritiesCond(const std::vector<std::string> &allowedAuthorities,
+ const std::string &authName, std::string &sql,
+ ListOfParams &params) {
+ 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<std::string> &allowedAuthorities,
+ std::vector<std::string> &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()) {
+ 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];
+ scopeCode = row[1];
+ } else {
+ scopeAuthName = authName;
+ scopeCode = "SCOPE_" + tableName + "_" + code;
+ const auto sqlToInsert = formatStatement(
+ "INSERT INTO scope VALUES('%q','%q','%q',0);",
+ scopeAuthName.c_str(), scopeCode.c_str(), scope->c_str());
+ appendSql(sqlStatements, sqlToInsert);
+ }
+ } 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<const metadata::GeographicBoundingBox *>(
+ geogElts.front().get());
+ if (bbox) {
+ 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 "
+ "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];
+ extentCode = row[1];
+ } else {
+ extentAuthName = authName;
+ extentCode = "EXTENT_" + tableName + "_" + code;
+ std::string description(*(extent->description()));
+ if (description.empty()) {
+ description = "unknown";
+ }
+ const auto sqlToInsert = 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, sqlToInsert);
+ }
+ }
+ }
+ }
+
+ 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<std::string> DatabaseContext::Private::getInsertStatementsFor(
+ const datum::PrimeMeridianNNPtr &pm, const std::string &authName,
+ const std::string &code, bool /*numericCode*/,
+ const std::vector<std::string> &allowedAuthorities) {
+
+ const auto self = NN_NO_CHECK(self_.lock());
+
+ // Check if the object is already known under that code
+ std::string pmAuthName;
+ std::string pmCode;
+ identifyFromNameOrCode(self, allowedAuthorities, authName, pm, pmAuthName,
+ pmCode);
+ if (pmAuthName == authName && pmCode == code) {
+ return {};
+ }
+
+ std::vector<std::string> 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<std::string> DatabaseContext::Private::getInsertStatementsFor(
+ const datum::EllipsoidNNPtr &ellipsoid, const std::string &authName,
+ const std::string &code, bool /*numericCode*/,
+ const std::vector<std::string> &allowedAuthorities) {
+
+ const auto self = NN_NO_CHECK(self_.lock());
+
+ // Check if the object is already known under that code
+ std::string ellipsoidAuthName;
+ std::string ellipsoidCode;
+ identifyFromNameOrCode(self, allowedAuthorities, authName, ellipsoid,
+ ellipsoidAuthName, ellipsoidCode);
+ if (ellipsoidAuthName == authName && ellipsoidCode == code) {
+ return {};
+ }
+
+ std::vector<std::string> 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<std::string> DatabaseContext::Private::getInsertStatementsFor(
+ const datum::GeodeticReferenceFrameNNPtr &datum,
+ const std::string &authName, const std::string &code, bool numericCode,
+ const std::vector<std::string> &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, datum,
+ datumAuthName, datumCode);
+ if (datumAuthName == authName && datumCode == code) {
+ return {};
+ }
+
+ std::vector<std::string> sqlStatements;
+
+ // Find or insert ellipsoid
+ std::string ellipsoidAuthName;
+ std::string ellipsoidCode;
+ const auto &ellipsoidOfDatum = datum->ellipsoid();
+ identifyFromNameOrCode(self, allowedAuthorities, authName, 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,
+ allowedAuthorities);
+ }
+
+ // Find or insert prime meridian
+ std::string pmAuthName;
+ std::string pmCode;
+ const auto &pmOfDatum = datum->primeMeridian();
+ identifyFromNameOrCode(self, allowedAuthorities, authName, 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, allowedAuthorities);
+ sqlStatements.insert(sqlStatements.end(), sqlStatementsTmp.begin(),
+ sqlStatementsTmp.end());
+ }
+
+ // 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<const datum::DynamicGeodeticReferenceFrame *>(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',%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(), publicationDate.c_str(), frameReferenceEpoch.c_str());
+ appendSql(sqlStatements, sql);
+
+ identifyOrInsertUsages(datum, "geodetic_datum", authName, code,
+ allowedAuthorities, sqlStatements);
+
+ return sqlStatements;
+}
+
+// ---------------------------------------------------------------------------
+
+std::vector<std::string> DatabaseContext::Private::getInsertStatementsFor(
+ const datum::DatumEnsembleNNPtr &ensemble, const std::string &authName,
+ const std::string &code, bool numericCode,
+ const std::vector<std::string> &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<std::string> sqlStatements;
+
+ const auto &members = ensemble->datums();
+ assert(!members.empty());
+
+ int counter = 1;
+ std::vector<std::pair<std::string, std::string>> 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<std::string, std::string>(memberAuthName, memberCode));
+
+ ++counter;
+ }
+
+ const bool isGeodetic =
+ util::nn_dynamic_pointer_cast<datum::GeodeticReferenceFrame>(
+ 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<std::string> DatabaseContext::Private::getInsertStatementsFor(
+ const crs::GeodeticCRSNNPtr &crs, const std::string &authName,
+ const std::string &code, bool numericCode,
+ const std::vector<std::string> &allowedAuthorities) {
+
+ const auto self = NN_NO_CHECK(self_.lock());
+
+ std::vector<std::string> sqlStatements;
+
+ // Find or insert datum/datum ensemble
+ std::string datumAuthName;
+ std::string datumCode;
+ const auto &ensemble = crs->datumEnsemble();
+ if (ensemble) {
+ const auto ensembleNN = NN_NO_CHECK(ensemble);
+ identifyFromNameOrCode(self, allowedAuthorities, authName, ensembleNN,
+ datumAuthName, datumCode);
+ if (datumAuthName.empty()) {
+ 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();
+ assert(datum);
+ const auto datumNN = NN_NO_CHECK(datum);
+ identifyFromNameOrCode(self, allowedAuthorities, authName, 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, allowedAuthorities);
+ }
+ }
+
+ // 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<const crs::GeographicCRS *>(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,
+ allowedAuthorities, sqlStatements);
+ return sqlStatements;
+}
+
+// ---------------------------------------------------------------------------
+
+std::vector<std::string> DatabaseContext::Private::getInsertStatementsFor(
+ const crs::ProjectedCRSNNPtr &crs, const std::string &authName,
+ const std::string &code, bool numericCode,
+ const std::vector<std::string> &allowedAuthorities) {
+
+ const auto self = NN_NO_CHECK(self_.lock());
+
+ std::vector<std::string> sqlStatements;
+
+ // Find or insert base geodetic CRS
+ const auto &baseCRS = crs->baseCRS();
+ std::string geodAuthName;
+ std::string geodCode;
+
+ 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;
+ }
+ }
+ }
+ if (geodAuthName.empty()) {
+ geodAuthName = authName;
+ geodCode = "GEODETIC_CRS_" + code;
+ sqlStatements = self->getInsertStatementsFor(
+ baseCRS, geodAuthName, geodCode, numericCode, allowedAuthorities);
+ }
+
+ // 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<int>(N_MAX_PARAMS)) +
+ " parameters");
+ }
+ for (const auto &genOpParamvalue : values) {
+ auto opParamValue =
+ dynamic_cast<const operation::OperationParameterValue *>(
+ genOpParamvalue.get());
+ if (!opParamValue) {
+ throw FactoryException("Cannot insert projection with "
+ "non-OperationParameterValue");
+ }
+ const auto &param = opParamValue->parameter();
+ const auto &paramIds = param->identifiers();
+ if (paramIds.empty()) {
+ throw FactoryException(
+ "Cannot insert projection with method parameter "
+ "without identifier");
+ }
+ const auto &paramId = paramIds.front();
+ const auto &paramAuthName = *(paramId->codeSpace());
+ const auto &paramCode = 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,
+ allowedAuthorities, 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,
+ allowedAuthorities, sqlStatements);
+
+ return sqlStatements;
+}
+
+// ---------------------------------------------------------------------------
+
+std::vector<std::string> DatabaseContext::Private::getInsertStatementsFor(
+ const datum::VerticalReferenceFrameNNPtr &datum,
+ const std::string &authName, const std::string &code,
+ bool /* numericCode */,
+ const std::vector<std::string> &allowedAuthorities) {
+
+ const auto self = NN_NO_CHECK(self_.lock());
+
+ std::vector<std::string> sqlStatements;
+
+ // Check if the object is already known under that code
+ std::string datumAuthName;
+ std::string datumCode;
+ identifyFromNameOrCode(self, allowedAuthorities, authName, datum,
+ datumAuthName, datumCode);
+ if (datumAuthName == authName && datumCode == code) {
+ return {};
+ }
+
+ // Insert new record in vertical_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<const datum::DynamicVerticalReferenceFrame *>(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,
+ allowedAuthorities, sqlStatements);
+
+ return sqlStatements;
+}
+
+// ---------------------------------------------------------------------------
+
+std::vector<std::string> DatabaseContext::Private::getInsertStatementsFor(
+ const crs::VerticalCRSNNPtr &crs, const std::string &authName,
+ const std::string &code, bool numericCode,
+ const std::vector<std::string> &allowedAuthorities) {
+
+ const auto self = NN_NO_CHECK(self_.lock());
+
+ std::vector<std::string> sqlStatements;
+
+ // Find or insert datum/datum ensemble
+ std::string datumAuthName;
+ std::string datumCode;
+ const auto &ensemble = crs->datumEnsemble();
+ if (ensemble) {
+ const auto ensembleNN = NN_NO_CHECK(ensemble);
+ identifyFromNameOrCode(self, allowedAuthorities, authName, ensembleNN,
+ datumAuthName, datumCode);
+ if (datumAuthName.empty()) {
+ 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();
+ assert(datum);
+ const auto datumNN = NN_NO_CHECK(datum);
+ identifyFromNameOrCode(self, allowedAuthorities, authName, 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, allowedAuthorities);
+ }
+ }
+
+ // 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,
+ allowedAuthorities, sqlStatements);
+
+ return sqlStatements;
+}
+
+// ---------------------------------------------------------------------------
+
+std::vector<std::string> DatabaseContext::Private::getInsertStatementsFor(
+ const crs::CompoundCRSNNPtr &crs, const std::string &authName,
+ const std::string &code, bool numericCode,
+ const std::vector<std::string> &allowedAuthorities) {
+
+ const auto self = NN_NO_CHECK(self_.lock());
+
+ std::vector<std::string> sqlStatements;
+
+ int counter = 1;
+ std::vector<std::pair<std::string, std::string>> componentsId;
+ const auto &components = crs->componentReferenceSystems();
+ if (components.size() != 2) {
+ 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;
+
+ 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) {
+ compCode = self->suggestsCodeFor(component, compAuthName, true);
+ } else {
+ compCode = "COMPONENT_" + code + '_' + toString(counter);
+ }
+ const auto sqlStatementsTmp =
+ self->getInsertStatementsFor(component, compAuthName, compCode,
+ numericCode, allowedAuthorities);
+ sqlStatements.insert(sqlStatements.end(), sqlStatementsTmp.begin(),
+ sqlStatementsTmp.end());
+ }
+
+ componentsId.emplace_back(
+ std::pair<std::string, std::string>(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,
+ allowedAuthorities, sqlStatements);
+
+ return sqlStatements;
+}
+
//! @endcond
// ---------------------------------------------------------------------------
//! @cond Doxygen_Suppress
-DatabaseContext::~DatabaseContext() = default;
+DatabaseContext::~DatabaseContext() {
+ try {
+ stopInsertStatementsSession();
+ } catch (const std::exception &) {
+ }
+}
//! @endcond
// ---------------------------------------------------------------------------
@@ -937,6 +2482,11 @@ DatabaseContext::DatabaseContext() : d(internal::make_unique<Private>()) {}
* 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
*/
@@ -945,11 +2495,26 @@ DatabaseContext::create(const std::string &databasePath,
const std::vector<std::string> &auxiliaryDatabasePaths,
PJ_CONTEXT *ctx) {
auto dbCtx = DatabaseContext::nn_make_shared<DatabaseContext>();
- dbCtx->getPrivate()->open(databasePath, ctx);
- if (!auxiliaryDatabasePaths.empty()) {
- dbCtx->getPrivate()->attachExtraDatabases(auxiliaryDatabasePaths);
+ auto dbCtxPrivate = dbCtx->getPrivate();
+ dbCtxPrivate->open(databasePath, ctx);
+ dbCtxPrivate->checkDatabaseLayout(databasePath, std::string());
+ 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;
}
- dbCtx->getPrivate()->checkDatabaseLayout();
+ dbCtxPrivate->self_ = dbCtx.as_nullable();
return dbCtx;
}
@@ -999,6 +2564,279 @@ 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.");
+ }
+
+ d->memoryDbForInsertPath_.clear();
+ const auto sqlStatements = getDatabaseStructure();
+
+ // 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
+ 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<const datum::PrimeMeridian *>(object.get())) {
+ tableName = "prime_meridian";
+ } else if (dynamic_cast<const datum::Ellipsoid *>(object.get())) {
+ tableName = "ellipsoid";
+ } else if (dynamic_cast<const datum::GeodeticReferenceFrame *>(
+ object.get())) {
+ tableName = "geodetic_datum";
+ } else if (dynamic_cast<const datum::VerticalReferenceFrame *>(
+ object.get())) {
+ tableName = "vertical_datum";
+ } else if (const auto ensemble =
+ dynamic_cast<const datum::DatumEnsemble *>(object.get())) {
+ const auto &datums = ensemble->datums();
+ if (!datums.empty() &&
+ dynamic_cast<const datum::GeodeticReferenceFrame *>(
+ datums[0].get())) {
+ tableName = "geodetic_datum";
+ } else {
+ tableName = "vertical_datum";
+ }
+ } else if (const auto boundCRS =
+ dynamic_cast<const crs::BoundCRS *>(object.get())) {
+ return suggestsCodeFor(boundCRS->baseCRS(), authName, numericCode);
+ } else if (dynamic_cast<const crs::CRS *>(object.get())) {
+ tableName = "crs_view";
+ } else if (dynamic_cast<const operation::Conversion *>(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)
+ * @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<std::string> DatabaseContext::getInsertStatementsFor(
+ const common::IdentifiedObjectNNPtr &object, const std::string &authName,
+ const std::string &code, bool numericCode,
+ const std::vector<std::string> &allowedAuthorities) {
+ if (d->memoryDbHandle_ == nullptr) {
+ throw FactoryException(
+ "startInsertStatementsSession() should be invoked first");
+ }
+
+ const auto crs = util::nn_dynamic_pointer_cast<crs::CRS>(object);
+ if (crs) {
+ // Check if the object is already known under that code
+ 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 {};
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (const auto pm =
+ util::nn_dynamic_pointer_cast<datum::PrimeMeridian>(object)) {
+ return d->getInsertStatementsFor(NN_NO_CHECK(pm), authName, code,
+ numericCode, allowedAuthorities);
+ }
+
+ else if (const auto ellipsoid =
+ util::nn_dynamic_pointer_cast<datum::Ellipsoid>(object)) {
+ return d->getInsertStatementsFor(NN_NO_CHECK(ellipsoid), authName, code,
+ numericCode, allowedAuthorities);
+ }
+
+ else if (const auto geodeticDatum =
+ util::nn_dynamic_pointer_cast<datum::GeodeticReferenceFrame>(
+ object)) {
+ return d->getInsertStatementsFor(NN_NO_CHECK(geodeticDatum), authName,
+ code, numericCode, allowedAuthorities);
+ }
+
+ else if (const auto ensemble =
+ util::nn_dynamic_pointer_cast<datum::DatumEnsemble>(object)) {
+ return d->getInsertStatementsFor(NN_NO_CHECK(ensemble), authName, code,
+ numericCode, allowedAuthorities);
+ }
+
+ else if (const auto geodCRS =
+ std::dynamic_pointer_cast<crs::GeodeticCRS>(crs)) {
+ return d->getInsertStatementsFor(NN_NO_CHECK(geodCRS), authName, code,
+ numericCode, allowedAuthorities);
+ }
+
+ else if (const auto projCRS =
+ std::dynamic_pointer_cast<crs::ProjectedCRS>(crs)) {
+ return d->getInsertStatementsFor(NN_NO_CHECK(projCRS), authName, code,
+ numericCode, allowedAuthorities);
+ }
+
+ else if (const auto verticalDatum =
+ util::nn_dynamic_pointer_cast<datum::VerticalReferenceFrame>(
+ object)) {
+ return d->getInsertStatementsFor(NN_NO_CHECK(verticalDatum), authName,
+ code, numericCode, allowedAuthorities);
+ }
+
+ else if (const auto vertCRS =
+ std::dynamic_pointer_cast<crs::VerticalCRS>(crs)) {
+ return d->getInsertStatementsFor(NN_NO_CHECK(vertCRS), authName, code,
+ numericCode, allowedAuthorities);
+ }
+
+ else if (const auto compoundCRS =
+ std::dynamic_pointer_cast<crs::CompoundCRS>(crs)) {
+ return d->getInsertStatementsFor(NN_NO_CHECK(compoundCRS), authName,
+ code, numericCode, allowedAuthorities);
+ }
+
+ else if (const auto boundCRS =
+ std::dynamic_pointer_cast<crs::BoundCRS>(crs)) {
+ return getInsertStatementsFor(boundCRS->baseCRS(), authName, code,
+ numericCode, allowedAuthorities);
+ }
+
+ 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 +3356,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<ObjectDomainNNPtr> usages;
@@ -1624,7 +3467,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<AuthorityFactory>(
context, knownName);
@@ -2802,8 +4646,7 @@ AuthorityFactory::createConversion(const std::string &code) const {
const size_t base_param_idx = idx;
std::vector<operation::OperationParameterNNPtr> parameters;
std::vector<operation::ParameterValueNNPtr> 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 &param_auth_name = row[base_param_idx + i * 6 + 0];
if (param_auth_name.empty()) {
break;
@@ -3523,8 +5366,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 +5405,7 @@ operation::CoordinateOperationNNPtr AuthorityFactory::createCoordinateOperation(
const size_t base_param_idx = idx;
std::vector<operation::OperationParameterNNPtr> parameters;
std::vector<operation::ParameterValueNNPtr> values;
- for (int i = 0; i < N_MAX_PARAMS; ++i) {
+ for (size_t i = 0; i < N_MAX_PARAMS; ++i) {
const auto &param_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 bcc0cccb..ef6d5841 100644
--- a/src/proj.h
+++ b/src/proj.h
@@ -1056,6 +1056,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);
@@ -1192,8 +1195,33 @@ 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 *allowed_authorities,
+ 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/cli/testprojinfo b/test/cli/testprojinfo
index 1e964a5b..20c14a8e 100755
--- a/test/cli/testprojinfo
+++ b/test/cli/testprojinfo
@@ -50,6 +50,14 @@ 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 \"+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}
@@ -280,6 +288,23 @@ 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}
+
+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 920374d9..8137b513 100644
--- a/test/cli/testprojinfo_out.dist
+++ b/test/cli/testprojinfo_out.dist
@@ -251,6 +251,26 @@ 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 "+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
-------------------------------------
@@ -1526,3 +1546,47 @@ 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');
+
+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"]]
+
diff --git a/test/unit/test_c_api.cpp b/test/unit/test_c_api.cpp
index 20fb0583..b624e063 100644
--- a/test/unit/test_c_api.cpp
+++ b/test/unit/test_c_api.cpp
@@ -1776,29 +1776,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);
+ }
}
// ---------------------------------------------------------------------------
@@ -2728,6 +2757,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);
@@ -5265,4 +5303,135 @@ TEST_F(CApi, proj_crs_is_derived) {
}
}
+// ---------------------------------------------------------------------------
+
+TEST_F(CApi, proj_get_insert_statements) {
+ {
+ 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);
+ }
+
+ 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, 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);
+ }
+
+ 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, 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, 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..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<std::string> 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<GeographicCRS>(crs);
EXPECT_TRUE(gcrs != nullptr);
}
+
+ const auto dbStructure = ctxt->getDatabaseStructure();
+ EXPECT_EQ(dbStructure, tableStructureBefore);
}
{
@@ -3434,4 +3439,653 @@ 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<BaseObject>(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<std::string> scope;
+ scope = "my scope";
+ array->add(ObjectDomain::create(scope, extent));
+ props.set(ObjectUsage::OBJECT_DOMAIN_KEY,
+ nn_static_pointer_cast<BaseObject>(array));
+ const auto crs = GeodeticCRS::create(
+ props, NN_NO_CHECK(GeodeticCRS::EPSG_4978->datum()),
+ NN_NO_CHECK(nn_dynamic_pointer_cast<CartesianCS>(
+ GeodeticCRS::EPSG_4978->coordinateSystem())));
+ const auto boundCRS = BoundCRS::createFromTOWGS84(
+ crs, std::vector<double>{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<std::string>(),
+ 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<std::string>(),
+ 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<std::string>(), 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<std::string>(), 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<std::string>(), 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<std::string>(), 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<CRS>(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();
+ }
+
+ // 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();
+ }
+
+ // 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