Coverage Report

Created: 2026-09-01 06:55

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/PROJ/src/projections/tmerczoned.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2026 Weston James Renoud
3
 *
4
 * Implementation of Transverse Mercator Zoned Grid System
5
 *
6
 * See IOGP Guidance Note 7-2, Section 3.2.3.2
7
 */
8
9
#include <errno.h>
10
#include <math.h>
11
12
#include "proj.h"
13
#include "proj_internal.h"
14
15
C_NAMESPACE PJ *pj_tmerc(PJ *);
16
17
PROJ_HEAD(tmerczoned, "Transverse Mercator Zoned Grid System");
18
19
namespace {
20
21
struct pj_tmerczoned_data {
22
    PJ *tmerc;
23
    double zoneWidth;
24
    int zoneCount;
25
    double perZoneEastingOffset;
26
};
27
28
// Compute the central meridian/longitude of natural origin for the zone.
29
0
double center_lam(struct pj_tmerczoned_data *Q, int zone) {
30
0
    return (zone - 0.5) * Q->zoneWidth;
31
0
}
32
33
} // namespace
34
35
0
static PJ_XY tmerczoned_forward(PJ_LP lp, PJ *P) {
36
0
    auto *Q = static_cast<struct pj_tmerczoned_data *>(P->opaque);
37
38
0
    if (lp.lam < 0)
39
0
        lp.lam += 2 * M_PI;
40
41
    // We want to nudge the calc slightly to bias the lower bound to be
42
    // inclusive and upper bound exclusive. It is not specified in IOGP Guidance
43
    // Note 7-2, but the use of `floor` suggests this is the intent.
44
0
    constexpr double eps = 1e-12;
45
46
    // Initial Longitude is already accounted for.
47
0
    int zone = static_cast<int>(lp.lam / Q->zoneWidth + eps) + 1;
48
49
0
    lp.lam -= center_lam(Q, zone);
50
0
    PJ_XY xy = Q->tmerc->fwd(lp, Q->tmerc);
51
0
    xy.x += zone * Q->perZoneEastingOffset;
52
53
0
    return xy;
54
0
}
55
56
0
static PJ_LP tmerczoned_inverse(PJ_XY xy, PJ *P) {
57
0
    auto *Q = static_cast<struct pj_tmerczoned_data *>(P->opaque);
58
59
0
    int zone = static_cast<int>(xy.x / Q->perZoneEastingOffset + 0.5);
60
61
0
    xy.x -= zone * Q->perZoneEastingOffset;
62
0
    PJ_LP lp = Q->tmerc->inv(xy, Q->tmerc);
63
0
    lp.lam += center_lam(Q, zone);
64
65
0
    return lp;
66
0
}
67
68
0
static PJ *tmerczoned_destructor(PJ *P, int errlev) { /* Destructor */
69
0
    if (nullptr == P)
70
0
        return nullptr;
71
72
0
    if (nullptr == P->opaque)
73
0
        return pj_default_destructor(P, errlev);
74
0
    proj_destroy(static_cast<struct pj_tmerczoned_data *>(P->opaque)->tmerc);
75
76
0
    return pj_default_destructor(P, errlev);
77
0
}
78
79
0
PJ *PJ_PROJECTION(tmerczoned) {
80
0
    auto *Q = static_cast<struct pj_tmerczoned_data *>(
81
0
        calloc(1, sizeof(struct pj_tmerczoned_data)));
82
0
    if (nullptr == Q)
83
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
84
0
    P->opaque = Q;
85
0
    P->destructor = tmerczoned_destructor;
86
0
    P->fwd = tmerczoned_forward;
87
0
    P->inv = tmerczoned_inverse;
88
89
0
    Q->zoneWidth = pj_param(P->ctx, P->params, "dwidth").f * DEG_TO_RAD;
90
0
    Q->zoneCount = static_cast<int>(M_PI / Q->zoneWidth);
91
92
    // Scaled to unit sphere/ellipsoid
93
0
    Q->perZoneEastingOffset = 1'000'000 / P->a;
94
95
    // TODO: copied from https://github.com/OSGeo/PROJ/pull/4770 where
96
    //       there is an open question if this is sufficient/correct.
97
0
    {
98
0
        Q->tmerc = pj_tmerc(nullptr);
99
0
        if (Q->tmerc == nullptr)
100
0
            return tmerczoned_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
101
102
0
        Q->tmerc->ctx = P->ctx;
103
0
        Q->tmerc->k0 = P->k0;
104
105
0
        pj_calc_ellipsoid_params(Q->tmerc, P->a, P->es);
106
107
0
        Q->tmerc = pj_tmerc(Q->tmerc);
108
0
        if (Q->tmerc == nullptr)
109
0
            return tmerczoned_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
110
0
    }
111
112
0
    return P;
113
0
}