#include "niml_private.h"

/*-----------------------------------------------------------------------*/
/* Vector labels [12 Sep 2018] */

void NI_set_veclab_from_stringlist( NI_element *nel , char *vstr )
{
   if( NI_element_type(nel) != NI_ELEMENT_TYPE || vstr == NULL ) return ;

   if( nel->vec_num > 0 ){
     int pp ;
     NI_str_array *sar = NI_decode_string_list( vstr , "," ) ;
     if( sar != NULL && sar->num > 0 ){
       int ns=sar->num  ;
       if( nel->vec_lab != NULL ) NI_free(nel->vec_lab) ;
       nel->vec_lab = (char **)NI_malloc(char*,sizeof(char *)*nel->vec_num) ;
       for( pp=0 ; pp < nel->vec_num ; pp++ ){
          nel->vec_lab[pp] = (pp < ns) ? NI_strdup(sar->str[pp])
                                       : NI_strdup("\0")        ;
       }
     }
   }
   return ;
}

/*-----------------------------------------------------------------------*/

void NI_set_attribute_from_veclab_array( NI_element *nel , char **vec_lab )
{
   char *vla ; int nvla , ii ;

   if( NI_element_type(nel) != NI_ELEMENT_TYPE || nel->vec_num == 0 ) return ;

   if( vec_lab == NULL ) vec_lab = nel->vec_lab ;
   if( vec_lab == NULL ) return ;

   for( nvla=ii=0 ; ii < nel->vec_num ; ii++ )
     nvla += NI_strlen(vec_lab[ii])+8 ;

   vla = (char *)malloc(sizeof(char)*nvla) ; vla[0] = '\0' ;
   for( ii=0 ; ii < nel->vec_num ; ii++ ){
     sprintf( vla+strlen(vla) , "%s%s" ,
              (vec_lab[ii] != NULL) ? vec_lab[ii] : "\0" ,
              (ii < nel->vec_num-1) ? ","         : "\0"  ) ;
   }

   NI_set_attribute(nel,"ni_veclab",vla) ;
   free(vla) ;
   return ;
}

/*-----------------------------------------------------------------------*/
/*! Construct an empty data element from a header.
    - The data vectors will have space allocated, but they will be
      filled with all zero bytes.
    - If the header was "empty" (ended in "/>"), then no vectors will
      be allocated, and nel->vec_num=0.
    - This function is used by NI_read_element() to create the
      data element after the header has been parsed.
    - 27 Mar 2003: modified to allow vec_len=0, indicating vector
      length to be inferred from amount of data
-------------------------------------------------------------------------*/

NI_element * make_empty_data_element( header_stuff *hs )
{
   NI_element *nel ;
   int ii ;

   if( hs == NULL || hs->name == NULL ) return NULL ;

#ifdef NIML_DEBUG
NI_dpr("ENTER make_empty_data_element\n") ;
#endif

   nel = NI_malloc(NI_element, sizeof(NI_element) ) ;

   nel->type = NI_ELEMENT_TYPE ;

   nel->outmode = -1 ;   /* 29 Mar 2005 */

   /* move name and attributes from hs to new element */

   nel->name = hs->name ; hs->name = NULL ;

   nel->attr_num = hs->nattr ;

   if( nel->attr_num > 0 ){
      nel->attr_lhs = hs->lhs ; hs->lhs = NULL ;
      nel->attr_rhs = hs->rhs ; hs->rhs = NULL ;
   } else {
      nel->attr_lhs = nel->attr_rhs = NULL ;
   }

   /* set default vector parameters [indicating no data] */

   nel->vec_num = 0 ;
   nel->vec_len = 0 ;
   nel->vec_typ = NULL ;
   nel->vec     = NULL ;
   nel->vec_lab = NULL ;  /* 11 Sep 2018 */
   nel->filename= NULL ;  /* 16 Jun 2020 */

   nel->vec_filled = 0 ;  /* no data has been filled into vectors */

   nel->vec_rank        = 0 ;
   nel->vec_axis_len    = NULL ;
   nel->vec_axis_delta  = NULL ;
   nel->vec_axis_origin = NULL ;
   nel->vec_axis_unit   = NULL ;
   nel->vec_axis_label  = NULL ;

   if( !hs->empty ){  /* find and process ni_* attributes about vectors */

     /* ni_type attribute: set types of vectors */

     ii = string_index( "ni_type" , nel->attr_num , nel->attr_lhs ) ;

     if( ii >= 0 && nel->attr_rhs[ii] != NULL ){
       int_array *iar = decode_type_string( nel->attr_rhs[ii] ) ;
       if( iar != NULL ){
         nel->vec_num = iar->num ;  /* number of vectors */
         nel->vec_typ = iar->ar ;   /* vector types */
         NI_free(iar) ;             /* just the shell of the struct */
       }
     }

     /* ni_dimen attribute: set vector length and rank */

     ii = string_index( "ni_dimen" , nel->attr_num , nel->attr_lhs ) ;

     if( ii >= 0 && nel->attr_rhs[ii] != NULL ){
        int_array *dar = decode_dimen_string( nel->attr_rhs[ii] ) ;
        if( dar != NULL && dar->num > 0 ){
           int nd=dar->num , qq,pp ;
           /* compute product of all dimensions */
           for( qq=1,pp=0 ; pp < nd ; pp++ ) qq *= dar->ar[pp] ;
           nel->vec_len      = qq ;      /* length of vectors */
           nel->vec_rank     = nd ;      /* number of dimensions */
           nel->vec_axis_len = dar->ar ; /* array of dimension lengths */
           NI_free(dar) ;                /* just the struct shell */
           if( nel->vec_len == 0 )       /* 27 Mar 2003 */
             nel->vec_rank = 1 ;
        }
     }

     /* if we had ni_dimen, also try ni_delta */

     ii = string_index( "ni_delta" , nel->attr_num , nel->attr_lhs ) ;
     if( ii >= 0 && nel->vec_rank > 0 ){
        NI_str_array *sar = NI_decode_string_list( nel->attr_rhs[ii] , NULL ) ;
        if( sar != NULL && sar->num > 0 ){
           int ns=sar->num , nd=nel->vec_rank , pp ;
           nel->vec_axis_delta = NI_malloc(float,sizeof(float)*nd) ;
           if( nd > ns ) nd = ns ;
           for( pp=0 ; pp < nd ; pp++ )
             sscanf( sar->str[pp] , "%f" , nel->vec_axis_delta+pp ) ;
           NI_delete_str_array(sar) ;
        }
     }

     /* if we had ni_dimen, also try ni_origin */

     ii = string_index( "ni_origin" , nel->attr_num , nel->attr_lhs ) ;
     if( ii >= 0 && nel->vec_rank > 0 ){
        NI_str_array *sar = NI_decode_string_list( nel->attr_rhs[ii] , NULL ) ;
        if( sar != NULL && sar->num > 0 ){
           int ns=sar->num , nd=nel->vec_rank , pp ;
           nel->vec_axis_origin = NI_malloc(float,sizeof(float)*nd) ;
           if( nd > ns ) nd = ns ;
           for( pp=0 ; pp < nd ; pp++ )
             sscanf( sar->str[pp] , "%f" , nel->vec_axis_origin+pp ) ;
           NI_delete_str_array(sar) ;
        }
     }

     /* if we had ni_dimen, also try ni_units */

     ii = string_index( "ni_units" , nel->attr_num , nel->attr_lhs ) ;
     if( ii >= 0 && nel->vec_rank > 0 ){
        NI_str_array *sar = NI_decode_string_list( nel->attr_rhs[ii] , NULL ) ;
        if( sar != NULL && sar->num > 0 ){
           int ns=sar->num , nd=nel->vec_rank , pp ;
           nel->vec_axis_unit = NI_malloc(char*,sizeof(char *)*nd) ;
           if( nd > ns ) nd = ns ;
           for( pp=0 ; pp < nd ; pp++ )
             nel->vec_axis_unit[pp] = NI_strdup(sar->str[pp]) ;
           NI_delete_str_array(sar) ;
        }
     }

     /* if we had ni_dimen, also try ni_axes */

     ii = string_index( "ni_axes" , nel->attr_num , nel->attr_lhs ) ;
     if( ii >= 0 && nel->vec_rank > 0 ){
        NI_str_array *sar = NI_decode_string_list( nel->attr_rhs[ii] , NULL ) ;
        if( sar != NULL && sar->num > 0 ){
           int ns=sar->num , nd=nel->vec_rank , pp ;
           nel->vec_axis_label = NI_malloc(char*,sizeof(char *)*nd) ;
           if( nd > ns ) nd = ns ;
           for( pp=0 ; pp < nd ; pp++ )
             nel->vec_axis_label[pp] = NI_strdup(sar->str[pp]) ;
           NI_delete_str_array(sar) ;
        }
     }

     /* see if we have vector label strings [11 Sep 2018] */

     ii = string_index( "ni_veclab" , nel->attr_num , nel->attr_lhs ) ;
     if( nel->vec_num > 0 && ii > 0 ){
       NI_set_veclab_from_stringlist( nel , nel->attr_rhs[ii] ) ;
     }

     /* supply vector parameters if none was given */
     /* (remember, we DON'T have an empty element) */

     if( nel->vec_num == 0 ){                    /* default type */
        nel->vec_num    = 1 ;
        nel->vec_typ    = NI_malloc(int,sizeof(int)) ;
        nel->vec_typ[0] = NI_BYTE ;
     }

     if( nel->vec_rank == 0 ){                  /* default dimensions */
        nel->vec_len         = 0 ;
        nel->vec_rank        = 1 ;
        nel->vec_axis_len    = NI_malloc(int, sizeof(int)) ;
        nel->vec_axis_len[0] = 1 ;
     }

     /* now allocate space for vectors defined above */

     nel->vec = NI_malloc(void*, sizeof(void *)*nel->vec_num ) ;

     /* 27 Mar 2003: only allocate space if we know how long they are */

     if( nel->vec_len > 0 ){
       for( ii=0 ; ii < nel->vec_num ; ii++ )
         nel->vec[ii] = NI_malloc(void,
                                  NI_type_size(nel->vec_typ[ii])*nel->vec_len) ;
     } else {
       for( ii=0 ; ii < nel->vec_num ; ii++ )
         nel->vec[ii] = NULL ;
     }

   } /* end of processing non-empty header stuff */

   return nel ;
}

