text: /*  This file, group.c, contains the grouping convention suport routines.  */
  
  /*  The FITSIO software was written by William Pence at the High Energy    */
  /*  Astrophysic Science Archive Research Center (HEASARC) at the NASA      */
  /*  Goddard Space Flight Center.                                           */
  /*                                                                         */
  /*  The group.c module of CFITSIO was written by Donald G. Jennings of     */
  /*  the INTEGRAL Science Data Centre (ISDC) under NASA contract task       */
  /*  66002J6. The above copyright laws apply. Copyright guidelines of The   */
  /*  University of Geneva might also apply.                                 */
  
  /*  The following routines are designed to create, read, and manipulate    */
  /*  FITS Grouping Tables as defined in the FITS Grouping Convention paper  */
  /*  by Jennings, Pence, Folk and Schlesinger. The development of the       */
  /*  grouping structure was partially funded under the NASA AISRP Program.  */
  
  #include "fitsio2.h"
  #include "group.h"
  #include <stdio.h>
  #include <string.h>
  #include <stdlib.h>
  
  #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__)
  #include <direct.h>   /* defines the getcwd function on Windows PCs */
  #else
  #include <unistd.h>  /* needed for getcwd prototype on unix machines */
  #endif
  
  #define HEX_ESCAPE '%'
  
  /*---------------------------------------------------------------------------
   Change record:
  
  D. Jennings, 18/06/98, version 1.0 of group module delivered to B. Pence for
                         integration into CFITSIO 2.005
  
  D. Jennings, 17/11/98, fixed bug in ffgtcpr(). Now use fits_find_nextkey()
                         correctly and insert auxiliary keyword records
                 directly before the TTYPE1 keyword in the copied
                 group table.
  
  D. Jennings, 22/01/99, ffgmop() now looks for relative file paths when
                         the MEMBER_LOCATION information is given in a
                 grouping table.
  
  D. Jennings, 01/02/99, ffgtop() now looks for relatve file paths when
                         the GRPLCn keyword value is supplied in the member
                 HDU header.
  
  D. Jennings, 01/02/99, ffgtam() now trys to construct relative file paths
                         from the member's file to the group table's file
                 (and visa versa) when both the member's file and
                 group table file are of access type FILE://.
  
  D. Jennings, 05/05/99, removed the ffgtcn() function; made obsolete by
                         fits_get_url().
  
  D. Jennings, 05/05/99, updated entire module to handle partial URLs and
                         absolute URLs more robustly. Host dependent directory
                 paths are now converted to true URLs before being
                 read from/written to grouping tables.
  
  D. Jennings, 05/05/99, added the following new functions (note, none of these
                         are directly callable by the application)
  
                 int fits_path2url()
                 int fits_url2path()
                 int fits_get_cwd()
                 int fits_get_url()
                 int fits_clean_url()
                 int fits_relurl2url()
                 int fits_encode_url()
                 int fits_unencode_url()
                 int fits_is_url_absolute()
  
  -----------------------------------------------------------------------------*/
  
  /*---------------------------------------------------------------------------*/
  int ffgtcr(fitsfile *fptr,      /* FITS file pointer                         */
         char    *grpname,    /* name of the grouping table                */
         int      grouptype,  /* code specifying the type of
                     grouping table information:
                     GT_ID_ALL_URI  0 ==> defualt (all columns)
                     GT_ID_REF      1 ==> ID by reference
                     GT_ID_POS      2 ==> ID by position
                     GT_ID_ALL      3 ==> ID by ref. and position
                     GT_ID_REF_URI 11 ==> (1) + URI info
                     GT_ID_POS_URI 12 ==> (2) + URI info       */
         int      *status    )/* return status code                        */
  
  /*
     create a grouping table at the end of the current FITS file. This
     function makes the last HDU in the file the CHDU, then calls the
     fits_insert_group() function to actually create the new grouping table.
  */
  
  {
    int hdutype;
    int hdunum;
  
  
    if(*status != 0) return(*status);
  
  
    *status = fits_get_num_hdus(fptr,&hdunum,status);
  
    /* If hdunum is 0 then we are at the beginning of the file and
       we actually haven't closed the first header yet, so don't do
       anything more */
  
    if (0 != hdunum) {
  
        *status = fits_movabs_hdu(fptr,hdunum,&hdutype,status);
    }
  
    /* Now, the whole point of the above two fits_ calls was to get to
       the end of file.  Let's ignore errors at this point and keep
       going since any error is likely to mean that we are already at the
       EOF, or the file is fatally corrupted.  If we are at the EOF then
       the next fits_ call will be ok.  If it's corrupted then the
       next call will fail, but that's not big deal at this point.
    */
  
    if (0 != *status ) *status = 0;
  
    *status = fits_insert_group(fptr,grpname,grouptype,status);
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int ffgtis(fitsfile *fptr,      /* FITS file pointer                         */
         char    *grpname,    /* name of the grouping table                */
         int      grouptype,  /* code specifying the type of
                     grouping table information:
                     GT_ID_ALL_URI  0 ==> defualt (all columns)
                     GT_ID_REF      1 ==> ID by reference
                     GT_ID_POS      2 ==> ID by position
                     GT_ID_ALL      3 ==> ID by ref. and position
                     GT_ID_REF_URI 11 ==> (1) + URI info
                     GT_ID_POS_URI 12 ==> (2) + URI info       */
         int      *status)     /* return status code                       */
  
  /*
     insert a grouping table just after the current HDU of the current FITS file.
     This is the same as fits_create_group() only it allows the user to select
     the place within the FITS file to add the grouping table.
  */
  
  {
  
    int tfields  = 0;
    int hdunum   = 0;
    int hdutype  = 0;
    int extver;
    int i;
  
    long pcount  = 0;
  
    char *ttype[6];
    char *tform[6];
  
    char ttypeBuff[102];
    char tformBuff[54];
  
    char  extname[] = "GROUPING";
    char  keyword[FLEN_KEYWORD];
    char  keyvalue[FLEN_VALUE];
    char  comment[FLEN_COMMENT];
  
    do
      {
  
        /* set up the ttype and tform character buffers */
  
        for(i = 0; i < 6; ++i)
      {
        ttype[i] = ttypeBuff+(i*17);
        tform[i] = tformBuff+(i*9);
      }
  
        /* define the columns required according to the grouptype parameter */
  
        *status = ffgtdc(grouptype,0,0,0,0,0,0,ttype,tform,&tfields,status);
  
        /* create the grouping table using the columns defined above */
  
        *status = fits_insert_btbl(fptr,0,tfields,ttype,tform,NULL,
                   NULL,pcount,status);
  
        if(*status != 0) continue;
  
        /*
       retrieve the hdu position of the new grouping table for
       future use
        */
  
        fits_get_hdu_num(fptr,&hdunum);
  
        /*
       add the EXTNAME and EXTVER keywords to the HDU just after the
       TFIELDS keyword; for now the EXTVER value is set to 0, it will be
       set to the correct value later on
        */
  
        fits_read_keyword(fptr,"TFIELDS",keyvalue,comment,status);
  
        fits_insert_key_str(fptr,"EXTNAME",extname,
                "HDU contains a Grouping Table",status);
        fits_insert_key_lng(fptr,"EXTVER",0,"Grouping Table vers. (this file)",
                status);
  
        /*
       if the grpname parameter value was defined (Non NULL and non zero
       length) then add the GRPNAME keyword and value
        */
  
        if(grpname != NULL && strlen(grpname) > 0)
      fits_insert_key_str(fptr,"GRPNAME",grpname,"Grouping Table name",
                  status);
  
        /*
       add the TNULL keywords and values for each integer column defined;
       integer null values are zero (0) for the MEMBER_POSITION and
       MEMBER_VERSION columns.
        */
  
        for(i = 0; i < tfields && *status == 0; ++i)
      {
        if(fits_strcasecmp(ttype[i],"MEMBER_POSITION") == 0 ||
           fits_strcasecmp(ttype[i],"MEMBER_VERSION")  == 0)
          {
            snprintf(keyword,FLEN_KEYWORD,"TFORM%d",i+1);
            *status = fits_read_key_str(fptr,keyword,keyvalue,comment,
                        status);
  
            snprintf(keyword,FLEN_KEYWORD,"TNULL%d",i+1);
  
            *status = fits_insert_key_lng(fptr,keyword,0,"Column Null Value",
                          status);
          }
      }
  
        /*
       determine the correct EXTVER value for the new grouping table
       by finding the highest numbered grouping table EXTVER value
       the currently exists
        */
  
        for(extver = 1;
        (fits_movnam_hdu(fptr,ANY_HDU,"GROUPING",extver,status)) == 0;
        ++extver);
  
        if(*status == BAD_HDU_NUM) *status = 0;
  
        /*
       move back to the new grouping table HDU and update the EXTVER
       keyword value
        */
  
        fits_movabs_hdu(fptr,hdunum,&hdutype,status);
  
        fits_modify_key_lng(fptr,"EXTVER",extver,"&",status);
  
      }while(0);
  
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int ffgtch(fitsfile *gfptr,     /* FITS pointer to group                     */
         int       grouptype, /* code specifying the type of
                     grouping table information:
                     GT_ID_ALL_URI  0 ==> defualt (all columns)
                     GT_ID_REF      1 ==> ID by reference
                     GT_ID_POS      2 ==> ID by position
                     GT_ID_ALL      3 ==> ID by ref. and position
                     GT_ID_REF_URI 11 ==> (1) + URI info
                     GT_ID_POS_URI 12 ==> (2) + URI info       */
         int      *status)     /* return status code                       */
  
  
  /*
     Change the grouping table structure of the grouping table pointed to by
     gfptr. The grouptype code specifies the new structure of the table. This
     operation only adds or removes grouping table columns, it does not add
     or delete group members (i.e., table rows). If the grouping table already
     has the desired structure then no operations are performed and function
     simply returns with a (0) success status code. If the requested structure
     change creates new grouping table columns, then the column values for all
     existing members will be filled with the appropriate null values.
  */
  
  {
    int xtensionCol, extnameCol, extverCol, positionCol, locationCol, uriCol;
    int ncols    = 0;
    int colnum   = 0;
    int nrows    = 0;
    int grptype  = 0;
    int i,j;
  
    long intNull  = 0;
    long tfields  = 0;
  
    char *tform[6];
    char *ttype[6];
  
    unsigned char  charNull[1] = {'\0'};
  
    char ttypeBuff[102];
    char tformBuff[54];
  
    char  keyword[FLEN_KEYWORD];
    char  keyvalue[FLEN_VALUE];
    char  comment[FLEN_COMMENT];
  
  
    if(*status != 0) return(*status);
  
    do
      {
        /* set up the ttype and tform character buffers */
  
        for(i = 0; i < 6; ++i)
      {
        ttype[i] = ttypeBuff+(i*17);
        tform[i] = tformBuff+(i*9);
      }
  
        /* retrieve positions of all Grouping table reserved columns */
  
        *status = ffgtgc(gfptr,&xtensionCol,&extnameCol,&extverCol,&positionCol,
                 &locationCol,&uriCol,&grptype,status);
  
        if(*status != 0) continue;
  
        /* determine the total number of grouping table columns */
  
        *status = fits_read_key_lng(gfptr,"TFIELDS",&tfields,comment,status);
  
        /* define grouping table columns to be added to the configuration */
  
        *status = ffgtdc(grouptype,xtensionCol,extnameCol,extverCol,positionCol,
                 locationCol,uriCol,ttype,tform,&ncols,status);
  
        /*
      delete any grouping tables columns that exist but do not belong to
      new desired configuration; note that we delete before creating new
      columns for (file size) efficiency reasons
        */
  
        switch(grouptype)
      {
  
      case GT_ID_ALL_URI:
  
        /* no columns to be deleted in this case */
  
        break;
  
      case GT_ID_REF:
  
        if(positionCol != 0)
          {
            *status = fits_delete_col(gfptr,positionCol,status);
            --tfields;
            if(uriCol      > positionCol)  --uriCol;
            if(locationCol > positionCol) --locationCol;
          }
        if(uriCol      != 0)
          {
          *status = fits_delete_col(gfptr,uriCol,status);
            --tfields;
            if(locationCol > uriCol) --locationCol;
          }
        if(locationCol != 0)
          *status = fits_delete_col(gfptr,locationCol,status);
  
        break;
  
      case  GT_ID_POS:
  
        if(xtensionCol != 0)
          {
            *status = fits_delete_col(gfptr,xtensionCol,status);
            --tfields;
            if(extnameCol  > xtensionCol)  --extnameCol;
            if(extverCol   > xtensionCol)  --extverCol;
            if(uriCol      > xtensionCol)  --uriCol;
            if(locationCol > xtensionCol)  --locationCol;
          }
        if(extnameCol  != 0)
          {
            *status = fits_delete_col(gfptr,extnameCol,status);
            --tfields;
            if(extverCol   > extnameCol)  --extverCol;
            if(uriCol      > extnameCol)  --uriCol;
            if(locationCol > extnameCol)  --locationCol;
          }
        if(extverCol   != 0)
          {
            *status = fits_delete_col(gfptr,extverCol,status);
            --tfields;
            if(uriCol      > extverCol)  --uriCol;
            if(locationCol > extverCol)  --locationCol;
          }
        if(uriCol      != 0)
          {
            *status = fits_delete_col(gfptr,uriCol,status);
            --tfields;
            if(locationCol > uriCol)  --locationCol;
          }
        if(locationCol != 0)
          {
            *status = fits_delete_col(gfptr,locationCol,status);
            --tfields;
          }
  
        break;
  
      case  GT_ID_ALL:
  
        if(uriCol      != 0)
          {
            *status = fits_delete_col(gfptr,uriCol,status);
            --tfields;
            if(locationCol > uriCol)  --locationCol;
          }
        if(locationCol != 0)
          {
            *status = fits_delete_col(gfptr,locationCol,status);
            --tfields;
          }
  
        break;
  
      case GT_ID_REF_URI:
  
        if(positionCol != 0)
          {
            *status = fits_delete_col(gfptr,positionCol,status);
            --tfields;
          }
  
        break;
  
      case  GT_ID_POS_URI:
  
        if(xtensionCol != 0)
          {
            *status = fits_delete_col(gfptr,xtensionCol,status);
            --tfields;
            if(extnameCol > xtensionCol)  --extnameCol;
            if(extverCol  > xtensionCol)  --extverCol;
          }
        if(extnameCol  != 0)
          {
            *status = fits_delete_col(gfptr,extnameCol,status);
            --tfields;
            if(extverCol > extnameCol)  --extverCol;
          }
        if(extverCol   != 0)
          {
            *status = fits_delete_col(gfptr,extverCol,status);
            --tfields;
          }
  
        break;
  
      default:
  
        *status = BAD_OPTION;
        ffpmsg("Invalid value for grouptype parameter specified (ffgtch)");
        break;
  
      }
  
        /*
      add all the new grouping table columns that were not there
      previously but are called for by the grouptype parameter
        */
  
        for(i = 0; i < ncols && *status == 0; ++i)
      *status = fits_insert_col(gfptr,tfields+i+1,ttype[i],tform[i],status);
  
        /*
       add the TNULL keywords and values for each new integer column defined;
       integer null values are zero (0) for the MEMBER_POSITION and
       MEMBER_VERSION columns. Insert a null ("/0") into each new string
       column defined: MEMBER_XTENSION, MEMBER_NAME, MEMBER_URI_TYPE and
       MEMBER_LOCATION. Note that by convention a null string is the
       TNULL value for character fields so no TNULL is required.
        */
  
        for(i = 0; i < ncols && *status == 0; ++i)
      {
        if(fits_strcasecmp(ttype[i],"MEMBER_POSITION") == 0 ||
           fits_strcasecmp(ttype[i],"MEMBER_VERSION")  == 0)
          {
            /* col contains int data; set TNULL and insert 0 for each col */
  
            *status = fits_get_colnum(gfptr,CASESEN,ttype[i],&colnum,
                      status);
  
            snprintf(keyword,FLEN_KEYWORD,"TFORM%d",colnum);
  
            *status = fits_read_key_str(gfptr,keyword,keyvalue,comment,
                        status);
  
            snprintf(keyword,FLEN_KEYWORD,"TNULL%d",colnum);
  
            *status = fits_insert_key_lng(gfptr,keyword,0,
                          "Column Null Value",status);
  
            for(j = 1; j <= nrows && *status == 0; ++j)
          *status = fits_write_col_lng(gfptr,colnum,j,1,1,&intNull,
                           status);
          }
        else if(fits_strcasecmp(ttype[i],"MEMBER_XTENSION") == 0 ||
            fits_strcasecmp(ttype[i],"MEMBER_NAME")     == 0 ||
            fits_strcasecmp(ttype[i],"MEMBER_URI_TYPE") == 0 ||
            fits_strcasecmp(ttype[i],"MEMBER_LOCATION") == 0)
          {
  
            /* new col contains character data; insert NULLs into each col */
  
            *status = fits_get_colnum(gfptr,CASESEN,ttype[i],&colnum,
                      status);
  
            for(j = 1; j <= nrows && *status == 0; ++j)
          /* WILL THIS WORK FOR VAR LENTH CHAR COLS??????*/
          *status = fits_write_col_byt(gfptr,colnum,j,1,1,charNull,
                           status);
          }
      }
  
      }while(0);
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int ffgtrm(fitsfile *gfptr,  /* FITS file pointer to group                   */
         int       rmopt,  /* code specifying if member
                  elements are to be deleted:
                  OPT_RM_GPT ==> remove only group table
                  OPT_RM_ALL ==> recursively remove members
                  and their members (if groups)                */
         int      *status) /* return status code                           */
  
  /*
    remove a grouping table, and optionally all its members. Any groups
    containing the grouping table are updated, and all members (if not
    deleted) have their GRPIDn and GRPLCn keywords updated accordingly.
    If the (deleted) members are members of another grouping table then those
    tables are also updated. The CHDU of the FITS file pointed to by gfptr must
    be positioned to the grouping table to be deleted.
  */
  
  {
    int hdutype;
  
    long i;
    long nmembers = 0;
  
    HDUtracker HDU;
  
  
    if(*status != 0) return(*status);
  
    /*
       remove the grouping table depending upon the rmopt parameter
    */
  
    switch(rmopt)
      {
  
      case OPT_RM_GPT:
  
        /*
       for this option, the grouping table is deleted, but the member
       HDUs remain; in this case we only have to remove each member from
       the grouping table by calling fits_remove_member() with the
       OPT_RM_ENTRY option
        */
  
        /* get the number of members contained by this table */
  
        *status = fits_get_num_members(gfptr,&nmembers,status);
  
        /* loop over all grouping table members and remove them */
  
        for(i = nmembers; i > 0 && *status == 0; --i)
      *status = fits_remove_member(gfptr,i,OPT_RM_ENTRY,status);
  
      break;
  
      case OPT_RM_ALL:
  
        /*
      for this option the entire Group is deleted -- this includes all
      members and their members (if grouping tables themselves). Call
      the recursive form of this function to perform the removal.
        */
  
        /* add the current grouping table to the HDUtracker struct */
  
        HDU.nHDU = 0;
  
        *status = fftsad(gfptr,&HDU,NULL,NULL);
  
        /* call the recursive group remove function */
  
        *status = ffgtrmr(gfptr,&HDU,status);
  
        /* free the memory allocated to the HDUtracker struct */
  
        for(i = 0; i < HDU.nHDU; ++i)
      {
        free(HDU.filename[i]);
        free(HDU.newFilename[i]);
      }
  
        break;
  
      default:
  
        *status = BAD_OPTION;
        ffpmsg("Invalid value for the rmopt parameter specified (ffgtrm)");
        break;
  
       }
  
    /*
       if all went well then unlink and delete the grouping table HDU
    */
  
    *status = ffgmul(gfptr,0,status);
  
    *status = fits_delete_hdu(gfptr,&hdutype,status);
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int ffgtcp(fitsfile *infptr,  /* input FITS file pointer                     */
         fitsfile *outfptr, /* output FITS file pointer                    */
         int        cpopt,  /* code specifying copy options:
                  OPT_GCP_GPT (0) ==> copy only grouping table
                  OPT_GCP_ALL (2) ==> recusrively copy members
                                      and their members (if
                              groups)                  */
         int      *status)  /* return status code                          */
  
  /*
    copy a grouping table, and optionally all its members, to a new FITS file.
    If the cpopt is set to OPT_GCP_GPT (copy grouping table only) then the
    existing members have their GRPIDn and GRPLCn keywords updated to reflect
    the existance of the new group, since they now belong to another group. If
    cpopt is set to OPT_GCP_ALL (copy grouping table and members recursively)
    then the original members are not updated; the new grouping table is
    modified to include only the copied member HDUs and not the original members.
  
    Note that the recursive version of this function, ffgtcpr(), is called
    to perform the group table copy. In the case of cpopt == OPT_GCP_GPT
    ffgtcpr() does not actually use recursion.
  */
  
  {
    int i;
  
    HDUtracker HDU;
  
  
    if(*status != 0) return(*status);
  
    /* make sure infptr and outfptr are not the same pointer */
  
    if(infptr == outfptr) *status = IDENTICAL_POINTERS;
    else
      {
  
        /* initialize the HDUtracker struct */
  
        HDU.nHDU = 0;
  
        *status = fftsad(infptr,&HDU,NULL,NULL);
  
        /*
       call the recursive form of this function to copy the grouping table.
       If the cpopt is OPT_GCP_GPT then there is actually no recursion
       performed
        */
  
        *status = ffgtcpr(infptr,outfptr,cpopt,&HDU,status);
  
        /* free memory allocated for the HDUtracker struct */
  
        for(i = 0; i < HDU.nHDU; ++i)
      {
        free(HDU.filename[i]);
        free(HDU.newFilename[i]);
      }
      }
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int ffgtmg(fitsfile *infptr,  /* FITS file ptr to source grouping table      */
         fitsfile *outfptr, /* FITS file ptr to target grouping table      */
         int       mgopt,   /* code specifying merge options:
                   OPT_MRG_COPY (0) ==> copy members to target
                                        group, leaving source
                                group in place
                   OPT_MRG_MOV  (1) ==> move members to target
                                        group, source group is
                                deleted after merge    */
         int      *status)   /* return status code                         */
  
  
  /*
    merge two grouping tables by combining their members into a single table.
    The source grouping table must be the CHDU of the fitsfile pointed to by
    infptr, and the target grouping table must be the CHDU of the fitsfile to by
    outfptr. All members of the source grouping table shall be copied to the
    target grouping table. If the mgopt parameter is OPT_MRG_COPY then the source
    grouping table continues to exist after the merge. If the mgopt parameter
    is OPT_MRG_MOV then the source grouping table is deleted after the merge,
    and all member HDUs are updated accordingly.
  */
  {
    long i ;
    long nmembers = 0;
  
    fitsfile *tmpfptr = NULL;
  
  
    if(*status != 0) return(*status);
  
    do
      {
  
        *status = fits_get_num_members(infptr,&nmembers,status);
  
        for(i = 1; i <= nmembers && *status == 0; ++i)
      {
        *status = fits_open_member(infptr,i,&tmpfptr,status);
        *status = fits_add_group_member(outfptr,tmpfptr,0,status);
  
        if(*status == HDU_ALREADY_MEMBER) *status = 0;
  
        if(tmpfptr != NULL)
          {
            fits_close_file(tmpfptr,status);
            tmpfptr = NULL;
          }
      }
  
        if(*status != 0) continue;
  
        if(mgopt == OPT_MRG_MOV)
      *status = fits_remove_group(infptr,OPT_RM_GPT,status);
  
      }while(0);
  
    if(tmpfptr != NULL)
      {
        fits_close_file(tmpfptr,status);
      }
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int ffgtcm(fitsfile *gfptr,  /* FITS file pointer to grouping table          */
         int       cmopt,  /* code specifying compact options
                  OPT_CMT_MBR      (1) ==> compact only direct
                                               members (if groups)
                  OPT_CMT_MBR_DEL (11) ==> (1) + delete all
                                           compacted groups    */
         int      *status) /* return status code                           */
  
  /*
    "Compact" a group pointed to by the FITS file pointer gfptr. This
    is achieved by flattening the tree structure of a group and its
    (grouping table) members. All members HDUs of a grouping table which is
    itself a member of the grouping table gfptr are added to gfptr. Optionally,
    the grouping tables which are "compacted" are deleted. If the grouping
    table contains no members that are themselves grouping tables then this
    function performs a NOOP.
  */
  
  {
    long i;
    long nmembers = 0;
  
    char keyvalue[FLEN_VALUE];
    char comment[FLEN_COMMENT];
  
    fitsfile *mfptr = NULL;
  
  
    if(*status != 0) return(*status);
  
    do
      {
        if(cmopt != OPT_CMT_MBR && cmopt != OPT_CMT_MBR_DEL)
      {
        *status = BAD_OPTION;
        ffpmsg("Invalid value for cmopt parameter specified (ffgtcm)");
        continue;
      }
  
        /* reteive the number of grouping table members */
  
        *status = fits_get_num_members(gfptr,&nmembers,status);
  
        /*
      loop over all the grouping table members; if the member is a
      grouping table then merge its members with the parent grouping
      table
        */
  
        for(i = 1; i <= nmembers && *status == 0; ++i)
      {
        *status = fits_open_member(gfptr,i,&mfptr,status);
  
        if(*status != 0) continue;
  
        *status = fits_read_key_str(mfptr,"EXTNAME",keyvalue,comment,status);
  
        /* if no EXTNAME keyword then cannot be a grouping table */
  
        if(*status == KEY_NO_EXIST)
          {
            *status = 0;
            continue;
          }
        prepare_keyvalue(keyvalue);
  
        if(*status != 0) continue;
  
        /* if EXTNAME == "GROUPING" then process member as grouping table */
  
        if(fits_strcasecmp(keyvalue,"GROUPING") == 0)
          {
            /* merge the member (grouping table) into the grouping table */
  
            *status = fits_merge_groups(mfptr,gfptr,OPT_MRG_COPY,status);
  
            *status = fits_close_file(mfptr,status);
            mfptr = NULL;
  
            /*
           remove the member from the grouping table now that all of
           its members have been transferred; if cmopt is set to
           OPT_CMT_MBR_DEL then remove and delete the member
            */
  
            if(cmopt == OPT_CMT_MBR)
          *status = fits_remove_member(gfptr,i,OPT_RM_ENTRY,status);
            else
          *status = fits_remove_member(gfptr,i,OPT_RM_MBR,status);
          }
        else
          {
            /* not a grouping table; just close the opened member */
  
            *status = fits_close_file(mfptr,status);
            mfptr = NULL;
          }
      }
  
      }while(0);
  
    return(*status);
  }
  
  /*--------------------------------------------------------------------------*/
  int ffgtvf(fitsfile *gfptr,       /* FITS file pointer to group             */
         long     *firstfailed, /* Member ID (if positive) of first failed
                       member HDU verify check or GRPID index
                       (if negitive) of first failed group
                       link verify check.                     */
         int      *status)      /* return status code                     */
  
  /*
   check the integrity of a grouping table to make sure that all group members
   are accessible and all the links to other grouping tables are valid. The
   firstfailed parameter returns the member ID of the first member HDU to fail
   verification if positive or the first group link to fail if negative;
   otherwise firstfailed contains a return value of 0.
  */
  
  {
    long i;
    long nmembers = 0;
    long ngroups  = 0;
  
    char errstr[FLEN_VALUE];
  
    fitsfile *fptr = NULL;
  
  
    if(*status != 0) return(*status);
  
    *firstfailed = 0;
  
    do
      {
        /*
      attempt to open all the members of the grouping table. We stop
      at the first member which cannot be opened (which implies that it
      cannot be located)
        */
  
        *status = fits_get_num_members(gfptr,&nmembers,status);
  
        for(i = 1; i <= nmembers && *status == 0; ++i)
      {
        *status = fits_open_member(gfptr,i,&fptr,status);
        fits_close_file(fptr,status);
      }
  
        /*
      if the status is non-zero from the above loop then record the
      member index that caused the error
        */
  
        if(*status != 0)
      {
        *firstfailed = i;
        snprintf(errstr,FLEN_VALUE,"Group table verify failed for member %ld (ffgtvf)",
            i);
        ffpmsg(errstr);
        continue;
      }
  
        /*
      attempt to open all the groups linked to this grouping table. We stop
      at the first group which cannot be opened (which implies that it
      cannot be located)
        */
  
        *status = fits_get_num_groups(gfptr,&ngroups,status);
  
        for(i = 1; i <= ngroups && *status == 0; ++i)
      {
        *status = fits_open_group(gfptr,i,&fptr,status);
        fits_close_file(fptr,status);
      }
  
        /*
      if the status from the above loop is non-zero, then record the
      GRPIDn index of the group that caused the failure
        */
  
        if(*status != 0)
      {
        *firstfailed = -1*i;
        snprintf(errstr,FLEN_VALUE,
            "Group table verify failed for GRPID index %ld (ffgtvf)",i);
        ffpmsg(errstr);
        continue;
      }
  
      }while(0);
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int ffgtop(fitsfile *mfptr,  /* FITS file pointer to the member HDU          */
         int       grpid,  /* group ID (GRPIDn index) within member HDU    */
         fitsfile **gfptr, /* FITS file pointer to grouping table HDU      */
         int      *status) /* return status code                           */
  
  /*
    open the grouping table that contains the member HDU. The member HDU must
    be the CHDU of the FITS file pointed to by mfptr, and the grouping table
    is identified by the Nth index number of the GRPIDn keywords specified in
    the member HDU's header. The fitsfile gfptr pointer is positioned with the
    appropriate FITS file with the grouping table as the CHDU. If the group
    grouping table resides in a file other than the member then an attempt
    is first made to open the file readwrite, and failing that readonly.
  
    Note that it is possible for the GRPIDn/GRPLCn keywords in a member
    header to be non-continuous, e.g., GRPID1, GRPID2, GRPID5, GRPID6. In
    such cases, the grpid index value specified in the function call shall
    identify the (grpid)th GRPID value. In the above example, if grpid == 3,
    then the group specified by GRPID5 would be opened.
  */
  {
    int i;
    int found;
  
    long ngroups   = 0;
    long grpExtver = 0;
  
    char keyword[FLEN_KEYWORD];
    char keyvalue[FLEN_FILENAME];
    char *tkeyvalue;
    char location[FLEN_FILENAME];
    char location1[FLEN_FILENAME];
    char location2[FLEN_FILENAME];
    char comment[FLEN_COMMENT];
  
    char *url[2];
  
  
    if(*status != 0) return(*status);
  
    do
      {
        /* set the grouping table pointer to NULL for error checking later */
  
        *gfptr = NULL;
  
        /*
      make sure that the group ID requested is valid ==> cannot be
      larger than the number of GRPIDn keywords in the member HDU header
        */
  
        *status = fits_get_num_groups(mfptr,&ngroups,status);
  
        if(grpid > ngroups)
      {
        *status = BAD_GROUP_ID;
        snprintf(comment,FLEN_COMMENT,
            "GRPID index %d larger total GRPID keywords %ld (ffgtop)",
            grpid,ngroups);
        ffpmsg(comment);
        continue;
      }
  
        /*
      find the (grpid)th group that the member HDU belongs to and read
      the value of the GRPID(grpid) keyword; fits_get_num_groups()
      automatically re-enumerates the GRPIDn/GRPLCn keywords to fill in
      any gaps
        */
  
        snprintf(keyword,FLEN_KEYWORD,"GRPID%d",grpid);
  
        *status = fits_read_key_lng(mfptr,keyword,&grpExtver,comment,status);
  
        if(*status != 0) continue;
  
        /*
      if the value of the GRPIDn keyword is positive then the member is
      in the same FITS file as the grouping table and we only have to
      reopen the current FITS file. Else the member and grouping table
      HDUs reside in different files and another FITS file must be opened
      as specified by the corresponding GRPLCn keyword
  
      The DO WHILE loop only executes once and is used to control the
      file opening logic.
        */
  
        do
      {
        if(grpExtver > 0)
          {
            /*
          the member resides in the same file as the grouping
           table, so just reopen the grouping table file
            */
  
            *status = fits_reopen_file(mfptr,gfptr,status);
            continue;
          }
  
        else if(grpExtver == 0)
          {
            /* a GRPIDn value of zero (0) is undefined */
  
            *status = BAD_GROUP_ID;
            snprintf(comment,FLEN_COMMENT,"Invalid value of %ld for GRPID%d (ffgtop)",
                grpExtver,grpid);
            ffpmsg(comment);
            continue;
          }
  
        /*
           The GRPLCn keyword value is negative, which implies that
           the grouping table must reside in another FITS file;
           search for the corresponding GRPLCn keyword
        */
  
        /* set the grpExtver value positive */
  
        grpExtver = -1*grpExtver;
  
        /* read the GRPLCn keyword value */
  
        snprintf(keyword,FLEN_KEYWORD,"GRPLC%d",grpid);
        /* SPR 1738 */
        *status = fits_read_key_longstr(mfptr,keyword,&tkeyvalue,comment,
                        status);
        if (0 == *status) {
          strcpy(keyvalue,tkeyvalue);
          free(tkeyvalue);
        }
  
  
        /* if the GRPLCn keyword was not found then there is a problem */
  
        if(*status == KEY_NO_EXIST)
          {
            *status = BAD_GROUP_ID;
  
            snprintf(comment,FLEN_COMMENT,"Cannot find GRPLC%d keyword (ffgtop)",
                grpid);
            ffpmsg(comment);
  
            continue;
          }
  
        prepare_keyvalue(keyvalue);
  
        /*
          if the GRPLCn keyword value specifies an absolute URL then
          try to open the file; we cannot attempt any relative URL
          or host-dependent file path reconstruction
        */
  
        if(fits_is_url_absolute(keyvalue))
          {
            ffpmsg("Try to open group table file as absolute URL (ffgtop)");
  
            *status = fits_open_file(gfptr,keyvalue,READWRITE,status);
  
            /* if the open was successful then continue */
  
            if(*status == 0) continue;
  
            /* if READWRITE failed then try opening it READONLY */
  
            ffpmsg("OK, try open group table file as READONLY (ffgtop)");
  
            *status = 0;
            *status = fits_open_file(gfptr,keyvalue,READONLY,status);
  
            /* continue regardless of the outcome */
  
            continue;
          }
  
        /*
          see if the URL gives a file path that is absolute on the
          host machine
        */
  
        *status = fits_url2path(keyvalue,location1,status);
  
        *status = fits_open_file(gfptr,location1,READWRITE,status);
  
        /* if the file opened then continue */
  
        if(*status == 0) continue;
  
        /* if READWRITE failed then try opening it READONLY */
  
        ffpmsg("OK, try open group table file as READONLY (ffgtop)");
  
        *status = 0;
        *status = fits_open_file(gfptr,location1,READONLY,status);
  
        /* if the file opened then continue */
  
        if(*status == 0) continue;
  
        /*
          the grouping table location given by GRPLCn must specify a
          relative URL. We assume that this URL is relative to the
          member HDU's FITS file. Try to construct a full URL location
          for the grouping table's FITS file and then open it
        */
  
        *status = 0;
  
        /* retrieve the URL information for the member HDU's file */
  
        url[0] = location1; url[1] = location2;
  
        *status = fits_get_url(mfptr,url[0],url[1],NULL,NULL,NULL,status);
  
        /*
          It is possible that the member HDU file has an initial
          URL it was opened with and a real URL that the file actually
          exists at (e.g., an HTTP accessed file copied to a local
          file). For each possible URL try to construct a
        */
  
        for(i = 0, found = 0, *gfptr = NULL; i < 2 && !found; ++i)
          {
  
            /* the url string could be empty */
  
            if(*url[i] == 0) continue;
  
            /*
           create a full URL from the partial and the member
           HDU file URL
            */
  
            *status = fits_relurl2url(url[i],keyvalue,location,status);
  
            /* if an error occured then contniue */
  
            if(*status != 0)
          {
            *status = 0;
            continue;
          }
  
            /*
          if the location does not specify an access method
          then turn it into a host dependent path
            */
  
            if(! fits_is_url_absolute(location))
          {
            *status = fits_url2path(location,url[i],status);
            strcpy(location,url[i]);
          }
  
            /* try to open the grouping table file READWRITE */
  
            *status = fits_open_file(gfptr,location,READWRITE,status);
  
            if(*status != 0)
          {
            /* try to open the grouping table file READONLY */
  
            ffpmsg("opening file as READWRITE failed (ffgtop)");
            ffpmsg("OK, try to open file as READONLY (ffgtop)");
            *status = 0;
            *status = fits_open_file(gfptr,location,READONLY,status);
          }
  
            /* either set the found flag or reset the status flag */
  
            if(*status == 0)
          found = 1;
            else
          *status = 0;
          }
  
      }while(0); /* end of file opening loop */
  
        /* if an error occured with the file opening then exit */
  
        if(*status != 0) continue;
  
        if(*gfptr == NULL)
      {
        ffpmsg("Cannot open or find grouping table FITS file (ffgtop)");
        *status = GROUP_NOT_FOUND;
        continue;
      }
  
        /* search for the grouping table in its FITS file */
  
        *status = fits_movnam_hdu(*gfptr,ANY_HDU,"GROUPING",(int)grpExtver,
                  status);
  
        if(*status != 0) *status = GROUP_NOT_FOUND;
  
      }while(0);
  
    if(*status != 0 && *gfptr != NULL)
      {
        fits_close_file(*gfptr,status);
        *gfptr = NULL;
      }
  
    return(*status);
  }
  /*---------------------------------------------------------------------------*/
  int ffgtam(fitsfile *gfptr,   /* FITS file pointer to grouping table HDU     */
         fitsfile *mfptr,   /* FITS file pointer to member HDU             */
         int       hdupos,  /* member HDU position IF in the same file as
                       the grouping table AND mfptr == NULL        */
         int      *status)  /* return status code                          */
  
  /*
    add a member HDU to an existing grouping table. The fitsfile pointer gfptr
    must be positioned with the grouping table as the CHDU. The member HDU
    may either be identifed with the fitsfile *mfptr (which must be positioned
    to the member HDU) or the hdupos parameter (the HDU number of the member
    HDU) if both reside in the same FITS file. The hdupos value is only used
    if the mfptr parameter has a value of NULL (0). The new member HDU shall
    have the appropriate GRPIDn and GRPLCn keywords created in its header.
  
    Note that if the member HDU to be added to the grouping table is already
    a member of the group then it will not be added a sceond time.
  */
  
  {
    int xtensionCol,extnameCol,extverCol,positionCol,locationCol,uriCol;
    int memberPosition = 0;
    int grptype        = 0;
    int hdutype        = 0;
    int useLocation    = 0;
    int nkeys          = 6;
    int found;
    int i;
  
    int memberIOstate;
    int groupIOstate;
    int iomode;
  
    long memberExtver = 0;
    long groupExtver  = 0;
    long memberID     = 0;
    long nmembers     = 0;
    long ngroups      = 0;
    long grpid        = 0;
  
    char memberAccess1[FLEN_VALUE];
    char memberAccess2[FLEN_VALUE];
    char memberFileName[FLEN_FILENAME];
    char memberLocation[FLEN_FILENAME];
    char grplc[FLEN_FILENAME];
    char *tgrplc;
    char memberHDUtype[FLEN_VALUE];
    char memberExtname[FLEN_VALUE];
    char memberURI[] = "URL";
  
    char groupAccess1[FLEN_VALUE];
    char groupAccess2[FLEN_VALUE];
    char groupFileName[FLEN_FILENAME];
    char groupLocation[FLEN_FILENAME];
    char tmprootname[FLEN_FILENAME], grootname[FLEN_FILENAME];
    char cwd[FLEN_FILENAME];
  
    char *keys[] = {"GRPNAME","EXTVER","EXTNAME","TFIELDS","GCOUNT","EXTEND"};
    char *tmpPtr[1];
  
    char keyword[FLEN_KEYWORD];
    char card[FLEN_CARD];
  
    unsigned char charNull[]  = {'\0'};
  
    fitsfile *tmpfptr = NULL;
  
    int parentStatus = 0;
  
    if(*status != 0) return(*status);
  
    do
      {
        /*
      make sure the grouping table can be modified before proceeding
        */
  
        fits_file_mode(gfptr,&iomode,status);
  
        if(iomode != READWRITE)
      {
        ffpmsg("cannot modify grouping table (ffgtam)");
        *status = BAD_GROUP_ATTACH;
        continue;
      }
  
        /*
       if the calling function supplied the HDU position of the member
       HDU instead of fitsfile pointer then get a fitsfile pointer
        */
  
        if(mfptr == NULL)
      {
        *status = fits_reopen_file(gfptr,&tmpfptr,status);
        *status = fits_movabs_hdu(tmpfptr,hdupos,&hdutype,status);
  
        if(*status != 0) continue;
      }
        else
      tmpfptr = mfptr;
  
        /*
       determine all the information about the member HDU that will
       be needed later; note that we establish the default values for
       all information values that are not explicitly found
        */
  
        *status = fits_read_key_str(tmpfptr,"XTENSION",memberHDUtype,card,
                    status);
  
        if(*status == KEY_NO_EXIST)
      {
        strcpy(memberHDUtype,"PRIMARY");
        *status = 0;
      }
        prepare_keyvalue(memberHDUtype);
  
        *status = fits_read_key_lng(tmpfptr,"EXTVER",&memberExtver,card,status);
  
        if(*status == KEY_NO_EXIST)
      {
        memberExtver = 1;
        *status      = 0;
      }
  
        *status = fits_read_key_str(tmpfptr,"EXTNAME",memberExtname,card,
                    status);
  
        if(*status == KEY_NO_EXIST)
      {
        memberExtname[0] = 0;
        *status          = 0;
      }
        prepare_keyvalue(memberExtname);
  
        fits_get_hdu_num(tmpfptr,&memberPosition);
  
        /*
      Determine if the member HDU's FITS file location needs to be
      taken into account when building its grouping table reference
  
      If the member location needs to be used (==> grouping table and member
      HDU reside in different files) then create an appropriate URL for
      the member HDU's file and grouping table's file. Note that the logic
      for this is rather complicated
        */
  
        /* SPR 3463, don't do this
       if(tmpfptr->Fptr == gfptr->Fptr)
       {  */
        /*
          member HDU and grouping table reside in the same file, no need
          to use the location information */
  
        /* printf ("same file\n");
  
         useLocation     = 0;
         memberIOstate   = 1;
         *memberFileName = 0;
      }
        else
        { */
        /*
           the member HDU and grouping table FITS file location information
           must be used.
  
           First determine the correct driver and file name for the group
           table and member HDU files. If either are disk files then
           construct an absolute file path for them. Finally, if both are
           disk files construct relative file paths from the group(member)
           file to the member(group) file.
  
        */
  
        /* set the USELOCATION flag to true */
  
        useLocation = 1;
  
        /*
           get the location, access type and iostate (RO, RW) of the
           member HDU file
        */
  
        *status = fits_get_url(tmpfptr,memberFileName,memberLocation,
                   memberAccess1,memberAccess2,&memberIOstate,
                   status);
  
        /*
           if the memberFileName string is empty then use the values of
           the memberLocation string. This corresponds to a file where
           the "real" file is a temporary memory file, and we must assume
           the the application really wants the original file to be the
           group member
         */
  
        if(strlen(memberFileName) == 0)
          {
            strcpy(memberFileName,memberLocation);
            strcpy(memberAccess1,memberAccess2);
          }
  
        /*
           get the location, access type and iostate (RO, RW) of the
           grouping table file
        */
  
        *status = fits_get_url(gfptr,groupFileName,groupLocation,
                   groupAccess1,groupAccess2,&groupIOstate,
                   status);
  
        if(*status != 0) continue;
  
        /*
          the grouping table file must be writable to continue
        */
  
        if(groupIOstate == 0)
          {
            ffpmsg("cannot modify grouping table (ffgtam)");
            *status = BAD_GROUP_ATTACH;
            continue;
          }
  
        /*
          determine how to construct the resulting URLs for the member and
          group files
        */
  
        if(fits_strcasecmp(groupAccess1,"file://")  &&
                                         fits_strcasecmp(memberAccess1,"file://"))
          {
                *cwd = 0;
            /*
           nothing to do in this case; both the member and group files
           must be of an access type that already gives valid URLs;
           i.e., URLs that we can pass directly to the file drivers
            */
          }
        else
          {
            /*
           retrieve the Current Working Directory as a Unix-like
           URL standard string
            */
  
            *status = fits_get_cwd(cwd,status);
  
            /*
           create full file path for the member HDU FITS file URL
           if it is of access type file://
            */
  
            if(fits_strcasecmp(memberAccess1,"file://") == 0)
          {
            if(*memberFileName == '/')
              {
                strcpy(memberLocation,memberFileName);
              }
            else
              {
                strcpy(memberLocation,cwd);
                        if (strlen(memberLocation)+strlen(memberFileName)+1 >
                                  FLEN_FILENAME-1)
                        {
                           ffpmsg("member path and filename is too long (ffgtam)");
                           *status = URL_PARSE_ERROR;
                           continue;
                        }
                strcat(memberLocation,"/");
                strcat(memberLocation,memberFileName);
              }
  
            *status = fits_clean_url(memberLocation,memberFileName,
                         status);
          }
  
            /*
           create full file path for the grouping table HDU FITS file URL
           if it is of access type file://
            */
  
            if(fits_strcasecmp(groupAccess1,"file://") == 0)
          {
            if(*groupFileName == '/')
              {
                strcpy(groupLocation,groupFileName);
              }
            else
              {
                strcpy(groupLocation,cwd);
                        if (strlen(groupLocation)+strlen(groupFileName)+1 >
                                  FLEN_FILENAME-1)
                        {
                           ffpmsg("group path and filename is too long (ffgtam)");
                           *status = URL_PARSE_ERROR;
                           continue;
                        }
  
                strcat(groupLocation,"/");
                strcat(groupLocation,groupFileName);
              }
  
            *status = fits_clean_url(groupLocation,groupFileName,status);
          }
  
            /*
          if both the member and group files are disk files then
          create a relative path (relative URL) strings with
          respect to the grouping table's file and the grouping table's
          file with respect to the member HDU's file
            */
  
            if(fits_strcasecmp(groupAccess1,"file://") == 0 &&
                                fits_strcasecmp(memberAccess1,"file://") == 0)
          {
            fits_url2relurl(memberFileName,groupFileName,
                                    groupLocation,status);
            fits_url2relurl(groupFileName,memberFileName,
                                    memberLocation,status);
  
            /*
               copy the resulting partial URL strings to the
               memberFileName and groupFileName variables for latter
               use in the function
             */
  
            strcpy(memberFileName,memberLocation);
            strcpy(groupFileName,groupLocation);
          }
          }
        /* beo done */
        /* }  */
  
  
        /* retrieve the grouping table's EXTVER value */
  
        *status = fits_read_key_lng(gfptr,"EXTVER",&groupExtver,card,status);
  
        /*
       if useLocation is true then make the group EXTVER value negative
       for the subsequent GRPIDn/GRPLCn matching
        */
        /* SPR 3463 change test;  WDP added test for same filename */
        /* Now, if either the Fptr values are the same, or the root filenames
           are the same, then assume these refer to the same file.
        */
        fits_parse_rootname(tmpfptr->Fptr->filename, tmprootname, status);
        fits_parse_rootname(gfptr->Fptr->filename, grootname, status);
  
        if((tmpfptr->Fptr != gfptr->Fptr) &&
            strncmp(tmprootname, grootname, FLEN_FILENAME))
         groupExtver = -1*groupExtver;
  
        /* retrieve the number of group members */
  
        *status = fits_get_num_members(gfptr,&nmembers,status);
  
      do {
  
        /*
       make sure the member HDU is not already an entry in the
       grouping table before adding it
        */
  
        *status = ffgmf(gfptr,memberHDUtype,memberExtname,memberExtver,
                memberPosition,memberFileName,&memberID,status);
  
        if(*status == MEMBER_NOT_FOUND) *status = 0;
        else if(*status == 0)
      {
        parentStatus = HDU_ALREADY_MEMBER;
      ffpmsg("Specified HDU is already a member of the Grouping table (ffgtam)");
        continue;
      }
        else continue;
  
        /*
       if the member HDU is not already recorded in the grouping table
       then add it
        */
  
        /* add a new row to the grouping table */
  
        *status = fits_insert_rows(gfptr,nmembers,1,status);
        ++nmembers;
  
        /* retrieve the grouping table column IDs and structure type */
  
        *status = ffgtgc(gfptr,&xtensionCol,&extnameCol,&extverCol,&positionCol,
                 &locationCol,&uriCol,&grptype,status);
  
        /* fill in the member HDU data in the new grouping table row */
  
        *tmpPtr = memberHDUtype;
  
        if(xtensionCol != 0)
      fits_write_col_str(gfptr,xtensionCol,nmembers,1,1,tmpPtr,status);
  
        *tmpPtr = memberExtname;
  
        if(extnameCol  != 0)
      {
        if(strlen(memberExtname) != 0)
          fits_write_col_str(gfptr,extnameCol,nmembers,1,1,tmpPtr,status);
        else
          /* WILL THIS WORK FOR VAR LENTH CHAR COLS??????*/
          fits_write_col_byt(gfptr,extnameCol,nmembers,1,1,charNull,status);
      }
  
        if(extverCol   != 0)
      fits_write_col_lng(gfptr,extverCol,nmembers,1,1,&memberExtver,
                 status);
  
        if(positionCol != 0)
      fits_write_col_int(gfptr,positionCol,nmembers,1,1,
                 &memberPosition,status);
  
        *tmpPtr = memberFileName;
  
        if(locationCol != 0)
      {
        /* Change the test for SPR 3463 */
        /* Now, if either the Fptr values are the same, or the root filenames
           are the same, then assume these refer to the same file.
        */
        fits_parse_rootname(tmpfptr->Fptr->filename, tmprootname, status);
        fits_parse_rootname(gfptr->Fptr->filename, grootname, status);
  
        if((tmpfptr->Fptr != gfptr->Fptr) &&
                strncmp(tmprootname, grootname, FLEN_FILENAME))
          fits_write_col_str(gfptr,locationCol,nmembers,1,1,tmpPtr,status);
        else
          /* WILL THIS WORK FOR VAR LENTH CHAR COLS??????*/
          fits_write_col_byt(gfptr,locationCol,nmembers,1,1,charNull,status);
      }
  
        *tmpPtr = memberURI;
  
        if(uriCol      != 0)
      {
  
        /* Change the test for SPR 3463 */
        /* Now, if either the Fptr values are the same, or the root filenames
           are the same, then assume these refer to the same file.
        */
        fits_parse_rootname(tmpfptr->Fptr->filename, tmprootname, status);
        fits_parse_rootname(gfptr->Fptr->filename, grootname, status);
  
        if((tmpfptr->Fptr != gfptr->Fptr) &&
                strncmp(tmprootname, grootname, FLEN_FILENAME))
          fits_write_col_str(gfptr,uriCol,nmembers,1,1,tmpPtr,status);
        else
          /* WILL THIS WORK FOR VAR LENTH CHAR COLS??????*/
          fits_write_col_byt(gfptr,uriCol,nmembers,1,1,charNull,status);
      }
      } while(0);
  
        if(0 != *status) continue;
        /*
       add GRPIDn/GRPLCn keywords to the member HDU header to link
       it to the grouing table if the they do not already exist and
       the member file is RW
        */
  
        fits_file_mode(tmpfptr,&iomode,status);
  
       if(memberIOstate == 0 || iomode != READWRITE)
      {
        ffpmsg("cannot add GRPID/LC keywords to member HDU: (ffgtam)");
        ffpmsg(memberFileName);
        continue;
      }
  
        *status = fits_get_num_groups(tmpfptr,&ngroups,status);
  
        /*
       look for the GRPID/LC keywords in the member HDU; if the keywords
       for the back-link to the grouping table already exist then no
       need to add them again
         */
  
        for(i = 1, found = 0; i <= ngroups && !found && *status == 0; ++i)
      {
        snprintf(keyword,FLEN_KEYWORD,"GRPID%d",(int)ngroups);
        *status = fits_read_key_lng(tmpfptr,keyword,&grpid,card,status);
  
        if(grpid == groupExtver)
          {
            if(grpid < 0)
          {
  
            /* have to make sure the GRPLCn keyword matches too */
  
            snprintf(keyword,FLEN_KEYWORD,"GRPLC%d",(int)ngroups);
            /* SPR 1738 */
            *status = fits_read_key_longstr(mfptr,keyword,&tgrplc,card,
                            status);
            if (0 == *status) {
              strcpy(grplc,tgrplc);
              free(tgrplc);
            }
  
            /*
               always compare files using absolute paths
                       the presence of a non-empty cwd indicates
                       that the file names may require conversion
                       to absolute paths
                    */
  
                    if(0 < strlen(cwd)) {
                      /* temp buffer for use in assembling abs. path(s) */
                      char tmp[FLEN_FILENAME];
  
                      /* make grplc absolute if necessary */
                      if(!fits_is_url_absolute(grplc)) {
                fits_path2url(grplc,FLEN_FILENAME,groupLocation,status);
  
                if(groupLocation[0] != '/')
              {
                strcpy(tmp, cwd);
                            if (strlen(tmp)+strlen(groupLocation)+1 >
                                      FLEN_FILENAME-1)
                            {
                               ffpmsg("path and group location is too long (ffgtam)");
                               *status = URL_PARSE_ERROR;
                               continue;
                            }
                strcat(tmp,"/");
                strcat(tmp,groupLocation);
                fits_clean_url(tmp,grplc,status);
              }
                      }
  
                      /* make groupFileName absolute if necessary */
                      if(!fits_is_url_absolute(groupFileName)) {
                fits_path2url(groupFileName,FLEN_FILENAME,groupLocation,status);
  
                if(groupLocation[0] != '/')
              {
                strcpy(tmp, cwd);
                            if (strlen(tmp)+strlen(groupLocation)+1 >
                                      FLEN_FILENAME-1)
                            {
                               ffpmsg("path and group location is too long (ffgtam)");
                               *status = URL_PARSE_ERROR;
                               continue;
                            }
                strcat(tmp,"/");
                strcat(tmp,groupLocation);
                            /*
                               note: use groupLocation (which is not used
                               below this block), to store the absolute
                               file name instead of using groupFileName.
                               The latter may be needed unaltered if the
                               GRPLC is written below
                            */
  
                fits_clean_url(tmp,groupLocation,status);
              }
                      }
                    }
            /*
              see if the grplc value and the group file name match
            */
  
            if(strcmp(grplc,groupLocation) == 0) found = 1;
          }
            else
          {
            /* the match is found with GRPIDn alone */
            found = 1;
          }
          }
      }
  
        /*
       if FOUND is true then no need to continue
        */
  
        if(found)
      {
        ffpmsg("HDU already has GRPID/LC keywords for group table (ffgtam)");
        continue;
      }
  
        /*
       add the GRPID/LC keywords to the member header for this grouping
       table
  
       If NGROUPS == 0 then we must position the header pointer to the
       record where we want to insert the GRPID/LC keywords (the pointer
       is already correctly positioned if the above search loop activiated)
        */
  
        if(ngroups == 0)
      {
        /*
           no GRPIDn/GRPLCn keywords currently exist in header so try
           to position the header pointer to a desirable position
        */
  
        for(i = 0, *status = KEY_NO_EXIST;
                             i < nkeys && *status == KEY_NO_EXIST; ++i)
          {
            *status = 0;
            *status = fits_read_card(tmpfptr,keys[i],card,status);
          }
  
        /* all else fails: move write pointer to end of header */
  
        if(*status == KEY_NO_EXIST)
          {
            *status = 0;
            fits_get_hdrspace(tmpfptr,&nkeys,&i,status);
            ffgrec(tmpfptr,nkeys,card,status);
          }
  
        /* any other error status then abort */
  
        if(*status != 0) continue;
      }
  
        /*
       now that the header pointer is positioned for the GRPID/LC
       keyword insertion increment the number of group links counter for
       the member HDU
        */
  
        ++ngroups;
  
        /*
       if the member HDU and grouping table reside in the same FITS file
       then there is no need to add a GRPLCn keyword
        */
        /* SPR 3463 change test */
        /* Now, if either the Fptr values are the same, or the root filenames
       are the same, then assume these refer to the same file.
        */
        fits_parse_rootname(tmpfptr->Fptr->filename, tmprootname, status);
        fits_parse_rootname(gfptr->Fptr->filename, grootname, status);
  
        if((tmpfptr->Fptr == gfptr->Fptr) ||
                strncmp(tmprootname, grootname, FLEN_FILENAME) == 0)
      {
        /* add the GRPIDn keyword only */
  
        snprintf(keyword,FLEN_KEYWORD,"GRPID%d",(int)ngroups);
        fits_insert_key_lng(tmpfptr,keyword,groupExtver,
                    "EXTVER of Group containing this HDU",status);
      }
        else
      {
        /* add the GRPIDn and GRPLCn keywords */
  
        snprintf(keyword,FLEN_KEYWORD,"GRPID%d",(int)ngroups);
        fits_insert_key_lng(tmpfptr,keyword,groupExtver,
                    "EXTVER of Group containing this HDU",status);
  
        snprintf(keyword,FLEN_KEYWORD,"GRPLC%d",(int)ngroups);
        /* SPR 1738 */
        fits_insert_key_longstr(tmpfptr,keyword,groupFileName,
                    "URL of file containing Group",status);
        fits_write_key_longwarn(tmpfptr,status);
  
      }
  
      }while(0);
  
    /* close the tmpfptr pointer if it was opened in this function */
  
    if(mfptr == NULL)
      {
        *status = fits_close_file(tmpfptr,status);
      }
  
    *status = 0 == *status ? parentStatus : *status;
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int ffgtnm(fitsfile *gfptr,    /* FITS file pointer to grouping table        */
         long     *nmembers, /* member count  of the groping table         */
         int      *status)   /* return status code                         */
  
  /*
    return the number of member HDUs in a grouping table. The fitsfile pointer
    gfptr must be positioned with the grouping table as the CHDU. The number
    of grouping table member HDUs is just the NAXIS2 value of the grouping
    table.
  */
  
  {
    char keyvalue[FLEN_VALUE];
    char comment[FLEN_COMMENT];
  
  
    if(*status != 0) return(*status);
  
    *status = fits_read_keyword(gfptr,"EXTNAME",keyvalue,comment,status);
  
    if(*status == KEY_NO_EXIST)
      *status = NOT_GROUP_TABLE;
    else
      {
        prepare_keyvalue(keyvalue);
  
        if(fits_strcasecmp(keyvalue,"GROUPING") != 0)
      {
        *status = NOT_GROUP_TABLE;
        ffpmsg("Specified HDU is not a Grouping table (ffgtnm)");
      }
  
        *status = fits_read_key_lng(gfptr,"NAXIS2",nmembers,comment,status);
      }
  
    return(*status);
  }
  
  /*--------------------------------------------------------------------------*/
  int ffgmng(fitsfile *mfptr,   /* FITS file pointer to member HDU            */
         long     *ngroups, /* total number of groups linked to HDU       */
         int      *status)  /* return status code                         */
  
  /*
    return the number of groups to which a HDU belongs, as defined by the number
    of GRPIDn/GRPLCn keyword records that appear in the HDU header. The
    fitsfile pointer mfptr must be positioned with the member HDU as the CHDU.
    Each time this function is called, the indicies of the GRPIDn/GRPLCn
    keywords are checked to make sure they are continuous (ie no gaps) and
    are re-enumerated to eliminate gaps if gaps are found to be present.
  */
  
  {
    int offset;
    int index;
    int newIndex;
    int i;
  
    long grpid;
  
    char *inclist[] = {"GRPID#"};
    char keyword[FLEN_KEYWORD];
    char newKeyword[FLEN_KEYWORD];
    char card[FLEN_CARD];
    char comment[FLEN_COMMENT];
    char *tkeyvalue;
  
    if(*status != 0) return(*status);
  
    *ngroups = 0;
  
    /* reset the member HDU keyword counter to the beginning */
  
    *status = ffgrec(mfptr,0,card,status);
  
    /*
      search for the number of GRPIDn keywords in the member HDU header
      and count them with the ngroups variable
    */
  
    while(*status == 0)
      {
        /* read the next GRPIDn keyword in the series */
  
        *status = fits_find_nextkey(mfptr,inclist,1,NULL,0,card,status);
  
        if(*status != 0) continue;
  
        ++(*ngroups);
      }
  
    if(*status == KEY_NO_EXIST) *status = 0;
  
    /*
       read each GRPIDn/GRPLCn keyword and adjust their index values so that
       there are no gaps in the index count
    */
  
    for(index = 1, offset = 0, i = 1; i <= *ngroups && *status == 0; ++index)
      {
        snprintf(keyword,FLEN_KEYWORD,"GRPID%d",index);
  
        /* try to read the next GRPIDn keyword in the series */
  
        *status = fits_read_key_lng(mfptr,keyword,&grpid,card,status);
  
        /* if not found then increment the offset counter and continue */
  
        if(*status == KEY_NO_EXIST)
      {
        *status = 0;
        ++offset;
      }
        else
      {
        /*
           increment the number_keys_found counter and see if the index
           of the keyword needs to be updated
        */
  
        ++i;
  
        if(offset > 0)
          {
            /* compute the new index for the GRPIDn/GRPLCn keywords */
            newIndex = index - offset;
  
            /* update the GRPIDn keyword index */
  
            snprintf(newKeyword,FLEN_KEYWORD,"GRPID%d",newIndex);
            fits_modify_name(mfptr,keyword,newKeyword,status);
  
            /* If present, update the GRPLCn keyword index */
  
            snprintf(keyword,FLEN_KEYWORD,"GRPLC%d",index);
            snprintf(newKeyword,FLEN_KEYWORD,"GRPLC%d",newIndex);
            /* SPR 1738 */
            *status = fits_read_key_longstr(mfptr,keyword,&tkeyvalue,comment,
                            status);
            if (0 == *status) {
          fits_delete_key(mfptr,keyword,status);
          fits_insert_key_longstr(mfptr,newKeyword,tkeyvalue,comment,status);
          fits_write_key_longwarn(mfptr,status);
          free(tkeyvalue);
            }
  
  
            if(*status == KEY_NO_EXIST) *status = 0;
          }
      }
      }
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int ffgmop(fitsfile *gfptr,  /* FITS file pointer to grouping table          */
         long      member, /* member ID (row num) within grouping table    */
         fitsfile **mfptr, /* FITS file pointer to member HDU              */
         int      *status) /* return status code                           */
  
  /*
    open a grouping table member, returning a pointer to the member's FITS file
    with the CHDU set to the member HDU. The grouping table must be the CHDU of
    the FITS file pointed to by gfptr. The member to open is identified by its
    row number within the grouping table (first row/member == 1).
  
    If the member resides in a FITS file different from the grouping
    table the member file is first opened readwrite and if this fails then
    it is opened readonly. For access type of FILE:// the member file is
    searched for assuming (1) an absolute path is given, (2) a path relative
    to the CWD is given, and (3) a path relative to the grouping table file
    but not relative to the CWD is given. If all of these fail then the
    error FILE_NOT_FOUND is returned.
  */
  
  {
    int xtensionCol,extnameCol,extverCol,positionCol,locationCol,uriCol;
    int grptype,hdutype;
    int dummy;
  
    long hdupos = 0;
    long extver = 0;
  
    char  xtension[FLEN_VALUE];
    char  extname[FLEN_VALUE];
    char  uri[FLEN_VALUE];
    char  grpLocation1[FLEN_FILENAME];
    char  grpLocation2[FLEN_FILENAME];
    char  mbrLocation1[FLEN_FILENAME];
    char  mbrLocation2[FLEN_FILENAME];
    char  mbrLocation3[FLEN_FILENAME];
    char  cwd[FLEN_FILENAME];
    char  card[FLEN_CARD];
    char  nstr[] = {'\0'};
    char *tmpPtr[1];
  
  
    if(*status != 0) return(*status);
  
    do
      {
        /*
      retrieve the Grouping Convention reserved column positions within
      the grouping table
        */
  
        *status = ffgtgc(gfptr,&xtensionCol,&extnameCol,&extverCol,&positionCol,
                 &locationCol,&uriCol,&grptype,status);
  
        if(*status != 0) continue;
  
        /* verify the column formats */
  
        *status = ffvcfm(gfptr,xtensionCol,extnameCol,extverCol,positionCol,
                 locationCol,uriCol,status);
  
        if(*status != 0) continue;
  
        /*
       extract the member information from grouping table
        */
  
        tmpPtr[0] = xtension;
  
        if(xtensionCol != 0)
      {
  
        *status = fits_read_col_str(gfptr,xtensionCol,member,1,1,nstr,
                        tmpPtr,&dummy,status);
  
        /* convert the xtension string to a hdutype code */
  
        if(fits_strcasecmp(xtension,"PRIMARY")       == 0) hdutype = IMAGE_HDU;
        else if(fits_strcasecmp(xtension,"IMAGE")    == 0) hdutype = IMAGE_HDU;
        else if(fits_strcasecmp(xtension,"TABLE")    == 0) hdutype = ASCII_TBL;
        else if(fits_strcasecmp(xtension,"BINTABLE") == 0) hdutype = BINARY_TBL;
        else hdutype = ANY_HDU;
      }
  
        tmpPtr[0] = extname;
  
        if(extnameCol  != 0)
        *status = fits_read_col_str(gfptr,extnameCol,member,1,1,nstr,
                        tmpPtr,&dummy,status);
  
        if(extverCol   != 0)
        *status = fits_read_col_lng(gfptr,extverCol,member,1,1,0,
                        (long*)&extver,&dummy,status);
  
        if(positionCol != 0)
        *status = fits_read_col_lng(gfptr,positionCol,member,1,1,0,
                        (long*)&hdupos,&dummy,status);
  
        tmpPtr[0] = mbrLocation1;
  
        if(locationCol != 0)
      *status = fits_read_col_str(gfptr,locationCol,member,1,1,nstr,
                      tmpPtr,&dummy,status);
        tmpPtr[0] = uri;
  
        if(uriCol != 0)
      *status = fits_read_col_str(gfptr,uriCol,member,1,1,nstr,
                      tmpPtr,&dummy,status);
  
        if(*status != 0) continue;
  
        /*
       decide what FITS file the member HDU resides in and open the file
       using the fitsfile* pointer mfptr; note that this logic is rather
       complicated and is based primiarly upon if a URL specifier is given
       for the member file in the grouping table
        */
  
        switch(grptype)
      {
  
      case GT_ID_POS:
      case GT_ID_REF:
      case GT_ID_ALL:
  
        /*
           no location information is given so we must assume that the
           member HDU resides in the same FITS file as the grouping table;
           if the grouping table was incorrectly constructed then this
           assumption will be false, but there is nothing to be done about
           it at this point
        */
  
        *status = fits_reopen_file(gfptr,mfptr,status);
  
        break;
  
      case GT_ID_REF_URI:
      case GT_ID_POS_URI:
      case GT_ID_ALL_URI:
  
        /*
          The member location column exists. Determine if the member
          resides in the same file as the grouping table or in a
          separate file; open the member file in either case
        */
  
        if(strlen(mbrLocation1) == 0)
          {
            /*
           since no location information was given we must assume
           that the member is in the same FITS file as the grouping
           table
            */
  
            *status = fits_reopen_file(gfptr,mfptr,status);
          }
        else
          {
            /*
          make sure the location specifiation is "URL"; we cannot
          decode any other URI types at this time
            */
  
            if(fits_strcasecmp(uri,"URL") != 0)
          {
            *status = FILE_NOT_OPENED;
            snprintf(card,FLEN_CARD,
            "Cannot open member HDU file with URI type %s (ffgmop)",
                uri);
            ffpmsg(card);
  
            continue;
          }
  
            /*
          The location string for the member is not NULL, so it
          does not necessially reside in the same FITS file as the
          grouping table.
  
          Three cases are attempted for opening the member's file
          in the following order:
  
          1. The URL given for the member's file is absolute (i.e.,
          access method supplied); try to open the member
  
          2. The URL given for the member's file is not absolute but
          is an absolute file path; try to open the member as a file
          after the file path is converted to a host-dependent form
  
          3. The URL given for the member's file is not absolute
              and is given as a relative path to the location of the
          grouping table's file. Create an absolute URL using the
          grouping table's file URL and try to open the member.
  
          If all three cases fail then an error is returned. In each
          case the file is first opened in read/write mode and failing
          that readonly mode.
  
          The following DO loop is only used as a mechanism to break
          (continue) when the proper file opening method is found
             */
  
            do
          {
            /*
               CASE 1:
  
               See if the member URL is absolute (i.e., includes a
               access directive) and if so open the file
             */
  
            if(fits_is_url_absolute(mbrLocation1))
              {
                /*
               the URL must specify an access method, which
               implies that its an absolute reference
  
               regardless of the access method, pass the whole
               URL to the open function for processing
                 */
  
                ffpmsg("member URL is absolute, try open R/W (ffgmop)");
  
                *status = fits_open_file(mfptr,mbrLocation1,READWRITE,
                             status);
  
                if(*status == 0) continue;
  
                *status = 0;
  
                /*
               now try to open file using full URL specs in
               readonly mode
                */
  
                ffpmsg("OK, now try to open read-only (ffgmop)");
  
                *status = fits_open_file(mfptr,mbrLocation1,READONLY,
                             status);
  
                /* break from DO loop regardless of status */
  
                continue;
              }
  
            /*
               CASE 2:
  
               If we got this far then the member URL location
               has no access type ==> FILE:// Try to open the member
               file using the URL as is, i.e., assume that it is given
               as absolute, if it starts with a '/' character
             */
  
            ffpmsg("Member URL is of type FILE (ffgmop)");
  
            if(*mbrLocation1 == '/')
              {
                ffpmsg("Member URL specifies abs file path (ffgmop)");
  
                /*
               convert the URL path to a host dependent path
                */
  
                *status = fits_url2path(mbrLocation1,mbrLocation2,
                            status);
  
                ffpmsg("Try to open member URL in R/W mode (ffgmop)");
  
                *status = fits_open_file(mfptr,mbrLocation2,READWRITE,
                             status);
  
                if(*status == 0) continue;
  
                *status = 0;
  
                /*
               now try to open file using the URL as an absolute
               path in readonly mode
                */
  
                ffpmsg("OK, now try to open read-only (ffgmop)");
  
                *status = fits_open_file(mfptr,mbrLocation2,READONLY,
                             status);
  
                /* break from the Do loop regardless of the status */
  
                continue;
              }
  
            /*
               CASE 3:
  
               If we got this far then the URL does not specify an
               absoulte file path or URL with access method. Since
               the path to the group table's file is (obviously) valid
               for the CWD, create a full location string for the
               member HDU using the grouping table URL as a basis
  
               The only problem is that the grouping table file might
               have two URLs, the original one used to open it and
               the one that points to the real file being accessed
               (i.e., a file accessed via HTTP but transferred to a
               local disk file). Have to attempt to build a URL to
               the member HDU file using both of these URLs if
               defined.
            */
  
            ffpmsg("Try to open member file as relative URL (ffgmop)");
  
            /* get the URL information for the grouping table file */
  
            *status = fits_get_url(gfptr,grpLocation1,grpLocation2,
                       NULL,NULL,NULL,status);
  
            /*
               if the "real" grouping table file URL is defined then
               build a full url for the member HDU file using it
               and try to open the member HDU file
            */
  
            if(*grpLocation1)
              {
                /* make sure the group location is absolute */
  
                if(! fits_is_url_absolute(grpLocation1) &&
                                            *grpLocation1 != '/')
              {
                fits_get_cwd(cwd,status);
                strcat(cwd,"/");
                            if (strlen(cwd)+strlen(grpLocation1)+1 >
                                      FLEN_FILENAME-1)
                            {
                               ffpmsg("cwd and group location1 is too long (ffgmop)");
                               *status = URL_PARSE_ERROR;
                               continue;
                            }
                strcat(cwd,grpLocation1);
                strcpy(grpLocation1,cwd);
              }
  
                /* create a full URL for the member HDU file */
  
                *status = fits_relurl2url(grpLocation1,mbrLocation1,
                          mbrLocation2,status);
  
                if(*status != 0) continue;
  
                /*
              if the URL does not have an access method given then
              translate it into a host dependent file path
                */
  
                if(! fits_is_url_absolute(mbrLocation2))
              {
                *status = fits_url2path(mbrLocation2,mbrLocation3,
                            status);
                strcpy(mbrLocation2,mbrLocation3);
              }
  
                /* try to open the member file READWRITE */
  
                *status = fits_open_file(mfptr,mbrLocation2,READWRITE,
                             status);
  
                if(*status == 0) continue;
  
                *status = 0;
  
                /* now try to open in readonly mode */
  
                ffpmsg("now try to open file as READONLY (ffgmop)");
  
                *status = fits_open_file(mfptr,mbrLocation2,READONLY,
                             status);
  
                if(*status == 0) continue;
  
                *status = 0;
              }
  
            /*
               if we got this far then either the "real" grouping table
               file URL was not defined or all attempts to open the
               resulting member HDU file URL failed.
  
               if the "original" grouping table file URL is defined then
               build a full url for the member HDU file using it
               and try to open the member HDU file
            */
  
            if(*grpLocation2)
              {
                /* make sure the group location is absolute */
  
                if(! fits_is_url_absolute(grpLocation2) &&
                                            *grpLocation2 != '/')
              {
                fits_get_cwd(cwd,status);
                            if (strlen(cwd)+strlen(grpLocation2)+1 >
                                      FLEN_FILENAME-1)
                            {
                               ffpmsg("cwd and group location2 is too long (ffgmop)");
                               *status = URL_PARSE_ERROR;
                               continue;
                            }
                strcat(cwd,"/");
                strcat(cwd,grpLocation2);
                strcpy(grpLocation2,cwd);
              }
  
                /* create an absolute URL for the member HDU file */
  
                *status = fits_relurl2url(grpLocation2,mbrLocation1,
                          mbrLocation2,status);
                if(*status != 0) continue;
  
                /*
              if the URL does not have an access method given then
              translate it into a host dependent file path
                */
  
                if(! fits_is_url_absolute(mbrLocation2))
              {
                *status = fits_url2path(mbrLocation2,mbrLocation3,
                            status);
                strcpy(mbrLocation2,mbrLocation3);
              }
  
                /* try to open the member file READWRITE */
  
                *status = fits_open_file(mfptr,mbrLocation2,READWRITE,
                             status);
  
                if(*status == 0) continue;
  
                *status = 0;
  
                /* now try to open in readonly mode */
  
                ffpmsg("now try to open file as READONLY (ffgmop)");
  
                *status = fits_open_file(mfptr,mbrLocation2,READONLY,
                             status);
  
                if(*status == 0) continue;
  
                *status = 0;
              }
  
            /*
               if we got this far then the member HDU file could not
               be opened using any method. Log the error.
            */
  
            ffpmsg("Cannot open member HDU FITS file (ffgmop)");
            *status = MEMBER_NOT_FOUND;
  
          }while(0);
          }
  
        break;
  
      default:
  
        /* no default action */
  
        break;
      }
  
        if(*status != 0) continue;
  
        /*
       attempt to locate the member HDU within its FITS file as determined
       and opened above
        */
  
        switch(grptype)
      {
  
      case GT_ID_POS:
      case GT_ID_POS_URI:
  
        /*
          try to find the member hdu in the the FITS file pointed to
          by mfptr based upon its HDU posistion value. Note that is
          impossible to verify if the HDU is actually the correct HDU due
          to a lack of information.
        */
  
        *status = fits_movabs_hdu(*mfptr,(int)hdupos,&hdutype,status);
  
        break;
  
      case GT_ID_REF:
      case GT_ID_REF_URI:
  
        /*
           try to find the member hdu in the FITS file pointed to
           by mfptr based upon its XTENSION, EXTNAME and EXTVER keyword
           values
        */
  
        *status = fits_movnam_hdu(*mfptr,hdutype,extname,extver,status);
  
        if(*status == BAD_HDU_NUM)
          {
            *status = MEMBER_NOT_FOUND;
            ffpmsg("Cannot find specified member HDU (ffgmop)");
          }
  
        /*
           if the above function returned without error then the
           mfptr is pointed to the member HDU
        */
  
        break;
  
      case GT_ID_ALL:
      case GT_ID_ALL_URI:
  
        /*
           if the member entry has reference information then use it
               (ID by reference is safer than ID by position) else use
           the position information
        */
  
        if(strlen(xtension) > 0 && strlen(extname) > 0 && extver > 0)
          {
            /* valid reference info exists so use it */
  
            /* try to find the member hdu in the grouping table's file */
  
            *status = fits_movnam_hdu(*mfptr,hdutype,extname,extver,status);
  
            if(*status == BAD_HDU_NUM)
          {
            *status = MEMBER_NOT_FOUND;
            ffpmsg("Cannot find specified member HDU (ffgmop)");
          }
          }
        else
            {
            *status = fits_movabs_hdu(*mfptr,(int)hdupos,&hdutype,
                          status);
            if(*status == END_OF_FILE) *status = MEMBER_NOT_FOUND;
            }
  
        /*
           if the above function returned without error then the
           mfptr is pointed to the member HDU
        */
  
        break;
  
      default:
  
        /* no default action */
  
        break;
      }
  
      }while(0);
  
    if(*status != 0 && *mfptr != NULL)
      {
        fits_close_file(*mfptr,status);
      }
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int ffgmcp(fitsfile *gfptr,  /* FITS file pointer to group                   */
         fitsfile *mfptr,  /* FITS file pointer to new member
                  FITS file                                    */
         long      member, /* member ID (row num) within grouping table    */
         int       cpopt,  /* code specifying copy options:
                  OPT_MCP_ADD  (0) ==> add copied member to the
                                        grouping table
                  OPT_MCP_NADD (1) ==> do not add member copy to
                                       the grouping table
                  OPT_MCP_REPL (2) ==> replace current member
                                       entry with member copy  */
         int      *status) /* return status code                           */
  
  /*
    copy a member HDU of a grouping table to a new FITS file. The grouping table
    must be the CHDU of the FITS file pointed to by gfptr. The copy of the
    group member shall be appended to the end of the FITS file pointed to by
    mfptr. If the cpopt parameter is set to OPT_MCP_ADD then the copy of the
    member is added to the grouping table as a new member, if OPT_MCP_NADD
    then the copied member is not added to the grouping table, and if
    OPT_MCP_REPL then the copied member is used to replace the original member.
    The copied member HDU also has its EXTVER value updated so that its
    combination of XTENSION, EXTNAME and EXVTER is unique within its new
    FITS file.
  */
  
  {
    int numkeys = 0;
    int keypos  = 0;
    int hdunum  = 0;
    int hdutype = 0;
    int i;
  
    char *incList[] = {"GRPID#","GRPLC#"};
    char  extname[FLEN_VALUE];
    char  card[FLEN_CARD];
    char  comment[FLEN_COMMENT];
    char  keyname[FLEN_CARD];
    char  value[FLEN_CARD];
  
    fitsfile *tmpfptr = NULL;
  
  
    if(*status != 0) return(*status);
  
    do
      {
        /* open the member HDU to be copied */
  
        *status = fits_open_member(gfptr,member,&tmpfptr,status);
  
        if(*status != 0) continue;
  
        /*
      if the member is a grouping table then copy it with a call to
      fits_copy_group() using the "copy only the grouping table" option
  
      if it is not a grouping table then copy the hdu with fits_copy_hdu()
      remove all GRPIDn and GRPLCn keywords, and update the EXTVER keyword
      value
        */
  
        /* get the member HDU's EXTNAME value */
  
        *status = fits_read_key_str(tmpfptr,"EXTNAME",extname,comment,status);
  
        /* if no EXTNAME value was found then set the extname to a null string */
  
        if(*status == KEY_NO_EXIST)
      {
        extname[0] = 0;
        *status    = 0;
      }
        else if(*status != 0) continue;
  
        prepare_keyvalue(extname);
  
        /* if a grouping table then copy with fits_copy_group() */
  
        if(fits_strcasecmp(extname,"GROUPING") == 0)
      *status = fits_copy_group(tmpfptr,mfptr,OPT_GCP_GPT,status);
        else
      {
        /* copy the non-grouping table HDU the conventional way */
  
        *status = fits_copy_hdu(tmpfptr,mfptr,0,status);
  
        ffgrec(mfptr,0,card,status);
  
        /* delete all the GRPIDn and GRPLCn keywords in the copied HDU */
  
        while(*status == 0)
          {
            *status = fits_find_nextkey(mfptr,incList,2,NULL,0,card,status);
            *status = fits_get_hdrpos(mfptr,&numkeys,&keypos,status);
            /* SPR 1738 */
            *status = fits_read_keyn(mfptr,keypos-1,keyname,value,
                         comment,status);
            *status = fits_read_record(mfptr,keypos-1,card,status);
            *status = fits_delete_key(mfptr,keyname,status);
          }
  
        if(*status == KEY_NO_EXIST) *status = 0;
        if(*status != 0) continue;
      }
  
        /*
       if the member HDU does not have an EXTNAME keyword then add one
       with a default value
        */
  
        if(strlen(extname) == 0)
      {
        if(fits_get_hdu_num(tmpfptr,&hdunum) == 1)
          {
            strcpy(extname,"PRIMARY");
            *status = fits_write_key_str(mfptr,"EXTNAME",extname,
                         "HDU was Formerly a Primary Array",
                         status);
          }
        else
          {
            strcpy(extname,"DEFAULT");
            *status = fits_write_key_str(mfptr,"EXTNAME",extname,
                         "default EXTNAME set by CFITSIO",
                         status);
          }
      }
  
        /*
       update the member HDU's EXTVER value (add it if not present)
        */
  
        fits_get_hdu_num(mfptr,&hdunum);
        fits_get_hdu_type(mfptr,&hdutype,status);
  
        /* set the EXTVER value to 0 for now */
  
        *status = fits_modify_key_lng(mfptr,"EXTVER",0,NULL,status);
  
        /* if the EXTVER keyword was not found then add it */
  
        if(*status == KEY_NO_EXIST)
      {
        *status = 0;
        *status = fits_read_key_str(mfptr,"EXTNAME",extname,comment,
                        status);
        *status = fits_insert_key_lng(mfptr,"EXTVER",0,
                      "Extension version ID",status);
      }
  
        if(*status != 0) continue;
  
        /* find the first available EXTVER value for the copied HDU */
  
        for(i = 1; fits_movnam_hdu(mfptr,hdutype,extname,i,status) == 0; ++i);
  
        *status = 0;
  
        fits_movabs_hdu(mfptr,hdunum,&hdutype,status);
  
        /* reset the copied member HDUs EXTVER value */
  
        *status = fits_modify_key_lng(mfptr,"EXTVER",(long)i,NULL,status);
  
        /*
      perform member copy operations that are dependent upon the cpopt
      parameter value
        */
  
        switch(cpopt)
      {
      case OPT_MCP_ADD:
  
        /*
          add the copied member to the grouping table, leaving the
          entry for the original member in place
        */
  
        *status = fits_add_group_member(gfptr,mfptr,0,status);
  
        break;
  
      case OPT_MCP_NADD:
  
        /*
          nothing to do for this copy option
        */
  
        break;
  
      case OPT_MCP_REPL:
  
        /*
          remove the original member from the grouping table and add the
          copied member in its place
        */
  
        *status = fits_remove_member(gfptr,member,OPT_RM_ENTRY,status);
        *status = fits_add_group_member(gfptr,mfptr,0,status);
  
        break;
  
      default:
  
        *status = BAD_OPTION;
        ffpmsg("Invalid value specified for the cmopt parameter (ffgmcp)");
  
        break;
      }
  
      }while(0);
  
    if(tmpfptr != NULL)
      {
        fits_close_file(tmpfptr,status);
      }
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int ffgmtf(fitsfile *infptr,   /* FITS file pointer to source grouping table */
         fitsfile *outfptr,  /* FITS file pointer to target grouping table */
         long      member,   /* member ID within source grouping table     */
         int       tfopt,    /* code specifying transfer opts:
                    OPT_MCP_ADD (0) ==> copy member to dest.
                    OPT_MCP_MOV (3) ==> move member to dest.   */
         int      *status)   /* return status code                         */
  
  /*
    transfer a group member from one grouping table to another. The source
    grouping table must be the CHDU of the fitsfile pointed to by infptr, and
    the destination grouping table must be the CHDU of the fitsfile to by
    outfptr. If the tfopt parameter is OPT_MCP_ADD then the member is made a
    member of the target group and remains a member of the source group. If
    the tfopt parameter is OPT_MCP_MOV then the member is deleted from the
    source group after the transfer to the destination group. The member to be
    transfered is identified by its row number within the source grouping table.
  */
  
  {
    fitsfile *mfptr = NULL;
  
  
    if(*status != 0) return(*status);
  
    if(tfopt != OPT_MCP_MOV && tfopt != OPT_MCP_ADD)
      {
        *status = BAD_OPTION;
        ffpmsg("Invalid value specified for the tfopt parameter (ffgmtf)");
      }
    else
      {
        /* open the member of infptr to be transfered */
  
        *status = fits_open_member(infptr,member,&mfptr,status);
  
        /* add the member to the outfptr grouping table */
  
        *status = fits_add_group_member(outfptr,mfptr,0,status);
  
        /* close the member HDU */
  
        *status = fits_close_file(mfptr,status);
  
        /*
       if the tfopt is "move member" then remove it from the infptr
       grouping table
        */
  
        if(tfopt == OPT_MCP_MOV)
      *status = fits_remove_member(infptr,member,OPT_RM_ENTRY,status);
      }
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int ffgmrm(fitsfile *gfptr,  /* FITS file pointer to group table             */
         long      member, /* member ID (row num) in the group             */
         int       rmopt,  /* code specifying the delete option:
                  OPT_RM_ENTRY ==> delete the member entry
                  OPT_RM_MBR   ==> delete entry and member HDU */
         int      *status)  /* return status code                          */
  
  /*
    remove a member HDU from a grouping table. The fitsfile pointer gfptr must
    be positioned with the grouping table as the CHDU, and the member to
    delete is identified by its row number in the table (first member == 1).
    The rmopt parameter determines if the member entry is deleted from the
    grouping table (in which case GRPIDn and GRPLCn keywords in the member
    HDU's header shall be updated accordingly) or if the member HDU shall
    itself be removed from its FITS file.
  */
  
  {
    int found;
    int hdutype   = 0;
    int index;
    int iomode    = 0;
  
    long i;
    long ngroups      = 0;
    long nmembers     = 0;
    long groupExtver  = 0;
    long grpid        = 0;
  
    char grpLocation1[FLEN_FILENAME];
    char grpLocation2[FLEN_FILENAME];
    char grpLocation3[FLEN_FILENAME];
    char cwd[FLEN_FILENAME];
    char keyword[FLEN_KEYWORD];
    /* SPR 1738 This can now be longer */
    char grplc[FLEN_FILENAME];
    char *tgrplc;
    char keyvalue[FLEN_VALUE];
    char card[FLEN_CARD];
    char *editLocation;
    char mrootname[FLEN_FILENAME], grootname[FLEN_FILENAME];
  
    fitsfile *mfptr  = NULL;
  
  
    if(*status != 0) return(*status);
  
    do
      {
        /*
      make sure the grouping table can be modified before proceeding
        */
  
        fits_file_mode(gfptr,&iomode,status);
  
        if(iomode != READWRITE)
      {
        ffpmsg("cannot modify grouping table (ffgtam)");
        *status = BAD_GROUP_DETACH;
        continue;
      }
  
        /* open the group member to be deleted and get its IOstatus*/
  
        *status = fits_open_member(gfptr,member,&mfptr,status);
        *status = fits_file_mode(mfptr,&iomode,status);
  
        /*
       if the member HDU is to be deleted then call fits_unlink_member()
       to remove it from all groups to which it belongs (including
       this one) and then delete it. Note that if the member is a
       grouping table then we have to recursively call fits_remove_member()
       for each member of the member before we delete the member itself.
        */
  
        if(rmopt == OPT_RM_MBR)
      {
          /* cannot delete a PHDU */
          if(fits_get_hdu_num(mfptr,&hdutype) == 1)
          {
              *status = BAD_HDU_NUM;
              continue;
          }
  
        /* determine if the member HDU is itself a grouping table */
  
        *status = fits_read_key_str(mfptr,"EXTNAME",keyvalue,card,status);
  
        /* if no EXTNAME is found then the HDU cannot be a grouping table */
  
        if(*status == KEY_NO_EXIST)
          {
            keyvalue[0] = 0;
            *status = 0;
          }
        prepare_keyvalue(keyvalue);
  
        /* Any other error is a reason to abort */
  
        if(*status != 0) continue;
  
        /* if the EXTNAME == GROUPING then the member is a grouping table */
  
        if(fits_strcasecmp(keyvalue,"GROUPING") == 0)
          {
            /* remove each of the grouping table members */
  
            *status = fits_get_num_members(mfptr,&nmembers,status);
  
            for(i = nmembers; i > 0 && *status == 0; --i)
          *status = fits_remove_member(mfptr,i,OPT_RM_ENTRY,status);
  
            if(*status != 0) continue;
          }
  
        /* unlink the member HDU from all groups that contain it */
  
        *status = ffgmul(mfptr,0,status);
  
        if(*status != 0) continue;
  
        /* reset the grouping table HDU struct */
  
        fits_set_hdustruc(gfptr,status);
  
        /* delete the member HDU */
  
        if(iomode != READONLY)
          *status = fits_delete_hdu(mfptr,&hdutype,status);
      }
        else if(rmopt == OPT_RM_ENTRY)
      {
        /*
           The member HDU is only to be removed as an entry from this
           grouping table. Actions are (1) find the GRPIDn/GRPLCn
           keywords that link the member to the grouping table, (2)
           remove the GRPIDn/GRPLCn keyword from the member HDU header
           and (3) remove the member entry from the grouping table
        */
  
        /*
          there is no need to seach for and remove the GRPIDn/GRPLCn
          keywords from the member HDU if it has not been opened
          in READWRITE mode
        */
  
        if(iomode == READWRITE)
          {
            /*
           determine the group EXTVER value of the grouping table; if
           the member HDU and grouping table HDU do not reside in the
           same file then set the groupExtver value to its negative
            */
  
            *status = fits_read_key_lng(gfptr,"EXTVER",&groupExtver,card,
                        status);
            /* Now, if either the Fptr values are the same, or the root filenames
               are the same, then assume these refer to the same file.
            */
            fits_parse_rootname(mfptr->Fptr->filename, mrootname, status);
            fits_parse_rootname(gfptr->Fptr->filename, grootname, status);
  
            if((mfptr->Fptr != gfptr->Fptr) &&
                strncmp(mrootname, grootname, FLEN_FILENAME))
                         groupExtver = -1*groupExtver;
  
            /*
          retrieve the URLs for the grouping table; note that it is
          possible that the grouping table file has two URLs, the
          one used to open it and the "real" one pointing to the
          actual file being accessed
            */
  
            *status = fits_get_url(gfptr,grpLocation1,grpLocation2,NULL,
                       NULL,NULL,status);
  
            if(*status != 0) continue;
  
            /*
          if either of the group location strings specify a relative
          file path then convert them into absolute file paths
            */
  
            *status = fits_get_cwd(cwd,status);
  
            if(*grpLocation1 != 0 && *grpLocation1 != '/' &&
           !fits_is_url_absolute(grpLocation1))
          {
            strcpy(grpLocation3,cwd);
                    if (strlen(grpLocation3)+strlen(grpLocation1)+1 >
                              FLEN_FILENAME-1)
                    {
                       ffpmsg("group locations are too long (ffgmrm)");
                       *status = URL_PARSE_ERROR;
                       continue;
                    }
            strcat(grpLocation3,"/");
            strcat(grpLocation3,grpLocation1);
            fits_clean_url(grpLocation3,grpLocation1,status);
          }
  
            if(*grpLocation2 != 0 && *grpLocation2 != '/' &&
           !fits_is_url_absolute(grpLocation2))
          {
            strcpy(grpLocation3,cwd);
                    if (strlen(grpLocation3)+strlen(grpLocation2)+1 >
                              FLEN_FILENAME-1)
                    {
                       ffpmsg("group locations are too long (ffgmrm)");
                       *status = URL_PARSE_ERROR;
                       continue;
                    }
            strcat(grpLocation3,"/");
            strcat(grpLocation3,grpLocation2);
            fits_clean_url(grpLocation3,grpLocation2,status);
          }
  
            /*
          determine the number of groups to which the member HDU
          belongs
            */
  
            *status = fits_get_num_groups(mfptr,&ngroups,status);
  
            /* reset the HDU keyword position counter to the beginning */
  
            *status = ffgrec(mfptr,0,card,status);
  
            /*
          loop over all the GRPIDn keywords in the member HDU header
          and find the appropriate GRPIDn and GRPLCn keywords that
          identify it as belonging to the group
            */
  
            for(index = 1, found = 0; index <= ngroups && *status == 0 &&
              !found; ++index)
          {
            /* read the next GRPIDn keyword in the series */
  
            snprintf(keyword,FLEN_KEYWORD,"GRPID%d",index);
  
            *status = fits_read_key_lng(mfptr,keyword,&grpid,card,
                            status);
            if(*status != 0) continue;
  
            /*
               grpid value == group EXTVER value then we could have a
               match
            */
  
            if(grpid == groupExtver && grpid > 0)
              {
                /*
              if GRPID is positive then its a match because
              both the member HDU and grouping table HDU reside
              in the same FITS file
                */
  
                found = index;
              }
            else if(grpid == groupExtver && grpid < 0)
              {
                /*
               have to look at the GRPLCn value to determine a
               match because the member HDU and grouping table
               HDU reside in different FITS files
                */
  
                snprintf(keyword,FLEN_KEYWORD,"GRPLC%d",index);
  
                /* SPR 1738 */
                *status = fits_read_key_longstr(mfptr,keyword,&tgrplc,
                                card, status);
                if (0 == *status) {
              strcpy(grplc,tgrplc);
              free(tgrplc);
                }
  
                if(*status == KEY_NO_EXIST)
              {
                /*
                   no GRPLCn keyword value found ==> grouping
                   convention not followed; nothing we can do
                   about it, so just continue
                */
  
                snprintf(card,FLEN_CARD,"No GRPLC%d found for GRPID%d",
                    index,index);
                ffpmsg(card);
                *status = 0;
                continue;
              }
                else if (*status != 0) continue;
  
                /* construct the URL for the GRPLCn value */
  
                prepare_keyvalue(grplc);
  
                /*
              if the grplc value specifies a relative path then
              turn it into a absolute file path for comparison
              purposes
                */
  
                if(*grplc != 0 && !fits_is_url_absolute(grplc) &&
               *grplc != '/')
              {
                  /* No, wrong,
                     strcpy(grpLocation3,cwd);
                     should be */
                  *status = fits_file_name(mfptr,grpLocation3,status);
                  /* Remove everything after the last / */
                  if (NULL != (editLocation = strrchr(grpLocation3,'/'))) {
                  *editLocation = '\0';
                  }
  
                            if (strlen(grpLocation3)+strlen(grplc)+1 >
                                      FLEN_FILENAME-1)
                            {
                               ffpmsg("group locations are too long (ffgmrm)");
                               *status = URL_PARSE_ERROR;
                               continue;
                            }
                strcat(grpLocation3,"/");
                strcat(grpLocation3,grplc);
                *status = fits_clean_url(grpLocation3,grplc,
                             status);
              }
  
                /*
              if the absolute value of GRPIDn is equal to the
              EXTVER value of the grouping table and (one of the
              possible two) grouping table file URL matches the
              GRPLCn keyword value then we hava a match
                */
  
                if(strcmp(grplc,grpLocation1) == 0  ||
               strcmp(grplc,grpLocation2) == 0)
              found = index;
              }
          }
  
            /*
          if found == 0 (false) after the above search then we assume
          that it is due to an inpromper updating of the GRPIDn and
          GRPLCn keywords in the member header ==> nothing to delete
          in the header. Else delete the GRPLCn and GRPIDn keywords
          that identify the member HDU with the group HDU and
          re-enumerate the remaining GRPIDn and GRPLCn keywords
            */
  
            if(found != 0)
          {
            snprintf(keyword,FLEN_KEYWORD,"GRPID%d",found);
            *status = fits_delete_key(mfptr,keyword,status);
  
            snprintf(keyword,FLEN_KEYWORD,"GRPLC%d",found);
            *status = fits_delete_key(mfptr,keyword,status);
  
            *status = 0;
  
            /* call fits_get_num_groups() to re-enumerate the GRPIDn */
  
            *status = fits_get_num_groups(mfptr,&ngroups,status);
          }
          }
  
        /*
           finally, remove the member entry from the current grouping table
           pointed to by gfptr
        */
  
        *status = fits_delete_rows(gfptr,member,1,status);
      }
        else
      {
        *status = BAD_OPTION;
        ffpmsg("Invalid value specified for the rmopt parameter (ffgmrm)");
      }
  
      }while(0);
  
    if(mfptr != NULL)
      {
        fits_close_file(mfptr,status);
      }
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------
                   Grouping Table support functions
    ---------------------------------------------------------------------------*/
  int ffgtgc(fitsfile *gfptr,  /* pointer to the grouping table                */
         int *xtensionCol, /* column ID of the MEMBER_XTENSION column      */
         int *extnameCol,  /* column ID of the MEMBER_NAME column          */
         int *extverCol,   /* column ID of the MEMBER_VERSION column       */
         int *positionCol, /* column ID of the MEMBER_POSITION column      */
         int *locationCol, /* column ID of the MEMBER_LOCATION column      */
         int *uriCol,      /* column ID of the MEMBER_URI_TYPE column      */
         int *grptype,     /* group structure type code specifying the
                  grouping table columns that are defined:
                  GT_ID_ALL_URI  (0) ==> all columns defined
                  GT_ID_REF      (1) ==> reference cols only
                  GT_ID_POS      (2) ==> position col only
                  GT_ID_ALL      (3) ==> ref & pos cols
                  GT_ID_REF_URI (11) ==> ref & loc cols
                  GT_ID_POS_URI (12) ==> pos & loc cols        */
         int *status)      /* return status code                           */
  /*
     examine the grouping table pointed to by gfptr and determine the column
     index ID of each possible grouping column. If a column is not found then
     an index of 0 is returned. the grptype parameter returns the structure
     of the grouping table ==> what columns are defined.
  */
  
  {
  
    char keyvalue[FLEN_VALUE];
    char comment[FLEN_COMMENT];
  
  
    if(*status != 0) return(*status);
  
    do
      {
        /*
      if the HDU does not have an extname of "GROUPING" then it is not
      a grouping table
        */
  
        *status = fits_read_key_str(gfptr,"EXTNAME",keyvalue,comment,status);
  
        if(*status == KEY_NO_EXIST)
      {
        *status = NOT_GROUP_TABLE;
        ffpmsg("Specified HDU is not a Grouping Table (ffgtgc)");
      }
        if(*status != 0) continue;
  
        prepare_keyvalue(keyvalue);
  
        if(fits_strcasecmp(keyvalue,"GROUPING") != 0)
      {
        *status = NOT_GROUP_TABLE;
        continue;
      }
  
        /*
          search for the MEMBER_XTENSION, MEMBER_NAME, MEMBER_VERSION,
      MEMBER_POSITION, MEMBER_LOCATION and MEMBER_URI_TYPE columns
      and determine their column index ID
        */
  
        *status = fits_get_colnum(gfptr,CASESEN,"MEMBER_XTENSION",xtensionCol,
                  status);
  
        if(*status == COL_NOT_FOUND)
      {
        *status      = 0;
         *xtensionCol = 0;
      }
  
        if(*status != 0) continue;
  
        *status = fits_get_colnum(gfptr,CASESEN,"MEMBER_NAME",extnameCol,status);
  
        if(*status == COL_NOT_FOUND)
      {
        *status     = 0;
        *extnameCol = 0;
      }
  
        if(*status != 0) continue;
  
        *status = fits_get_colnum(gfptr,CASESEN,"MEMBER_VERSION",extverCol,
                  status);
  
        if(*status == COL_NOT_FOUND)
      {
        *status    = 0;
        *extverCol = 0;
      }
  
        if(*status != 0) continue;
  
        *status = fits_get_colnum(gfptr,CASESEN,"MEMBER_POSITION",positionCol,
                  status);
  
        if(*status == COL_NOT_FOUND)
      {
        *status      = 0;
        *positionCol = 0;
      }
  
        if(*status != 0) continue;
  
        *status = fits_get_colnum(gfptr,CASESEN,"MEMBER_LOCATION",locationCol,
                  status);
  
        if(*status == COL_NOT_FOUND)
      {
        *status      = 0;
        *locationCol = 0;
      }
  
        if(*status != 0) continue;
  
        *status = fits_get_colnum(gfptr,CASESEN,"MEMBER_URI_TYPE",uriCol,
                  status);
  
        if(*status == COL_NOT_FOUND)
      {
        *status = 0;
        *uriCol = 0;
      }
  
        if(*status != 0) continue;
  
        /*
       determine the type of grouping table structure used by this
       grouping table and record it in the grptype parameter
        */
  
        if(*xtensionCol && *extnameCol && *extverCol && *positionCol &&
       *locationCol && *uriCol)
      *grptype = GT_ID_ALL_URI;
  
        else if(*xtensionCol && *extnameCol && *extverCol &&
            *locationCol && *uriCol)
      *grptype = GT_ID_REF_URI;
  
        else if(*xtensionCol && *extnameCol && *extverCol && *positionCol)
      *grptype = GT_ID_ALL;
  
        else if(*xtensionCol && *extnameCol && *extverCol)
      *grptype = GT_ID_REF;
  
        else if(*positionCol && *locationCol && *uriCol)
      *grptype = GT_ID_POS_URI;
  
        else if(*positionCol)
      *grptype = GT_ID_POS;
  
        else
      *status = NOT_GROUP_TABLE;
  
      }while(0);
  
    /*
      if the table contained more than one column with a reserved name then
      this cannot be considered a vailid grouping table
    */
  
    if(*status == COL_NOT_UNIQUE)
      {
        *status = NOT_GROUP_TABLE;
        ffpmsg("Specified HDU has multipule Group table cols defined (ffgtgc)");
      }
  
    return(*status);
  }
  
  /*****************************************************************************/
  int ffvcfm(fitsfile *gfptr, int xtensionCol, int extnameCol, int extverCol,
         int positionCol, int locationCol, int uriCol, int *status)
  {
  /*
     Perform validation on column formats to ensure this matches the grouping
     format the get functions expect.  Particularly want to check widths of
     string columns.
  */
  
     int typecode=0;
     long repeat=0, width=0;
  
     if (*status != 0) return (*status);
  
     do {
         if (xtensionCol)
         {
            fits_get_coltype(gfptr, xtensionCol, &typecode, &repeat, &width, status);
            if (*status || typecode != TSTRING || repeat != width || repeat > 8)
            {
               if (*status==0) *status=NOT_GROUP_TABLE;
               ffpmsg("Wrong format for Grouping xtension col. (ffvcfm)");
               continue;
            }
         }
         if (extnameCol)
         {
            fits_get_coltype(gfptr, extnameCol, &typecode, &repeat, &width, status);
            if (*status || typecode != TSTRING || repeat != width || repeat > 32)
            {
               if (*status==0) *status=NOT_GROUP_TABLE;
               ffpmsg("Wrong format for Grouping name col. (ffvcfm)");
               continue;
            }
         }
         if (extverCol)
         {
            fits_get_coltype(gfptr, extverCol, &typecode, &repeat, &width, status);
            if (*status || typecode != TINT32BIT ||  repeat > 1)
            {
               if (*status==0) *status=NOT_GROUP_TABLE;
               ffpmsg("Wrong format for Grouping version col. (ffvcfm)");
               continue;
            }
         }
         if (positionCol)
         {
            fits_get_coltype(gfptr, positionCol, &typecode, &repeat, &width, status);
            if (*status || typecode != TINT32BIT ||  repeat > 1)
            {
               if (*status==0) *status=NOT_GROUP_TABLE;
               ffpmsg("Wrong format for Grouping position col. (ffvcfm)");
               continue;
            }
         }
         if (locationCol)
         {
            fits_get_coltype(gfptr, locationCol, &typecode, &repeat, &width, status);
            if (*status || typecode != TSTRING || repeat != width || repeat > 256)
            {
               if (*status==0) *status=NOT_GROUP_TABLE;
               ffpmsg("Wrong format for Grouping location col. (ffvcfm)");
               continue;
            }
         }
         if (uriCol)
         {
            fits_get_coltype(gfptr, uriCol, &typecode, &repeat, &width, status);
            if (*status || typecode != TSTRING || repeat != width || repeat > 3)
            {
               if (*status==0) *status=NOT_GROUP_TABLE;
               ffpmsg("Wrong format for Grouping URI col. (ffvcfm)");
               continue;
            }
         }
     } while (0);
     return (*status);
  }
  
  
  /*****************************************************************************/
  int ffgtdc(int   grouptype,     /* code specifying the type of
                     grouping table information:
                     GT_ID_ALL_URI  0 ==> defualt (all columns)
                     GT_ID_REF      1 ==> ID by reference
                     GT_ID_POS      2 ==> ID by position
                     GT_ID_ALL      3 ==> ID by ref. and position
                     GT_ID_REF_URI 11 ==> (1) + URI info
                     GT_ID_POS_URI 12 ==> (2) + URI info       */
         int   xtensioncol, /* does MEMBER_XTENSION already exist?         */
         int   extnamecol,  /* does MEMBER_NAME aleady exist?              */
         int   extvercol,   /* does MEMBER_VERSION already exist?          */
         int   positioncol, /* does MEMBER_POSITION already exist?         */
         int   locationcol, /* does MEMBER_LOCATION already exist?         */
         int   uricol,      /* does MEMBER_URI_TYPE aleardy exist?         */
         char *ttype[],     /* array of grouping table column TTYPE names
                   to define (if *col var false)               */
         char *tform[],     /* array of grouping table column TFORM values
                   to define (if*col variable false)           */
         int  *ncols,       /* number of TTYPE and TFORM values returned   */
         int  *status)      /* return status code                          */
  
  /*
    create the TTYPE and TFORM values for the grouping table according to the
    value of the grouptype parameter and the values of the *col flags. The
    resulting TTYPE and TFORM are returned in ttype[] and tform[] respectively.
    The number of TTYPE and TFORMs returned is given by ncols. Both the TTYPE[]
    and TTFORM[] arrays must contain enough pre-allocated strings to hold
    the returned information.
  */
  
  {
  
    int i = 0;
  
    char  xtension[]  = "MEMBER_XTENSION";
    char  xtenTform[] = "8A";
  
    char  name[]      = "MEMBER_NAME";
    char  nameTform[] = "32A";
  
    char  version[]   = "MEMBER_VERSION";
    char  verTform[]  = "1J";
  
    char  position[]  = "MEMBER_POSITION";
    char  posTform[]  = "1J";
  
    char  URI[]       = "MEMBER_URI_TYPE";
    char  URITform[]  = "3A";
  
    char  location[]  = "MEMBER_LOCATION";
    /* SPR 01720, move from 160A to 256A */
    char  locTform[]  = "256A";
  
  
    if(*status != 0) return(*status);
  
    switch(grouptype)
      {
  
      case GT_ID_ALL_URI:
  
        if(xtensioncol == 0)
      {
        strcpy(ttype[i],xtension);
        strcpy(tform[i],xtenTform);
        ++i;
      }
        if(extnamecol == 0)
      {
        strcpy(ttype[i],name);
        strcpy(tform[i],nameTform);
        ++i;
      }
        if(extvercol == 0)
      {
        strcpy(ttype[i],version);
        strcpy(tform[i],verTform);
        ++i;
      }
        if(positioncol == 0)
      {
        strcpy(ttype[i],position);
        strcpy(tform[i],posTform);
        ++i;
      }
        if(locationcol == 0)
      {
        strcpy(ttype[i],location);
        strcpy(tform[i],locTform);
        ++i;
      }
        if(uricol == 0)
      {
        strcpy(ttype[i],URI);
        strcpy(tform[i],URITform);
        ++i;
      }
        break;
  
      case GT_ID_REF:
  
        if(xtensioncol == 0)
      {
        strcpy(ttype[i],xtension);
        strcpy(tform[i],xtenTform);
        ++i;
      }
        if(extnamecol == 0)
      {
        strcpy(ttype[i],name);
        strcpy(tform[i],nameTform);
        ++i;
      }
        if(extvercol == 0)
      {
        strcpy(ttype[i],version);
        strcpy(tform[i],verTform);
        ++i;
      }
        break;
  
      case GT_ID_POS:
  
        if(positioncol == 0)
      {
        strcpy(ttype[i],position);
        strcpy(tform[i],posTform);
        ++i;
      }
        break;
  
      case GT_ID_ALL:
  
        if(xtensioncol == 0)
      {
        strcpy(ttype[i],xtension);
        strcpy(tform[i],xtenTform);
        ++i;
      }
        if(extnamecol == 0)
      {
        strcpy(ttype[i],name);
        strcpy(tform[i],nameTform);
        ++i;
      }
        if(extvercol == 0)
      {
        strcpy(ttype[i],version);
        strcpy(tform[i],verTform);
        ++i;
      }
        if(positioncol == 0)
      {
        strcpy(ttype[i],position);
        strcpy(tform[i], posTform);
        ++i;
      }
  
        break;
  
      case GT_ID_REF_URI:
  
        if(xtensioncol == 0)
      {
        strcpy(ttype[i],xtension);
        strcpy(tform[i],xtenTform);
        ++i;
      }
        if(extnamecol == 0)
      {
        strcpy(ttype[i],name);
        strcpy(tform[i],nameTform);
        ++i;
      }
        if(extvercol == 0)
      {
        strcpy(ttype[i],version);
        strcpy(tform[i],verTform);
        ++i;
      }
        if(locationcol == 0)
      {
        strcpy(ttype[i],location);
        strcpy(tform[i],locTform);
        ++i;
      }
        if(uricol == 0)
      {
        strcpy(ttype[i],URI);
        strcpy(tform[i],URITform);
        ++i;
      }
        break;
  
      case GT_ID_POS_URI:
  
        if(positioncol == 0)
      {
        strcpy(ttype[i],position);
        strcpy(tform[i],posTform);
        ++i;
      }
        if(locationcol == 0)
      {
        strcpy(ttype[i],location);
        strcpy(tform[i],locTform);
        ++i;
      }
        if(uricol == 0)
      {
        strcpy(ttype[i],URI);
        strcpy(tform[i],URITform);
        ++i;
      }
        break;
  
      default:
  
        *status = BAD_OPTION;
        ffpmsg("Invalid value specified for the grouptype parameter (ffgtdc)");
  
        break;
  
      }
  
    *ncols = i;
  
    return(*status);
  }
  
  /*****************************************************************************/
  int ffgmul(fitsfile *mfptr,   /* pointer to the grouping table member HDU    */
             int       rmopt,   /* 0 ==> leave GRPIDn/GRPLCn keywords,
                   1 ==> remove GRPIDn/GRPLCn keywords         */
         int      *status) /* return status code                          */
  
  /*
     examine all the GRPIDn and GRPLCn keywords in the member HDUs header
     and remove the member from the grouping tables referenced; This
     effectively "unlinks" the member from all of its groups. The rmopt
     specifies if the GRPIDn/GRPLCn keywords are to be removed from the
     member HDUs header after the unlinking.
  */
  
  {
    int memberPosition = 0;
    int iomode;
  
    long index;
    long ngroups      = 0;
    long memberExtver = 0;
    long memberID     = 0;
  
    char mbrLocation1[FLEN_FILENAME];
    char mbrLocation2[FLEN_FILENAME];
    char memberHDUtype[FLEN_VALUE];
    char memberExtname[FLEN_VALUE];
    char keyword[FLEN_KEYWORD];
    char card[FLEN_CARD];
  
    fitsfile *gfptr = NULL;
  
  
    if(*status != 0) return(*status);
  
    do
      {
        /*
       determine location parameters of the member HDU; note that
       default values are supplied if the expected keywords are not
       found
        */
  
        *status = fits_read_key_str(mfptr,"XTENSION",memberHDUtype,card,status);
  
        if(*status == KEY_NO_EXIST)
      {
        strcpy(memberHDUtype,"PRIMARY");
        *status = 0;
      }
        prepare_keyvalue(memberHDUtype);
  
        *status = fits_read_key_lng(mfptr,"EXTVER",&memberExtver,card,status);
  
        if(*status == KEY_NO_EXIST)
      {
        memberExtver = 1;
        *status      = 0;
      }
  
        *status = fits_read_key_str(mfptr,"EXTNAME",memberExtname,card,status);
  
        if(*status == KEY_NO_EXIST)
      {
        memberExtname[0] = 0;
        *status          = 0;
      }
        prepare_keyvalue(memberExtname);
  
        fits_get_hdu_num(mfptr,&memberPosition);
  
        *status = fits_get_url(mfptr,mbrLocation1,mbrLocation2,NULL,NULL,
                   NULL,status);
  
        if(*status != 0) continue;
  
        /*
       open each grouping table linked to this HDU and remove the member
       from the grouping tables
        */
  
        *status = fits_get_num_groups(mfptr,&ngroups,status);
  
        /* loop over each group linked to the member HDU */
  
        for(index = 1; index <= ngroups && *status == 0; ++index)
      {
        /* open the (index)th group linked to the member HDU */
  
        *status = fits_open_group(mfptr,index,&gfptr,status);
  
        /* if the group could not be opened then just skip it */
  
        if(*status != 0)
          {
            *status = 0;
            snprintf(card,FLEN_CARD,"Cannot open the %dth group table (ffgmul)",
                (int)index);
            ffpmsg(card);
            continue;
          }
  
        /*
          make sure the grouping table can be modified before proceeding
        */
  
        fits_file_mode(gfptr,&iomode,status);
  
        if(iomode != READWRITE)
          {
            snprintf(card,FLEN_CARD,"The %dth group cannot be modified (ffgtam)",
                (int)index);
            ffpmsg(card);
            continue;
          }
  
        /*
           try to find the member's row within the grouping table; first
           try using the member HDU file's "real" URL string then try
           using its originally opened URL string if either string exist
         */
  
        memberID = 0;
  
        if(strlen(mbrLocation1) != 0)
          {
            *status = ffgmf(gfptr,memberHDUtype,memberExtname,memberExtver,
                    memberPosition,mbrLocation1,&memberID,status);
          }
  
        if(*status == MEMBER_NOT_FOUND && strlen(mbrLocation2) != 0)
          {
            *status = 0;
            *status = ffgmf(gfptr,memberHDUtype,memberExtname,memberExtver,
                    memberPosition,mbrLocation2,&memberID,status);
          }
  
        /* if the member was found then delete it from the grouping table */
  
        if(*status == 0)
          *status = fits_delete_rows(gfptr,memberID,1,status);
  
        /*
           continue the loop over all member groups even if an error
           was generated
        */
  
        if(*status == MEMBER_NOT_FOUND)
          {
            ffpmsg("cannot locate member's entry in group table (ffgmul)");
          }
        *status = 0;
  
        /*
           close the file pointed to by gfptr if it is non NULL to
           prepare for the next loop iterration
        */
  
        if(gfptr != NULL)
          {
            fits_close_file(gfptr,status);
            gfptr = NULL;
          }
      }
  
        if(*status != 0) continue;
  
        /*
       if rmopt is non-zero then find and delete the GRPIDn/GRPLCn
       keywords from the member HDU header
        */
  
        if(rmopt != 0)
      {
        fits_file_mode(mfptr,&iomode,status);
  
        if(iomode == READONLY)
          {
            ffpmsg("Cannot modify member HDU, opened READONLY (ffgmul)");
            continue;
          }
  
        /* delete all the GRPIDn/GRPLCn keywords */
  
        for(index = 1; index <= ngroups && *status == 0; ++index)
          {
            snprintf(keyword,FLEN_KEYWORD,"GRPID%d",(int)index);
            fits_delete_key(mfptr,keyword,status);
  
            snprintf(keyword,FLEN_KEYWORD,"GRPLC%d",(int)index);
            fits_delete_key(mfptr,keyword,status);
  
            if(*status == KEY_NO_EXIST) *status = 0;
          }
      }
      }while(0);
  
    /* make sure the gfptr has been closed */
  
    if(gfptr != NULL)
      {
        fits_close_file(gfptr,status);
      }
  
  return(*status);
  }
  
  /*--------------------------------------------------------------------------*/
  int ffgmf(fitsfile *gfptr, /* pointer to grouping table HDU to search       */
         char *xtension,  /* XTENSION value for member HDU                */
         char *extname,   /* EXTNAME value for member HDU                 */
         int   extver,    /* EXTVER value for member HDU                  */
         int   position,  /* HDU position value for member HDU            */
         char *location,  /* FITS file location value for member HDU      */
         long *member,    /* member HDU ID within group table (if found)  */
         int  *status)    /* return status code                           */
  
  /*
     try to find the entry for the member HDU defined by the xtension, extname,
     extver, position, and location parameters within the grouping table
     pointed to by gfptr. If the member HDU is found then its ID (row number)
     within the grouping table is returned in the member variable; if not
     found then member is returned with a value of 0 and the status return
     code will be set to MEMBER_NOT_FOUND.
  
     Note that the member HDU postion information is used to obtain a member
     match only if the grouping table type is GT_ID_POS_URI or GT_ID_POS. This
     is because the position information can become invalid much more
     easily then the reference information for a group member.
  */
  
  {
    int xtensionCol,extnameCol,extverCol,positionCol,locationCol,uriCol;
    int mposition = 0;
    int grptype;
    int dummy;
    int i;
  
    long nmembers = 0;
    long mextver  = 0;
  
    char  charBuff1[FLEN_FILENAME];
    char  charBuff2[FLEN_FILENAME];
    char  tmpLocation[FLEN_FILENAME];
    char  mbrLocation1[FLEN_FILENAME];
    char  mbrLocation2[FLEN_FILENAME];
    char  mbrLocation3[FLEN_FILENAME];
    char  grpLocation1[FLEN_FILENAME];
    char  grpLocation2[FLEN_FILENAME];
    char  cwd[FLEN_FILENAME];
  
    char  nstr[] = {'\0'};
    char *tmpPtr[2];
  
    if(*status != 0) return(*status);
  
    *member = 0;
  
    tmpPtr[0] = charBuff1;
    tmpPtr[1] = charBuff2;
  
  
    if(*status != 0) return(*status);
  
    /*
      if the passed LOCATION value is not an absolute URL then turn it
      into an absolute path
    */
  
    if(location == NULL)
      {
        *tmpLocation = 0;
      }
  
    else if(*location == 0)
      {
        *tmpLocation = 0;
      }
  
    else if(!fits_is_url_absolute(location))
      {
        fits_path2url(location,FLEN_FILENAME,tmpLocation,status);
  
        if(*tmpLocation != '/')
      {
        fits_get_cwd(cwd,status);
            if (strlen(cwd)+strlen(tmpLocation)+1 >
                      FLEN_FILENAME-1)
            {
               ffpmsg("cwd and location are too long (ffgmf)");
               return (*status = URL_PARSE_ERROR);
            }
        strcat(cwd,"/");
        strcat(cwd,tmpLocation);
        fits_clean_url(cwd,tmpLocation,status);
      }
      }
  
    else
      strcpy(tmpLocation,location);
  
    /*
       retrieve the Grouping Convention reserved column positions within
       the grouping table
    */
  
    *status = ffgtgc(gfptr,&xtensionCol,&extnameCol,&extverCol,&positionCol,
             &locationCol,&uriCol,&grptype,status);
  
    /* retrieve the number of group members */
  
    *status = fits_get_num_members(gfptr,&nmembers,status);
  
    /*
       loop over all grouping table rows until the member HDU is found
    */
  
    for(i = 1; i <= nmembers && *member == 0 && *status == 0; ++i)
      {
        if(xtensionCol != 0)
      {
        fits_read_col_str(gfptr,xtensionCol,i,1,1,nstr,tmpPtr,&dummy,status);
        if(fits_strcasecmp(tmpPtr[0],xtension) != 0) continue;
      }
  
        if(extnameCol  != 0)
      {
        fits_read_col_str(gfptr,extnameCol,i,1,1,nstr,tmpPtr,&dummy,status);
        if(fits_strcasecmp(tmpPtr[0],extname) != 0) continue;
      }
  
        if(extverCol   != 0)
      {
        fits_read_col_lng(gfptr,extverCol,i,1,1,0,
                  (long*)&mextver,&dummy,status);
        if(extver != mextver) continue;
      }
  
        /* note we only use postionCol if we have to */
  
        if(positionCol != 0 &&
                  (grptype == GT_ID_POS || grptype == GT_ID_POS_URI))
      {
        fits_read_col_int(gfptr,positionCol,i,1,1,0,
                  &mposition,&dummy,status);
        if(position != mposition) continue;
      }
  
        /*
      if no location string was passed to the function then assume that
      the calling application does not wish to use it as a comparision
      critera ==> if we got this far then we have a match
        */
  
        if(location == NULL)
      {
        ffpmsg("NULL Location string given ==> ingore location (ffgmf)");
        *member = i;
        continue;
      }
  
        /*
      if the grouping table MEMBER_LOCATION column exists then read the
      location URL for the member, else set the location string to
      a zero-length string for subsequent comparisions
        */
  
        if(locationCol != 0)
      {
        fits_read_col_str(gfptr,locationCol,i,1,1,nstr,tmpPtr,&dummy,status);
        strcpy(mbrLocation1,tmpPtr[0]);
        *mbrLocation2 = 0;
      }
        else
      *mbrLocation1 = 0;
  
        /*
       if the member location string from the grouping table is zero
       length (either implicitly or explicitly) then assume that the
       member HDU is in the same file as the grouping table HDU; retrieve
       the possible URL values of the grouping table HDU file
         */
  
        if(*mbrLocation1 == 0)
      {
        /* retrieve the possible URLs of the grouping table file */
        *status = fits_get_url(gfptr,mbrLocation1,mbrLocation2,NULL,NULL,
                   NULL,status);
  
        /* if non-NULL, make sure the first URL is absolute or a full path */
        if(*mbrLocation1 != 0 && !fits_is_url_absolute(mbrLocation1) &&
           *mbrLocation1 != '/')
          {
            fits_get_cwd(cwd,status);
                if (strlen(cwd)+strlen(mbrLocation1)+1 >
                          FLEN_FILENAME-1)
                {
                   ffpmsg("cwd and member locations are too long (ffgmf)");
                   *status = URL_PARSE_ERROR;
                   continue;
                }
            strcat(cwd,"/");
            strcat(cwd,mbrLocation1);
            fits_clean_url(cwd,mbrLocation1,status);
          }
  
        /* if non-NULL, make sure the first URL is absolute or a full path */
        if(*mbrLocation2 != 0 && !fits_is_url_absolute(mbrLocation2) &&
           *mbrLocation2 != '/')
          {
            fits_get_cwd(cwd,status);
                if (strlen(cwd)+strlen(mbrLocation2)+1 >
                          FLEN_FILENAME-1)
                {
                   ffpmsg("cwd and member locations are too long (ffgmf)");
                   *status = URL_PARSE_ERROR;
                   continue;
                }
            strcat(cwd,"/");
            strcat(cwd,mbrLocation2);
            fits_clean_url(cwd,mbrLocation2,status);
          }
      }
  
        /*
      if the member location was specified, then make sure that it is
      either an absolute URL or specifies a full path
        */
  
        else if(!fits_is_url_absolute(mbrLocation1) && *mbrLocation1 != '/')
      {
        strcpy(mbrLocation2,mbrLocation1);
  
        /* get the possible URLs for the grouping table file */
        *status = fits_get_url(gfptr,grpLocation1,grpLocation2,NULL,NULL,
                   NULL,status);
  
        if(*grpLocation1 != 0)
          {
            /* make sure the first grouping table URL is absolute */
            if(!fits_is_url_absolute(grpLocation1) && *grpLocation1 != '/')
          {
            fits_get_cwd(cwd,status);
                    if (strlen(cwd)+strlen(grpLocation1)+1 >
                              FLEN_FILENAME-1)
                    {
                       ffpmsg("cwd and group locations are too long (ffgmf)");
                       *status = URL_PARSE_ERROR;
                       continue;
                    }
            strcat(cwd,"/");
            strcat(cwd,grpLocation1);
            fits_clean_url(cwd,grpLocation1,status);
          }
  
            /* create an absoute URL for the member */
  
            fits_relurl2url(grpLocation1,mbrLocation1,mbrLocation3,status);
  
            /*
           if URL construction succeeded then copy it to the
           first location string; else set the location string to
           empty
            */
  
            if(*status == 0)
          {
            strcpy(mbrLocation1,mbrLocation3);
          }
  
            else if(*status == URL_PARSE_ERROR)
          {
            *status       = 0;
            *mbrLocation1 = 0;
          }
          }
        else
          *mbrLocation1 = 0;
  
        if(*grpLocation2 != 0)
          {
            /* make sure the second grouping table URL is absolute */
            if(!fits_is_url_absolute(grpLocation2) && *grpLocation2 != '/')
          {
            fits_get_cwd(cwd,status);
                    if (strlen(cwd)+strlen(grpLocation2)+1 >
                              FLEN_FILENAME-1)
                    {
                       ffpmsg("cwd and group locations are too long (ffgmf)");
                       *status = URL_PARSE_ERROR;
                       continue;
                    }
            strcat(cwd,"/");
            strcat(cwd,grpLocation2);
            fits_clean_url(cwd,grpLocation2,status);
          }
  
            /* create an absolute URL for the member */
  
            fits_relurl2url(grpLocation2,mbrLocation2,mbrLocation3,status);
  
            /*
           if URL construction succeeded then copy it to the
           second location string; else set the location string to
           empty
            */
  
            if(*status == 0)
          {
            strcpy(mbrLocation2,mbrLocation3);
          }
  
            else if(*status == URL_PARSE_ERROR)
          {
            *status       = 0;
            *mbrLocation2 = 0;
          }
          }
        else
          *mbrLocation2 = 0;
      }
  
        /*
      compare the passed member HDU file location string with the
      (possibly two) member location strings to see if there is a match
         */
  
        if(strcmp(mbrLocation1,tmpLocation) != 0 &&
       strcmp(mbrLocation2,tmpLocation) != 0   ) continue;
  
        /* if we made it this far then a match to the member HDU was found */
  
        *member = i;
      }
  
    /* if a match was not found then set the return status code */
  
    if(*member == 0 && *status == 0)
      {
        *status = MEMBER_NOT_FOUND;
        ffpmsg("Cannot find specified member HDU (ffgmf)");
      }
  
    return(*status);
  }
  
  /*--------------------------------------------------------------------------
                          Recursive Group Functions
    --------------------------------------------------------------------------*/
  int ffgtrmr(fitsfile   *gfptr,  /* FITS file pointer to group               */
          HDUtracker *HDU,    /* list of processed HDUs                   */
          int        *status) /* return status code                       */
  
  /*
    recursively remove a grouping table and all its members. Each member of
    the grouping table pointed to by gfptr it processed. If the member is itself
    a grouping table then ffgtrmr() is recursively called to process all
    of its members. The HDUtracker struct *HDU is used to make sure a member
    is not processed twice, thus avoiding an infinite loop (e.g., a grouping
    table contains itself as a member).
  */
  
  {
    int i;
    int hdutype;
  
    long nmembers = 0;
  
    char keyvalue[FLEN_VALUE];
    char comment[FLEN_COMMENT];
  
    fitsfile *mfptr = NULL;
  
  
    if(*status != 0) return(*status);
  
    /* get the number of members contained by this grouping table */
  
    *status = fits_get_num_members(gfptr,&nmembers,status);
  
    /* loop over all group members and delete them */
  
    for(i = nmembers; i > 0 && *status == 0; --i)
      {
        /* open the member HDU */
  
        *status = fits_open_member(gfptr,i,&mfptr,status);
  
        /* if the member cannot be opened then just skip it and continue */
  
        if(*status == MEMBER_NOT_FOUND)
      {
        *status = 0;
        continue;
      }
  
        /* Any other error is a reason to abort */
  
        if(*status != 0) continue;
  
        /* add the member HDU to the HDUtracker struct */
  
        *status = fftsad(mfptr,HDU,NULL,NULL);
  
        /* status == HDU_ALREADY_TRACKED ==> HDU has already been processed */
  
        if(*status == HDU_ALREADY_TRACKED)
      {
        *status = 0;
        fits_close_file(mfptr,status);
        continue;
      }
        else if(*status != 0) continue;
  
        /* determine if the member HDU is itself a grouping table */
  
        *status = fits_read_key_str(mfptr,"EXTNAME",keyvalue,comment,status);
  
        /* if no EXTNAME is found then the HDU cannot be a grouping table */
  
        if(*status == KEY_NO_EXIST)
      {
        *status     = 0;
        keyvalue[0] = 0;
      }
        prepare_keyvalue(keyvalue);
  
        /* Any other error is a reason to abort */
  
        if(*status != 0) continue;
  
        /*
       if the EXTNAME == GROUPING then the member is a grouping table
       and we must call ffgtrmr() to process its members
        */
  
        if(fits_strcasecmp(keyvalue,"GROUPING") == 0)
        *status = ffgtrmr(mfptr,HDU,status);
  
        /*
       unlink all the grouping tables that contain this HDU as a member
       and then delete the HDU (if not a PHDU)
        */
  
        if(fits_get_hdu_num(mfptr,&hdutype) == 1)
            *status = ffgmul(mfptr,1,status);
        else
        {
            *status = ffgmul(mfptr,0,status);
            *status = fits_delete_hdu(mfptr,&hdutype,status);
        }
  
        /* close the fitsfile pointer */
  
        fits_close_file(mfptr,status);
      }
  
    return(*status);
  }
  /*--------------------------------------------------------------------------*/
  int ffgtcpr(fitsfile   *infptr,  /* input FITS file pointer                 */
          fitsfile   *outfptr, /* output FITS file pointer                */
          int         cpopt,   /* code specifying copy options:
                      OPT_GCP_GPT (0) ==> cp only grouping table
                      OPT_GCP_ALL (2) ==> recusrively copy
                      members and their members (if groups)   */
          HDUtracker *HDU,     /* list of already copied HDUs             */
          int        *status)  /* return status code                      */
  
  /*
    copy a Group to a new FITS file. If the cpopt parameter is set to
    OPT_GCP_GPT (copy grouping table only) then the existing members have their
    GRPIDn and GRPLCn keywords updated to reflect the existance of the new group,
    since they now belong to another group. If cpopt is set to OPT_GCP_ALL
    (copy grouping table and members recursively) then the original members are
    not updated; the new grouping table is modified to include only the copied
    member HDUs and not the original members.
  
    Note that this function is recursive. When copt is OPT_GCP_ALL it will call
    itself whenever a member HDU of the current grouping table is itself a
    grouping table (i.e., EXTNAME = 'GROUPING').
  */
  
  {
  
    int i;
    int nexclude     = 8;
    int hdutype      = 0;
    int groupHDUnum  = 0;
    int numkeys      = 0;
    int keypos       = 0;
    int startSearch  = 0;
    int newPosition  = 0;
  
    long nmembers    = 0;
    long tfields     = 0;
    long newTfields  = 0;
  
    char keyword[FLEN_KEYWORD];
    char keyvalue[FLEN_VALUE];
    char card[FLEN_CARD];
    char comment[FLEN_CARD];
    char *tkeyvalue;
  
    char *includeList[] = {"*"};
    char *excludeList[] = {"EXTNAME","EXTVER","GRPNAME","GRPID#","GRPLC#",
               "THEAP","TDIM#","T????#"};
  
    fitsfile *mfptr = NULL;
  
  
    if(*status != 0) return(*status);
  
    do
      {
        /*
      create a new grouping table in the FITS file pointed to by outptr
        */
  
        *status = fits_get_num_members(infptr,&nmembers,status);
  
        *status = fits_read_key_str(infptr,"GRPNAME",keyvalue,card,status);
  
        if(*status == KEY_NO_EXIST)
      {
        keyvalue[0] = 0;
        *status     = 0;
      }
        prepare_keyvalue(keyvalue);
  
        *status = fits_create_group(outfptr,keyvalue,GT_ID_ALL_URI,status);
  
        /* save the new grouping table's HDU position for future use */
  
        fits_get_hdu_num(outfptr,&groupHDUnum);
  
        /* update the HDUtracker struct with the grouping table's new position */
  
        *status = fftsud(infptr,HDU,groupHDUnum,NULL);
  
        /*
      Now populate the copied grouping table depending upon the
      copy option parameter value
        */
  
        switch(cpopt)
      {
  
        /*
          for the "copy grouping table only" option we only have to
          add the members of the original grouping table to the new
          grouping table
        */
  
      case OPT_GCP_GPT:
  
        for(i = 1; i <= nmembers && *status == 0; ++i)
          {
            *status = fits_open_member(infptr,i,&mfptr,status);
            *status = fits_add_group_member(outfptr,mfptr,0,status);
  
            fits_close_file(mfptr,status);
            mfptr = NULL;
          }
  
        break;
  
      case OPT_GCP_ALL:
  
        /*
          for the "copy the entire group" option
         */
  
        /* loop over all the grouping table members */
  
        for(i = 1; i <= nmembers && *status == 0; ++i)
          {
            /* open the ith member */
  
            *status = fits_open_member(infptr,i,&mfptr,status);
  
            if(*status != 0) continue;
  
            /* add it to the HDUtracker struct */
  
            *status = fftsad(mfptr,HDU,&newPosition,NULL);
  
            /* if already copied then just add the member to the group */
  
            if(*status == HDU_ALREADY_TRACKED)
          {
            *status = 0;
            *status = fits_add_group_member(outfptr,NULL,newPosition,
                            status);
            fits_close_file(mfptr,status);
                    mfptr = NULL;
            continue;
          }
            else if(*status != 0) continue;
  
            /* see if the member is a grouping table */
  
            *status = fits_read_key_str(mfptr,"EXTNAME",keyvalue,card,
                        status);
  
            if(*status == KEY_NO_EXIST)
          {
            keyvalue[0] = 0;
            *status     = 0;
          }
            prepare_keyvalue(keyvalue);
  
            /*
          if the member is a grouping table then copy it and all of
          its members using ffgtcpr(), else copy it using
          fits_copy_member(); the outptr will point to the newly
          copied member upon return from both functions
            */
  
            if(fits_strcasecmp(keyvalue,"GROUPING") == 0)
          *status = ffgtcpr(mfptr,outfptr,OPT_GCP_ALL,HDU,status);
            else
          *status = fits_copy_member(infptr,outfptr,i,OPT_MCP_NADD,
                         status);
  
            /* retrieve the position of the newly copied member */
  
            fits_get_hdu_num(outfptr,&newPosition);
  
            /* update the HDUtracker struct with member's new position */
  
            if(fits_strcasecmp(keyvalue,"GROUPING") != 0)
          *status = fftsud(mfptr,HDU,newPosition,NULL);
  
            /* move the outfptr back to the copied grouping table HDU */
  
            *status = fits_movabs_hdu(outfptr,groupHDUnum,&hdutype,status);
  
            /* add the copied member HDU to the copied grouping table */
  
            *status = fits_add_group_member(outfptr,NULL,newPosition,status);
  
            /* close the mfptr pointer */
  
            fits_close_file(mfptr,status);
            mfptr = NULL;
          }
  
        break;
  
      default:
  
        *status = BAD_OPTION;
        ffpmsg("Invalid value specified for cmopt parameter (ffgtcpr)");
        break;
      }
  
        if(*status != 0) continue;
  
        /*
       reposition the outfptr to the grouping table so that the grouping
       table is the CHDU upon return to the calling function
        */
  
        fits_movabs_hdu(outfptr,groupHDUnum,&hdutype,status);
  
        /*
       copy all auxiliary keyword records from the original grouping table
       to the new grouping table; they are copied in their original order
       and inserted just before the TTYPE1 keyword record
        */
  
        *status = fits_read_card(outfptr,"TTYPE1",card,status);
        *status = fits_get_hdrpos(outfptr,&numkeys,&keypos,status);
        --keypos;
  
        startSearch = 8;
  
        while(*status == 0)
      {
        ffgrec(infptr,startSearch,card,status);
  
        *status = fits_find_nextkey(infptr,includeList,1,excludeList,
                        nexclude,card,status);
  
        *status = fits_get_hdrpos(infptr,&numkeys,&startSearch,status);
  
        --startSearch;
        /* SPR 1738 */
        if (strncmp(card,"GRPLC",5)) {
          /* Not going to be a long string so we're ok */
          *status = fits_insert_record(outfptr,keypos,card,status);
        } else {
          /* We could have a long string */
          *status = fits_read_record(infptr,startSearch,card,status);
          card[9] = '\0';
          *status = fits_read_key_longstr(infptr,card,&tkeyvalue,comment,
                          status);
          if (0 == *status) {
            fits_insert_key_longstr(outfptr,card,tkeyvalue,comment,status);
            fits_write_key_longwarn(outfptr,status);
            free(tkeyvalue);
          }
        }
  
        ++keypos;
      }
  
  
        if(*status == KEY_NO_EXIST)
      *status = 0;
        else if(*status != 0) continue;
  
        /*
       search all the columns of the original grouping table and copy
       those to the new grouping table that were not part of the grouping
       convention. Note that is legal to have additional columns in a
       grouping table. Also note that the order of the columns may
       not be the same in the original and copied grouping table.
        */
  
        /* retrieve the number of columns in the original and new group tables */
  
        *status = fits_read_key_lng(infptr,"TFIELDS",&tfields,card,status);
        *status = fits_read_key_lng(outfptr,"TFIELDS",&newTfields,card,status);
  
        for(i = 1; i <= tfields; ++i)
      {
        snprintf(keyword,FLEN_KEYWORD,"TTYPE%d",i);
        *status = fits_read_key_str(infptr,keyword,keyvalue,card,status);
  
        if(*status == KEY_NO_EXIST)
          {
            *status = 0;
                keyvalue[0] = 0;
          }
        prepare_keyvalue(keyvalue);
  
        if(fits_strcasecmp(keyvalue,"MEMBER_XTENSION") != 0 &&
           fits_strcasecmp(keyvalue,"MEMBER_NAME")     != 0 &&
           fits_strcasecmp(keyvalue,"MEMBER_VERSION")  != 0 &&
           fits_strcasecmp(keyvalue,"MEMBER_POSITION") != 0 &&
           fits_strcasecmp(keyvalue,"MEMBER_LOCATION") != 0 &&
           fits_strcasecmp(keyvalue,"MEMBER_URI_TYPE") != 0   )
          {
  
            /* SPR 3956, add at the end of the table */
            *status = fits_copy_col(infptr,outfptr,i,newTfields+1,1,status);
            ++newTfields;
          }
      }
  
      }while(0);
  
    if(mfptr != NULL)
      {
        fits_close_file(mfptr,status);
      }
  
    return(*status);
  }
  
  /*--------------------------------------------------------------------------
                  HDUtracker struct manipulation functions
    --------------------------------------------------------------------------*/
  int fftsad(fitsfile   *mfptr,       /* pointer to an member HDU             */
         HDUtracker *HDU,         /* pointer to an HDU tracker struct     */
         int        *newPosition, /* new HDU position of the member HDU   */
         char       *newFileName) /* file containing member HDU           */
  
  /*
    add an HDU to the HDUtracker struct pointed to by HDU. The HDU is only
    added if it does not already reside in the HDUtracker. If it already
    resides in the HDUtracker then the new HDU postion and file name are
    returned in  newPosition and newFileName (if != NULL)
  */
  
  {
    int i;
    int hdunum;
    int status = 0;
  
    char filename1[FLEN_FILENAME];
    char filename2[FLEN_FILENAME];
  
    do
      {
        /* retrieve the HDU's position within the FITS file */
  
        fits_get_hdu_num(mfptr,&hdunum);
  
        /* retrieve the HDU's file name */
  
        status = fits_file_name(mfptr,filename1,&status);
  
        /* parse the file name and construct the "standard" URL for it */
  
        status = ffrtnm(filename1,filename2,&status);
  
        /*
       examine all the existing HDUs in the HDUtracker an see if this HDU
       has already been registered
        */
  
        for(i = 0;
         i < HDU->nHDU &&  !(HDU->position[i] == hdunum
                 && strcmp(HDU->filename[i],filename2) == 0);
        ++i);
  
        if(i != HDU->nHDU)
      {
        status = HDU_ALREADY_TRACKED;
        if(newPosition != NULL) *newPosition = HDU->newPosition[i];
        if(newFileName != NULL) strcpy(newFileName,HDU->newFilename[i]);
        continue;
      }
  
        if(HDU->nHDU == MAX_HDU_TRACKER)
      {
        status = TOO_MANY_HDUS_TRACKED;
        continue;
      }
  
        HDU->filename[i] = (char*) malloc(FLEN_FILENAME * sizeof(char));
  
        if(HDU->filename[i] == NULL)
      {
        status = MEMORY_ALLOCATION;
        continue;
      }
  
        HDU->newFilename[i] = (char*) malloc(FLEN_FILENAME * sizeof(char));
  
        if(HDU->newFilename[i] == NULL)
      {
        status = MEMORY_ALLOCATION;
        free(HDU->filename[i]);
        continue;
      }
  
        HDU->position[i]    = hdunum;
        HDU->newPosition[i] = hdunum;
  
        strcpy(HDU->filename[i],filename2);
        strcpy(HDU->newFilename[i],filename2);
  
         ++(HDU->nHDU);
  
      }while(0);
  
    return(status);
  }
  /*--------------------------------------------------------------------------*/
  int fftsud(fitsfile   *mfptr,       /* pointer to an member HDU             */
         HDUtracker *HDU,         /* pointer to an HDU tracker struct     */
         int         newPosition, /* new HDU position of the member HDU   */
         char       *newFileName) /* file containing member HDU           */
  
  /*
    update the HDU information in the HDUtracker struct pointed to by HDU. The
    HDU to update is pointed to by mfptr. If non-zero, the value of newPosition
    is used to update the HDU->newPosition[] value for the mfptr, and if
    non-NULL the newFileName value is used to update the HDU->newFilename[]
    value for mfptr.
  */
  
  {
    int i;
    int hdunum;
    int status = 0;
  
    char filename1[FLEN_FILENAME];
    char filename2[FLEN_FILENAME];
  
  
    /* retrieve the HDU's position within the FITS file */
  
    fits_get_hdu_num(mfptr,&hdunum);
  
    /* retrieve the HDU's file name */
  
    status = fits_file_name(mfptr,filename1,&status);
  
    /* parse the file name and construct the "standard" URL for it */
  
    status = ffrtnm(filename1,filename2,&status);
  
    /*
       examine all the existing HDUs in the HDUtracker an see if this HDU
       has already been registered
    */
  
    for(i = 0; i < HDU->nHDU &&
        !(HDU->position[i] == hdunum && strcmp(HDU->filename[i],filename2) == 0);
        ++i);
  
    /* if previously registered then change newPosition and newFileName */
  
    if(i != HDU->nHDU)
      {
        if(newPosition  != 0) HDU->newPosition[i] = newPosition;
        if(newFileName  != NULL)
      {
        strcpy(HDU->newFilename[i],newFileName);
      }
      }
    else
      status = MEMBER_NOT_FOUND;
  
    return(status);
  }
  
  /*---------------------------------------------------------------------------*/
  
  void prepare_keyvalue(char *keyvalue) /* string containing keyword value     */
  
  /*
    strip off all single quote characters "'" and blank spaces from a keyword
    value retrieved via fits_read_key*() routines
  
    this is necessary so that a standard comparision of keyword values may
    be made
  */
  
  {
  
    int i;
    int length;
  
    /*
      strip off any leading or trailing single quotes (`) and (') from
      the keyword value
    */
  
    length = strlen(keyvalue) - 1;
  
    if(keyvalue[0] == '\'' && keyvalue[length] == '\'')
      {
        for(i = 0; i < length - 1; ++i) keyvalue[i] = keyvalue[i+1];
        keyvalue[length-1] = 0;
      }
  
    /*
      strip off any trailing blanks from the keyword value; note that if the
      keyvalue consists of nothing but blanks then no blanks are stripped
    */
  
    length = strlen(keyvalue) - 1;
  
    for(i = 0; i < length && keyvalue[i] == ' '; ++i);
  
    if(i != length)
      {
        for(i = length; i >= 0 && keyvalue[i] == ' '; --i) keyvalue[i] = '\0';
      }
  }
  
  /*---------------------------------------------------------------------------
          Host dependent directory path to/from URL functions
    --------------------------------------------------------------------------*/
  int fits_path2url(char *inpath,  /* input file path string                  */
                    int maxlength, /* I max number of chars that can be written
                               to output, including terminating NULL */
            char *outpath, /* output file path string                 */
            int  *status)
    /*
       convert a file path into its Unix-style equivelent for URL
       purposes. Note that this process is platform dependent. This
       function supports Unix, MSDOS/WIN32, VMS and Macintosh platforms.
       The plaform dependant code is conditionally compiled depending upon
       the setting of the appropriate C preprocessor macros.
     */
  {
    char buff[FLEN_FILENAME];
  
  #if defined(WINNT) || defined(__WINNT__)
  
    /*
      Microsoft Windows NT case. We assume input file paths of the form:
  
      //disk/path/filename
  
       All path segments may be null, so that a single file name is the
       simplist case.
  
       The leading "//" becomes a single "/" if present. If no "//" is present,
       then make sure the resulting URL path is relative, i.e., does not
       begin with a "/". In other words, the only way that an absolute URL
       file path may be generated is if the drive specification is given.
    */
  
    if(*status > 0) return(*status);
  
    if(inpath[0] == '/')
      {
        strcpy(buff,inpath+1);
      }
    else
      {
        strcpy(buff,inpath);
      }
  
  #elif defined(MSDOS) || defined(__WIN32__) || defined(WIN32)
  
    /*
       MSDOS or Microsoft windows/NT case. The assumed form of the
       input path is:
  
       disk:\path\filename
  
       All path segments may be null, so that a single file name is the
       simplist case.
  
       All back-slashes '\' become slashes '/'; if the path starts with a
       string of the form "X:" then it is replaced with "/X/"
    */
  
    int i,j,k;
    int size;
    if(*status > 0) return(*status);
  
    for(i = 0, j = 0, size = strlen(inpath), buff[0] = 0;
                                             i < size; j = strlen(buff))
      {
        switch(inpath[i])
      {
  
      case ':':
  
        /*
           must be a disk desiginator; add a slash '/' at the start of
           outpath to designate that the path is absolute, then change
           the colon ':' to a slash '/'
         */
  
        for(k = j; k >= 0; --k) buff[k+1] = buff[k];
        buff[0] = '/';
        strcat(buff,"/");
        ++i;
  
        break;
  
      case '\\':
  
        /* just replace the '\' with a '/' IF its not the first character */
  
        if(i != 0 && buff[(j == 0 ? 0 : j-1)] != '/')
          {
            buff[j] = '/';
            buff[j+1] = 0;
          }
  
        ++i;
  
        break;
  
      default:
  
        /* copy the character from inpath to buff as is */
  
        buff[j]   = inpath[i];
        buff[j+1] = 0;
        ++i;
  
        break;
      }
      }
  
  #elif defined(VMS) || defined(vms) || defined(__vms)
  
    /*
       VMS case. Assumed format of the input path is:
  
       node::disk:[path]filename.ext;version
  
       Any part of the file path may be missing, so that in the simplist
       case a single file name/extension is given.
  
       all brackets "[", "]" and dots "." become "/"; dashes "-" become "..",
       all single colons ":" become ":/", all double colons "::" become
       "FILE://"
     */
  
    int i,j,k;
    int done;
    int size;
  
    if(*status > 0) return(*status);
  
    /* see if inpath contains a directory specification */
  
    if(strchr(inpath,']') == NULL)
      done = 1;
    else
      done = 0;
  
    for(i = 0, j = 0, size = strlen(inpath), buff[0] = 0;
                             i < size && j < FLEN_FILENAME - 8; j = strlen(buff))
      {
        switch(inpath[i])
      {
  
      case ':':
  
        /*
           must be a logical/symbol separator or (in the case of a double
           colon "::") machine node separator
         */
  
        if(inpath[i+1] == ':')
          {
            /* insert a "FILE://" at the start of buff ==> machine given */
  
            for(k = j; k >= 0; --k) buff[k+7] = buff[k];
            strncpy(buff,"FILE://",7);
            i += 2;
          }
        else if(strstr(buff,"FILE://") == NULL)
          {
            /* insert a "/" at the start of buff ==> absolute path */
  
            for(k = j; k >= 0; --k) buff[k+1] = buff[k];
            buff[0] = '/';
            ++i;
          }
        else
          ++i;
  
        /* a colon always ==> path separator */
  
        strcat(buff,"/");
  
        break;
  
      case ']':
  
        /* end of directory spec, file name spec begins after this */
  
        done = 1;
  
        buff[j]   = '/';
        buff[j+1] = 0;
        ++i;
  
        break;
  
      case '[':
  
        /*
           begin directory specification; add a '/' only if the last char
           is not '/'
        */
  
        if(i != 0 && buff[(j == 0 ? 0 : j-1)] != '/')
          {
            buff[j]   = '/';
            buff[j+1] = 0;
          }
  
        ++i;
  
        break;
  
      case '.':
  
        /*
           directory segment separator or file name/extension separator;
           we decide which by looking at the value of done
        */
  
        if(!done)
          {
          /* must be a directory segment separator */
            if(inpath[i-1] == '[')
          {
            strcat(buff,"./");
            ++j;
          }
            else
          buff[j] = '/';
          }
        else
          /* must be a filename/extension separator */
          buff[j] = '.';
  
        buff[j+1] = 0;
  
        ++i;
  
        break;
  
      case '-':
  
        /*
           a dash is the same as ".." in Unix speak, but lets make sure
           that its not part of the file name first!
         */
  
        if(!done)
          /* must be part of the directory path specification */
          strcat(buff,"..");
        else
          {
            /* the dash is part of the filename, so just copy it as is */
            buff[j] = '-';
            buff[j+1] = 0;
          }
  
        ++i;
  
        break;
  
      default:
  
        /* nothing special, just copy the character as is */
  
        buff[j]   = inpath[i];
        buff[j+1] = 0;
  
        ++i;
  
        break;
  
      }
      }
  
    if(j > FLEN_FILENAME - 8)
      {
        *status = URL_PARSE_ERROR;
        ffpmsg("resulting path to URL conversion too big (fits_path2url)");
      }
  
  #elif defined(macintosh)
  
    /*
       MacOS case. The assumed form of the input path is:
  
       disk:path:filename
  
       It is assumed that all paths are absolute with disk and path specified,
       unless no colons ":" are supplied with the string ==> a single file name
       only. All colons ":" become slashes "/", and if one or more colon is
       encountered then the path is specified as absolute.
    */
  
    int i,j,k;
    int firstColon;
    int size;
  
    if(*status > 0) return(*status);
  
    for(i = 0, j = 0, firstColon = 1, size = strlen(inpath), buff[0] = 0;
                                                     i < size; j = strlen(buff))
      {
        switch(inpath[i])
      {
  
      case ':':
  
        /*
           colons imply path separators. If its the first colon encountered
           then assume that its the disk designator and add a slash to the
           beginning of the buff string
         */
  
        if(firstColon)
          {
            firstColon = 0;
  
            for(k = j; k >= 0; --k) buff[k+1] = buff[k];
            buff[0] = '/';
          }
  
        /* all colons become slashes */
  
        strcat(buff,"/");
  
        ++i;
  
        break;
  
      default:
  
        /* copy the character from inpath to buff as is */
  
        buff[j]   = inpath[i];
        buff[j+1] = 0;
  
        ++i;
  
        break;
      }
      }
  
  #else
  
    /*
       Default Unix case.
  
       Nothing special to do here except to remove the double or more // and
       replace them with single /
     */
  
    int ii = 0;
    int jj = 0;
  
    if(*status > 0) return(*status);
  
    while (inpath[ii]) {
        if (inpath[ii] == '/' && inpath[ii+1] == '/') {
        /* do nothing */
        } else {
        buff[jj] = inpath[ii];
        jj++;
        }
        ii++;
    }
    buff[jj] = '\0';
    /* printf("buff is %s\ninpath is %s\n",buff,inpath); */
    /* strcpy(buff,inpath); */
  
  #endif
  
    /*
      encode all "unsafe" and "reserved" URL characters
    */
  
    *status = fits_encode_url(buff,maxlength,outpath,status);
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int fits_url2path(char *inpath,  /* input file path string  */
            char *outpath, /* output file path string */
            int  *status)
    /*
       convert a Unix-style URL into a platform dependent directory path.
       Note that this process is platform dependent. This
       function supports Unix, MSDOS/WIN32, VMS and Macintosh platforms. Each
       platform dependent code segment is conditionally compiled depending
       upon the setting of the appropriate C preprocesser macros.
     */
  {
    char buff[FLEN_FILENAME];
    int absolute;
  
  #if defined(MSDOS) || defined(__WIN32__) || defined(WIN32)
    char *tmpStr, *saveptr;
  #elif defined(VMS) || defined(vms) || defined(__vms)
    int i;
    char *tmpStr, *saveptr;
  #elif defined(macintosh)
    char *tmpStr, *saveptr;
  #endif
  
    if(*status != 0) return(*status);
  
    /*
      make a copy of the inpath so that we can manipulate it
    */
  
    strcpy(buff,inpath);
  
    /*
      convert any encoded characters to their unencoded values
    */
  
    *status = fits_unencode_url(inpath,buff,status);
  
    /*
      see if the URL is given as absolute w.r.t. the "local" file system
    */
  
    if(buff[0] == '/')
      absolute = 1;
    else
      absolute = 0;
  
  #if defined(WINNT) || defined(__WINNT__)
  
    /*
      Microsoft Windows NT case. We create output paths of the form
  
      //disk/path/filename
  
       All path segments but the last may be null, so that a single file name
       is the simplist case.
    */
  
    if(absolute)
      {
        strcpy(outpath,"/");
        strcat(outpath,buff);
      }
    else
      {
        strcpy(outpath,buff);
      }
  
  #elif defined(MSDOS) || defined(__WIN32__) || defined(WIN32)
  
    /*
       MSDOS or Microsoft windows/NT case. The output path will be of the
       form
  
       disk:\path\filename
  
       All path segments but the last may be null, so that a single file name
       is the simplist case.
    */
  
    /*
      separate the URL into tokens at each slash '/' and process until
      all tokens have been examined
    */
  
    for(tmpStr = ffstrtok(buff,"/",&saveptr), outpath[0] = 0;
                                   tmpStr != NULL; tmpStr = ffstrtok(NULL,"/",&saveptr))
      {
        strcat(outpath,tmpStr);
  
        /*
       if the absolute flag is set then process the token as a disk
       specification; else just process it as a directory path or filename
        */
  
        if(absolute)
      {
        strcat(outpath,":\\");
        absolute = 0;
      }
        else
      strcat(outpath,"\\");
      }
  
    /* remove the last "\" from the outpath, it does not belong there */
  
    outpath[strlen(outpath)-1] = 0;
  
  #elif defined(VMS) || defined(vms) || defined(__vms)
  
    /*
       VMS case. The output path will be of the form:
  
       node::disk:[path]filename.ext;version
  
       Any part of the file path may be missing execpt filename.ext, so that in
       the simplist case a single file name/extension is given.
  
       if the path is specified as relative starting with "./" then the first
       part of the VMS path is "[.". If the path is relative and does not start
       with "./" (e.g., "a/b/c") then the VMS path is constructed as
       "[a.b.c]"
     */
  
    /*
      separate the URL into tokens at each slash '/' and process until
      all tokens have been examined
    */
  
    for(tmpStr = ffstrtok(buff,"/",&saveptr), outpath[0] = 0;
                                   tmpStr != NULL; tmpStr = ffstrtok(NULL,"/",&saveptr))
      {
  
        if(fits_strcasecmp(tmpStr,"FILE:") == 0)
      {
        /* the next token should contain the DECnet machine name */
  
        tmpStr = ffstrtok(NULL,"/",&saveptr);
        if(tmpStr == NULL) continue;
  
        strcat(outpath,tmpStr);
        strcat(outpath,"::");
  
        /* set the absolute flag to true for the next token */
        absolute = 1;
      }
  
        else if(strcmp(tmpStr,"..") == 0)
      {
        /* replace all Unix-like ".." with VMS "-" */
  
        if(strlen(outpath) == 0) strcat(outpath,"[");
        strcat(outpath,"-.");
      }
  
        else if(strcmp(tmpStr,".") == 0 && strlen(outpath) == 0)
      {
        /*
          must indicate a relative path specifier
        */
  
        strcat(outpath,"[.");
      }
  
        else if(strchr(tmpStr,'.') != NULL)
      {
        /*
           must be up to the file name; turn the last "." path separator
           into a "]" and then add the file name to the outpath
        */
  
        i = strlen(outpath);
        if(i > 0 && outpath[i-1] == '.') outpath[i-1] = ']';
  
        strcat(outpath,tmpStr);
      }
  
        else
      {
        /*
          process the token as a a directory path segement
        */
  
        if(absolute)
          {
            /* treat the token as a disk specifier */
            absolute = 0;
            strcat(outpath,tmpStr);
            strcat(outpath,":[");
          }
        else if(strlen(outpath) == 0)
          {
            /* treat the token as the first directory path specifier */
            strcat(outpath,"[");
            strcat(outpath,tmpStr);
            strcat(outpath,".");
          }
        else
          {
            /* treat the token as an imtermediate path specifier */
            strcat(outpath,tmpStr);
            strcat(outpath,".");
          }
      }
      }
  
  #elif defined(macintosh)
  
    /*
       MacOS case. The output path will be of the form
  
       disk:path:filename
  
       All path segments but the last may be null, so that a single file name
       is the simplist case.
    */
  
    /*
      separate the URL into tokens at each slash '/' and process until
      all tokens have been examined
    */
  
    for(tmpStr = ffstrtok(buff,"/",&saveptr), outpath[0] = 0;
                                   tmpStr != NULL; tmpStr = ffstrtok(NULL,"/",&saveptr))
      {
        strcat(outpath,tmpStr);
        strcat(outpath,":");
      }
  
    /* remove the last ":" from the outpath, it does not belong there */
  
    outpath[strlen(outpath)-1] = 0;
  
  #else
  
    /*
       Default Unix case.
  
       Nothing special to do here
     */
  
    strcpy(outpath,buff);
  
  #endif
  
    return(*status);
  }
  
  /****************************************************************************/
  int fits_get_cwd(char *cwd,  /* IO current working directory string */
           int  *status)
    /*
       retrieve the string containing the current working directory absolute
       path in Unix-like URL standard notation. It is assumed that the CWD
       string has a size of at least FLEN_FILENAME.
  
       Note that this process is platform dependent. This
       function supports Unix, MSDOS/WIN32, VMS and Macintosh platforms. Each
       platform dependent code segment is conditionally compiled depending
       upon the setting of the appropriate C preprocesser macros.
     */
  {
  
    char buff[FLEN_FILENAME];
  
  
    if(*status != 0) return(*status);
  
  #if defined(macintosh)
  
    /*
       MacOS case. Currently unknown !!!!
    */
  
    *buff = 0;
  
  #else
    /*
      Good old getcwd() seems to work with all other platforms
    */
  
    if (!getcwd(buff,FLEN_FILENAME))
    {
       cwd[0]=0;
       ffpmsg("Path and file name too long (fits_get_cwd)");
       return (*status=URL_PARSE_ERROR);
    }
  
  #endif
  
    /*
      convert the cwd string to a URL standard path string
    */
  
    fits_path2url(buff,FLEN_FILENAME,cwd,status);
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int  fits_get_url(fitsfile *fptr,       /* I ptr to FITS file to evaluate    */
            char     *realURL,    /* O URL of real FITS file           */
            char     *startURL,   /* O URL of starting FITS file       */
            char     *realAccess, /* O true access method of FITS file */
            char     *startAccess,/* O "official" access of FITS file  */
            int      *iostate,    /* O can this file be modified?      */
            int      *status)
  /*
    For grouping convention purposes, determine the URL of the FITS file
    associated with the fitsfile pointer fptr. The true access type (file://,
    mem://, shmem://, root://), starting "official" access type, and iostate
    (0 ==> readonly, 1 ==> readwrite) are also returned.
  
    It is assumed that the url string has enough room to hold the resulting
    URL, and the the accessType string has enough room to hold the access type.
  */
  {
    int i;
    int tmpIOstate = 0;
  
    char infile[FLEN_FILENAME];
    char outfile[FLEN_FILENAME];
    char tmpStr1[FLEN_FILENAME];
    char tmpStr2[FLEN_FILENAME];
    char tmpStr3[FLEN_FILENAME];
    char tmpStr4[FLEN_FILENAME];
    char *tmpPtr;
  
  
    if(*status != 0) return(*status);
  
    do
      {
        /*
       retrieve the member HDU's file name as opened by ffopen()
       and parse it into its constitutent pieces; get the currently
       active driver token too
         */
  
        *tmpStr1 = *tmpStr2 = *tmpStr3 = *tmpStr4 = 0;
  
        *status = fits_file_name(fptr,tmpStr1,status);
  
        *status = ffiurl(tmpStr1,NULL,infile,outfile,NULL,tmpStr2,tmpStr3,
                 tmpStr4,status);
  
        if((*tmpStr2) || (*tmpStr3) || (*tmpStr4)) tmpIOstate = -1;
  
        *status = ffurlt(fptr,tmpStr3,status);
  
        strcpy(tmpStr4,tmpStr3);
  
        *status = ffrtnm(tmpStr1,tmpStr2,status);
        strcpy(tmpStr1,tmpStr2);
  
        /*
      for grouping convention purposes (only) determine the URL of the
      actual FITS file being used for the given fptr, its true access
      type (file://, mem://, shmem://, root://) and its iostate (0 ==>
      read only, 1 ==> readwrite)
        */
  
        /*
      The first set of access types are "simple" in that they do not
      use any redirection to temporary memory or outfiles
         */
  
        /* standard disk file driver is in use */
  
        if(fits_strcasecmp(tmpStr3,"file://")              == 0)
      {
        tmpIOstate = 1;
  
        if(strlen(outfile)) strcpy(tmpStr1,outfile);
        else *tmpStr2 = 0;
  
        /*
          make sure no FILE:// specifier is given in the tmpStr1
          or tmpStr2 strings; the convention calls for local files
          to have no access specification
        */
  
        if((tmpPtr = strstr(tmpStr1,"://")) != NULL)
          {
            strcpy(infile,tmpPtr+3);
            strcpy(tmpStr1,infile);
          }
  
        if((tmpPtr = strstr(tmpStr2,"://")) != NULL)
          {
            strcpy(infile,tmpPtr+3);
            strcpy(tmpStr2,infile);
          }
      }
  
        /* file stored in conventional memory */
  
        else if(fits_strcasecmp(tmpStr3,"mem://")          == 0)
      {
        if(tmpIOstate < 0)
          {
            /* file is a temp mem file only */
            ffpmsg("cannot make URL from temp MEM:// file (fits_get_url)");
            *status = URL_PARSE_ERROR;
          }
        else
          {
            /* file is a "perminate" mem file for this process */
            tmpIOstate = 1;
            *tmpStr2 = 0;
          }
      }
  
        /* file stored in conventional memory */
  
       else if(fits_strcasecmp(tmpStr3,"memkeep://")      == 0)
      {
        strcpy(tmpStr3,"mem://");
        *tmpStr4 = 0;
        *tmpStr2 = 0;
        tmpIOstate = 1;
      }
  
        /* file residing in shared memory */
  
        else if(fits_strcasecmp(tmpStr3,"shmem://")        == 0)
      {
        *tmpStr4   = 0;
        *tmpStr2   = 0;
        tmpIOstate = 1;
      }
  
        /* file accessed via the ROOT network protocol */
  
        else if(fits_strcasecmp(tmpStr3,"root://")         == 0)
      {
        *tmpStr4   = 0;
        *tmpStr2   = 0;
        tmpIOstate = 1;
      }
  
        /*
      the next set of access types redirect the contents of the original
      file to an special outfile because the original could not be
      directly modified (i.e., resides on the network, was compressed).
      In these cases the URL string takes on the value of the OUTFILE,
      the access type becomes file://, and the iostate is set to 1 (can
      read/write to the file).
        */
  
        /* compressed file uncompressed and written to disk */
  
        else if(fits_strcasecmp(tmpStr3,"compressfile://") == 0)
      {
        strcpy(tmpStr1,outfile);
        strcpy(tmpStr2,infile);
        strcpy(tmpStr3,"file://");
        strcpy(tmpStr4,"file://");
        tmpIOstate = 1;
      }
  
        /* HTTP accessed file written locally to disk */
  
        else if(fits_strcasecmp(tmpStr3,"httpfile://")     == 0)
      {
        strcpy(tmpStr1,outfile);
        strcpy(tmpStr3,"file://");
        strcpy(tmpStr4,"http://");
        tmpIOstate = 1;
      }
  
        /* FTP accessd file written locally to disk */
  
        else if(fits_strcasecmp(tmpStr3,"ftpfile://")      == 0)
      {
        strcpy(tmpStr1,outfile);
        strcpy(tmpStr3,"file://");
        strcpy(tmpStr4,"ftp://");
        tmpIOstate = 1;
      }
  
        /* file from STDIN written to disk */
  
        else if(fits_strcasecmp(tmpStr3,"stdinfile://")    == 0)
      {
        strcpy(tmpStr1,outfile);
        strcpy(tmpStr3,"file://");
        strcpy(tmpStr4,"stdin://");
        tmpIOstate = 1;
      }
  
        /*
       the following access types use memory resident files as temporary
       storage; they cannot be modified or be made group members for
       grouping conventions purposes, but their original files can be.
       Thus, their tmpStr3s are reset to mem://, their iostate
       values are set to 0 (for no-modification), and their URL string
       values remain set to their original values
         */
  
        /* compressed disk file uncompressed into memory */
  
        else if(fits_strcasecmp(tmpStr3,"compress://")     == 0)
      {
        *tmpStr1 = 0;
        strcpy(tmpStr2,infile);
        strcpy(tmpStr3,"mem://");
        strcpy(tmpStr4,"file://");
        tmpIOstate = 0;
      }
  
        /* HTTP accessed file transferred into memory */
  
        else if(fits_strcasecmp(tmpStr3,"http://")         == 0)
      {
        *tmpStr1 = 0;
        strcpy(tmpStr3,"mem://");
        strcpy(tmpStr4,"http://");
        tmpIOstate = 0;
      }
  
        /* HTTP accessed compressed file transferred into memory */
  
        else if(fits_strcasecmp(tmpStr3,"httpcompress://") == 0)
      {
        *tmpStr1 = 0;
        strcpy(tmpStr3,"mem://");
        strcpy(tmpStr4,"http://");
        tmpIOstate = 0;
      }
  
        /* FTP accessed file transferred into memory */
  
        else if(fits_strcasecmp(tmpStr3,"ftp://")          == 0)
      {
        *tmpStr1 = 0;
        strcpy(tmpStr3,"mem://");
        strcpy(tmpStr4,"ftp://");
        tmpIOstate = 0;
      }
  
        /* FTP accessed compressed file transferred into memory */
  
        else if(fits_strcasecmp(tmpStr3,"ftpcompress://")  == 0)
      {
        *tmpStr1 = 0;
        strcpy(tmpStr3,"mem://");
        strcpy(tmpStr4,"ftp://");
        tmpIOstate = 0;
      }
  
        /*
      The last set of access types cannot be used to make a meaningful URL
      strings from; thus an error is generated
         */
  
        else if(fits_strcasecmp(tmpStr3,"stdin://")        == 0)
      {
        *status = URL_PARSE_ERROR;
        ffpmsg("cannot make vaild URL from stdin:// (fits_get_url)");
        *tmpStr1 = *tmpStr2 = 0;
      }
  
        else if(fits_strcasecmp(tmpStr3,"stdout://")       == 0)
      {
        *status = URL_PARSE_ERROR;
        ffpmsg("cannot make vaild URL from stdout:// (fits_get_url)");
        *tmpStr1 = *tmpStr2 = 0;
      }
  
        else if(fits_strcasecmp(tmpStr3,"irafmem://")      == 0)
      {
        *status = URL_PARSE_ERROR;
        ffpmsg("cannot make vaild URL from irafmem:// (fits_get_url)");
        *tmpStr1 = *tmpStr2 = 0;
      }
  
        if(*status != 0) continue;
  
        /*
       assign values to the calling parameters if they are non-NULL
        */
  
        if(realURL != NULL)
      {
        if(strlen(tmpStr1) == 0)
          *realURL = 0;
        else
          {
            if((tmpPtr = strstr(tmpStr1,"://")) != NULL)
          {
            tmpPtr += 3;
            i = (long)tmpPtr - (long)tmpStr1;
            strncpy(realURL,tmpStr1,i);
          }
            else
          {
            tmpPtr = tmpStr1;
            i = 0;
          }
  
            *status = fits_path2url(tmpPtr,FLEN_FILENAME-i,realURL+i,status);
          }
      }
  
        if(startURL != NULL)
      {
        if(strlen(tmpStr2) == 0)
          *startURL = 0;
        else
          {
            if((tmpPtr = strstr(tmpStr2,"://")) != NULL)
          {
            tmpPtr += 3;
            i = (long)tmpPtr - (long)tmpStr2;
            strncpy(startURL,tmpStr2,i);
          }
            else
          {
            tmpPtr = tmpStr2;
            i = 0;
          }
  
            *status = fits_path2url(tmpPtr,FLEN_FILENAME-i,startURL+i,status);
          }
      }
  
        if(realAccess  != NULL)  strcpy(realAccess,tmpStr3);
        if(startAccess != NULL)  strcpy(startAccess,tmpStr4);
        if(iostate     != NULL) *iostate = tmpIOstate;
  
      }while(0);
  
    return(*status);
  }
  
  /*--------------------------------------------------------------------------
                             URL parse support functions
    --------------------------------------------------------------------------*/
  
  /* simple push/pop/shift/unshift string stack for use by fits_clean_url */
  typedef char* grp_stack_data; /* type of data held by grp_stack */
  
  typedef struct grp_stack_item_struct {
    grp_stack_data data; /* value of this stack item */
    struct grp_stack_item_struct* next; /* next stack item */
    struct grp_stack_item_struct* prev; /* previous stack item */
  } grp_stack_item;
  
  typedef struct grp_stack_struct {
    size_t stack_size; /* number of items on stack */
    grp_stack_item* top; /* top item */
  } grp_stack;
  
  static char* grp_stack_default = NULL; /* initial value for new instances
                                            of grp_stack_data */
  
  /* the following functions implement the group string stack grp_stack */
  static void delete_grp_stack(grp_stack** mystack);
  static grp_stack_item* grp_stack_append(
    grp_stack_item* last, grp_stack_data data
  );
  static grp_stack_data grp_stack_remove(grp_stack_item* last);
  static grp_stack* new_grp_stack(void);
  static grp_stack_data pop_grp_stack(grp_stack* mystack);
  static void push_grp_stack(grp_stack* mystack, grp_stack_data data);
  static grp_stack_data shift_grp_stack(grp_stack* mystack);
  /* static void unshift_grp_stack(grp_stack* mystack, grp_stack_data data); */
  
  int fits_clean_url(char *inURL,  /* I input URL string                      */
             char *outURL, /* O output URL string                     */
             int  *status)
  /*
    clean the URL by eliminating any ".." or "." specifiers in the inURL
    string, and write the output to the outURL string.
  
    Note that this function must have a valid Unix-style URL as input; platform
    dependent path strings are not allowed.
   */
  {
    grp_stack* mystack; /* stack to hold pieces of URL */
    char* tmp;
    char *saveptr;
  
    if(*status) return *status;
  
    mystack = new_grp_stack();
    *outURL = 0;
  
    do {
      /* handle URL scheme and domain if they exist */
      tmp = strstr(inURL, "://");
      if(tmp) {
        /* there is a URL scheme, so look for the end of the domain too */
        tmp = strchr(tmp + 3, '/');
        if(tmp) {
          /* tmp is now the end of the domain, so
           * copy URL scheme and domain as is, and terminate by hand */
          size_t string_size = (size_t) (tmp - inURL);
          strncpy(outURL, inURL, string_size);
          outURL[string_size] = 0;
  
          /* now advance the input pointer to just after the domain and go on */
          inURL = tmp;
        } else {
          /* '/' was not found, which means there are no path-like
           * portions, so copy whole inURL to outURL and we're done */
          strcpy(outURL, inURL);
          continue; /* while(0) */
        }
      }
  
      /* explicitly copy a leading / (absolute path) */
      if('/' == *inURL) strcat(outURL, "/");
  
      /* now clean the remainder of the inURL. push URL segments onto
       * stack, dealing with .. and . as we go */
      tmp = ffstrtok(inURL, "/",&saveptr); /* finds first / */
      while(tmp) {
        if(!strcmp(tmp, "..")) {
          /* discard previous URL segment, if there was one. if not,
           * add the .. to the stack if this is *not* an absolute path
           * (for absolute paths, leading .. has no effect, so skip it) */
          if(0 < mystack->stack_size) pop_grp_stack(mystack);
          else if('/' != *inURL) push_grp_stack(mystack, tmp);
        } else {
          /* always just skip ., but otherwise add segment to stack */
          if(strcmp(tmp, ".")) push_grp_stack(mystack, tmp);
        }
        tmp = ffstrtok(NULL, "/",&saveptr); /* get the next segment */
      }
  
      /* stack now has pieces of cleaned URL, so just catenate them
       * onto output string until stack is empty */
      while(0 < mystack->stack_size) {
        tmp = shift_grp_stack(mystack);
        if (strlen(outURL) + strlen(tmp) + 1 > FLEN_FILENAME-1)
        {
           outURL[0]=0;
           ffpmsg("outURL is too long (fits_clean_url)");
           *status = URL_PARSE_ERROR;
           delete_grp_stack(&mystack);
           return *status;
        }
        strcat(outURL, tmp);
        strcat(outURL, "/");
      }
      outURL[strlen(outURL) - 1] = 0; /* blank out trailing / */
    } while(0);
    delete_grp_stack(&mystack);
    return *status;
  }
  
  /* free all stack contents using pop_grp_stack before freeing the
   * grp_stack itself */
  static void delete_grp_stack(grp_stack** mystack) {
    if(!mystack || !*mystack) return;
    while((*mystack)->stack_size) pop_grp_stack(*mystack);
    free(*mystack);
    *mystack = NULL;
  }
  
  /* append an item to the stack, handling the special case of the first
   * item appended */
  static grp_stack_item* grp_stack_append(
    grp_stack_item* last, grp_stack_data data
  ) {
    /* first create a new stack item, and copy data to it */
    grp_stack_item* new_item = (grp_stack_item*) malloc(sizeof(grp_stack_item));
    new_item->data = data;
    if(last) {
      /* attach this item between the "last" item and its "next" item */
      new_item->next = last->next;
      new_item->prev = last;
      last->next->prev = new_item;
      last->next = new_item;
    } else {
      /* stack is empty, so "next" and "previous" both point back to it */
      new_item->next = new_item;
      new_item->prev = new_item;
    }
    return new_item;
  }
  
  /* remove an item from the stack, handling the special case of the last
   * item removed */
  static grp_stack_data grp_stack_remove(grp_stack_item* last) {
    grp_stack_data retval = last->data;
    last->prev->next = last->next;
    last->next->prev = last->prev;
    free(last);
    return retval;
  }
  
  /* create new stack dynamically, and give it valid initial values */
  static grp_stack* new_grp_stack(void) {
    grp_stack* retval = (grp_stack*) malloc(sizeof(grp_stack));
    if(retval) {
      retval->stack_size = 0;
      retval->top = NULL;
    }
    return retval;
  }
  
  /* return the value at the top of the stack and remove it, updating
   * stack_size. top->prev becomes the new "top" */
  static grp_stack_data pop_grp_stack(grp_stack* mystack) {
    grp_stack_data retval = grp_stack_default;
    if(mystack && mystack->top) {
      grp_stack_item* newtop = mystack->top->prev;
      retval = grp_stack_remove(mystack->top);
      mystack->top = newtop;
      if(0 == --mystack->stack_size) mystack->top = NULL;
    }
    return retval;
  }
  
  /* add to the stack after the top element. the added element becomes
   * the new "top" */
  static void push_grp_stack(grp_stack* mystack, grp_stack_data data) {
    if(!mystack) return;
    mystack->top = grp_stack_append(mystack->top, data);
    ++mystack->stack_size;
    return;
  }
  
  /* return the value at the bottom of the stack and remove it, updating
   * stack_size. "top" pointer is unaffected */
  static grp_stack_data shift_grp_stack(grp_stack* mystack) {
    grp_stack_data retval = grp_stack_default;
    if(mystack && mystack->top) {
      retval = grp_stack_remove(mystack->top->next); /* top->next == bottom */
      if(0 == --mystack->stack_size) mystack->top = NULL;
    }
    return retval;
  }
  
  /* add to the stack after the top element. "top" is unaffected, except
   * in the special case of an initially empty stack */
  /* static void unshift_grp_stack(grp_stack* mystack, grp_stack_data data) {
     if(!mystack) return;
     if(mystack->top) grp_stack_append(mystack->top, data);
     else mystack->top = grp_stack_append(NULL, data);
     ++mystack->stack_size;
     return;
     } */
  
  /*--------------------------------------------------------------------------*/
  int fits_url2relurl(char     *refURL, /* I reference URL string             */
              char     *absURL, /* I absoulute URL string to process  */
              char     *relURL, /* O resulting relative URL string    */
              int      *status)
  /*
    create a relative URL to the file referenced by absURL with respect to the
    reference URL refURL. The relative URL is returned in relURL.
  
    Both refURL and absURL must be absolute URL strings; i.e. either begin
    with an access method specification "XXX://" or with a '/' character
    signifiying that they are absolute file paths.
  
    Note that it is possible to make a relative URL from two input URLs
    (absURL and refURL) that are not compatable. This function does not
    check to see if the resulting relative URL makes any sence. For instance,
    it is impossible to make a relative URL from the following two inputs:
  
    absURL = ftp://a.b.c.com/x/y/z/foo.fits
    refURL = /a/b/c/ttt.fits
  
    The resulting relURL will be:
  
    ../../../ftp://a.b.c.com/x/y/z/foo.fits
  
    Which is syntically correct but meaningless. The problem is that a file
    with an access method of ftp:// cannot be expressed a a relative URL to
    a local disk file.
  */
  
  {
    int i,j;
    int refcount,abscount;
    int refsize,abssize;
    int done;
  
  
    if(*status != 0) return(*status);
  
    /* initialize the relative URL string */
    relURL[0] = 0;
  
    do
      {
        /*
      refURL and absURL must be absolute to process
        */
  
        if(!(fits_is_url_absolute(refURL) || *refURL == '/') ||
       !(fits_is_url_absolute(absURL) || *absURL == '/'))
      {
        *status = URL_PARSE_ERROR;
        ffpmsg("Cannot make rel. URL from non abs. URLs (fits_url2relurl)");
        continue;
      }
  
        /* determine the size of the refURL and absURL strings */
  
        refsize = strlen(refURL);
        abssize = strlen(absURL);
  
        /* process the two URL strings and build the relative URL between them */
  
  
        for(done = 0, refcount = 0, abscount = 0;
        !done && refcount < refsize && abscount < abssize;
        ++refcount, ++abscount)
      {
        for(; abscount < abssize && absURL[abscount] == '/'; ++abscount);
        for(; refcount < refsize && refURL[refcount] == '/'; ++refcount);
  
        /* find the next path segment in absURL */
        for(i = abscount; absURL[i] != '/' && i < abssize; ++i);
  
        /* find the next path segment in refURL */
        for(j = refcount; refURL[j] != '/' && j < refsize; ++j);
  
        /* do the two path segments match? */
        if(i == j &&
           strncmp(absURL+abscount, refURL+refcount,i-refcount) == 0)
          {
            /* they match, so ignore them and continue */
            abscount = i; refcount = j;
            continue;
          }
  
        /* We found a difference in the paths in refURL and absURL.
           For every path segment remaining in the refURL string, append
           a "../" path segment to the relataive URL relURL.
        */
  
        for(j = refcount; j < refsize; ++j)
          if(refURL[j] == '/')
              {
                 if (strlen(relURL)+3 > FLEN_FILENAME-1)
                 {
                *status = URL_PARSE_ERROR;
                ffpmsg("relURL too long (fits_url2relurl)");
                return (*status);
                 }
                 strcat(relURL,"../");
              }
  
        /* copy all remaining characters of absURL to the output relURL */
  
            if (strlen(relURL) + strlen(absURL+abscount) > FLEN_FILENAME-1)
            {
           *status = URL_PARSE_ERROR;
           ffpmsg("relURL too long (fits_url2relurl)");
           return (*status);
            }
        strcat(relURL,absURL+abscount);
  
        /* we are done building the relative URL */
        done = 1;
      }
  
      }while(0);
  
    return(*status);
  }
  /*--------------------------------------------------------------------------*/
  int fits_relurl2url(char     *refURL, /* I reference URL string             */
              char     *relURL, /* I relative URL string to process   */
              char     *absURL, /* O absolute URL string              */
              int      *status)
  /*
    create an absolute URL from a relative url and a reference URL. The
    reference URL is given by the FITS file pointed to by fptr.
  
    The construction of the absolute URL from the partial and reference URl
    is performed using the rules set forth in:
  
    http://www.w3.org/Addressing/URL/URL_TOC.html
    and
    http://www.w3.org/Addressing/URL/4_3_Partial.html
  
    Note that the relative URL string relURL must conform to the Unix-like
    URL syntax; host dependent partial URL strings are not allowed.
  */
  {
    int i;
  
    char tmpStr[FLEN_FILENAME];
  
    char *tmpStr1, *tmpStr2;
  
  
    if(*status != 0) return(*status);
  
    do
      {
  
        /*
      make a copy of the reference URL string refURL for parsing purposes
        */
  
        if (strlen(refURL) > FLEN_FILENAME-1)
        {
           absURL[0]=0;
           ffpmsg("ref URL is too long (fits_relurl2url)");
           *status = URL_PARSE_ERROR;
           continue;
        }
        strcpy(tmpStr,refURL);
  
        /*
      if the reference file has an access method of mem:// or shmem://
      then we cannot use it as the basis of an absolute URL construction
      for a partial URL
        */
  
        if(fits_strncasecmp(tmpStr,"MEM:",4)   == 0 ||
                                      fits_strncasecmp(tmpStr,"SHMEM:",6) == 0)
      {
        ffpmsg("ref URL has access mem:// or shmem:// (fits_relurl2url)");
        ffpmsg("   cannot construct full URL from a partial URL and ");
        ffpmsg("   MEM/SHMEM base URL");
        *status = URL_PARSE_ERROR;
        continue;
      }
  
        if(relURL[0] != '/')
      {
        /*
          just append the relative URL string to the reference URL
          string (minus the reference URL file name) to form the
          absolute URL string
        */
  
        tmpStr1 = strrchr(tmpStr,'/');
  
        if(tmpStr1 != NULL) tmpStr1[1] = 0;
        else                tmpStr[0]  = 0;
  
            if (strlen(tmpStr)+strlen(relURL) > FLEN_FILENAME-1)
            {
                absURL[0]=0;
                ffpmsg("rel + ref URL is too long (fits_relurl2url)");
                *status = URL_PARSE_ERROR;
                continue;
            }
        strcat(tmpStr,relURL);
      }
        else
      {
        /*
          have to parse the refURL string for the first occurnace of the
          same number of '/' characters as contained in the beginning of
          location that is not followed by a greater number of consective
          '/' charaters (yes, that is a confusing statement); this is the
          location in the refURL string where the relURL string is to
          be appended to form the new absolute URL string
         */
  
        /*
          first, build up a slash pattern string that has one more
          slash in it than the starting slash pattern of the
          relURL string
        */
  
        strcpy(absURL,"/");
  
        for(i = 0; relURL[i] == '/'; ++i)
            {
               if (strlen(absURL) + 1 > FLEN_FILENAME-1)
               {
                   absURL[0]=0;
                   ffpmsg("abs URL is too long (fits_relurl2url)");
                   *status = URL_PARSE_ERROR;
                   return (*status);
               }
               strcat(absURL,"/");
            }
  
        /*
          loop over the refURL string until the slash pattern stored
          in absURL is no longer found
        */
  
        for(tmpStr1 = tmpStr, i = strlen(absURL);
            (tmpStr2 = strstr(tmpStr1,absURL)) != NULL;
            tmpStr1 = tmpStr2 + i);
  
        /* reduce the slash pattern string by one slash */
  
        absURL[i-1] = 0;
  
        /*
           search for the slash pattern in the remaining portion
           of the refURL string
        */
  
        tmpStr2 = strstr(tmpStr1,absURL);
  
        /* if no slash pattern match was found */
  
        if(tmpStr2 == NULL)
          {
            /* just strip off the file name from the refURL  */
  
            tmpStr2 = strrchr(tmpStr1,'/');
  
            if(tmpStr2 != NULL) tmpStr2[0] = 0;
            else                tmpStr[0]  = 0;
          }
        else
          {
            /* set a string terminator at the slash pattern match */
  
            *tmpStr2 = 0;
          }
  
        /*
          conatenate the relURL string to the refURL string to form
          the absURL
         */
  
            if (strlen(tmpStr)+strlen(relURL) > FLEN_FILENAME-1)
            {
                absURL[0]=0;
                ffpmsg("rel + ref URL is too long (fits_relurl2url)");
                *status = URL_PARSE_ERROR;
                continue;
            }
        strcat(tmpStr,relURL);
      }
  
        /*
      normalize the absURL by removing any ".." or "." specifiers
      in the string
        */
  
        *status = fits_clean_url(tmpStr,absURL,status);
  
      }while(0);
  
    return(*status);
  }
  /*--------------------------------------------------------------------------*/
  int fits_encode_url(char *inpath,  /* I URL  to be encoded                  */
                      int maxlength, /* I max number of chars that may be copied
                                 to outpath, including terminating NULL. */
              char *outpath, /* O output encoded URL                  */
              int *status)
       /*
         encode all URL "unsafe" and "reserved" characters using the "%XX"
         convention, where XX stand for the two hexidecimal digits of the
         encode character's ASCII code.
  
         Note that the outpath length, as specified by the maxlength argument,
         should be at least as large as inpath and preferably larger (to hold
         any characters that need encoding).  If more than maxlength chars are
         required for outpath, including the terminating NULL, outpath will
         be set to size 0 and an error status will be returned.
  
         This function was adopted from code in the libwww.a library available
         via the W3 consortium <URL: http://www.w3.org>
       */
  {
    unsigned char a;
  
    char *p;
    char *q;
    char *hex = "0123456789ABCDEF";
    int iout=0;
  
  unsigned const char isAcceptable[96] =
  {/* 0x0 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xA 0xB 0xC 0xD 0xE 0xF */
  
      0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xF,0xE,0x0,0xF,0xF,0xC,
                                             /* 2x  !"#$%&'()*+,-./   */
      0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0x8,0x0,0x0,0x0,0x0,0x0,
                                             /* 3x 0123456789:;<=>?   */
      0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,
                                             /* 4x @ABCDEFGHIJKLMNO   */
      0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0x0,0x0,0x0,0x0,0xF,
                                             /* 5X PQRSTUVWXYZ[\]^_   */
      0x0,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,
                                             /* 6x `abcdefghijklmno   */
      0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0x0,0x0,0x0,0x0,0x0
                                             /* 7X pqrstuvwxyz{\}~DEL */
  };
  
    if(*status != 0) return(*status);
  
    /* loop over all characters in inpath until '\0' is encountered */
  
    for(q = outpath, p = inpath; *p && (iout < maxlength-1) ; p++)
      {
        a = (unsigned char)*p;
  
        /* if the charcter requires encoding then process it */
  
        if(!( a>=32 && a<128 && (isAcceptable[a-32])))
      {
             if (iout+2 < maxlength-1)
             {
           /* add a '%' character to the outpath */
           *q++ = HEX_ESCAPE;
           /* add the most significant ASCII code hex value */
           *q++ = hex[a >> 4];
           /* add the least significant ASCII code hex value */
           *q++ = hex[a & 15];
               iout += 3;
             }
             else
             {
                ffpmsg("URL input is too long to encode (fits_encode_url)");
                *status = URL_PARSE_ERROR;
                outpath[0] = 0;
                return (*status);
             }
      }
        /* else just copy the character as is */
        else
        {
           *q++ = *p;
           iout++;
        }
      }
  
    /* null terminate the outpath string */
  
    if (*p && (iout == maxlength-1))
    {
       ffpmsg("URL input is too long to encode (fits_encode_url)");
       *status = URL_PARSE_ERROR;
       outpath[0] = 0;
       return (*status);
    }
    *q++ = 0;
  
    return(*status);
  }
  
  /*---------------------------------------------------------------------------*/
  int fits_unencode_url(char *inpath,  /* I input URL with encoding            */
                char *outpath, /* O unencoded URL                      */
                int  *status)
       /*
         unencode all URL "unsafe" and "reserved" characters to their actual
         ASCII representation. All tokens of the form "%XX" where XX is the
         hexidecimal code for an ASCII character, are searched for and
         translated into the actuall ASCII character (so three chars become
         1 char).
  
         It is assumed that OUTPATH has enough room to hold the unencoded
         URL.
  
         This function was adopted from code in the libwww.a library available
         via the W3 consortium <URL: http://www.w3.org>
       */
  
  {
      char *p;
      char *q;
      char  c;
  
      if(*status != 0) return(*status);
  
      p = inpath;
      q = outpath;
  
      /*
         loop over all characters in the inpath looking for the '%' escape
         character; if found the process the escape sequence
      */
  
      while(*p != 0)
        {
      /*
         if the character is '%' then unencode the sequence, else
         just copy the character from inpath to outpath
          */
  
          if (*p == HEX_ESCAPE)
        {
              if((c = *(++p)) != 0)
            {
          *q = (
                (c >= '0' && c <= '9') ?
                (c - '0') : ((c >= 'A' && c <= 'F') ?
                     (c - 'A' + 10) : (c - 'a' + 10))
                )*16;
  
          if((c = *(++p)) != 0)
            {
              *q = *q + (
                     (c >= '0' && c <= '9') ?
                         (c - '0') : ((c >= 'A' && c <= 'F') ?
                          (c - 'A' + 10) : (c - 'a' + 10))
                     );
              p++, q++;
            }
            }
        }
      else
        *q++ = *p++;
        }
  
      /* terminate the outpath */
      *q = 0;
  
      return(*status);
  }
  /*---------------------------------------------------------------------------*/
  
  int fits_is_url_absolute(char *url)
  /*
    Return a True (1) or False (0) value indicating whether or not the passed
    URL string contains an access method specifier or not. Note that this is
    a boolean function and it neither reads nor returns the standard error
    status parameter
  */
  {
    char *tmpStr1, *tmpStr2;
  
    char reserved[] = {':',';','/','?','@','&','=','+','$',','};
  
    /*
      The rule for determing if an URL is relative or absolute is that it (1)
      must have a colon ":" and (2) that the colon must appear before any other
      reserved URL character in the URL string. We first see if a colon exists,
      get its position in the string, and then check to see if any of the other
      reserved characters exists and if their position in the string is greater
      than that of the colons.
     */
  
    if( (tmpStr1 = strchr(url,reserved[0])) != NULL                       &&
       ((tmpStr2 = strchr(url,reserved[1])) == NULL || tmpStr2 > tmpStr1) &&
       ((tmpStr2 = strchr(url,reserved[2])) == NULL || tmpStr2 > tmpStr1) &&
       ((tmpStr2 = strchr(url,reserved[3])) == NULL || tmpStr2 > tmpStr1) &&
       ((tmpStr2 = strchr(url,reserved[4])) == NULL || tmpStr2 > tmpStr1) &&
       ((tmpStr2 = strchr(url,reserved[5])) == NULL || tmpStr2 > tmpStr1) &&
       ((tmpStr2 = strchr(url,reserved[6])) == NULL || tmpStr2 > tmpStr1) &&
       ((tmpStr2 = strchr(url,reserved[7])) == NULL || tmpStr2 > tmpStr1) &&
       ((tmpStr2 = strchr(url,reserved[8])) == NULL || tmpStr2 > tmpStr1) &&
       ((tmpStr2 = strchr(url,reserved[9])) == NULL || tmpStr2 > tmpStr1)   )
      {
        return(1);
      }
    else
      {
        return(0);
      }
  }
  