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 #define PJ_LIB__
67
68 #include <errno.h>
69 #include <math.h>
70 #include <string.h>
71 #include <time.h>
72
73 #include "proj_internal.h"
74 #include <math.h>
75
76 PROJ_HEAD(unitconvert, "Unit conversion");
77
78 typedef double (*tconvert)(double);
79
80 namespace { // anonymous namespace
81 struct TIME_UNITS {
82 const char *id; /* units keyword */
83 tconvert t_in; /* unit -> mod. julian date function pointer */
84 tconvert t_out; /* mod. julian date > unit function pointer */
85 const char *name; /* comments */
86 };
87 } // anonymous namespace
88
89 namespace { // anonymous namespace
90 struct pj_opaque_unitconvert {
91 int t_in_id; /* time unit id for the time input unit */
92 int t_out_id; /* time unit id for the time output unit */
93 double xy_factor; /* unit conversion factor for horizontal components */
94 double z_factor; /* unit conversion factor for vertical components */
95 };
96 } // anonymous namespace
97
98
99 /***********************************************************************/
is_leap_year(long year)100 static int is_leap_year(long year) {
101 /***********************************************************************/
102 return ((year % 4 == 0 && year % 100 != 0) || year % 400 ==0);
103 }
104
105
106 /***********************************************************************/
days_in_year(long year)107 static int days_in_year(long year) {
108 /***********************************************************************/
109 return is_leap_year(year) ? 366 : 365;
110 }
111
112 /***********************************************************************/
days_in_month(unsigned long year,unsigned long month)113 static unsigned int days_in_month(unsigned long year, unsigned long month) {
114 /***********************************************************************/
115 const unsigned int month_table[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
116 unsigned int days;
117
118 if (month > 12) month = 12;
119 if (month == 0) month = 1;
120
121 days = month_table[month-1];
122 if (is_leap_year(year) && month == 2) days++;
123
124 return days;
125 }
126
127
128 /***********************************************************************/
daynumber_in_year(unsigned long year,unsigned long month,unsigned long day)129 static int daynumber_in_year(unsigned long year, unsigned long month, unsigned long day) {
130 /***********************************************************************/
131 unsigned int daynumber=0, i;
132
133 if (month > 12) month = 12;
134 if (month == 0) month = 1;
135 if (day > days_in_month(year, month)) day = days_in_month(year, month);
136
137 for (i = 1; i < month; i++)
138 daynumber += days_in_month(year, i);
139
140 daynumber += day;
141
142 return daynumber;
143
144 }
145
146 /***********************************************************************/
mjd_to_mjd(double mjd)147 static double mjd_to_mjd(double mjd) {
148 /***********************************************************************
149 Modified julian date no-op function.
150
151 The Julian date is defined as (fractional) days since midnight
152 on 16th of November in 1858.
153 ************************************************************************/
154 return mjd;
155 }
156
157
158 /***********************************************************************/
decimalyear_to_mjd(double decimalyear)159 static double decimalyear_to_mjd(double decimalyear) {
160 /***********************************************************************
161 Epoch of modified julian date is 1858-11-16 00:00
162 ************************************************************************/
163 long year;
164 double fractional_year;
165 double mjd;
166
167 // Written this way to deal with NaN input
168 if( !(decimalyear >= -10000 && decimalyear <= 10000) )
169 return 0;
170
171 year = lround(floor(decimalyear));
172 fractional_year = decimalyear - year;
173 mjd = (year - 1859)*365 + 14 + 31;
174 mjd += (double)fractional_year*(double)days_in_year(year);
175
176 /* take care of leap days */
177 year--;
178 for (; year > 1858; year--)
179 if (is_leap_year(year))
180 mjd++;
181
182 return mjd;
183 }
184
185
186 /***********************************************************************/
mjd_to_decimalyear(double mjd)187 static double mjd_to_decimalyear(double mjd) {
188 /***********************************************************************
189 Epoch of modified julian date is 1858-11-16 00:00
190 ************************************************************************/
191 double decimalyear = mjd;
192 double mjd_iter = 14 + 31;
193 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 for (; mjd >= mjd_iter; year++) {
198 mjd_iter += days_in_year(year);
199 }
200 year--;
201 mjd_iter -= days_in_year(year);
202
203 decimalyear = year + (mjd-mjd_iter)/days_in_year(year);
204 return decimalyear;
205 }
206
207
208 /***********************************************************************/
gps_week_to_mjd(double gps_week)209 static double gps_week_to_mjd(double gps_week) {
210 /***********************************************************************
211 GPS weeks are defined as the number of weeks since January the 6th
212 1980.
213
214 Epoch of gps weeks is 1980-01-06 00:00, which in modified Julian
215 date is 44244.
216 ************************************************************************/
217 return 44244.0 + gps_week*7.0;
218 }
219
220
221 /***********************************************************************/
mjd_to_gps_week(double mjd)222 static double mjd_to_gps_week(double mjd) {
223 /***********************************************************************
224 GPS weeks are defined as the number of weeks since January the 6th
225 1980.
226
227 Epoch of gps weeks is 1980-01-06 00:00, which in modified Julian
228 date is 44244.
229 ************************************************************************/
230 return (mjd - 44244.0) / 7.0;
231 }
232
233
234 /***********************************************************************/
yyyymmdd_to_mjd(double yyyymmdd)235 static double yyyymmdd_to_mjd(double yyyymmdd) {
236 /************************************************************************
237 Date given in YYYY-MM-DD format.
238 ************************************************************************/
239
240 long year = lround(floor( yyyymmdd / 10000 ));
241 long month = lround(floor((yyyymmdd - year*10000) / 100));
242 long day = lround(floor( yyyymmdd - year*10000 - month*100));
243 double mjd = daynumber_in_year(year, month, day);
244
245 for (year -= 1; year > 1858; year--)
246 mjd += days_in_year(year);
247
248 return mjd + 13 + 31;
249 }
250
251
252 /***********************************************************************/
mjd_to_yyyymmdd(double mjd)253 static double mjd_to_yyyymmdd(double mjd) {
254 /************************************************************************
255 Date given in YYYY-MM-DD format.
256 ************************************************************************/
257 double mjd_iter = 14 + 31;
258 int year = 1859, month=0, day=0;
259
260 for (; mjd >= mjd_iter; year++) {
261 mjd_iter += days_in_year(year);
262 }
263 year--;
264 mjd_iter -= days_in_year(year);
265
266 for (month=1; mjd_iter + days_in_month(year, month) <= mjd; month++)
267 mjd_iter += days_in_month(year, month);
268
269 day = (int)(mjd - mjd_iter + 1);
270
271 return year*10000.0 + month*100.0 + day;
272 }
273
274 static const struct TIME_UNITS time_units[] = {
275 {"mjd", mjd_to_mjd, mjd_to_mjd, "Modified julian date"},
276 {"decimalyear", decimalyear_to_mjd, mjd_to_decimalyear, "Decimal year"},
277 {"gps_week", gps_week_to_mjd, mjd_to_gps_week, "GPS Week"},
278 {"yyyymmdd", yyyymmdd_to_mjd, mjd_to_yyyymmdd, "YYYYMMDD date"},
279 {nullptr, nullptr, nullptr, nullptr}
280 };
281
282
283 /***********************************************************************/
forward_2d(PJ_LP lp,PJ * P)284 static PJ_XY forward_2d(PJ_LP lp, PJ *P) {
285 /************************************************************************
286 Forward unit conversions in the plane
287 ************************************************************************/
288 struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *) P->opaque;
289 PJ_COORD point = {{0,0,0,0}};
290 point.lp = lp;
291
292 point.xy.x *= Q->xy_factor;
293 point.xy.y *= Q->xy_factor;
294
295 return point.xy;
296 }
297
298
299 /***********************************************************************/
reverse_2d(PJ_XY xy,PJ * P)300 static PJ_LP reverse_2d(PJ_XY xy, PJ *P) {
301 /************************************************************************
302 Reverse unit conversions in the plane
303 ************************************************************************/
304 struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *) P->opaque;
305 PJ_COORD point = {{0,0,0,0}};
306 point.xy = xy;
307
308 point.xy.x /= Q->xy_factor;
309 point.xy.y /= Q->xy_factor;
310
311 return point.lp;
312 }
313
314
315 /***********************************************************************/
forward_3d(PJ_LPZ lpz,PJ * P)316 static PJ_XYZ forward_3d(PJ_LPZ lpz, PJ *P) {
317 /************************************************************************
318 Forward unit conversions the vertical component
319 ************************************************************************/
320 struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *) P->opaque;
321 PJ_COORD point = {{0,0,0,0}};
322 point.lpz = lpz;
323
324 /* take care of the horizontal components in the 2D function */
325 point.xy = forward_2d(point.lp, P);
326
327 point.xyz.z *= Q->z_factor;
328
329 return point.xyz;
330 }
331
332 /***********************************************************************/
reverse_3d(PJ_XYZ xyz,PJ * P)333 static PJ_LPZ reverse_3d(PJ_XYZ xyz, PJ *P) {
334 /************************************************************************
335 Reverse unit conversions the vertical component
336 ************************************************************************/
337 struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *) P->opaque;
338 PJ_COORD point = {{0,0,0,0}};
339 point.xyz = xyz;
340
341 /* take care of the horizontal components in the 2D function */
342 point.lp = reverse_2d(point.xy, P);
343
344 point.xyz.z /= Q->z_factor;
345
346 return point.lpz;
347 }
348
349
350 /***********************************************************************/
forward_4d(PJ_COORD obs,PJ * P)351 static PJ_COORD forward_4d(PJ_COORD obs, PJ *P) {
352 /************************************************************************
353 Forward conversion of time units
354 ************************************************************************/
355 struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *) P->opaque;
356 PJ_COORD out = obs;
357
358 /* delegate unit conversion of physical dimensions to the 3D function */
359 out.xyz = forward_3d(obs.lpz, P);
360
361 if (Q->t_in_id >= 0)
362 out.xyzt.t = time_units[Q->t_in_id].t_in( obs.xyzt.t );
363 if (Q->t_out_id >= 0)
364 out.xyzt.t = time_units[Q->t_out_id].t_out( out.xyzt.t );
365
366 return out;
367 }
368
369
370 /***********************************************************************/
reverse_4d(PJ_COORD obs,PJ * P)371 static PJ_COORD reverse_4d(PJ_COORD obs, PJ *P) {
372 /************************************************************************
373 Reverse conversion of time units
374 ************************************************************************/
375 struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *) P->opaque;
376 PJ_COORD out = obs;
377
378 /* delegate unit conversion of physical dimensions to the 3D function */
379 out.lpz = reverse_3d(obs.xyz, P);
380
381 if (Q->t_out_id >= 0)
382 out.xyzt.t = time_units[Q->t_out_id].t_in( obs.xyzt.t );
383 if (Q->t_in_id >= 0)
384 out.xyzt.t = time_units[Q->t_in_id].t_out( out.xyzt.t );
385
386 return out;
387 }
388
389 /***********************************************************************/
get_unit_conversion_factor(const char * name,int * p_is_linear,const char ** p_normalized_name)390 static double get_unit_conversion_factor(const char* name,
391 int* p_is_linear,
392 const char** p_normalized_name) {
393 /***********************************************************************/
394 int i;
395 const char* s;
396 const PJ_UNITS *units = pj_list_linear_units();
397
398 /* Try first with linear units */
399 for (i = 0; (s = units[i].id) ; ++i) {
400 if ( strcmp(s, name) == 0 ) {
401 if( p_normalized_name ) {
402 *p_normalized_name = units[i].name;
403 }
404 if( p_is_linear ) {
405 *p_is_linear = 1;
406 }
407 return units[i].factor;
408 }
409 }
410
411 /* And then angular units */
412 units = pj_list_angular_units();
413 for (i = 0; (s = units[i].id) ; ++i) {
414 if ( strcmp(s, name) == 0 ) {
415 if( p_normalized_name ) {
416 *p_normalized_name = units[i].name;
417 }
418 if( p_is_linear ) {
419 *p_is_linear = 0;
420 }
421 return units[i].factor;
422 }
423 }
424 if( p_normalized_name ) {
425 *p_normalized_name = nullptr;
426 }
427 if( p_is_linear ) {
428 *p_is_linear = -1;
429 }
430 return 0.0;
431 }
432
433 /***********************************************************************/
434 PJ *CONVERSION(unitconvert,0) {
435 /***********************************************************************/
436 struct pj_opaque_unitconvert *Q = static_cast<struct pj_opaque_unitconvert*>(pj_calloc (1, sizeof (struct pj_opaque_unitconvert)));
437 const char *s, *name;
438 int i;
439 double f;
440 int xy_in_is_linear = -1; /* unknown */
441 int xy_out_is_linear = -1; /* unknown */
442 int z_in_is_linear = -1; /* unknown */
443 int z_out_is_linear = -1; /* unknown */
444
445 if (nullptr==Q)
446 return pj_default_destructor (P, ENOMEM);
447 P->opaque = (void *) Q;
448
449 P->fwd4d = forward_4d;
450 P->inv4d = reverse_4d;
451 P->fwd3d = forward_3d;
452 P->inv3d = reverse_3d;
453 P->fwd = forward_2d;
454 P->inv = reverse_2d;
455
456 P->left = PJ_IO_UNITS_WHATEVER;
457 P->right = PJ_IO_UNITS_WHATEVER;
458 P->skip_fwd_prepare = 1;
459 P->skip_inv_prepare = 1;
460
461 /* if no time input/output unit is specified we can skip them */
462 Q->t_in_id = -1;
463 Q->t_out_id = -1;
464
465 Q->xy_factor = 1.0;
466 Q->z_factor = 1.0;
467
468 if ((name = pj_param (P->ctx, P->params, "sxy_in").s) != nullptr) {
469 const char* normalized_name = nullptr;
470 f = get_unit_conversion_factor(name, &xy_in_is_linear, &normalized_name);
471 if (f != 0.0) {
472 proj_log_trace(P, "xy_in unit: %s", normalized_name);
473 } else {
474 f = pj_param (P->ctx, P->params, "dxy_in").f;
475 if (f == 0.0 || 1.0 / f == 0.0)
476 return pj_default_destructor(P, PJD_ERR_UNKNOWN_UNIT_ID);
477 }
478 Q->xy_factor = f;
479 if (normalized_name != nullptr) {
480 if (strcmp(normalized_name, "Radian") == 0)
481 P->left = PJ_IO_UNITS_RADIANS;
482 if (strcmp(normalized_name, "Degree") == 0)
483 P->left = PJ_IO_UNITS_DEGREES;
484 }
485 }
486
487 if ((name = pj_param (P->ctx, P->params, "sxy_out").s) != nullptr) {
488 const char* normalized_name = nullptr;
489 f = get_unit_conversion_factor(name, &xy_out_is_linear, &normalized_name);
490 if (f != 0.0) {
491 proj_log_trace(P, "xy_out unit: %s", normalized_name);
492 } else {
493 f = pj_param (P->ctx, P->params, "dxy_out").f;
494 if (f == 0.0 || 1.0 / f == 0.0)
495 return pj_default_destructor(P, PJD_ERR_UNKNOWN_UNIT_ID);
496 }
497 Q->xy_factor /= f;
498 if (normalized_name != nullptr) {
499 if (strcmp(normalized_name, "Radian") == 0)
500 P->right= PJ_IO_UNITS_RADIANS;
501 if (strcmp(normalized_name, "Degree") == 0)
502 P->right= PJ_IO_UNITS_DEGREES;
503 }
504 }
505
506 if( xy_in_is_linear >= 0 && xy_out_is_linear >= 0 &&
507 xy_in_is_linear != xy_out_is_linear ) {
508 proj_log_debug(P, "inconsistent unit type between xy_in and xy_out");
509 return pj_default_destructor(P, PJD_ERR_INCONSISTENT_UNIT);
510 }
511
512 if ((name = pj_param (P->ctx, P->params, "sz_in").s) != nullptr) {
513 const char* normalized_name = nullptr;
514 f = get_unit_conversion_factor(name, &z_in_is_linear, &normalized_name);
515 if (f != 0.0) {
516 proj_log_trace(P, "z_in unit: %s", normalized_name);
517 } else {
518 f = pj_param (P->ctx, P->params, "dz_in").f;
519 if (f == 0.0 || 1.0 / f == 0.0)
520 return pj_default_destructor(P, PJD_ERR_UNKNOWN_UNIT_ID);
521 }
522 Q->z_factor = f;
523 }
524
525 if ((name = pj_param (P->ctx, P->params, "sz_out").s) != nullptr) {
526 const char* normalized_name = nullptr;
527 f = get_unit_conversion_factor(name, &z_out_is_linear, &normalized_name);
528 if (f != 0.0) {
529 proj_log_trace(P, "z_out unit: %s", normalized_name);
530 } else {
531 f = pj_param (P->ctx, P->params, "dz_out").f;
532 if (f == 0.0 || 1.0 / f == 0.0)
533 return pj_default_destructor(P, PJD_ERR_UNKNOWN_UNIT_ID);
534 }
535 Q->z_factor /= f;
536 }
537
538 if( z_in_is_linear >= 0 && z_out_is_linear >= 0 &&
539 z_in_is_linear != z_out_is_linear ) {
540 proj_log_debug(P, "inconsistent unit type between z_in and z_out");
541 return pj_default_destructor(P, PJD_ERR_INCONSISTENT_UNIT);
542 }
543
544 if ((name = pj_param (P->ctx, P->params, "st_in").s) != nullptr) {
545 for (i = 0; (s = time_units[i].id) && strcmp(name, s) ; ++i);
546
547 if (!s) return pj_default_destructor(P, PJD_ERR_UNKNOWN_UNIT_ID); /* unknown unit conversion id */
548
549 Q->t_in_id = i;
550 proj_log_trace(P, "t_in unit: %s", time_units[i].name);
551 }
552
553 s = nullptr;
554 if ((name = pj_param (P->ctx, P->params, "st_out").s) != nullptr) {
555 for (i = 0; (s = time_units[i].id) && strcmp(name, s) ; ++i);
556
557 if (!s) return pj_default_destructor(P, PJD_ERR_UNKNOWN_UNIT_ID); /* unknown unit conversion id */
558
559 Q->t_out_id = i;
560 proj_log_trace(P, "t_out unit: %s", time_units[i].name);
561 }
562
563 return P;
564 }
565