/*-------------------------------------------------------------------------*/
/*! Make an empty group element from parsed header info.
    The attributes in the header are assigned to the group, and the group
    parts are initialized to nothing.
---------------------------------------------------------------------------*/

NI_group * make_empty_group_element( header_stuff *hs )
{
   NI_group *ngr ;

   if( hs == NULL || hs->name == NULL ) return NULL ;

   ngr = NI_malloc(NI_group, sizeof(NI_group) ) ;

   ngr->type = NI_GROUP_TYPE ;

   ngr->name = hs->name ; hs->name = NULL ;  /* 24 Feb 2005 */

   ngr->outmode = -1 ;   /* 29 Mar 2005 */

   /* move attributes from hs to new element */

   ngr->attr_num = hs->nattr ;

   if( ngr->attr_num > 0 ){
     ngr->attr_lhs = hs->lhs ; hs->lhs = NULL ;
     ngr->attr_rhs = hs->rhs ; hs->rhs = NULL ;
   } else {
     ngr->attr_lhs = ngr->attr_rhs = NULL ;
   }

   /* have no pieces-parts yet */

   ngr->part_num = 0 ;
   ngr->part_typ = NULL ;
   ngr->part     = NULL ;
   ngr->filename = NULL ; /* 16 Jun 2020 */

   return ngr ;
}

/*-------------------------------------------------------------------------*/
/*! Byte size of a given integer type code.
    Modified 13 Feb 2003 to use the new rowtype stuff.
---------------------------------------------------------------------------*/

int NI_type_size( int tval )
{
   int ii = NI_rowtype_code_to_size( tval ) ;
   return (ii > 0) ? ii : 0 ;
}

/*************************************************************************/
/********** Functions to create NIML data and group elements *************/
/*************************************************************************/

/*-----------------------------------------------------------------------*/
/*! Return the type of something that points to a NI element.
    - The input should be point to a NI_element, NI_group, or NI_procins.
    - The return value is NI_ELEMENT_TYPE, NI_GROUP_TYPE, NI_PROCINS_TYPE,
      or -1 if the type is anything else or unknowable.
-------------------------------------------------------------------------*/

int NI_element_type( void *nini )
{
   NI_element *nel = (NI_element *) nini ;
   NI_group   *ngr = (NI_group *)   nini ;
   NI_procins *npi = (NI_procins *) nini ;  /* 16 Mar 2005 */

   if( nini == NULL ) return -1 ;

   if( nel->type == NI_ELEMENT_TYPE ) return NI_ELEMENT_TYPE ;
   if( ngr->type == NI_GROUP_TYPE   ) return NI_GROUP_TYPE   ;
   if( npi->type == NI_PROCINS_TYPE ) return NI_PROCINS_TYPE ;

   return -1 ;
}

/*-----------------------------------------------------------------------*/
/*! Return the name of a NI element.  If the input is bad, returns
    a NULL pointer.  Do not free this pointer!  It points to the
    name string inside the element struct.
-------------------------------------------------------------------------*/

char * NI_element_name( void *nini )
{
   NI_element *nel = (NI_element *) nini ;
   NI_group   *ngr = (NI_group *)   nini ;
   NI_procins *npi = (NI_procins *) nini ;

   if( nini == NULL ) return NULL ;

   if( nel->type == NI_ELEMENT_TYPE ) return nel->name ;
   if( ngr->type == NI_GROUP_TYPE   ) return ngr->name ;
   if( npi->type == NI_PROCINS_TYPE ) return npi->name ;

   return NULL ;
}

/*-----------------------------------------------------------------------*/
/*! Expunge a data or group element and its contents from the universe.
-------------------------------------------------------------------------*/

void NI_free_element( void *nini )
{
   int ii , tt=NI_element_type(nini) ;

   if( tt < 0 ) return ; /* bad input */

   /*-- erase contents of data element --*/

   if( tt == NI_ELEMENT_TYPE ){
      NI_element *nel = (NI_element *)nini ;

      NI_free(nel->name) ;
      for( ii=0 ; ii < nel->attr_num ; ii++ ){
         NI_free( nel->attr_lhs[ii] ) ;
         NI_free( nel->attr_rhs[ii] ) ;
      }
      NI_free( nel->attr_lhs ) ;
      NI_free( nel->attr_rhs ) ;

      /* 14 Feb 2003: NI_free_column() will also free var dim arrays */

      if( nel->vec != NULL ){
        for( ii=0 ; ii < nel->vec_num ; ii++ ){
           NI_free_column( NI_rowtype_find_code(nel->vec_typ[ii]) ,
                           nel->vec_len , nel->vec[ii]             ) ;
        }
      }

      NI_free( nel->vec_typ  ) ;
      NI_free( nel->vec ) ;

      NI_free(nel->vec_axis_len) ;
      NI_free(nel->vec_axis_delta) ;
      NI_free(nel->vec_axis_origin) ;
      NI_free(nel->vec_axis_unit) ;
      NI_free(nel->vec_axis_label) ;

      if( nel->vec_lab != NULL ){  /* 12 Sep 2018 */
        for( ii=0 ; ii < nel->vec_num ; ii++ ) NI_free(nel->vec_lab[ii]) ;
        NI_free(nel->vec_lab) ;
      }

      NI_free( nel->filename ) ;  /* 16 Jun 2020 */

      NI_free( nel ) ;

   /*-- erase contents of group element --*/

   } else if( tt == NI_GROUP_TYPE ){
      NI_group *ngr = (NI_group *)nini ;

      for( ii=0 ; ii < ngr->attr_num ; ii++ ){
        NI_free( ngr->attr_lhs[ii] ) ;
        NI_free( ngr->attr_rhs[ii] ) ;
      }
      NI_free( ngr->attr_lhs ) ;
      NI_free( ngr->attr_rhs ) ;

      if( ngr->part != NULL ){
        for( ii=0 ; ii < ngr->part_num ; ii++ )
          NI_free_element( ngr->part[ii] ) ;     /* recursion */
      }

      NI_free( ngr->part_typ ) ;
      NI_free( ngr->part ) ;
      NI_free( ngr->name ) ;    /* 03 Jun 2002 */
      NI_free( ngr->filename ); /* 16 Jun 2020 */
      NI_free( ngr ) ;

   /*-- erase contents of processing instruction --*/

   } else if( tt == NI_PROCINS_TYPE ){
      NI_procins *npi = (NI_procins *)nini ;

      for( ii=0 ; ii < npi->attr_num ; ii++ ){
        NI_free( npi->attr_lhs[ii] ) ;
        NI_free( npi->attr_rhs[ii] ) ;
      }
      NI_free( npi->attr_lhs ) ;
      NI_free( npi->attr_rhs ) ;

      NI_free( npi->name ) ;    /* 03 Jun 2002 */
      NI_free( npi ) ;
   }

   return ;
}

/*-----------------------------------------------------------------------*/
/*! Expunge all data from element or group.           17 Jul 2006 [rickr]
-------------------------------------------------------------------------*/

void NI_free_element_data( void *nini )
{
   int ii , tt=NI_element_type(nini) ;

   if( tt < 0 ) return ; /* bad input */

   /*-- if element, nuke data --*/

   if( tt == NI_ELEMENT_TYPE ){
      NI_element *nel = (NI_element *)nini ;

      if( nel->vec != NULL ){
        for( ii=0 ; ii < nel->vec_num ; ii++ )
           NI_free_column( NI_rowtype_find_code(nel->vec_typ[ii]) ,
                           nel->vec_len , nel->vec[ii]             ) ;
         NI_free( nel->vec ) ;
         nel->vec = NULL ;
      }

   /*-- if group, recur --*/

   } else if( tt == NI_GROUP_TYPE ){
      NI_group *ngr = (NI_group *)nini ;

      if( ngr->part != NULL ){
        for( ii=0 ; ii < ngr->part_num ; ii++ )
          NI_free_element_data( ngr->part[ii] ) ;     /* recursion */
      }
   }

   /*-- no other cases for data --*/

   return ;
}

/*-----------------------------------------------------------------------*/

static void NI_init_veclen( NI_element *nel , int veclen )
{
   if( veclen == 0 ){                      /* empty element */
     nel->vec_len      = 0 ;
     nel->vec_filled   = 0 ;
     nel->vec_rank     = 0 ;
     nel->vec_axis_len = NULL ;
   } else {                                /* element with data to */
     nel->vec_len         = veclen ;       /* come via NI_add_column */
     nel->vec_filled      = veclen ;
     nel->vec_rank        = 1 ;
     nel->vec_axis_len    = NI_malloc(int, sizeof(int)) ;
     nel->vec_axis_len[0] = veclen ;
   }
}

/*-----------------------------------------------------------------------*/
/*! Create a new data element.

    - name   = string name for header.
    - veclen = size (length) of vectors (ni_dimen attribute).
               - Vectors are added with NI_add_column().
               - Set this to zero for "empty" elements (those with only
                 headers, no data).

    Return is NULL if inputs are stupid or criminal or insane.
-------------------------------------------------------------------------*/

