aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEven Rouault <even.rouault@spatialys.com>2020-11-22 15:08:41 +0100
committerEven Rouault <even.rouault@spatialys.com>2020-11-23 20:58:26 +0100
commitd8fe9964bcbc6d1eeaddf6f5d47ca6d444dc8744 (patch)
tree58fdb4c19e45972dcf407a51301999834d9b2c1b
parent5974b85665c260a020367b4527613414d2d6c157 (diff)
downloadPROJ-d8fe9964bcbc6d1eeaddf6f5d47ca6d444dc8744.tar.gz
PROJ-d8fe9964bcbc6d1eeaddf6f5d47ca6d444dc8744.zip
Add +proj=topocentric geocentric->topocentric conversion (fixes #500)
-rw-r--r--docs/source/operations/conversions/images/ECEF_ENU_Longitude_Latitude_relationships.pngbin0 -> 30731 bytes
-rw-r--r--docs/source/operations/conversions/images/ECEF_ENU_Longitude_Latitude_relationships.svg348
-rw-r--r--docs/source/operations/conversions/index.rst1
-rw-r--r--docs/source/operations/conversions/topocentric.rst115
-rw-r--r--include/proj/internal/coordinateoperation_constants.hpp41
-rw-r--r--src/Makefile.am1
-rw-r--r--src/conversions/topocentric.cpp165
-rw-r--r--src/iso19111/coordinateoperation.cpp55
-rw-r--r--src/lib_proj.cmake1
-rw-r--r--src/pj_list.h1
-rw-r--r--src/proj_constants.h20
-rw-r--r--test/gie/builtins.gie42
-rw-r--r--test/unit/test_operation.cpp122
13 files changed, 904 insertions, 8 deletions
diff --git a/docs/source/operations/conversions/images/ECEF_ENU_Longitude_Latitude_relationships.png b/docs/source/operations/conversions/images/ECEF_ENU_Longitude_Latitude_relationships.png
new file mode 100644
index 00000000..224ff738
--- /dev/null
+++ b/docs/source/operations/conversions/images/ECEF_ENU_Longitude_Latitude_relationships.png
Binary files differ
diff --git a/docs/source/operations/conversions/images/ECEF_ENU_Longitude_Latitude_relationships.svg b/docs/source/operations/conversions/images/ECEF_ENU_Longitude_Latitude_relationships.svg
new file mode 100644
index 00000000..c97ca181
--- /dev/null
+++ b/docs/source/operations/conversions/images/ECEF_ENU_Longitude_Latitude_relationships.svg
@@ -0,0 +1,348 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="520"
+ height="500"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs6"><marker
+ refX="0"
+ refY="0"
+ orient="auto"
+ id="Arrow2Mend"
+ style="overflow:visible"><path
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="scale(-0.6,-0.6)"
+ id="path3759"
+ style="font-size:12px;fill:#0d6299;fill-rule:evenodd;stroke:#0d6299;stroke-width:0.625;stroke-linejoin:round" /></marker><marker
+ refX="0"
+ refY="0"
+ orient="auto"
+ id="Arrow1Mstart"
+ style="overflow:visible"><path
+ d="M 0,0 5,-5 -12.5,0 5,5 0,0 z"
+ transform="matrix(0.4,0,0,0.4,4,0)"
+ id="path3738"
+ style="fill:#00b800;fill-rule:evenodd;stroke:#00b800;stroke-width:1pt;marker-start:none" /></marker><marker
+ refX="0"
+ refY="0"
+ orient="auto"
+ id="Arrow1Mend"
+ style="overflow:visible"><path
+ d="M 0,0 5,-5 -12.5,0 5,5 0,0 z"
+ transform="matrix(-0.4,0,0,-0.4,-4,0)"
+ id="path3741"
+ style="fill:#00b800;fill-rule:evenodd;stroke:#00b800;stroke-width:1pt;marker-start:none" /></marker><marker
+ refX="0"
+ refY="0"
+ orient="auto"
+ id="Arrow2Mstart"
+ style="overflow:visible"><path
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="scale(0.6,0.6)"
+ id="path3756"
+ style="font-size:12px;fill:#0d6299;fill-rule:evenodd;stroke:#0d6299;stroke-width:0.625;stroke-linejoin:round" /></marker><marker
+ refX="0"
+ refY="0"
+ orient="auto"
+ id="Arrow1Lstart"
+ style="overflow:visible"><path
+ d="M 0,0 5,-5 -12.5,0 5,5 0,0 z"
+ transform="matrix(0.8,0,0,0.8,10,0)"
+ id="path3732"
+ style="fill:#ff0000;fill-rule:evenodd;stroke:#ff0000;stroke-width:1pt;marker-start:none" /></marker><marker
+ refX="0"
+ refY="0"
+ orient="auto"
+ id="Arrow1Lend"
+ style="overflow:visible"><path
+ d="M 0,0 5,-5 -12.5,0 5,5 0,0 z"
+ transform="matrix(-0.8,0,0,-0.8,-10,0)"
+ id="path3735"
+ style="fill:#ff0000;fill-rule:evenodd;stroke:#ff0000;stroke-width:1pt;marker-start:none" /></marker></defs><g
+ transform="matrix(1.25,0,0,-1.25,-136.61316,788.09803)"
+ id="g12"><g
+ transform="matrix(0,1,-1,0,718.75,123.67)"
+ id="g18"
+ style="stroke:#808080;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:4, 8;stroke-dashoffset:0"><path
+ d="m 283.32,265.39 1.08,0 0.96,0.36 1.08,0.48 0.96,0.72 0.96,0.84 0.96,1.08 1.08,1.32 0.96,1.44 0.96,1.68 0.96,1.8 0.96,2.04 0.84,2.28 0.96,2.4 0.96,2.64 0.84,2.76 0.84,2.88 0.84,3.12 0.84,3.24 0.84,3.48 0.84,3.48 0.72,3.84 0.72,3.84 0.72,4.08 0.72,4.08 0.72,4.32 0.6,4.44 0.6,4.56 0.6,4.68 0.6,4.8 0.6,4.93 0.48,5.03 0.48,5.16 0.48,5.16 0.36,5.28 0.36,5.4 0.36,5.4 0.36,5.52 0.24,5.52 0.24,5.64 0.24,5.64 0.24,5.76 0.12,5.76 0.12,5.76 0.12,5.76 0,5.76 0,5.88 0,5.76 0,5.76 -0.12,5.76 -0.12,5.88 -0.12,5.64 -0.24,5.77 -0.24,5.63 -0.24,5.64 -0.24,5.52 -0.36,5.52 -0.36,5.4 -0.36,5.4 -0.36,5.28 -0.48,5.16 -0.48,5.16 -0.48,5.04 -0.6,4.92 -0.6,4.8 -0.6,4.68 -0.6,4.56 -0.6,4.44 -0.72,4.32 -0.72,4.08 -0.72,4.08 -0.72,3.96 -0.72,3.72 -0.84,3.6 -0.84,3.36 -0.84,3.24 -0.84,3.12 -0.84,3 -0.84,2.76 -0.96,2.52 -0.96,2.4 -0.84,2.28 -0.96,2.04 -0.96,1.8 -0.96,1.68 -0.96,1.44 -1.08,1.32 -0.96,1.08 -0.96,0.84 -0.96,0.72 -1.08,0.49 -0.96,0.35 -1.08,0"
+ id="path20"
+ style="fill:none;stroke:#808080;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:4, 8;stroke-dashoffset:0" /></g><g
+ transform="matrix(0,1,-1,0,718.75,123.67)"
+ id="g22"
+ style="stroke:#808080;stroke-width:1.60000002;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"><path
+ d="m 283.32,605.35 -0.96,0 -0.96,-0.35 -1.08,-0.49 -0.96,-0.72 -0.96,-0.84 -1.08,-1.08 -0.96,-1.32 -0.96,-1.44 -0.96,-1.68 -0.96,-1.8 -0.96,-2.04 -0.96,-2.28 -0.84,-2.4 -0.96,-2.52 -0.84,-2.76 -0.84,-3 -0.96,-3.12 -0.72,-3.24 -0.84,-3.36 -0.84,-3.6 -0.72,-3.72 -0.84,-3.96 -0.72,-4.08 -0.6,-4.08 -0.72,-4.32 -0.6,-4.44 -0.72,-4.56 -0.6,-4.68 -0.48,-4.8 -0.6,-4.92 -0.48,-5.04 -0.48,-5.16 -0.48,-5.16 -0.36,-5.28 -0.36,-5.4 -0.36,-5.4 -0.36,-5.52 -0.24,-5.52 -0.36,-5.64 -0.12,-5.63 -0.24,-5.77 -0.12,-5.64 -0.12,-5.88 -0.12,-5.76 0,-5.76 -0.12,-5.76 0.12,-5.88 0,-5.76 0.12,-5.76 0.12,-5.76 0.12,-5.76 0.24,-5.76 0.12,-5.64 0.36,-5.64 0.24,-5.52 0.36,-5.52 0.36,-5.4 0.36,-5.4 0.36,-5.28 0.48,-5.16 0.48,-5.16 0.48,-5.03 0.6,-4.93 0.48,-4.8 0.6,-4.68 0.72,-4.56 0.6,-4.44 0.72,-4.32 0.6,-4.08 0.72,-4.08 0.84,-3.84 0.72,-3.84 0.84,-3.48 0.84,-3.48 0.72,-3.24 0.96,-3.12 0.84,-2.88 0.84,-2.76 0.96,-2.64 0.84,-2.4 0.96,-2.28 0.96,-2.04 0.96,-1.8 0.96,-1.68 0.96,-1.44 0.96,-1.32 1.08,-1.08 0.96,-0.84 0.96,-0.72 1.08,-0.48 0.96,-0.36 0.96,0"
+ id="path24"
+ style="fill:none;stroke:#808080;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
+ transform="matrix(0,1,-1,0,718.75,123.67)"
+ id="g26"
+ style="stroke:#808080;stroke-width:1.60000002;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"><path
+ d="m 283.32,605.35 -8.88,-0.12 -8.88,-0.72 -8.76,-1.2 -8.76,-1.56 -8.64,-2.16 -8.52,-2.52 -8.4,-3 -8.28,-3.36 -8.04,-3.84 -7.8,-4.2 -7.56,-4.67 -7.32,-5.05 -7.08,-5.4 -6.84,-5.76 -6.36,-6.24 -6.24,-6.36 -5.76,-6.84 -5.4,-7.08 -5.04,-7.32 -4.68,-7.56 -4.2,-7.8 -3.84,-8.04 -3.36,-8.28 -3,-8.4 -2.52,-8.52 -2.16,-8.64 -1.56,-8.76 -1.2,-8.76 -0.72,-8.88 -0.12,-8.88 0.12,-9 0.72,-8.88 1.2,-8.76 1.56,-8.76 2.16,-8.64 2.52,-8.52 3,-8.4 3.36,-8.28 3.84,-8.04 4.2,-7.8 4.68,-7.56 5.04,-7.32 5.4,-7.08 5.76,-6.84 6.24,-6.36 6.36,-6.12 6.84,-5.88 7.08,-5.4 7.32,-5.04 7.56,-4.68 7.8,-4.2 8.04,-3.84 8.28,-3.36 8.4,-3 8.52,-2.52 8.64,-2.04 8.76,-1.68 8.76,-1.2 8.88,-0.6 8.88,-0.24"
+ id="path28"
+ style="fill:none;stroke:#808080;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
+ transform="matrix(0,1,-1,0,718.75,123.67)"
+ id="g30"
+ style="stroke:#808080;stroke-width:1.60000002;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"><path
+ d="m 453.36,435.43 0,0.96 -0.36,0.96 -0.48,1.08 -0.72,0.96 -0.84,0.96 -1.08,1.08 -1.32,0.96 -1.44,0.96 -1.68,0.96 -1.92,0.96 -2.04,0.96 -2.16,0.96 -2.52,0.84 -2.52,0.97 -2.76,0.83 -3,0.96 -3.12,0.84 -3.24,0.84 -3.48,0.84 -3.6,0.72 -3.72,0.84 -3.96,0.72 -3.96,0.72 -4.2,0.72 -4.32,0.6 -4.56,0.72 -4.56,0.6 -4.68,0.6 -4.8,0.6 -4.92,0.48 -5.04,0.48 -5.16,0.48 -5.16,0.48 -5.28,0.48 -5.4,0.36 -5.52,0.36 -5.52,0.24 -5.52,0.36 -5.64,0.24 -5.64,0.24 -5.76,0.12 -5.76,0.12 -5.76,0.12 -5.88,0.12 -5.76,0.12 -5.76,0 -5.88,-0.12 -5.76,0 -5.88,-0.12 -5.76,-0.12 -5.76,-0.12"
+ id="path32"
+ style="fill:none;stroke:#808080;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
+ transform="matrix(0,1,-1,0,718.75,123.67)"
+ id="g46"
+ style="stroke:#ff9900;stroke-width:1.60000002;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"><path
+ d="m 270.6,392.35 -0.24,3.48 -0.36,3.6 -0.24,3.6 -0.24,3.72 -0.24,3.72 -0.24,3.84 -0.12,3.96 -0.12,3.84 -0.12,3.96 0,3.96 -0.12,4.08 0,3.96 0.12,3.96 0,3.97 0.12,3.95"
+ id="path48"
+ style="fill:none;stroke:#ff9900;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><path
+ d="m 481.68,406.99 -198.36,0 -33.84,-34.44"
+ id="path16"
+ style="fill:none;stroke:#0d6299;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:url(#Arrow2Mstart);marker-end:url(#Arrow2Mend)" /><g
+ transform="matrix(0,1,-1,0,718.75,123.67)"
+ id="g34"
+ style="stroke:#ff9900;stroke-width:1.60000002;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"><path
+ d="m 363,358.51 -79.68,76.92"
+ id="path36"
+ style="fill:none;stroke:#ff9900;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
+ transform="matrix(0,1,-1,0,718.75,123.67)"
+ id="g38"
+ style="stroke:#808080;stroke-width:1.60000002;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"><path
+ d="m 453.36,435.43 -0.12,-3.96 -0.48,-3.96 -0.96,-3.96 -1.2,-3.84 -1.56,-3.84 -1.92,-3.84 -2.16,-3.84 -2.64,-3.72 -2.88,-3.6 -3.24,-3.6 -3.48,-3.48 -3.84,-3.48 -4.08,-3.24 -4.44,-3.24 -4.68,-3.12 -5.04,-3 -5.16,-2.88 -5.52,-2.88 -5.76,-2.64 -5.88,-2.4 -6.24,-2.4 -6.36,-2.28 -6.48,-2.04 -6.84,-1.92 -6.84,-1.68 -7.08,-1.68 -7.2,-1.44 -7.2,-1.2 -7.44,-1.08 -7.44,-0.96 -7.56,-0.72 -7.68,-0.6 -7.68,-0.36 -7.68,-0.24 -7.68,0 -7.68,0.12 -7.68,0.36 -7.68,0.48"
+ id="path40"
+ style="fill:none;stroke:#808080;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
+ transform="matrix(0,1,-1,0,718.75,123.67)"
+ id="g42"
+ style="stroke:#ff9900;stroke-width:1.60000002;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"><path
+ d="m 257.88,349.27 25.44,86.16"
+ id="path44"
+ style="fill:none;stroke:#ff9900;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
+ transform="matrix(0,1,-1,0,718.75,123.67)"
+ id="g50"
+ style="stroke:#ff9900;stroke-width:1.60000002;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"><path
+ d="m 274.92,406.63 4.44,-0.24 4.56,0 4.44,0.12 4.56,0.24 4.32,0.48 4.44,0.72 4.2,0.84 4.08,0.96"
+ id="path52"
+ style="fill:none;stroke:#ff9900;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
+ transform="matrix(0,1,-1,0,718.75,123.67)"
+ id="g58"><path
+ d="m 337.92,327.31 0,48.72 50.16,13.56 0,-48.72 -50.16,-13.56 z"
+ id="path60"
+ style="fill:none;stroke:#00b800;stroke-width:0.72000003;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none" /></g><path
+ d="m 342.72,461.59 -13.56,50.16 48.72,0 13.56,-50.16 -48.72,0 z"
+ id="path72"
+ style="fill:#ffffff;fill-opacity:1;stroke:#00b800;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none" /><g
+ transform="matrix(0,1,-1,0,718.75,123.67)"
+ id="g74"
+ style="fill:none;stroke:#808080;stroke-width:1.60000002;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"><path
+ d="m 283.32,265.39 9,0.24 8.88,0.6 8.76,1.2 8.76,1.68 8.64,2.04 8.52,2.52 8.4,3 8.28,3.36 8.04,3.84 7.8,4.2 7.56,4.68 7.32,5.04 7.08,5.4 6.84,5.88 6.36,6.12 6.12,6.36 5.88,6.84 5.4,7.08 5.04,7.32 4.68,7.56 4.2,7.8 3.84,8.04 3.36,8.28 3,8.4 2.52,8.52 2.04,8.64 1.68,8.76 1.2,8.76 0.6,8.88 0.24,9 -0.24,8.88 -0.6,8.88 -1.2,8.76 -1.68,8.76 -2.04,8.64 -2.52,8.52 -3,8.4 -3.36,8.28 -3.84,8.04 -4.2,7.8 -4.68,7.56 -5.04,7.32 -5.4,7.08 -5.88,6.84 -6.12,6.36 -6.36,6.24 -6.84,5.76 -7.08,5.4 -7.32,5.05 -7.56,4.67 -7.8,4.2 -8.04,3.84 -8.28,3.36 -8.4,3 -8.52,2.52 -8.64,2.16 -8.76,1.56 -8.76,1.2 -8.88,0.72 -9,0.12"
+ id="path76"
+ style="fill:none;stroke:#808080;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><path
+ d="m 283.32,406.99 0,198.36"
+ id="path56"
+ style="fill:none;stroke:#0d6299;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-mid:none;marker-end:url(#Arrow2Mend)" /><path
+ d="m 360.24,486.67 97.44,0"
+ id="path68"
+ style="fill:none;stroke:#00b800;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;marker-start:none;marker-end:url(#Arrow1Mend)" /><path
+ d="M 411.6,539.71 360.24,486.67 333.12,586.75"
+ id="path64"
+ style="fill:none;stroke:#00b800;stroke-width:1.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;marker-start:url(#Arrow1Mstart);marker-mid:none;marker-end:url(#Arrow1Mend)" /><text
+ x="277.50943"
+ y="-613.04358"
+ transform="scale(1,-1)"
+ id="text3721"
+ xml:space="preserve"
+ style="font-size:19.20000076px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:sans-serif"><tspan
+ x="277.50943"
+ y="-613.04358"
+ id="tspan3727">Z</tspan></text>
+
+
+
+
+
+
+<text
+ x="488.21332"
+ y="-399.92703"
+ transform="scale(1,-1)"
+ id="text3721-4"
+ xml:space="preserve"
+ style="font-size:19.20000076px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:sans-serif"><tspan
+ x="488.21332"
+ y="-399.92703"
+ id="tspan4386">Y</tspan></text>
+
+
+
+
+
+
+<text
+ x="238.70586"
+ y="-350.1015"
+ transform="scale(1,-1)"
+ id="text3721-4-9"
+ xml:space="preserve"
+ style="font-size:19.20000076px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:sans-serif"><tspan
+ x="238.70586"
+ y="-350.1015"
+ id="tspan4386-4">X</tspan></text>
+
+
+
+
+
+
+<text
+ x="341.8902"
+ y="-575.89331"
+ transform="scale(1,-1)"
+ id="text3721-8"
+ xml:space="preserve"
+ style="font-size:16px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:sans-serif"><tspan
+ x="341.8902"
+ y="-575.89331"
+ id="tspan3727-8">North</tspan></text>
+
+
+
+
+
+
+<text
+ x="435.72714"
+ y="-491.9921"
+ transform="scale(1,-1)"
+ id="text3721-8-2"
+ xml:space="preserve"
+ style="font-size:16px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:sans-serif"><tspan
+ x="435.72714"
+ y="-491.9921"
+ id="tspan3727-8-4">East</tspan></text>
+
+
+
+
+
+
+<text
+ x="414.17657"
+ y="-527.3114"
+ transform="scale(1,-1)"
+ id="text3721-8-2-5"
+ xml:space="preserve"
+ style="font-size:16px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:sans-serif"><tspan
+ x="414.17657"
+ y="-527.3114"
+ id="tspan3727-8-4-5">Up</tspan></text>
+
+
+
+
+
+
+<text
+ x="290.61334"
+ y="-612.47876"
+ transform="scale(1,-1)"
+ id="text3721-0"
+ xml:space="preserve"
+ style="font-size:19.20000076px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:sans-serif"><tspan
+ x="290.61334"
+ y="-612.47876"
+ id="tspan3727-9"
+ style="font-size:11.19999981px">ecef</tspan></text>
+
+
+<text
+ x="501.1416"
+ y="-400.26947"
+ transform="scale(1,-1)"
+ id="text3721-0-4"
+ xml:space="preserve"
+ style="font-size:19.20000076px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:sans-serif"><tspan
+ x="501.1416"
+ y="-400.26947"
+ id="tspan3727-9-8"
+ style="font-size:11.19999981px">ecef</tspan></text>
+
+
+<text
+ x="253.27524"
+ y="-351.07462"
+ transform="scale(1,-1)"
+ id="text3721-0-2"
+ xml:space="preserve"
+ style="font-size:19.20000076px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:sans-serif"><tspan
+ x="253.27524"
+ y="-351.07462"
+ id="tspan3727-9-4"
+ style="font-size:11.19999981px">ecef</tspan></text>
+
+
+<g
+ transform="matrix(-1,0,0,1,314.12527,7.5684393)"
+ id="text4066"
+ style="font-size:32px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:sans-serif"><path
+ d="m 63.106132,443.21212 11.436928,0.6089 -0.229685,4.3142 c -0.04044,0.75933 -0.107718,1.3373 -0.201842,1.73391 -0.123533,0.55671 -0.326725,1.01791 -0.609576,1.38361 -0.282873,0.36568 -0.667108,0.65164 -1.152709,0.8579 -0.485618,0.20623 -1.011876,0.29427 -1.578773,0.26409 -0.972587,-0.0518 -1.779164,-0.40506 -2.419733,-1.05983 -0.64058,-0.65478 -0.917809,-1.79091 -0.83169,-3.40841 l 0.156169,-2.93334 -4.649665,-0.24755 z m 5.91874,1.83288 -0.157415,2.95675 c -0.05206,0.97778 0.09301,1.6818 0.435204,2.11206 0.342187,0.43025 0.843544,0.66297 1.504074,0.69814 0.478481,0.0255 0.894495,-0.0737 1.248042,-0.29735 0.35353,-0.22372 0.595432,-0.5303 0.725706,-0.91975 0.08117,-0.25125 0.140036,-0.72014 0.176596,-1.40666 l 0.155754,-2.92555 z"
+ id="path5083"
+ style="font-size:16px" /><path
+ d="m 62.494207,453.68612 8.288312,0.37685 -0.05748,1.26431 -1.256515,-0.0571 c 0.57326,0.34932 0.947337,0.66481 1.122232,0.94648 0.174879,0.28167 0.254871,0.5864 0.239976,0.91419 -0.02154,0.47347 -0.194303,0.94788 -0.518304,1.42324 l -1.28134,-0.54313 c 0.218521,-0.33418 0.335592,-0.67296 0.351212,-1.01635 0.01395,-0.30698 -0.06587,-0.58693 -0.239444,-0.83987 -0.173593,-0.25294 -0.421914,-0.43889 -0.744963,-0.55785 -0.492393,-0.1788 -1.035154,-0.28168 -1.628287,-0.30865 l -4.339267,-0.19729 z"
+ id="path5085"
+ style="font-size:16px" /><path
+ d="m 72.064046,459.64751 1.614176,0.0987 -0.08579,1.40363 -1.614175,-0.0987 z m -9.817616,-0.60005 8.281421,0.50616 -0.08579,1.40363 -8.281421,-0.50616 z"
+ id="path5087"
+ style="font-size:16px" /><path
+ d="m 62.072234,462.59865 8.273028,0.62861 -0.0953,1.25419 -1.160717,-0.0882 c 0.385345,0.29044 0.684987,0.66056 0.898927,1.11036 0.213925,0.44979 0.299977,0.94993 0.258157,1.50043 -0.04657,0.61282 -0.211986,1.1056 -0.496246,1.47837 -0.284275,0.37276 -0.661032,0.62228 -1.130274,0.74855 0.916238,0.72776 1.334505,1.61616 1.254803,2.66523 -0.06235,0.82054 -0.337508,1.43427 -0.825462,1.84119 -0.487967,0.4069 -1.204542,0.57444 -2.149729,0.50264 l -5.678943,-0.4315 0.105951,-1.39442 5.211541,0.39598 c 0.560878,0.0426 0.968114,0.0279 1.22171,-0.0443 0.253584,-0.0721 0.464759,-0.22195 0.633524,-0.44939 0.168753,-0.22746 0.265366,-0.50218 0.289837,-0.82416 0.04419,-0.58166 -0.112565,-1.07934 -0.470263,-1.49304 -0.357711,-0.41372 -0.962419,-0.65293 -1.814125,-0.71763 l -4.806458,-0.36521 0.106543,-1.40221 5.375131,0.40842 c 0.623198,0.0473 1.099282,-0.0314 1.428252,-0.23622 0.328958,-0.20484 0.51317,-0.56692 0.552637,-1.08625 0.02998,-0.3947 -0.04616,-0.76743 -0.228439,-1.11818 -0.182289,-0.35076 -0.467653,-0.61664 -0.856092,-0.79762 -0.388451,-0.181 -0.959192,-0.3001 -1.712225,-0.35732 l -4.292315,-0.32614 z"
+ id="path5089"
+ style="font-size:16px" /><path
+ d="m 63.112413,481.79967 -0.313505,1.43024 c -0.824084,-0.30683 -1.44079,-0.7905 -1.850119,-1.45104 -0.409329,-0.66055 -0.569371,-1.47052 -0.480128,-2.42991 0.1124,-1.20833 0.573615,-2.13182 1.383645,-2.77048 0.81003,-0.63866 1.886624,-0.89551 3.229786,-0.77057 1.389828,0.12928 2.435218,0.58745 3.136173,1.37451 0.700941,0.78705 0.998351,1.75103 0.89223,2.89194 -0.102759,1.1046 -0.562677,1.97198 -1.379756,2.60215 -0.817092,0.63014 -1.907586,0.88178 -3.271484,0.75492 -0.08298,-0.008 -0.2072,-0.0219 -0.372665,-0.0425 l 0.573093,-6.1609 c -0.912368,-0.0326 -1.631162,0.15949 -2.156387,0.57618 -0.525227,0.41669 -0.81992,0.96989 -0.884079,1.65963 -0.04776,0.5134 0.04631,0.96416 0.282215,1.35226 0.2359,0.3881 0.63956,0.71596 1.210981,0.98358 z m 2.691313,-4.38677 -0.429095,4.6129 c 0.7007,0.002 1.238289,-0.12544 1.612767,-0.38353 0.580818,-0.39582 0.904275,-0.94897 0.97037,-1.65944 0.05981,-0.64306 -0.105115,-1.20372 -0.494779,-1.68196 -0.389676,-0.47826 -0.942763,-0.77425 -1.659263,-0.88797 z"
+ id="path5091"
+ style="font-size:16px" /><path
+ d="m 59.792635,489.32216 11.383842,1.25786 -0.250542,2.26745 -8.358053,1.80391 c -0.778099,0.16554 -1.359804,0.28467 -1.745119,0.35738 0.399845,0.17517 0.985813,0.44428 1.757906,0.80732 l 7.61938,3.60078 -0.223944,2.02673 -11.383842,-1.25786 0.16045,-1.4521 9.52795,1.0528 -9.162433,-4.36079 0.150154,-1.35892 10.05482,-2.22165 -9.691019,-1.07081 z"
+ id="path5093"
+ style="font-size:16px" /><path
+ d="m 60.144362,508.42535 -0.370341,1.41658 c -0.811182,-0.33947 -1.40809,-0.84738 -1.790728,-1.52372 -0.382638,-0.67637 -0.510223,-1.49208 -0.382757,-2.44715 0.16054,-1.20288 0.658247,-2.10722 1.493124,-2.71304 0.834875,-0.60581 1.920864,-0.81949 3.257969,-0.64104 1.38356,0.18465 2.409829,0.68418 3.078811,1.49859 0.668967,0.8144 0.927664,1.78949 0.77609,2.92526 -0.146767,1.09961 -0.64094,1.94794 -1.48252,2.54499 -0.841592,0.59703 -1.941261,0.80495 -3.29901,0.62374 -0.0826,-0.011 -0.20616,-0.0301 -0.37067,-0.0573 l 0.818543,-6.13312 c -0.91034,-0.0689 -1.636228,0.0943 -2.177666,0.48966 -0.54144,0.39539 -0.857979,0.93639 -0.949617,1.62302 -0.06821,0.51108 0.0078,0.96523 0.228016,1.36245 0.220221,0.3972 0.610473,0.74091 1.170756,1.03113 z m 2.864264,-4.27586 -0.612874,4.5921 c 0.700046,0.0304 1.242309,-0.0759 1.62679,-0.31885 0.596155,-0.37233 0.941432,-0.91212 1.035833,-1.61939 0.08543,-0.64016 -0.05699,-1.20695 -0.427251,-1.70037 -0.370277,-0.49343 -0.911109,-0.81126 -1.622498,-0.95349 z"
+ id="path5095"
+ style="font-size:16px" /><path
+ d="m 57.017756,511.24663 8.203227,1.24307 -0.18962,1.25134 -1.243615,-0.18845 c 0.533511,0.40744 0.872478,0.76039 1.016902,1.05883 0.144409,0.29844 0.192036,0.60987 0.142884,0.9343 -0.07102,0.4686 -0.292535,0.92231 -0.664552,1.36111 l -1.217391,-0.67438 c 0.252328,-0.30944 0.404246,-0.63409 0.455754,-0.97396 0.04603,-0.30383 -0.004,-0.5906 -0.150142,-0.86033 -0.146139,-0.26973 -0.373613,-0.48067 -0.682422,-0.63282 -0.470952,-0.2294 -0.999949,-0.38858 -1.586992,-0.47754 l -4.294722,-0.65079 z"
+ id="path5097"
+ style="font-size:16px" /><path
+ d="m 65.979482,517.99715 1.599793,0.23656 -0.2057,1.39112 -1.599793,-0.23655 z m -9.730141,-1.43875 8.207633,1.21362 -0.2057,1.39113 -8.207633,-1.21363 z"
+ id="path5099"
+ style="font-size:16px" /><path
+ d="m 54.756351,525.36962 1.030675,0.18346 c -0.717999,-0.66212 -0.987553,-1.49569 -0.808664,-2.50072 0.115914,-0.65123 0.401943,-1.21795 0.858087,-1.70016 0.456144,-0.48221 1.032678,-0.81604 1.729604,-1.00146 0.696922,-0.18543 1.463294,-0.20376 2.299118,-0.055 0.815306,0.14512 1.530795,0.41266 2.14647,0.80263 0.615662,0.38996 1.052029,0.8882 1.309104,1.49472 0.25706,0.60651 0.325811,1.24564 0.206255,1.91738 -0.08763,0.49225 -0.2695,0.91219 -0.545619,1.25982 -0.276133,0.34761 -0.602296,0.61225 -0.97849,0.79394 l 4.045786,0.72012 -0.245061,1.3768 -11.275898,-2.00704 z m 4.855545,-3.65092 c -1.046061,-0.1862 -1.867287,-0.10489 -2.463679,0.24391 -0.596395,0.3488 -0.947985,0.82318 -1.054771,1.42313 -0.107699,0.60506 0.04821,1.16316 0.467744,1.67428 0.419528,0.51111 1.136938,0.85702 2.152232,1.03774 1.117843,0.19896 1.976614,0.12963 2.576315,-0.20799 0.599691,-0.33765 0.95567,-0.82182 1.067939,-1.45252 0.109518,-0.61534 -0.05024,-1.17412 -0.479282,-1.67634 -0.429051,-0.50223 -1.18455,-0.84963 -2.266498,-1.04221 z"
+ id="path5101"
+ style="font-size:16px" /><path
+ d="m 63.779537,530.73494 1.587034,0.31084 -0.270294,1.38003 -1.587034,-0.31084 z m -9.652536,-1.89055 8.142171,1.59473 -0.270294,1.38003 -8.142171,-1.59474 z"
+ id="path5103"
+ style="font-size:16px" /><path
+ d="m 53.252402,537.8338 c -0.320697,-0.60367 -0.518405,-1.16033 -0.593125,-1.66998 -0.07472,-0.50966 -0.05287,-1.03411 0.06554,-1.57334 0.19549,-0.89025 0.563212,-1.52671 1.103167,-1.90939 0.539954,-0.38268 1.148225,-0.49974 1.824814,-0.35116 0.396794,0.0871 0.739423,0.25702 1.027889,0.50966 0.28846,0.25265 0.499774,0.54701 0.633944,0.88308 0.134161,0.33608 0.211901,0.69842 0.233217,1.08704 0.01201,0.28525 -0.0087,0.7073 -0.06207,1.26615 -0.111262,1.13803 -0.132853,1.98915 -0.06477,2.55334 0.187103,0.0464 0.306091,0.0752 0.356967,0.0864 0.559579,0.12287 0.982317,0.0797 1.268214,-0.12945 0.387387,-0.28287 0.657045,-0.77023 0.808974,-1.46207 0.141864,-0.64607 0.133401,-1.14785 -0.02539,-1.50532 -0.1588,-0.35749 -0.509421,-0.67311 -1.051865,-0.94686 l 0.478046,-1.30279 c 0.548029,0.24832 0.968104,0.5512 1.260227,0.90863 0.292108,0.35742 0.475461,0.82161 0.550059,1.39257 0.07458,0.57095 0.03536,1.20489 -0.117674,1.90183 -0.15193,0.69184 -0.356761,1.2361 -0.614495,1.63276 -0.257746,0.39666 -0.53232,0.66964 -0.823723,0.81895 -0.291415,0.1493 -0.634279,0.22332 -1.028595,0.22206 -0.244066,-0.006 -0.671324,-0.0754 -1.281775,-0.20948 l -1.831366,-0.40215 c -1.276869,-0.28039 -2.090873,-0.42848 -2.442014,-0.44426 -0.351139,-0.0158 -0.700857,0.0287 -1.049154,0.13356 l 0.315017,-1.43457 c 0.316159,-0.0799 0.669473,-0.0983 1.059943,-0.0552 z m 3.092672,0.55914 c -0.09401,-0.54323 -0.102764,-1.32902 -0.02625,-2.35737 0.0426,-0.58254 0.03872,-1.00199 -0.01163,-1.25834 -0.05036,-0.25635 -0.159502,-0.46962 -0.327427,-0.6398 -0.167929,-0.17019 -0.37144,-0.28153 -0.610533,-0.33403 -0.366275,-0.0804 -0.701942,-0.009 -1.007004,0.2148 -0.305064,0.22362 -0.516242,0.60251 -0.633535,1.13666 -0.116177,0.52906 -0.103775,1.02503 0.03721,1.48792 0.14098,0.46288 0.391187,0.8351 0.750622,1.11667 0.277918,0.21566 0.719561,0.38996 1.324931,0.5229 z"
+ id="path5105"
+ style="font-size:16px" /><path
+ d="m 51.49594,541.02374 8.010721,2.16021 -0.329523,1.22197 -1.139001,-0.30714 c 0.721357,0.82566 0.932194,1.79416 0.632512,2.90551 -0.130189,0.48275 -0.336606,0.90314 -0.619253,1.26118 -0.282659,0.35801 -0.59191,0.59963 -0.927754,0.72485 -0.335855,0.1252 -0.708232,0.17582 -1.117135,0.15187 -0.264999,-0.0175 -0.711791,-0.11103 -1.340375,-0.28053 l -4.925613,-1.32826 0.366137,-1.35775 4.872811,1.31402 c 0.553154,0.14916 0.981002,0.20789 1.283548,0.17621 0.302537,-0.0317 0.571798,-0.16004 0.807785,-0.385 0.235977,-0.22497 0.402786,-0.51848 0.500428,-0.88054 0.155941,-0.57831 0.106983,-1.1269 -0.146876,-1.64579 -0.253869,-0.51889 -0.893728,-0.91665 -1.919578,-1.19328 l -4.37497,-1.17978 z"
+ id="path5107"
+ style="font-size:16px" /></g>
+<text
+ x="315.86932"
+ y="-413.14511"
+ transform="scale(1,-1)"
+ id="text3721-8-2-5-1"
+ xml:space="preserve"
+ style="font-size:16px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:sans-serif"><tspan
+ x="315.86932"
+ y="-413.14511"
+ id="tspan3727-8-4-5-7">φ</tspan></text>
+
+<text
+ x="292.20465"
+ y="-379.30618"
+ transform="scale(1,-1)"
+ id="text3721-8-2-5-1-1"
+ xml:space="preserve"
+ style="font-size:16px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:sans-serif"><tspan
+ x="292.20465"
+ y="-379.30618"
+ id="tspan3727-8-4-5-7-1">λ</tspan></text>
+
+</g></svg> \ No newline at end of file
diff --git a/docs/source/operations/conversions/index.rst b/docs/source/operations/conversions/index.rst
index b0bd0d37..b335c2ac 100644
--- a/docs/source/operations/conversions/index.rst
+++ b/docs/source/operations/conversions/index.rst
@@ -19,4 +19,5 @@ conversions.
pop
push
set
+ topocentric
unitconvert
diff --git a/docs/source/operations/conversions/topocentric.rst b/docs/source/operations/conversions/topocentric.rst
new file mode 100644
index 00000000..828ef4e0
--- /dev/null
+++ b/docs/source/operations/conversions/topocentric.rst
@@ -0,0 +1,115 @@
+.. _topocentric:
+
+================================================================================
+Geocentric to topocentric conversion
+================================================================================
+
+.. versionadded:: 8.0.0
+
+Convert geocentric coordinates to topocentric coordinates (in the forward path).
+
++---------------------+--------------------------------------------------------+
+| **Alias** | topocentric |
++---------------------+--------------------------------------------------------+
+| **Domain** | 3D |
++---------------------+--------------------------------------------------------+
+| **Input type** | Geocentric cartesian coordinates |
++---------------------+--------------------------------------------------------+
+| **Output type** | Topocentric cartesian coordinates |
++---------------------+--------------------------------------------------------+
+
+This operation converts geocentric coordinate values (X, Y, Z) to topocentric
+(E/East, N/North, U/Up) values. This is also sometimes known as the ECEF (Earth
+Centered Earth Fixed) to ENU conversion.
+
+Topocentric coordinates are expressed in a frame whose East and North axis form
+a local tangent plane to the Earth's ellipsoidal surface fixed to a specific
+location (the topocentric origin), and the Up axis points upwards along the
+normal to that plane.
+
+.. image:: ./images/ECEF_ENU_Longitude_Latitude_relationships.png
+ :align: center
+ :scale: 100%
+ :alt: ENU coordinate frame
+
+..
+ Source : https://en.wikipedia.org/wiki/Local_tangent_plane_coordinates#/media/File:ECEF_ENU_Longitude_Latitude_relationships.svg
+ Public domain
+..
+
+The topocentric origin is a required parameter of the conversion, and can be
+expressed either as geocentric coordinates (``X_0``, ``Y_0`` and ``Z_0``) or
+as geographic coordinates (``lat_0``, ``lon_0``, ``h_0``).
+
+When conversion between geographic and topocentric coordinates is desired, the
+topocentric conversion must be preceded by the :ref:`cart` conversion to
+perform the initial geographic to geocentric coordinates conversion.
+
+The formulas used come from the "Geocentric/topocentric conversions" paragraph
+of :cite:`IOGP2018`
+
+Usage
+################################################################################
+
+Convert geocentric coordinates to topocentric coordinates, with the topocentric
+origin specified in geocentric coordinates::
+
+ echo 3771793.968 140253.342 5124304.349 2020 | \
+ cct -d 3 +proj=topocentric +ellps=WGS84 +X_0=3652755.3058 +Y_0=319574.6799 +Z_0=5201547.3536
+
+ -189013.869 -128642.040 -4220.171 2020.0000
+
+Convert geographic coordinates to topocentric coordinates, with the topocentric
+origin specified in geographic coordinates::
+
+ echo 2.12955 53.80939444 73 2020 | cct -d 3 +proj=pipeline \
+ +step +proj=cart +ellps=WGS84 \
+ +step +proj=topocentric +ellps=WGS84 +lon_0=5 +lat_0=55 +h_0=200
+
+ -189013.869 -128642.040 -4220.171 2020.0000
+
+
+Parameters
+################################################################################
+
+.. include:: ../options/ellps.rst
+
+Topocentric origin described as geocentric coordinates
+------------------------------------------------------
+
+.. note::
+
+ The below options are mutually exclusive with the ones to express the origin as geographic coordinates.
+
+.. option:: +X_0=<value>
+
+ Geocentric X value of the topocentric origin (in metre)
+
+.. option:: +Y_0=<value>
+
+ Geocentric Y value of the topocentric origin (in metre)
+
+.. option:: +Z_0=<value>
+
+ Geocentric Z value of the topocentric origin (in metre)
+
+Topocentric origin described as geographic coordinates
+------------------------------------------------------
+
+.. note::
+
+ The below options are mutually exclusive with the ones to express the origin as geocentric coordinates.
+
+.. option:: +lat_0=<value>
+
+ Latitude of topocentric origin (in degree)
+
+.. option:: +lon_0=<value>
+
+ Longitude of topocentric origin (in degree)
+
+.. option:: +h_0=<value>
+
+ Ellipsoidal height of topocentric origin (in metre)
+
+ *Defaults to 0.0.*
diff --git a/include/proj/internal/coordinateoperation_constants.hpp b/include/proj/internal/coordinateoperation_constants.hpp
index 592f6263..0ed3a027 100644
--- a/include/proj/internal/coordinateoperation_constants.hpp
+++ b/include/proj/internal/coordinateoperation_constants.hpp
@@ -532,6 +532,34 @@ static const ParamMapping *const paramsColombiaUrban[] = {
&paramProjectionPlaneOriginHeight,
nullptr};
+static const ParamMapping paramGeocentricXTopocentricOrigin = {
+ EPSG_NAME_PARAMETER_GEOCENTRIC_X_TOPOCENTRIC_ORIGIN,
+ EPSG_CODE_PARAMETER_GEOCENTRIC_X_TOPOCENTRIC_ORIGIN, nullptr,
+ common::UnitOfMeasure::Type::LINEAR, "X_0"};
+
+static const ParamMapping paramGeocentricYTopocentricOrigin = {
+ EPSG_NAME_PARAMETER_GEOCENTRIC_Y_TOPOCENTRIC_ORIGIN,
+ EPSG_CODE_PARAMETER_GEOCENTRIC_Y_TOPOCENTRIC_ORIGIN, nullptr,
+ common::UnitOfMeasure::Type::LINEAR, "Y_0"};
+
+static const ParamMapping paramGeocentricZTopocentricOrigin = {
+ EPSG_NAME_PARAMETER_GEOCENTRIC_Z_TOPOCENTRIC_ORIGIN,
+ EPSG_CODE_PARAMETER_GEOCENTRIC_Z_TOPOCENTRIC_ORIGIN, nullptr,
+ common::UnitOfMeasure::Type::LINEAR, "Z_0"};
+
+static const ParamMapping *const paramsGeocentricTopocentric[] = {
+ &paramGeocentricXTopocentricOrigin, &paramGeocentricYTopocentricOrigin,
+ &paramGeocentricZTopocentricOrigin, nullptr};
+
+static const ParamMapping paramHeightTopoOriginWithH0 = {
+ EPSG_NAME_PARAMETER_ELLIPSOIDAL_HEIGHT_TOPOCENTRIC_ORIGIN,
+ EPSG_CODE_PARAMETER_ELLIPSOIDAL_HEIGHT_TOPOCENTRIC_ORIGIN, nullptr,
+ common::UnitOfMeasure::Type::LINEAR, "h_0"};
+
+static const ParamMapping *const paramsGeographicTopocentric[] = {
+ &paramLatTopoOrigin, &paramLonTopoOrigin, &paramHeightTopoOriginWithH0,
+ nullptr};
+
static const MethodMapping projectionMethodMappings[] = {
{EPSG_NAME_METHOD_TRANSVERSE_MERCATOR, EPSG_CODE_METHOD_TRANSVERSE_MERCATOR,
"Transverse_Mercator", "tmerc", nullptr, paramsNatOriginScaleK},
@@ -836,6 +864,14 @@ static const MethodMapping projectionMethodMappings[] = {
{EPSG_NAME_METHOD_COLOMBIA_URBAN, EPSG_CODE_METHOD_COLOMBIA_URBAN, nullptr,
"col_urban", nullptr, paramsColombiaUrban},
+
+ {EPSG_NAME_METHOD_GEOCENTRIC_TOPOCENTRIC,
+ EPSG_CODE_METHOD_GEOCENTRIC_TOPOCENTRIC, nullptr, "topocentric", nullptr,
+ paramsGeocentricTopocentric},
+
+ {EPSG_NAME_METHOD_GEOGRAPHIC_TOPOCENTRIC,
+ EPSG_CODE_METHOD_GEOGRAPHIC_TOPOCENTRIC, nullptr, nullptr, nullptr,
+ paramsGeographicTopocentric},
};
#define METHOD_NAME_CODE(method) \
@@ -878,6 +914,8 @@ static const struct MethodNameCode {
METHOD_NAME_CODE(AXIS_ORDER_REVERSAL_2D),
METHOD_NAME_CODE(AXIS_ORDER_REVERSAL_3D),
METHOD_NAME_CODE(GEOGRAPHIC_GEOCENTRIC),
+ METHOD_NAME_CODE(GEOCENTRIC_TOPOCENTRIC),
+ METHOD_NAME_CODE(GEOGRAPHIC_TOPOCENTRIC),
// Transformations
METHOD_NAME_CODE(LONGITUDE_ROTATION),
METHOD_NAME_CODE(AFFINE_PARAMETRIC_TRANSFORMATION),
@@ -943,6 +981,9 @@ static const struct ParamNameCode {
PARAM_NAME_CODE(LONGITUDE_OF_ORIGIN),
PARAM_NAME_CODE(ELLIPSOID_SCALE_FACTOR),
PARAM_NAME_CODE(PROJECTION_PLANE_ORIGIN_HEIGHT),
+ PARAM_NAME_CODE(GEOCENTRIC_X_TOPOCENTRIC_ORIGIN),
+ PARAM_NAME_CODE(GEOCENTRIC_Y_TOPOCENTRIC_ORIGIN),
+ PARAM_NAME_CODE(GEOCENTRIC_Z_TOPOCENTRIC_ORIGIN),
// Parameters of transformations
PARAM_NAME_CODE(SEMI_MAJOR_AXIS_DIFFERENCE),
PARAM_NAME_CODE(FLATTENING_DIFFERENCE),
diff --git a/src/Makefile.am b/src/Makefile.am
index 5b36c8bd..4b904d9f 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -187,6 +187,7 @@ libproj_la_SOURCES = \
conversions/geoc.cpp \
conversions/geocent.cpp \
conversions/noop.cpp \
+ conversions/topocentric.cpp \
conversions/set.cpp \
conversions/unitconvert.cpp \
\
diff --git a/src/conversions/topocentric.cpp b/src/conversions/topocentric.cpp
new file mode 100644
index 00000000..214f8221
--- /dev/null
+++ b/src/conversions/topocentric.cpp
@@ -0,0 +1,165 @@
+/******************************************************************************
+ * Project: PROJ
+ * Purpose: Convert between geocentric coordinates and topocentric (ENU) coordinates
+ *
+ ******************************************************************************
+ * 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__
+
+#include "proj_internal.h"
+#include <errno.h>
+#include <math.h>
+
+PROJ_HEAD(topocentric, "Geocentric/Topocentric conversion");
+
+// Notations and formulas taken from IOGP Publication 373-7-2 -
+// Geomatics Guidance Note number 7, part 2 - October 2020
+
+namespace { // anonymous namespace
+struct pj_opaque {
+ double X0;
+ double Y0;
+ double Z0;
+ double sinphi0;
+ double cosphi0;
+ double sinlam0;
+ double coslam0;
+};
+} // anonymous namespace
+
+// Convert from geocentric to topocentric
+static PJ_COORD topocentric_fwd(PJ_COORD in, PJ * P)
+{
+ struct pj_opaque *Q = static_cast<struct pj_opaque*>(P->opaque);
+ PJ_COORD out;
+ const double dX = in.xyz.x - Q->X0;
+ const double dY = in.xyz.y - Q->Y0;
+ const double dZ = in.xyz.z - Q->Z0;
+ out.xyz.x = -dX * Q->sinlam0 + dY * Q->coslam0;
+ out.xyz.y = -dX * Q->sinphi0 * Q->coslam0 - dY * Q->sinphi0 * Q->sinlam0 + dZ * Q->cosphi0;
+ out.xyz.z = dX * Q->cosphi0 * Q->coslam0 + dY * Q->cosphi0 * Q->sinlam0 + dZ * Q->sinphi0;
+ return out;
+}
+
+// Convert from topocentric to geocentric
+static PJ_COORD topocentric_inv(PJ_COORD in, PJ * P)
+{
+ struct pj_opaque *Q = static_cast<struct pj_opaque*>(P->opaque);
+ PJ_COORD out;
+ out.xyz.x = Q->X0 - in.xyz.x * Q->sinlam0 - in.xyz.y * Q->sinphi0 * Q->coslam0 + in.xyz.z * Q->cosphi0 * Q->coslam0;
+ out.xyz.y = Q->Y0 + in.xyz.x * Q->coslam0 - in.xyz.y * Q->sinphi0 * Q->sinlam0 + in.xyz.z * Q->cosphi0 * Q->sinlam0;
+ out.xyz.z = Q->Z0 + in.xyz.y * Q->cosphi0 + in.xyz.z * Q->sinphi0;
+ return out;
+}
+
+
+/*********************************************************************/
+PJ *CONVERSION(topocentric,1) {
+/*********************************************************************/
+ struct pj_opaque *Q = static_cast<struct pj_opaque*>(pj_calloc (1, sizeof (struct pj_opaque)));
+ if (nullptr==Q)
+ return pj_default_destructor (P, ENOMEM);
+ P->opaque = static_cast<void *>(Q);
+
+ // The topocentric origin can be specified either in geocentric coordinates
+ // (X_0,Y_0,Z_0) or as geographic coordinates (lon_0,lat_0,h_0)
+ // Checks:
+ // - X_0 or lon_0 must be specified
+ // - If X_0 is specified, the Y_0 and Z_0 must also be
+ // - If lon_0 is specified, then lat_0 must also be
+ // - If any of X_0, Y_0, Z_0 is specified, then any of lon_0,lat_0,h_0 must
+ // not be, and vice versa.
+ const auto hasX0 = pj_param_exists(P->params, "X_0");
+ const auto hasY0 = pj_param_exists(P->params, "Y_0");
+ const auto hasZ0 = pj_param_exists(P->params, "Z_0");
+ const auto hasLon0 = pj_param_exists(P->params, "lon_0");
+ const auto hasLat0 = pj_param_exists(P->params, "lat_0");
+ const auto hash0 = pj_param_exists(P->params, "h_0");
+ if( !hasX0 && !hasLon0 )
+ {
+ return pj_default_destructor(P, PJD_ERR_MISSING_ARGS);
+ }
+ if ( (hasX0 || hasY0 || hasZ0) &&
+ (hasLon0 || hasLat0 || hash0) )
+ {
+ return pj_default_destructor(P, PJD_ERR_MUTUALLY_EXCLUSIVE_ARGS);
+ }
+ if( hasX0 && (!hasY0 || !hasZ0) )
+ {
+ return pj_default_destructor(P, PJD_ERR_MISSING_ARGS);
+ }
+ if( hasLon0 && !hasLat0 ) // allow missing h_0
+ {
+ return pj_default_destructor(P, PJD_ERR_MISSING_ARGS);
+ }
+
+ // Pass a dummy ellipsoid definition that will be overridden just afterwards
+ PJ* cart = proj_create(P->ctx, "+proj=cart +a=1");
+ if (cart == nullptr)
+ return pj_default_destructor(P, ENOMEM);
+ /* inherit ellipsoid definition from P to cart */
+ pj_inherit_ellipsoid_def (P, cart);
+
+ if( hasX0 )
+ {
+ Q->X0 = pj_param(P->ctx, P->params, "dX_0").f;
+ Q->Y0 = pj_param(P->ctx, P->params, "dY_0").f;
+ Q->Z0 = pj_param(P->ctx, P->params, "dZ_0").f;
+
+ // Compute lam0, phi0 from X0,Y0,Z0
+ PJ_XYZ xyz;
+ xyz.x = Q->X0;
+ xyz.y = Q->Y0;
+ xyz.z = Q->Z0;
+ const auto lpz = pj_inv3d(xyz, cart);
+ Q->sinphi0 = sin(lpz.phi);
+ Q->cosphi0 = cos(lpz.phi);
+ Q->sinlam0 = sin(lpz.lam);
+ Q->coslam0 = cos(lpz.lam);
+ }
+ else
+ {
+ // Compute X0,Y0,Z0 from lam0, phi0, h0
+ PJ_LPZ lpz;
+ lpz.lam = P->lam0;
+ lpz.phi = P->phi0;
+ lpz.z = pj_param(P->ctx, P->params, "dh_0").f;
+ const auto xyz = pj_fwd3d(lpz, cart);
+ Q->X0 = xyz.x;
+ Q->Y0 = xyz.y;
+ Q->Z0 = xyz.z;
+
+ Q->sinphi0 = sin(P->phi0);
+ Q->cosphi0 = cos(P->phi0);
+ Q->sinlam0 = sin(P->lam0);
+ Q->coslam0 = cos(P->lam0);
+ }
+
+ proj_destroy(cart);
+
+ P->fwd4d = topocentric_fwd;
+ P->inv4d = topocentric_inv;
+ P->left = PJ_IO_UNITS_CARTESIAN;
+ P->right = PJ_IO_UNITS_CARTESIAN;
+ return P;
+}
diff --git a/src/iso19111/coordinateoperation.cpp b/src/iso19111/coordinateoperation.cpp
index 1d3b7a90..dfd349b5 100644
--- a/src/iso19111/coordinateoperation.cpp
+++ b/src/iso19111/coordinateoperation.cpp
@@ -6034,28 +6034,39 @@ void Conversion::_exportToPROJString(
!isHeightDepthReversal;
bool applyTargetCRSModifiers = applySourceCRSModifiers;
+ if (formatter->getCRSExport()) {
+ if (methodEPSGCode == EPSG_CODE_METHOD_GEOCENTRIC_TOPOCENTRIC ||
+ methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC_TOPOCENTRIC) {
+ throw io::FormattingException("Transformation cannot be exported "
+ "as a PROJ.4 string (but can be part "
+ "of a PROJ pipeline)");
+ }
+ }
+
auto l_sourceCRS = sourceCRS();
+ crs::GeographicCRSPtr srcGeogCRS;
if (!formatter->getCRSExport() && l_sourceCRS && applySourceCRSModifiers) {
- crs::CRS *horiz = l_sourceCRS.get();
- const auto compound = dynamic_cast<const crs::CompoundCRS *>(horiz);
+ crs::CRSPtr horiz = l_sourceCRS;
+ const auto compound =
+ dynamic_cast<const crs::CompoundCRS *>(l_sourceCRS.get());
if (compound) {
const auto &components = compound->componentReferenceSystems();
if (!components.empty()) {
- horiz = components.front().get();
+ horiz = components.front().as_nullable();
}
}
- auto geogCRS = dynamic_cast<const crs::GeographicCRS *>(horiz);
- if (geogCRS) {
+ srcGeogCRS = std::dynamic_pointer_cast<crs::GeographicCRS>(horiz);
+ if (srcGeogCRS) {
formatter->setOmitProjLongLatIfPossible(true);
formatter->startInversion();
- geogCRS->_exportToPROJString(formatter);
+ srcGeogCRS->_exportToPROJString(formatter);
formatter->stopInversion();
formatter->setOmitProjLongLatIfPossible(false);
}
- auto projCRS = dynamic_cast<const crs::ProjectedCRS *>(horiz);
+ auto projCRS = dynamic_cast<const crs::ProjectedCRS *>(horiz.get());
if (projCRS) {
formatter->startInversion();
formatter->pushOmitZUnitConversion();
@@ -6325,6 +6336,30 @@ void Conversion::_exportToPROJString(
}
bConversionDone = true;
bEllipsoidParametersDone = true;
+ } else if (methodEPSGCode == EPSG_CODE_METHOD_GEOGRAPHIC_TOPOCENTRIC) {
+ if (!srcGeogCRS) {
+ throw io::FormattingException(
+ "Export of Geographic/Topocentric conversion to a PROJ string "
+ "requires an input geographic CRS");
+ }
+
+ formatter->addStep("cart");
+ srcGeogCRS->ellipsoid()->_exportToPROJString(formatter);
+
+ formatter->addStep("topocentric");
+ const auto latOrigin = parameterValueNumeric(
+ EPSG_CODE_PARAMETER_LATITUDE_TOPOGRAPHIC_ORIGIN,
+ common::UnitOfMeasure::DEGREE);
+ const auto lonOrigin = parameterValueNumeric(
+ EPSG_CODE_PARAMETER_LONGITUDE_TOPOGRAPHIC_ORIGIN,
+ common::UnitOfMeasure::DEGREE);
+ const auto heightOrigin = parameterValueNumeric(
+ EPSG_CODE_PARAMETER_ELLIPSOIDAL_HEIGHT_TOPOCENTRIC_ORIGIN,
+ common::UnitOfMeasure::METRE);
+ formatter->addParam("lat_0", latOrigin);
+ formatter->addParam("lon_0", lonOrigin);
+ formatter->addParam("h_0", heightOrigin);
+ bConversionDone = true;
}
auto l_targetCRS = targetCRS();
@@ -6449,7 +6484,9 @@ void Conversion::_exportToPROJString(
}
if (!bEllipsoidParametersDone) {
- auto targetGeogCRS = horiz->extractGeographicCRS();
+ auto targetGeodCRS = horiz->extractGeodeticCRS();
+ auto targetGeogCRS =
+ std::dynamic_pointer_cast<crs::GeographicCRS>(targetGeodCRS);
if (targetGeogCRS) {
if (formatter->getCRSExport()) {
targetGeogCRS->addDatumInfoToPROJString(formatter);
@@ -6458,6 +6495,8 @@ void Conversion::_exportToPROJString(
targetGeogCRS->primeMeridian()->_exportToPROJString(
formatter);
}
+ } else if (targetGeodCRS) {
+ targetGeodCRS->ellipsoid()->_exportToPROJString(formatter);
}
}
diff --git a/src/lib_proj.cmake b/src/lib_proj.cmake
index 67bc1f4e..9f482d19 100644
--- a/src/lib_proj.cmake
+++ b/src/lib_proj.cmake
@@ -173,6 +173,7 @@ set(SRC_LIBPROJ_CONVERSIONS
conversions/geoc.cpp
conversions/geocent.cpp
conversions/noop.cpp
+ conversions/topocentric.cpp
conversions/set.cpp
conversions/unitconvert.cpp
)
diff --git a/src/pj_list.h b/src/pj_list.h
index bcdc189e..d00e780f 100644
--- a/src/pj_list.h
+++ b/src/pj_list.h
@@ -155,6 +155,7 @@ PROJ_HEAD(tinshift, "Triangulation based transformation")
PROJ_HEAD(tissot, "Tissot Conic")
PROJ_HEAD(tmerc, "Transverse Mercator")
PROJ_HEAD(tobmerc, "Tobler-Mercator")
+PROJ_HEAD(topocentric, "Geocentric/Topocentric conversion")
PROJ_HEAD(tpeqd, "Two Point Equidistant")
PROJ_HEAD(tpers, "Tilted perspective")
PROJ_HEAD(unitconvert, "Unit conversion")
diff --git a/src/proj_constants.h b/src/proj_constants.h
index a3da2c10..ce3b2157 100644
--- a/src/proj_constants.h
+++ b/src/proj_constants.h
@@ -651,4 +651,24 @@
#define EPSG_CODE_METHOD_HEIGHT_DEPTH_REVERSAL 1068
#define EPSG_NAME_METHOD_HEIGHT_DEPTH_REVERSAL "Height Depth Reversal"
+/* ------------------------------------------------------------------------ */
+
+#define EPSG_NAME_METHOD_GEOCENTRIC_TOPOCENTRIC "Geocentric/topocentric conversions"
+#define EPSG_CODE_METHOD_GEOCENTRIC_TOPOCENTRIC 9836
+
+#define EPSG_NAME_PARAMETER_GEOCENTRIC_X_TOPOCENTRIC_ORIGIN "Geocentric X of topocentric origin"
+#define EPSG_CODE_PARAMETER_GEOCENTRIC_X_TOPOCENTRIC_ORIGIN 8837
+
+#define EPSG_NAME_PARAMETER_GEOCENTRIC_Y_TOPOCENTRIC_ORIGIN "Geocentric Y of topocentric origin"
+#define EPSG_CODE_PARAMETER_GEOCENTRIC_Y_TOPOCENTRIC_ORIGIN 8838
+
+#define EPSG_NAME_PARAMETER_GEOCENTRIC_Z_TOPOCENTRIC_ORIGIN "Geocentric Z of topocentric origin"
+#define EPSG_CODE_PARAMETER_GEOCENTRIC_Z_TOPOCENTRIC_ORIGIN 8839
+
+/* ------------------------------------------------------------------------ */
+
+#define EPSG_NAME_METHOD_GEOGRAPHIC_TOPOCENTRIC "Geographic/topocentric conversions"
+#define EPSG_CODE_METHOD_GEOGRAPHIC_TOPOCENTRIC 9837
+
+
#endif /* PROJ_CONSTANTS_INCLUDED */
diff --git a/test/gie/builtins.gie b/test/gie/builtins.gie
index add5d925..72984931 100644
--- a/test/gie/builtins.gie
+++ b/test/gie/builtins.gie
@@ -6681,4 +6681,46 @@ accept -74.25 4.8
expect 80859.033 122543.174
roundtrip 1
+===============================================================================
+# Geocentric/topocentric conversion
+===============================================================================
+
+# Test parameter and point from IOGP Publication 373-7-2 - Geomatics Guidance Note number 7, part 2 - October 2020
+operation +proj=topocentric +ellps=WGS84 +X_0=3652755.3058 +Y_0=319574.6799 +Z_0=5201547.3536
+tolerance 1 mm
+accept 3771793.968 140253.342 5124304.349
+expect -189013.869 -128642.040 -4220.171
+roundtrip 1
+
+===============================================================================
+# Geographic/topocentric conversion
+===============================================================================
+
+# Test parameter and point from IOGP Publication 373-7-2 - Geomatics Guidance Note number 7, part 2 - October 2020
+operation +proj=pipeline +step +proj=cart +ellps=WGS84 +step +proj=topocentric +ellps=WGS84 +lon_0=5 +lat_0=55 +h_0=200
+tolerance 1 mm
+accept 2.12955 53.80939444444444 73
+expect -189013.869 -128642.040 -4220.171
+roundtrip 1
+
+===============================================================================
+# Error cases of topocentric
+===============================================================================
+
+# missing X_0,Y_0,Z_0 or lon_0,lat_0
+operation +proj=topocentric +ellps=WGS84
+expect failure errno missing_args
+
+# missing Z_0
+operation +proj=topocentric +ellps=WGS84 +X_0=0 +Y_0=0
+expect failure errno missing_args
+
+# missing lat_0
+operation +proj=topocentric +ellps=WGS84 +lon_0=0
+expect failure errno missing_args
+
+# X_0 and lon_0 are mutually exclusive
+operation +proj=topocentric +ellps=WGS84 +X_0=0 +lon_0=0
+expect failure errno mutually_exclusive_args
+
</gie-strict>
diff --git a/test/unit/test_operation.cpp b/test/unit/test_operation.cpp
index b6e555b1..269b1bbd 100644
--- a/test/unit/test_operation.cpp
+++ b/test/unit/test_operation.cpp
@@ -10098,6 +10098,128 @@ TEST(operation, derivedGeographicCRS_with_to_wgs84_to_geographicCRS) {
// ---------------------------------------------------------------------------
+TEST(operation, geographic_topocentric) {
+ auto wkt =
+ "PROJCRS[\"EPSG topocentric example A\",\n"
+ " BASEGEOGCRS[\"WGS 84\",\n"
+ " ENSEMBLE[\"World Geodetic System 1984 ensemble\",\n"
+ " MEMBER[\"World Geodetic System 1984 (Transit)\"],\n"
+ " MEMBER[\"World Geodetic System 1984 (G730)\"],\n"
+ " MEMBER[\"World Geodetic System 1984 (G873)\"],\n"
+ " MEMBER[\"World Geodetic System 1984 (G1150)\"],\n"
+ " MEMBER[\"World Geodetic System 1984 (G1674)\"],\n"
+ " MEMBER[\"World Geodetic System 1984 (G1762)\"],\n"
+ " ELLIPSOID[\"WGS 84\",6378137,298.257223563,\n"
+ " LENGTHUNIT[\"metre\",1]],\n"
+ " ENSEMBLEACCURACY[2.0]],\n"
+ " PRIMEM[\"Greenwich\",0,\n"
+ " ANGLEUNIT[\"degree\",0.0174532925199433]],\n"
+ " ID[\"EPSG\",4979]],\n"
+ " CONVERSION[\"EPSG topocentric example A\",\n"
+ " METHOD[\"Geographic/topocentric conversions\",\n"
+ " ID[\"EPSG\",9837]],\n"
+ " PARAMETER[\"Latitude of topocentric origin\",55,\n"
+ " ANGLEUNIT[\"degree\",0.0174532925199433],\n"
+ " ID[\"EPSG\",8834]],\n"
+ " PARAMETER[\"Longitude of topocentric origin\",5,\n"
+ " ANGLEUNIT[\"degree\",0.0174532925199433],\n"
+ " ID[\"EPSG\",8835]],\n"
+ " PARAMETER[\"Ellipsoidal height of topocentric origin\",0,\n"
+ " LENGTHUNIT[\"metre\",1],\n"
+ " ID[\"EPSG\",8836]]],\n"
+ " CS[Cartesian,3],\n"
+ " AXIS[\"topocentric East (U)\",east,\n"
+ " ORDER[1],\n"
+ " LENGTHUNIT[\"metre\",1]],\n"
+ " AXIS[\"topocentric North (V)\",north,\n"
+ " ORDER[2],\n"
+ " LENGTHUNIT[\"metre\",1]],\n"
+ " AXIS[\"topocentric height (W)\",up,\n"
+ " ORDER[3],\n"
+ " LENGTHUNIT[\"metre\",1]],\n"
+ " USAGE[\n"
+ " SCOPE[\"Example only (fictitious).\"],\n"
+ " AREA[\"Description of the extent of the CRS.\"],\n"
+ " BBOX[-90,-180,90,180]],\n"
+ " ID[\"EPSG\",5819]]";
+ auto obj = WKTParser().createFromWKT(wkt);
+ auto dst = nn_dynamic_pointer_cast<ProjectedCRS>(obj);
+ ASSERT_TRUE(dst != nullptr);
+ auto op = CoordinateOperationFactory::create()->createOperation(
+ GeographicCRS::EPSG_4979, NN_CHECK_ASSERT(dst));
+ ASSERT_TRUE(op != nullptr);
+ EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()),
+ "+proj=pipeline "
+ "+step +proj=axisswap +order=2,1 "
+ "+step +proj=unitconvert +xy_in=deg +z_in=m +xy_out=rad +z_out=m "
+ "+step +proj=cart +ellps=WGS84 "
+ "+step +proj=topocentric +lat_0=55 +lon_0=5 +h_0=0 +ellps=WGS84");
+}
+
+// ---------------------------------------------------------------------------
+
+TEST(operation, geocentric_topocentric) {
+ auto wkt =
+ "PROJCRS[\"EPSG topocentric example B\",\n"
+ " BASEGEODCRS[\"WGS 84\",\n"
+ " ENSEMBLE[\"World Geodetic System 1984 ensemble\",\n"
+ " MEMBER[\"World Geodetic System 1984 (Transit)\"],\n"
+ " MEMBER[\"World Geodetic System 1984 (G730)\"],\n"
+ " MEMBER[\"World Geodetic System 1984 (G873)\"],\n"
+ " MEMBER[\"World Geodetic System 1984 (G1150)\"],\n"
+ " MEMBER[\"World Geodetic System 1984 (G1674)\"],\n"
+ " MEMBER[\"World Geodetic System 1984 (G1762)\"],\n"
+ " ELLIPSOID[\"WGS 84\",6378137,298.257223563,\n"
+ " LENGTHUNIT[\"metre\",1]],\n"
+ " ENSEMBLEACCURACY[2.0]],\n"
+ " PRIMEM[\"Greenwich\",0,\n"
+ " ANGLEUNIT[\"degree\",0.0174532925199433]],\n"
+ " ID[\"EPSG\",4978]],\n"
+ " CONVERSION[\"EPSG topocentric example B\",\n"
+ " METHOD[\"Geocentric/topocentric conversions\",\n"
+ " ID[\"EPSG\",9836]],\n"
+ " PARAMETER[\"Geocentric X of topocentric origin\",3771793.97,\n"
+ " LENGTHUNIT[\"metre\",1],\n"
+ " ID[\"EPSG\",8837]],\n"
+ " PARAMETER[\"Geocentric Y of topocentric origin\",140253.34,\n"
+ " LENGTHUNIT[\"metre\",1],\n"
+ " ID[\"EPSG\",8838]],\n"
+ " PARAMETER[\"Geocentric Z of topocentric origin\",5124304.35,\n"
+ " LENGTHUNIT[\"metre\",1],\n"
+ " ID[\"EPSG\",8839]]],\n"
+ " CS[Cartesian,3],\n"
+ " AXIS[\"topocentric East (U)\",east,\n"
+ " ORDER[1],\n"
+ " LENGTHUNIT[\"metre\",1]],\n"
+ " AXIS[\"topocentric North (V)\",north,\n"
+ " ORDER[2],\n"
+ " LENGTHUNIT[\"metre\",1]],\n"
+ " AXIS[\"topocentric height (W)\",up,\n"
+ " ORDER[3],\n"
+ " LENGTHUNIT[\"metre\",1]],\n"
+ " USAGE[\n"
+ " SCOPE[\"Example only (fictitious).\"],\n"
+ " AREA[\"Description of the extent of the CRS.\"],\n"
+ " BBOX[-90,-180,90,180]],\n"
+ " ID[\"EPSG\",5820]]";
+ auto dbContext = DatabaseContext::create();
+ // Need a database so that EPSG:4978 is resolved
+ auto obj = WKTParser().attachDatabaseContext(dbContext).createFromWKT(wkt);
+ auto dst = nn_dynamic_pointer_cast<ProjectedCRS>(obj);
+ ASSERT_TRUE(dst != nullptr);
+ auto f(NS_PROJ::io::WKTFormatter::create(
+ NS_PROJ::io::WKTFormatter::Convention::WKT2_2019));
+ std::cerr << GeodeticCRS::EPSG_4978->exportToWKT(f.get()) << std::endl;
+ auto op = CoordinateOperationFactory::create()->createOperation(
+ GeodeticCRS::EPSG_4978, NN_CHECK_ASSERT(dst));
+ ASSERT_TRUE(op != nullptr);
+ EXPECT_EQ(op->exportToPROJString(PROJStringFormatter::create().get()),
+ "+proj=topocentric +X_0=3771793.97 +Y_0=140253.34 "
+ "+Z_0=5124304.35 +ellps=WGS84");
+}
+
+// ---------------------------------------------------------------------------
+
TEST(operation, mercator_variant_A_to_variant_B) {
auto projCRS = ProjectedCRS::create(
PropertyMap(), GeographicCRS::EPSG_4326,