aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/Makefile.am7
-rw-r--r--data/tests/tinshift_crs_implicit.json46
-rw-r--r--data/tests/tinshift_simplified_kkj_etrs.json50
-rw-r--r--data/tests/tinshift_simplified_n60_n2000.json50
-rw-r--r--data/triangulation.schema.json169
-rw-r--r--docs/Makefile1
-rw-r--r--docs/source/operations/transformations/index.rst1
-rw-r--r--docs/source/operations/transformations/tinshift.rst201
-rwxr-xr-xscripts/reformat_cpp.sh5
-rw-r--r--src/Makefile.am5
-rw-r--r--src/lib_proj.cmake1
-rw-r--r--src/pj_list.h1
-rw-r--r--src/quadtree.hpp257
-rw-r--r--src/transformations/tinshift.cpp133
-rw-r--r--src/transformations/tinshift.hpp257
-rw-r--r--src/transformations/tinshift_exceptions.hpp52
-rw-r--r--src/transformations/tinshift_impl.hpp555
-rw-r--r--test/CMakeLists.txt1
-rw-r--r--test/gie/Makefile.am8
-rw-r--r--test/gie/tinshift.gie50
-rw-r--r--test/unit/CMakeLists.txt11
-rw-r--r--test/unit/Makefile.am9
-rw-r--r--test/unit/test_tinshift.cpp211
23 files changed, 2077 insertions, 4 deletions
diff --git a/data/Makefile.am b/data/Makefile.am
index b48f53fa..bdfabe0f 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -4,7 +4,8 @@ pkgdata_DATA = proj.ini GL27 nad.lst nad27 nad83 world other.extra \
CH \
ITRF2000 ITRF2008 ITRF2014 proj.db \
projjson.schema.json \
- deformation_model.schema.json
+ deformation_model.schema.json \
+ triangulation.schema.json
SQL_ORDERED_LIST = sql/begin.sql \
sql/proj_db_table_defs.sql \
@@ -45,6 +46,7 @@ EXTRA_DIST = proj.ini GL27 nad.lst nad27 nad83 \
ITRF2000 ITRF2008 ITRF2014 \
projjson.schema.json \
deformation_model.schema.json \
+ triangulation.schema.json \
CMakeLists.txt \
tests/test_nodata.gtx \
tests/test_vgrid_bigendian_bigtiff.tif \
@@ -116,6 +118,9 @@ EXTRA_DIST = proj.ini GL27 nad.lst nad27 nad83 \
tests/simple_model_wrap_west.json \
tests/simple_model_wrap_west.tif \
tests/simple_model_projected.json \
+ tests/tinshift_crs_implicit.json \
+ tests/tinshift_simplified_kkj_etrs.json \
+ tests/tinshift_simplified_n60_n2000.json \
generate_all_sql_in.cmake sql_filelist.cmake \
$(SQL_ORDERED_LIST)
diff --git a/data/tests/tinshift_crs_implicit.json b/data/tests/tinshift_crs_implicit.json
new file mode 100644
index 00000000..e1939fd0
--- /dev/null
+++ b/data/tests/tinshift_crs_implicit.json
@@ -0,0 +1,46 @@
+{
+ "file_type": "triangulation_file",
+ "format_version": "1.0",
+ "name": "Name",
+ "version": "Version",
+ "publication_date": "2018-07-01T00:00:00Z",
+ "license": "Creative Commons Attribution 4.0 International",
+ "description": "Test triangulation",
+ "authority": {
+ "name": "Authority name",
+ "url": "http://example.com",
+ "address": "Adress",
+ "email": "test@example.com"
+ },
+ "links": [
+ {
+ "href": "https://example.com/about.html",
+ "rel": "about",
+ "type": "text/html",
+ "title": "About"
+ },
+ {
+ "href": "https://example.com/download",
+ "rel": "source",
+ "type": "application/zip",
+ "title": "Authoritative source"
+ },
+ {
+ "href": "https://creativecommons.org/licenses/by/4.0/",
+ "rel": "license",
+ "type": "text/html",
+ "title": "Creative Commons Attribution 4.0 International license"
+ },
+ {
+ "href": "https://example.com/metadata.xml",
+ "rel": "metadata",
+ "type": "application/xml",
+ "title": " ISO 19115 XML encoded metadata regarding the deformation model"
+ }
+ ],
+ "transformed_components": [ "horizontal" ],
+ "vertices_columns": [ "source_x", "source_y", "target_x", "target_y" ],
+ "triangles_columns": [ "idx_vertex1", "idx_vertex2", "idx_vertex3" ],
+ "vertices": [ [2,49,2.1,49.1], [3,50,3.1,50.1], [2, 50, 2.1,50.1] ],
+ "triangles": [ [0, 1, 2] ]
+}
diff --git a/data/tests/tinshift_simplified_kkj_etrs.json b/data/tests/tinshift_simplified_kkj_etrs.json
new file mode 100644
index 00000000..7d46107f
--- /dev/null
+++ b/data/tests/tinshift_simplified_kkj_etrs.json
@@ -0,0 +1,50 @@
+{
+ "file_type": "triangulation_file",
+ "format_version": "1.0",
+ "name": "Name",
+ "version": "Version",
+ "publication_date": "2018-07-01T00:00:00Z",
+ "license": "Creative Commons Attribution 4.0 International",
+ "description": "Test triangulation",
+ "authority": {
+ "name": "Authority name",
+ "url": "http://example.com",
+ "address": "Adress",
+ "email": "test@example.com"
+ },
+ "links": [
+ {
+ "href": "https://example.com/about.html",
+ "rel": "about",
+ "type": "text/html",
+ "title": "About"
+ },
+ {
+ "href": "https://example.com/download",
+ "rel": "source",
+ "type": "application/zip",
+ "title": "Authoritative source"
+ },
+ {
+ "href": "https://creativecommons.org/licenses/by/4.0/",
+ "rel": "license",
+ "type": "text/html",
+ "title": "Creative Commons Attribution 4.0 International license"
+ },
+ {
+ "href": "https://example.com/metadata.xml",
+ "rel": "metadata",
+ "type": "application/xml",
+ "title": " ISO 19115 XML encoded metadata regarding the triangulation"
+ }
+ ],
+ "input_crs": "EPSG:2393",
+ "output_crs": "EPSG:3067",
+ "transformed_components": [ "horizontal" ],
+ "vertices_columns": [ "source_x", "source_y", "target_x", "target_y" ],
+ "triangles_columns": [ "idx_vertex1", "idx_vertex2", "idx_vertex3" ],
+ "vertices": [ [3244102.707, 6693710.937, 244037.137, 6690900.686],
+ [3205290.722, 6715311.822, 205240.895, 6712492.577],
+ [3218328.492, 6649538.429, 218273.648, 6646745.973] ],
+ "triangles": [ [0, 1, 2] ]
+}
diff --git a/data/tests/tinshift_simplified_n60_n2000.json b/data/tests/tinshift_simplified_n60_n2000.json
new file mode 100644
index 00000000..f6d3d4aa
--- /dev/null
+++ b/data/tests/tinshift_simplified_n60_n2000.json
@@ -0,0 +1,50 @@
+{
+ "file_type": "triangulation_file",
+ "format_version": "1.0",
+ "name": "Name",
+ "version": "Version",
+ "publication_date": "2018-07-01T00:00:00Z",
+ "license": "Creative Commons Attribution 4.0 International",
+ "description": "Test triangulation",
+ "authority": {
+ "name": "Authority name",
+ "url": "http://example.com",
+ "address": "Adress",
+ "email": "test@example.com"
+ },
+ "links": [
+ {
+ "href": "https://example.com/about.html",
+ "rel": "about",
+ "type": "text/html",
+ "title": "About"
+ },
+ {
+ "href": "https://example.com/download",
+ "rel": "source",
+ "type": "application/zip",
+ "title": "Authoritative source"
+ },
+ {
+ "href": "https://creativecommons.org/licenses/by/4.0/",
+ "rel": "license",
+ "type": "text/html",
+ "title": "Creative Commons Attribution 4.0 International license"
+ },
+ {
+ "href": "https://example.com/metadata.xml",
+ "rel": "metadata",
+ "type": "application/xml",
+ "title": " ISO 19115 XML encoded metadata regarding the tirangulation"
+ }
+ ],
+ "input_crs": "EPSG:2393+5717",
+ "output_crs": "EPSG:2393+5941",
+ "transformed_components": [ "vertical" ],
+ "vertices_columns": [ "source_x", "source_y", "source_z", "target_z" ],
+ "triangles_columns": [ "idx_vertex1", "idx_vertex2", "idx_vertex3" ],
+ "vertices": [ [3188607.0, 6688748.0, 23.123, 23.4133],
+ [3184981.0, 6725255.0, 8.044, 8.34499],
+ [3220912.0, 6699508.0, 1.724, 2.0101] ],
+ "triangles": [ [0, 1, 2] ]
+}
diff --git a/data/triangulation.schema.json b/data/triangulation.schema.json
new file mode 100644
index 00000000..ee5cae0c
--- /dev/null
+++ b/data/triangulation.schema.json
@@ -0,0 +1,169 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "Schema for triangulation based transformation",
+ "type": "object",
+ "properties": {
+ "file_type": {
+ "type": "string",
+ "enum": [
+ "triangulation_file"
+ ],
+ "description": "File type. Always \"triangulation_file\""
+ },
+ "format_version": {
+ "type": "string",
+ "enum": [
+ "1.0"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A brief descriptive name of the triangulation"
+ },
+ "version": {
+ "type": "string",
+ "description": "A string identifying the version of the triangulation. The format for specifying version will be defined by the agency responsible for the triangulation"
+ },
+ "publication_date": {
+ "$ref": "#/definitions/datetime",
+ "description": "The date on which this version of the triangulation was published (or possibly the date on which it takes effect?)"
+ },
+ "license": {
+ "type": "string",
+ "description": "License under which the file is published"
+ },
+ "description": {
+ "type": "string",
+ "description": "A text description of the file"
+ },
+ "authority": {
+ "type": "object",
+ "description": "Basic information about the agency responsible for the data set",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the agency"
+ },
+ "url": {
+ "type": "string",
+ "description": "The url of the agency website",
+ "format": "uri"
+ },
+ "address": {
+ "type": "string",
+ "description": "The postal address of the agency"
+ },
+ "email": {
+ "type": "string",
+ "description": "An email contact address for the agency",
+ "format": "email"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "additionalProperties": false
+ },
+ "links": {
+ "type": "array",
+ "description": "Links to related information",
+ "items": {
+ "type": "object",
+ "properties": {
+ "href": {
+ "type": "string",
+ "description": "The URL holding the information",
+ "format": "uri"
+ },
+ "rel": {
+ "type": "string",
+ "description": "The relationship to the dataset. Proposed relationships are:\n- \"about\": a web page for human consumption describing the model\n- \"source\": the authoritative source data from which the triangulation is built.\n- \"metadata\": ISO 19115 XML metadata regarding the triangulation."
+ },
+ "type": {
+ "type": "string",
+ "description": "MIME type"
+ },
+ "title": {
+ "type": "string",
+ "description": "Description of the link"
+ }
+ },
+ "required": [
+ "href"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "input_crs": {
+ "$ref": "#/definitions/crs",
+ "description": "String identifying the CRS of source coordinates in the vertices. Typically \"EPSG:XXXX\". If the transformation is for vertical component, this should be the code for a compound CRS (can be EPSG:XXXX+YYYY where XXXX is the code of the horizontal CRS and YYYY the code of the vertical CRS). For example, for the KKJ->ETRS89 transformation, this is EPSG:2393 (\"KKJ / Finland Uniform Coordinate System\"). The input coordinates are assumed to be passed in the \"normalized for visualisation\" / \"GIS friendly\" order, that is longitude, latitude for geographic coordinates and easting, northing for projected coordinates."
+ },
+ "output_crs": {
+ "$ref": "#/definitions/crs",
+ "description": "String identifying the CRS of target coordinates in the vertices. Typically \"EPSG:XXXX\". If the transformation is for vertical component, this should be the code for a compound CRS (can be EPSG:XXXX+YYYY where XXXX is the code of the horizontal CRS and YYYY the code of the vertical CRS). For example, for the KKJ->ETRS89 transformation, this is EPSG:3067 (\"ETRS89 / TM35FIN(E,N)\"). The output coordinates will be returned in the \"normalized for visualisation\" / \"GIS friendly\" order, that is easting, that is longitude, latitude for geographic coordinates and easting, northing for projected coordinates."
+ },
+ "transformed_components": {
+ "type": "array",
+ "description": "Specify which component of the coordinates are transformed. Either \"horizontal\", \"vertical\" or both",
+ "minItems": 1,
+ "maxItems": 2,
+ "items": {
+ "type": "string",
+ "enum": [
+ "horizontal",
+ "vertical"
+ ]
+ }
+ },
+ "vertices_columns": {
+ "type": "array",
+ "description": "Specify the name of the columns of the rows in the \"vertices\" array. There must be exactly as many elements in \"vertices_columns\" as in a row of \"vertices\". The following names have a special meaning: \"source_x\", \"source_y\", \"target_x\", \"target_y\", \"source_z\", \"target_z\" and \"offset_z\". \"source_x\" and \"source_y\" are compulsory. \"source_x\" is for the source longitude (in degree) or easting. \"source_y\" is for the source latitude (in degree) or northing. \"target_x\" and \"target_y\" are compulsory when \"horizontal\" is specified in \"transformed_components\". (\"source_z\" and \"target_z\") or \"offset_z\" are compulsory when \"vertical\" is specified in \"transformed_components\".",
+ "minItems": 3,
+ "items": {
+ "type": "string"
+ }
+ },
+ "triangles_columns": {
+ "type": "array",
+ "description": "Specify the name of the columns of the rows in the \"triangles\" array. There must be exactly as many elements in \"triangles_columns\" as in a row of \"triangles\". The following names have a special meaning: \"idx_vertex1\", \"idx_vertex2\", \"idx_vertex3\". They are compulsory.",
+ "minItems": 3,
+ "items": {
+ "type": "string"
+ }
+ },
+ "vertices": {
+ "type": "array",
+ "description": "an array whose items are themselves arrays with as many columns as described in \"vertices_columns\"",
+ "items": {
+ "type": "array"
+ }
+ },
+ "triangles": {
+ "type": "array",
+ "description": "an array whose items are themselves arrays with as many columns as described in \"triangles_columns\". The value of the \"idx_vertexN\" columns must be indices (between 0 and len(\"vertices\"-1) of items of the \"vertices\" array",
+ "items": {
+ "type": "array"
+ }
+ }
+ },
+ "required": [
+ "file_type",
+ "format_version",
+ "transformed_components",
+ "vertices_columns",
+ "triangles_columns",
+ "vertices",
+ "triangles"
+ ],
+ "additionalProperties": false,
+ "definitions": {
+ "crs": {
+ "type": "string"
+ },
+ "datetime": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"
+ }
+ }
+}
diff --git a/docs/Makefile b/docs/Makefile
index c0801a9b..a63498ab 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -66,6 +66,7 @@ html: .doxygen_up_to_date
@sed "s/<em class=\"property\">namespace <\/em>//g" < $(BUILDDIR)/html/development/reference/cpp/cpp_general.html > $(BUILDDIR)/html/development/reference/cpp/cpp_general.html.tmp
@mv $(BUILDDIR)/html/development/reference/cpp/cpp_general.html.tmp $(BUILDDIR)/html/development/reference/cpp/cpp_general.html
@cp -r ../schemas $(BUILDDIR)/html/schemas
+ @cp ../data/triangulation.schema.json $(BUILDDIR)/html/schemas
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
diff --git a/docs/source/operations/transformations/index.rst b/docs/source/operations/transformations/index.rst
index 9f7900f1..a413b8f6 100644
--- a/docs/source/operations/transformations/index.rst
+++ b/docs/source/operations/transformations/index.rst
@@ -19,5 +19,6 @@ systems are based on different datums.
molodensky
molobadekas
hgridshift
+ tinshift
vgridshift
xyzgridshift
diff --git a/docs/source/operations/transformations/tinshift.rst b/docs/source/operations/transformations/tinshift.rst
new file mode 100644
index 00000000..c29d233a
--- /dev/null
+++ b/docs/source/operations/transformations/tinshift.rst
@@ -0,0 +1,201 @@
+.. _tinshift:
+
+================================================================================
+Triangulated Irregular Network based transformation
+================================================================================
+
+.. versionadded:: 7.2.0
+
++---------------------+--------------------------------------------------------------------+
+| **Alias** | tinshift |
++---------------------+--------------------------------------------------------------------+
+| **Input type** | Projected or geographic coordinates (horizontal), meters (vertical)|
++---------------------+--------------------------------------------------------------------+
+| **Output type** | Projected or geographic coordinates (horizontal), meters (vertical)|
++---------------------+--------------------------------------------------------------------+
+| **Domain** | 2D or 3D |
++---------------------+--------------------------------------------------------------------+
+| **Available forms** | Forward and inverse |
++---------------------+--------------------------------------------------------------------+
+
+
+The ``tinshift`` transformation takes one mandatory
+argument, ``file``, that points to a JSON file, which contains the
+triangulation and associated metadata. Input and output coordinates must be
+geographic or projected coordinates.
+Depending on the content of the JSON file, horizontal, vertical or both
+components of the coordinates may be transformed.
+
+The transformation is invertible, with the same computational complexity than
+the forward transformation.
+
+Parameters
+-------------------------------------------------------------------------------
+
+Required
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+.. option:: +file=<filename>
+
+ Filename to the JSON file for the TIN.
+
+
+Example
+-------------------------------------------------------------------------------
+
+Transformating a point with the Finland EPSG:2393 ("KKJ / Finland Uniform
+Coordinate System") projected CRS to EPSG:3067 ("ETRS89 / TM35FIN(E,N)")
+
+::
+
+ $ echo 3210000.0000 6700000.0000 0 2020 | cct +proj=tinshift +file=./triangulation_kkj.json
+
+ 209948.3217 6697187.0009 0.0000 2020
+
+
+Algorithm
++++++++++
+
+Internally, ``tinshift`` ingest the whole file into memory. It is considered that
+triangulation should be small enough for that.
+
+When a point is transformed, one must find the triangle into which it falls into.
+Instead of iterating over all triangles, we build a in-memory quadtree to speed-up
+the identification of candidates triangles.
+
+To determine if a point falls into a triangle, one computes its 3
+`barycentric coordinates <https://en.wikipedia.org/wiki/Barycentric_coordinate_system#Conversion_between_barycentric_and_Cartesian_coordinates>`_
+from its projected coordinates, :math:`\lambda_i` for :math:`i=1,2,3`.
+They are real values (in the [0,1] range for a point inside the triangle),
+giving the weight of each of the 3 vertices of the triangles.
+
+Once those weights are known, interpolating the target horizontal
+coordinate is a matter of doing the linear combination of those weights with
+the target horizontal coordinates at the 3 vertices of the triangle (:math:`Xt_i` and :math:`Yt_i`):
+
+.. math::
+
+ X_{target} = Xt_1 * \lambda_1 + Xt_2 * \lambda_2 + Xt_3 * \lambda_3
+
+ Y_{target} = Yt_1 * \lambda_1 + Yt_2 * \lambda_2 + Yt_3 * \lambda_3
+
+This interpolation is exact at the vertices of the triangulation, and has linear properties
+inside each triangle. It is completely equivalent to other formulations of
+triangular interpolation, such as
+
+.. math::
+
+ X_{target} = A + X_{source} * B + Y_{source} * C
+
+ Y_{target} = D + X_{source} * E + Y_{source} * F
+
+where the A, B, C, D, E, F constants (for a given triangle) are found by solving
+the 2 systems of 3 linear equations, constraint by the source and target coordinate pairs
+of the 3 vertices of the triangle:
+
+.. math::
+
+ Xt_i = A + Xs_i * B + Ys_i * C
+
+ Yt_i = D + Xs_i * E + Ys_i * F
+
+Similarly for a vertical coordinate transformation, where :math:`Zoff_i` is the vertical
+offset at each vertex of the triangle:
+
+.. math::
+
+ Z_{target} = Z_{source} + Zoff_1 * \lambda_1 + Zoff_2 * \lambda_2 + Zoff_3 * \lambda_3
+
+Constraints on the triangulation
+++++++++++++++++++++++++++++++++
+
+No check is done on the consistence of the triangulation. It is highly
+recommended that triangles do not overlap each other (when considering the
+source coordinates or the forward transformation, or the target coordinates for
+the inverse transformation), otherwise which triangle will be selected is
+unspecified. Besides that, the triangulation does not need to have particular
+properties (like being a Delaunay triangulation)
+
+File format
++++++++++++
+
+The triangulation is stored in a text-based format, using JSON as a serialization.
+
+Below a minimal example, from the KKJ to ETRS89 transformation, with just a
+single triangle:
+
+.. literalinclude:: ../../../../data/tests/tinshift_crs_implicit.json
+ :language: json
+
+
+So after the generic metadata, we define the input and output CRS (informative
+only), and that the transformation affects horizontal components of
+coordinates. We name the columns of the ``vertices`` and ``triangles`` arrays.
+We defined the source and target coordinates of each vertex, and define a
+triangle by referring to the index of its vertices in the ``vertices`` array.
+
+More formally, the specific items for the triangulation file are:
+
+input_crs
+ String identifying the CRS of source coordinates
+ in the vertices. Typically ``EPSG:XXXX``. If the transformation is for vertical
+ component, this should be the code for a compound CRS (can be EPSG:XXXX+YYYY
+ where XXXX is the code of the horizontal CRS and YYYY the code of the vertical CRS).
+ For example, for the KKJ->ETRS89 transformation, this is EPSG:2393
+ (``KKJ / Finland Uniform Coordinate System``). The input coordinates are assumed
+ to be passed in the "normalized for visualisation" / "GIS friendly" order,
+ that is longitude, latitude for geographic coordinates and
+ easting, northing for projected coordinates.
+
+
+output_crs
+ String identifying the CRS of target coordinates in the vertices.
+ Typically ``EPSG:XXXX``. If the transformation is for vertical component,
+ this should be the code for a compound CRS (can be EPSG:XXXX+YYYY where
+ XXXX is the code of the horizontal CRS and YYYY the code of the vertical CRS).
+ For example, for the KKJ->ETRS89 transformation, this is EPSG:3067
+ (\"ETRS89 / TM35FIN(E,N)\"). The output coordinates will be returned in
+ the "normalized for visualisation" / "GIS friendly" order,
+ that is longitude, latitude for geographic coordinates and
+ easting, northing for projected coordinates.
+
+
+transformed_components
+ Array which may contain one or two strings: "horizontal" when horizontal
+ components of the coordinates are transformed and/or "vertical" when the
+ vertical component is transformed.
+
+
+vertices_columns
+ Specify the name of the columns of the rows in the ``vertices``
+ array. There must be exactly as many elements in ``vertices_columns`` as in a
+ row of ``vertices``. The following names have a special meaning: ``source_x``,
+ ``source_y``, ``target_x``, ``target_y``, ``source_z``, ``target_z`` and
+ ``offset_z``. ``source_x`` and ``source_y`` are compulsory.
+ ``source_x`` is for the source longitude (in degree) or easting.
+ ``source_y`` is for the source latitude (in degree) or northing.
+ ``target_x`` and ``target_y`` are compulsory when ``horizontal`` is specified
+ in ``transformed_components``. (``source_z`` and ``target_z``) or
+ ``offset_z`` are compulsory when ``vertical`` is specified in ``transformed_components``
+
+
+triangles_columns
+ Specify the name of the columns of the rows in the
+ ``triangles`` array. There must be exactly as many elements in ``triangles_columns``
+ as in a row of ``triangles``. The following names have a special meaning:
+ ``idx_vertex1``, ``idx_vertex2``, ``idx_vertex3``. They are compulsory.
+
+
+vertices
+ An array whose items are themselves arrays with as many columns as
+ described in ``vertices_columns``.
+
+
+triangles
+ An array whose items are themselves arrays with as many columns as
+ described in ``triangles_columns``.
+ The value of the ``idx_vertexN`` columns must be indices
+ (between 0 and len(``vertices``-1) of items of the ``vertices`` array.
+
+A `JSON schema <https://proj.org/schemas/triangulation.schema.json>`_ is available
+for this file format.
diff --git a/scripts/reformat_cpp.sh b/scripts/reformat_cpp.sh
index 0c57daba..50a572a1 100755
--- a/scripts/reformat_cpp.sh
+++ b/scripts/reformat_cpp.sh
@@ -27,6 +27,11 @@ for i in "$TOPDIR"/include/proj/*.hpp "$TOPDIR"/include/proj/internal/*.hpp \
"$TOPDIR"/src/transformations/defmodel_exceptions.hpp \
"$TOPDIR"/src/transformations/defmodel_impl.hpp \
"$TOPDIR"/src/transformations/defmodel.cpp \
+ "$TOPDIR"/src/transformations/tinshift.hpp \
+ "$TOPDIR"/src/transformations/tinshift_exceptions.hpp \
+ "$TOPDIR"/src/transformations/tinshift_impl.hpp \
+ "$TOPDIR"/src/transformations/tinshift.cpp \
+ "$TOPDIR"/src/quadtree.hpp \
; do
if ! echo "$i" | grep -q "lru_cache.hpp"; then
"$SCRIPT_DIR"/reformat.sh "$i";
diff --git a/src/Makefile.am b/src/Makefile.am
index 86444df3..68aeadf2 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -201,6 +201,10 @@ libproj_la_SOURCES = \
transformations/defmodel.hpp \
transformations/defmodel_exceptions.hpp \
transformations/defmodel_impl.hpp \
+ transformations/tinshift.cpp \
+ transformations/tinshift.hpp \
+ transformations/tinshift_exceptions.hpp \
+ transformations/tinshift_impl.hpp \
\
aasincos.cpp adjlon.cpp \
dmstor.cpp auth.cpp \
@@ -213,6 +217,7 @@ libproj_la_SOURCES = \
release.cpp gauss.cpp \
fileapi.cpp \
generic_inverse.cpp \
+ quadtree.hpp \
\
datums.cpp datum_set.cpp transform.cpp \
utils.cpp \
diff --git a/src/lib_proj.cmake b/src/lib_proj.cmake
index 3d41fe9a..1cd7f421 100644
--- a/src/lib_proj.cmake
+++ b/src/lib_proj.cmake
@@ -186,6 +186,7 @@ set(SRC_LIBPROJ_TRANSFORMATIONS
transformations/vgridshift.cpp
transformations/xyzgridshift.cpp
transformations/defmodel.cpp
+ transformations/tinshift.cpp
)
set(SRC_LIBPROJ_ISO19111
diff --git a/src/pj_list.h b/src/pj_list.h
index c7e6d209..5b91af9b 100644
--- a/src/pj_list.h
+++ b/src/pj_list.h
@@ -150,6 +150,7 @@ PROJ_HEAD(gstmerc, "Gauss-Schreiber Transverse Mercator (aka Gauss-Laborde Reuni
PROJ_HEAD(tcc, "Transverse Central Cylindrical")
PROJ_HEAD(tcea, "Transverse Cylindrical Equal Area")
PROJ_HEAD(times, "Times Projection")
+PROJ_HEAD(tinshift, "Triangulation based transformation")
PROJ_HEAD(tissot, "Tissot Conic")
PROJ_HEAD(tmerc, "Transverse Mercator")
PROJ_HEAD(tobmerc, "Tobler-Mercator")
diff --git a/src/quadtree.hpp b/src/quadtree.hpp
new file mode 100644
index 00000000..b744c83d
--- /dev/null
+++ b/src/quadtree.hpp
@@ -0,0 +1,257 @@
+/******************************************************************************
+ * Project: PROJ
+ * Purpose: Implementation of quadtree building and searching functions.
+ * Derived from shapelib, mapserver and GDAL implementations
+ * Author: Even Rouault, <even.rouault at spatialys.com>
+ *
+ ******************************************************************************
+ * Copyright (c) 1999-2008, Frank Warmerdam
+ * Copyright (c) 2008-2020, Even Rouault <even dot rouault at spatialys.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *****************************************************************************/
+
+#ifndef QUADTREE_HPP
+#define QUADTREE_HPP
+
+#include "proj/util.hpp"
+
+#include <functional>
+#include <vector>
+
+//! @cond Doxygen_Suppress
+
+NS_PROJ_START
+
+namespace QuadTree {
+
+/* -------------------------------------------------------------------- */
+/* If the following is 0.5, psNodes will be split in half. If it */
+/* is 0.6 then each apSubNode will contain 60% of the parent */
+/* psNode, with 20% representing overlap. This can be help to */
+/* prevent small objects on a boundary from shifting too high */
+/* up the hQuadTree. */
+/* -------------------------------------------------------------------- */
+constexpr double DEFAULT_SPLIT_RATIO = 0.55;
+
+/** Describe a rectangle */
+struct RectObj {
+ double minx = 0; /**< Minimum x */
+ double miny = 0; /**< Minimum y */
+ double maxx = 0; /**< Maximum x */
+ double maxy = 0; /**< Maximum y */
+
+ /* Returns whether this rectangle is contained by other */
+ inline bool isContainedBy(const RectObj &other) const {
+ return minx >= other.minx && maxx <= other.maxx && miny >= other.miny &&
+ maxy <= other.maxy;
+ }
+
+ /* Returns whether this rectangles overlaps other */
+ inline bool overlaps(const RectObj &other) const {
+ return minx <= other.maxx && maxx >= other.minx && miny <= other.maxy &&
+ maxy >= other.miny;
+ }
+
+ /* Returns whether this rectangles contains the specified point */
+ inline bool contains(double x, double y) const {
+ return minx <= x && maxx >= x && miny <= y && maxy >= y;
+ }
+
+ /* Return whether this rectangles is different from other */
+ inline bool operator!=(const RectObj &other) const {
+ return minx != other.minx || miny != other.miny || maxx != other.maxx ||
+ maxy != other.maxy;
+ }
+};
+
+/** Quadtree */
+template <class Feature> class QuadTree {
+
+ struct Node {
+ RectObj rect{}; /* area covered by this psNode */
+
+ /* list of shapes stored at this node. */
+ std::vector<std::pair<Feature, RectObj>> features{};
+
+ std::vector<Node> subnodes{};
+
+ explicit Node(const RectObj &rectIn) : rect(rectIn) {}
+ };
+
+ Node root{};
+ unsigned nBucketCapacity = 8;
+ double dfSplitRatio = DEFAULT_SPLIT_RATIO;
+
+ public:
+ /** Construct a new quadtree with the global bounds of all objects to be
+ * inserted */
+ explicit QuadTree(const RectObj &globalBounds) : root(globalBounds) {}
+
+ /** Add a new feature, with its bounds specified in featureBounds */
+ void insert(const Feature &feature, const RectObj &featureBounds) {
+ insert(root, feature, featureBounds);
+ }
+
+#ifdef UNUSED
+ /** Retrieve all features whose bounds intersects aoiRect */
+ void
+ search(const RectObj &aoiRect,
+ std::vector<std::reference_wrapper<const Feature>> &features) const {
+ search(root, aoiRect, features);
+ }
+#endif
+
+ /** Retrieve all features whose bounds contains (x,y) */
+ void search(double x, double y, std::vector<Feature> &features) const {
+ search(root, x, y, features);
+ }
+
+ private:
+ void splitBounds(const RectObj &in, RectObj &out1, RectObj &out2) {
+ // The output bounds will be very similar to the input bounds,
+ // so just copy over to start.
+ out1 = in;
+ out2 = in;
+
+ // Split in X direction.
+ if ((in.maxx - in.minx) > (in.maxy - in.miny)) {
+ const double range = in.maxx - in.minx;
+
+ out1.maxx = in.minx + range * dfSplitRatio;
+ out2.minx = in.maxx - range * dfSplitRatio;
+ }
+
+ // Otherwise split in Y direction.
+ else {
+ const double range = in.maxy - in.miny;
+
+ out1.maxy = in.miny + range * dfSplitRatio;
+ out2.miny = in.maxy - range * dfSplitRatio;
+ }
+ }
+
+ void insert(Node &node, const Feature &feature,
+ const RectObj &featureBounds) {
+ if (node.subnodes.empty()) {
+ // If we have reached the max bucket capacity, try to insert
+ // in a subnode if possible.
+ if (node.features.size() >= nBucketCapacity) {
+ RectObj half1;
+ RectObj half2;
+ RectObj quad1;
+ RectObj quad2;
+ RectObj quad3;
+ RectObj quad4;
+
+ splitBounds(node.rect, half1, half2);
+ splitBounds(half1, quad1, quad2);
+ splitBounds(half2, quad3, quad4);
+
+ if (node.rect != quad1 && node.rect != quad2 &&
+ node.rect != quad3 && node.rect != quad4 &&
+ (featureBounds.isContainedBy(quad1) ||
+ featureBounds.isContainedBy(quad2) ||
+ featureBounds.isContainedBy(quad3) ||
+ featureBounds.isContainedBy(quad4))) {
+ node.subnodes.reserve(4);
+ node.subnodes.emplace_back(Node(quad1));
+ node.subnodes.emplace_back(Node(quad2));
+ node.subnodes.emplace_back(Node(quad3));
+ node.subnodes.emplace_back(Node(quad4));
+
+ auto features = std::move(node.features);
+ node.features.clear();
+ for (auto &pair : features) {
+ insert(node, pair.first, pair.second);
+ }
+
+ /* recurse back on this psNode now that it has apSubNodes */
+ insert(node, feature, featureBounds);
+ return;
+ }
+ }
+ } else {
+ // If we have sub nodes, then consider whether this object will
+ // fit in them.
+ for (auto &subnode : node.subnodes) {
+ if (featureBounds.isContainedBy(subnode.rect)) {
+ insert(subnode, feature, featureBounds);
+ return;
+ }
+ }
+ }
+
+ // If none of that worked, just add it to this nodes list.
+ node.features.push_back(
+ std::pair<Feature, RectObj>(feature, featureBounds));
+ }
+
+#ifdef UNUSED
+ void
+ search(const Node &node, const RectObj &aoiRect,
+ std::vector<std::reference_wrapper<const Feature>> &features) const {
+ // Does this node overlap the area of interest at all? If not,
+ // return without adding to the list at all.
+ if (!node.rect.overlaps(aoiRect))
+ return;
+
+ // Add the local features to the list.
+ for (const auto &pair : node.features) {
+ if (pair.second.overlaps(aoiRect)) {
+ features.push_back(
+ std::reference_wrapper<const Feature>(pair.first));
+ }
+ }
+
+ // Recurse to subnodes if they exist.
+ for (const auto &subnode : node.subnodes) {
+ search(subnode, aoiRect, features);
+ }
+ }
+#endif
+
+ void search(const Node &node, double x, double y,
+ std::vector<Feature> &features) const {
+ // Does this node overlap the area of interest at all? If not,
+ // return without adding to the list at all.
+ if (!node.rect.contains(x, y))
+ return;
+
+ // Add the local features to the list.
+ for (const auto &pair : node.features) {
+ if (pair.second.contains(x, y)) {
+ features.push_back(pair.first);
+ }
+ }
+
+ // Recurse to subnodes if they exist.
+ for (const auto &subnode : node.subnodes) {
+ search(subnode, x, y, features);
+ }
+ }
+};
+
+} // namespace QuadTree
+
+NS_PROJ_END
+
+//! @endcond
+
+#endif // QUADTREE_HPP
diff --git a/src/transformations/tinshift.cpp b/src/transformations/tinshift.cpp
new file mode 100644
index 00000000..96e0ea4f
--- /dev/null
+++ b/src/transformations/tinshift.cpp
@@ -0,0 +1,133 @@
+/******************************************************************************
+ * Project: PROJ
+ * Purpose: Functionality related to triangulation transformation
+ * Author: Even Rouault, <even.rouault at spatialys.com>
+ *
+ ******************************************************************************
+ * Copyright (c) 2020, Even Rouault, <even.rouault at spatialys.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *****************************************************************************/
+
+#define PJ_LIB__
+#define PROJ_COMPILATION
+
+#include "tinshift.hpp"
+#include "filemanager.hpp"
+#include "proj_internal.h"
+
+PROJ_HEAD(tinshift, "Triangulation based transformation");
+
+using namespace TINSHIFT_NAMESPACE;
+
+namespace {
+
+struct tinshiftData {
+ std::unique_ptr<Evaluator> evaluator{};
+
+ tinshiftData() = default;
+
+ tinshiftData(const tinshiftData &) = delete;
+ tinshiftData &operator=(const tinshiftData &) = delete;
+};
+
+} // namespace
+
+static PJ *destructor(PJ *P, int errlev) {
+ if (nullptr == P)
+ return nullptr;
+
+ auto Q = static_cast<struct tinshiftData *>(P->opaque);
+ delete Q;
+ P->opaque = nullptr;
+
+ return pj_default_destructor(P, errlev);
+}
+
+static PJ_COORD tinshift_forward_4d(PJ_COORD in, PJ *P) {
+ auto *Q = (struct tinshiftData *)P->opaque;
+
+ PJ_COORD out = in;
+ if (!Q->evaluator->forward(in.xyz.x, in.xyz.y, in.xyz.z, out.xyz.x,
+ out.xyz.y, out.xyz.z)) {
+ return proj_coord_error();
+ }
+ return out;
+}
+
+static PJ_COORD tinshift_reverse_4d(PJ_COORD in, PJ *P) {
+ auto *Q = (struct tinshiftData *)P->opaque;
+
+ PJ_COORD out = in;
+ if (!Q->evaluator->inverse(in.xyz.x, in.xyz.y, in.xyz.z, out.xyz.x,
+ out.xyz.y, out.xyz.z)) {
+ return proj_coord_error();
+ }
+ return out;
+}
+
+PJ *TRANSFORMATION(tinshift, 1) {
+
+ const char *filename = pj_param(P->ctx, P->params, "sfile").s;
+ if (!filename) {
+ proj_log_error(P, "tinshift: +file= should be specified.");
+ return destructor(P, PJD_ERR_NO_ARGS);
+ }
+
+ auto file = NS_PROJ::FileManager::open_resource_file(P->ctx, filename);
+ if (nullptr == file) {
+ proj_log_error(P, "tinshift: Cannot open %s", filename);
+ return destructor(P, PJD_ERR_INVALID_ARG);
+ }
+ file->seek(0, SEEK_END);
+ unsigned long long size = file->tell();
+ // Arbitrary threshold to avoid ingesting an arbitrarily large JSON file,
+ // that could be a denial of service risk. 10 MB should be sufficiently
+ // large for any valid use !
+ if (size > 10 * 1024 * 1024) {
+ proj_log_error(P, "tinshift: File %s too large", filename);
+ return destructor(P, PJD_ERR_INVALID_ARG);
+ }
+ file->seek(0);
+ std::string jsonStr;
+ jsonStr.resize(static_cast<size_t>(size));
+ if (file->read(&jsonStr[0], jsonStr.size()) != jsonStr.size()) {
+ proj_log_error(P, "tinshift: Cannot read %s", filename);
+ return destructor(P, PJD_ERR_INVALID_ARG);
+ }
+
+ auto Q = new tinshiftData();
+ P->opaque = (void *)Q;
+ P->destructor = destructor;
+
+ try {
+ Q->evaluator.reset(new Evaluator(TINShiftFile::parse(jsonStr)));
+ } catch (const std::exception &e) {
+ proj_log_error(P, "tinshift: invalid model: %s", e.what());
+ return destructor(P, PJD_ERR_INVALID_ARG);
+ }
+
+ P->destructor = destructor;
+ P->fwd4d = tinshift_forward_4d;
+ P->inv4d = tinshift_reverse_4d;
+ P->left = PJ_IO_UNITS_WHATEVER;
+ P->right = PJ_IO_UNITS_WHATEVER;
+
+ return P;
+}
diff --git a/src/transformations/tinshift.hpp b/src/transformations/tinshift.hpp
new file mode 100644
index 00000000..7cebab9f
--- /dev/null
+++ b/src/transformations/tinshift.hpp
@@ -0,0 +1,257 @@
+/******************************************************************************
+ * Project: PROJ
+ * Purpose: Functionality related to TIN based transformations
+ * Author: Even Rouault, <even.rouault at spatialys.com>
+ *
+ ******************************************************************************
+ * Copyright (c) 2020, Even Rouault, <even.rouault at spatialys.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *****************************************************************************/
+
+#ifndef TINSHIFT_HPP
+#define TINSHIFT_HPP
+
+#ifdef PROJ_COMPILATION
+#include "proj/internal/include_nlohmann_json.hpp"
+#else
+#include "nlohmann/json.hpp"
+#endif
+
+#include <algorithm>
+#include <cmath>
+#include <exception>
+#include <functional>
+#include <limits>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "quadtree.hpp"
+
+#ifndef TINSHIFT_NAMESPACE
+#define TINSHIFT_NAMESPACE TINShift
+#endif
+
+#include "tinshift_exceptions.hpp"
+
+namespace TINSHIFT_NAMESPACE {
+
+using json = nlohmann::json;
+
+// ---------------------------------------------------------------------------
+
+/** Content of a TINShift file. */
+class TINShiftFile {
+ public:
+ /** Parse the provided serialized JSON content and return an object.
+ *
+ * @throws ParsingException
+ */
+ static std::unique_ptr<TINShiftFile> parse(const std::string &text);
+
+ /** Get file type. Should always be "triangulation_file" */
+ const std::string &fileType() const { return mFileType; }
+
+ /** Get the version of the format. At time of writing, only "1.0" is known
+ */
+ const std::string &formatVersion() const { return mFormatVersion; }
+
+ /** Get brief descriptive name of the deformation model. */
+ const std::string &name() const { return mName; }
+
+ /** Get a string identifying the version of the deformation model.
+ * The format for specifying version is defined by the agency
+ * responsible for the deformation model. */
+ const std::string &version() const { return mVersion; }
+
+ /** Get a string identifying the license of the file.
+ * e.g "Create Commons Attribution 4.0 International" */
+ const std::string &license() const { return mLicense; }
+
+ /** Get a text description of the model. Intended to be longer than name()
+ */
+ const std::string &description() const { return mDescription; }
+
+ /** Get a text description of the model. Intended to be longer than name()
+ */
+ const std::string &publicationDate() const { return mPublicationDate; }
+
+ /** Basic information on the agency responsible for the model. */
+ struct Authority {
+ std::string name{};
+ std::string url{};
+ std::string address{};
+ std::string email{};
+ };
+
+ /** Get basic information on the agency responsible for the model. */
+ const Authority &authority() const { return mAuthority; }
+
+ /** Hyperlink related to the model. */
+ struct Link {
+ /** URL holding the information */
+ std::string href{};
+
+ /** Relationship to the dataset. e.g. "about", "source", "license",
+ * "metadata" */
+ std::string rel{};
+
+ /** Mime type */
+ std::string type{};
+
+ /** Description of the link */
+ std::string title{};
+ };
+
+ /** Get links to related information. */
+ const std::vector<Link> links() const { return mLinks; }
+
+ /** Get a string identifying the CRS of source coordinates in the
+ * vertices. Typically "EPSG:XXXX". If the transformation is for vertical
+ * component, this should be the code for a compound CRS (can be
+ * EPSG:XXXX+YYYY where XXXX is the code of the horizontal CRS and YYYY
+ * the code of the vertical CRS).
+ * For example, for the KKJ->ETRS89 transformation, this is EPSG:2393
+ * ("KKJ / Finland Uniform Coordinate System").
+ * The input coordinates are assumed to
+ * be passed in the "normalized for visualisation" / "GIS friendly" order,
+ * that is longitude, latitude for geographic coordinates and
+ * easting, northing for projected coordinates.
+ * This may be empty for unspecified CRS.
+ */
+ const std::string &inputCRS() const { return mInputCRS; }
+
+ /** Get a string identifying the CRS of target coordinates in the
+ * vertices. Typically "EPSG:XXXX". If the transformation is for vertical
+ * component, this should be the code for a compound CRS (can be
+ * EPSG:XXXX+YYYY where XXXX is the code of the horizontal CRS and YYYY
+ * the code of the vertical CRS).
+ * For example, for the KKJ->ETRS89 transformation, this is EPSG:3067
+ * ("ETRS89 / TM35FIN(E,N)").
+ * The output coordinates will be
+ * returned in the "normalized for visualisation" / "GIS friendly" order,
+ * that is longitude, latitude for geographic coordinates and
+ * easting, northing for projected coordinates.
+ * This may be empty for unspecified CRS.
+ */
+ const std::string &outputCRS() const { return mOutputCRS; }
+
+ /** Return whether horizontal coordinates are transformed. */
+ bool transformHorizontalComponent() const {
+ return mTransformHorizontalComponent;
+ }
+
+ /** Return whether vertical coordinates are transformed. */
+ bool transformVerticalComponent() const {
+ return mTransformVerticalComponent;
+ }
+
+ /** Indices of vertices of a triangle */
+ struct VertexIndices {
+ /** Index of first vertex */
+ unsigned idx1;
+ /** Index of second vertex */
+ unsigned idx2;
+ /** Index of third vertex */
+ unsigned idx3;
+ };
+
+ /** Return number of elements per vertex of vertices() */
+ unsigned verticesColumnCount() const { return mVerticesColumnCount; }
+
+ /** Return description of triangulation vertices.
+ * Each vertex is described by verticesColumnCount() consecutive values.
+ * They are respectively:
+ * - the source X value
+ * - the source Y value
+ * - (if transformHorizontalComponent() is true) the target X value
+ * - (if transformHorizontalComponent() is true) the target Y value
+ * - (if transformVerticalComponent() is true) the delta Z value (to go from
+ * source to target Z)
+ *
+ * X is assumed to be a longitude (in degrees) or easting value.
+ * Y is assumed to be a latitude (in degrees) or northing value.
+ */
+ const std::vector<double> &vertices() const { return mVertices; }
+
+ /** Return triangles*/
+ const std::vector<VertexIndices> &triangles() const { return mTriangles; }
+
+ private:
+ TINShiftFile() = default;
+
+ std::string mFileType{};
+ std::string mFormatVersion{};
+ std::string mName{};
+ std::string mVersion{};
+ std::string mLicense{};
+ std::string mDescription{};
+ std::string mPublicationDate{};
+ Authority mAuthority{};
+ std::vector<Link> mLinks{};
+ std::string mInputCRS{};
+ std::string mOutputCRS{};
+ bool mTransformHorizontalComponent = false;
+ bool mTransformVerticalComponent = false;
+ unsigned mVerticesColumnCount = 0;
+ std::vector<double> mVertices{};
+ std::vector<VertexIndices> mTriangles{};
+};
+
+// ---------------------------------------------------------------------------
+
+/** Class to evaluate the transformation of a coordinate */
+class Evaluator {
+ public:
+ /** Constructor. */
+ explicit Evaluator(std::unique_ptr<TINShiftFile> &&fileIn);
+
+ /** Get file */
+ const TINShiftFile &file() const { return *(mFile.get()); }
+
+ /** Evaluate displacement of a position given by (x,y,z,t) and
+ * return it in (x_out,y_out_,z_out).
+ */
+ bool forward(double x, double y, double z, double &x_out, double &y_out,
+ double &z_out);
+
+ /** Apply inverse transformation. */
+ bool inverse(double x, double y, double z, double &x_out, double &y_out,
+ double &z_out);
+
+ private:
+ std::unique_ptr<TINShiftFile> mFile;
+
+ // Reused between invokations to save memory allocations
+ std::vector<unsigned> mTriangleIndices{};
+
+ std::unique_ptr<NS_PROJ::QuadTree::QuadTree<unsigned>> mQuadTreeForward{};
+ std::unique_ptr<NS_PROJ::QuadTree::QuadTree<unsigned>> mQuadTreeInverse{};
+};
+
+// ---------------------------------------------------------------------------
+
+} // namespace TINSHIFT_NAMESPACE
+
+// ---------------------------------------------------------------------------
+
+#include "tinshift_impl.hpp"
+
+#endif // TINSHIFT_HPP
diff --git a/src/transformations/tinshift_exceptions.hpp b/src/transformations/tinshift_exceptions.hpp
new file mode 100644
index 00000000..d6d71302
--- /dev/null
+++ b/src/transformations/tinshift_exceptions.hpp
@@ -0,0 +1,52 @@
+/******************************************************************************
+ * Project: PROJ
+ * Purpose: Functionality related to TIN based transformations
+ * Author: Even Rouault, <even.rouault at spatialys.com>
+ *
+ ******************************************************************************
+ * Copyright (c) 2020, Even Rouault, <even.rouault at spatialys.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *****************************************************************************/
+
+#ifndef TINSHIFT_NAMESPACE
+#error "Should be included only by tinshift.hpp"
+#endif
+
+#include <exception>
+
+namespace TINSHIFT_NAMESPACE {
+
+// ---------------------------------------------------------------------------
+
+/** Parsing exception. */
+class ParsingException : public std::exception {
+ public:
+ explicit ParsingException(const std::string &msg) : msg_(msg) {}
+ const char *what() const noexcept override;
+
+ private:
+ std::string msg_;
+};
+
+const char *ParsingException::what() const noexcept { return msg_.c_str(); }
+
+// ---------------------------------------------------------------------------
+
+} // namespace DEFORMATON_MODEL_NAMESPACE
diff --git a/src/transformations/tinshift_impl.hpp b/src/transformations/tinshift_impl.hpp
new file mode 100644
index 00000000..90f2b2f8
--- /dev/null
+++ b/src/transformations/tinshift_impl.hpp
@@ -0,0 +1,555 @@
+/******************************************************************************
+ * Project: PROJ
+ * Purpose: Functionality related to TIN based transformations
+ * Author: Even Rouault, <even.rouault at spatialys.com>
+ *
+ ******************************************************************************
+ * Copyright (c) 2020, Even Rouault, <even.rouault at spatialys.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *****************************************************************************/
+
+#ifndef TINSHIFT_NAMESPACE
+#error "Should be included only by tinshift.hpp"
+#endif
+
+#include <limits>
+
+namespace TINSHIFT_NAMESPACE {
+
+// ---------------------------------------------------------------------------
+
+static std::string getString(const json &j, const char *key, bool optional) {
+ if (!j.contains(key)) {
+ if (optional) {
+ return std::string();
+ }
+ throw ParsingException(std::string("Missing \"") + key + "\" key");
+ }
+ const json v = j[key];
+ if (!v.is_string()) {
+ throw ParsingException(std::string("The value of \"") + key +
+ "\" should be a string");
+ }
+ return v.get<std::string>();
+}
+
+static std::string getReqString(const json &j, const char *key) {
+ return getString(j, key, false);
+}
+
+static std::string getOptString(const json &j, const char *key) {
+ return getString(j, key, true);
+}
+
+// ---------------------------------------------------------------------------
+
+static json getArrayMember(const json &j, const char *key) {
+ if (!j.contains(key)) {
+ throw ParsingException(std::string("Missing \"") + key + "\" key");
+ }
+ const json obj = j[key];
+ if (!obj.is_array()) {
+ throw ParsingException(std::string("The value of \"") + key +
+ "\" should be a array");
+ }
+ return obj;
+}
+
+// ---------------------------------------------------------------------------
+
+std::unique_ptr<TINShiftFile> TINShiftFile::parse(const std::string &text) {
+ std::unique_ptr<TINShiftFile> tinshiftFile(new TINShiftFile());
+ json j;
+ try {
+ j = json::parse(text);
+ } catch (const std::exception &e) {
+ throw ParsingException(e.what());
+ }
+ if (!j.is_object()) {
+ throw ParsingException("Not an object");
+ }
+ tinshiftFile->mFileType = getReqString(j, "file_type");
+ tinshiftFile->mFormatVersion = getReqString(j, "format_version");
+ tinshiftFile->mName = getOptString(j, "name");
+ tinshiftFile->mVersion = getOptString(j, "version");
+ tinshiftFile->mLicense = getOptString(j, "license");
+ tinshiftFile->mDescription = getOptString(j, "description");
+ tinshiftFile->mPublicationDate = getOptString(j, "publication_date");
+
+ if (j.contains("authority")) {
+ const json jAuthority = j["authority"];
+ if (!jAuthority.is_object()) {
+ throw ParsingException("authority is not a object");
+ }
+ tinshiftFile->mAuthority.name = getOptString(jAuthority, "name");
+ tinshiftFile->mAuthority.url = getOptString(jAuthority, "url");
+ tinshiftFile->mAuthority.address = getOptString(jAuthority, "address");
+ tinshiftFile->mAuthority.email = getOptString(jAuthority, "email");
+ }
+
+ if (j.contains("links")) {
+ const json jLinks = j["links"];
+ if (!jLinks.is_array()) {
+ throw ParsingException("links is not an array");
+ }
+ for (const json &jLink : jLinks) {
+ if (!jLink.is_object()) {
+ throw ParsingException("links[] item is not an object");
+ }
+ Link link;
+ link.href = getOptString(jLink, "href");
+ link.rel = getOptString(jLink, "rel");
+ link.type = getOptString(jLink, "type");
+ link.title = getOptString(jLink, "title");
+ tinshiftFile->mLinks.emplace_back(std::move(link));
+ }
+ }
+ tinshiftFile->mInputCRS = getOptString(j, "input_crs");
+ tinshiftFile->mOutputCRS = getOptString(j, "output_crs");
+
+ const auto jTransformedComponents =
+ getArrayMember(j, "transformed_components");
+ for (const json &jComp : jTransformedComponents) {
+ if (!jComp.is_string()) {
+ throw ParsingException(
+ "transformed_components[] item is not a string");
+ }
+ const auto jCompStr = jComp.get<std::string>();
+ if (jCompStr == "horizontal") {
+ tinshiftFile->mTransformHorizontalComponent = true;
+ } else if (jCompStr == "vertical") {
+ tinshiftFile->mTransformVerticalComponent = true;
+ } else {
+ throw ParsingException("transformed_components[] = " + jCompStr +
+ " is not handled");
+ }
+ }
+
+ const auto jVerticesColumns = getArrayMember(j, "vertices_columns");
+ int sourceXCol = -1;
+ int sourceYCol = -1;
+ int sourceZCol = -1;
+ int targetXCol = -1;
+ int targetYCol = -1;
+ int targetZCol = -1;
+ int offsetZCol = -1;
+ for (size_t i = 0; i < jVerticesColumns.size(); ++i) {
+ const json &jColumn = jVerticesColumns[i];
+ if (!jColumn.is_string()) {
+ throw ParsingException("vertices_columns[] item is not a string");
+ }
+ const auto jColumnStr = jColumn.get<std::string>();
+ if (jColumnStr == "source_x") {
+ sourceXCol = static_cast<int>(i);
+ } else if (jColumnStr == "source_y") {
+ sourceYCol = static_cast<int>(i);
+ } else if (jColumnStr == "source_z") {
+ sourceZCol = static_cast<int>(i);
+ } else if (jColumnStr == "target_x") {
+ targetXCol = static_cast<int>(i);
+ } else if (jColumnStr == "target_y") {
+ targetYCol = static_cast<int>(i);
+ } else if (jColumnStr == "target_z") {
+ targetZCol = static_cast<int>(i);
+ } else if (jColumnStr == "offset_z") {
+ offsetZCol = static_cast<int>(i);
+ }
+ }
+ if (sourceXCol < 0) {
+ throw ParsingException(
+ "source_x must be specified in vertices_columns[]");
+ }
+ if (sourceYCol < 0) {
+ throw ParsingException(
+ "source_y must be specified in vertices_columns[]");
+ }
+ if (tinshiftFile->mTransformHorizontalComponent) {
+ if (targetXCol < 0) {
+ throw ParsingException(
+ "target_x must be specified in vertices_columns[]");
+ }
+ if (targetYCol < 0) {
+ throw ParsingException(
+ "target_y must be specified in vertices_columns[]");
+ }
+ }
+ if (tinshiftFile->mTransformVerticalComponent) {
+ if (offsetZCol >= 0) {
+ // do nothing
+ } else {
+ if (sourceZCol < 0) {
+ throw ParsingException("source_z or delta_z must be specified "
+ "in vertices_columns[]");
+ }
+ if (targetZCol < 0) {
+ throw ParsingException(
+ "target_z must be specified in vertices_columns[]");
+ }
+ }
+ }
+
+ const auto jTrianglesColumns = getArrayMember(j, "triangles_columns");
+ int idxVertex1Col = -1;
+ int idxVertex2Col = -1;
+ int idxVertex3Col = -1;
+ for (size_t i = 0; i < jTrianglesColumns.size(); ++i) {
+ const json &jColumn = jTrianglesColumns[i];
+ if (!jColumn.is_string()) {
+ throw ParsingException("triangles_columns[] item is not a string");
+ }
+ const auto jColumnStr = jColumn.get<std::string>();
+ if (jColumnStr == "idx_vertex1") {
+ idxVertex1Col = static_cast<int>(i);
+ } else if (jColumnStr == "idx_vertex2") {
+ idxVertex2Col = static_cast<int>(i);
+ } else if (jColumnStr == "idx_vertex3") {
+ idxVertex3Col = static_cast<int>(i);
+ }
+ }
+ if (idxVertex1Col < 0) {
+ throw ParsingException(
+ "idx_vertex1 must be specified in triangles_columns[]");
+ }
+ if (idxVertex2Col < 0) {
+ throw ParsingException(
+ "idx_vertex2 must be specified in triangles_columns[]");
+ }
+ if (idxVertex3Col < 0) {
+ throw ParsingException(
+ "idx_vertex3 must be specified in triangles_columns[]");
+ }
+
+ const auto jVertices = getArrayMember(j, "vertices");
+ tinshiftFile->mVerticesColumnCount = 2;
+ if (tinshiftFile->mTransformHorizontalComponent)
+ tinshiftFile->mVerticesColumnCount += 2;
+ if (tinshiftFile->mTransformVerticalComponent)
+ tinshiftFile->mVerticesColumnCount += 1;
+
+ tinshiftFile->mVertices.reserve(tinshiftFile->mVerticesColumnCount *
+ jVertices.size());
+ for (const auto &jVertex : jVertices) {
+ if (!jVertex.is_array()) {
+ throw ParsingException("vertices[] item is not an array");
+ }
+ if (jVertex.size() != jVerticesColumns.size()) {
+ throw ParsingException(
+ "vertices[] item has not expected number of elements");
+ }
+ if (!jVertex[sourceXCol].is_number()) {
+ throw ParsingException("vertices[][] item is not a number");
+ }
+ tinshiftFile->mVertices.push_back(jVertex[sourceXCol].get<double>());
+ if (!jVertex[sourceYCol].is_number()) {
+ throw ParsingException("vertices[][] item is not a number");
+ }
+ tinshiftFile->mVertices.push_back(jVertex[sourceYCol].get<double>());
+ if (tinshiftFile->mTransformHorizontalComponent) {
+ if (!jVertex[targetXCol].is_number()) {
+ throw ParsingException("vertices[][] item is not a number");
+ }
+ tinshiftFile->mVertices.push_back(
+ jVertex[targetXCol].get<double>());
+ if (!jVertex[targetYCol].is_number()) {
+ throw ParsingException("vertices[][] item is not a number");
+ }
+ tinshiftFile->mVertices.push_back(
+ jVertex[targetYCol].get<double>());
+ }
+ if (tinshiftFile->mTransformVerticalComponent) {
+ if (offsetZCol >= 0) {
+ if (!jVertex[offsetZCol].is_number()) {
+ throw ParsingException("vertices[][] item is not a number");
+ }
+ tinshiftFile->mVertices.push_back(
+ jVertex[offsetZCol].get<double>());
+ } else {
+ if (!jVertex[sourceZCol].is_number()) {
+ throw ParsingException("vertices[][] item is not a number");
+ }
+ const double sourceZ = jVertex[sourceZCol].get<double>();
+ if (!jVertex[targetZCol].is_number()) {
+ throw ParsingException("vertices[][] item is not a number");
+ }
+ const double targetZ = jVertex[targetZCol].get<double>();
+ tinshiftFile->mVertices.push_back(targetZ - sourceZ);
+ }
+ }
+ }
+
+ const auto jTriangles = getArrayMember(j, "triangles");
+ tinshiftFile->mTriangles.reserve(jTriangles.size());
+ for (const auto &jTriangle : jTriangles) {
+ if (!jTriangle.is_array()) {
+ throw ParsingException("triangles[] item is not an array");
+ }
+ if (jTriangle.size() != jTrianglesColumns.size()) {
+ throw ParsingException(
+ "triangles[] item has not expected number of elements");
+ }
+
+ if (jTriangle[idxVertex1Col].type() != json::value_t::number_unsigned) {
+ throw ParsingException("triangles[][] item is not an integer");
+ }
+ const unsigned vertex1 = jTriangle[idxVertex1Col].get<unsigned>();
+ if (vertex1 >= jVertices.size()) {
+ throw ParsingException("Invalid value for a vertex index");
+ }
+
+ if (jTriangle[idxVertex2Col].type() != json::value_t::number_unsigned) {
+ throw ParsingException("triangles[][] item is not an integer");
+ }
+ const unsigned vertex2 = jTriangle[idxVertex2Col].get<unsigned>();
+ if (vertex2 >= jVertices.size()) {
+ throw ParsingException("Invalid value for a vertex index");
+ }
+
+ if (jTriangle[idxVertex3Col].type() != json::value_t::number_unsigned) {
+ throw ParsingException("triangles[][] item is not an integer");
+ }
+ const unsigned vertex3 = jTriangle[idxVertex3Col].get<unsigned>();
+ if (vertex3 >= jVertices.size()) {
+ throw ParsingException("Invalid value for a vertex index");
+ }
+
+ VertexIndices vi;
+ vi.idx1 = vertex1;
+ vi.idx2 = vertex2;
+ vi.idx3 = vertex3;
+ tinshiftFile->mTriangles.push_back(vi);
+ }
+
+ return tinshiftFile;
+}
+
+// ---------------------------------------------------------------------------
+
+static NS_PROJ::QuadTree::RectObj GetBounds(const TINShiftFile &file,
+ bool forward) {
+ NS_PROJ::QuadTree::RectObj rect;
+ rect.minx = std::numeric_limits<double>::max();
+ rect.miny = std::numeric_limits<double>::max();
+ rect.maxx = -std::numeric_limits<double>::max();
+ rect.maxy = -std::numeric_limits<double>::max();
+ const auto &vertices = file.vertices();
+ const unsigned colCount = file.verticesColumnCount();
+ const int idxX = file.transformHorizontalComponent() && !forward ? 2 : 0;
+ const int idxY = file.transformHorizontalComponent() && !forward ? 3 : 1;
+ for (size_t i = 0; i + colCount - 1 < vertices.size(); i += colCount) {
+ const double x = vertices[i + idxX];
+ const double y = vertices[i + idxY];
+ rect.minx = std::min(rect.minx, x);
+ rect.miny = std::min(rect.miny, y);
+ rect.maxx = std::max(rect.maxx, x);
+ rect.maxy = std::max(rect.maxy, y);
+ }
+ return rect;
+}
+
+// ---------------------------------------------------------------------------
+
+static std::unique_ptr<NS_PROJ::QuadTree::QuadTree<unsigned>>
+BuildQuadTree(const TINShiftFile &file, bool forward) {
+ auto quadtree = std::unique_ptr<NS_PROJ::QuadTree::QuadTree<unsigned>>(
+ new NS_PROJ::QuadTree::QuadTree<unsigned>(GetBounds(file, forward)));
+ const auto &triangles = file.triangles();
+ const auto &vertices = file.vertices();
+ const int idxX = file.transformHorizontalComponent() && !forward ? 2 : 0;
+ const int idxY = file.transformHorizontalComponent() && !forward ? 3 : 1;
+ const unsigned colCount = file.verticesColumnCount();
+ for (size_t i = 0; i < triangles.size(); ++i) {
+ const unsigned i1 = triangles[i].idx1;
+ const unsigned i2 = triangles[i].idx2;
+ const unsigned i3 = triangles[i].idx3;
+ const double x1 = vertices[i1 * colCount + idxX];
+ const double y1 = vertices[i1 * colCount + idxY];
+ const double x2 = vertices[i2 * colCount + idxX];
+ const double y2 = vertices[i2 * colCount + idxY];
+ const double x3 = vertices[i3 * colCount + idxX];
+ const double y3 = vertices[i3 * colCount + idxY];
+ NS_PROJ::QuadTree::RectObj rect;
+ rect.minx = x1;
+ rect.miny = y1;
+ rect.maxx = x1;
+ rect.maxy = y1;
+ rect.minx = std::min(rect.minx, x2);
+ rect.miny = std::min(rect.miny, y2);
+ rect.maxx = std::max(rect.maxx, x2);
+ rect.maxy = std::max(rect.maxy, y2);
+ rect.minx = std::min(rect.minx, x3);
+ rect.miny = std::min(rect.miny, y3);
+ rect.maxx = std::max(rect.maxx, x3);
+ rect.maxy = std::max(rect.maxy, y3);
+ quadtree->insert(static_cast<unsigned>(i), rect);
+ }
+
+ return quadtree;
+}
+
+// ---------------------------------------------------------------------------
+
+Evaluator::Evaluator(std::unique_ptr<TINShiftFile> &&fileIn)
+ : mFile(std::move(fileIn)) {}
+
+// ---------------------------------------------------------------------------
+
+static const TINShiftFile::VertexIndices *
+FindTriangle(const TINShiftFile &file,
+ const NS_PROJ::QuadTree::QuadTree<unsigned> &quadtree,
+ std::vector<unsigned> &triangleIndices, double x, double y,
+ bool forward, double &lambda1, double &lambda2, double &lambda3) {
+#define USE_QUADTREE
+#ifdef USE_QUADTREE
+ triangleIndices.clear();
+ quadtree.search(x, y, triangleIndices);
+#endif
+ const auto &triangles = file.triangles();
+ const auto &vertices = file.vertices();
+ constexpr double EPS = 1e-10;
+ const int idxX = file.transformHorizontalComponent() && !forward ? 2 : 0;
+ const int idxY = file.transformHorizontalComponent() && !forward ? 3 : 1;
+ const unsigned colCount = file.verticesColumnCount();
+#ifdef USE_QUADTREE
+ for (unsigned i : triangleIndices)
+#else
+ for (size_t i = 0; i < triangles.size(); ++i)
+#endif
+ {
+ const auto &triangle = triangles[i];
+ const unsigned i1 = triangle.idx1;
+ const unsigned i2 = triangle.idx2;
+ const unsigned i3 = triangle.idx3;
+ const double x1 = vertices[i1 * colCount + idxX];
+ const double y1 = vertices[i1 * colCount + idxY];
+ const double x2 = vertices[i2 * colCount + idxX];
+ const double y2 = vertices[i2 * colCount + idxY];
+ const double x3 = vertices[i3 * colCount + idxX];
+ const double y3 = vertices[i3 * colCount + idxY];
+ const double det_T = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3);
+ lambda1 = ((y2 - y3) * (x - x3) + (x3 - x2) * (y - y3)) / det_T;
+ lambda2 = ((y3 - y1) * (x - x3) + (x1 - x3) * (y - y3)) / det_T;
+ if (lambda1 >= -EPS && lambda1 <= 1 + EPS && lambda2 >= -EPS &&
+ lambda2 <= 1 + EPS) {
+ lambda3 = 1 - lambda1 - lambda2;
+ if (lambda3 >= 0) {
+ return &triangle;
+ }
+ }
+ }
+ return nullptr;
+}
+
+// ---------------------------------------------------------------------------
+
+bool Evaluator::forward(double x, double y, double z, double &x_out,
+ double &y_out, double &z_out) {
+ if (!mQuadTreeForward)
+ mQuadTreeForward = BuildQuadTree(*(mFile.get()), true);
+
+ double lambda1 = 0.0;
+ double lambda2 = 0.0;
+ double lambda3 = 0.0;
+ const auto *triangle =
+ FindTriangle(*mFile, *mQuadTreeForward, mTriangleIndices, x, y, true,
+ lambda1, lambda2, lambda3);
+ if (!triangle)
+ return false;
+ const auto &vertices = mFile->vertices();
+ const unsigned i1 = triangle->idx1;
+ const unsigned i2 = triangle->idx2;
+ const unsigned i3 = triangle->idx3;
+ const unsigned colCount = mFile->verticesColumnCount();
+ if (mFile->transformHorizontalComponent()) {
+ constexpr unsigned idxTargetColX = 2;
+ constexpr unsigned idxTargetColY = 3;
+ x_out = vertices[i1 * colCount + idxTargetColX] * lambda1 +
+ vertices[i2 * colCount + idxTargetColX] * lambda2 +
+ vertices[i3 * colCount + idxTargetColX] * lambda3;
+ y_out = vertices[i1 * colCount + idxTargetColY] * lambda1 +
+ vertices[i2 * colCount + idxTargetColY] * lambda2 +
+ vertices[i3 * colCount + idxTargetColY] * lambda3;
+ } else {
+ x_out = x;
+ y_out = y;
+ }
+ if (mFile->transformVerticalComponent()) {
+ const int idxCol = mFile->transformHorizontalComponent() ? 4 : 2;
+ z_out = z + (vertices[i1 * colCount + idxCol] * lambda1 +
+ vertices[i2 * colCount + idxCol] * lambda2 +
+ vertices[i3 * colCount + idxCol] * lambda3);
+ } else {
+ z_out = z;
+ }
+ return true;
+}
+
+// ---------------------------------------------------------------------------
+
+bool Evaluator::inverse(double x, double y, double z, double &x_out,
+ double &y_out, double &z_out) {
+ NS_PROJ::QuadTree::QuadTree<unsigned> *quadtree;
+ if (!mFile->transformHorizontalComponent() &&
+ mFile->transformVerticalComponent()) {
+ if (!mQuadTreeForward)
+ mQuadTreeForward = BuildQuadTree(*(mFile.get()), true);
+ quadtree = mQuadTreeForward.get();
+ } else {
+ if (!mQuadTreeInverse)
+ mQuadTreeInverse = BuildQuadTree(*(mFile.get()), false);
+ quadtree = mQuadTreeInverse.get();
+ }
+
+ double lambda1 = 0.0;
+ double lambda2 = 0.0;
+ double lambda3 = 0.0;
+ const auto *triangle = FindTriangle(*mFile, *quadtree, mTriangleIndices, x,
+ y, false, lambda1, lambda2, lambda3);
+ if (!triangle)
+ return false;
+ const auto &vertices = mFile->vertices();
+ const unsigned i1 = triangle->idx1;
+ const unsigned i2 = triangle->idx2;
+ const unsigned i3 = triangle->idx3;
+ const unsigned colCount = mFile->verticesColumnCount();
+ if (mFile->transformHorizontalComponent()) {
+ constexpr unsigned idxTargetColX = 0;
+ constexpr unsigned idxTargetColY = 1;
+ x_out = vertices[i1 * colCount + idxTargetColX] * lambda1 +
+ vertices[i2 * colCount + idxTargetColX] * lambda2 +
+ vertices[i3 * colCount + idxTargetColX] * lambda3;
+ y_out = vertices[i1 * colCount + idxTargetColY] * lambda1 +
+ vertices[i2 * colCount + idxTargetColY] * lambda2 +
+ vertices[i3 * colCount + idxTargetColY] * lambda3;
+ } else {
+ x_out = x;
+ y_out = y;
+ }
+ if (mFile->transformVerticalComponent()) {
+ const int idxCol = mFile->transformHorizontalComponent() ? 4 : 2;
+ z_out = z - (vertices[i1 * colCount + idxCol] * lambda1 +
+ vertices[i2 * colCount + idxCol] * lambda2 +
+ vertices[i3 * colCount + idxCol] * lambda3);
+ } else {
+ z_out = z;
+ }
+ return true;
+}
+
+} // namespace TINSHIFT_NAMESPACE
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 9fc29103..c60f344b 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -17,6 +17,7 @@ proj_add_gie_test("adams_ws2" "gie/adams_ws2.gie")
proj_add_gie_test("guyou" "gie/guyou.gie")
proj_add_gie_test("peirce_q" "gie/peirce_q.gie")
proj_add_gie_test("defmodel" "gie/defmodel.gie")
+proj_add_gie_test("tinshift" "gie/tinshift.gie")
# GIGS tests. Uncommented tests are expected to fail due to issues with
# various projections. Should be investigated further and fixed.
diff --git a/test/gie/Makefile.am b/test/gie/Makefile.am
index e6bb9582..20f0ec8f 100644
--- a/test/gie/Makefile.am
+++ b/test/gie/Makefile.am
@@ -16,7 +16,8 @@ EXTRA_DIST = 4D-API_cs2cs-style.gie \
adams_ws2.gie \
guyou.gie \
peirce_q.gie \
- defmodel.gie
+ defmodel.gie \
+ tinshift.gie
PROJ_LIB ?= ../../data/for_tests
@@ -68,4 +69,7 @@ peirce_q: peirce_q.gie
defmodel: defmodel.gie
PROJ_SKIP_READ_USER_WRITABLE_DIRECTORY=YES PROJ_LIB=$(PROJ_LIB) $(GIEEXE) $<
-check-local: 4D-API-cs2cs-style GDA axisswap builtins deformation ellipsoid more_builtins unitconvert DHDN_ETRS89 geotiff_grids adams_hemi adams_ws1 adams_ws2 guyou peirce_q defmodel
+tinshift: tinshift.gie
+ PROJ_SKIP_READ_USER_WRITABLE_DIRECTORY=YES PROJ_LIB=$(PROJ_LIB) $(GIEEXE) $<
+
+check-local: 4D-API-cs2cs-style GDA axisswap builtins deformation ellipsoid more_builtins unitconvert DHDN_ETRS89 geotiff_grids adams_hemi adams_ws1 adams_ws2 guyou peirce_q defmodel tinshift
diff --git a/test/gie/tinshift.gie b/test/gie/tinshift.gie
new file mode 100644
index 00000000..dff62693
--- /dev/null
+++ b/test/gie/tinshift.gie
@@ -0,0 +1,50 @@
+
+-------------------------------------------------------------------------------
+===============================================================================
+Test +proj=tinshift
+===============================================================================
+
+<gie-strict>
+
+# Missing +file
+operation +proj=tinshift
+expect failure errno no_args
+
+# +file doesn't point to an existing file
+operation +proj=tinshift +file=i_do_not_exist
+expect failure errno invalid_arg
+
+# Not a JSON file
+operation +proj=tinshift +file=proj.ini
+expect failure errno invalid_arg
+
+
+# Tests on a file without explicit CRS
+operation +proj=tinshift +file=tests/tinshift_crs_implicit.json
+accept 2 49
+expect 2.1 49.1
+roundtrip 1
+
+accept 0 0
+expect failure
+
+direction inverse
+accept 0 0
+expect failure
+
+
+# Tests on a file with explicit CRS
+operation +proj=tinshift +file=tests/tinshift_simplified_kkj_etrs.json
+tolerance 0.1 mm
+# Verified with https://kartta.paikkatietoikkuna.fi/?lang=en with EPSG:2393 to EPSG:3067
+accept 3210000.0000 6700000.0000
+expect 209948.3217 6697187.0009
+roundtrip 1
+
+operation +proj=tinshift +file=tests/tinshift_simplified_n60_n2000.json
+tolerance 0.1 mm
+accept 3210000.0000 6700000.0000 10.0
+expect 3210000.0000 6700000.0000 10.2886
+roundtrip 1
+
+</gie-strict>
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 04c6acb6..2c0c19a9 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -181,3 +181,14 @@ target_link_libraries(test_defmodel
add_test(NAME test_defmodel COMMAND test_defmodel)
set_property(TEST test_defmodel
PROPERTY ENVIRONMENT ${PROJ_TEST_ENVIRONMENT})
+
+
+add_executable(test_tinshift
+ main.cpp
+ test_tinshift.cpp)
+target_link_libraries(test_tinshift
+ GTest::gtest
+ ${PROJ_LIBRARIES})
+add_test(NAME test_tinshift COMMAND test_tinshift)
+set_property(TEST test_tinshift
+ PROPERTY ENVIRONMENT ${PROJ_TEST_ENVIRONMENT})
diff --git a/test/unit/Makefile.am b/test/unit/Makefile.am
index 2dd0dbff..483cb0bd 100644
--- a/test/unit/Makefile.am
+++ b/test/unit/Makefile.am
@@ -19,6 +19,7 @@ noinst_PROGRAMS += gie_self_tests
noinst_PROGRAMS += include_proj_h_from_c
noinst_PROGRAMS += test_network
noinst_PROGRAMS += test_defmodel
+noinst_PROGRAMS += test_tinshift
pj_transform_test_SOURCES = pj_transform_test.cpp main.cpp
pj_transform_test_LDADD = ../../src/libproj.la @GTEST_LIBS@
@@ -77,6 +78,12 @@ test_defmodel_LDADD = ../../src/libproj.la @GTEST_LIBS@
test_defmodel-check: test_defmodel
PROJ_LIB=$(PROJ_LIB) ./test_defmodel
+test_tinshift_SOURCES = test_tinshift.cpp main.cpp
+test_tinshift_LDADD = ../../src/libproj.la @GTEST_LIBS@
+
+test_tinshift-check: test_tinshift
+ PROJ_LIB=$(PROJ_LIB) ./test_tinshift
+
check-local: pj_transform_test-check pj_phi2_test-check proj_errno_string_test-check \
proj_angular_io_test-check proj_context_test-check test_cpp_api-check \
- gie_self_tests-check test_network-check test_defmodel-check
+ gie_self_tests-check test_network-check test_defmodel-check test_tinshift-check
diff --git a/test/unit/test_tinshift.cpp b/test/unit/test_tinshift.cpp
new file mode 100644
index 00000000..96da6e25
--- /dev/null
+++ b/test/unit/test_tinshift.cpp
@@ -0,0 +1,211 @@
+/******************************************************************************
+ *
+ * Project: PROJ
+ * Purpose: Test TIN shift
+ * Author: Even Rouault <even dot rouault at spatialys dot com>
+ *
+ ******************************************************************************
+ * Copyright (c) 2020, Even Rouault <even dot rouault at spatialys dot com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ ****************************************************************************/
+
+#include "gtest_include.h"
+
+#define PROJ_COMPILATION
+#define TINSHIFT_NAMESPACE TestTINShift
+#include "transformations/tinshift.hpp"
+
+using namespace TINSHIFT_NAMESPACE;
+
+namespace {
+
+static json getMinValidContent() {
+ json j;
+ j["file_type"] = "triangulation_file";
+ j["format_version"] = "1.0";
+ j["input_crs"] = "EPSG:2393";
+ j["output_crs"] = "EPSG:3067";
+ j["transformed_components"] = {"horizontal"};
+ j["vertices_columns"] = {"source_x", "source_y", "target_x", "target_y"};
+ j["triangles_columns"] = {"idx_vertex1", "idx_vertex2", "idx_vertex3"};
+ j["vertices"] = {{0, 0, 101, 101}, {0, 1, 100, 101}, {1, 1, 100, 100}};
+ j["triangles"] = {{0, 1, 2}};
+
+ return j;
+}
+
+// ---------------------------------------------------------------------------
+
+TEST(tinshift, basic) {
+ EXPECT_THROW(TINShiftFile::parse("foo"), ParsingException);
+ EXPECT_THROW(TINShiftFile::parse("null"), ParsingException);
+ EXPECT_THROW(TINShiftFile::parse("{}"), ParsingException);
+
+ const auto jMinValid(getMinValidContent());
+ {
+ auto f = TINShiftFile::parse(jMinValid.dump());
+ EXPECT_EQ(f->fileType(), "triangulation_file");
+ EXPECT_EQ(f->formatVersion(), "1.0");
+ EXPECT_EQ(f->inputCRS(), "EPSG:2393");
+ EXPECT_EQ(f->outputCRS(), "EPSG:3067");
+
+ auto eval = Evaluator(std::move(f));
+ double x_out = 0;
+ double y_out = 0;
+ double z_out = 0;
+
+ EXPECT_FALSE(eval.forward(-0.1, 0.0, 1000.0, x_out, y_out, z_out));
+
+ EXPECT_TRUE(eval.forward(0.0, 0.0, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 101.0);
+ EXPECT_EQ(y_out, 101.0);
+ EXPECT_EQ(z_out, 1000.0);
+
+ EXPECT_TRUE(eval.forward(0.0, 1.0, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 100.0);
+ EXPECT_EQ(y_out, 101.0);
+ EXPECT_EQ(z_out, 1000.0);
+
+ EXPECT_TRUE(eval.forward(1.0, 1.0, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 100.0);
+ EXPECT_EQ(y_out, 100.0);
+ EXPECT_EQ(z_out, 1000.0);
+
+ EXPECT_TRUE(eval.forward(0.0, 0.5, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 100.5);
+ EXPECT_EQ(y_out, 101.0);
+ EXPECT_EQ(z_out, 1000.0);
+
+ EXPECT_TRUE(eval.forward(0.5, 0.5, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 100.5);
+ EXPECT_EQ(y_out, 100.5);
+ EXPECT_EQ(z_out, 1000.0);
+
+ EXPECT_TRUE(eval.forward(0.5, 0.75, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 100.25);
+ EXPECT_EQ(y_out, 100.5);
+ EXPECT_EQ(z_out, 1000.0);
+
+ EXPECT_TRUE(eval.inverse(100.25, 100.5, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 0.5);
+ EXPECT_EQ(y_out, 0.75);
+ EXPECT_EQ(z_out, 1000.0);
+ }
+
+ // Vertical only, through source_z / target_z
+ {
+ auto j(jMinValid);
+ j["transformed_components"] = {"vertical"};
+ j["vertices_columns"] = {"source_x", "source_y", "source_z",
+ "target_z"};
+ j["triangles_columns"] = {"idx_vertex1", "idx_vertex2", "idx_vertex3"};
+ j["vertices"] = {
+ {0, 0, 10.5, 10.6}, {0, 1, 15.0, 15.2}, {1, 1, 17.5, 18.0}};
+ j["triangles"] = {{0, 1, 2}};
+
+ auto f = TINShiftFile::parse(j.dump());
+ auto eval = Evaluator(std::move(f));
+ double x_out = 0;
+ double y_out = 0;
+ double z_out = 0;
+
+ EXPECT_TRUE(eval.forward(0.0, 0.0, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 0.0);
+ EXPECT_EQ(y_out, 0.0);
+ EXPECT_EQ(z_out, 1000.1);
+
+ EXPECT_TRUE(eval.forward(0.5, 0.75, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 0.5);
+ EXPECT_EQ(y_out, 0.75);
+ EXPECT_EQ(z_out, 1000.325);
+
+ EXPECT_TRUE(eval.inverse(0.5, 0.75, 1000.325, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 0.5);
+ EXPECT_EQ(y_out, 0.75);
+ EXPECT_EQ(z_out, 1000.0);
+ }
+
+ // Vertical only, through offset_z
+ {
+ auto j(jMinValid);
+ j["transformed_components"] = {"vertical"};
+ j["vertices_columns"] = {"source_x", "source_y", "offset_z"};
+ j["triangles_columns"] = {"idx_vertex1", "idx_vertex2", "idx_vertex3"};
+ j["vertices"] = {{0, 0, 0.1}, {0, 1, 0.2}, {1, 1, 0.5}};
+ j["triangles"] = {{0, 1, 2}};
+
+ auto f = TINShiftFile::parse(j.dump());
+ auto eval = Evaluator(std::move(f));
+ double x_out = 0;
+ double y_out = 0;
+ double z_out = 0;
+
+ EXPECT_TRUE(eval.forward(0.0, 0.0, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 0.0);
+ EXPECT_EQ(y_out, 0.0);
+ EXPECT_EQ(z_out, 1000.1);
+
+ EXPECT_TRUE(eval.forward(0.5, 0.75, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 0.5);
+ EXPECT_EQ(y_out, 0.75);
+ EXPECT_EQ(z_out, 1000.325);
+
+ EXPECT_TRUE(eval.inverse(0.5, 0.75, 1000.325, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 0.5);
+ EXPECT_EQ(y_out, 0.75);
+ EXPECT_EQ(z_out, 1000.0);
+ }
+
+ // Horizontal + vertical
+ {
+ auto j(jMinValid);
+ j["transformed_components"] = {"horizontal", "vertical"};
+ j["vertices_columns"] = {"source_x", "source_y", "target_x", "target_y",
+ "offset_z"};
+ j["triangles_columns"] = {"idx_vertex1", "idx_vertex2", "idx_vertex3"};
+ j["vertices"] = {{0, 0, 101, 101, 0.1},
+ {0, 1, 100, 101, 0.2},
+ {1, 1, 100, 100, 0.5}};
+ j["triangles"] = {{0, 1, 2}};
+
+ auto f = TINShiftFile::parse(j.dump());
+ auto eval = Evaluator(std::move(f));
+ double x_out = 0;
+ double y_out = 0;
+ double z_out = 0;
+
+ EXPECT_TRUE(eval.forward(0.0, 0.0, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 101.0);
+ EXPECT_EQ(y_out, 101.0);
+ EXPECT_EQ(z_out, 1000.1);
+
+ EXPECT_TRUE(eval.forward(0.5, 0.75, 1000.0, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 100.25);
+ EXPECT_EQ(y_out, 100.5);
+ EXPECT_EQ(z_out, 1000.325);
+
+ EXPECT_TRUE(eval.inverse(100.25, 100.5, 1000.325, x_out, y_out, z_out));
+ EXPECT_EQ(x_out, 0.5);
+ EXPECT_EQ(y_out, 0.75);
+ EXPECT_EQ(z_out, 1000.0);
+ }
+}
+
+} // namespace