/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 | } |