Coverage Report

Created: 2026-09-03 06:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/PROJ/src/conversions/unitconvert.cpp
Line
Count
Source
1
/***********************************************************************
2
3
            Unit conversion pseudo-projection for use with
4
                       transformation pipelines.
5
6
                      Kristian Evers, 2017-05-16
7
8
************************************************************************
9
10
A pseudo-projection that can be used to convert units of input and
11
output data. Primarily useful in pipelines.
12
13
Unit conversion is performed by means of a pivot unit. The pivot unit
14
for distance units are the meter and for time we use the modified julian
15
date. A time unit conversion is performed like
16
17
    Unit A -> Modified Julian date -> Unit B
18
19
distance units are converted in the same manner, with meter being the
20
central unit.
21
22
The modified Julian date is chosen as the pivot unit since it has a
23
fairly high precision, goes sufficiently long backwards in time, has no
24
danger of hitting the upper limit in the near future and it is a fairly
25
common time unit in astronomy and geodesy. Note that we are using the
26
Julian date and not day. The difference being that the latter is defined
27
as an integer and is thus limited to days in resolution. This approach
28
has been extended wherever it makes sense, e.g. the GPS week unit also
29
has a fractional part that makes it possible to determine the day, hour
30
and minute of an observation.
31
32
In- and output units are controlled with the parameters
33
34
    +xy_in, +xy_out, +z_in, +z_out, +t_in and +t_out
35
36
where xy denotes horizontal units, z vertical units and t time units.
37
38
************************************************************************
39
40
Kristian Evers, kreve@sdfe.dk, 2017-05-09
41
Last update: 2017-05-16
42
43
************************************************************************
44
* Copyright (c) 2017, Kristian Evers / SDFE
45
*
46
* Permission is hereby granted, free of charge, to any person obtaining a
47
* copy of this software and associated documentation files (the "Software"),
48
* to deal in the Software without restriction, including without limitation
49
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
50
* and/or sell copies of the Software, and to permit persons to whom the
51
* Software is furnished to do so, subject to the following conditions:
52
*
53
* The above copyright notice and this permission notice shall be included
54
* in all copies or substantial portions of the Software.
55
*
56
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
57
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
58
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
59
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
60
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
61
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
62
* DEALINGS IN THE SOFTWARE.
63
*
64
***********************************************************************/
65
66
#include <errno.h>
67
#include <math.h>
68
#include <string.h>
69
#include <time.h>
70
71
#include "proj_internal.h"
72
#include <math.h>
73
74
PROJ_HEAD(unitconvert, "Unit conversion");
75
76
typedef double (*tconvert)(double);
77
78
namespace { // anonymous namespace
79
struct TIME_UNITS {
80
    const char *id;   /* units keyword */
81
    tconvert t_in;    /* unit -> mod. julian date function pointer */
82
    tconvert t_out;   /* mod. julian date > unit function pointer */
83
    const char *name; /* comments */
84
};
85
} // anonymous namespace
86
87
namespace { // anonymous namespace
88
struct pj_opaque_unitconvert {
89
    int t_in_id;      /* time unit id for the time input unit   */
90
    int t_out_id;     /* time unit id for the time output unit  */
91
    double xy_factor; /* unit conversion factor for horizontal components */
92
    double z_factor;  /* unit conversion factor for vertical components */
93
};
94
} // anonymous namespace
95
96
/***********************************************************************/
97
0
static int is_leap_year(long year) {
98
    /***********************************************************************/
99
0
    return ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0);