NI_element * NI_new_data_element( char *name , int veclen )
{
   NI_element *nel ;

   if( name == NULL || name[0] == '\0' || veclen < 0 ) return NULL ;

   nel = NI_malloc(NI_element, sizeof(NI_element) ) ;

   nel->type = NI_ELEMENT_TYPE ;  /* mark as being a data element */

   nel->outmode = -1 ;   /* 29 Mar 2005 */

   nel->name = NI_strdup(name) ;
   nel->attr_num = 0 ;
   nel->attr_lhs = nel->attr_rhs = NULL ;  /* no attributes yes */

   nel->vec_num = 0 ;                      /* no vectors yet */
   nel->vec_typ = NULL ;
   nel->vec     = NULL ;
   nel->vec_lab = NULL ;  /* 11 Sep 2018 */
   nel->filename= NULL ;  /* 16 Jun 2020 */

   NI_init_veclen( nel , veclen ) ;  /* 19 Sep 2008 */

   nel->vec_axis_delta  = NULL ;
   nel->vec_axis_origin = NULL ;
   nel->vec_axis_unit   = NULL ;
   nel->vec_axis_label  = NULL ;

   return nel ;
}

/*-----------------------------------------------------------------------*/
/*! Insert a label for column #cc into a data element.
    Note that if column #cc does not exist, nothing will happen,
    so this function should be called AFTER NI_add_column(). [11 Sep 2018]
-------------------------------------------------------------------------*/

void NI_set_column_label( NI_element *nel , int cc , char *lab )
{

   if( nel == NULL || nel->vec_len <= 0 )            return ;
   if( nel->type != NI_ELEMENT_TYPE )                return ;
   if( cc < 0 || cc >= nel->vec_num || lab == NULL ) return ;

   if( nel->vec_lab == NULL ){  /* create empty label array */
     int pp ;
     nel->vec_lab = (char **)NI_malloc(char*,sizeof(char *)*nel->vec_num) ;
     for( pp=0 ; pp < nel->vec_num ; pp++ ) nel->vec_lab[pp] = NI_strdup("\0") ;
   }

   if( nel->vec_lab[cc] != NULL ) NI_free(nel->vec_lab[cc]) ;
   nel->vec_lab[cc] = NI_strdup(lab) ;
   return ;
}

/*-----------------------------------------------------------------------*/
/*! Add a vector (column) of data to a data element.

    - nel = data element to modify
    - typ = integer type code of data (e.g., NI_FLOAT)
    - arr = pointer to data values - must be an array of length veclen
            (same value as used in NI_new_data_element() call)
    - if arr is NULL, then will add a zero-filled column of the given
      type to the data element

    The data array is copied into the element.  If the element was
    specified with veclen=0, then this function will do nothing.
    Since this function has no return value, the only way to check for
    such an error is to see if nel->vec_num was incremented.  Or don't
    be so stupid as to make this error.
-------------------------------------------------------------------------*/

void NI_add_column( NI_element *nel , int typ , void *arr )
{
   int nn ;
   NI_rowtype *rt ;

   /* check for reasonable inputs */

   if( nel == NULL || nel->vec_len <= 0 )            return ;
   if( nel->type != NI_ELEMENT_TYPE )                return ;
   rt = NI_rowtype_find_code(typ) ; if( rt == NULL ) return ;

   /* get number of vectors currently in element */

   nn = nel->vec_num ;

   /* add 1 to the vec_typ array */

   nel->vec_typ     = NI_realloc( nel->vec_typ, int, sizeof(int)*(nn+1) ) ;
   nel->vec_typ[nn] = typ ;

   /* add 1 element to the vec array, and copy data into it */

   nel->vec = NI_realloc( nel->vec , void*, sizeof(void *)*(nn+1) ) ;
   if( arr != NULL )
     nel->vec[nn] = NI_copy_column( rt , nel->vec_len , arr ) ;
   else
     nel->vec[nn] = NI_malloc(void, rt->size * nel->vec_len ) ;

   if( nel->vec_lab != NULL ){  /* 11 Sep 2018 */
     nel->vec_lab     = NI_realloc( nel->vec_lab , char* , sizeof(char *)*(nn+1) ) ;
     nel->vec_lab[nn] = NI_strdup("\0") ;
   }

   /* add 1 to the count of vectors */

   nel->vec_num = nn+1 ;

   /* if element has "ni_type" attribute, adjust it   14 Jul 2006 [rickr] */
   if( NI_get_attribute(nel, "ni_type") )
      NI_set_ni_type_atr(nel) ;

   return ;
}

/*-------------------------------------------------------------------------*/
/*!
   Like add_column, but inserts the column at nel->vec[icol] rather than
   at the end.
   if icol < 0 || icol > nel->vec_num then icol = nel->vec_num
*/
void NI_insert_column( NI_element *nel , int typ , void *arr, int icol )
{
   int nn, ii ;
   NI_rowtype *rt ;

   /* check for reasonable inputs */

   if( nel == NULL || nel->vec_len <= 0 )            return ;
   if( nel->type != NI_ELEMENT_TYPE )                return ;
   rt = NI_rowtype_find_code(typ) ; if( rt == NULL ) return ;

   /* get number of vectors currently in element */
   nn = nel->vec_num ;

   if (icol > nn || icol < 0) icol = nn;

   /* call add column */
   NI_add_column(nel, typ, arr);

   /* check on success */
   if (nel->vec_num != nn+1) return ;  /* misere */
   nn = nel->vec_num ;  /* the new number of vectors */

   NI_move_column(nel, nn-1, icol);

   return ;
}

/*-------------------------------------------------------------------------*/
/*!
   move a column from index ibefore to iafter
   if ibefore (or iafter) is (< 0 || > nel->vec_num) then
      ibefore (or iafter) = nel->vec_num-1
*/
void NI_move_column(NI_element *nel, int ibefore, int iafter)
{
   int nn, ii ;
   int typ_buf;
   void *col_buf;
   char *lab_buf=NULL ;  /* 11 Sep 2018 */

   if (nel == NULL || nel->vec_len <= 0 )            return ;

   nn = nel->vec_num ;
   if (ibefore < 0 || ibefore >= nn) ibefore = nn-1;
   if (iafter  < 0 || iafter  >= nn) iafter  = nn-1;

   /* nothing to see here? */
   if (ibefore == iafter) return;

   /* do the deed */
   /* store the initial values */
   typ_buf = nel->vec_typ[ibefore];
   col_buf = nel->vec[ibefore];
   if( nel->vec_lab != NULL ) lab_buf = nel->vec_lab[ibefore] ;
   /* shift */
   if (ibefore > iafter) {
      /* shift columns to left*/
      for (ii=ibefore; ii > iafter; --ii) {
         nel->vec[ii] = nel->vec[ii-1];
         nel->vec_typ[ii] = nel->vec_typ[ii-1];
         if( nel->vec_lab != NULL )
           nel->vec_lab[ii] = nel->vec_lab[ii-1] ;
      }
   } else {
      /* shift columns to right*/
      for (ii=ibefore; ii < iafter; ++ii) {
         nel->vec[ii] = nel->vec[ii+1];
         nel->vec_typ[ii] = nel->vec_typ[ii+1];
         if( nel->vec_lab != NULL )
           nel->vec_lab[ii] = nel->vec_lab[ii+1] ;
      }
   }

   /* insert the trouble maker back*/
   nel->vec[iafter] = col_buf;
   nel->vec_typ[iafter] = typ_buf;
   if( nel->vec_lab != NULL )
     nel->vec_lab[iafter] = lab_buf ;

   /* house keeping */
   /* if element has "ni_type" attribute, adjust it   14 Jul 2006 [rickr] */
   if( NI_get_attribute(nel, "ni_type") )
      NI_set_ni_type_atr(nel) ;

   return ;
}

/*-------------------------------------------------------------------------*/
/*!
   Do we really need to document this too?        [[Cox says YES]]
   Removes column irm from nel. If irm < 0 or
   irm >= nel->vec_num irm = nel->vec_num -1
*/
void NI_remove_column(NI_element *nel, int irm)
{
   int nn;

   if (nel == NULL || nel->vec_len <= 0 )            return ;

   if (!(nn = nel->vec_num)) return;

   if (irm < 0 || irm >= nn) irm = nn-1;

   /* move irm to last column */
   NI_move_column(nel, irm, -1);

   /* free the last column */
   NI_free_column( NI_rowtype_find_code(nel->vec_typ[nn-1]) ,
                           nel->vec_len , nel->vec[nn-1]             ) ;
   nel->vec[nn-1] = NULL; /* to be sure */

   /* decrease the number of columns */
   --nn;
   nel->vec_num = nn;

   /* get rid of extra space */
   nel->vec_typ = NI_realloc( nel->vec_typ, int, sizeof(int)*(nn) ) ;
   nel->vec = NI_realloc( nel->vec , void*, sizeof(void *)*(nn) ) ;
   if( nel->vec_lab != NULL )
     nel->vec_lab = NI_realloc( nel->vec_lab , char*, sizeof(char *)*(nn) ) ;

   /* if element has "ni_type" attribute, adjust it   14 Jul 2006 [rickr] */
   if( NI_get_attribute(nel, "ni_type") )
      NI_set_ni_type_atr(nel) ;

   return;
}

/*------------------------------------------------------------------------*/
/*! Change the length of all the columns in a data element.
     - If the columns are longer, they will be zero filled.
     - New values can be inserted later with NI_insert_value().
     - If the columns are shorter, data will be lost.
     - You can use this to convert an element from empty to non-empty
       by entering newlen > 0 when the original vec_len is 0.
     - But, you cannot use this to convert an element to empty from
       non-empty, by entering newlen == 0 when vec_len > 0!
--------------------------------------------------------------------------*/

