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