From a3e41dd33754e4ff6106d46e863fa691d4afdfc6 Mon Sep 17 00:00:00 2001 From: Kristian Evers Date: Mon, 8 May 2017 16:50:13 +0200 Subject: Extended Helmert transformation to 14-parameters Extended Helmert transformation to 14-parameters. This commit extends the Helmert transformationto the fourth dimension and enables spatio-temporal datum shifts in PROJ.4. On top of the usual 7 parameters (+x, +y, +z, +s, +rx, +ry, +rz) the rates of change of the seven parameters can now be used as well. The new parameters are called +dx, +dy, +dx, +ds, +drx, +dry and +drz. To keep track of the datum epoch and coordinate epoch two additional parameters have been added to the Helmert transformation, one of which is mandatory in the 14-parameter case. The mandatory datum epoch is controlled with +epoch (given in YYYY.yyyy format) and the coordinate, or observation, epoch is either controlled in the proj-string with +tobs or by using 4D-coordinates when transformating coordinates with pj_trans(). See the test functions for examples of how to set up the transformation with 14 parameters. --- src/PJ_helmert.c | 541 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 372 insertions(+), 169 deletions(-) (limited to 'src') diff --git a/src/PJ_helmert.c b/src/PJ_helmert.c index af35582a..6dee7e5d 100644 --- a/src/PJ_helmert.c +++ b/src/PJ_helmert.c @@ -6,7 +6,8 @@ ************************************************************************ - Implements 3- and 7-parameter Helmert transformations for 3D data. + Implements 3-, 7- and 14-parameter Helmert transformations for 3D + data. Primarily useful for implementation of datum shifts in transformation pipelines. @@ -14,7 +15,8 @@ ************************************************************************ Thomas Knudsen, thokn@sdfe.dk, 2016-05-24/06-05 -Last update: 2016-11-24 +Kristian Evers, kreve@sdfe.dk, 2017-05-01 +Last update: 2017-05-08 ************************************************************************ * Copyright (c) 2016, Thomas Knudsen / SDFE @@ -46,7 +48,7 @@ Last update: 2016-11-24 #include #include #include -PROJ_HEAD(helmert, "3- and 7-parameter Helmert shift"); +PROJ_HEAD(helmert, "3-, 7- and 14-parameter Helmert shift"); static XYZ helmert_forward_3d (LPZ lpz, PJ *P); static LPZ helmert_reverse_3d (XYZ xyz, PJ *P); @@ -81,10 +83,17 @@ struct pj_opaque_helmert { Projection specific elements for the "helmert" PJ object ************************************************************************/ XYZ xyz; + XYZ xyz_0; + XYZ dxyz; PJ_OPK opk; + PJ_OPK opk_0; + PJ_OPK dopk; double scale; + double scale_0; + double dscale; double R[3][3]; - int no_rotation; + double epoch, t_obs; + int no_rotation, approximate, transpose; }; @@ -101,7 +110,210 @@ struct pj_opaque_helmert { #define R21 (Q->R[2][1]) #define R22 (Q->R[2][2]) +static void update_parameters(PJ *P) { + /************************************************************************** + + Update transformation parameters. + --------------------------------- + + The 14-parameter Helmert transformation is at it's core the same as the + 7-parameter transformation, since the transformation parameters are + projected forward or backwards in time via the rate of changes of the + parameters. The transformation parameters are calculated for a specific + epoch before the actual Helmert transformation is carried out. + + The transformation parameters are updated with the following equation [0]: + + . + P(t) = P(EPOCH) + P * (t - EPOCH) + + . + where EPOCH is the epoch indicated in the above table and P is the rate + of that parameter. + + [0] http://itrf.ign.fr/doc_ITRF/Transfo-ITRF2008_ITRFs.txt + **************************************************************************/ + + struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *) P->opaque; + double dt = Q->t_obs - Q->epoch; + + Q->xyz.x = Q->xyz_0.x + Q->dxyz.x * dt; + Q->xyz.y = Q->xyz_0.y + Q->dxyz.y * dt; + Q->xyz.z = Q->xyz_0.z + Q->dxyz.z * dt; + + Q->opk.o = Q->opk_0.o + Q->dopk.o * dt; + Q->opk.p = Q->opk_0.p + Q->dopk.p * dt; + Q->opk.k = Q->opk_0.k + Q->dopk.k * dt; + + Q->scale = Q->scale_0 + Q->dscale * dt; + + /* debugging output */ + if (pj_err_level(P, PJ_ERR_TELL) >= PJ_LOG_TRACE) { + pj_log_trace(P, "Transformation parameters for observation epoch %g:", Q->t_obs); + pj_log_trace(P, "x: %g", Q->xyz.x); + pj_log_trace(P, "y: %g", Q->xyz.y); + pj_log_trace(P, "z: %g", Q->xyz.z); + pj_log_trace(P, "s: %g", Q->scale*1e-6); + pj_log_trace(P, "rx: %g", Q->opk.o); + pj_log_trace(P, "ry: %g", Q->opk.p); + pj_log_trace(P, "rz: %g", Q->opk.k); + } + return; +} + +static void build_rot_matrix(PJ *P) { + /************************************************************************** + + Build rotation matrix. + ---------------------- + + Here we rename rotation indices from omega, phi, kappa (opk), to + fi (i.e. phi), theta, psi (ftp), in order to reduce the mental agility + needed to implement the expression for the rotation matrix derived over + at https://en.wikipedia.org/wiki/Rotation_formalisms_in_three_dimensions + The relevant section is Euler angles ( z-’-x" intrinsic) -> Rotation matrix + + If the option "approximate" is set, small angle approximations are used: + The matrix elements are approximated by expanding the trigonometric + functions to linear order (i.e. cos(x) = 1, sin(x) = x), and discarding + products of second order. + + This was a useful hack when calculating by hand was the only option, + but in general, today, should be avoided because: + + 1. It does not save much computation time, as the rotation matrix + is built only once and probably used many times (except when + transforming spatio-temporal coordinates). + + 2. The error induced may be too large for ultra high accuracy + applications: the Earth is huge and the linear error is + approximately the angular error multiplied by the Earth radius. + + However, in many cases the approximation is necessary, since it has + been used historically: Rotation angles from older published datum + shifts may actually be a least squares fit to the linearized rotation + approximation, hence not being strictly valid for deriving the full + rotation matrix. + + So in order to fit historically derived coordinates, the access to + the approximate rotation matrix is necessary - at least in principle. + + Also, when using any published datum transformation information, one + should always check which convention (exact or approximate rotation + matrix) is expected, and whether the induced error for selecting + the opposite convention is acceptable (which it often is). + + + Sign conventions + ---------------- + + Take care: Two different sign conventions exist. + + Conceptually they relate to whether we rotate the coordinate system + or the "position vector" (the vector going from the coordinate system + origin to the point being transformed, i.e. the point coordinates + interpreted as vector coordinates). + + Switching between the "position vector" and "coordinate system" + conventions is simply a matter of switching the sign of the rotation + angles, which algebraically also translates into a transposition of + the rotation matrix. + + Hence, as geodetic constants should preferably be referred to exactly + as published, the "transpose" option provides the ability to switch + between the conventions. + + ***************************************************************************/ + struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *) P->opaque; + + double f, t, p; /* phi/fi , theta, psi */ + double cf, ct, cp; /* cos (fi, theta, psi) */ + double sf, st, sp; /* sin (fi, theta, psi) */ + + /* rename (omega, phi, kappa) to (fi, theta, psi) */ + f = Q->opk.o; + t = Q->opk.p; + p = Q->opk.k; + + if (Q->approximate) { + R00 = 1; + R01 = p; + R02 = -t; + + R10 = -p; + R11 = 1; + R12 = f; + + R20 = t; + R21 = -f; + R22 = 1; + } + else { + cf = cos(f); + sf = sin(f); + ct = cos(t); + st = sin(t); + cp = cos(p); + sp = sin(p); + + + R00 = ct*cp; + R01 = cf*sp + sf*st*cp; + R02 = sf*sp - cf*st*cp; + + R10 = -ct*sp; + R11 = cf*cp - sf*st*sp; + R12 = sf*cp + cf*st*sp; + + R20 = st; + R21 = -sf*ct; + R22 = cf*ct; + } + + /* + For comparison: Description from Engsager/Poder implementation + in set_dtm_1.c (trlib) + + DATUM SHIFT: + TO = scale * ROTZ * ROTY * ROTX * FROM + TRANSLA + + ( cz sz 0) (cy 0 -sy) (1 0 0) + ROTZ=(-sz cz 0), ROTY=(0 1 0), ROTX=(0 cx sx) + ( 0 0 1) (sy 0 cy) (0 -sx cx) + + trp->r11 = cos_ry*cos_rz; + trp->r12 = cos_rx*sin_rz + sin_rx*sin_ry*cos_rz; + trp->r13 = sin_rx*sin_rz - cos_rx*sin_ry*cos_rz; + + trp->r21 = -cos_ry*sin_rz; + trp->r22 = cos_rx*cos_rz - sin_rx*sin_ry*sin_rz; + trp->r23 = sin_rx*cos_rz + cos_rx*sin_ry*sin_rz; + + trp->r31 = sin_ry; + trp->r32 = -sin_rx*cos_ry; + trp->r33 = cos_rx*cos_ry; + + trp->scale = 1.0 + scale; + */ + + + if (Q->transpose) { + double r; + r = R01; R01 = R10; R10 = r; + r = R02; R02 = R20; R20 = r; + r = R12; R12 = R21; R21 = r; + } + /* some debugging output */ + if (pj_err_level(P, PJ_ERR_TELL) >= PJ_LOG_TRACE) { + pj_log_trace(P, "Rotation Matrix:"); + pj_log_trace(P, " | % 6.6g % 6.6g % 6.6g |", R00, R01, R02); + pj_log_trace(P, " | % 6.6g % 6.6g % 6.6g |", R10, R11, R12); + pj_log_trace(P, " | % 6.6g % 6.6g % 6.6g |", R20, R21, R22); + } + + return; +} @@ -143,6 +355,7 @@ static XYZ helmert_forward_3d (LPZ lpz, PJ *P) { struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *) P->opaque; PJ_TRIPLET point; double X, Y, Z, scale; + if (Q->no_rotation) { point.xyz.x = lpz.lam + Q->xyz.x; point.xyz.y = lpz.phi + Q->xyz.y; @@ -203,29 +416,45 @@ static LPZ helmert_reverse_3d (XYZ xyz, PJ *P) { static PJ_OBS helmert_forward_obs (PJ_OBS point, PJ *P) { + struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *) P->opaque; + + /* We only need to rebuild the rotation matrix if the + * observation time is different from the last call */ + if (point.coo.xyzt.t != Q->t_obs) { + Q->t_obs = point.coo.xyzt.t; + update_parameters(P); + build_rot_matrix(P); + } + point.coo.xyz = helmert_forward_3d (point.coo.lpz, P); + return point; } static PJ_OBS helmert_reverse_obs (PJ_OBS point, PJ *P) { + struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *) P->opaque; + + /* We only need to rebuild the rotation matrix if the + * observation time is different from the last call */ + if (point.coo.xyzt.t != Q->t_obs) { + Q->t_obs = point.coo.xyzt.t; + update_parameters(P); + build_rot_matrix(P); + } + point.coo.lpz = helmert_reverse_3d (point.coo.xyz, P); + return point; } -/* Milliarcsecond to radians */ -#define MAS_TO_RAD (DEG_TO_RAD / 3600000.0) - +/* Arcsecond to radians */ +#define ARCSEC_TO_RAD (DEG_TO_RAD / 3600.0) /***********************************************************************/ PJ *PROJECTION(helmert) { /***********************************************************************/ - XYZ xyz = {0, 0, 0}; - PJ_OPK opk = {0, 0, 0}; - double scale = 0; - int approximate = 0, transpose = 0; - struct pj_opaque_helmert *Q = pj_calloc (1, sizeof (struct pj_opaque_helmert)); if (0==Q) return freeup_msg (P, ENOMEM); @@ -233,8 +462,6 @@ PJ *PROJECTION(helmert) { P->fwdobs = helmert_forward_obs; P->invobs = helmert_reverse_obs; - P->fwdobs = 0; - P->invobs = 0; P->fwd3d = helmert_forward_3d; P->inv3d = helmert_reverse_3d; P->fwd = helmert_forward; @@ -245,196 +472,104 @@ PJ *PROJECTION(helmert) { /* Translations */ if (pj_param (P->ctx, P->params, "tx").i) - xyz.x = pj_param (P->ctx, P->params, "dx").f; + Q->xyz_0.x = pj_param (P->ctx, P->params, "dx").f; if (pj_param (P->ctx, P->params, "ty").i) - xyz.y = pj_param (P->ctx, P->params, "dy").f; + Q->xyz_0.y = pj_param (P->ctx, P->params, "dy").f; if (pj_param (P->ctx, P->params, "tz").i) - xyz.z = pj_param (P->ctx, P->params, "dz").f; + Q->xyz_0.z = pj_param (P->ctx, P->params, "dz").f; /* Rotations */ if (pj_param (P->ctx, P->params, "trx").i) - opk.o = pj_param (P->ctx, P->params, "drx").f * MAS_TO_RAD; + Q->opk_0.o = pj_param (P->ctx, P->params, "drx").f * ARCSEC_TO_RAD; if (pj_param (P->ctx, P->params, "try").i) - opk.p = pj_param (P->ctx, P->params, "dry").f * MAS_TO_RAD; + Q->opk_0.p = pj_param (P->ctx, P->params, "dry").f * ARCSEC_TO_RAD; if (pj_param (P->ctx, P->params, "trz").i) - opk.k = pj_param (P->ctx, P->params, "drz").f * MAS_TO_RAD; + Q->opk_0.k = pj_param (P->ctx, P->params, "drz").f * ARCSEC_TO_RAD; /* Scale */ if (pj_param (P->ctx, P->params, "ts").i) - scale = pj_param (P->ctx, P->params, "ds").f; - - /* Use small angle approximations? */ - if (pj_param (P->ctx, P->params, "bapprox").i) - approximate = 1; - - /* Use "other" rotation sign convention? */ - if (pj_param (P->ctx, P->params, "ttranspose").i) - transpose = 1; - - Q->xyz = xyz; - Q->opk = opk; - Q->scale = scale; - - if ((opk.o==0) && (opk.p==0) && (opk.k==0) && (scale==0)) { - Q->no_rotation = 1; - return P; - } + Q->scale_0 = pj_param (P->ctx, P->params, "ds").f; - /************************************************************************** - - Build rotation matrix. - ---------------------- - - Here we rename rotation indices from omega, phi, kappa (opk), to - fi (i.e. phi), theta, psi (ftp), in order to reduce the mental agility - needed to implement the expression for the rotation matrix derived over - at https://en.wikipedia.org/wiki/Rotation_formalisms_in_three_dimensions - - If the option "approximate" is set, small angle approximations are used: - The matrix elements are approximated by expanding the trigonometric - functions to linear order (i.e. cos(x) = 1, sin(x) = x), and discarding - products of second order. - - This was a useful hack when calculating by hand was the only option, - but in general, today, should be avoided because: + /* Translation rates */ + if (pj_param(P->ctx, P->params, "tdx").i) + Q->dxyz.x = pj_param (P->ctx, P->params, "ddx").f; - 1. It does not save much computation time, as the rotation matrix - is built only once, and probably used many times. + if (pj_param(P->ctx, P->params, "tdy").i) + Q->dxyz.y = pj_param (P->ctx, P->params, "ddy").f; - 2. The error induced may be too large for ultra high accuracy - applications: the Earth is huge and the linear error is - approximately the angular error multiplied by the Earth radius. + if (pj_param(P->ctx, P->params, "tdz").i) + Q->dxyz.z = pj_param (P->ctx, P->params, "ddz").f; - However, in many cases the approximation is necessary, since it has - been used historically: Rotation angles from older published datum - shifts may actually be a least squares fit to the linearized rotation - approximation, hence not being strictly valid for deriving the full - rotation matrix. + /* Rotations rates */ + if (pj_param (P->ctx, P->params, "tdrx").i) + Q->dopk.o = pj_param (P->ctx, P->params, "ddrx").f * ARCSEC_TO_RAD; - So in order to fit historically derived coordinates, the access to - the approximate rotation matrix is necessary - at least in principle. + if (pj_param (P->ctx, P->params, "tdry").i) + Q->dopk.p = pj_param (P->ctx, P->params, "ddry").f * ARCSEC_TO_RAD; - Also, when using any published datum transformation information, one - should always check which convention (exact or approximate rotation - matrix) is expected, and whether the induced error for selecting - the opposite convention is acceptable (which it often is). + if (pj_param (P->ctx, P->params, "tdrz").i) + Q->dopk.k = pj_param (P->ctx, P->params, "ddrz").f * ARCSEC_TO_RAD; - Sign conventions - ---------------- + /* Scale rate */ + if (pj_param (P->ctx, P->params, "tds").i) + Q->dscale = pj_param (P->ctx, P->params, "dds").f; - Take care: Two different sign conventions exist. - Conceptually they relate to whether we rotate the coordinate system - or the "position vector" (the vector going from the coordinate system - origin to the point being transformed, i.e. the point coordinates - interpreted as vector coordinates). + /* Epoch */ + if (pj_param(P->ctx, P->params, "tepoch").i) + Q->epoch = pj_param (P->ctx, P->params, "depoch").f; - Switching between the "position vector" and "coordinate system" - conventions is simply a matter of switching the sign of the rotation - angles, which algebraically also translates into a transposition of - the rotation matrix. + if (pj_param(P->ctx, P->params, "ttobs").i) + Q->t_obs = pj_param (P->ctx, P->params, "dtobs").f; - Hence, as geodetic constants should preferably be referred to exactly - as published, the "transpose" option provides the ability to switch - between the conventions. + /* Use small angle approximations? */ + if (pj_param (P->ctx, P->params, "bapprox").i) + Q->approximate = 1; - ***************************************************************************/ - { - double f, t, p; /* phi/fi , theta, psi */ - double cf, ct, cp; /* cos (fi, theta, psi) */ - double sf, st, sp; /* sin (fi, theta, psi) */ - - /* rename (omega, phi, kappa) to (fi, theta, psi) */ - f = opk.o; - t = opk.p; - p = opk.k; - - if (approximate) { - R00 = 1; - R01 = p; - R02 = -t; - - R10 = -p; - R11 = 1; - R12 = f; - - R20 = t; - R21 = -f; - R22 = 1; - } - else { - cf = cos(f); - sf = sin(f); - ct = cos(t); - st = sin(t); - cp = cos(p); - sp = sin(p); - - - R00 = ct*cp; - R01 = cf*sp + sf*st*cp; - R02 = sf*sp - cf*st*cp; - - R10 = -ct*sp; - R11 = cf*cp - sf*st*sp; - R12 = sf*cp + cf*st*sp; - - R20 = st; - R21 = -sf*ct; - R22 = cf*ct; - } + /* Use "other" rotation sign convention? */ + if (pj_param (P->ctx, P->params, "ttranspose").i) + Q->transpose = 1; + + Q->xyz = Q->xyz_0; + Q->opk = Q->opk_0; + Q->scale = Q->scale_0; + + /* Let's help with debugging */ + if (pj_err_level(P, PJ_ERR_TELL) >= PJ_LOG_DEBUG) { + pj_log_debug(P, "Helmert parameters:"); + pj_log_debug(P, "x= % 3.5f y= % 3.5f z= % 3.5f", Q->xyz.x, Q->xyz.y, Q->xyz.z); + pj_log_debug(P, "rx= % 3.5f ry= % 3.5f rz= % 3.5f", + Q->opk.o / ARCSEC_TO_RAD, Q->opk.p / ARCSEC_TO_RAD, Q->opk.k / ARCSEC_TO_RAD); + pj_log_debug(P, "s=% 3.5f approximate=% d transpose=% d", + Q->scale, Q->approximate, Q->transpose); + pj_log_debug(P, "dx= % 3.5f dy= % 3.5f dz= % 3.5f", Q->dxyz.x, Q->dxyz.y, Q->dxyz.z); + pj_log_debug(P, "drx=% 3.5f dry=% 3.5f drz=% 3.5f", Q->dopk.o, Q->dopk.p, Q->dopk.k); + pj_log_debug(P, "ds=% 3.5f epoch=% 5.5f tobs=% 5.5f", Q->dscale, Q->epoch, Q->t_obs); } - /* - For comparison: Description from Engsager/Poder implementation - in set_dtm_1.c (trlib) - - DATUM SHIFT: - TO = scale * ROTZ * ROTY * ROTX * FROM + TRANSLA - - ( cz sz 0) (cy 0 -sy) (1 0 0) - ROTZ=(-sz cz 0), ROTY=(0 1 0), ROTX=(0 cx sx) - ( 0 0 1) (sy 0 cy) (0 -sx cx) - - trp->r11 = cos_ry*cos_rz; - trp->r12 = cos_rx*sin_rz + sin_rx*sin_ry*cos_rz; - trp->r13 = sin_rx*sin_rz - cos_rx*sin_ry*cos_rz; - - trp->r21 = -cos_ry*sin_rz; - trp->r22 = cos_rx*cos_rz - sin_rx*sin_ry*sin_rz; - trp->r23 = sin_rx*cos_rz + cos_rx*sin_ry*sin_rz; - - trp->r31 = sin_ry; - trp->r32 = -sin_rx*cos_ry; - trp->r33 = cos_rx*cos_ry; - - trp->scale = 1.0 + scale; - */ - - - if (transpose) { - double r; - r = R01; R01 = R10; R10 = r; - r = R02; R02 = R20; R20 = r; - r = R12; R12 = R21; R21 = r; + if ((Q->opk.o==0) && (Q->opk.p==0) && (Q->opk.k==0) && (Q->scale==0)) { + Q->no_rotation = 1; + return P; } + update_parameters(P); + build_rot_matrix(P); + return P; } - #ifndef PJ_SELFTEST -int pj_helmert_selftest (void) {return 0;} /* self test stub */ +int pj_helmert_selftest (void) {return 0;} #else @@ -447,12 +582,22 @@ static int test (char *args, PJ_TRIPLET in, PJ_TRIPLET expect) { return 5; out.xyz = pj_fwd3d (in.lpz, P); - if (pj_xyz_dist (out.xyz, expect.xyz) > 1e-4) + if (pj_xyz_dist (out.xyz, expect.xyz) > 1e-4) { + pj_log_error(P, "Tolerance of forward calculation not met"); + pj_log_error(P, " Expect: %10.10f, %10.10f, %10.10f", expect.xyz.x, expect.xyz.y, expect.xyz.z); + pj_log_error(P, " Out: %10.10f, %10.10f, %10.10f", out.xyz.x, out.xyz.y, out.xyz.z); + pj_log_level(NULL, 0); return 1; + } out.lpz = pj_inv3d (out.xyz, P); - if (pj_xyz_dist (out.xyz, in.xyz) > 1e-4) + if (pj_xyz_dist (out.xyz, in.xyz) > 1e-4) { + pj_log_error(P, "Tolerance of inverse calculation not met"); + pj_log_error(P, " In: %10.10f, %10.10f, %10.10f", in.xyz.x, in.xyz.y, in.xyz.z); + pj_log_error(P, " Out: %10.10f, %10.10f, %10.10f", out.xyz.x, out.xyz.y, out.xyz.z); + pj_log_level(NULL, 0); return 2; + } return 0; } @@ -460,6 +605,7 @@ static int test (char *args, PJ_TRIPLET in, PJ_TRIPLET expect) { int pj_helmert_selftest (void) { + int ret; /* This example is from Lotti Jivall: @@ -469,7 +615,8 @@ int pj_helmert_selftest (void) { char args1[] = { " +proj=helmert +ellps=GRS80" " +x=0.67678 +y=0.65495 +z=-0.52827" - " +rx=-22.742 +ry=12.667 +rz=22.704 +s=-0.01070" + " +rx=-0.022742 +ry=0.012667 +rz=0.022704 +s=-0.01070" + /*" +rx=-22.742 +ry=12.667 +rz=22.704 +s=-0.01070" */ }; @@ -479,12 +626,68 @@ int pj_helmert_selftest (void) { char args2[] = { " +proj=helmert +ellps=GRS80" " +x=-81.0703 +y=-89.3603 +z=-115.7526" - " +rx=-484.88 +ry=-24.36 +rz=-413.21 +s=-0.540645" + " +rx=-0.48488 +ry=-0.02436 +rz=-0.41321 +s=-0.540645" }; - int ret; + + /* This example is a coordinate from the geodetic observatory in Onsala, + Sweden transformed from ITRF2000 @ 2017.0 to ITRF93 @ 2017.0. + The test coordinate was transformed using GNSStrans. + Transformation parameters published by ITRF: + ftp://itrf.ensg.ign.fr/pub/itrf/ITRF.TP */ + PJ_TRIPLET in3 = {{3370658.378, 711877.314, 5349787.086}}; /* ITRF2000@2017.0 */ + PJ_TRIPLET expect3 = {{3370658.18890, 711877.42370, 5349787.12430}}; /* ITRF93@2017.0 */ + char args3[] = { + " +proj=helmert" + " +x=0.0127 +y=0.0065 +z=-0.0209 +s=0.00195" + " +rx=-0.00039 +ry=0.00080 +rz=-0.00114" + " +dx=-0.0029 +dy=-0.0002 +dz=-0.0006 +ds=0.00001" + " +drx=-0.00011 +dry=-0.00019 +drz=0.00007" + " +epoch=1988.0 +tobs=2017.0 +transpose" + }; + + /* Test the 4D-capabilities of the proj.h API, especially that the rotation + matrix is updated when necessary. Test coordinates from GNSStrans. */ + XYZ expect4a = {3370658.18890, 711877.42370, 5349787.12430}; + XYZ expect4b = {3370658.18087, 711877.42750, 5349787.12648}; + PJ_OBS in4 = {{{3370658.378, 711877.314, 5349787.086, 2017.0}}, {{ 0, 0, 0}}, 0, 0}; + PJ_OBS out; + + PJ *helmert = pj_create( + " +proj=helmert" + " +x=0.0127 +y=0.0065 +z=-0.0209 +s=0.00195" + " +rx=-0.00039 +ry=0.00080 +rz=-0.00114" + " +dx=-0.0029 +dy=-0.0002 +dz=-0.0006 +ds=0.00001" + " +drx=-0.00011 +dry=-0.00019 +drz=0.00007" + " +epoch=1988.0 +transpose" + ); + + + /* Run tests 1-3 */ ret = test (args1, in1, expect1); if (ret) return ret; ret = test (args2, in2, expect2); if (ret) return ret + 10; + ret = test (args3, in3, expect3); if (ret) return ret + 20; + + /* Run test 4 */ + out = pj_trans(helmert, PJ_FWD, in4); + if (pj_xyz_dist (out.coo.xyz, expect4a) > 1e-4) { + pj_log_error(helmert, "Tolerance of test 4a not met!"); + pj_log_error(helmert, " In: %10.10f, %10.10f, %10.10f", in4.coo.xyz.x, in4.coo.xyz.y, in4.coo.xyz.z); + pj_log_error(helmert, " Out: %10.10f, %10.10f, %10.10f", out.coo.xyz.x, out.coo.xyz.y, out.coo.xyz.z); + return 31; + } + + in4.coo.xyzt.t = 2018.0; + out = pj_trans(helmert, PJ_FWD, in4); + if (pj_xyz_dist (out.coo.xyz, expect4b) > 1e-4) { + pj_log_error(helmert, "Tolerance of test 4b not met!"); + pj_log_error(helmert, " In: %10.10f, %10.10f, %10.10f", in4.coo.xyz.x, in4.coo.xyz.y, in4.coo.xyz.z); + pj_log_error(helmert, " Out: %10.10f, %10.10f, %10.10f", out.coo.xyz.x, out.coo.xyz.y, out.coo.xyz.z); + return 32; + } + + pj_free(helmert); + return 0; } -- cgit v1.2.3