void NI_alter_veclen( NI_element *nel , int newlen )
{
   int oldlen , ii ;
   NI_rowtype *rt ;
   char *pt ;

   if( nel    == NULL || nel->type != NI_ELEMENT_TYPE ) return ;
   if( newlen <= 0                                    ) return ;

   if( nel->vec_num == 0 ){                       /* if have no data yet */
     nel->vec_len = nel->vec_filled = newlen; return;
   }

   if( nel->vec_len == 0 ) NI_init_veclen( nel , newlen ) ;  /* 19 Sep 2008 */

   oldlen = nel->vec_len ; if( oldlen == newlen ) return ;

   for( ii=0 ; ii < nel->vec_num ; ii++ ){
     rt = NI_rowtype_find_code( nel->vec_typ[ii] ) ;
     nel->vec[ii] = NI_realloc( nel->vec[ii] , void , rt->size * newlen ) ;
     if( oldlen < newlen ){
       pt = ((char *)nel->vec[ii]) + (rt->size * oldlen) ; /* zero fill */
       memset( pt , 0 , (newlen-oldlen)*rt->size ) ;       /* new data! */
     }
   }

   nel->vec_len = nel->vec_filled = newlen ; return ;
}

/*------------------------------------------------------------------------*/
/*! As in NI_add_column(), but adding every stride-th element from arr.
    Thus, arr should be at least nel->vec_len * stride elements long.
--------------------------------------------------------------------------*/

void NI_add_column_stride( NI_element *nel, int typ, void *arr, int stride )
{
   int nn , ii ;
   NI_rowtype *rt ;
   char *idat ;

   /* check for reasonable inputs */

   if( nel == NULL || nel->vec_len <= 0 )            return ;
   if( nel->type != NI_ELEMENT_TYPE )                return ;
   rt = NI_rowtype_find_code(typ) ; if( rt == NULL ) return ;

   /* add an empty column */

   NI_add_column( nel , typ , NULL ) ;
   if( arr == NULL ) return ;          /* no input data ==> we're done */

   /* loop over inputs and put them in one at a time */

   nn   = nel->vec_num-1 ;
   idat = (char *) arr ;

   for( ii=0 ; ii < nel->vec_len ; ii++ )
     NI_insert_value( nel , ii , nn , idat + (ii*stride*rt->size) ) ;

   return ;
}

/*-------------------------------------------------------------------------*/
/*!
   See NI_insert_column for inspiration
*/
void NI_insert_column_stride( NI_element *nel, int typ, void *arr, int stride, int icol )
{
   int nn , ii ;
   NI_rowtype *rt ;
   char *idat ;

   /* check for reasonable inputs */

   if( nel == NULL || nel->vec_len <= 0 )            return ;
   if( nel->type != NI_ELEMENT_TYPE )                return ;
   rt = NI_rowtype_find_code(typ) ; if( rt == NULL ) return ;

   /* get number of vectors currently in element */
   nn = nel->vec_num ;

   if (icol > nn || icol < 0) icol = nn;

   /* call add column_stride */
   NI_add_column_stride(nel, typ, arr, stride);

   /* check on success */
   if (nel->vec_num != nn+1) return ;  /* misere */
   nn = nel->vec_num ;  /* the new number of vectors */

   NI_move_column(nel, nn-1, icol);

   return ;
}


/*------------------------------------------------------------------------*/
/*! ZSS; Fills an already created column with values up to vec_filled
         the values in arr are inserted into nel->vec[nn]
--------------------------------------------------------------------------*/

void NI_fill_column_stride( NI_element *nel, int typ,
                            void *arr, int nn, int stride )
{
   int  ii , nf;
   NI_rowtype *rt ;
   char *idat ;

   /* check for reasonable inputs */

   if( nel == NULL || nel->vec_len <= 0 )            return ;
   if( nel->type != NI_ELEMENT_TYPE )                return ;
   rt = NI_rowtype_find_code(typ) ; if( rt == NULL ) return ;

   /* check for NULL column or other similar errors*/

   if( arr == NULL )                                 return ;
   if( nel->vec[nn] == NULL )                        return ;
   if( nn < 0 || nn >= nel->vec_num )                return ;
   if( typ != nel->vec_typ[nn] )                     return ;

   /* loop over inputs and put them in */

   if( nel->vec_filled > 0 && nel->vec_filled <= nel->vec_len )
     nf = nel->vec_filled ;
   else
     nf = nel->vec_len ;

   idat = (char *) arr ;

   for( ii=0 ; ii < nf ; ii++ )
     NI_insert_value( nel , ii , nn , idat + (ii*stride*rt->size) ) ;

   return ;
}

/*------------------------------------------------------------------------*/
/*! Replace the row-th value in the col-th column of the data element.
     - dat is the pointer to the data values to copy into the element.
     - The column must have been created with NI_add_column() before
       calling this function!
     - NOTE WELL: When the column type is NI_STRING, it is a mistake
       to call this function with dat being a pointer to the C string
       to insert.  Instead, dat should be a pointer to the pointer to
       the C string.  For example:
        - char *me = "RWCox" ;
        - WRONG:  NI_insert_value ( nel, 3,5,  me ) ; [Seg Fault ensues]
        - RIGHT:  NI_insert_value ( nel, 3,5, &me ) ;
        - RIGHT:  NI_insert_string( nel, 3,5,  me ) ;
        - The last case illustrates the NI_insert_string() function,
          which can be used to simplify insertion into a column
          of Strings; that function is just a simple wrapper to call
          NI_insert_value() properly.
        - The reason the first example is WRONG is that dat is supposed
          to point to the data to be stored.  In the case of a String,
          the data is the pointer to the C string.
--------------------------------------------------------------------------*/

void NI_insert_value( NI_element *nel, int row, int col, void *dat )
{
   NI_rowtype *rt ;
   char *cdat , *idat=(char *)dat , *qpt ;
   int jj , kk ;

   /* check for reasonable inputs */

   if( nel == NULL || idat == NULL        ) return ;
   if( nel->type    != NI_ELEMENT_TYPE    ) return ;
   if( nel->vec_len <= 0                  ) return ;
   if( row < 0     || row >= nel->vec_len ) return ;
   if( col < 0     || col >= nel->vec_num ) return ;

   rt = NI_rowtype_find_code( nel->vec_typ[col] ) ;
   if( rt == NULL )                         return ;

   cdat = (char *) nel->vec[col] ;   /* points to column data */
   cdat = cdat + rt->size * row ;    /* points to data to alter */

   /* shallow copy of input data over data now present */

   memcpy( cdat , idat , rt->size ) ;  /* cdat now contains input data */

   /* copy any var dim arrays inside */

   if( ROWTYPE_is_varsize(rt) ){
     for( jj=0 ; jj < rt->part_num ; jj++ ){            /* loop over parts */

       if( rt->part_typ[jj] == NI_STRING ){                   /* a string part */
         char **apt = (char **)(cdat+rt->part_off[jj]) ; /* *apt => input data */
         qpt = NI_strdup(*apt) ;                 /* qpt = copy of input string */
        *apt = qpt ;                     /* reset *apt to copy of input string */

       } else if( rt->part_dim[jj] >= 0 ){                /* var dim array */
         char **apt = (char **)(cdat+rt->part_off[jj]) ;   /* *apt => data */
         if( *apt != NULL ){
           kk  = ROWTYPE_part_dimen(rt,cdat,jj) * rt->part_rtp[jj]->size ;
           qpt = NI_malloc(char, kk) ; memcpy(qpt,*apt,kk) ; *apt = qpt ;
         }
       }
     }
   }

   return ;
}

/*------------------------------------------------------------------------*/

void NI_insert_string( NI_element *nel, int row, int col, char *str )
{
   if( nel == NULL || str == NULL         ) return ;
   if( nel->type   != NI_ELEMENT_TYPE     ) return ;
   if( row < 0     || row >= nel->vec_len ) return ;
   if( col < 0     || col >= nel->vec_num ) return ;
   if( nel->vec_typ[col] != NI_STRING     ) return ;

   NI_insert_value( nel , row,col , &str ); return ;
}

/*------------------------------------------------------------------------*/
/* Return a float value. If the data type does not pass NI_IS_NUMERIC_TYPE,
   or if any other thing is non-copasetic, then return value is 0.0f.
*//*----------------------------------------------------------------------*/

float NI_extract_float_value( NI_element *nel , int row , int col )
{
   char *cdat ;
   NI_rowtype *rt ;

   /* check for stoopid stufff */

   if( nel         == NULL                     ) return 0.0f ;
   if( nel->type   != NI_ELEMENT_TYPE          ) return 0.0f ;
   if( row < 0     || row >= nel->vec_len      ) return 0.0f ;
   if( col < 0     || col >= nel->vec_num      ) return 0.0f ;
   if( ! NI_IS_NUMERIC_TYPE(nel->vec_typ[col]) ) return 0.0f ;

   rt = NI_rowtype_find_code( nel->vec_typ[col] ) ;
   if( rt == NULL )                             return 0.0f ;

   cdat = (char *) nel->vec[col] ;   /* points to column data */
   cdat = cdat + rt->size * row ;    /* points to data to get */

   switch( nel->vec_typ[col] ){
      default: return 0.0f ;         /* should not happen */

      case NI_FLOAT:{
        float *ar = (float *)cdat ; return ar[0] ;
      }

      case NI_BYTE:{
        byte *ar = (byte *)cdat ; return (float)ar[0] ;
      }

      case NI_SHORT:{
        short *ar = (short *)cdat ; return (float)ar[0] ;
      }

      case NI_INT:{
        int *ar = (int *)cdat ; return (float)ar[0] ;
      }

      case NI_DOUBLE:{
        double *ar = (double *)cdat ; return (float)ar[0] ;
      }

      case NI_COMPLEX:{
        complex *ar = (complex *)cdat ;
        double x = (double)ar[0].r , y = (double)ar[0].i ;
        return (float)sqrt(x*x+y*y) ;
      }

      case NI_RGB:{
        rgb *ar = (rgb *)cdat ;
        float rr = (float)ar[0].r, gg = (float)ar[0].g, bb = (float)ar[0].b ;
        return (0.299f*rr+0.587f*gg+0.114f*bb) ;
      }

      case NI_RGBA:{
        rgba *ar = (rgba *)cdat ;
        float rr = (float)ar[0].r, gg = (float)ar[0].g, bb = (float)ar[0].b ;
        float aa = (float)ar[0].a ;
        return (0.299f*rr+0.587f*gg+0.114f*bb)*(aa/255.0f) ;
      }

    }

    return 0.0f ; /* unreachable */
}

