Coverage Report

Created: 2025-07-12 06:32

/src/PROJ/src/conversions/unitconvert.cpp
Line
Count
Source (jump to first uncovered line)
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.60M
static PJ_XY forward_2d(PJ_LP lp, PJ *P) {
280
    /************************************************************************
281
        Forward unit conversions in the plane
282
    ************************************************************************/
283
1.60M
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
284
1.60M
    PJ_COORD point = {{0, 0, 0, 0}};
285
1.60M
    point.lp = lp;
286
287
1.60M
    point.xy.x *= Q->xy_factor;
288
1.60M
    point.xy.y *= Q->xy_factor;
289
290
1.60M
    return point.xy;
291
1.60M
}
292
293
/***********************************************************************/
294
672
static PJ_LP reverse_2d(PJ_XY xy, PJ *P) {
295
    /************************************************************************
296
        Reverse unit conversions in the plane
297
    ************************************************************************/
298
672
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
299
672
    PJ_COORD point = {{0, 0, 0, 0}};
300
672
    point.xy = xy;
301
302
672
    point.xy.x /= Q->xy_factor;
303
672
    point.xy.y /= Q->xy_factor;
304
305
672
    return point.lp;
306
672
}
307
308
/***********************************************************************/
309
1.60M
static PJ_XYZ forward_3d(PJ_LPZ lpz, PJ *P) {
310
    /************************************************************************
311
        Forward unit conversions the vertical component
312
    ************************************************************************/
313
1.60M
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
314
1.60M
    PJ_COORD point = {{0, 0, 0, 0}};
315
1.60M
    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.60M
    const auto xy = forward_2d(point.lp, P);
323
1.60M
    point.xy = xy;
324
325
1.60M
    point.xyz.z *= Q->z_factor;
326
327
1.60M
    return point.xyz;
328
1.60M
}
329
330
/***********************************************************************/
331
672
static PJ_LPZ reverse_3d(PJ_XYZ xyz, PJ *P) {
332
    /************************************************************************
333
        Reverse unit conversions the vertical component
334
    ************************************************************************/
335
672
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
336
672
    PJ_COORD point = {{0, 0, 0, 0}};
337
672
    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
672
    const auto lp = reverse_2d(point.xy, P);
345
672
    point.lp = lp;
346
347
672
    point.xyz.z /= Q->z_factor;
348
349
672
    return point.lpz;
350
672
}
351
352
/***********************************************************************/
353
1.60M
static void forward_4d(PJ_COORD &coo, PJ *P) {
354
    /************************************************************************
355
        Forward conversion of time units
356
    ************************************************************************/
357
1.60M
    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.60M
    const auto xyz = forward_3d(coo.lpz, P);
365
1.60M
    coo.xyz = xyz;
366
367
1.60M
    if (Q->t_in_id >= 0)
368
0
        coo.xyzt.t = time_units[Q->t_in_id].t_in(coo.xyzt.t);
369
1.60M
    if (Q->t_out_id >= 0)
370
0
        coo.xyzt.t = time_units[Q->t_out_id].t_out(coo.xyzt.t);
371
1.60M
}
372
373
/***********************************************************************/
374
672
static void reverse_4d(PJ_COORD &coo, PJ *P) {
375
    /************************************************************************
376
        Reverse conversion of time units
377
    ************************************************************************/
378
672
    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
672
    const auto lpz = reverse_3d(coo.xyz, P);
386
672
    coo.lpz = lpz;
387
388
672
    if (Q->t_out_id >= 0)
389
0
        coo.xyzt.t = time_units[Q->t_out_id].t_in(coo.xyzt.t);
390
672
    if (Q->t_in_id >= 0)
391
0
        coo.xyzt.t = time_units[Q->t_in_id].t_out(coo.xyzt.t);
392
672
}
393
394
/***********************************************************************/
395
static double get_unit_conversion_factor(const char *name, int *p_is_linear,
396
30.5k
                                         const char **p_normalized_name) {
397
    /***********************************************************************/
398
30.5k
    int i;
399
30.5k
    const char *s;
400
30.5k
    const PJ_UNITS *units = pj_list_linear_units();
401
402
    /* Try first with linear units */
403
607k
    for (i = 0; (s = units[i].id); ++i) {
404
580k
        if (strcmp(s, name) == 0) {
405
3.40k
            if (p_normalized_name) {
406
3.40k
                *p_normalized_name = units[i].name;
407
3.40k
            }
408
3.40k
            if (p_is_linear) {
409
3.40k
                *p_is_linear = 1;
410
3.40k
            }
411
3.40k
            return units[i].factor;
412
3.40k
        }
413
580k
    }
414
415
    /* And then angular units */
416
27.1k
    units = pj_list_angular_units();
417
41.6k
    for (i = 0; (s = units[i].id); ++i) {
418
41.2k
        if (strcmp(s, name) == 0) {
419
26.8k
            if (p_normalized_name) {
420
26.8k
                *p_normalized_name = units[i].name;
421
26.8k
            }
422
26.8k
            if (p_is_linear) {
423
26.8k
                *p_is_linear = 0;
424
26.8k
            }
425
26.8k
            return units[i].factor;
426
26.8k
        }
427
41.2k
    }
428
365
    if (p_normalized_name) {
429
365
        *p_normalized_name = nullptr;
430
365
    }
431
365
    if (p_is_linear) {
432
365
        *p_is_linear = -1;
433
365
    }
434
365
    return 0.0;
435
27.1k
}
436
437
/***********************************************************************/
438
14.5k
PJ *PJ_CONVERSION(unitconvert, 0) {
439
    /***********************************************************************/
440
14.5k
    struct pj_opaque_unitconvert *Q =
441
14.5k
        static_cast<struct pj_opaque_unitconvert *>(
442
14.5k
            calloc(1, sizeof(struct pj_opaque_unitconvert)));
443
14.5k
    const char *s, *name;
444
14.5k
    int i;
445
14.5k
    double f;
446
14.5k
    int xy_in_is_linear = -1;  /* unknown */
447
14.5k
    int xy_out_is_linear = -1; /* unknown */
448
14.5k
    int z_in_is_linear = -1;   /* unknown */
449
14.5k
    int z_out_is_linear = -1;  /* unknown */
450
451
14.5k
    if (nullptr == Q)
452
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
453
14.5k
    P->opaque = (void *)Q;
454
455
14.5k
    P->fwd4d = forward_4d;
456
14.5k
    P->inv4d = reverse_4d;
457
14.5k
    P->fwd3d = forward_3d;
458
14.5k
    P->inv3d = reverse_3d;
459
14.5k
    P->fwd = forward_2d;
460
14.5k
    P->inv = reverse_2d;
461
462
14.5k
    P->left = PJ_IO_UNITS_WHATEVER;
463
14.5k
    P->right = PJ_IO_UNITS_WHATEVER;
464
14.5k
    P->skip_fwd_prepare = 1;
465
14.5k
    P->skip_inv_prepare = 1;
466
467
    /* if no time input/output unit is specified we can skip them */
468
14.5k
    Q->t_in_id = -1;
469
14.5k
    Q->t_out_id = -1;
470
471
14.5k
    Q->xy_factor = 1.0;
472
14.5k
    Q->z_factor = 1.0;
473
474
14.5k
    if ((name = pj_param(P->ctx, P->params, "sxy_in").s) != nullptr) {
475
14.0k
        const char *normalized_name = nullptr;
476
14.0k
        f = get_unit_conversion_factor(name, &xy_in_is_linear,
477
14.0k
                                       &normalized_name);
478
14.0k
        if (f != 0.0) {
479
13.9k
            proj_log_trace(P, "xy_in unit: %s", normalized_name);
480
13.9k
        } else {
481
118
            f = pj_param(P->ctx, P->params, "dxy_in").f;
482
118
            if (f == 0.0 || 1.0 / f == 0.0) {
483
30
                proj_log_error(P, _("unknown xy_in unit"));
484
30
                return pj_default_destructor(
485
30
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
486
30
            }
487
118
        }
488
14.0k
        Q->xy_factor = f;
489
14.0k
        if (normalized_name != nullptr) {
490
13.9k
            if (strcmp(normalized_name, "Radian") == 0)
491
6.88k
                P->left = PJ_IO_UNITS_RADIANS;
492
13.9k
            if (strcmp(normalized_name, "Degree") == 0)
493
6.52k
                P->left = PJ_IO_UNITS_DEGREES;
494
13.9k
        }
495
14.0k
    }
496
497
14.5k
    if ((name = pj_param(P->ctx, P->params, "sxy_out").s) != nullptr) {
498
13.9k
        const char *normalized_name = nullptr;
499
13.9k
        f = get_unit_conversion_factor(name, &xy_out_is_linear,
500
13.9k
                                       &normalized_name);
501
13.9k
        if (f != 0.0) {
502
13.9k
            proj_log_trace(P, "xy_out unit: %s", normalized_name);
503
13.9k
        } else {
504
65
            f = pj_param(P->ctx, P->params, "dxy_out").f;
505
65
            if (f == 0.0 || 1.0 / f == 0.0) {
506
51
                proj_log_error(P, _("unknown xy_out unit"));
507
51
                return pj_default_destructor(
508
51
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
509
51
            }
510
65
        }
511
13.9k
        Q->xy_factor /= f;
512
13.9k
        if (normalized_name != nullptr) {
513
13.9k
            if (strcmp(normalized_name, "Radian") == 0)
514
6.55k
                P->right = PJ_IO_UNITS_RADIANS;
515
13.9k
            if (strcmp(normalized_name, "Degree") == 0)
516
6.83k
                P->right = PJ_IO_UNITS_DEGREES;
517
13.9k
        }
518
13.9k
    }
519
520
14.4k
    if (xy_in_is_linear >= 0 && xy_out_is_linear >= 0 &&
521
14.4k
        xy_in_is_linear != xy_out_is_linear) {
522
2
        proj_log_error(P, _("inconsistent unit type between xy_in and xy_out"));
523
2
        return pj_default_destructor(P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
524
2
    }
525
526
14.4k
    if ((name = pj_param(P->ctx, P->params, "sz_in").s) != nullptr) {
527
1.26k
        const char *normalized_name = nullptr;
528
1.26k
        f = get_unit_conversion_factor(name, &z_in_is_linear, &normalized_name);
529
1.26k
        if (f != 0.0) {
530
1.16k
            proj_log_trace(P, "z_in unit: %s", normalized_name);
531
1.16k
        } else {
532
101
            f = pj_param(P->ctx, P->params, "dz_in").f;
533
101
            if (f == 0.0 || 1.0 / f == 0.0) {
534
9
                proj_log_error(P, _("unknown z_in unit"));
535
9
                return pj_default_destructor(
536
9
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
537
9
            }
538
101
        }
539
1.25k
        Q->z_factor = f;
540
1.25k
    }
541
542
14.4k
    if ((name = pj_param(P->ctx, P->params, "sz_out").s) != nullptr) {
543
1.25k
        const char *normalized_name = nullptr;
544
1.25k
        f = get_unit_conversion_factor(name, &z_out_is_linear,
545
1.25k
                                       &normalized_name);
546
1.25k
        if (f != 0.0) {
547
1.17k
            proj_log_trace(P, "z_out unit: %s", normalized_name);
548
1.17k
        } else {
549
81
            f = pj_param(P->ctx, P->params, "dz_out").f;
550
81
            if (f == 0.0 || 1.0 / f == 0.0) {
551
38
                proj_log_error(P, _("unknown z_out unit"));
552
38
                return pj_default_destructor(
553
38
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
554
38
            }
555
81
        }
556
1.22k
        Q->z_factor /= f;
557
1.22k
    }
558
559
14.4k
    if (z_in_is_linear >= 0 && z_out_is_linear >= 0 &&
560
14.4k
        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
14.4k
    if ((name = pj_param(P->ctx, P->params, "st_in").s) != nullptr) {
566
34
        for (i = 0; (s = time_units[i].id) && strcmp(name, s); ++i)
567
26
            ;
568
569
8
        if (!s) {
570
2
            proj_log_error(P, _("unknown t_in unit"));
571
2
            return pj_default_destructor(P,
572
2
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
573
2
        }
574
575
6
        Q->t_in_id = i;
576
6
        proj_log_trace(P, "t_in unit: %s", time_units[i].name);
577
6
    }
578
579
14.4k
    s = nullptr;
580
14.4k
    if ((name = pj_param(P->ctx, P->params, "st_out").s) != nullptr) {
581
76
        for (i = 0; (s = time_units[i].id) && strcmp(name, s); ++i)
582
52
            ;
583
584
24
        if (!s) {
585
1
            proj_log_error(P, _("unknown t_out unit"));
586
1
            return pj_default_destructor(P,
587
1
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
588
1
        }
589
590
23
        Q->t_out_id = i;
591
23
        proj_log_trace(P, "t_out unit: %s", time_units[i].name);
592
23
    }
593
594
14.4k
    return P;
595
14.4k
}