100
0
}
101
102
/***********************************************************************/
103
0
static int days_in_year(long year) {
104
    /***********************************************************************/
105
0
    return is_leap_year(year) ? 366 : 365;
106
0
}
107
108
/***********************************************************************/
109
0
static unsigned int days_in_month(unsigned long year, unsigned long month) {
110
    /***********************************************************************/
111
0
    const unsigned int month_table[] = {31, 28, 31, 30, 31, 30,
112
0
                                        31, 31, 30, 31, 30, 31};
113
0
    unsigned int days;
114
115
0
    if (month > 12)
116
0
        month = 12;
117
0
    if (month == 0)
118
0
        month = 1;
119
120
0
    days = month_table[month - 1];
121
0
    if (is_leap_year(year) && month == 2)
122
0
        days++;
123
124
0
    return days;
125
0
}
126
127
/***********************************************************************/
128
static int daynumber_in_year(unsigned long year, unsigned long month,
129
0
                             unsigned long day) {
130
    /***********************************************************************/
131
0
    unsigned int daynumber = 0, i;
132
133
0
    if (month > 12)
134
0
        month = 12;
135
0
    if (month == 0)
136
0
        month = 1;
137
0
    if (day > days_in_month(year, month))
138
0
        day = days_in_month(year, month);
139
140
0
    for (i = 1; i < month; i++)
141
0
        daynumber += days_in_month(year, i);
142
143
0
    daynumber += day;
144
145
0
    return daynumber;
146
0
}
147
148
/***********************************************************************/
149
0
static double mjd_to_mjd(double mjd) {
150
    /***********************************************************************
151
        Modified julian date no-op function.
152
153
        The Julian date is defined as (fractional) days since midnight
154
        on 16th of November in 1858.
155
    ************************************************************************/
156
0
    return mjd;
157
0
}
158
159
/***********************************************************************/
160
0
static double decimalyear_to_mjd(double decimalyear) {
161
    /***********************************************************************
162
        Epoch of modified julian date is 1858-11-16 00:00
163
    ************************************************************************/
164
0
    long year;
165
0
    double fractional_year;
166
0
    double mjd;
167
168
    // Written this way to deal with NaN input
169
0
    if (!(decimalyear >= -10000 && decimalyear <= 10000))
170
0
        return 0;
171
172
0
    year = lround(floor(decimalyear));
173
0
    fractional_year = decimalyear - year;
174
0
    mjd = (year - 1859) * 365 + 14 + 31;
175
0
    mjd += (double)fractional_year * (double)days_in_year(year);
176
177
    /* take care of leap days */
178
0
    year--;
179
0
    for (; year > 1858; year--)
180
0
        if (is_leap_year(year))
181
0
            mjd++;
182
183
0
    return mjd;
184
0
}
185
186
/***********************************************************************/
187
0
static double mjd_to_decimalyear(double mjd) {
188
    /***********************************************************************
189
        Epoch of modified julian date is 1858-11-16 00:00
190
    ************************************************************************/
191
0
    double decimalyear = mjd;
192
0
    double mjd_iter = 14 + 31;
193
0
    int year = 1859;
194
195
    /* a smarter brain than mine could probably to do this more elegantly
196
       - I'll just brute-force my way out of this... */
197
0
    for (; mjd >= mjd_iter; year++) {
198
0
        mjd_iter += days_in_year(year);
199
0
    }
200
0
    year--;
201
0
    mjd_iter -= days_in_year(year);
202
203
0
    decimalyear = year + (mjd - mjd_iter) / days_in_year(year);
204
0
    return decimalyear;
205
0
}
206
207
/***********************************************************************/
208
0
static double gps_week_to_mjd(double gps_week) {
209
    /***********************************************************************
210
        GPS weeks are defined as the number of weeks since January the 6th
211
        1980.
212
213
        Epoch of gps weeks is 1980-01-06 00:00, which in modified Julian
214
        date is 44244.
215
    ************************************************************************/
216
0
    return 44244.0 + gps_week * 7.0;
217
0
}
218
219
/***********************************************************************/
220
0
static double mjd_to_gps_week(double mjd) {
221
    /***********************************************************************
222
        GPS weeks are defined as the number of weeks since January the 6th
223
        1980.
224
225
        Epoch of gps weeks is 1980-01-06 00:00, which in modified Julian
226
        date is 44244.
227
    ************************************************************************/
228
0
    return (mjd - 44244.0) / 7.0;
229
0
}
230
231
/***********************************************************************/
232
0
static double yyyymmdd_to_mjd(double yyyymmdd) {
233
    /************************************************************************
234
        Date given in YYYY-MM-DD format.
235
    ************************************************************************/
236
237
0
    long year = lround(floor(yyyymmdd / 10000));
238
0
    long month = lround(floor((yyyymmdd - year * 10000) / 100));
239
0
    long day = lround(floor(yyyymmdd - year * 10000 - month * 100));
240
0
    double mjd = daynumber_in_year(year, month, day);
241
242
0
    for (year -= 1; year > 1858; year--)
243
0
        mjd += days_in_year(year);
244
245
0
    return mjd + 13 + 31;
246
0
}
247
248
/***********************************************************************/
249
0
static double mjd_to_yyyymmdd(double mjd) {
250
    /************************************************************************
251
        Date returned in YYYY-MM-DD format.
252
    ************************************************************************/
253
0
    unsigned int date_iter = 14 + 31;
254
0
    unsigned int year = 1859, month = 0, day = 0;
255
0
    unsigned int date = (int)lround(mjd);
256
257
0
    for (; date >= date_iter; year++) {
258
0
        date_iter += days_in_year(year);
259
0
    }
260
0
    year--;
261
0
    date_iter -= days_in_year(year);
262
263
0
    for (month = 1; date_iter + days_in_month(year, month) <= date; month++)
264
0
        date_iter += days_in_month(year, month);
265
266
0
    day = date - date_iter + 1;
267
268
0
    return year * 10000.0 + month * 100.0 + day;
269
0
}
270
271
static const struct TIME_UNITS time_units[] = {
272
    {"mjd", mjd_to_mjd, mjd_to_mjd, "Modified julian date"},
273
    {"decimalyear", decimalyear_to_mjd, mjd_to_decimalyear, "Decimal year"},
274
    {"gps_week", gps_week_to_mjd, mjd_to_gps_week, "GPS Week"},
275
    {"yyyymmdd", yyyymmdd_to_mjd, mjd_to_yyyymmdd, "YYYYMMDD date"},
276
    {nullptr, nullptr, nullptr, nullptr}};