/*------------------------------------------------------------------------*/
/* Return a malloc()-ed string value from one value in a data element.
   Adapted from NI_write_columns().
   Result should be free()-ed when you are done with it.
                                          [19 Jun 2020 - RWCox == NIMLman]
*//*----------------------------------------------------------------------*/

char * NI_extract_text_value( NI_element *nel , int row , int col )
{
   int fsiz , col_typ , ii ;
   NI_rowtype *rt ;
   char *ptr , *col_dat , *outstr ;

   /* check for stoopid stufff */

   if( nel       == NULL                ) return NULL ;
   if( nel->type != NI_ELEMENT_TYPE     ) return NULL ;
   if( row < 0   || row >= nel->vec_len ) return NULL ;
   if( col < 0   || col >= nel->vec_num ) return NULL ;

   col_typ = nel->vec_typ[col] ;             /* type of data in column */
   rt      = NI_rowtype_find_code( col_typ ) ;
   if( rt == NULL                       ) return NULL ; /* impossible? */

   fsiz    = rt->size ;                 /* fixed size of struct (w/padding) */
   col_dat = (char *)nel->vec[col] ;    /* ptr to column data */
   ptr     = col_dat + fsiz*row ;       /* ptr to row-th struct in column */

   outstr = (char *)malloc(sizeof(char)*999*rt->part_num) ; outstr[0] = '\0' ;

   /* write each part of this struct into the buffer */

   for( ii=0 ; ii < rt->part_num ; ii++ ){  /*-- loop over parts --*/

     if( rt->part_dim[ii] < 0 ){             /*-- a single value --*/
       NI_val_to_text( rt->part_rtp[ii],
                       ptr+rt->part_off[ii], outstr ) ;
     } else {      /* a compound data type with variable size data */
       int dim ;
       char **apt = (char **)(ptr+rt->part_off[ii]); /* data in struct */
                                                     /* is ptr to array */

       dim = ROWTYPE_part_dimen(rt,ptr,ii) ;         /* dimension of part */
       if( dim > 0 && *apt != NULL ){
         NI_multival_to_text( rt->part_rtp[ii] , dim , *apt , outstr ) ;
       }
     }
   }

   /* pare down the output string size */

   ii     = strlen(outstr) ;
   outstr = realloc( outstr , ii+1 ) ;
   return outstr ;
}

/*------------------------------------------------------------------------*/
/*! Remove an attribute, if it exists from a data or group element.
                                    ZSS Feb 09
--------------------------------------------------------------------------*/

void NI_kill_attribute( void *nini , char *attname  )
{
   int nn , tt=NI_element_type(nini) ;

   if( tt < 0 || attname == NULL || attname[0] == '\0' ) return ;

   /* input is a data element */

   if( tt == NI_ELEMENT_TYPE ){
      NI_element *nel = (NI_element *) nini ;

      /* see if name is already in element header */

      for( nn=0 ; nn < nel->attr_num ; nn++ )
         if( strcmp(nel->attr_lhs[nn],attname) == 0 ) break ;

      if( nn == nel->attr_num ){ /* not found, return */
        return;
      } else {
        NI_free(nel->attr_lhs[nn]) ;  /* free old attribute */
        NI_free(nel->attr_rhs[nn]) ;
        if ( nn < nel->attr_num-1 ) { /* move last attr to nn */
         nel->attr_lhs[nn] = nel->attr_lhs[nel->attr_num-1];
         nel->attr_lhs[nel->attr_num-1] = NULL;
         nel->attr_rhs[nn] = nel->attr_rhs[nel->attr_num-1];
         nel->attr_rhs[nel->attr_num-1] = NULL;
        }
        --nel->attr_num;
        /* reallocate */
        nel->attr_lhs = NI_realloc( nel->attr_lhs,
                                    char*, sizeof(char *)*(nel->attr_num) );
        nel->attr_rhs = NI_realloc( nel->attr_rhs,
                                    char*, sizeof(char *)*(nel->attr_num) );
      }

   /* input is a group element */

   } else if( tt == NI_GROUP_TYPE ){
      NI_group *ngr = (NI_group *) nini ;

      for( nn=0 ; nn < ngr->attr_num ; nn++ )
         if( strcmp(ngr->attr_lhs[nn],attname) == 0 ) break ;

      if( nn == ngr->attr_num ){
        return;
      } else {
        NI_free(ngr->attr_lhs[nn]) ;
        NI_free(ngr->attr_rhs[nn]) ;
        if ( nn < ngr->attr_num-1 ) { /* move last attr to nn */
         ngr->attr_lhs[nn] = ngr->attr_lhs[ngr->attr_num-1];
         ngr->attr_lhs[ngr->attr_num-1] = NULL;
         ngr->attr_rhs[nn] = ngr->attr_rhs[ngr->attr_num-1];
         ngr->attr_rhs[ngr->attr_num-1] = NULL;
        }
        --ngr->attr_num;
        /* reallocate */
        ngr->attr_lhs = NI_realloc( ngr->attr_lhs,
                                    char*, sizeof(char *)*(ngr->attr_num) );
        ngr->attr_rhs = NI_realloc( ngr->attr_rhs,
                                    char*, sizeof(char *)*(ngr->attr_num) );
      }

   /* input is a processing instruction */

   } else if( tt == NI_PROCINS_TYPE ){
      NI_procins *npi = (NI_procins *) nini ;

      for( nn=0 ; nn < npi->attr_num ; nn++ )
        if( strcmp(npi->attr_lhs[nn],attname) == 0 ) break ;

      if( nn == npi->attr_num ){
        return;
      } else {
        NI_free(npi->attr_lhs[nn]) ;
        NI_free(npi->attr_rhs[nn]) ;
        if ( nn < npi->attr_num-1 ) { /* move last attr to nn */
         npi->attr_lhs[nn] = npi->attr_lhs[npi->attr_num-1];
         npi->attr_lhs[npi->attr_num-1] = NULL;
         npi->attr_rhs[nn] = npi->attr_rhs[npi->attr_num-1];
         npi->attr_rhs[npi->attr_num-1] = NULL;
        }
        --npi->attr_num;
        /* reallocate */
        npi->attr_lhs = NI_realloc( npi->attr_lhs,
                                    char*, sizeof(char *)*(npi->attr_num) );
        npi->attr_rhs = NI_realloc( npi->attr_rhs,
                                    char*, sizeof(char *)*(npi->attr_num) );
      }

   }

   return ;
}

/*------------------------------------------------------------------------*/
/*! Add an attribute to a data or group element.
    If an attribute with the same attname already exists, then
    it will be replaced with this one.
--------------------------------------------------------------------------*/

void NI_set_attribute( void *nini , char *attname , char *attvalue )
{
   int nn , tt=NI_element_type(nini) ;

   if( tt < 0 || attname == NULL || attname[0] == '\0' ) return ;

   /* input is a data element */

   if( tt == NI_ELEMENT_TYPE ){
      NI_element *nel = (NI_element *) nini ;

      /* see if name is already in element header */

      for( nn=0 ; nn < nel->attr_num ; nn++ )
         if( strcmp(nel->attr_lhs[nn],attname) == 0 ) break ;

      /* if not, then add a header attribute */

      if( nn == nel->attr_num ){
        nel->attr_lhs = NI_realloc( nel->attr_lhs, char*, sizeof(char *)*(nn+1) );
        nel->attr_rhs = NI_realloc( nel->attr_rhs, char*, sizeof(char *)*(nn+1) );
        nel->attr_num = nn+1 ;
      } else {
        NI_free(nel->attr_lhs[nn]) ;  /* free old attribute */
        NI_free(nel->attr_rhs[nn]) ;
      }

      nel->attr_lhs[nn] = NI_strdup(attname) ;
      nel->attr_rhs[nn] = NI_strdup(attvalue);

   /* input is a group element */

   } else if( tt == NI_GROUP_TYPE ){
      NI_group *ngr = (NI_group *) nini ;

      for( nn=0 ; nn < ngr->attr_num ; nn++ )
         if( strcmp(ngr->attr_lhs[nn],attname) == 0 ) break ;

      if( nn == ngr->attr_num ){
        ngr->attr_lhs = NI_realloc( ngr->attr_lhs, char*, sizeof(char *)*(nn+1) );
        ngr->attr_rhs = NI_realloc( ngr->attr_rhs, char*, sizeof(char *)*(nn+1) );
        ngr->attr_num = nn+1 ;
      } else {
        NI_free(ngr->attr_lhs[nn]) ;
        NI_free(ngr->attr_rhs[nn]) ;
      }

      ngr->attr_lhs[nn] = NI_strdup(attname) ;
      ngr->attr_rhs[nn] = NI_strdup(attvalue);

   /* input is a processing instruction */

   } else if( tt == NI_PROCINS_TYPE ){
      NI_procins *npi = (NI_procins *) nini ;

      for( nn=0 ; nn < npi->attr_num ; nn++ )
        if( strcmp(npi->attr_lhs[nn],attname) == 0 ) break ;

      if( nn == npi->attr_num ){
        npi->attr_lhs = NI_realloc( npi->attr_lhs, char*, sizeof(char *)*(nn+1) );
        npi->attr_rhs = NI_realloc( npi->attr_rhs, char*, sizeof(char *)*(nn+1) );
        npi->attr_num = nn+1 ;
      } else {
        NI_free(npi->attr_lhs[nn]) ;
        NI_free(npi->attr_rhs[nn]) ;
      }

      npi->attr_lhs[nn] = NI_strdup(attname) ;
      npi->attr_rhs[nn] = NI_strdup(attvalue);
   }

   return ;
}

