From ddd8a1243a5eb43ad209e934526c5c87848343b7 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 24 Nov 2019 19:34:17 +0100 Subject: pipeline.cpp: hopefully make code more readable by using more C++ goodness. No functional change intended (except a likely minor correction/improvement in get_next_non_whatever_unit in the PJ_INV case where the iteration should start at step-1) --- src/pipeline.cpp | 251 ++++++++++++++++++++++++++----------------------------- 1 file changed, 119 insertions(+), 132 deletions(-) diff --git a/src/pipeline.cpp b/src/pipeline.cpp index 847f8194..bc6708b0 100644 --- a/src/pipeline.cpp +++ b/src/pipeline.cpp @@ -101,6 +101,7 @@ Thomas Knudsen, thokn@sdfe.dk, 2016-05-20 #include #include #include +#include #include "geodesic.h" #include "proj.h" @@ -112,12 +113,25 @@ PROJ_HEAD(push, "Save coordinate value on pipeline stack"); /* Projection specific elements for the PJ object */ namespace { // anonymous namespace + +struct Step { + PJ* pj = nullptr; + + Step(PJ* pjIn): pj(pjIn) {} + Step(Step&& other): pj(std::move(other.pj)) { other.pj = nullptr; } + Step(const Step&) = delete; + Step& operator=(const Step&) = delete; + + ~Step() { + proj_destroy(pj); + } +}; + struct pj_opaque { - int steps; - char **argv; - char **current_argv; - PJ **pipeline; - std::stack *stack[4]; + char **argv = nullptr; + char **current_argv = nullptr; + std::vector steps{}; + std::stack stack[4]; }; struct pj_opaque_pushpop { @@ -138,32 +152,27 @@ static PJ_LP pipeline_reverse (PJ_XY xy, PJ *P); void pj_pipeline_assign_context_to_steps( PJ* P, PJ_CONTEXT* ctx ) { - for (int i = 1; i <= static_cast(P->opaque)->steps; i++) - proj_assign_context(static_cast(P->opaque)->pipeline[i], ctx); + auto opaque = static_cast(P->opaque); + for( auto& step: opaque->steps ) + proj_assign_context(step.pj, ctx); } static PJ_COORD pipeline_forward_4d (PJ_COORD point, PJ *P) { - int i, first_step, last_step; - - first_step = 1; - last_step = static_cast(P->opaque)->steps + 1; - - for (i = first_step; i != last_step; i++) - point = proj_trans (static_cast(P->opaque)->pipeline[i], PJ_FWD, point); + auto opaque = static_cast(P->opaque); + for( auto& step: opaque->steps ) + point = proj_trans (step.pj, PJ_FWD, point); return point; } static PJ_COORD pipeline_reverse_4d (PJ_COORD point, PJ *P) { - int i, first_step, last_step; - - first_step = static_cast(P->opaque)->steps; - last_step = 0; - - for (i = first_step; i != last_step; i--) - point = proj_trans (static_cast(P->opaque)->pipeline[i], PJ_INV, point); + auto opaque = static_cast(P->opaque); + for( auto it = opaque->steps.rbegin(); it != opaque->steps.rend(); ++it ) + { + point = proj_trans (it->pj, PJ_INV, point); + } return point; } @@ -173,11 +182,10 @@ static PJ_COORD pipeline_reverse_4d (PJ_COORD point, PJ *P) { static PJ_XYZ pipeline_forward_3d (PJ_LPZ lpz, PJ *P) { PJ_COORD point = {{0,0,0,0}}; - int i; point.lpz = lpz; - - for (i = 1; i <= static_cast(P->opaque)->steps; i++) - point = pj_approx_3D_trans (static_cast(P->opaque)->pipeline[i], PJ_FWD, point); + auto opaque = static_cast(P->opaque); + for( auto& step: opaque->steps ) + point = pj_approx_3D_trans (step.pj, PJ_FWD, point); return point.xyz; } @@ -185,11 +193,12 @@ static PJ_XYZ pipeline_forward_3d (PJ_LPZ lpz, PJ *P) { static PJ_LPZ pipeline_reverse_3d (PJ_XYZ xyz, PJ *P) { PJ_COORD point = {{0,0,0,0}}; - int i; point.xyz = xyz; - - for (i = static_cast(P->opaque)->steps; i > 0 ; i--) - point = pj_approx_3D_trans (static_cast(P->opaque)->pipeline[i], PJ_INV, point); + auto opaque = static_cast(P->opaque); + for( auto it = opaque->steps.rbegin(); it != opaque->steps.rend(); ++it ) + { + point = proj_trans (it->pj, PJ_INV, point); + } return point.lpz; } @@ -199,11 +208,10 @@ static PJ_LPZ pipeline_reverse_3d (PJ_XYZ xyz, PJ *P) { static PJ_XY pipeline_forward (PJ_LP lp, PJ *P) { PJ_COORD point = {{0,0,0,0}}; - int i; point.lp = lp; - - for (i = 1; i <= static_cast(P->opaque)->steps; i++) - point = pj_approx_2D_trans (static_cast(P->opaque)->pipeline[i], PJ_FWD, point); + auto opaque = static_cast(P->opaque); + for( auto& step: opaque->steps ) + point = pj_approx_2D_trans (step.pj, PJ_FWD, point); return point.xy; } @@ -211,10 +219,12 @@ static PJ_XY pipeline_forward (PJ_LP lp, PJ *P) { static PJ_LP pipeline_reverse (PJ_XY xy, PJ *P) { PJ_COORD point = {{0,0,0,0}}; - int i; point.xy = xy; - for (i = static_cast(P->opaque)->steps; i > 0 ; i--) - point = pj_approx_2D_trans (static_cast(P->opaque)->pipeline[i], PJ_INV, point); + auto opaque = static_cast(P->opaque); + for( auto it = opaque->steps.rbegin(); it != opaque->steps.rend(); ++it ) + { + point = pj_approx_2D_trans (it->pj, PJ_INV, point); + } return point.lp; } @@ -223,44 +233,24 @@ static PJ_LP pipeline_reverse (PJ_XY xy, PJ *P) { static PJ *destructor (PJ *P, int errlev) { - int i; if (nullptr==P) return nullptr; if (nullptr==P->opaque) return pj_default_destructor (P, errlev); - /* Deallocate each pipeline step, then pipeline array */ - if (nullptr!=static_cast(P->opaque)->pipeline) - for (i = 0; i < static_cast(P->opaque)->steps; i++) - proj_destroy (static_cast(P->opaque)->pipeline[i+1]); - pj_dealloc (static_cast(P->opaque)->pipeline); + auto opaque = static_cast(P->opaque); - pj_dealloc (static_cast(P->opaque)->argv); - pj_dealloc (static_cast(P->opaque)->current_argv); + pj_dealloc (opaque->argv); + pj_dealloc (opaque->current_argv); - for (i=0; i<4; i++) - delete static_cast(P->opaque)->stack[i]; + delete opaque; + P->opaque = nullptr; return pj_default_destructor(P, errlev); } -static PJ *pj_create_pipeline (PJ *P, size_t steps) { - - /* Room for the pipeline: An array of PJ * with room for sentinels at both ends */ - static_cast(P->opaque)->pipeline = static_cast(pj_calloc (steps + 2, sizeof(PJ *))); - if (nullptr==static_cast(P->opaque)->pipeline) - return nullptr; - - static_cast(P->opaque)->steps = (int)steps; - - return P; -} - - - - /* count the number of args in pipeline definition, and mark all args as used */ static size_t argc_params (paralist *params) { size_t argc = 0; @@ -345,28 +335,30 @@ static void set_ellipsoid(PJ *P) { } -static enum pj_io_units get_next_non_whatever_unit(void *pipeline_data, int step, PJ_DIRECTION dir) { - PJ **pipeline = static_cast(pipeline_data)->pipeline; - int nsteps = static_cast(pipeline_data)->steps; - int i; +static enum pj_io_units get_next_non_whatever_unit(struct pj_opaque* opaque, size_t step, PJ_DIRECTION dir) { + const auto& steps = opaque->steps; + const auto nsteps = steps.size(); if (dir == PJ_FWD) { - for (i = step+1; i<=nsteps; i++) { - if (pj_left(pipeline[i]) != pj_right(pipeline[i])) - return pj_left(pipeline[i]); - if (pj_left(pipeline[i]) != PJ_IO_UNITS_WHATEVER) - return pj_left(pipeline[i]); - if (pj_right(pipeline[i]) != PJ_IO_UNITS_WHATEVER) - return pj_right(pipeline[i]); + for (size_t i = step+1; i1; i--) { - if (pj_right(pipeline[i]) != pj_left(pipeline[i])) - return pj_right(pipeline[i]); - if (pj_right(pipeline[i]) != PJ_IO_UNITS_WHATEVER) - return pj_right(pipeline[i]); - if (pj_left(pipeline[i]) != PJ_IO_UNITS_WHATEVER) - return pj_left(pipeline[i]); + for (size_t i=step; i>0;) { + i--; + auto pj = steps[i].pj; + if (pj_right(pj) != pj_left(pj)) + return pj_right(pj); + if (pj_right(pj) != PJ_IO_UNITS_WHATEVER) + return pj_right(pj); + if (pj_left(pj) != PJ_IO_UNITS_WHATEVER) + return pj_left(pj); } } return PJ_IO_UNITS_WHATEVER; @@ -396,20 +388,17 @@ PJ *OPERATION(pipeline,0) { P->skip_inv_finalize = 1; - P->opaque = static_cast(pj_calloc (1, sizeof(struct pj_opaque))); + P->opaque = new (std::nothrow) pj_opaque(); if (nullptr==P->opaque) return destructor(P, ENOMEM); - /* initialize stack */ - for (i=0; i<4; i++) - static_cast(P->opaque)->stack[i] = new std::stack; - argc = (int)argc_params (P->params); - static_cast(P->opaque)->argv = argv = argv_params (P->params, argc); + auto opaque = static_cast(P->opaque); + opaque->argv = argv = argv_params (P->params, argc); if (nullptr==argv) return destructor (P, ENOMEM); - static_cast(P->opaque)->current_argv = current_argv = static_cast(pj_calloc (argc, sizeof (char *))); + opaque->current_argv = current_argv = static_cast(pj_calloc (argc, sizeof (char *))); if (nullptr==current_argv) return destructor (P, ENOMEM); @@ -435,7 +424,6 @@ PJ *OPERATION(pipeline,0) { } } nsteps--; /* Last instance of +step is just a sentinel */ - static_cast(P->opaque)->steps = nsteps; if (-1==i_pipeline) return destructor (P, PJD_ERR_MALFORMED_PIPELINE); /* ERROR: no pipeline def */ @@ -443,10 +431,6 @@ PJ *OPERATION(pipeline,0) { if (0==nsteps) return destructor (P, PJD_ERR_MALFORMED_PIPELINE); /* ERROR: no pipeline def */ - /* Make room for the pipeline and execution indicators */ - if (nullptr==pj_create_pipeline (P, nsteps)) - return destructor (P, ENOMEM); - set_ellipsoid(P); /* Now loop over all steps, building a new set of arguments for each init */ @@ -498,14 +482,14 @@ PJ *OPERATION(pipeline,0) { next_step->inverted = next_step->inverted == 0 ? 1 : 0; } - static_cast(P->opaque)->pipeline[i+1] = next_step; + opaque->steps.emplace_back(next_step); proj_log_trace (P, "Pipeline at [%p]: step at [%p] (%s) done", P, next_step, current_argv[0]); } /* Require a forward path through the pipeline */ - for (i = 1; i <= nsteps; i++) { - PJ *Q = static_cast(P->opaque)->pipeline[i]; + for( auto& step: opaque->steps) { + PJ *Q = step.pj; if ( ( Q->inverted && (Q->inv || Q->inv3d || Q->fwd4d) ) || (!Q->inverted && (Q->fwd || Q->fwd3d || Q->fwd4d) ) ) { continue; @@ -516,8 +500,8 @@ PJ *OPERATION(pipeline,0) { } /* determine if an inverse operation is possible */ - for (i = 1; i <= nsteps; i++) { - PJ *Q = static_cast(P->opaque)->pipeline[i]; + for( auto& step: opaque->steps) { + PJ *Q = step.pj; if ( pj_has_inverse(Q) ) { continue; } else { @@ -534,31 +518,33 @@ PJ *OPERATION(pipeline,0) { /* proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step ... */ /* where the left-hand side units of the first step shouldn't be changed to RADIANS */ /* as it will result in deg->rad conversions in cs2cs and other applications. */ - PJ **pipeline = static_cast(P->opaque)->pipeline; - for (i=1; i<=nsteps; i++) { - if (pj_left(pipeline[i]) == PJ_IO_UNITS_WHATEVER && pj_right(pipeline[i]) == PJ_IO_UNITS_WHATEVER) { - pipeline[i]->left = get_next_non_whatever_unit(P->opaque, i, PJ_FWD); - pipeline[i]->right = get_next_non_whatever_unit(P->opaque, i, PJ_FWD); + for (i=0; isteps[i].pj; + if (pj_left(pj) == PJ_IO_UNITS_WHATEVER && pj_right(pj) == PJ_IO_UNITS_WHATEVER) { + pj->left = get_next_non_whatever_unit(opaque, i, PJ_FWD); + pj->right = get_next_non_whatever_unit(opaque, i, PJ_FWD); } } - for (i=nsteps; i>0; i--) { - if (pj_left(pipeline[i]) == PJ_IO_UNITS_WHATEVER && pj_right(pipeline[i]) == PJ_IO_UNITS_WHATEVER) { - pipeline[i]->right = get_next_non_whatever_unit(P->opaque, i, PJ_INV); - pipeline[i]->left = get_next_non_whatever_unit(P->opaque, i, PJ_INV); + for (i=nsteps; i>0;) { + --i; + auto pj = opaque->steps[i].pj; + if (pj_left(pj) == PJ_IO_UNITS_WHATEVER && pj_right(pj) == PJ_IO_UNITS_WHATEVER) { + pj->right = get_next_non_whatever_unit(opaque, i, PJ_INV); + pj->left = get_next_non_whatever_unit(opaque, i, PJ_INV); } } /* Check that units between each steps match each other, fail if they don't */ - for (i = 1; i < nsteps; i++) { - enum pj_io_units curr_step_output = pj_right (pipeline[i]); - enum pj_io_units next_step_input = pj_left (pipeline[i+1]); + for (i = 0; i + 1 < nsteps; i++) { + enum pj_io_units curr_step_output = pj_right (opaque->steps[i].pj); + enum pj_io_units next_step_input = pj_left (opaque->steps[i+1].pj); if ( curr_step_output == PJ_IO_UNITS_WHATEVER || next_step_input == PJ_IO_UNITS_WHATEVER ) continue; if ( curr_step_output != next_step_input ) { - proj_log_error (P, "Pipeline: Mismatched units between step %d and %d", i, i+1); + proj_log_error (P, "Pipeline: Mismatched units between step %d and %d", i+1, i+2); return destructor (P, PJD_ERR_MALFORMED_PIPELINE); } } @@ -566,10 +552,10 @@ PJ *OPERATION(pipeline,0) { proj_log_trace (P, "Pipeline: %d steps built. Determining i/o characteristics", nsteps); /* Determine forward input (= reverse output) data type */ - P->left = pj_left (static_cast(P->opaque)->pipeline[1]); + P->left = pj_left (opaque->steps.front().pj); /* Now, correspondingly determine forward output (= reverse input) data type */ - P->right = pj_right (static_cast(P->opaque)->pipeline[nsteps]); + P->right = pj_right (opaque->steps.back().pj); return P; } @@ -581,13 +567,13 @@ static PJ_COORD push(PJ_COORD point, PJ *P) { struct pj_opaque_pushpop *opaque = static_cast(P->opaque); if (opaque->v1) - pipeline->stack[0]->push(point.v[0]); + pipeline->stack[0].push(point.v[0]); if (opaque->v2) - pipeline->stack[1]->push(point.v[1]); + pipeline->stack[1].push(point.v[1]); if (opaque->v3) - pipeline->stack[2]->push(point.v[2]); + pipeline->stack[2].push(point.v[2]); if (opaque->v4) - pipeline->stack[3]->push(point.v[3]); + pipeline->stack[3].push(point.v[3]); return point; } @@ -599,24 +585,24 @@ static PJ_COORD pop(PJ_COORD point, PJ *P) { struct pj_opaque *pipeline = static_cast(P->parent->opaque); struct pj_opaque_pushpop *opaque = static_cast(P->opaque); - if (opaque->v1 && !pipeline->stack[0]->empty()) { - point.v[0] = pipeline->stack[0]->top(); - pipeline->stack[0]->pop(); + if (opaque->v1 && !pipeline->stack[0].empty()) { + point.v[0] = pipeline->stack[0].top(); + pipeline->stack[0].pop(); } - if (opaque->v2 && !pipeline->stack[1]->empty()) { - point.v[1] = pipeline->stack[1]->top(); - pipeline->stack[1]->pop(); + if (opaque->v2 && !pipeline->stack[1].empty()) { + point.v[1] = pipeline->stack[1].top(); + pipeline->stack[1].pop(); } - if (opaque->v3 && !pipeline->stack[2]->empty()) { - point.v[2] = pipeline->stack[2]->top(); - pipeline->stack[2]->pop(); + if (opaque->v3 && !pipeline->stack[2].empty()) { + point.v[2] = pipeline->stack[2].top(); + pipeline->stack[2].pop(); } - if (opaque->v4 && !pipeline->stack[3]->empty()) { - point.v[3] = pipeline->stack[3]->top(); - pipeline->stack[3]->pop(); + if (opaque->v4 && !pipeline->stack[3].empty()) { + point.v[3] = pipeline->stack[3].top(); + pipeline->stack[3].pop(); } return point; @@ -625,21 +611,22 @@ static PJ_COORD pop(PJ_COORD point, PJ *P) { static PJ *setup_pushpop(PJ *P) { - P->opaque = static_cast(pj_calloc (1, sizeof(struct pj_opaque_pushpop))); + auto opaque = static_cast(pj_calloc (1, sizeof(struct pj_opaque_pushpop))); + P->opaque = opaque; if (nullptr==P->opaque) return destructor(P, ENOMEM); if (pj_param_exists(P->params, "v_1")) - static_cast(P->opaque)->v1 = true; + opaque->v1 = true; if (pj_param_exists(P->params, "v_2")) - static_cast(P->opaque)->v2 = true; + opaque->v2 = true; if (pj_param_exists(P->params, "v_3")) - static_cast(P->opaque)->v3 = true; + opaque->v3 = true; if (pj_param_exists(P->params, "v_4")) - static_cast(P->opaque)->v4 = true; + opaque->v4 = true; P->left = PJ_IO_UNITS_WHATEVER; P->right = PJ_IO_UNITS_WHATEVER; -- cgit v1.2.3 From d81ffc6fa8a32db72bdfd1ff034c705222d0cdb3 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 25 Nov 2019 14:56:31 +0100 Subject: Pipeline: support +omit_fwd and +omit_inv keywords Inspired from syntax of https://github.com/OSGeo/PROJ/pull/453/files but 'rebased' on top of previous commit that cleans up the pipeline implementation Different situations: - +omit_fwd: the step when followed in the forward path will be omitted the step when followed in the reverse path will be executed - +omit_fwd +inv: the step when followed in the forward path will be omitted the step when followed in the reverse path will be executed (with the inv method) - +omit_inv: the step when followed in the forward path will be executed the step when followed in the reverse path will be omitted - +omit_inv +inv: the step when followed in the forward path will be executed (with the inv method) the step when followed in the reverse path will be omitted This will be used in the next commit to optimize constructs like +step +proj=hgridshift +grids=foo +step +proj=vgridshift +grids=bar +step +inv +proj=hgridshift +grids=foo Such steps are used for CRS to CRS transformations where applying the vertical grid requires to do a transformation to an interpolating CRS. One can notice that in the last step will just restore the horizontal coordinates before the first step, so doing an inverse hgridshift is overkill. So that could be optimized as: +step +proj=push +v_1 +v_2 +step +proj=hgridshift +grids=foo +omit_inv +step +proj=vgridshift +grids=bar +step +inv +proj=hgridshift +grids=foo +omit_fwd +step +proj=pop +v_1 +v_2 In the forward path, this will be equivalent to: +step +proj=push +v_1 +v_2 +step +proj=hgridshift +grids=foo +step +proj=vgridshift +grids=bar +step +prop=pop +v_1 +v_2 And similarly in the reverse path, this will be quivalent to: +step +proj=push +v_1 +v_2 +step +proj=hgridshift +grids=foo +step +inv +proj=vgridshift +grids=bar +step +proj=pop +v_1 +v_2 --- src/iso19111/io.cpp | 13 +++++++++- src/pipeline.cpp | 57 +++++++++++++++++++++++++++++++++-------- test/gie/4D-API_cs2cs-style.gie | 49 +++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/src/iso19111/io.cpp b/src/iso19111/io.cpp index 678c5d98..d994277b 100644 --- a/src/iso19111/io.cpp +++ b/src/iso19111/io.cpp @@ -6284,6 +6284,15 @@ struct Step { }; std::vector paramValues{}; + + bool hasKey(const char *keyName) const { + for (const auto &kv : paramValues) { + if (kv.key == keyName) { + return true; + } + } + return false; + } }; Step::KeyValue::KeyValue(const char *keyIn, const std::string &valueIn) @@ -6813,7 +6822,9 @@ const std::string &PROJStringFormatter::toString() const { if (d->steps_.size() > 1 || (d->steps_.size() == 1 && - (d->steps_.front().inverted || !d->globalParamValues_.empty()))) { + (d->steps_.front().inverted || d->steps_.front().hasKey("omit_inv") || + d->steps_.front().hasKey("omit_fwd") || + !d->globalParamValues_.empty()))) { d->appendToResult("+proj=pipeline"); for (const auto ¶mValue : d->globalParamValues_) { diff --git a/src/pipeline.cpp b/src/pipeline.cpp index bc6708b0..99b2bdfb 100644 --- a/src/pipeline.cpp +++ b/src/pipeline.cpp @@ -116,9 +116,14 @@ namespace { // anonymous namespace struct Step { PJ* pj = nullptr; - - Step(PJ* pjIn): pj(pjIn) {} - Step(Step&& other): pj(std::move(other.pj)) { other.pj = nullptr; } + bool omit_fwd = false; + bool omit_inv = false; + + Step(PJ* pjIn, bool omitFwdIn, bool omitInvIn): + pj(pjIn), omit_fwd(omitFwdIn), omit_inv(omitInvIn) {} + Step(Step&& other): pj(std::move(other.pj)), + omit_fwd(other.omit_fwd), + omit_inv(other.omit_inv) { other.pj = nullptr; } Step(const Step&) = delete; Step& operator=(const Step&) = delete; @@ -161,7 +166,12 @@ void pj_pipeline_assign_context_to_steps( PJ* P, PJ_CONTEXT* ctx ) static PJ_COORD pipeline_forward_4d (PJ_COORD point, PJ *P) { auto opaque = static_cast(P->opaque); for( auto& step: opaque->steps ) - point = proj_trans (step.pj, PJ_FWD, point); + { + if( !step.omit_fwd ) + { + point = proj_trans (step.pj, PJ_FWD, point); + } + } return point; } @@ -171,7 +181,11 @@ static PJ_COORD pipeline_reverse_4d (PJ_COORD point, PJ *P) { auto opaque = static_cast(P->opaque); for( auto it = opaque->steps.rbegin(); it != opaque->steps.rend(); ++it ) { - point = proj_trans (it->pj, PJ_INV, point); + const auto& step = *it; + if( !step.omit_inv ) + { + point = proj_trans (step.pj, PJ_INV, point); + } } return point; @@ -185,7 +199,12 @@ static PJ_XYZ pipeline_forward_3d (PJ_LPZ lpz, PJ *P) { point.lpz = lpz; auto opaque = static_cast(P->opaque); for( auto& step: opaque->steps ) - point = pj_approx_3D_trans (step.pj, PJ_FWD, point); + { + if( !step.omit_fwd ) + { + point = pj_approx_3D_trans (step.pj, PJ_FWD, point); + } + } return point.xyz; } @@ -197,7 +216,11 @@ static PJ_LPZ pipeline_reverse_3d (PJ_XYZ xyz, PJ *P) { auto opaque = static_cast(P->opaque); for( auto it = opaque->steps.rbegin(); it != opaque->steps.rend(); ++it ) { - point = proj_trans (it->pj, PJ_INV, point); + const auto& step = *it; + if( !step.omit_inv ) + { + point = proj_trans (step.pj, PJ_INV, point); + } } return point.lpz; @@ -211,7 +234,12 @@ static PJ_XY pipeline_forward (PJ_LP lp, PJ *P) { point.lp = lp; auto opaque = static_cast(P->opaque); for( auto& step: opaque->steps ) - point = pj_approx_2D_trans (step.pj, PJ_FWD, point); + { + if( !step.omit_fwd ) + { + point = pj_approx_2D_trans (step.pj, PJ_FWD, point); + } + } return point.xy; } @@ -223,7 +251,11 @@ static PJ_LP pipeline_reverse (PJ_XY xy, PJ *P) { auto opaque = static_cast(P->opaque); for( auto it = opaque->steps.rbegin(); it != opaque->steps.rend(); ++it ) { - point = pj_approx_2D_trans (it->pj, PJ_INV, point); + const auto& step = *it; + if( !step.omit_inv ) + { + point = pj_approx_2D_trans (step.pj, PJ_INV, point); + } } return point.lp; @@ -476,13 +508,16 @@ PJ *OPERATION(pipeline,0) { proj_errno_restore (P, err); /* Is this step inverted? */ - for (j = 0; j < current_argc; j++) + for (j = 0; j < current_argc; j++) { if (0==strcmp("inv", current_argv[j])) { /* if +inv exists in both global and local args the forward operation should be used */ next_step->inverted = next_step->inverted == 0 ? 1 : 0; } + } - opaque->steps.emplace_back(next_step); + bool omit_fwd = pj_param(P->ctx, next_step->params, "bomit_fwd").i != 0; + bool omit_inv = pj_param(P->ctx, next_step->params, "bomit_inv").i != 0; + opaque->steps.emplace_back(next_step, omit_fwd, omit_inv); proj_log_trace (P, "Pipeline at [%p]: step at [%p] (%s) done", P, next_step, current_argv[0]); } diff --git a/test/gie/4D-API_cs2cs-style.gie b/test/gie/4D-API_cs2cs-style.gie index 19b3ad96..e5722b5e 100644 --- a/test/gie/4D-API_cs2cs-style.gie +++ b/test/gie/4D-API_cs2cs-style.gie @@ -386,6 +386,55 @@ operation +proj=pop +v_3 accept 12 56 0 0 expect 12 56 0 0 +------------------------------------------------------------------------------- +Test Pipeline +omit_inv +------------------------------------------------------------------------------- + +operation +proj=pipeline + +step +proj=affine +xoff=1 +yoff=1 +omit_inv + +accept 2 49 0 0 +expect 3 50 0 0 + +direction inverse +accept 2 49 0 0 +expect 2 49 0 0 + + +operation +proj=pipeline + +step +inv +proj=affine +xoff=1 +yoff=1 +omit_inv + +accept 2 49 0 0 +expect 1 48 0 0 + +direction inverse +accept 2 49 0 0 +expect 2 49 0 0 + +------------------------------------------------------------------------------- +Test Pipeline +omit_fwd +------------------------------------------------------------------------------- + +operation +proj=pipeline + +step +proj=affine +xoff=1 +yoff=1 +omit_fwd + +accept 2 49 0 0 +expect 2 49 0 0 + +direction inverse +accept 2 49 0 0 +expect 1 48 0 0 + + +operation +proj=pipeline + +step +inv +proj=affine +xoff=1 +yoff=1 +omit_fwd + +accept 2 49 0 0 +expect 2 49 0 0 + +direction inverse +accept 2 49 0 0 +expect 3 50 0 0 ------------------------------------------------------------------------------- Test bugfix of https://github.com/OSGeo/proj.4/issues/1002 -- cgit v1.2.3 From d1945ecbafc202a0034ad1aa1bb5f38cd10ee74c Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 25 Nov 2019 15:12:33 +0100 Subject: PROJStringFormatter::toString(): optimize hgridshift, vgridshift, hgridshift inv constructs Given an initial pipeline with +step +proj=hgridshift +grids=foo +step +proj=vgridshift +grids=bar +step +inv +proj=hgridshift +grids=foo Transform it as +step +proj=push +v_1 +v_2 +step +proj=hgridshift +grids=foo +omit_inv +step +proj=vgridshift +grids=bar +step +inv +proj=hgridshift +grids=foo +omit_fwd +step +proj=pop +v_1 +v_2 So as to avoid doing a double application of the hgridshift. --- src/iso19111/io.cpp | 44 +++++++++++++++++++++++++ test/unit/test_io.cpp | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/src/iso19111/io.cpp b/src/iso19111/io.cpp index d994277b..0d98e2de 100644 --- a/src/iso19111/io.cpp +++ b/src/iso19111/io.cpp @@ -6278,6 +6278,10 @@ struct Step { return key == otherKey && value == otherVal; } + bool operator==(const KeyValue &other) const noexcept { + return key == other.key && value == other.value; + } + bool operator!=(const KeyValue &other) const noexcept { return key != other.key || value != other.value; } @@ -6799,6 +6803,46 @@ const std::string &PROJStringFormatter::toString() const { } } + // +step +proj=hgridshift +grids=grid_A + // +step +proj=vgridshift [...] <== curStep + // +step +inv +proj=hgridshift +grids=grid_A + // ==> + // +step +proj=push +v_1 +v_2 + // +step +proj=hgridshift +grids=grid_A +omit_inv + // +step +proj=vgridshift [...] + // +step +inv +proj=hgridshift +grids=grid_A +omit_fwd + // +step +proj=pop +v_1 +v_2 + if (i + 1 < d->steps_.size() && prevStep.name == "hgridshift" && + prevStepParamCount == 1 && curStep.name == "vgridshift") { + auto iterNext = iterCur; + ++iterNext; + auto &nextStep = *iterNext; + if (nextStep.name == "hgridshift" && + nextStep.inverted != prevStep.inverted && + nextStep.paramValues.size() == 1 && + prevStep.paramValues[0] == nextStep.paramValues[0]) { + Step pushStep; + pushStep.name = "push"; + pushStep.paramValues.emplace_back("v_1"); + pushStep.paramValues.emplace_back("v_2"); + d->steps_.insert(iterPrev, pushStep); + + prevStep.paramValues.emplace_back("omit_inv"); + + nextStep.paramValues.emplace_back("omit_fwd"); + + Step popStep; + popStep.name = "pop"; + popStep.paramValues.emplace_back("v_1"); + popStep.paramValues.emplace_back("v_2"); + ++iterNext; + d->steps_.insert(iterNext, popStep); + + changeDone = true; + break; + } + } + // detect a step and its inverse if (curStep.inverted != prevStep.inverted && curStep.name == prevStep.name && diff --git a/test/unit/test_io.cpp b/test/unit/test_io.cpp index 07c4c6f1..0dc0dc1c 100644 --- a/test/unit/test_io.cpp +++ b/test/unit/test_io.cpp @@ -6832,6 +6832,95 @@ TEST(io, projstringformatter_axisswap_unitconvert_axisswap) { // --------------------------------------------------------------------------- +TEST(io, projstringformatter_optim_hgridshift_vgridshift_hgridshift_inv) { + // Nominal case + { + auto fmt = PROJStringFormatter::create(); + fmt->addStep("hgridshift"); + fmt->addParam("grids", "foo"); + + fmt->addStep("vgridshift"); + fmt->addParam("grids", "bar"); + + fmt->startInversion(); + fmt->addStep("hgridshift"); + fmt->addParam("grids", "foo"); + fmt->stopInversion(); + + EXPECT_EQ(fmt->toString(), + "+proj=pipeline " + "+step +proj=push +v_1 +v_2 " + "+step +proj=hgridshift +grids=foo +omit_inv " + "+step +proj=vgridshift +grids=bar " + "+step +inv +proj=hgridshift +grids=foo +omit_fwd " + "+step +proj=pop +v_1 +v_2"); + } + + // Variant with first hgridshift inverted, and second forward + { + auto fmt = PROJStringFormatter::create(); + + fmt->startInversion(); + fmt->addStep("hgridshift"); + fmt->addParam("grids", "foo"); + fmt->stopInversion(); + + fmt->addStep("vgridshift"); + fmt->addParam("grids", "bar"); + + fmt->addStep("hgridshift"); + fmt->addParam("grids", "foo"); + + EXPECT_EQ(fmt->toString(), + "+proj=pipeline " + "+step +proj=push +v_1 +v_2 " + "+step +inv +proj=hgridshift +grids=foo +omit_inv " + "+step +proj=vgridshift +grids=bar " + "+step +proj=hgridshift +grids=foo +omit_fwd " + "+step +proj=pop +v_1 +v_2"); + } + + // Do not apply ! not same grid name + { + auto fmt = PROJStringFormatter::create(); + fmt->addStep("hgridshift"); + fmt->addParam("grids", "foo"); + + fmt->addStep("vgridshift"); + fmt->addParam("grids", "bar"); + + fmt->startInversion(); + fmt->addStep("hgridshift"); + fmt->addParam("grids", "foo2"); + fmt->stopInversion(); + + EXPECT_EQ(fmt->toString(), "+proj=pipeline " + "+step +proj=hgridshift +grids=foo " + "+step +proj=vgridshift +grids=bar " + "+step +inv +proj=hgridshift +grids=foo2"); + } + + // Do not apply ! missing inversion + { + auto fmt = PROJStringFormatter::create(); + fmt->addStep("hgridshift"); + fmt->addParam("grids", "foo"); + + fmt->addStep("vgridshift"); + fmt->addParam("grids", "bar"); + + fmt->addStep("hgridshift"); + fmt->addParam("grids", "foo"); + + EXPECT_EQ(fmt->toString(), "+proj=pipeline " + "+step +proj=hgridshift +grids=foo " + "+step +proj=vgridshift +grids=bar " + "+step +proj=hgridshift +grids=foo"); + } +} + +// --------------------------------------------------------------------------- + TEST(io, projparse_longlat) { auto expected = "GEODCRS[\"unknown\",\n" -- cgit v1.2.3 From 3c616a0dc2aed391fbe268a623119a92de615f9a Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 25 Nov 2019 16:06:42 +0100 Subject: pipeline.cpp: use more explict variable names --- src/pipeline.cpp | 123 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 63 insertions(+), 60 deletions(-) diff --git a/src/pipeline.cpp b/src/pipeline.cpp index 99b2bdfb..96767143 100644 --- a/src/pipeline.cpp +++ b/src/pipeline.cpp @@ -132,14 +132,14 @@ struct Step { } }; -struct pj_opaque { +struct Pipeline { char **argv = nullptr; char **current_argv = nullptr; std::vector steps{}; std::stack stack[4]; }; -struct pj_opaque_pushpop { +struct PushPop { bool v1; bool v2; bool v3; @@ -157,15 +157,15 @@ static PJ_LP pipeline_reverse (PJ_XY xy, PJ *P); void pj_pipeline_assign_context_to_steps( PJ* P, PJ_CONTEXT* ctx ) { - auto opaque = static_cast(P->opaque); - for( auto& step: opaque->steps ) + auto pipeline = static_cast(P->opaque); + for( auto& step: pipeline->steps ) proj_assign_context(step.pj, ctx); } static PJ_COORD pipeline_forward_4d (PJ_COORD point, PJ *P) { - auto opaque = static_cast(P->opaque); - for( auto& step: opaque->steps ) + auto pipeline = static_cast(P->opaque); + for( auto& step: pipeline->steps ) { if( !step.omit_fwd ) { @@ -178,10 +178,11 @@ static PJ_COORD pipeline_forward_4d (PJ_COORD point, PJ *P) { static PJ_COORD pipeline_reverse_4d (PJ_COORD point, PJ *P) { - auto opaque = static_cast(P->opaque); - for( auto it = opaque->steps.rbegin(); it != opaque->steps.rend(); ++it ) + auto pipeline = static_cast(P->opaque); + for( auto iterStep = pipeline->steps.rbegin(); + iterStep != pipeline->steps.rend(); ++iterStep ) { - const auto& step = *it; + const auto& step = *iterStep; if( !step.omit_inv ) { point = proj_trans (step.pj, PJ_INV, point); @@ -197,8 +198,8 @@ static PJ_COORD pipeline_reverse_4d (PJ_COORD point, PJ *P) { static PJ_XYZ pipeline_forward_3d (PJ_LPZ lpz, PJ *P) { PJ_COORD point = {{0,0,0,0}}; point.lpz = lpz; - auto opaque = static_cast(P->opaque); - for( auto& step: opaque->steps ) + auto pipeline = static_cast(P->opaque); + for( auto& step: pipeline->steps ) { if( !step.omit_fwd ) { @@ -213,10 +214,11 @@ static PJ_XYZ pipeline_forward_3d (PJ_LPZ lpz, PJ *P) { static PJ_LPZ pipeline_reverse_3d (PJ_XYZ xyz, PJ *P) { PJ_COORD point = {{0,0,0,0}}; point.xyz = xyz; - auto opaque = static_cast(P->opaque); - for( auto it = opaque->steps.rbegin(); it != opaque->steps.rend(); ++it ) + auto pipeline = static_cast(P->opaque); + for( auto iterStep = pipeline->steps.rbegin(); + iterStep != pipeline->steps.rend(); ++iterStep ) { - const auto& step = *it; + const auto& step = *iterStep; if( !step.omit_inv ) { point = proj_trans (step.pj, PJ_INV, point); @@ -232,8 +234,8 @@ static PJ_LPZ pipeline_reverse_3d (PJ_XYZ xyz, PJ *P) { static PJ_XY pipeline_forward (PJ_LP lp, PJ *P) { PJ_COORD point = {{0,0,0,0}}; point.lp = lp; - auto opaque = static_cast(P->opaque); - for( auto& step: opaque->steps ) + auto pipeline = static_cast(P->opaque); + for( auto& step: pipeline->steps ) { if( !step.omit_fwd ) { @@ -248,10 +250,11 @@ static PJ_XY pipeline_forward (PJ_LP lp, PJ *P) { static PJ_LP pipeline_reverse (PJ_XY xy, PJ *P) { PJ_COORD point = {{0,0,0,0}}; point.xy = xy; - auto opaque = static_cast(P->opaque); - for( auto it = opaque->steps.rbegin(); it != opaque->steps.rend(); ++it ) + auto pipeline = static_cast(P->opaque); + for( auto iterStep = pipeline->steps.rbegin(); + iterStep != pipeline->steps.rend(); ++iterStep ) { - const auto& step = *it; + const auto& step = *iterStep; if( !step.omit_inv ) { point = pj_approx_2D_trans (step.pj, PJ_INV, point); @@ -271,12 +274,12 @@ static PJ *destructor (PJ *P, int errlev) { if (nullptr==P->opaque) return pj_default_destructor (P, errlev); - auto opaque = static_cast(P->opaque); + auto pipeline = static_cast(P->opaque); - pj_dealloc (opaque->argv); - pj_dealloc (opaque->current_argv); + pj_dealloc (pipeline->argv); + pj_dealloc (pipeline->current_argv); - delete opaque; + delete pipeline; P->opaque = nullptr; return pj_default_destructor(P, errlev); @@ -367,8 +370,8 @@ static void set_ellipsoid(PJ *P) { } -static enum pj_io_units get_next_non_whatever_unit(struct pj_opaque* opaque, size_t step, PJ_DIRECTION dir) { - const auto& steps = opaque->steps; +static enum pj_io_units get_next_non_whatever_unit(struct Pipeline* pipeline, size_t step, PJ_DIRECTION dir) { + const auto& steps = pipeline->steps; const auto nsteps = steps.size(); if (dir == PJ_FWD) { @@ -420,17 +423,17 @@ PJ *OPERATION(pipeline,0) { P->skip_inv_finalize = 1; - P->opaque = new (std::nothrow) pj_opaque(); + P->opaque = new (std::nothrow) Pipeline(); if (nullptr==P->opaque) return destructor(P, ENOMEM); argc = (int)argc_params (P->params); - auto opaque = static_cast(P->opaque); - opaque->argv = argv = argv_params (P->params, argc); + auto pipeline = static_cast(P->opaque); + pipeline->argv = argv = argv_params (P->params, argc); if (nullptr==argv) return destructor (P, ENOMEM); - opaque->current_argv = current_argv = static_cast(pj_calloc (argc, sizeof (char *))); + pipeline->current_argv = current_argv = static_cast(pj_calloc (argc, sizeof (char *))); if (nullptr==current_argv) return destructor (P, ENOMEM); @@ -517,13 +520,13 @@ PJ *OPERATION(pipeline,0) { bool omit_fwd = pj_param(P->ctx, next_step->params, "bomit_fwd").i != 0; bool omit_inv = pj_param(P->ctx, next_step->params, "bomit_inv").i != 0; - opaque->steps.emplace_back(next_step, omit_fwd, omit_inv); + pipeline->steps.emplace_back(next_step, omit_fwd, omit_inv); proj_log_trace (P, "Pipeline at [%p]: step at [%p] (%s) done", P, next_step, current_argv[0]); } /* Require a forward path through the pipeline */ - for( auto& step: opaque->steps) { + for( auto& step: pipeline->steps) { PJ *Q = step.pj; if ( ( Q->inverted && (Q->inv || Q->inv3d || Q->fwd4d) ) || (!Q->inverted && (Q->fwd || Q->fwd3d || Q->fwd4d) ) ) { @@ -535,7 +538,7 @@ PJ *OPERATION(pipeline,0) { } /* determine if an inverse operation is possible */ - for( auto& step: opaque->steps) { + for( auto& step: pipeline->steps) { PJ *Q = step.pj; if ( pj_has_inverse(Q) ) { continue; @@ -554,26 +557,26 @@ PJ *OPERATION(pipeline,0) { /* where the left-hand side units of the first step shouldn't be changed to RADIANS */ /* as it will result in deg->rad conversions in cs2cs and other applications. */ for (i=0; isteps[i].pj; + auto pj = pipeline->steps[i].pj; if (pj_left(pj) == PJ_IO_UNITS_WHATEVER && pj_right(pj) == PJ_IO_UNITS_WHATEVER) { - pj->left = get_next_non_whatever_unit(opaque, i, PJ_FWD); - pj->right = get_next_non_whatever_unit(opaque, i, PJ_FWD); + pj->left = get_next_non_whatever_unit(pipeline, i, PJ_FWD); + pj->right = get_next_non_whatever_unit(pipeline, i, PJ_FWD); } } for (i=nsteps; i>0;) { --i; - auto pj = opaque->steps[i].pj; + auto pj = pipeline->steps[i].pj; if (pj_left(pj) == PJ_IO_UNITS_WHATEVER && pj_right(pj) == PJ_IO_UNITS_WHATEVER) { - pj->right = get_next_non_whatever_unit(opaque, i, PJ_INV); - pj->left = get_next_non_whatever_unit(opaque, i, PJ_INV); + pj->right = get_next_non_whatever_unit(pipeline, i, PJ_INV); + pj->left = get_next_non_whatever_unit(pipeline, i, PJ_INV); } } /* Check that units between each steps match each other, fail if they don't */ for (i = 0; i + 1 < nsteps; i++) { - enum pj_io_units curr_step_output = pj_right (opaque->steps[i].pj); - enum pj_io_units next_step_input = pj_left (opaque->steps[i+1].pj); + enum pj_io_units curr_step_output = pj_right (pipeline->steps[i].pj); + enum pj_io_units next_step_input = pj_left (pipeline->steps[i+1].pj); if ( curr_step_output == PJ_IO_UNITS_WHATEVER || next_step_input == PJ_IO_UNITS_WHATEVER ) continue; @@ -587,10 +590,10 @@ PJ *OPERATION(pipeline,0) { proj_log_trace (P, "Pipeline: %d steps built. Determining i/o characteristics", nsteps); /* Determine forward input (= reverse output) data type */ - P->left = pj_left (opaque->steps.front().pj); + P->left = pj_left (pipeline->steps.front().pj); /* Now, correspondingly determine forward output (= reverse input) data type */ - P->right = pj_right (opaque->steps.back().pj); + P->right = pj_right (pipeline->steps.back().pj); return P; } @@ -598,16 +601,16 @@ static PJ_COORD push(PJ_COORD point, PJ *P) { if (P->parent == nullptr) return point; - struct pj_opaque *pipeline = static_cast(P->parent->opaque); - struct pj_opaque_pushpop *opaque = static_cast(P->opaque); + struct Pipeline *pipeline = static_cast(P->parent->opaque); + struct PushPop *pushpop = static_cast(P->opaque); - if (opaque->v1) + if (pushpop->v1) pipeline->stack[0].push(point.v[0]); - if (opaque->v2) + if (pushpop->v2) pipeline->stack[1].push(point.v[1]); - if (opaque->v3) + if (pushpop->v3) pipeline->stack[2].push(point.v[2]); - if (opaque->v4) + if (pushpop->v4) pipeline->stack[3].push(point.v[3]); return point; @@ -617,25 +620,25 @@ static PJ_COORD pop(PJ_COORD point, PJ *P) { if (P->parent == nullptr) return point; - struct pj_opaque *pipeline = static_cast(P->parent->opaque); - struct pj_opaque_pushpop *opaque = static_cast(P->opaque); + struct Pipeline *pipeline = static_cast(P->parent->opaque); + struct PushPop *pushpop = static_cast(P->opaque); - if (opaque->v1 && !pipeline->stack[0].empty()) { + if (pushpop->v1 && !pipeline->stack[0].empty()) { point.v[0] = pipeline->stack[0].top(); pipeline->stack[0].pop(); } - if (opaque->v2 && !pipeline->stack[1].empty()) { + if (pushpop->v2 && !pipeline->stack[1].empty()) { point.v[1] = pipeline->stack[1].top(); pipeline->stack[1].pop(); } - if (opaque->v3 && !pipeline->stack[2].empty()) { + if (pushpop->v3 && !pipeline->stack[2].empty()) { point.v[2] = pipeline->stack[2].top(); pipeline->stack[2].pop(); } - if (opaque->v4 && !pipeline->stack[3].empty()) { + if (pushpop->v4 && !pipeline->stack[3].empty()) { point.v[3] = pipeline->stack[3].top(); pipeline->stack[3].pop(); } @@ -646,22 +649,22 @@ static PJ_COORD pop(PJ_COORD point, PJ *P) { static PJ *setup_pushpop(PJ *P) { - auto opaque = static_cast(pj_calloc (1, sizeof(struct pj_opaque_pushpop))); - P->opaque = opaque; + auto pushpop = static_cast(pj_calloc (1, sizeof(struct PushPop))); + P->opaque = pushpop; if (nullptr==P->opaque) return destructor(P, ENOMEM); if (pj_param_exists(P->params, "v_1")) - opaque->v1 = true; + pushpop->v1 = true; if (pj_param_exists(P->params, "v_2")) - opaque->v2 = true; + pushpop->v2 = true; if (pj_param_exists(P->params, "v_3")) - opaque->v3 = true; + pushpop->v3 = true; if (pj_param_exists(P->params, "v_4")) - opaque->v4 = true; + pushpop->v4 = true; P->left = PJ_IO_UNITS_WHATEVER; P->right = PJ_IO_UNITS_WHATEVER; -- cgit v1.2.3 From 49c15bc4a08c1b302bfbd509b2588dd80142de31 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 25 Nov 2019 20:17:50 +0100 Subject: Doc: pipeline: document +omit_fwd and +omit_inv --- docs/source/operations/pipeline.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/source/operations/pipeline.rst b/docs/source/operations/pipeline.rst index 33112536..90112c05 100644 --- a/docs/source/operations/pipeline.rst +++ b/docs/source/operations/pipeline.rst @@ -128,3 +128,34 @@ Optional .. option:: +inv Invert a step in a pipeline. + +.. option:: +omit_fwd + + .. versionadded:: 6.3.0 + + Skip a step of the pipeline when it is followed in the forward path. + + The following example shows a combined use of :ref:`push ` and :ref:`pop ` operators, + with ``omit_fwd`` and ``omit_inv`` options, to implement a vertical adjustment that must + be done in a interpolation CRS that is different from the horizontal CRS + used in input and output. +omit_fwd in the forward path avoid a useless + inverse horizontal transformation and relies on the pop operator to restore + initial horizontal coordinates. +omit_inv serves the similar purpose when + the pipeline is executed in the reverse direction + + :: + + +proj=pipeline + +step +proj=unitconvert +xy_in=deg +xy_out=rad + +step +proj=push +v_1 +v_2 + +step +proj=hgridshift +grids=nvhpgn.gsb +omit_inv + +step +proj=vgridshift +grids=g1999u05.gtx +multiplier=1 + +step +inv +proj=hgridshift +grids=nvhpgn.gsb +omit_fwd + +step +proj=pop +v_1 +v_2 + +step +proj=unitconvert +xy_in=rad +xy_out=deg + +.. option:: +omit_inv + + .. versionadded:: 6.3.0 + + Skip a step of the pipeline when it is followed in the reverse path. -- cgit v1.2.3