1 /*============================================================================
2   WCSLIB 7.7 - an implementation of the FITS WCS standard.
3   Copyright (C) 1995-2021, Mark Calabretta
4 
5   This file is part of WCSLIB.
6 
7   WCSLIB is free software: you can redistribute it and/or modify it under the
8   terms of the GNU Lesser General Public License as published by the Free
9   Software Foundation, either version 3 of the License, or (at your option)
10   any later version.
11 
12   WCSLIB is distributed in the hope that it will be useful, but WITHOUT ANY
13   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14   FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
15   more details.
16 
17   You should have received a copy of the GNU Lesser General Public License
18   along with WCSLIB.  If not, see http://www.gnu.org/licenses.
19 
20   Author: Mark Calabretta, Australia Telescope National Facility, CSIRO.
21   http://www.atnf.csiro.au/people/Mark.Calabretta
22   $Id: wcshdr.c,v 7.7 2021/07/12 06:36:49 mcalabre Exp $
23 *===========================================================================*/
24 
25 #include <ctype.h>
26 #include <math.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30 
31 #include "wcserr.h"
32 #include "wcsmath.h"
33 #include "wcsutil.h"
34 #include "wcshdr.h"
35 #include "wtbarr.h"
36 #include "tab.h"
37 #include "dis.h"
38 #include "wcs.h"
39 
40 extern const int WCSSET;
41 
42 extern const int DIS_DOTPD;
43 
44 // Map status return value to message.
45 const char *wcshdr_errmsg[] = {
46   "Success",
47   "Null wcsprm pointer passed",
48   "Memory allocation failed",
49   "Invalid column selection",
50   "Fatal error returned by Flex parser",
51   "Invalid tabular parameters"};
52 
53 // Map error returns for lower-level routines.
54 const int wcshdr_taberr[] = {
55   WCSHDRERR_SUCCESS,		//  0: TABERR_SUCCESS
56   WCSHDRERR_NULL_POINTER,	//  1: TABERR_NULL_POINTER
57   WCSHDRERR_MEMORY,		//  2: TABERR_MEMORY
58   WCSHDRERR_BAD_TABULAR_PARAMS	//  3: TABERR_BAD_PARAMS
59 				//  4: TABERR_BAD_X
60 				//  5: TABERR_BAD_WORLD
61 };
62 
63 // Convenience macro for invoking wcserr_set().
64 #define WCSHDR_ERRMSG(status) WCSERR_SET(status), wcshdr_errmsg[status]
65 
66 // Internal helper functions, not for general use.
67 static void wcshdo_format(int, int, const double [], char *);
68 static void wcshdo_tpdterm(int, int, char *);
69 static void wcshdo_util(int, const char [], const char [], int, const char [],
70   int, int, int, char, int, int [], char [], const char [], int *, char **,
71   int *);
72 
73 //----------------------------------------------------------------------------
74 
wcstab(struct wcsprm * wcs)75 int wcstab(struct wcsprm *wcs)
76 
77 {
78   static const char *function = "wcstab";
79 
80   char (*PSi_0a)[72] = 0x0, (*PSi_1a)[72] = 0x0, (*PSi_2a)[72] = 0x0;
81   int  *PVi_1a = 0x0, *PVi_2a = 0x0, *PVi_3a = 0x0, *tabax, *tabidx = 0x0;
82   int   getcrd, i, ip, itab, itabax, j, jtabax, m, naxis, ntabax, status;
83   struct wtbarr *wtbp;
84   struct tabprm *tabp;
85   struct wcserr **err;
86 
87   if (wcs == 0x0) return WCSHDRERR_NULL_POINTER;
88   err = &(wcs->err);
89 
90   // Free memory previously allocated by wcstab().
91   if (wcs->flag != -1 && wcs->m_flag == WCSSET) {
92     if (wcs->wtb == wcs->m_wtb) wcs->wtb = 0x0;
93     if (wcs->tab == wcs->m_tab) wcs->tab = 0x0;
94 
95     if (wcs->m_wtb) free(wcs->m_wtb);
96     if (wcs->m_tab) {
97       for (j = 0; j < wcs->ntab; j++) {
98         tabfree(wcs->m_tab + j);
99       }
100 
101       free(wcs->m_tab);
102     }
103   }
104 
105   wcs->ntab = 0;
106   wcs->nwtb = 0;
107   wcs->wtb  = 0x0;
108   wcs->tab  = 0x0;
109 
110 
111   // Determine the number of -TAB axes.
112   naxis = wcs->naxis;
113   if (!(tabax = calloc(naxis, sizeof(int)))) {
114     return wcserr_set(WCSHDR_ERRMSG(WCSHDRERR_MEMORY));
115   }
116 
117   ntabax = 0;
118   for (i = 0; i < naxis; i++) {
119     // Null fill.
120     wcsutil_null_fill(72, wcs->ctype[i]);
121 
122     if (!strcmp(wcs->ctype[i]+4, "-TAB")) {
123       tabax[i] = ntabax++;
124     } else {
125       tabax[i] = -1;
126     }
127   }
128 
129   if (ntabax == 0) {
130     // No lookup tables.
131     status = 0;
132     goto cleanup;
133   }
134 
135 
136   // Collect information from the PSi_ma and PVi_ma keyvalues.
137   if (!((PSi_0a = calloc(ntabax, sizeof(char[72]))) &&
138         (PVi_1a = calloc(ntabax, sizeof(int)))      &&
139         (PVi_2a = calloc(ntabax, sizeof(int)))      &&
140         (PSi_1a = calloc(ntabax, sizeof(char[72]))) &&
141         (PSi_2a = calloc(ntabax, sizeof(char[72]))) &&
142         (PVi_3a = calloc(ntabax, sizeof(int)))      &&
143         (tabidx = calloc(ntabax, sizeof(int))))) {
144     status = wcserr_set(WCSHDR_ERRMSG(WCSHDRERR_MEMORY));
145     goto cleanup;
146   }
147 
148   for (itabax = 0; itabax < ntabax; itabax++) {
149     // Remember that calloc() zeroes allocated memory.
150     PVi_1a[itabax] = 1;
151     PVi_2a[itabax] = 1;
152     PVi_3a[itabax] = 1;
153   }
154 
155   for (ip = 0; ip < wcs->nps; ip++) {
156     itabax = tabax[wcs->ps[ip].i - 1];
157     if (itabax >= 0) {
158       switch (wcs->ps[ip].m) {
159       case 0:
160         // EXTNAME.
161         strcpy(PSi_0a[itabax], wcs->ps[ip].value);
162         wcsutil_null_fill(72, PSi_0a[itabax]);
163         break;
164       case 1:
165         // TTYPEn for coordinate array.
166         strcpy(PSi_1a[itabax], wcs->ps[ip].value);
167         wcsutil_null_fill(72, PSi_1a[itabax]);
168         break;
169       case 2:
170         // TTYPEn for index vector.
171         strcpy(PSi_2a[itabax], wcs->ps[ip].value);
172         wcsutil_null_fill(72, PSi_2a[itabax]);
173         break;
174       }
175     }
176   }
177 
178   for (ip = 0; ip < wcs->npv; ip++) {
179     itabax = tabax[wcs->pv[ip].i - 1];
180     if (itabax >= 0) {
181       switch (wcs->pv[ip].m) {
182       case 1:
183         // EXTVER.
184         PVi_1a[itabax] = (int)(wcs->pv[ip].value + 0.5);
185         break;
186       case 2:
187         // EXTLEVEL.
188         PVi_2a[itabax] = (int)(wcs->pv[ip].value + 0.5);
189         break;
190       case 3:
191         // Table axis number.
192         PVi_3a[itabax] = (int)(wcs->pv[ip].value + 0.5);
193         break;
194       }
195     }
196   }
197 
198 
199   // Determine the number of independent tables.
200   for (itabax = 0; itabax < ntabax; itabax++) {
201     // These have no defaults.
202     if (!PSi_0a[itabax][0] || !PSi_1a[itabax][0]) {
203       status = wcserr_set(WCSERR_SET(WCSHDRERR_BAD_TABULAR_PARAMS),
204         "Invalid tabular parameters: PSi_0a and PSi_1a must be specified");
205       goto cleanup;
206     }
207 
208     tabidx[itabax] = -1;
209     for (jtabax = 0; jtabax < i; jtabax++) {
210       // EXTNAME, EXTVER, EXTLEVEL, and TTYPEn for the coordinate array
211       // must match for each axis of a multi-dimensional lookup table.
212       if (strcmp(PSi_0a[itabax], PSi_0a[jtabax]) == 0 &&
213           strcmp(PSi_1a[itabax], PSi_1a[jtabax]) == 0 &&
214           PVi_1a[itabax] == PVi_1a[jtabax] &&
215           PVi_2a[itabax] == PVi_2a[jtabax]) {
216         tabidx[itabax] = tabidx[jtabax];
217         break;
218       }
219     }
220 
221     if (jtabax == itabax) {
222       tabidx[itabax] = wcs->ntab;
223       wcs->ntab++;
224     }
225   }
226 
227   if (!(wcs->tab = calloc(wcs->ntab, sizeof(struct tabprm)))) {
228     status = wcserr_set(WCSHDR_ERRMSG(WCSHDRERR_MEMORY));
229     goto cleanup;
230   }
231   wcs->m_tab = wcs->tab;
232 
233   // Table dimensionality; find the largest axis number.
234   for (itabax = 0; itabax < ntabax; itabax++) {
235     tabp = wcs->tab + tabidx[itabax];
236 
237     // PVi_3a records the 1-relative table axis number.
238     if (PVi_3a[itabax] > tabp->M) {
239       tabp->M = PVi_3a[itabax];
240     }
241   }
242 
243   for (itab = 0; itab < wcs->ntab; itab++) {
244     if ((status = tabini(1, wcs->tab[itab].M, 0, wcs->tab + itab))) {
245       status = wcserr_set(WCSHDR_ERRMSG(wcshdr_taberr[status]));
246       goto cleanup;
247     }
248   }
249 
250 
251   // Copy parameters into the tabprm structs.
252   for (i = 0; i < naxis; i++) {
253     if ((itabax = tabax[i]) < 0) {
254       // Not a -TAB axis.
255       continue;
256     }
257 
258     // PVi_3a records the 1-relative table axis number.
259     m = PVi_3a[itabax] - 1;
260 
261     tabp = wcs->tab + tabidx[itabax];
262     tabp->map[m] = i;
263     tabp->crval[m] = wcs->crval[i];
264   }
265 
266   // Check for completeness.
267   for (itab = 0; itab < wcs->ntab; itab++) {
268     for (m = 0; m < wcs->tab[itab].M; m++) {
269       if (wcs->tab[itab].map[m] < 0) {
270         status = wcserr_set(WCSERR_SET(WCSHDRERR_BAD_TABULAR_PARAMS),
271           "Invalid tabular parameters: the axis mapping is undefined");
272         goto cleanup;
273       }
274     }
275   }
276 
277 
278   // Set up for reading the arrays; how many arrays are there?
279   for (itabax = 0; itabax < ntabax; itabax++) {
280     // Does this -TAB axis have a non-degenerate index array?
281     if (PSi_2a[itabax][0]) {
282       wcs->nwtb++;
283     }
284   }
285 
286   // Add one coordinate array for each table.
287   wcs->nwtb += wcs->ntab;
288 
289   // Allocate memory for structs to be returned.
290   if (!(wcs->wtb = calloc(wcs->nwtb, sizeof(struct wtbarr)))) {
291     wcs->nwtb = 0;
292 
293     status = wcserr_set(WCSHDR_ERRMSG(WCSHDRERR_MEMORY));
294     goto cleanup;
295   }
296   wcs->m_wtb = wcs->wtb;
297 
298   // Set pointers for the index and coordinate arrays.
299   wtbp = wcs->wtb;
300   for (itab = 0; itab < wcs->ntab; itab++) {
301     getcrd = 1;
302     for (itabax = 0; itabax < ntabax; itabax++) {
303       if (tabidx[itabax] != itab) continue;
304 
305       if (getcrd) {
306         // Coordinate array.
307         wtbp->i = itabax + 1;
308         wtbp->m = PVi_3a[itabax];
309         wtbp->kind = 'c';
310 
311         strcpy(wtbp->extnam, PSi_0a[itabax]);
312         wtbp->extver = PVi_1a[itabax];
313         wtbp->extlev = PVi_2a[itabax];
314         strcpy(wtbp->ttype, PSi_1a[itabax]);
315         wtbp->row    = 1L;
316         wtbp->ndim   = wcs->tab[itab].M + 1;
317         wtbp->dimlen = wcs->tab[itab].K;
318         wtbp->arrayp = &(wcs->tab[itab].coord);
319 
320         // Signal for tabset() to take this memory.
321         wcs->tab[itab].m_coord = (double *)0x1;
322 
323         wtbp++;
324         getcrd = 0;
325       }
326 
327       if (PSi_2a[itabax][0]) {
328         // Index array.
329         wtbp->i = itabax + 1;
330         wtbp->m = PVi_3a[itabax];
331         wtbp->kind = 'i';
332 
333         m = wtbp->m - 1;
334         strcpy(wtbp->extnam, PSi_0a[itabax]);
335         wtbp->extver = PVi_1a[itabax];
336         wtbp->extlev = PVi_2a[itabax];
337         strcpy(wtbp->ttype, PSi_2a[itabax]);
338         wtbp->row    = 1L;
339         wtbp->ndim   = 1;
340         wtbp->dimlen = wcs->tab[itab].K + m;
341         wtbp->arrayp = wcs->tab[itab].index + m;
342 
343         // Signal for tabset() to take this memory.
344         wcs->tab[itab].m_indxs[m] = (double *)0x1;
345 
346         wtbp++;
347       }
348     }
349   }
350 
351   status = 0;
352 
353 cleanup:
354   if (tabax)  free(tabax);
355   if (tabidx) free(tabidx);
356   if (PSi_0a) free(PSi_0a);
357   if (PVi_1a) free(PVi_1a);
358   if (PVi_2a) free(PVi_2a);
359   if (PSi_1a) free(PSi_1a);
360   if (PSi_2a) free(PSi_2a);
361   if (PVi_3a) free(PVi_3a);
362 
363   if (status) {
364     if (wcs->tab) free(wcs->tab);
365     if (wcs->wtb) free(wcs->wtb);
366   }
367 
368   return status;
369 }
370 
371 //----------------------------------------------------------------------------
372 
wcsidx(int nwcs,struct wcsprm ** wcs,int alts[27])373 int wcsidx(int nwcs, struct wcsprm **wcs, int alts[27])
374 
375 {
376   int a, iwcs;
377   struct wcsprm *wcsp;
378 
379   for (a = 0; a < 27; a++) {
380     alts[a] = -1;
381   }
382 
383   if (wcs == 0x0) {
384     return WCSHDRERR_NULL_POINTER;
385   }
386 
387   wcsp = *wcs;
388   for (iwcs = 0; iwcs < nwcs; iwcs++, wcsp++) {
389     if (wcsp->colnum || wcsp->colax[0]) continue;
390 
391     if (wcsp->alt[0] == ' ') {
392       a = 0;
393     } else {
394       a = wcsp->alt[0] - 'A' + 1;
395     }
396 
397     alts[a] = iwcs;
398   }
399 
400   return 0;
401 }
402 
403 //----------------------------------------------------------------------------
404 
wcsbdx(int nwcs,struct wcsprm ** wcs,int type,short alts[1000][28])405 int wcsbdx(int nwcs, struct wcsprm **wcs, int type, short alts[1000][28])
406 
407 {
408   short  *ip;
409   int    a, i, icol, iwcs;
410   struct wcsprm *wcsp;
411 
412   for (ip = alts[0]; ip < alts[0] + 28*1000; ip++) {
413     *ip = -1;
414   }
415 
416   for (icol = 0; icol < 1000; icol++) {
417     alts[icol][27] = 0;
418   }
419 
420   if (wcs == 0x0) {
421     return WCSHDRERR_NULL_POINTER;
422   }
423 
424   wcsp = *wcs;
425   for (iwcs = 0; iwcs < nwcs; iwcs++, wcsp++) {
426     if (wcsp->alt[0] == ' ') {
427       a = 0;
428     } else {
429       a = wcsp->alt[0] - 'A' + 1;
430     }
431 
432     if (type) {
433       // Pixel list.
434       if (wcsp->colax[0]) {
435         for (i = 0; i < wcsp->naxis; i++) {
436           alts[wcsp->colax[i]][a]  = iwcs;
437           alts[wcsp->colax[i]][27]++;
438         }
439       } else if (!wcsp->colnum) {
440         alts[0][a]  = iwcs;
441         alts[0][27]++;
442       }
443 
444     } else {
445       // Binary table image array.
446       if (wcsp->colnum) {
447         alts[wcsp->colnum][a] = iwcs;
448         alts[wcsp->colnum][27]++;
449       } else if (!wcsp->colax[0]) {
450         alts[0][a]  = iwcs;
451         alts[0][27]++;
452       }
453     }
454   }
455 
456   return 0;
457 }
458 
459 //----------------------------------------------------------------------------
460 
wcsvfree(int * nwcs,struct wcsprm ** wcs)461 int wcsvfree(int *nwcs, struct wcsprm **wcs)
462 
463 {
464   int a, status = 0;
465   struct wcsprm *wcsp;
466 
467   if (wcs == 0x0) {
468     return WCSHDRERR_NULL_POINTER;
469   }
470 
471   wcsp = *wcs;
472   for (a = 0; a < *nwcs; a++, wcsp++) {
473     status |= wcsfree(wcsp);
474   }
475 
476   free(*wcs);
477 
478   *nwcs = 0;
479   *wcs = 0x0;
480 
481   return status;
482 }
483 
484 //----------------------------------------------------------------------------
485 // Matching the definitions in dis.c.
486 #define I_DTYPE   0	// Distortion type code.
487 #define I_NIPARM  1	// Full (allocated) length of iparm[].
488 #define I_NDPARM  2	// No. of parameters in dparm[], excl. work space.
489 #define I_TPDNCO  3	// No. of TPD coefficients, forward...
490 #define I_TPDINV  4	// ...and inverse.
491 #define I_TPDAUX  5	// True if auxiliary variables are used.
492 #define I_TPDRAD  6	// True if the radial variable is used.
493 
wcshdo(int ctrl,struct wcsprm * wcs,int * nkeyrec,char ** header)494 int wcshdo(int ctrl, struct wcsprm *wcs, int *nkeyrec, char **header)
495 
496 // ::: CUBEFACE and STOKES handling?
497 
498 {
499   static const char *function = "wcshdo";
500 
501   const char axid[] = "xyxuvu", *cp;
502   const int  nTPD[] = {1, 4, 7, 12, 17, 24, 31, 40, 49, 60};
503 
504   char alt, comment[72], ctemp[32], *ctypei, format[16], fmt01[8],
505        keyvalue[96], keyword[16], *kp, obsg[8] = "OBSG?",
506        obsgeo[8] = "OBSGEO-?", pq, ptype, xtype, term[16], timeunit[16],
507        tpdsrc[24], xyz[] = "XYZ";
508   int  *axmap, bintab, *colax, colnum, degree, direct = 0, doaux = 0, dofmt,
509        dosip, dotpd, dotpv, i, idis, idp, *iparm, j, jhat, k, kp0, kpi, m,
510        naxis, ncoeff, Nhat, p, pixlist, precision, primage, q, status = 0;
511   double *dparm, keyval;
512   struct auxprm *aux;
513   struct disprm *dis;
514   struct dpkey  *keyp;
515   struct wcserr **err;
516 
517   *nkeyrec = 0;
518   *header  = 0x0;
519 
520   if (wcs == 0x0) return WCSHDRERR_NULL_POINTER;
521   err = &(wcs->err);
522 
523   if (wcs->flag != WCSSET) {
524     if ((status = wcsset(wcs))) return status;
525   }
526 
527   if ((naxis = wcs->naxis) == 0) {
528     return 0;
529   }
530 
531 
532   // These are mainly for convenience.
533   alt = wcs->alt[0];
534   if (alt == ' ') alt = '\0';
535   colnum = wcs->colnum;
536   colax  = wcs->colax;
537 
538   primage = 0;
539   bintab  = 0;
540   pixlist = 0;
541   if (colnum) {
542     bintab  = 1;
543   } else if (colax[0]) {
544     pixlist = 1;
545   } else {
546     primage = 1;
547   }
548 
549 
550   // Initialize floating point format control.
551   *format = '\0';
552   if (ctrl & WCSHDO_P17) {
553     strcpy(format, "% 20.17G");
554   } else if (ctrl & WCSHDO_P16) {
555     strcpy(format, "% 20.16G");
556   } else if (ctrl & WCSHDO_P15) {
557     strcpy(format, "% 20.15G");
558   } else if (ctrl & WCSHDO_P14) {
559     strcpy(format, "% 20.14G");
560   } else if (ctrl & WCSHDO_P13) {
561     strcpy(format, "% 20.13G");
562   } else if (ctrl & WCSHDO_P12) {
563     strcpy(format, "%20.12G");
564   }
565 
566   if (*format && (ctrl & WCSHDO_EFMT)) {
567     if (format[6] == 'G') {
568       format[6] = 'E';
569     } else {
570       format[7] = 'E';
571     }
572   }
573 
574   dofmt = (*format == '\0');
575 
576 
577   // WCS dimension.
578   if (!pixlist) {
579     sprintf(keyvalue, "%20d", naxis);
580     wcshdo_util(ctrl, "WCSAXES", "WCAX", 0, 0x0, 0, 0, 0, alt, colnum, colax,
581       keyvalue, "Number of coordinate axes", nkeyrec, header, &status);
582   }
583 
584   // Reference pixel coordinates.
585   if (dofmt) wcshdo_format('G', naxis, wcs->crpix, format);
586   for (j = 0; j < naxis; j++) {
587     wcsutil_double2str(keyvalue, format, wcs->crpix[j]);
588     wcshdo_util(ctrl, "CRPIX", "CRP", WCSHDO_CRPXna, "CRPX", 0, j+1, 0, alt,
589       colnum, colax, keyvalue, "Pixel coordinate of reference point", nkeyrec,
590       header, &status);
591   }
592 
593   // Linear transformation matrix.
594   if (dofmt) wcshdo_format('G', naxis*naxis, wcs->pc, format);
595   k = 0;
596   for (i = 0; i < naxis; i++) {
597     for (j = 0; j < naxis; j++, k++) {
598       if (i == j) {
599         if (wcs->pc[k] == 1.0) continue;
600       } else {
601         if (wcs->pc[k] == 0.0) continue;
602       }
603 
604       wcsutil_double2str(keyvalue, format, wcs->pc[k]);
605       wcshdo_util(ctrl, "PC", bintab ? "PC" : "P", WCSHDO_TPCn_ka,
606         bintab ? 0x0 : "PC", i+1, j+1, 0, alt, colnum, colax,
607         keyvalue, "Coordinate transformation matrix element",
608         nkeyrec, header, &status);
609     }
610   }
611 
612   // Coordinate increment at reference point.
613   if (dofmt) wcshdo_format('G', naxis, wcs->cdelt, format);
614   for (i = 0; i < naxis; i++) {
615     wcsutil_double2str(keyvalue, format, wcs->cdelt[i]);
616     comment[0] = '\0';
617     if (wcs->cunit[i][0]) sprintf(comment, "[%s] ", wcs->cunit[i]);
618     strcat(comment, "Coordinate increment at reference point");
619     wcshdo_util(ctrl, "CDELT", "CDE", WCSHDO_CRPXna, "CDLT", i+1, 0, 0, alt,
620       colnum, colax, keyvalue, comment, nkeyrec, header, &status);
621   }
622 
623   // Units of coordinate increment and reference value.
624   for (i = 0; i < naxis; i++) {
625     if (wcs->cunit[i][0] == '\0') continue;
626 
627     sprintf(keyvalue, "'%s'", wcs->cunit[i]);
628     wcshdo_util(ctrl, "CUNIT", "CUN", WCSHDO_CRPXna, "CUNI", i+1, 0, 0, alt,
629       colnum, colax, keyvalue, "Units of coordinate increment and value",
630       nkeyrec, header, &status);
631   }
632 
633   // May need to alter ctype for particular distortions so do basic checks
634   // now.  Note that SIP, TPV, DSS, TNX, and ZPX are restricted to exactly
635   // two axes and cannot coexist with other distortion types.
636   dosip = 0;
637   dotpv = 0;
638   dotpd = 0;
639 
640   if ((dis = wcs->lin.dispre)) {
641     for (i = 0; i < naxis; i++) {
642       if (strcmp(dis->dtype[i], "SIP") == 0) {
643         // Simple Imaging Polynomial (SIP).  Write it in its native form
644         // if possible, unless specifically requested to write it as TPD.
645         dotpd = (dis->iparm[i][I_DTYPE] & DIS_DOTPD);
646 
647         if (!dotpd) {;
648           if (alt ||
649               dis->Nhat[0]      != 2 ||
650               dis->Nhat[1]      != 2 ||
651               dis->axmap[0][0]  != 0 ||
652               dis->axmap[0][1]  != 1 ||
653               dis->axmap[1][0]  != 0 ||
654               dis->axmap[1][1]  != 1 ||
655               dis->offset[0][0] != wcs->crpix[0] ||
656               dis->offset[0][1] != wcs->crpix[1] ||
657               dis->offset[1][0] != wcs->crpix[0] ||
658               dis->offset[1][1] != wcs->crpix[1] ||
659               dis->scale[0][0]  != 1.0 ||
660               dis->scale[0][1]  != 1.0 ||
661               dis->scale[1][0]  != 1.0 ||
662               dis->scale[1][1]  != 1.0) {
663             // Must have been read as a 'SIP' distortion, CPDISja = 'SIP'.
664             // Cannot be written as native SIP so write it as TPD.
665             dotpd = DIS_DOTPD;
666           } else if (strncmp(wcs->ctype[0], "RA---TAN", 8) ||
667                      strncmp(wcs->ctype[1], "DEC--TAN", 8)) {
668             // Must have been permuted by wcssub().
669             // Native SIP doesn't have axis mapping so write it as TPD.
670             dotpd = DIS_DOTPD;
671           }
672 
673           if (dotpd) {
674             strcpy(tpdsrc, "SIP coordinates");
675           } else {
676             dosip = 1;
677           }
678         }
679 
680         break;
681       }
682     }
683   }
684 
685   if ((dis = wcs->lin.disseq)) {
686     for (i = 0; i < naxis; i++) {
687       if (strcmp(dis->dtype[i], "TPV") == 0) {
688         // TPV "projection".  Write it in its native form if possible,
689         // unless specifically requested to write it as TPD.
690         dotpd = (dis->iparm[i][I_DTYPE] & DIS_DOTPD);
691 
692         if (!dotpd) {;
693           if (dis->axmap[wcs->lng][0] != wcs->lng ||
694               dis->axmap[wcs->lng][1] != wcs->lat ||
695               dis->axmap[wcs->lat][0] != wcs->lat ||
696               dis->axmap[wcs->lat][1] != wcs->lng ||
697               dis->offset[wcs->lng][wcs->lng] != 0.0 ||
698               dis->offset[wcs->lng][wcs->lat] != 0.0 ||
699               dis->offset[wcs->lat][wcs->lng] != 0.0 ||
700               dis->offset[wcs->lat][wcs->lat] != 0.0 ||
701               dis->scale[wcs->lng][wcs->lng]  != 1.0 ||
702               dis->scale[wcs->lng][wcs->lat]  != 1.0 ||
703               dis->scale[wcs->lat][wcs->lng]  != 1.0 ||
704               dis->scale[wcs->lat][wcs->lat]  != 1.0) {
705             // Must have been read as a 'TPV' distortion, CPDISja = 'TPV'.
706             // Cannot be written as native TPV so write it as TPD.
707             dotpd = DIS_DOTPD;
708           }
709 
710           if (dotpd) {
711             strcpy(tpdsrc, "TPV \"projection\"");
712           } else {
713             dotpv = 1;
714           }
715         }
716 
717         break;
718 
719       } else if (strcmp(dis->dtype[i], "DSS") == 0) {
720         // Always written as TPD.
721         dotpd = DIS_DOTPD;
722         strcpy(tpdsrc, dis->dtype[i]);
723 
724       } else if (strncmp(dis->dtype[i], "WAT", 3) == 0) {
725         // Always written as TPD.
726         dotpd = DIS_DOTPD;
727         strcpy(tpdsrc, dis->dtype[i]+4);
728 
729         if (strcmp(dis->dtype[i], "DSS") == 0) {
730           strcpy(tpdsrc, wcs->wcsname);
731         } else {
732           strcat(tpdsrc, " \"projection\"");
733         }
734 
735         break;
736       }
737     }
738   }
739 
740   // Coordinate type.
741   for (i = 0; i < naxis; i++) {
742     if (wcs->ctype[i][0] == '\0') continue;
743 
744     sprintf(keyvalue, "'%s'", wcs->ctype[i]);
745     strcpy(comment, "Coordinate type code");
746 
747     ctypei = keyvalue + 1;
748     if (i == wcs->lng || i == wcs->lat) {
749       // Alter ctype for particular distortions.
750       if (dosip) {
751         // It could have come in as CPDISja = 'SIP'.
752         strcpy(ctypei+8, "-SIP'");
753       } else if (dotpv) {
754         // Reinstate projection code edited by wcsset().
755         strcpy(ctypei+4, "-TPV'");
756       }
757 
758       if (strncmp(ctypei+8, "-SIP", 4) == 0) {
759         strcpy(comment, "TAN (gnomonic) projection + SIP distortions");
760 
761       } else if (strncmp(ctypei+4, "-TPV", 4) == 0) {
762         strcpy(comment, "TAN (gnomonic) projection + distortions");
763 
764       } else {
765         if (strncmp(ctypei, "RA--", 4) == 0) {
766           strcpy(comment, "Right ascension, ");
767 
768         } else if (strncmp(ctypei, "DEC-", 4) == 0) {
769           strcpy(comment, "Declination, ");
770 
771         } else if (strncmp(ctypei+1, "LON", 3) == 0 ||
772                    strncmp(ctypei+1, "LAT", 3) == 0) {
773           ctypei[0] = toupper(ctypei[0]);
774 
775           switch (ctypei[0]) {
776           case 'G':
777             strcpy(comment, "galactic ");
778             break;
779           case 'E':
780             strcpy(comment, "ecliptic ");
781             break;
782           case 'H':
783             strcpy(comment, "helioecliptic ");
784             break;
785           case 'S':
786             strcpy(comment, "supergalactic ");
787             break;
788           }
789 
790           if (i == wcs->lng) {
791             strcat(comment, "longitude, ");
792           } else {
793             strcat(comment, "latitude, ");
794           }
795         }
796 
797         strcat(comment, wcs->cel.prj.name);
798         strcat(comment, " projection");
799       }
800 
801     } else if (i == wcs->spec) {
802       spctyp(wcs->ctype[i], 0x0, 0x0, comment, 0x0, &ptype, &xtype, 0x0);
803       if (ptype == xtype) {
804         strcat(comment, " (linear)");
805       } else {
806         switch (xtype) {
807         case 'F':
808           strcat(comment, " (linear in frequency)");
809           break;
810         case 'V':
811           strcat(comment, " (linear in velocity)");
812           break;
813         case 'W':
814           strcat(comment, " (linear in wavelength)");
815           break;
816         }
817       }
818     }
819 
820     wcshdo_util(ctrl, "CTYPE", "CTY", WCSHDO_CRPXna, "CTYP", i+1, 0, 0, alt,
821       colnum, colax, keyvalue, comment, nkeyrec, header, &status);
822   }
823 
824   // Coordinate value at reference point.
825   for (i = 0; i < naxis; i++) {
826     if (dofmt) wcshdo_format('G', 1, wcs->crval+i, format);
827     wcsutil_double2str(keyvalue, format, wcs->crval[i]);
828     comment[0] = '\0';
829     if (wcs->cunit[i][0]) sprintf(comment, "[%s] ", wcs->cunit[i]);
830     strcat(comment, "Coordinate value at reference point");
831     wcshdo_util(ctrl, "CRVAL", "CRV", WCSHDO_CRPXna, "CRVL", i+1, 0, 0, alt,
832       colnum, colax, keyvalue, comment, nkeyrec, header, &status);
833   }
834 
835   // Parameter values.
836   if (dofmt) strcpy(format, "%20.12G");
837   for (k = 0; k < wcs->npv; k++) {
838     wcsutil_double2str(keyvalue, format, (wcs->pv[k]).value);
839     if ((wcs->pv[k]).i == (wcs->lng + 1)) {
840       switch ((wcs->pv[k]).m) {
841       case 1:
842         strcpy(comment, "[deg] Native longitude of the reference point");
843         break;
844       case 2:
845         strcpy(comment, "[deg] Native latitude  of the reference point");
846         break;
847       case 3:
848         if (primage) {
849           sprintf(keyword, "LONPOLE%c", alt);
850         } else if (bintab) {
851           sprintf(keyword, "LONP%d%c", colnum, alt);
852         } else {
853           sprintf(keyword, "LONP%d%c", colax[(wcs->pv[k]).i - 1], alt);
854         }
855         sprintf(comment, "[deg] alias for %s (has precedence)", keyword);
856         break;
857       case 4:
858         if (primage) {
859           sprintf(keyword, "LATPOLE%c", alt);
860         } else if (bintab) {
861           sprintf(keyword, "LATP%d%c", colnum, alt);
862         } else {
863           sprintf(keyword, "LATP%d%c", colax[(wcs->pv[k]).i - 1], alt);
864         }
865         sprintf(comment, "[deg] alias for %s (has precedence)", keyword);
866         break;
867       }
868     } else if ((wcs->pv[k]).i == (wcs->lat + 1)) {
869       sprintf(comment, "%s projection parameter", wcs->cel.prj.code);
870     } else {
871       strcpy(comment, "Coordinate transformation parameter");
872     }
873 
874     wcshdo_util(ctrl, "PV", "V", WCSHDO_PVn_ma, "PV", wcs->pv[k].i, -1,
875       wcs->pv[k].m, alt, colnum, colax, keyvalue, comment,
876       nkeyrec, header, &status);
877   }
878 
879   for (k = 0; k < wcs->nps; k++) {
880     sprintf(keyvalue, "'%s'", (wcs->ps[k]).value);
881     wcshdo_util(ctrl, "PS", "S", WCSHDO_PVn_ma, "PS", wcs->ps[k].i, -1,
882       wcs->ps[k].m, alt, colnum, colax, keyvalue,
883       "Coordinate transformation parameter",
884       nkeyrec, header, &status);
885   }
886 
887   // Celestial and spectral transformation parameters.
888   if (!undefined(wcs->lonpole)) {
889     wcsutil_double2str(keyvalue, format, wcs->lonpole);
890     wcshdo_util(ctrl, "LONPOLE", "LONP", 0, 0x0, 0, 0, 0, alt,
891       colnum, colax, keyvalue, "[deg] Native longitude of celestial pole",
892       nkeyrec, header, &status);
893   }
894 
895   if (!undefined(wcs->latpole)) {
896     wcsutil_double2str(keyvalue, format, wcs->latpole);
897     wcshdo_util(ctrl, "LATPOLE", "LATP", 0, 0x0, 0, 0, 0, alt,
898       colnum, colax, keyvalue, "[deg] Native latitude of celestial pole",
899       nkeyrec, header, &status);
900   }
901 
902   if (wcs->restfrq != 0.0) {
903     wcsutil_double2str(keyvalue, format, wcs->restfrq);
904     wcshdo_util(ctrl, "RESTFRQ", "RFRQ", 0, 0x0, 0, 0, 0, alt,
905       colnum, colax, keyvalue, "[Hz] Line rest frequency",
906       nkeyrec, header, &status);
907   }
908 
909   if (wcs->restwav != 0.0) {
910     wcsutil_double2str(keyvalue, format, wcs->restwav);
911     wcshdo_util(ctrl, "RESTWAV", "RWAV", 0, 0x0, 0, 0, 0, alt,
912       colnum, colax, keyvalue, "[Hz] Line rest wavelength",
913       nkeyrec, header, &status);
914   }
915 
916   // - - - - - - - - - - - - - - - - -  Auxiliary coordinate axis information.
917   sprintf(timeunit, "%.15s", wcs->timeunit[0] ? wcs->timeunit : "s");
918 
919   // Coordinate axis title.
920   if (wcs->cname) {
921     for (i = 0; i < naxis; i++) {
922       if (wcs->cname[i][0] == '\0') continue;
923 
924       sprintf(keyvalue, "'%s'", wcs->cname[i]);
925       wcshdo_util(ctrl, "CNAME", "CNA", WCSHDO_CNAMna, "CNAM", i+1, 0, 0,
926         alt, colnum, colax, keyvalue, "Axis name for labelling purposes",
927         nkeyrec, header, &status);
928     }
929   }
930 
931   // Random error in coordinate.
932   if (wcs->crder) {
933     for (i = 0; i < naxis; i++) {
934       if (undefined(wcs->crder[i])) continue;
935 
936       wcsutil_double2str(keyvalue, format, wcs->crder[i]);
937       comment[0] = '\0';
938       if (wcs->cunit[i][0]) sprintf(comment, "[%s] ", wcs->cunit[i]);
939       strcat(comment, "Random error in coordinate");
940       wcshdo_util(ctrl, "CRDER", "CRD", WCSHDO_CNAMna, "CRDE", i+1, 0, 0,
941         alt, colnum, colax, keyvalue, comment, nkeyrec, header, &status);
942     }
943   }
944 
945   // Systematic error in coordinate.
946   if (wcs->csyer) {
947     for (i = 0; i < naxis; i++) {
948       if (undefined(wcs->csyer[i])) continue;
949 
950       wcsutil_double2str(keyvalue, format, wcs->csyer[i]);
951       comment[0] = '\0';
952       if (wcs->cunit[i][0]) sprintf(comment, "[%s] ", wcs->cunit[i]);
953       strcat(comment, "Systematic error in coordinate");
954       wcshdo_util(ctrl, "CSYER", "CSY", WCSHDO_CNAMna, "CSYE", i+1, 0, 0,
955         alt, colnum, colax, keyvalue, comment, nkeyrec, header, &status);
956     }
957   }
958 
959   // Time at zero point of phase axis.
960   if (wcs->czphs) {
961     for (i = 0; i < naxis; i++) {
962       if (undefined(wcs->czphs[i])) continue;
963 
964       wcsutil_double2str(keyvalue, format, wcs->czphs[i]);
965       sprintf(comment, "[%s] Time at zero point of phase axis", timeunit);
966       wcshdo_util(ctrl, "CZPHS", "CZP", WCSHDO_CNAMna, "CZPH", i+1, 0, 0,
967         alt, colnum, colax, keyvalue, comment, nkeyrec, header, &status);
968     }
969   }
970 
971   // Period of phase axis.
972   if (wcs->cperi) {
973     for (i = 0; i < naxis; i++) {
974       if (undefined(wcs->cperi[i])) continue;
975 
976       wcsutil_double2str(keyvalue, format, wcs->cperi[i]);
977       sprintf(comment, "[%s] Period of phase axis", timeunit);
978       wcshdo_util(ctrl, "CPERI", "CPR", WCSHDO_CNAMna, "CPER", i+1, 0, 0,
979         alt, colnum, colax, keyvalue, comment, nkeyrec, header, &status);
980     }
981   }
982 
983   // - - - - - - - - - - - - - - - - - - - - - - - -  Coordinate system title.
984 
985   // Coordinate system title.
986   if (wcs->wcsname[0]) {
987     sprintf(keyvalue, "'%s'", wcs->wcsname);
988     if (bintab) {
989       wcshdo_util(ctrl, "WCSNAME", "WCSN", 0, 0x0, 0, 0, 0, alt,
990         colnum, colax, keyvalue, "Coordinate system title",
991         nkeyrec, header, &status);
992     } else {
993       // TWCS was a mistake.
994       wcshdo_util(ctrl, "WCSNAME", "TWCS", WCSHDO_WCSNna, "WCSN", 0, 0, 0,
995         alt, colnum, colax, keyvalue, "Coordinate system title",
996         nkeyrec, header, &status);
997     }
998   }
999 
1000   // - - - - - - - - - - - - - - - - -  Time reference system and measurement.
1001 
1002   // Time scale.
1003   if (wcs->timesys[0]) {
1004     sprintf(keyvalue, "'%s'", wcs->timesys);
1005     wcshdo_util(ctrl, "TIMESYS", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1006       "Time scale", nkeyrec, header, &status);
1007   }
1008 
1009   // Time reference position.
1010   if (wcs->trefpos[0]) {
1011     sprintf(keyvalue, "'%s'", wcs->trefpos);
1012     wcshdo_util(ctrl, "TREFPOS", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1013       "Time reference position", nkeyrec, header, &status);
1014   }
1015 
1016   // Time reference direction.
1017   if (wcs->trefdir[0]) {
1018     sprintf(keyvalue, "'%s'", wcs->trefdir);
1019     wcshdo_util(ctrl, "TREFDIR", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1020       "Time reference direction", nkeyrec, header, &status);
1021   }
1022 
1023   // Ephemerides used for pathlength delay calculation.
1024   if (wcs->plephem[0]) {
1025     sprintf(keyvalue, "'%s'", wcs->plephem);
1026     wcshdo_util(ctrl, "PLEPHEM", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1027       "Ephemerides used for pathlength delays", nkeyrec, header, &status);
1028   }
1029 
1030   // Time units.
1031   if (wcs->timeunit[0]) {
1032     sprintf(keyvalue, "'%s'", wcs->timeunit);
1033     wcshdo_util(ctrl, "TIMEUNIT", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1034       "Time units", nkeyrec, header, &status);
1035   }
1036 
1037   // Fiducial (reference) time.
1038   if (wcs->mjdref[0] == 0.0 && wcs->mjdref[1] == 0.0) {
1039     // MJD of fiducial time (simplified if it takes its default value).
1040     wcsutil_double2str(keyvalue, format, 0.0);
1041     wcshdo_util(ctrl, "MJDREF", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1042       "[d] MJD of fiducial time", nkeyrec, header, &status);
1043 
1044   } else {
1045     // ISO-8601 fiducial time.
1046     if (wcs->dateref[0]) {
1047       sprintf(keyvalue, "'%s'", wcs->dateref);
1048       wcshdo_util(ctrl, "DATEREF", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0,
1049         keyvalue, "ISO-8601 fiducial time", nkeyrec, header, &status);
1050     }
1051 
1052     if (wcs->mjdref[1] == 0.0) {
1053       // MJD of fiducial time (no fractional part).
1054       if (!undefined(wcs->mjdref[0])) {
1055         wcsutil_double2str(keyvalue, format, wcs->mjdref[0]);
1056         wcshdo_util(ctrl, "MJDREF", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0,
1057           keyvalue, "[d] MJD of fiducial time", nkeyrec, header, &status);
1058       }
1059 
1060     } else {
1061       // MJD of fiducial time, integer part.
1062       if (!undefined(wcs->mjdref[0])) {
1063         wcsutil_double2str(keyvalue, format, wcs->mjdref[0]);
1064         wcshdo_util(ctrl, "MJDREFI", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0,
1065           keyvalue, "[d] MJD of fiducial time, integer part", nkeyrec,
1066           header, &status);
1067       }
1068 
1069       // MJD of fiducial time, fractional part.
1070       if (!undefined(wcs->mjdref[1])) {
1071         wcsutil_double2str(keyvalue, format, wcs->mjdref[1]);
1072         wcshdo_util(ctrl, "MJDREFF", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0,
1073           keyvalue, "[d] MJD of fiducial time, fractional part", nkeyrec,
1074           header, &status);
1075       }
1076     }
1077   }
1078 
1079   // Clock correction.
1080   if (!undefined(wcs->timeoffs)) {
1081     wcsutil_double2str(keyvalue, format, wcs->timeoffs);
1082     sprintf(comment, "[%s] Clock correction", timeunit);
1083     wcshdo_util(ctrl, "TIMEOFFS", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1084       comment, nkeyrec, header, &status);
1085   }
1086 
1087   // - - - - - - - - - - - - - - - - - - - - -  Data timestamps and durations.
1088 
1089   // ISO-8601 time of observation.
1090   if (wcs->dateobs[0]) {
1091     sprintf(keyvalue, "'%s'", wcs->dateobs);
1092     strcpy(comment, "ISO-8601 time of observation");
1093 
1094     if (ctrl & 1) {
1095       // Allow DOBSn.
1096       wcshdo_util(ctrl, "DATE-OBS", "DOBS", WCSHDO_DOBSn, 0x0, 0, 0, 0, ' ',
1097         colnum, colax, keyvalue, comment, nkeyrec, header, &status);
1098     } else {
1099       // Force DATE-OBS.
1100       wcshdo_util(ctrl, "DATE-OBS", 0x0, 0, 0x0, 0, 0, 0, ' ',
1101         0, 0x0, keyvalue, comment, nkeyrec, header, &status);
1102     }
1103   }
1104 
1105   // MJD of observation.
1106   if (!undefined(wcs->mjdobs)) {
1107     wcsutil_double2str(keyvalue, format, wcs->mjdobs);
1108     wcshdo_util(ctrl, "MJD-OBS", "MJDOB", 0, 0x0, 0, 0, 0, ' ',
1109       colnum, colax, keyvalue, "[d] MJD of observation",
1110       nkeyrec, header, &status);
1111   }
1112 
1113   // Julian epoch of observation.
1114   if (!undefined(wcs->jepoch)) {
1115     wcsutil_double2str(keyvalue, format, wcs->jepoch);
1116     wcshdo_util(ctrl, "JEPOCH", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1117       "[a] Julian epoch of observation", nkeyrec, header, &status);
1118   }
1119 
1120   // Besselian epoch of observation.
1121   if (!undefined(wcs->bepoch)) {
1122     wcsutil_double2str(keyvalue, format, wcs->bepoch);
1123     wcshdo_util(ctrl, "BEPOCH", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1124       "[a] Besselian epoch of observation", nkeyrec, header, &status);
1125   }
1126 
1127   // ISO-8601 time at start of observation.
1128   if (wcs->datebeg[0]) {
1129     sprintf(keyvalue, "'%s'", wcs->datebeg);
1130     wcshdo_util(ctrl, "DATE-BEG", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1131       "ISO-8601 time at start of observation", nkeyrec, header, &status);
1132   }
1133 
1134   // MJD at start of observation.
1135   if (!undefined(wcs->mjdbeg)) {
1136     wcsutil_double2str(keyvalue, format, wcs->mjdbeg);
1137     wcshdo_util(ctrl, "MJD-BEG", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1138       "[d] MJD at start of observation", nkeyrec, header, &status);
1139   }
1140 
1141   // Time elapsed at start since fiducial time.
1142   if (!undefined(wcs->tstart)) {
1143     wcsutil_double2str(keyvalue, format, wcs->tstart);
1144     sprintf(comment, "[%s] Time elapsed since fiducial time at start",
1145       timeunit);
1146     wcshdo_util(ctrl, "TSTART", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1147       comment, nkeyrec, header, &status);
1148   }
1149 
1150   // ISO-8601 time at midpoint of observation.
1151   if (wcs->dateavg[0]) {
1152     sprintf(keyvalue, "'%s'", wcs->dateavg);
1153     wcshdo_util(ctrl, "DATE-AVG", "DAVG", 0, 0x0, 0, 0, 0, ' ',
1154       colnum, colax, keyvalue, "ISO-8601 time at midpoint of observation",
1155       nkeyrec, header, &status);
1156   }
1157 
1158   // MJD at midpoint of observation.
1159   if (!undefined(wcs->mjdavg)) {
1160     wcsutil_double2str(keyvalue, format, wcs->mjdavg);
1161     wcshdo_util(ctrl, "MJD-AVG", "MJDA", 0, 0x0, 0, 0, 0, ' ',
1162       colnum, colax, keyvalue, "[d] MJD at midpoint of observation",
1163       nkeyrec, header, &status);
1164   }
1165 
1166   // ISO-8601 time at end of observation.
1167   if (wcs->dateend[0]) {
1168     sprintf(keyvalue, "'%s'", wcs->dateend);
1169     wcshdo_util(ctrl, "DATE-END", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1170       "ISO-8601 time at end of observation", nkeyrec, header, &status);
1171   }
1172 
1173   // MJD at end of observation.
1174   if (!undefined(wcs->mjdend)) {
1175     wcsutil_double2str(keyvalue, format, wcs->mjdend);
1176     wcshdo_util(ctrl, "MJD-END", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1177       "[d] MJD at end of observation", nkeyrec, header, &status);
1178   }
1179 
1180   // Time elapsed at end since fiducial time.
1181   if (!undefined(wcs->tstop)) {
1182     wcsutil_double2str(keyvalue, format, wcs->tstop);
1183     sprintf(comment, "[%s] Time elapsed since fiducial time at end",
1184       timeunit);
1185     wcshdo_util(ctrl, "TSTOP", "", 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1186       comment, nkeyrec, header, &status);
1187   }
1188 
1189   // Exposure (integration) time.
1190   if (!undefined(wcs->xposure)) {
1191     wcsutil_double2str(keyvalue, format, wcs->xposure);
1192     sprintf(comment, "[%s] Exposure (integration) time", timeunit);
1193     wcshdo_util(ctrl, "XPOSURE", "", 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1194       comment, nkeyrec, header, &status);
1195   }
1196 
1197   // Elapsed time (start to stop).
1198   if (!undefined(wcs->telapse)) {
1199     wcsutil_double2str(keyvalue, format, wcs->telapse);
1200     sprintf(comment, "[%s] Elapsed time (start to stop)", timeunit);
1201     wcshdo_util(ctrl, "TELAPSE", "", 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1202       comment, nkeyrec, header, &status);
1203   }
1204 
1205   // - - - - - - - - - - - - - - - - - - - - - - - - - - - -  Timing accuracy.
1206 
1207   // Systematic error in time measurements.
1208   if (!undefined(wcs->timsyer)) {
1209     wcsutil_double2str(keyvalue, format, wcs->timsyer);
1210     sprintf(comment, "[%s] Systematic error in time measurements", timeunit);
1211     wcshdo_util(ctrl, "TIMSYER", "", 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1212       comment, nkeyrec, header, &status);
1213   }
1214 
1215   // Relative error in time measurements.
1216   if (!undefined(wcs->timrder)) {
1217     wcsutil_double2str(keyvalue, format, wcs->timrder);
1218     sprintf(comment, "[%s] Relative error in time measurements", timeunit);
1219     wcshdo_util(ctrl, "TIMRDER", "", 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1220       comment, nkeyrec, header, &status);
1221   }
1222 
1223   // Time resolution.
1224   if (!undefined(wcs->timedel)) {
1225     wcsutil_double2str(keyvalue, format, wcs->timedel);
1226     sprintf(comment, "[%s] Time resolution", timeunit);
1227     wcshdo_util(ctrl, "TIMEDEL", "", 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1228       comment, nkeyrec, header, &status);
1229   }
1230 
1231   // Reference position of timestamp in binned data.
1232   if (!undefined(wcs->timepixr)) {
1233     wcsutil_double2str(keyvalue, format, wcs->timepixr);
1234     wcshdo_util(ctrl, "TIMEPIXR", "", 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1235       "Reference position of timestamp in binned data", nkeyrec, header,
1236       &status);
1237   }
1238 
1239   // - - - - - - - - - - - - - - - - - -  Spatial & celestial reference frame.
1240 
1241   // Observatory coordinates.
1242   if (!undefined(wcs->obsgeo[0]) &&
1243       !undefined(wcs->obsgeo[1]) &&
1244       !undefined(wcs->obsgeo[2])) {
1245 
1246     for (k = 0; k < 3; k++) {
1247       wcsutil_double2str(keyvalue, format, wcs->obsgeo[k]);
1248       sprintf(comment, "[m] observatory %c-coordinate", xyz[k]);
1249       obsgeo[7] = xyz[k];
1250       obsg[4]   = xyz[k];
1251       wcshdo_util(ctrl, obsgeo, obsg, 0, 0x0, 0, 0, 0, ' ',
1252         colnum, colax, keyvalue, comment, nkeyrec, header, &status);
1253     }
1254 
1255   } else if (
1256       !undefined(wcs->obsgeo[3]) &&
1257       !undefined(wcs->obsgeo[4]) &&
1258       !undefined(wcs->obsgeo[5])) {
1259 
1260     wcsutil_double2str(keyvalue, format, wcs->obsgeo[3]);
1261     wcshdo_util(ctrl, "OBSGEO-L", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1262       "[deg] IAU(1976) observatory longitude", nkeyrec, header, &status);
1263 
1264     wcsutil_double2str(keyvalue, format, wcs->obsgeo[4]);
1265     wcshdo_util(ctrl, "OBSGEO-B", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1266       "[deg] IAU(1976) observatory latitude", nkeyrec, header, &status);
1267 
1268     wcsutil_double2str(keyvalue, format, wcs->obsgeo[5]);
1269     wcshdo_util(ctrl, "OBSGEO-L", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1270       "[m]   IAU(1976) observatory height", nkeyrec, header, &status);
1271   }
1272 
1273   // Spacecraft orbit ephemeris file.
1274   if (wcs->obsorbit[0]) {
1275     sprintf(keyvalue, "'%s'", wcs->obsorbit);
1276     wcshdo_util(ctrl, "OBSORBIT", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0, keyvalue,
1277       "Spacecraft orbit ephemeris file", nkeyrec, header, &status);
1278   }
1279 
1280   // Equatorial coordinate system type.
1281   if (wcs->radesys[0]) {
1282     sprintf(keyvalue, "'%s'", wcs->radesys);
1283     wcshdo_util(ctrl, "RADESYS", "RADE", 0, 0x0, 0, 0, 0, alt, colnum, colax,
1284       keyvalue, "Equatorial coordinate system", nkeyrec, header, &status);
1285   }
1286 
1287   // Equinox of equatorial coordinate system.
1288   if (!undefined(wcs->equinox)) {
1289     wcsutil_double2str(keyvalue, format, wcs->equinox);
1290     wcshdo_util(ctrl, "EQUINOX", "EQUI", 0, 0x0, 0, 0, 0, alt, colnum, colax,
1291       keyvalue, "[yr] Equinox of equatorial coordinates", nkeyrec, header,
1292       &status);
1293   }
1294 
1295   // Reference frame of spectral coordinates.
1296   if (wcs->specsys[0]) {
1297     sprintf(keyvalue, "'%s'", wcs->specsys);
1298     wcshdo_util(ctrl, "SPECSYS", "SPEC", 0, 0x0, 0, 0, 0, alt, colnum, colax,
1299       keyvalue, "Reference frame of spectral coordinates", nkeyrec, header,
1300       &status);
1301   }
1302 
1303   // Reference frame of spectral observation.
1304   if (wcs->ssysobs[0]) {
1305     sprintf(keyvalue, "'%s'", wcs->ssysobs);
1306     wcshdo_util(ctrl, "SSYSOBS", "SOBS", 0, 0x0, 0, 0, 0, alt, colnum, colax,
1307       keyvalue, "Reference frame of spectral observation", nkeyrec, header,
1308       &status);
1309   }
1310 
1311   // Observer's velocity towards source.
1312   if (!undefined(wcs->velosys)) {
1313     wcsutil_double2str(keyvalue, format, wcs->velosys);
1314     wcshdo_util(ctrl, "VELOSYS", "VSYS", 0, 0x0, 0, 0, 0, alt, colnum, colax,
1315       keyvalue, "[m/s] Velocity towards source", nkeyrec, header, &status);
1316   }
1317 
1318   // Redshift of the source.
1319   if (!undefined(wcs->zsource)) {
1320     wcsutil_double2str(keyvalue, format, wcs->zsource);
1321     wcshdo_util(ctrl, "ZSOURCE", "ZSOU", 0, 0x0, 0, 0, 0, alt, colnum, colax,
1322       keyvalue, "Redshift of the source", nkeyrec, header, &status);
1323   }
1324 
1325   // Reference frame of source redshift.
1326   if (wcs->ssyssrc[0]) {
1327     sprintf(keyvalue, "'%s'", wcs->ssyssrc);
1328     wcshdo_util(ctrl, "SSYSSRC", "SSRC", 0, 0x0, 0, 0, 0, alt, colnum, colax,
1329       keyvalue, "Reference frame of source redshift", nkeyrec, header,
1330       &status);
1331   }
1332 
1333   // Velocity orientation angle.
1334   if (!undefined(wcs->velangl)) {
1335     wcsutil_double2str(keyvalue, format, wcs->velangl);
1336     wcshdo_util(ctrl, "VELANGL", "VANG", 0, 0x0, 0, 0, 0, alt, colnum, colax,
1337       keyvalue, "[deg] Velocity orientation angle", nkeyrec, header, &status);
1338   }
1339 
1340   // - - - - - - - - - - - - - - - - - - - -  Additional auxiliary parameters.
1341 
1342   if ((aux = wcs->aux)) {
1343     if (!undefined(aux->rsun_ref)) {
1344       wcsutil_double2str(keyvalue, format, aux->rsun_ref);
1345       wcshdo_util(ctrl, "RSUN_REF", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0,
1346         keyvalue, "[m] Solar radius", nkeyrec, header, &status);
1347     }
1348 
1349     if (!undefined(aux->dsun_obs)) {
1350       wcsutil_double2str(keyvalue, format, aux->dsun_obs);
1351       wcshdo_util(ctrl, "DSUN_OBS", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0,
1352         keyvalue, "[m] Distance from centre of Sun to observer", nkeyrec,
1353         header, &status);
1354     }
1355 
1356     if (!undefined(aux->crln_obs)) {
1357       wcsutil_double2str(keyvalue, format, aux->crln_obs);
1358       wcshdo_util(ctrl, "CRLN_OBS", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0,
1359         keyvalue, "[deg] Carrington heliographic lng of observer", nkeyrec,
1360         header, &status);
1361 
1362       if (!undefined(aux->hglt_obs)) {
1363         wcsutil_double2str(keyvalue, format, aux->hglt_obs);
1364         wcshdo_util(ctrl, "CRLT_OBS", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0,
1365           keyvalue, "[deg] Heliographic latitude of observer", nkeyrec,
1366           header, &status);
1367       }
1368     }
1369 
1370     if (!undefined(aux->hgln_obs)) {
1371       wcsutil_double2str(keyvalue, format, aux->hgln_obs);
1372       wcshdo_util(ctrl, "HGLN_OBS", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0,
1373         keyvalue, "[deg] Stonyhurst heliographic lng of observer", nkeyrec,
1374         header, &status);
1375 
1376       if (!undefined(aux->hglt_obs)) {
1377         wcsutil_double2str(keyvalue, format, aux->hglt_obs);
1378         wcshdo_util(ctrl, "HGLT_OBS", 0x0, 0, 0x0, 0, 0, 0, ' ', 0, 0x0,
1379           keyvalue, "[deg] Heliographic latitude of observer", nkeyrec,
1380           header, &status);
1381       }
1382     }
1383   }
1384 
1385   // - - - - - - - - - - - - - - - - - - - - - Distortion function parameters.
1386 
1387   if (dosip) {
1388     // Simple Imaging Polynomial (SIP) is handled by translating its dpkey
1389     // records.  Determine a suitable numerical precision for the
1390     // polynomial coefficients to avoid trailing zeroes common to all of
1391     // them.
1392     dis = wcs->lin.dispre;
1393     if (dofmt) {
1394       keyp = dis->dp;
1395       kp0  = 2;
1396       for (idp = 0; idp < dis->ndp; idp++, keyp++) {
1397         cp = strchr(keyp->field, '.') + 1;
1398         if (strncmp(cp, "SIP.", 4) != 0) continue;
1399         wcsutil_double2str(keyvalue, "%20.13E", dpkeyd(keyp));
1400 
1401         kpi = 15;
1402         while (kp0 < kpi && keyvalue[kpi] == '0') kpi--;
1403         kp0 = kpi;
1404       }
1405 
1406       precision = kp0 - 2;
1407       if (precision < 1)  precision = 1;
1408       if (13 < precision) precision = 13;
1409       sprintf(format, "%%20.%dE", precision);
1410     }
1411 
1412     // Ensure the coefficients are written in a human-readable sequence.
1413     for (j = 0; j <= 1; j++) {
1414       // Distortion function polynomial coefficients.
1415       wcshdo_util(ctrl, "", "", 0, 0x0, 0, 0, 0, ' ', 0, 0, "", "",
1416         nkeyrec, header, &status);
1417 
1418       if (j == 0) {
1419         strcpy(keyword, "A_");
1420       } else {
1421         strcpy(keyword, "B_");
1422       }
1423 
1424       ncoeff = dis->iparm[j][I_TPDNCO];
1425       for (degree = 0; degree <= 9; degree++) {
1426         if (ncoeff <= nTPD[degree]) break;
1427       }
1428 
1429       strcpy(keyword+2, "ORDER");
1430       sprintf(keyvalue, "%20d", degree);
1431       sprintf(comment, "SIP polynomial degree, axis %d, pixel-to-sky", j+1);
1432       wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, ' ', 0, 0,
1433         keyvalue, comment, nkeyrec, header, &status);
1434 
1435       keyp = dis->dp;
1436       for (idp = 0; idp < dis->ndp; idp++, keyp++) {
1437         if (keyp->j != j+1) continue;
1438         if ((keyval = dpkeyd(keyp)) == 0.0) continue;
1439 
1440         cp = strchr(keyp->field, '.') + 1;
1441         if (strncmp(cp, "SIP.FWD.", 8) != 0) continue;
1442         cp += 8;
1443         strcpy(keyword+2, cp);
1444         sscanf(cp, "%d_%d", &p, &q);
1445         strncpy(term, "xxxxxxxxx", p);
1446         strncpy(term+p, "yyyyyyyyy", q);
1447         term[p+q] = '\0';
1448 
1449         wcsutil_double2str(keyvalue, format, keyval);
1450         sprintf(comment, "SIP distortion coefficient: %s", term);
1451         wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, ' ', 0, 0,
1452           keyvalue, comment, nkeyrec, header, &status);
1453       }
1454 
1455       if (dis->maxdis[j] != 0.0) {
1456         strcpy(keyword+2, "DMAX");
1457         wcsutil_double2str(keyvalue, "%20.3f", dis->maxdis[j]);
1458         wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, ' ', 0, 0,
1459           keyvalue, "Maximum value of distortion function", nkeyrec,
1460           header, &status);
1461       }
1462 
1463       // Inverse distortion function polynomial coefficients.
1464       if (dis->disx2p == 0x0) continue;
1465 
1466       wcshdo_util(ctrl, "", "", 0, 0x0, 0, 0, 0, ' ', 0, 0, "", "",
1467         nkeyrec, header, &status);
1468 
1469       if (j == 0) {
1470         strcpy(keyword, "AP_");
1471       } else {
1472         strcpy(keyword, "BP_");
1473       }
1474 
1475       ncoeff = dis->iparm[j][I_NDPARM] - dis->iparm[j][I_TPDNCO];
1476       for (degree = 0; degree <= 9; degree++) {
1477         if (ncoeff <= nTPD[degree]) break;
1478       }
1479 
1480       strcpy(keyword+3, "ORDER");
1481       sprintf(keyvalue, "%20d", degree);
1482       sprintf(comment, "SIP polynomial degree, axis %d, sky-to-pixel", j+1);
1483       wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, ' ', 0, 0,
1484         keyvalue, comment, nkeyrec, header, &status);
1485 
1486       keyp = dis->dp;
1487       for (idp = 0; idp < dis->ndp; idp++, keyp++) {
1488         if (keyp->j != j+1) continue;
1489         if ((keyval = dpkeyd(keyp)) == 0.0) continue;
1490 
1491         cp = strchr(keyp->field, '.') + 1;
1492         if (strncmp(cp, "SIP.REV.", 8) != 0) continue;
1493         cp += 8;
1494         strcpy(keyword+3, cp);
1495         sscanf(cp, "%d_%d", &p, &q);
1496         strncpy(term, "xxxxxxxxx", p);
1497         strncpy(term+p, "yyyyyyyyy", q);
1498         term[p+q] = '\0';
1499 
1500         wcsutil_double2str(keyvalue, format, keyval);
1501         sprintf(comment, "SIP inverse coefficient: %s", term);
1502         wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, ' ', 0, 0,
1503           keyvalue, comment, nkeyrec, header, &status);
1504       }
1505     }
1506   }
1507 
1508   for (idis = 0; idis < 2; idis++) {
1509     if (idis == 0 && (dis = wcs->lin.dispre) == 0x0) continue;
1510     if (idis == 1 && (dis = wcs->lin.disseq) == 0x0) continue;
1511 
1512     for (j = 0; j < naxis; j++) {
1513       if (dis->disp2x[j] == 0x0) continue;
1514 
1515       iparm = dis->iparm[j];
1516       dparm = dis->dparm[j];
1517 
1518       // Identify the distortion type.
1519       if (dotpv) {
1520         // TPV "projection" is handled by translating its dpkey records,
1521         // which were originally translated from PVi_ma by wcsset(), or
1522         // possibly input directly as a CQDISia = 'TPV' distortion type.
1523         // Determine a suitable numerical precision for the polynomial
1524         // coefficients to avoid trailing zeroes common to all of them.
1525         if (dofmt) wcshdo_format('E', iparm[I_NDPARM], dparm, format);
1526         sprintf(fmt01, "%.3ss", format);
1527 
1528         wcshdo_util(ctrl, "", "", 0, 0x0, 0, 0, 0, ' ', 0, 0, "", "",
1529           nkeyrec, header, &status);
1530 
1531         // Distortion function polynomial coefficients.
1532         sprintf(keyword, "PV%d_", j+1);
1533         kp = keyword + strlen(keyword);
1534 
1535         keyp = dis->dp;
1536         for (idp = 0; idp < dis->ndp; idp++, keyp++) {
1537           if (keyp->j != j+1) continue;
1538           if ((keyval = dpkeyd(keyp)) == 0.0) continue;
1539 
1540           cp = strchr(keyp->field, '.') + 1;
1541           if (strncmp(cp, "TPV.", 4) != 0) continue;
1542           strcpy(kp, cp+4);
1543 
1544           // Identify the term of the TPV polynomial for human readers.
1545           sscanf(cp+4, "%d", &m);
1546           wcshdo_tpdterm(m, j == wcs->lng, term);
1547           sprintf(comment, "TPV coefficient: %s", term);
1548 
1549           if (keyval == 1.0) {
1550             sprintf(keyvalue, fmt01, "1.0");
1551           } else {
1552             wcsutil_double2str(keyvalue, format, keyval);
1553           }
1554           wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1555             keyvalue, comment, nkeyrec, header, &status);
1556         }
1557 
1558       } else if (strcmp(dis->dtype[j], "TPD") == 0 || dotpd ||
1559                  strcmp(dis->dtype[j], "Polynomial")  == 0 ||
1560                  strcmp(dis->dtype[j], "Polynomial*") == 0) {
1561         // One of the Paper IV type polynomial distortions.
1562         wcshdo_util(ctrl, "", "", 0, 0x0, 0, 0, 0, ' ', 0, 0, "", "",
1563           nkeyrec, header, &status);
1564 
1565         if (strcmp(dis->dtype[j], "TPD") == 0) {
1566           // Pure TPD.
1567           dotpd = 1;
1568         } else if (strncmp(dis->dtype[j], "Polynomial", 10) == 0) {
1569           // Polynomial distortion.  Write it as TPD by request?
1570           dotpd = (iparm[I_DTYPE] & DIS_DOTPD);
1571           strcpy(tpdsrc, "Polynomial distortion");
1572         }
1573 
1574         pq = idis ? 'Q' : 'P';
1575         Nhat = dis->Nhat[j];
1576 
1577         // CPDISja/CQDISia
1578         sprintf(keyword, "C%cDIS%d", pq, j+1);
1579         if (idis == 0) {
1580           strcpy(comment, "P = prior, ");
1581         } else {
1582           strcpy(comment, "Q = sequent, ");
1583         }
1584 
1585         if (dotpd) {
1586           strcpy(keyvalue, "'TPD'");
1587           strcat(comment, "Template Polynomial Distortion");
1588 
1589           // For identifying terms of the TPD polynomial.
1590           axmap  = dis->axmap[j];
1591           direct = 1;
1592           doaux  = iparm[I_TPDAUX];
1593           if (Nhat == 2) {
1594             // Associate x with longitude, y with latitude.
1595             if (axmap[0] == wcs->lng && axmap[1] == wcs->lat) {
1596               direct = 1;
1597             } else if (axmap[0] == wcs->lat && axmap[1] == wcs->lng) {
1598               direct = 0;
1599             } else {
1600               // Non-celestial.
1601               direct = (axmap[0] < axmap[1]);
1602             }
1603           }
1604         } else {
1605           strcpy(keyvalue, "'Polynomial'");
1606           strcat(comment, "general Polynomial distortion");
1607         }
1608 
1609         wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1610           keyvalue, comment, nkeyrec, header, &status);
1611 
1612         // NAXES.
1613         sprintf(keyword,  "D%c%d", pq, j+1);
1614         sprintf(keyvalue, "'NAXES:  %d'", Nhat);
1615         if (Nhat == 1) {
1616           strcpy(comment,  "One independent variable");
1617         } else if (Nhat == 2) {
1618           strcpy(comment,  "Two independent variables");
1619         } else {
1620           strcpy(comment,  "Number of independent variables");
1621         }
1622         wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1623           keyvalue, comment, nkeyrec, header, &status);
1624 
1625         // AXIS.jhat
1626         for (jhat = 0; jhat < Nhat; jhat++) {
1627           axmap = dis->axmap[j];
1628           sprintf(keyvalue, "'AXIS.%d: %d'", jhat+1, axmap[jhat]+1);
1629           if (jhat == 0) {
1630             strcpy(comment, "1st");
1631           } else if (jhat == 1) {
1632             strcpy(comment, "2nd");
1633           } else if (jhat == 2) {
1634             strcpy(comment, "3rd");
1635           } else {
1636             sprintf(comment, "%dth", jhat+1);
1637           }
1638 
1639           sprintf(comment+strlen(comment), " independent variable: axis %d",
1640             axmap[jhat]+1);
1641           if (dotpd) {
1642             // axid is "xyxuvu".
1643             cp = axid;
1644             if (!direct) cp++;
1645             if (doaux) cp += 3;
1646             sprintf(comment+strlen(comment), " (= %c)", cp[jhat]);
1647           }
1648 
1649           wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1650             keyvalue, comment, nkeyrec, header, &status);
1651         }
1652 
1653         // OFFSET.jhat
1654         if (dofmt) wcshdo_format('f', Nhat, dis->offset[j], format);
1655         for (jhat = 0; jhat < Nhat; jhat++) {
1656           if (dis->offset[j][jhat] == 0.0) continue;
1657 
1658           wcsutil_double2str(ctemp, format, dis->offset[j][jhat]);
1659           sprintf(keyvalue, "'OFFSET.%d: %s'", jhat+1, ctemp);
1660           sprintf(comment, "Variable %d renormalization offset", jhat+1);
1661 
1662           wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1663             keyvalue, comment, nkeyrec, header, &status);
1664         }
1665 
1666         // SCALE.jhat
1667         if (dofmt) wcshdo_format('f', Nhat, dis->scale[j], format);
1668         for (jhat = 0; jhat < Nhat; jhat++) {
1669           if (dis->scale[j][jhat] == 1.0) continue;
1670 
1671           wcsutil_double2str(ctemp, format, dis->scale[j][jhat]);
1672           sprintf(keyvalue, "'SCALE.%d: %s'", jhat+1, ctemp);
1673           sprintf(comment, "Variable %d renormalization scale", jhat+1);
1674 
1675           wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1676             keyvalue, comment, nkeyrec, header, &status);
1677         }
1678 
1679         // Does the distortion function compute a correction?
1680         if (dis->docorr[j]) {
1681           wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1682             "'DOCORR: 1'", "Distortion function computes a correction",
1683             nkeyrec, header, &status);
1684         } else {
1685           wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1686             "'DOCORR: 0'", "Distortion function computes coordinates",
1687             nkeyrec, header, &status);
1688         }
1689 
1690         if (dotpd) {
1691           // Template Polynomial Distortion (TPD).  As it may have been
1692           // translated from SIP, TPV, DSS, TNX, ZPX, or perhaps
1693           // Polynomial, the dpkey records may not relate to TPD.
1694           // Output is therefore handled via dparm.
1695           if (dofmt) wcshdo_format('E', iparm[I_NDPARM], dparm, format);
1696           sprintf(fmt01, "%.3ss", format);
1697 
1698           // AUX.jhat.COEFF.m
1699           if (doaux) {
1700             for (idp = 0; idp < 6; idp++) {
1701               if (dparm[idp] == 0.0) {
1702                 sprintf(ctemp, fmt01, "0.0");
1703               } else if (dparm[idp] == 1.0) {
1704                 sprintf(ctemp, fmt01, "1.0");
1705               } else {
1706                 wcsutil_double2str(ctemp, format, dparm[idp]);
1707               }
1708 
1709               if (idp < 3) {
1710                 sprintf(keyvalue, "'AUX.1.COEFF.%d: %s'", idp%3, ctemp);
1711                 sprintf(comment, "TPD: x = c0 + c1*u + c2*v");
1712               } else {
1713                 sprintf(keyvalue, "'AUX.2.COEFF.%d: %s'", idp%3, ctemp);
1714                 sprintf(comment, "TPD: y = d0 + d1*u + d2*v");
1715               }
1716 
1717               wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1718                 keyvalue, comment, nkeyrec, header, &status);
1719 
1720             }
1721 
1722             dparm += 6;
1723           }
1724 
1725           // TPD.FWD.m
1726           for (idp = 0; idp < iparm[I_TPDNCO]; idp++) {
1727             if (dparm[idp] == 0.0) continue;
1728 
1729             if (dparm[idp] == 1.0) {
1730               sprintf(ctemp, fmt01, "1.0");
1731             } else {
1732               wcsutil_double2str(ctemp, format, dparm[idp]);
1733             }
1734 
1735             m = idp;
1736             sprintf(keyvalue, "'TPD.FWD.%d:%s %s'", m, (m<10)?" ":"", ctemp);
1737             wcshdo_tpdterm(m, direct, term);
1738             sprintf(comment, "TPD coefficient: %s", term);
1739 
1740             wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1741               keyvalue, comment, nkeyrec, header, &status);
1742           }
1743 
1744           // CPERRja/CQERRia
1745           if (dis->maxdis[j] != 0.0) {
1746             sprintf(keyword,  "C%cERR%d", pq, j+1);
1747             sprintf(keyvalue, "%20.2f", dis->maxdis[j]);
1748             sprintf(comment, "%sMaximum absolute value of distortion",
1749               idis?"":"[pix] ");
1750             wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1751               keyvalue, comment, nkeyrec, header, &status);
1752           }
1753 
1754           // Inverse distortion function polynomial coefficients.
1755           if (dis->disx2p[j] == 0x0) continue;
1756 
1757           wcshdo_util(ctrl, "", "", 0, 0x0, 0, 0, 0, ' ', 0, 0, "", "",
1758             nkeyrec, header, &status);
1759 
1760           // TPD.REV.m
1761           sprintf(keyword,  "D%c%d", pq, j+1);
1762           for (idp = iparm[I_TPDNCO]; idp < iparm[I_NDPARM]; idp++) {
1763             if (dparm[idp] == 0.0) continue;
1764 
1765             wcsutil_double2str(ctemp, format, dparm[idp]);
1766             m = idp - iparm[I_TPDNCO];
1767             sprintf(keyvalue, "'TPD.REV.%d:%s %s'", m, (m<10)?" ":"", ctemp);
1768             wcshdo_tpdterm(m, direct, term);
1769             sprintf(comment, "TPD coefficient: %s", term);
1770 
1771             wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1772               keyvalue, comment, nkeyrec, header, &status);
1773           }
1774 
1775         } else {
1776           // General polynomial distortion, handled via its dpkey records
1777           // since iparm and dparm may hold a translation to TPD.
1778 
1779           // Do auxiliary variables first.
1780           keyp = dis->dp;
1781           for (idp = 0; idp < dis->ndp; idp++, keyp++) {
1782             if (keyp->j != j+1) continue;
1783 
1784             cp = strchr(keyp->field, '.') + 1;
1785             if (strncmp(cp, "NAUX", 4) != 0) continue;
1786 
1787             sprintf(keyvalue, "'%s: %d'", cp, dpkeyi(keyp));
1788             wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1789               keyvalue, "Number of auxiliary variables", nkeyrec, header,
1790               &status);
1791 
1792             keyp = dis->dp;
1793             for (idp = 0; idp < dis->ndp; idp++, keyp++) {
1794               if (keyp->j != j+1) continue;
1795 
1796               keyval = dpkeyd(keyp);
1797 
1798               cp = strchr(keyp->field, '.') + 1;
1799               if (strncmp(cp, "AUX.", 4) != 0) continue;
1800 
1801               sscanf(cp+4, "%d", &m);
1802               sprintf(keyvalue, "'%s:", cp);
1803 
1804               cp = strchr(cp+4, '.') + 1;
1805               kp = keyvalue + strlen(keyvalue);
1806 
1807               if ((double)((int)keyval) == keyval) {
1808                 sprintf(kp, "%4d'", (int)keyval);
1809               } else if (keyval == 0.5) {
1810                 strcat(kp, " 0.5'");
1811               } else {
1812                 wcsutil_double2str(kp, "%21.13E", keyval);
1813                 strcat(keyvalue, "'");
1814               }
1815 
1816               sscanf(cp+6, "%d", &p);
1817               if (strncmp(cp, "POWER.", 4) == 0) {
1818                 if (p) {
1819                   sprintf(comment, "Aux %d: var %d power", m, p);
1820                 } else {
1821                   sprintf(comment, "Aux %d: power of sum of terms", m);
1822                 }
1823               } else {
1824                 if (p) {
1825                   sprintf(comment, "Aux %d: var %d coefficient", m, p);
1826                 } else {
1827                   sprintf(comment, "Aux %d: offset term", m);
1828                 }
1829               }
1830 
1831               wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1832                 keyvalue, comment, nkeyrec, header, &status);
1833             }
1834 
1835             break;
1836           }
1837 
1838           // Do polynomial terms.
1839           keyp = dis->dp;
1840           for (idp = 0; idp < dis->ndp; idp++, keyp++) {
1841             if (keyp->j != j+1) continue;
1842 
1843             cp = strchr(keyp->field, '.') + 1;
1844             if (strncmp(cp, "NTERMS", 6) != 0) continue;
1845 
1846             sprintf(keyvalue, "'%s: %d'", cp, dpkeyi(keyp));
1847             wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1848               keyvalue, "Number of terms in the polynomial", nkeyrec, header,
1849               &status);
1850           }
1851 
1852           keyp = dis->dp;
1853           for (idp = 0; idp < dis->ndp; idp++, keyp++) {
1854             if (keyp->j != j+1) continue;
1855 
1856             if ((keyval = dpkeyd(keyp)) == 0.0) continue;
1857 
1858             cp = strchr(keyp->field, '.') + 1;
1859             if (strncmp(cp, "TERM.", 5) != 0) continue;
1860 
1861             sscanf(cp+5, "%d", &m);
1862             sprintf(keyvalue, "'%s:%s ", cp, (m<10)?" ":"");
1863 
1864             cp = strchr(cp+5, '.') + 1;
1865             kp = keyvalue + strlen(keyvalue);
1866             if (strncmp(cp, "VAR.", 4) == 0) {
1867               if ((double)((int)keyval) == keyval) {
1868                 sprintf(kp, "%20d", (int)keyval);
1869               } else {
1870                 wcsutil_double2str(kp, "%20.13f", keyval);
1871               }
1872 
1873               sscanf(cp+4, "%d", &p);
1874               if (p <= Nhat) {
1875                 sprintf(comment, "Poly term %d: var %d power", m, p);
1876               } else {
1877                 sprintf(comment, "Poly term %d: aux %d power", m, p-Nhat);
1878               }
1879 
1880             } else {
1881               wcsutil_double2str(kp, "%20.13E", keyval);
1882               sprintf(comment, "Poly term %d: coefficient", m);
1883             }
1884             strcat(keyvalue, "'");
1885 
1886             wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1887               keyvalue, comment, nkeyrec, header, &status);
1888           }
1889 
1890           // CPERRja/CQERRia
1891           if (dis->maxdis[j] != 0.0) {
1892             sprintf(keyword,  "C%cERR%d", pq, j+1);
1893             sprintf(keyvalue, "%20.2f", dis->maxdis[j]);
1894             sprintf(comment, "%sMaximum absolute value of distortion",
1895               idis?"":"[pix] ");
1896             wcshdo_util(ctrl, keyword, "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1897               keyvalue, comment, nkeyrec, header, &status);
1898           }
1899         }
1900       }
1901     }
1902 
1903     // DVERRa
1904     if (dis->totdis != 0.0) {
1905       sprintf(keyvalue, "%20.2f", dis->totdis);
1906       sprintf(comment, "Maximum combined distortion");
1907       wcshdo_util(ctrl, "DVERR", "", 0, 0x0, 0, 0, 0, alt, 0, 0,
1908         keyvalue, comment, nkeyrec, header, &status);
1909     }
1910   }
1911 
1912 
1913   // Add identification.
1914   wcshdo_util(ctrl, "", "", 0, 0x0, 0, 0, 0, ' ', 0, 0, "", "",
1915     nkeyrec, header, &status);
1916 
1917   if (dotpd == DIS_DOTPD) {
1918     // TPD by translation.
1919     sprintf(comment, "Translated from %s to TPD by WCSLIB %s", tpdsrc,
1920       wcslib_version(0x0));
1921   } else {
1922     sprintf(comment, "WCS header keyrecords produced by WCSLIB %s",
1923       wcslib_version(0x0));
1924   }
1925 
1926   wcshdo_util(ctrl, "COMMENT", "", 0, 0x0, 0, 0, 0, ' ', 0, 0,
1927     "", comment, nkeyrec, header, &status);
1928 
1929 
1930   if (status == WCSHDRERR_MEMORY) {
1931     wcserr_set(WCSHDR_ERRMSG(status));
1932   }
1933   return status;
1934 }
1935 
1936 //----------------------------------------------------------------------------
1937 // Determine a suitable floating point format for a set of parameters.
1938 
wcshdo_format(int fmt,int nval,const double val[],char * format)1939 void wcshdo_format(
1940   int fmt,
1941   int nval,
1942   const double val[],
1943   char *format)
1944 
1945 {
1946   int emax = -999;
1947   int emin = +999;
1948   int precision = 0;
1949   for (int i = 0; i < nval; i++) {
1950     // Double precision has at least 15 significant digits, and up to 17:
1951     // http://en.wikipedia.org/wiki/Double-precision_floating-point_format
1952     char cval[24];
1953     wcsutil_double2str(cval, "%21.14E", val[i]);
1954 
1955     int cpi = 16;
1956     while (2 < cpi && cval[cpi] == '0') cpi--;
1957 
1958     // Precision for 'E' format.
1959     cpi -= 2;
1960     if (precision < cpi) precision = cpi;
1961 
1962     // Range of significant digits for 'f' format.
1963     int expon;
1964     sscanf(cval+18, "%d", &expon);
1965 
1966     if (emax < expon) emax = expon;
1967     expon -= cpi;
1968     if (expon < emin) emin = expon;
1969   }
1970 
1971 
1972   if (fmt == 'G') {
1973     // Because e.g. writing 1e4 as 10000 requires an extra digit.
1974     emax++;
1975 
1976     if (emin < -15 || 15 < emax || 15 < (emax - emin)) {
1977       fmt = 'E';
1978     } else {
1979       fmt = 'f';
1980     }
1981   }
1982 
1983   if (fmt == 'f') {
1984     precision = -emin;
1985     if (precision < 1)  precision = 1;
1986     if (17 < precision) precision = 17;
1987     sprintf(format, "%%20.%df", precision);
1988 
1989   } else {
1990     if (precision < 1)  precision = 1;
1991     if (14 < precision) precision = 14;
1992     if (precision < 14) {
1993       sprintf(format, "%%20.%dE", precision);
1994     } else {
1995       sprintf(format, "%%21.%dE", precision);
1996     }
1997   }
1998 }
1999 
2000 //----------------------------------------------------------------------------
2001 // Construct a string that identifies the term of a TPD or TPV polynomial.
2002 
wcshdo_tpdterm(int m,int direct,char * term)2003 void wcshdo_tpdterm(
2004   int m,
2005   int direct,
2006   char *term)
2007 
2008 {
2009   const int nTPD[] = {1, 4, 7, 12, 17, 24, 31, 40, 49, 60};
2010 
2011   int degree, k;
2012 
2013   for (degree = 0; degree <= 9; degree++) {
2014     if (m < nTPD[degree]) break;
2015   }
2016 
2017   if (degree == 0) {
2018     strcpy(term, "1");
2019 
2020   } else {
2021     k = degree - (m - nTPD[degree-1]);
2022 
2023     if (k < 0) {
2024       memcpy(term, "rrrrrrrrr", degree);
2025     } else if (direct) {
2026       memcpy(term, "xxxxxxxxx", k);
2027       memcpy(term+k, "yyyyyyyyy", degree-k);
2028     } else {
2029       memcpy(term, "yyyyyyyyy", k);
2030       memcpy(term+k, "xxxxxxxxx", degree-k);
2031     }
2032 
2033     term[degree] = '\0';
2034   }
2035 }
2036 
2037 //----------------------------------------------------------------------------
2038 // Construct a keyrecord from the components given.
2039 
wcshdo_util(int relax,const char pikey[],const char tbkey[],int level,const char tlkey[],int i,int j,int m,char alt,int btcol,int plcol[],char keyvalue[],const char keycomment[],int * nkeyrec,char ** header,int * status)2040 void wcshdo_util(
2041   int relax,
2042   const char pikey[],
2043   const char tbkey[],
2044   int level,
2045   const char tlkey[],
2046   int i,
2047   int j,
2048   int m,
2049   char alt,
2050   int  btcol,
2051   int  plcol[],
2052   char keyvalue[],
2053   const char keycomment[],
2054   int  *nkeyrec,
2055   char **header,
2056   int  *status)
2057 
2058 {
2059   char ch0, ch1, *hptr, keyword[32], *kptr;
2060   int  nbyte, nc = 47, nv;
2061 
2062   if (*status) return;
2063 
2064   // Reallocate memory in blocks of 2880 bytes.
2065   if ((*nkeyrec)%32 == 0) {
2066     nbyte = ((*nkeyrec)/32 + 1) * 2880;
2067     if (!(hptr = realloc(*header, nbyte))) {
2068       *status = WCSHDRERR_MEMORY;
2069       return;
2070     }
2071 
2072     *header = hptr;
2073   }
2074 
2075   // Construct the keyword.
2076   if (alt == ' ') alt = '\0';
2077   if (btcol) {
2078     // Binary table image array.
2079     if (i > 0 && j) {
2080       if (j > 0) {
2081         sprintf(keyword, "%d%d%s%d%c", i, j, tbkey, btcol, alt);
2082       } else {
2083         sprintf(keyword, "%d%s%d_%d%c", i, tbkey, btcol, m, alt);
2084       }
2085     } else if (i > 0) {
2086       sprintf(keyword, "%d%s%d%c", i, tbkey, btcol, alt);
2087     } else if (j > 0) {
2088       sprintf(keyword, "%d%s%d%c", j, tbkey, btcol, alt);
2089     } else {
2090       sprintf(keyword, "%s%d%c", tbkey, btcol, alt);
2091     }
2092 
2093     if ((strlen(keyword) < 8) && tlkey && (relax & level)) {
2094       // Use the long form.
2095       if (i > 0 && j) {
2096         if (j > 0) {
2097           sprintf(keyword, "%d%d%s%d%c", i, j, tlkey, btcol, alt);
2098         } else {
2099           sprintf(keyword, "%d%s%d_%d%c", i, tlkey, btcol, m, alt);
2100         }
2101       } else if (i > 0) {
2102         sprintf(keyword, "%d%s%d%c", i, tlkey, btcol, alt);
2103       } else if (j > 0) {
2104         sprintf(keyword, "%d%s%d%c", j, tlkey, btcol, alt);
2105       } else {
2106         sprintf(keyword, "%s%d%c", tlkey, btcol, alt);
2107       }
2108     }
2109 
2110   } else if (plcol && plcol[0]) {
2111     // Pixel list.
2112     if (i > 0 && j) {
2113       if (j > 0) {
2114         sprintf(keyword, "T%s%d_%d%c", tbkey, plcol[i-1], plcol[j-1], alt);
2115       } else {
2116         sprintf(keyword, "T%s%d_%d%c", tbkey, plcol[i-1], m, alt);
2117       }
2118     } else if (i > 0) {
2119       sprintf(keyword, "T%s%d%c", tbkey, plcol[i-1], alt);
2120     } else if (j > 0) {
2121       sprintf(keyword, "T%s%d%c", tbkey, plcol[j-1], alt);
2122     } else {
2123       sprintf(keyword, "%s%d%c", tbkey, plcol[0], alt);
2124     }
2125 
2126     if ((strlen(keyword) < 8) && tlkey && (relax & level)) {
2127       // Use the long form.
2128       if (i > 0 && j) {
2129         if (j > 0) {
2130           sprintf(keyword, "T%s%d_%d%c", tlkey, plcol[i-1], plcol[j-1], alt);
2131         } else {
2132           sprintf(keyword, "T%s%d_%d%c", tlkey, plcol[i-1], m, alt);
2133         }
2134       } else if (i > 0) {
2135         sprintf(keyword, "T%s%d%c", tlkey, plcol[i-1], alt);
2136       } else if (j > 0) {
2137         sprintf(keyword, "T%s%d%c", tlkey, plcol[j-1], alt);
2138       } else {
2139         sprintf(keyword, "%s%d%c", tlkey, plcol[0], alt);
2140       }
2141     }
2142   } else {
2143     if (i > 0 && j) {
2144       if (j > 0) {
2145         sprintf(keyword, "%s%d_%d%c", pikey, i, j, alt);
2146       } else {
2147         sprintf(keyword, "%s%d_%d%c", pikey, i, m, alt);
2148       }
2149     } else if (i > 0) {
2150       sprintf(keyword, "%s%d%c", pikey, i, alt);
2151     } else if (j > 0) {
2152       sprintf(keyword, "%s%d%c", pikey, j, alt);
2153     } else {
2154       sprintf(keyword, "%s%c", pikey, alt);
2155     }
2156   }
2157 
2158   // Double-up single-quotes in string keyvalues.
2159   if (*keyvalue == '\'') {
2160     hptr = keyvalue + 1;
2161     while (*hptr) {
2162       if (*hptr == '\'') {
2163         kptr = hptr++;
2164         if (*hptr) {
2165           ch0 = *kptr;
2166           while (*kptr) {
2167             ch1 = *(++kptr);
2168             *kptr = ch0;
2169             ch0 = ch1;
2170           }
2171         } else {
2172           break;
2173         }
2174       }
2175 
2176       hptr++;
2177     }
2178 
2179     // Check length.
2180     if (strlen(keyvalue) > 70) {
2181       // Truncate.
2182       keyvalue[69] = '\'';
2183       keyvalue[70] = '\0';
2184     }
2185 
2186   } else {
2187     // Check length.
2188     if (strlen(keyvalue) > 70) {
2189       // Truncate.
2190       keyvalue[70] = '\0';
2191     }
2192   }
2193 
2194   if ((nv = strlen(keyvalue) > 20)) {
2195     // Rob the keycomment to make space for the keyvalue.
2196     nc -= (nv - 20);
2197   }
2198 
2199   hptr = *header + (80 * ((*nkeyrec)++));
2200   if (*keyword == '\0') {
2201     sprintf(hptr, "%80.80s", " ");
2202   } else if (strcmp(keyword, "COMMENT") == 0) {
2203     sprintf(hptr, "%-8.8s %-71.71s", keyword, keycomment);
2204   } else {
2205     sprintf(hptr, "%-8.8s= %-20s / %-*.*s", keyword, keyvalue, nc, nc,
2206       keycomment);
2207   }
2208 }
2209