/*------------------------------------------------------------------------*/
/*! Copy all attributes from one element to the next.
    The function is not recursive and must have a target nel
    with no attributes.    ZSS Oct 2010
--------------------------------------------------------------------------*/

void NI_copy_all_attributes( void *nisrc , void *nitrg )
{
   int nn , ttsrc=NI_element_type(nisrc), tttrg=NI_element_type(nitrg) ;

   if( ttsrc < 0 || tttrg < 0 ) return ;
   if( tttrg != ttsrc) {
      fprintf(stderr,"Error NI_copy_all_attributes:\n"
                     "Src and trg elements must have same type.\n");
      return;
   }
   /* input is a data element */
   if( ttsrc == NI_ELEMENT_TYPE ){
      NI_element *nelsrc = (NI_element *) nisrc ;
      NI_element *neltrg = (NI_element *) nitrg ;
      if( neltrg->attr_num != 0) {
         /* don't allow for this now. Else, you need to clear
            all pre-existing ones, or just common pre-existing ones
            then continue below */
         fprintf(stderr,"Error NI_copy_all_attributes:\n"
                        "Must have no attributes in target element.\n");
         return;
      }

      neltrg->attr_lhs = NI_realloc ( neltrg->attr_lhs,
                                     char*, sizeof(char *)*nelsrc->attr_num);
      neltrg->attr_rhs = NI_realloc ( neltrg->attr_rhs,
                                     char*, sizeof(char *)*nelsrc->attr_num);
      neltrg->attr_num = nelsrc->attr_num;

      for( nn=0 ; nn < nelsrc->attr_num ; nn++ ) {
         neltrg->attr_lhs[nn] = NI_strdup(nelsrc->attr_lhs[nn]);
         neltrg->attr_rhs[nn] = NI_strdup(nelsrc->attr_rhs[nn]);
      }

   /* input is a group element */

   } else if( ttsrc == NI_GROUP_TYPE ){
      NI_group *ngrsrc = (NI_group *) nisrc ;
      NI_group *ngrtrg = (NI_group *) nitrg ;
      if( ngrtrg->attr_num != 0) {
         /* don't allow for this now. Else, you need to clear
            all pre-existing ones, or just common pre-existing ones
            then continue below */
         fprintf(stderr,"Error NI_copy_all_attributes:\n"
                        "Must have no attributes in target element.\n");
         return;
      }

      ngrtrg->attr_lhs = NI_realloc ( ngrtrg->attr_lhs,
                                     char*, sizeof(char *)*ngrsrc->attr_num);
      ngrtrg->attr_rhs = NI_realloc ( ngrtrg->attr_rhs,
                                     char*, sizeof(char *)*ngrsrc->attr_num);
      ngrtrg->attr_num = ngrsrc->attr_num;

      for( nn=0 ; nn < ngrsrc->attr_num ; nn++ ) {
         ngrtrg->attr_lhs[nn] = NI_strdup(ngrsrc->attr_lhs[nn]);
         ngrtrg->attr_rhs[nn] = NI_strdup(ngrsrc->attr_rhs[nn]);
      }

   /* input is a processing instruction */

   } else if( ttsrc == NI_PROCINS_TYPE ){
      NI_procins *npisrc = (NI_procins *) nisrc ;
      NI_procins *npitrg = (NI_procins *) nitrg ;
      if( npitrg->attr_num != 0) {
         /* don't allow for this now. Else, you need to clear
            all pre-existing ones, or just common pre-existing ones
            then continue below */
         fprintf(stderr,"Error NI_copy_all_attributes:\n"
                        "Must have no attributes in target element.\n");
         return;
      }

      npitrg->attr_lhs = NI_realloc ( npitrg->attr_lhs,
                                     char*, sizeof(char *)*npisrc->attr_num);
      npitrg->attr_rhs = NI_realloc ( npitrg->attr_rhs,
                                     char*, sizeof(char *)*npisrc->attr_num);
      npitrg->attr_num = npisrc->attr_num;

      for( nn=0 ; nn < npisrc->attr_num ; nn++ ) {
         npitrg->attr_lhs[nn] = NI_strdup(npisrc->attr_lhs[nn]);
         npitrg->attr_rhs[nn] = NI_strdup(npisrc->attr_rhs[nn]);
      }
   }

   return ;
}

/*------------------------------------------------------------------------*/
/*! Duplicate a niml group or element.
   Function tested with test program niccc and -dup option  ZSS Oct 2010
--------------------------------------------------------------------------*/

void *NI_duplicate(void *vel, byte with_data)
{
   void *vdup=NULL;
   int tt=-1;

   if (!vel) return(vdup);
   tt = NI_element_type(vel);
   if (tt == NI_ELEMENT_TYPE) {
      return(NI_duplicate_element(vel, with_data));
   } else if (tt == NI_GROUP_TYPE){
      return(NI_duplicate_group(vel, with_data));
   } else {
      fprintf(stderr,
         "Error NI_duplicate:\n"
         "Can only deal with elements on group types\n");
      return(NULL);
   }
   return(NULL);
}

/*------------------------------------------------------------------------*/
/*! Duplicate niml element only.
    Better use function NI_duplicate                     ZSS Oct 2010
--------------------------------------------------------------------------*/

void *NI_duplicate_element (void *vel, byte with_data)
{
   NI_element *ndup = NULL;
   NI_element *nel = (NI_element *)vel;
   NI_group *gel = (NI_group *)vel;
   NI_group *gdup = NULL;
   int tt=-1, i=0;
   void *vdup=NULL;

   if (!vel) return(vdup);

   tt = NI_element_type(vel);
   if (tt != NI_ELEMENT_TYPE) {
      fprintf(stderr,
         "Error NI_duplicate_element:\n"
         "Can only deal with elements\n");
      return(vdup);
   }

   ndup = NI_new_data_element(nel->name, nel->vec_len);

   /* copy the attributes */
   NI_copy_all_attributes(nel, ndup);

   if (with_data) {
      /* copy the columns */
      for (i=0; i<nel->vec_num; ++i) {
         NI_add_column(ndup, nel->vec_typ[i],(void *)nel->vec[i]);
      }
   }

   if( nel->vec_lab != NULL && ndup->vec_num > 0 ){  /* 12 Sep 2018 */
     NI_set_attribute_from_veclab_array( ndup , nel->vec_lab ) ;
   }

   if (tt == NI_ELEMENT_TYPE) return((void *)ndup);
   else return(vdup);
}

/*------------------------------------------------------------------------*/
/* Make a new element from a list of columns in the old element */

NI_element * NI_extract_columns( NI_element *nel , int nc , int *cc )
{
   NI_element *vnel ; int ii , jj ;

   if( NI_element_type(nel) != NI_ELEMENT_TYPE ) return(NULL) ;
   if( nc < 1 || cc == NULL                    ) return(NULL) ;

   vnel = NI_new_data_element(nel->name, nel->vec_len);

   NI_copy_all_attributes( nel, vnel ) ;

   /* copy desired columns */

   for( ii=0 ; ii < nc ; ii++ ){
     jj = cc[ii] ; if( jj < 0 || jj >= nel->vec_num ) continue ;
     NI_add_column( vnel , nel->vec_typ[jj] , (void *)nel->vec[jj] ) ;
     if( nel->vec_lab != NULL )
       NI_set_column_label( vnel , ii , nel->vec_lab[jj] ) ;
   }

   return(vnel) ;
}

/*------------------------------------------------------------------------*/
/*! Duplicate niml group only.
    Better use function NI_duplicate                        ZSS Oct 2010
--------------------------------------------------------------------------*/

void *NI_duplicate_group (void *vel, byte with_data)
{
   NI_group *gel = (NI_group *)vel;
   NI_group *gdup = NULL;
   void *vv=NULL;
   int tt=-1, i=0;
   void *vdup=NULL, *gg=NULL;

   if (!vel) return(vdup);

   tt = NI_element_type(vel);
   if (tt != NI_GROUP_TYPE) {
      fprintf(stderr,
         "Error NI_duplicate_group:\n"
         "Can only deal with groups in this function\n");
      return(vdup);
   }

   gdup = NI_new_group_element();
   NI_rename_group(gdup, gel->name);

   /* copy the attributes to gdup*/
   NI_copy_all_attributes(gel, gdup);

   /* copy all the elements */
   for (i=0; i<gel->part_num; ++i) {
      switch( gel->part_typ[i] ){
         /*-- a sub-group ==> recursion! --*/
         case NI_GROUP_TYPE:
            if (!(gg = NI_duplicate_group(gel->part[i], with_data))) {
               fprintf(stderr,
                  "Error NI_duplicate_group:\n"
                  "Failed at recursion\n");
               return(NULL);
            }
            NI_add_to_group(gdup, gg);
            break ;
         case NI_ELEMENT_TYPE:
            if (!(gg = NI_duplicate_element(gel->part[i],with_data))) {
               fprintf(stderr,
                  "Error NI_duplicate_element:\n"
                  "Failed at recursion\n");
               return(NULL);
            }
            NI_add_to_group(gdup, gg);
            break;
         default:
            fprintf(stderr,
                  "Error NI_duplicate_group:\n"
                  "No duplication implemented for type %d, ignoring.\n",
                  gel->part_typ[i]);
            break;
      }
   }
   return(gdup);
}

/*-----------------------------------------------------------------------*/
/*! Get an attribute with the given LHS name.  Returns a pointer to the
    RHS field in the element if the attribute name is found; otherwise
    returns NULL.  If the LHS is found, but the RHS is NULL, returns
    a pointer to an empty C string ("\0").  Do not free() the result
    from this function, since it points to the internal field
    of the element!
-------------------------------------------------------------------------*/