277
278
/***********************************************************************/
279
1.94M
static PJ_XY forward_2d(PJ_LP lp, PJ *P) {
280
    /************************************************************************
281
        Forward unit conversions in the plane
282
    ************************************************************************/
283
1.94M
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
284
1.94M
    PJ_COORD point = {{0, 0, 0, 0}};
285
1.94M
    point.lp = lp;
286
287
1.94M
    point.xy.x *= Q->xy_factor;
288
1.94M
    point.xy.y *= Q->xy_factor;
289
290
1.94M
    return point.xy;
291
1.94M
}
292
293
/***********************************************************************/
294
336
static PJ_LP reverse_2d(PJ_XY xy, PJ *P) {
295
    /************************************************************************
296
        Reverse unit conversions in the plane
297
    ************************************************************************/
298
336
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
299
336
    PJ_COORD point = {{0, 0, 0, 0}};
300
336
    point.xy = xy;
301
302
336
    point.xy.x /= Q->xy_factor;
303
336
    point.xy.y /= Q->xy_factor;
304
305
336
    return point.lp;
306
336
}
307
308
/***********************************************************************/
309
1.94M
static PJ_XYZ forward_3d(PJ_LPZ lpz, PJ *P) {
310
    /************************************************************************
311
        Forward unit conversions the vertical component
312
    ************************************************************************/
313
1.94M
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
314
1.94M
    PJ_COORD point = {{0, 0, 0, 0}};
315
1.94M
    point.lpz = lpz;
316
317
    /* take care of the horizontal components in the 2D function */
318
319
    // Assigning in 2 steps avoids cppcheck warning
320
    // "Overlapping read/write of union is undefined behavior"
321
    // Cf https://github.com/OSGeo/PROJ/pull/3527#pullrequestreview-1233332710
322
1.94M
    const auto xy = forward_2d(point.lp, P);
323
1.94M
    point.xy = xy;
324
325
1.94M
    point.xyz.z *= Q->z_factor;
326
327
1.94M
    return point.xyz;
328
1.94M
}
329
330
/***********************************************************************/
331
336
static PJ_LPZ reverse_3d(PJ_XYZ xyz, PJ *P) {
332
    /************************************************************************
333
        Reverse unit conversions the vertical component
334
    ************************************************************************/
335
336
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
336
336
    PJ_COORD point = {{0, 0, 0, 0}};
337
336
    point.xyz = xyz;
338
339
    /* take care of the horizontal components in the 2D function */
340
341
    // Assigning in 2 steps avoids cppcheck warning
342
    // "Overlapping read/write of union is undefined behavior"
343
    // Cf https://github.com/OSGeo/PROJ/pull/3527#pullrequestreview-1233332710
344
336
    const auto lp = reverse_2d(point.xy, P);
345
336
    point.lp = lp;
346
347
336
    point.xyz.z /= Q->z_factor;
348
349
336
    return point.lpz;
350
336
}
351
352
/***********************************************************************/
353
1.94M
static void forward_4d(PJ_COORD &coo, PJ *P) {
354
    /************************************************************************
355
        Forward conversion of time units
356
    ************************************************************************/
357
1.94M
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
358
359
    /* delegate unit conversion of physical dimensions to the 3D function */
360
361
    // Assigning in 2 steps avoids cppcheck warning
362
    // "Overlapping read/write of union is undefined behavior"
363
    // Cf https://github.com/OSGeo/PROJ/pull/3527#pullrequestreview-1233332710
364
1.94M
    const auto xyz = forward_3d(coo.lpz, P);
365
1.94M
    coo.xyz = xyz;
366
367
1.94M
    if (Q->t_in_id >= 0)
368
0
        coo.xyzt.t = time_units[Q->t_in_id].t_in(coo.xyzt.t);
369
1.94M
    if (Q->t_out_id >= 0)
370
0
        coo.xyzt.t = time_units[Q->t_out_id].t_out(coo.xyzt.t);
371
1.94M
}
372
373
/***********************************************************************/
374
336
static void reverse_4d(PJ_COORD &coo, PJ *P) {
375
    /************************************************************************
376
        Reverse conversion of time units
377
    ************************************************************************/
378
336
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
379
380
    /* delegate unit conversion of physical dimensions to the 3D function */
381
382
    // Assigning in 2 steps avoids cppcheck warning
383
    // "Overlapping read/write of union is undefined behavior"
384
    // Cf https://github.com/OSGeo/PROJ/pull/3527#pullrequestreview-1233332710
385
336
    const auto lpz = reverse_3d(coo.xyz, P);
386
336
    coo.lpz = lpz;
387
388
336
    if (Q->t_out_id >= 0)
389
0
        coo.xyzt.t = time_units[Q->t_out_id].t_in(coo.xyzt.t);
390
336
    if (Q->t_in_id >= 0)
391
0
        coo.xyzt.t = time_units[Q->t_in_id].t_out(coo.xyzt.t);
392
336
}
393
394
/***********************************************************************/
395
static double get_unit_conversion_factor(const char *name, int *p_is_linear,
396
45.0k
                                         const char **p_normalized_name) {
397
    /***********************************************************************/
398
45.0k
    int i;
399
45.0k
    const char *s;
400
45.0k
    const PJ_UNITS *units = pj_list_linear_units();
401
402
    /* Try first with linear units */
403
911k
    for (i = 0; (s = units[i].id); ++i) {
404
870k
        if (strcmp(s, name) == 0) {
405
4.28k
            if (p_normalized_name) {
406
4.28k
                *p_normalized_name = units[i].name;
407
4.28k
            }
408
4.28k
            if (p_is_linear) {
409
4.28k
                *p_is_linear = 1;
410
4.28k
            }
411
4.28k
            return units[i].factor;
412
4.28k
        }
413
870k
    }
414
415
    /* And then angular units */
416
40.7k
    units = pj_list_angular_units();
417
62.7k
    for (i = 0; (s = units[i].id); ++i) {
418
62.1k
        if (strcmp(s, name) == 0) {
419
40.1k
            if (p_normalized_name) {
420
40.1k
                *p_normalized_name = units[i].name;
421
40.1k
            }
422
40.1k
            if (p_is_linear) {
423
40.1k
                *p_is_linear = 0;
424
40.1k
            }
425
40.1k
            return units[i].factor;
426
40.1k
        }
427
62.1k
    }
428
657
    if (p_normalized_name) {
429
657
        *p_normalized_name = nullptr;
430
657
    }
431
657
    if (p_is_linear) {
432
657
        *p_is_linear = -1;
433
657
    }
434
657
    return 0.0;
435
40.7k
}
436
437
/***********************************************************************/
438
21.6k
PJ *PJ_CONVERSION(unitconvert, 0) {
439
    /***********************************************************************/
440
21.6k
    struct pj_opaque_unitconvert *Q =
441
21.6k
        static_cast<struct pj_opaque_unitconvert *>(
442
21.6k
            calloc(1, sizeof(struct pj_opaque_unitconvert)));
443
21.6k
    const char *s, *name;
444
21.6k
    int i;
445
21.6k
    double f;
446
21.6k
    int xy_in_is_linear = -1;  /* unknown */
447
21.6k
    int xy_out_is_linear = -1; /* unknown */
448
21.6k
    int z_in_is_linear = -1;   /* unknown */
449
21.6k
    int z_out_is_linear = -1;  /* unknown */
450
451
21.6k
    if (nullptr == Q)
452
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
453
21.6k
    P->opaque = (void *)Q;
454
455
21.6k
    P->fwd4d = forward_4d;
456
21.6k
    P->inv4d = reverse_4d;
457
21.6k
    P->fwd3d = forward_3d;
458
21.6k
    P->inv3d = reverse_3d;
459
21.6k
    P->fwd = forward_2d;
460
21.6k
    P->inv = reverse_2d;
461
462
21.6k
    P->left = PJ_IO_UNITS_WHATEVER;
463
21.6k
    P->right = PJ_IO_UNITS_WHATEVER;
464
21.6k
    P->skip_fwd_prepare = 1;
465
21.6k
    P->skip_inv_prepare = 1;
466
467
    /* if no time input/output unit is specified we can skip them */
468
21.6k
    Q->t_in_id = -1;
469
21.6k
    Q->t_out_id = -1;
470
471
21.6k
    Q->xy_factor = 1.0;
472
21.6k
    Q->z_factor = 1.0;
473
474
21.6k
    if ((name = pj_param(P->ctx, P->params, "sxy_in").s) != nullptr) {
475
20.9k
        const char *normalized_name = nullptr;
476
20.9k
        f = get_unit_conversion_factor(name, &xy_in_is_linear,
477
20.9k
                                       &normalized_name);
478
20.9k
        if (f != 0.0) {
479
20.7k
            proj_log_trace(P, "xy_in unit: %s", normalized_name);
480
20.7k
        } else {
481
158
            f = pj_param(P->ctx, P->params, "dxy_in").f;
482
158
            if (f == 0.0 || 1.0 / f == 0.0) {
483
57
                proj_log_error(P, _("unknown xy_in unit"));
484
57
                return pj_default_destructor(
485
57
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
486
57
            }
487
158
        }
488
20.8k
        Q->xy_factor = f;
489
20.8k
        if (normalized_name != nullptr) {
490
20.7k
            if (strcmp(normalized_name, "Radian") == 0)
491
10.1k
                P->left = PJ_IO_UNITS_RADIANS;
492
20.7k
            if (strcmp(normalized_name, "Degree") == 0)
493
9.96k
                P->left = PJ_IO_UNITS_DEGREES;
494
20.7k
        }
495
20.8k
    }
496
497
21.5k
    if ((name = pj_param(P->ctx, P->params, "sxy_out").s) != nullptr) {
498
20.8k
        const char *normalized_name = nullptr;
499
20.8k
        f = get_unit_conversion_factor(name, &xy_out_is_linear,
500
20.8k
                                       &normalized_name);
501
20.8k
        if (f != 0.0) {
502
20.7k
            proj_log_trace(P, "xy_out unit: %s", normalized_name);
503
20.7k
        } else {
504
91
            f = pj_param(P->ctx, P->params, "dxy_out").f;
505
91
            if (f == 0.0 || 1.0 / f == 0.0) {
506
72
                proj_log_error(P, _("unknown xy_out unit"));
507
72
                return pj_default_destructor(
508
72
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
509
72
            }
510
91
        }
511
20.7k
        Q->xy_factor /= f;
512
20.7k
        if (normalized_name != nullptr) {
513
20.7k
            if (strcmp(normalized_name, "Radian") == 0)
514
9.97k
                P->right = PJ_IO_UNITS_RADIANS;
515
20.7k
            if (strcmp(normalized_name, "Degree") == 0)
516
10.0k
                P->right = PJ_IO_UNITS_DEGREES;
517
20.7k
        }
518
20.7k
    }
519
520
21.4k
    if (xy_in_is_linear >= 0 && xy_out_is_linear >= 0 &&
521
20.7k
        xy_in_is_linear != xy_out_is_linear) {
522
0
        proj_log_error(P, _("inconsistent unit type between xy_in and xy_out"));
523
0
        return pj_default_destructor(P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
524
0
    }
525
526
21.4k
    if ((name = pj_param(P->ctx, P->params, "sz_in").s) != nullptr) {
527
1.63k
        const char *normalized_name = nullptr;
528
1.63k
        f = get_unit_conversion_factor(name, &z_in_is_linear, &normalized_name);
529
1.63k
        if (f != 0.0) {
530
1.41k
            proj_log_trace(P, "z_in unit: %s", normalized_name);
531
1.41k
        } else {
532
215
            f = pj_param(P->ctx, P->params, "dz_in").f;
533
215
            if (f == 0.0 || 1.0 / f == 0.0) {
534
27
                proj_log_error(P, _("unknown z_in unit"));
535
27
                return pj_default_destructor(
536
27
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
537
27
            }
538
215
        }
539
1.60k
        Q->z_factor = f;
540
1.60k
    }
541
542
21.4k
    if ((name = pj_param(P->ctx, P->params, "sz_out").s) != nullptr) {
543
1.62k
        const char *normalized_name = nullptr;
544
1.62k
        f = get_unit_conversion_factor(name, &z_out_is_linear,
545
1.62k
                                       &normalized_name);
546
1.62k
        if (f != 0.0) {
547
1.42k
            proj_log_trace(P, "z_out unit: %s", normalized_name);
548
1.42k
        } else {
549
193
            f = pj_param(P->ctx, P->params, "dz_out").f;
550
193
            if (f == 0.0 || 1.0 / f == 0.0) {
551
21
                proj_log_error(P, _("unknown z_out unit"));
552
21
                return pj_default_destructor(
553
21
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
554
21
            }
555
193
        }
556
1.59k
        Q->z_factor /= f;
557
1.59k
    }
558
559
21.4k
    if (z_in_is_linear >= 0 && z_out_is_linear >= 0 &&
560
1.23k
        z_in_is_linear != z_out_is_linear) {
561
0
        proj_log_error(P, _("inconsistent unit type between z_in and z_out"));
562
0
        return pj_default_destructor(P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
563
0
    }
564
565
21.4k
    if ((name = pj_param(P->ctx, P->params, "st_in").s) != nullptr) {
566
51
        for (i = 0; (s = time_units[i].id) && strcmp(name, s); ++i)
567
39
            ;
568
569
12
        if (!s) {
570
3
            proj_log_error(P, _("unknown t_in unit"));
571
3
            return pj_default_destructor(P,
572
3
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
573
3
        }
574
575
9
        Q->t_in_id = i;
576
9
        proj_log_trace(P, "t_in unit: %s", time_units[i].name);
577
9
    }
578
579
21.4k
    s = nullptr;
580
21.4k
    if ((name = pj_param(P->ctx, P->params, "st_out").s) != nullptr) {
581
99
        for (i = 0; (s = time_units[i].id) && strcmp(name, s); ++i)
582
70
            ;
583
584
29
        if (!s) {
585
4
            proj_log_error(P, _("unknown t_out unit"));
586
4
            return pj_default_destructor(P,
587
4
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
588
4
        }
589
590
25
        Q->t_out_id = i;
591
25
        proj_log_trace(P, "t_out unit: %s", time_units[i].name);
592
25
    }
593
594
21.4k
    return P;
595
21.4k
}