char * NI_get_attribute_generic( void *nini , char *attname ,
                                 int (*STRcomp)(const char *, const char *) )
{
   int nn , tt=NI_element_type(nini) ;
   static char *zorkon = "\0" ;

   if( tt < 0 || attname == NULL || attname[0] == '\0' ) return NULL ;

   /* input is a data element */

   if( tt == NI_ELEMENT_TYPE ){
      NI_element *nel = (NI_element *) nini ;

      for( nn=0 ; nn < nel->attr_num ; nn++ )
         if( STRcomp(nel->attr_lhs[nn],attname) == 0 ) break ;

      if( nn == nel->attr_num ) return NULL ;

      if( nel->attr_rhs[nn] == NULL ) return zorkon ;

      return nel->attr_rhs[nn] ;

   /* input is a group element */

   } else if( tt == NI_GROUP_TYPE ){
      NI_group *ngr = (NI_group *) nini ;

      for( nn=0 ; nn < ngr->attr_num ; nn++ )
         if( STRcomp(ngr->attr_lhs[nn],attname) == 0 ) break ;

      if( nn == ngr->attr_num ) return NULL ;

      if( ngr->attr_rhs[nn] == NULL ) return zorkon ;

      return ngr->attr_rhs[nn] ;

   /* input is a processing instruction */

   } else if( tt == NI_PROCINS_TYPE ){
      NI_procins *npi = (NI_procins *) nini ;

      for( nn=0 ; nn < npi->attr_num ; nn++ )
        if( STRcomp(npi->attr_lhs[nn],attname) == 0 ) break ;

      if( nn == npi->attr_num ) return NULL ;

      if( npi->attr_rhs[nn] == NULL ) return zorkon ;

      return npi->attr_rhs[nn] ;
   }

   return NULL ; /* should never be reached */
}

/*-----------------------------------------------------------------------*/
/* This stuff added 20 Aug 2019 [RWC] */

char * NI_get_attribute( void *nini , char *attname )
{
   return NI_get_attribute_generic( nini , attname, strcmp ) ;
}

char * NI_get_attribute_nocase( void *nini , char *attname )
{
   return NI_get_attribute_generic( nini , attname, strcasecmp ) ;
}

/*-----------------------------------------------------------------------*/
/*! Set the dimen attribute for a data element.
-------------------------------------------------------------------------*/

void NI_set_dimen( NI_element *nel , int rank , int *nd )
{
   int ii , ntot ;

   if( nel == NULL || nel->type != NI_ELEMENT_TYPE ||
       rank < 1    || nd == NULL                     ) return ; /* bad */

   for( ntot=1,ii=0 ; ii < rank ; ii++ ){
      if( nd[ii] <= 0 ) return ;                                /* bad */
      ntot *= nd[ii] ;
   }
   if( ntot != nel->vec_len ) return ;                          /* bad */

   nel->vec_rank = rank ;
   nel->vec_axis_len = NI_realloc( nel->vec_axis_len, int, sizeof(int)*rank ) ;
   memcpy( nel->vec_axis_len , nd , sizeof(int)*rank ) ;
   return ;
}

/*-----------------------------------------------------------------------*/
/*! Set the delta attribute for a data element.
    Do not call this function until NI_set_dimen() has been called,
    unless there is only 1 dimension (which is the default).
-------------------------------------------------------------------------*/

void NI_set_delta( NI_element *nel , float *del )
{
   if( nel == NULL       || nel->type != NI_ELEMENT_TYPE ||
       nel->vec_rank < 1 || del == NULL                    ) return ;

   nel->vec_axis_delta = NI_realloc( nel->vec_axis_delta , float,
                                     nel->vec_rank * sizeof(float) ) ;
   memcpy( nel->vec_axis_delta , del , nel->vec_rank * sizeof(float) ) ;
   return ;
}

/*-----------------------------------------------------------------------*/
/*! Set the origin attribute for a data element.
    Do not call this function until NI_set_dimen() has been called,
    unless there is only 1 dimension (which is the default).
-------------------------------------------------------------------------*/

void NI_set_origin( NI_element *nel , float *org )
{
   if( nel == NULL       || nel->type != NI_ELEMENT_TYPE ||
       nel->vec_rank < 1 || org == NULL                    ) return ;

   nel->vec_axis_origin = NI_realloc( nel->vec_axis_origin , float,
                                      nel->vec_rank * sizeof(float) ) ;
   memcpy( nel->vec_axis_origin , org , nel->vec_rank * sizeof(float) ) ;
   return ;
}

/*-----------------------------------------------------------------------*/
/*! Set the units attribute for a data element.
    Do not call this function until NI_set_dimen() has been called,
    unless there is only 1 dimension (which is the default).
-------------------------------------------------------------------------*/

void NI_set_units( NI_element *nel , char **units )
{
   int ii ;

   if( nel == NULL       || nel->type != NI_ELEMENT_TYPE ||
       nel->vec_rank < 1 || units == NULL                  ) return ;

   nel->vec_axis_unit = NI_realloc( nel->vec_axis_unit , char*,
                                    nel->vec_rank * sizeof(char *) ) ;
   for( ii=0 ; ii < nel->vec_rank ; ii++ )
      nel->vec_axis_unit[ii] = NI_strdup( units[ii] ) ;
   return ;
}

/*-----------------------------------------------------------------------*/
/*! Set the axes attribute for a data element.
    Do not call this function until NI_set_dimen() has been called,
    unless there is only 1 dimension (which is the default).
-------------------------------------------------------------------------*/

void NI_set_axes( NI_element *nel , char **ax )
{
   int ii ;

   if( nel == NULL       || nel->type != NI_ELEMENT_TYPE ||
       nel->vec_rank < 1 || ax == NULL                     ) return ;

   nel->vec_axis_label = NI_realloc( nel->vec_axis_label , char*,
                                     nel->vec_rank * sizeof(char *) ) ;
   for( ii=0 ; ii < nel->vec_rank ; ii++ )
      nel->vec_axis_label[ii] = NI_strdup( ax[ii] ) ;
   return ;
}

/*-----------------------------------------------------------------------*/
/*! Create a new processing instruction with a given 'target' name.
-------------------------------------------------------------------------*/

NI_procins * NI_new_processing_instruction( char *name )
{
   NI_procins *npi ;

   if( name == NULL || name[0] == '\0' ) return NULL ;

   npi = NI_malloc(NI_procins,sizeof(NI_procins)) ;

   npi->type = NI_PROCINS_TYPE ;
   npi->name = NI_strdup(name) ;

   npi->attr_num = 0 ;
   npi->attr_lhs = npi->attr_rhs = NULL ;

   return npi ;
}

/*-----------------------------------------------------------------------*/
/*! Create a new group element.
-------------------------------------------------------------------------*/

NI_group * NI_new_group_element(void)
{
   NI_group *ngr ;

   ngr = NI_malloc(NI_group, sizeof(NI_group) ) ;

   ngr->type = NI_GROUP_TYPE ;

   ngr->outmode = -1 ;   /* 29 Mar 2005 */

   ngr->attr_num = 0 ;
   ngr->attr_lhs = ngr->attr_rhs = NULL ;

   ngr->part_num = 0 ;
   ngr->part_typ = NULL ;
   ngr->part     = NULL ;
   ngr->name     = NULL ;  /* 03 Jun 2002 */
   ngr->filename = NULL ;  /* 16 Jun 2020 */

   return ngr ;
}

/*-----------------------------------------------------------------------*/
/*! Add an element to a group element.
-------------------------------------------------------------------------*/

void NI_add_to_group( NI_group *ngr , void *nini )
{
   int nn , tt=NI_element_type(nini) ;

   if( ngr == NULL || ngr->type != NI_GROUP_TYPE || tt < 0 ) return ;

   nn = ngr->part_num ;

   ngr->part_typ     = NI_realloc( ngr->part_typ , int, sizeof(int)*(nn+1) ) ;
   ngr->part_typ[nn] = tt ;
   ngr->part         = NI_realloc( ngr->part , void*, sizeof(void *)*(nn+1) );
   ngr->part[nn]     = nini ;
   ngr->part_num     = nn+1 ;
   return ;
}

/*-----------------------------------------------------------------------*/
/*! Remove an element from a group.  Does NOT delete the element;
    that is the caller's responsibility, if desired.
-------------------------------------------------------------------------*/

void NI_remove_from_group( NI_group *ngr , void *nini )  /* 16 Apr 2005 */
{
   int ii , nn , jj ;

   if( ngr == NULL || ngr->type != NI_GROUP_TYPE || nini == NULL ) return ;

   nn = ngr->part_num ;
   for( ii=0 ; ii < nn ; ii++ )       /* search for part */
     if( nini == ngr->part[ii] ) break ;
   if( ii == nn ) return ;            /* not found */

   for( jj=ii+1 ; jj < nn ; jj++ ){   /* move parts above down */
     ngr->part_typ[jj-1] = ngr->part_typ[jj] ;
     ngr->part    [jj-1] = ngr->part    [jj] ;
   }
   ngr->part[nn-1] = NULL ;    /* NULL-ify last part to be safe */
   ngr->part_num -- ;          /* reduce part count */
   return ;
}

/*-----------------------------------------------------------------------*/
/*! Rename a group element from the default - 03 Jun 2002.
-------------------------------------------------------------------------*/

void NI_rename_group( NI_group *ngr , char *nam )
{
   if( ngr == NULL || ngr->type != NI_GROUP_TYPE ) return ;
   NI_free( ngr->name ) ;
   ngr->name = NI_strdup(nam) ;
   return ;
}

/*-----------------------------------------------------------------------*/
/*! Return a list of all elements in a group that have a given name.
      - This is a 'shallow' search: if the group itself contains
        groups, these sub-groups are not searched.
      - Return value of function is number of elements found (might be 0).
      - If something is found, then *nipt is an array of 'void *', each
        of which points to a matching element.
      - The returned elements might be group or data elements.
      - Sample usage:
          - int n,i ; void **nelar ;
          - n = NI_search_group_shallow( ngr , "fred" , &nelar ) ;
          - for( i=0 ; i < n ; i++ ) do_something( nelar[ii] ) ;
          - if( n > 0 ) NI_free(nelar) ;
-------------------------------------------------------------------------*/

int NI_search_group_shallow( NI_group *ngr , char *enam , void ***nipt )
{
   void **nelar=NULL , *nini ;
   int ii , nn=0 ;
   char *nm ;

   if( ngr  == NULL || ngr->type != NI_GROUP_TYPE    ) return 0 ;
   if( enam == NULL || *enam == '\0' || nipt == NULL ) return 0 ;
   if( ngr->part_num == 0                            ) return 0 ;

   for( ii=0 ; ii < ngr->part_num ; ii++ ){
     nini = ngr->part[ii] ;
     nm   = NI_element_name( nini ) ;
     if( nm != NULL && strcmp(nm,enam) == 0 ){
                                  /* added *size  11 Jul 2006 [rickr] */
       nelar = (void **) NI_realloc(nelar,void*,(nn+1)*sizeof(void *)) ;
       nelar[nn++] = nini ;
     }
   }

   if( nn > 0 ) *nipt = nelar ;
   return nn ;
}

/*-----------------------------------------------------------------------*/
/*! Return a list of all elements in a group that have a given name.
      - This is a 'deep' search: if the group itself contains
        groups, these sub-groups are searched, etc.
      - If a group element has the name 'enam' AND a data element within
        that group has the name 'enam' as well, they will BOTH be returned
        in this list.
      - Return value of function is number of elements found (might be 0).
      - If something is found, then *nipt is an array of 'void *', each
        of which points to a matching element.
      - The returned elements might be group or data elements.
      - Sample usage:
          - int n,i ; void **nelar ;
          - n = NI_search_group_shallow( ngr , "fred" , &nelar ) ;
          - for( i=0 ; i < n ; i++ ) do_something( nelar[ii] ) ;
          - if( n > 0 ) NI_free(nelar) ;
-------------------------------------------------------------------------*/

int NI_search_group_deep( NI_group *ngr , char *enam , void ***nipt )
{
   void **nelar=NULL , *nini ;
   int ii , nn=0 ;
   char *nm ;

   if( ngr  == NULL || ngr->type != NI_GROUP_TYPE    ) return 0 ;
   if( enam == NULL || *enam == '\0' || nipt == NULL ) return 0 ;
   if( ngr->part_num == 0                            ) return 0 ;

   for( ii=0 ; ii < ngr->part_num ; ii++ ){
     nini = ngr->part[ii] ;
     nm   = NI_element_name( nini ) ;
     if( nm != NULL && strcmp(nm,enam) == 0 ){
       nelar = (void **) NI_realloc(nelar,void*,(nn+1)*sizeof(void *)) ;
       nelar[nn++] = nini ;
     }
     if( NI_element_type(nini) == NI_GROUP_TYPE ){  /* recursion */
       int nsub , jj ; void **esub ;
       nsub = NI_search_group_deep( nini , enam , &esub ) ;
       if( nsub > 0 ){
         nelar = (void **) NI_realloc(nelar,void*,(nn+nsub)*sizeof(void *)) ;
         for( jj=0 ; jj < nsub ; jj++ ) nelar[nn++] = esub[jj] ;
         NI_free(esub) ;
       }
     }
   }

   if( nn > 0 ) *nipt = nelar ;
   return nn ;
}

/*-----------------------------------------------------------------------*/
/*! add a ni_type attribute to the element            14 Jul 2006 [rickr]
    (based on NI_write_element())
-------------------------------------------------------------------------*/
void NI_set_ni_type_atr( NI_element * nel )
{
   char * buf ;  /* alloc in blocks of ~1K (bob noted names up to 255 chars) */
   char * posn ;
   int    ii, prev, count=0 ;
   int    req_len, total_len=1024 ;

   if( ! nel || nel->vec_num <= 0 ) return ;

   /* make enough space for a list of buffers, and init. to empty */
   /* -- NI_rowtype_define uses 255 as a max rowtype name length  */
   buf = (char *)NI_malloc(char, total_len * sizeof(char)) ;
   buf[0] = '\0' ;

   for( prev=-1, ii=0; ii < nel->vec_num; ii++ ){
      if( nel->vec_typ[ii] != prev ){   /* not the previous type */
         if( prev >= 0 ){               /* apply previous type now */
            posn = buf + strlen(buf);
            if( count > 1 ) sprintf(posn, "%d*%s,", count, NI_type_name(prev));
            else            sprintf(posn, "%s,",           NI_type_name(prev));
         }
         prev = nel->vec_typ[ii] ;   /* save new type code */
         count = 1 ;                 /* have 1 such type   */

         /* make sure there is enough space for the new code        */
         /* (old, new, and space for "5280*")   17 Jul 2006 [rickr] */
         req_len = strlen(buf) + strlen(NI_type_name(prev)) + 10 ;
         if( total_len < req_len )
            buf = (char *)NI_realloc(buf, char, (req_len+1024) * sizeof(char)) ;

      } else {                       /* same as previous type  */
         count++ ;                   /* so increment its count */
      }
   }

   /* now write the last type found */
   posn = buf + strlen(buf) ;
   if( count > 1 ) sprintf(posn, "%d*%s", count, NI_type_name(prev));
   else            sprintf(posn, "%s",           NI_type_name(prev));

   /* now add the string as an attribute, and nuke the old string */
   NI_set_attribute(nel, "ni_type", buf) ;

   NI_free(buf) ;

   return ;
}

/*-----------------------------------------------------------------------*/
/* Return a preview string for the top of a data element [22 Jun 2020].
   This string should be free()-ed after you are done with it.
*//*---------------------------------------------------------------------*/

char * NI_preview_string( NI_element *nel , int ntop , char *title )
{
   int *cwid , csum , ii , jj , qq , nview , ncol ;
   char *qpt,*zpt , *vstring ;

   if( NI_element_type(nel) != NI_ELEMENT_TYPE ) return NULL ;

   ncol = nel->vec_num ; if( ncol <= 0 ) return NULL ;
   cwid = (int *)malloc(sizeof(int)*ncol) ; /* column width */
   if( ntop < 1 ) ntop = 1 ;
   nview = (ntop < nel->vec_len) ? ntop : nel->vec_len ;

   /* scan for max width of each column */

   NI_set_raw_val_to_text(1) ;

   for( jj=0 ; jj < ncol ; jj++ ){         /* col #jj */
     cwid[jj] = 4 ;
     if( nel->vec_lab != NULL ){           /* col label */
       qq = NI_strlen(nel->vec_lab[jj]) ;
       if( qq > cwid[jj] ) cwid[jj] = qq ;
     }
     qpt = NI_rowtype_code_to_name( nel->vec_typ[jj] ) ; /* type name */
     qq  = NI_strlen(qpt) ;                       /* do NOT free qpt! */
     if( qq > cwid[jj] ) cwid[jj] = qq ;
     for( ii=0 ; ii < nview ; ii++ ){  /* col values down the rows */
       qpt = NI_extract_text_value( nel , ii , jj ) ;
       qq  = NI_strlen(qpt) ; free(qpt) ;
       if( qq > cwid[jj] ) cwid[jj] = qq ;
     }
   }

   /* sum up all widths, create output string */

   for( csum=jj=0 ; jj < ncol ; jj++ ) csum += cwid[jj]+2 ;
   if( title == NULL ) title = nel->filename ;
   jj = NI_strlen( title ) ;
   if( csum < jj+2 ) csum = jj+2 ;

   vstring = (char *)calloc( sizeof(char), (nview+5)*(csum+1)+666 ) ;

   if( jj > 0 ){
     jj = (csum-jj)/2 ;  /* spacing to center title */
     sprintf( vstring , "%-*s%s\n" , jj , " " , title ) ;
   }

   /* fill up output string */
   /* first row of printout = column labels (if any) */

   for( jj=0 ; jj < ncol ; jj++ ){         /* col #jj */
     qpt = (nel->vec_lab != NULL ) ? nel->vec_lab[jj] : NULL ;
     if( qpt != NULL ){
       sprintf( vstring+strlen(vstring), " %-*s ", cwid[jj], qpt ) ;
     } else {
       char qstr[32] ;
       sprintf(qstr,"[%d]",jj) ;
       sprintf( vstring+strlen(vstring), " %-*s ", cwid[jj], qstr ) ;
     }
   }
   sprintf( vstring+strlen(vstring), "\n" ) ;

#if 0
   /* next row = column type labels (e.g., "float") */

   for( jj=0 ; jj < ncol ; jj++ ){
     qpt = NI_rowtype_code_to_name( nel->vec_typ[jj] ) ;
     sprintf( vstring+strlen(vstring), " %-*s ", cwid[jj], qpt ) ;
   }
   sprintf( vstring+strlen(vstring), "\n" ) ;
#endif

#if 1
   /* next row = underlining of column labels */
#define DASHES "---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"
   for( jj=0 ; jj < ncol ; jj++ ){
     sprintf( vstring+strlen(vstring) ,
              " %*.*s " , cwid[jj] , cwid[jj] , DASHES ) ;
   }
   sprintf( vstring+strlen(vstring), "\n" ) ;
#endif

   /* then do data rows */

   for( ii=0 ; ii < nview ; ii++ ){
     for( jj=0 ; jj < ncol ; jj++ ){
       qpt = NI_extract_text_value( nel, ii, jj ) ;
       sprintf( vstring+strlen(vstring), " %-*s ", cwid[jj], qpt ) ;
       free(qpt) ; /* unlike the type labels, we free this string */
     }
     if( ii < nview-1 ) sprintf( vstring+strlen(vstring), "\n" ) ;
   }

   NI_set_raw_val_to_text(0) ;
   free(cwid) ;
   return vstring ;
}
