i/netcdf

Home Manual Packages Global Index Keywords Quick Reference |
    /*
       NETCDF.I

       Yorick procedures to open a netCDF file

       The definitive reference for netCDF files is:
       Anonymous FTP site: unidata.ucar.edu [128.117.140.3]

       File:               pub/netcdf/netcdf.tar.Z

       $Id: netcdf.i,v 1.1 1993/08/27 18:50:06 munro Exp $
     */
    /*    Copyright (c) 1994.  The Regents of the University of California.
                        All rights reserved.  */

    /* Note: I don't use netCDF files, so there are probably still some
             bugs in this code (DHM).  */

    local netcdf ;

    /* DOCUMENT nc_open, nc_create, nc_vardef, nc_enddef, nc_addrec

         are the main routines to read and write netCDF files.

         The ordinary openb function will also open netCDF files.

         Writing a netCDF file is more problematic in Yorick, since

         you must define the entire file structure before you write

         any data.  Therefore, the nc_create call returns only a

         "token" for nc_vardef, which you use to declare variables
         in the file.  When you are done declaring variables, you

         call nc_enddef, which returns an ordinary Yorick file object.

         You can then write data to the file (with f.var=value or

         save,f,var).  To add a record, you must use nc_addrec instead

         of add_record (nc_addrec updates the record count in the file).
     */

    /* ------------- netCDF interface --------------------------------- */

    func nc_open (_nc_open_filename, mode)

    /* DOCUMENT f= nc_open(filename, mode)
         opens a netCDF file FILENAME for reading or update as specified
         by MODE, which defaults to "rb".  Attributes and dimension names

         can be found in the three external variables nc_dims (an array of

         type NC_dim), nc_attrs (an array of type NC_attr), and nc_vars

         (an array of type NC_var) after this call.
         MODE should be either "rb" or "r+b"; nothing else makes sense.

         If FILENAME is an array of strings, exactly those files will be

         opened as a family (if possible).  Note that nc_open("myfile00")

         potentially opens myfile01, myfile02, and so on, as for openb,

         but that nc_open(["myfile00"]) opens myfile00 only.

       SEE ALSO: nc_create, nc_enddef, nc_attribute, nc_dimsof
     */
    {
      if (is_void(mode)) mode= "rb";
      f= open(_nc_open_filename(1), mode);

      if (raw_not_cdf(f)) return [];
      return f;
    }

    func raw_not_cdf (f)
    {
      i= array(char, 4);
      _read, f, 0, i;
      if (string(&i)!="CDF\001") return 1;  /* test magic number */

      xdr_primitives, f;

      numrecs= long(0);
      _read, f, 4, numrecs;

      /* Each of the major components of a netCDF file contains information
         not used by Yorick.  Specifically, dimensions are named, the file
         may have attributes, and any variable contained in the file may

         have attributes.  After a call to nc_open, this additional
         information is stored in the external variable nc_file.  */
      extern nc_file;

      pf= print(f);
      name= dir= "";

      sread, pf(where(strmatch(pf,"binary stream:"))), dir, name,
        format= "%s binary stream: %s";

      sread, pf(where(strmatch(pf,"In directory:"))), dir,
        format= " In directory: %s";

      address= 8;

      nc_dims= NC_ReadArray(f, address);

      nc_attrs= NC_ReadArray(f, address);

      nc_vars= NC_ReadArray(f, address);

      nc_file= NC_file(numrecs=numrecs, dims=&nc_dims, attrs=&nc_attrs,
    		   vars=&nc_vars, filename=dir+name);

      if (_nc_declare(f, nc_file)) {
        /* check for presence of a file family */
        dims= dimsof(_nc_open_filename);

        ifile= (!is_void(dims) && dims(1));

        while (!_nc_add_next_file(f, ifile)) {
          i= array(char, 4);
          _read, f, 0, i;
          if (string(&i)!="CDF\001") break;  /* test magic number */
          _read, f, 4, numrecs;
          if (!numrecs) continue;
          nc_file.numrecs= numrecs;
          address= 8;

          dims= NC_ReadArray(f, address);

          attrs= NC_ReadArray(f, address);

          vars= NC_ReadArray(f, address);

          if (numberof(dims)!=numberof(nc_dims) ||
    	  anyof(dims.size!=nc_dims.size) ||

    	  !_nc_declare(f, nc_file, vars))

            error, "File '"+_nc_open_filename(ifile)+"' has different structure";
        }
      }

      jr, f, 1;
      return 0;
    }

    func _nc_add_next_file (f, &ifile)
    {

      /* modified add_next file checks whether nc_open has been called
       * with an explicit list of file names

       * -- eventually this should be installed in openb generally
       */

      if (!ifile) return add_next_file(f);
      ifile+= 1;

      if (ifile>numberof(_nc_open_filename)) return 1;

      if (add_next_file(f, _nc_open_filename(ifile), 0))

        error, "File '"+_nc_open_filename(ifile)+"' not found";
      return 0;
    }

    func _nc_declare (f, ncf, check_vars)
    {
      nc_dims= *ncf.dims;
      nc_vars= *ncf.vars;
      numrecs= ncf.numrecs;

      if (!is_void(check_vars)) {

        if (numberof(nc_vars)!=numberof(check_vars) ||
    	anyof(nc_vars.name!=check_vars.name) ||
    	anyof(nc_vars.type!=check_vars.type) ||
    	anyof(nc_vars.len!=check_vars.len) ||
    	anyof(nc_vars.address!=check_vars.address)) return 0;
      }

      /* check whether there is an unlimited (history) dimension */

      if (is_void(nc_dims)) unlimited= -1;
      else {
        unlimited= (nc_dims.size==0);

        if (noneof(unlimited)) unlimited= -1;

        else unlimited= where(unlimited)(0);
      }

      /* first pass sets non-history variables */
      type_names= ["char", "char", "short", "long", "float", "double"];
      nvars= numberof(nc_vars);

      if (nvars) nonRecord= array(int, nvars);
      else nonRecord= 1;
      for (i=1 ; i<=nvars ; ++i) {
        var= nc_vars(i);
        dimlist= *var.dimlist;
        if (numberof(dimlist)) {
          dimlist+= 1;
          if (dimlist(1)==unlimited) continue;  /* record variable */

          dimlist= grow([numberof(dimlist)], nc_dims.size(dimlist(0:1:-1)));
        } else {
          dimlist= [0];
        }
        nonRecord(i)= 1;
        if (is_void(check_vars))

          add_variable, f, var.address, var.name, type_names(var.type), dimlist;
      }

      /* second pass sets history variables */
      if (nallof(nonRecord)) {
        if (is_void(check_vars))
          add_record, f;
        recList= where(!nonRecord);
        ha= min(nc_vars.address(recList));
        time_address= 0;
        time_type= 0;
        for (i=1 ; i<=nvars ; ++i) {
          if (nonRecord(i)) continue;
          var= nc_vars(i);
          dimlist= *var.dimlist;
          ++dimlist;

          if (numberof(dimlist) > 1) {

    	dimlist= grow([numberof(dimlist)-1], nc_dims.size(dimlist(0:2:-1)));
          } else {
    	dimlist= [0];
    	if (var.name=="time" && (var.type==5 || var.type==6)) {
    	  time_address= var.address;
    	  time_type= var.type;
    	}
          }
          if (is_void(check_vars))
    	add_variable, f, var.address-ha,
    	                 var.name, type_names(var.type), dimlist;
        }

        /* Note: If there is exactly one record variable, then the records
           need not begin on a 4-byte boundary.  This is only an issue for

           a single record variable of type char or short.  */

        if (numberof(recList)>1 || var.type>3) {
          hs= sum(nc_vars.len(recList));
        } else {
          hs= (var.type==3? 2 : 1);
          for (i=2 ; i<=1+dimlist(1) ; ++i) hs*= dimlist(i);
        }
        if (numrecs) {
          if (time_address) {
    	if (time_type==6) time= 0.0;
    	else time= 0.0f;
    	times= array(0.0, numrecs);
    	for (i=1 ; i<=numrecs ; ++i) {
    	  _read, f, time_address, time;
    	  time_address+= hs;
    	  times(i)= time;
    	}

    	add_record, f, times, , ha+hs*indgen(0:numrecs-1);
          } else {

    	add_record, f, , , ha+hs*indgen(0:numrecs-1);
          }
        }

        return 1;
      }

      return 0;
    }

    func nc_create (filename)

    /* DOCUMENT ncf= nc_create(filename)
         creates a netCDF file FILENAME.

         After this call, use nc_vardef to declare the netCDF variables.

         Then use nc_enddef to write the netCDF self-descriptive

         information.  Only after this are you free to actually write data.

       SEE ALSO: nc_open, nc_vardef, nc_attrdef, nc_enddef, nc_addrec,

                 nc_attribute, nc_dimsof
     */
    {

      return NC_file(filename=filename);
    }

    func nc_vardef (ncf, name, type, dims, template=, record=, dimnames=)

    /* DOCUMENT nc_vardef, ncf, name, type, dims, record=0/1

           -or- nc_vardef, ncf, name, type, record=0/1

           -or- nc_vardef, ncf, name, template=template, record=0/1

         define a variable in the NCF (returned by nc_create) with name

         NAME, type TYPE (as returned by typeof or structof), and dimensions

         DIMS (as returned by dimsof).  The template= keyword may be used

         instead of type and dims; the type and dims will be those of the

         TEMPLATE.  If dims is not specified, a scalar is assumed.  If the
         record= keyword is present and non-zero, the variable is a record
         variable; otherwise it is a non-record variable.

         You can use the dimnames= keyword to write specific dimension
         names into the netCDF file.  These are not useful to Yorick, but

         other codes may require them.  If two variables share a dimension
         name, the corresponding dimension must have the same length.  For
         example:

           nc_vardef, ncf, "theta", double, [1,nlat], dimnames=["latitude"]

           nc_vardef, ncf, "phi", double, [1,nlong], dimnames=["longitude"]

           nc_vardef, ncf, "elevation", double,
                      dimnames=["latitude","longitude"]
         A dimension name of "" lets Yorick invent a fake dimension name,
         as it does by default.  If dimnames= is present and the lengths
         of the dimensions have previously been defined, then the DIMS

         parameter is unnecessary, as in the "elevation" array in the example.

         You can use the nc_dimdef function to define a named dimension size
         before you define any variables with that dimension.

       SEE ALSO: nc_create, nc_attrdef, nc_enddef, nc_addrec, nc_dimdef
     */
    {
      nc_vars= *ncf.vars;
      if (!is_void(nc_vars) &&

          anyof(nc_vars.name==name)) error, "duplicate netCDF name: "+name;

      if (!is_void(template)) {

        type= typeof(template);
        dims= dimsof(template);
      } else {

        if (!is_array(type)) {

          if (type==char) type= "char";

          else if (type==short) type= "short";

          else if (type==long || type==int) type= "long";

          else if (type==float) type= "float";

          else if (type==double) type= "double";
          else type= "illegal";
        }

        if (is_void(dims) && is_void(dimnames)) dims= [0];
      }

      if (type=="int") type= "long";

      list= where(type==["char","char","short","long","float","double"]);

      if (!numberof(list)) error, "illegal netCDF data type: "+type;
      type= list(1);
      len= [1, 1, 2, 4, 4, 8](type);

      if (record && numberof(dims)) {

        if (numberof(dimnames) && numberof(dimnames)==dims(1))
          grow, dimnames, "";
        grow, dims, [0];
        dims(1)+= 1;
      }

      if (numberof(dimnames) && numberof(dims) &&
          numberof(dimnames)!=dims(1))

        error, "dimnames= keyword does not match variable: "+name;

      if (!numberof(dims) && numberof(dimnames)) {
        /* use existing named dimensions */
        eq_nocopy, nc_dims, *ncf.dims;

        if (is_void(nc_dims)) error, "no dimensions yet defined";
        ncnames= nc_dims.name;

        dims= array(0, numberof(dimnames));

        for (i=1 ; i<=numberof(dims) ; ++i) {

          list= where(ncnames==dimnames(i));

          if (!numberof(list)) error, "dimension not yet defined: "+dimnames(i);

          dims(numberof(dims)-i+1)= list(1)-1;
          size= nc_dims(list(1)).size;
          if (size) len*= size;
        }

      } else if (numberof(dims)>1) {
        eq_nocopy, nc_dims, *ncf.dims;
        ndims= dims(1);
        for (i=2 ; i<=1+ndims ; ++i) {
          size= dims(i);
          if (size) len*= size;

          if (numberof(dimnames) && strlen((dimname= dimnames(i-1)))) {
    	if (changed) ncf.dims= &nc_dims;

    	dims(i)= nc_dimdef(ncf, dimname, size);
    	eq_nocopy, nc_dims, *ncf.dims;
    	continue;

          } else if (numberof(nc_dims)) {
    	list= where((size==nc_dims.size) &

    		    (strpart(nc_dims.name,1:2)=="D_"));
    	if (numberof(list)) {
    	  dims(i)= list(1);
    	  continue;
    	}
    	dimname= "D_"+pr1(size);
          } else {
    	dimname= "D_"+pr1(size);
          }

          grow, nc_dims, [NC_dim(name=dimname,size=size)];
          dims(i)= numberof(nc_dims);
          ncf.dims= &nc_dims;
        }
        dims= dims(0:2:-1)-1;

      } else {
        dims= [];
      }

      grow, nc_vars, [NC_var(name= name, dimlist= &dims, type= type, len= len)];
      ncf.vars= &nc_vars;
    }

    func nc_dimdef (ncf, dimname, size)

    /* DOCUMENT nc_dimdef, ncf, dim_name, size

           -or- nc_dimdef, ncf, dim_name, "unlimited"
         define a named dimension.  The SIZE parameter is the length of
         the dimension, or the string "unlimited" for the unlimited
         dimension.  (The numerical value 0 is the same as "unlimited".)
         You can also define named dimensions implicitly using nc_vardef.
       SEE ALSO: nc_vardef
     */
    {
      if (size=="unlimited") size= 0;
      nc_dims= *ncf.dims;

      list= numberof(nc_dims)? where(nc_dims.name==dimname) : [];
      if (numberof(list)) {
        list= list(1);
        if (nc_dims(list).size!=size)

          error, "different sizes sepcified for dimension "+dimname;
        return list;

      } else if (size || !numberof(nc_dims) || allof(nc_dims.size)) {

          grow, nc_dims, [NC_dim(name=dimname,size=size)];
          ncf.dims= &nc_dims;
          return numberof(nc_dims);
      } else {

        error, "only one unlimited dimension is allowed";
      }
    }

    func nc_attrdef (ncf, attr_name, var_name, value)

    /* DOCUMENT nc_attrdef, ncf, attr_name, var_name, value
         sets the value of the netCDF attribute ATTR_NAME associated

         with variable VAR_NAME to VALUE (note that the data type of VALUE

         becomes the data type of the attribute).

         The NCF is the structure returned by nc_create; nc_attrdef

         must be called prior to nc_enddef, which actually writes the
         attribute data to the file.
         If VAR_NAME is omitted, ATTR_NAME refers to the whole file.

       SEE ALSO: nc_open, nc_dimsof, nc_create, nc_enddef, nc_attribute
     */
    {
      attr= [NC_attr(name=attr_name, data=&value(*))];
      if (is_void(var_name)) {
        ncf.attrs= &grow(*ncf.attrs, attr);
      } else {
        local vars;
        eq_nocopy, vars, *ncf.vars;
        if (is_void(vars) ||
    	!numberof((

    		   attrs= where(vars.name==var_name))))

          error, "file has no such variable: "+var_name;
        attrs= attrs(0);

        vars(attrs).attrs= &grow(*vars(attrs).attrs, attr);

        /* *ncf.vars already updated, since eq_nocopy used above */
      }
    }

    func nc_enddef (ncf)
    /* DOCUMENT f= nc_enddef(ncf)

         creates netCDF file NCF (returned by nc_create), and writes the self-
         descriptive information.  Returns the ordinary Yorick file object

         corresponding to the new file.  You are then free to write variables,

         or use the save or nc_addrec functions.

       SEE ALSO: nc_create, nc_addrec, nc_open, nc_attrdef, nc_dimsof
     */
    {
      /* first, need to fill in addresses */
      vars= *ncf.vars;
      dims= *ncf.dims;

      rec= array(0, numberof(vars));
      if (!is_void(dims)) {
        list= where(dims.size==0);
        if (numberof(list)) {
          /* record variables need to be sorted separately */
          list= list(1)-1;

          for (i=1 ; i<=numberof(vars) ; ++i) {
    	dimlist= *vars(i).dimlist;

    	if (!is_void(dimlist) && dimlist(1)==list) rec(i)= 1;
          }
        }
      }
      lens= vars.len;
      lens= 4*((lens+3)/4);
      list= where(!rec);
      if (numberof(list)) {
        addrs= lens(list)(cum);
        lens(list)= addrs(1:-1);
        rec_addr= addrs(0);
      } else {
        rec_addr= 0;
      }
      list= where(rec);

      if (numberof(list)) lens(list)= rec_addr + lens(list)(cum)(1:-1);

      rec_addr= _nc_desc_len(ncf);  /* actually address of 1st datum */
      vars.address= rec_addr + lens;
      ncf.vars= &vars;

      f= open(ncf.filename, "w+b");
      xdr_primitives, f;

      if (!numberof(where(!rec))) {

        /* work around laziness of not computing address in nc_addrec */

        add_variable, f, rec_addr-1, "?ignore_me?", char;
      }

      _nc_declare, f, ncf;

      _write, f, 0, (*pointer("CDF\001"))(1:4);
      _write, f, 4, 0;

      address= 8;

      NC_WriteArray, f, address, dims;

      NC_WriteArray, f, address, *ncf.attrs;

      NC_WriteArray, f, address, vars;

      return f;
    }

    func _nc_desc_len (ncf)
    {

      _write= _dummy_write;  /* overlay _write with noop */
      address= 8;

      NC_WriteArray, f, address, *ncf.dims;

      NC_WriteArray, f, address, *ncf.attrs;

      NC_WriteArray, f, address, *ncf.vars;
      return address;
    }

    func _dummy_write (f, a, v)
    {
      return sizeof(v);
    }

    func nc_addrec (f, time)
    /* DOCUMENT nc_addrec, f, time
           -or- nc_addrec, f
         adds a new record to the netCDF file F at time TIME.

       SEE ALSO: nc_create, nc_vardef, nc_enddef
     */
    {
      add_record, f, time, , -1;
      files= *get_addrs(f)(4);

      _write,f,4, sum(files==files(0));
    }

    func nc_attribute (attr_name, var_name)

    /* DOCUMENT value= nc_attribute(attr_name, var_name)
         gets the value of the netCDF attribute ATTR_NAME associated
         with variable VAR_NAME, or nil if none.  Uses the external
         variable nc_file set by nc_open.
         If VAR_NAME is omitted, ATTR_NAME refers to the whole file,
         and is retrieved (if present) from the nc_file.attrs variable.

       SEE ALSO: nc_open, nc_attrdef, nc_dimsof, nc_create, nc_enddef
     */
    {

      if (is_void(var_name)) attrs= *nc_file.attrs;
      else {

        attrs= where(nc_file.vars->name==var_name);

        if (!numberof(attrs)) return [];
        attrs= *(nc_file.vars->attrs(attrs(0)));
      }
      if (is_void(attrs)) return [];
      value= where(attrs.name==attr_name);
      if (!numberof(value)) return [];
      data= *attrs(value(0)).data;

      if (numberof(data)==1) return data(1);
      else return data;
    }

    func nc_dimsof (var_name)

    /* DOCUMENT def_string= nc_dimsof(var_name)
         returns the dimension list of a netCDF variable VAR_NAME in symbolic
         form, i.e.- using the netCDF dimension names.  This requires the
         nc_file external variable set by nc_open.

       SEE ALSO: nc_open, nc_dimsof, nc_create, nc_enddef
     */
    {
      dims= (nc_file.vars.name==var_name);
      if (noneof(dims)) return [];

      dims= *nc_file.vars(where(dims)(0)).dimlist;
      result= "(";
      n= numberof(dims);
      for (i=n ; i>0 ; --i) {

        result+= swrite(format="%ld", nc_file.dims(dims(i)).size);
        if (i) result+= ",";
      }
      return result+")";
    }

    /* ------------- data structures --------------------------------- */

    /* nc_dims is an array of NC_dim */
    struct NC_dim {
      string name;  /* represented as NC_string in file */
      long size;  /* length of dimension */
    }

    /* nc_attrs is an array of NC_attr */
    struct NC_attr {
      string name;    /* represented as NC_string in file */
      pointer data;   /* represented as NC_array in file */
    }

    /* nc_vars is an array of NC_var */
    struct NC_var {
      string name;         /* represented as NC_string in file */
      pointer dimlist;     /* represented as NC_iarray in file, dims index */
      pointer attrs;       /* represented as NC_array in file */

      int type;   /* netCDF data type number */
      long len;   /* bytes */
      long address;
    }

    struct NC_file {
      string filename;     /* for later creation */
      long numrecs;
      pointer dims, attrs, vars;
    }

    /* ------------- object read routines --------------------------------- */

    func NC_ReadDim (f, &address)
    {

      name= NC_ReadString(f, address);
      size= long(0);
      _read, f, address, size;
      address+= 4;

      return NC_dim(name=name, size=size);
    }

    func NC_ReadAttr (f, &address)
    {

      name= NC_ReadString(f, address);

      data= &(NC_ReadArray(f, address));

      return NC_attr(name= name, data= data);
    }

    func NC_ReadString (f, &address)
    {
      count= long(0);
      _read, f, address, count;
      address+= 4;
      if (count<=0) return string();
      result= array(char, count);
      _read, f, address, result;
      address+= 4*((count+3)/4);
      return string(&result);
    }

    func NC_ReadInts (f, &address)
    {
      count= long(0);
      _read, f, address, count;
      address+= 4;
      if (count<=0) return &[];
      result= array(int, count);
      _read, f, address, result;
      address+= 4*count;
      return &result;
    }

    func NC_ReadVar (f, &address)
    {

      name= NC_ReadString(f, address);

      dimlist= NC_ReadInts(f, address);

      attrs= &(NC_ReadArray(f, address));   /* must be type NC_attr */
      type= int(0);

      _read, f, address, type;
      address+= 4;
      len= addr= long(0);
      _read, f, address, len;
      address+= 4;
      _read, f, address, addr;
      address+= 4;

      return NC_var(name= name, dimlist= dimlist, attrs= attrs, type= type,
    		len= len, address= addr);
    }

    func NC_ReadArray (f, &address)
    {
      type= int(0);

      _read, f, address, type;
      address+= 4;
      count= long(0);
      _read, f, address, count;
      address+= 4;

      if (type<=0 || type>12 || count<=0)
        return [];

      else if (type<=2)     /* byte (1) or char (2) */

        result= NC_ReadPrim(f, address, char, 1, count);
      else if (type==3)     /* short */

        result= NC_ReadPrim(f, address, short, 2, count);
      else if (type==4)     /* long */

        result= NC_ReadPrim(f, address, long, 4, count);
      else if (type==5)     /* float */

        result= NC_ReadPrim(f, address, float, 4, count);
      else if (type==6)     /* double */

        result= NC_ReadPrim(f, address, double, 8, count);

      else if (type==7)     /* bitfield */
        return [];
      else if (type==8)     /* string */

        result= NC_ReadMulti(f, address, string, count, NC_ReadString);
      else if (type==9)     /* iarray */

        result= NC_ReadMulti(f, address, pointer, count, NC_ReadInts);

      else if (type==10)    /* dimension */

        result= NC_ReadMulti(f, address, NC_dim, count, NC_ReadDim);

      else if (type==11)    /* variable */

        result= NC_ReadMulti(f, address, NC_var, count, NC_ReadVar);

      else if (type==12)    /* attribute */

        result= NC_ReadMulti(f, address, NC_attr, count, NC_ReadAttr);

      /* presume that type char means a string */

      if (type==2) result= string(&result);

      return result;
    }

    func NC_ReadPrim (f, &address, type, size, count)
    {

      result= array(type, count);
      _read, f, address, result;
      address+= 4*((size*count+3)/4);
      return result;
    }

    func NC_ReadMulti (f, &address, type, count, Reader)
    {

      result= array(type, count);
      for (i=1 ; i<=count ; ++i) result(i)= Reader(f, address);
      return result;
    }

    /* ------------- object write routines --------------------------------- */

    func NC_WriteDim (f, &address, dim)
    {

      NC_WriteString, f, address, dim.name;
      size= long(0);
      _write, f, address, dim.size;
      address+= 4;
    }

    func NC_WriteAttr (f, &address, attr)
    {

      NC_WriteString, f, address, attr.name;

      NC_WriteArray, f, address, *attr.data;
    }

    func NC_WriteString (f, &address, text)
    {

      _write, f, address, strlen(text);
      address+= 4;
      if (strlen(text)<=0) return;

      _write, f, address, (*pointer(text))(1:-1);
      address+= 4*((strlen(text)+3)/4);
    }

    func NC_WriteInts (f, &address, vals)
    {
      vals= *vals;

      _write, f, address, numberof(vals);
      address+= 4;
      if (numberof(vals)<=0) return;
      _write, f, address, long(vals);
      address+= 4*numberof(vals);
    }

    func NC_WriteVar (f, &address, var)
    {

      NC_WriteString, f, address, var.name;

      NC_WriteInts, f, address, var.dimlist;

      NC_WriteArray, f, address, *var.attrs;   /* must be type NC_attr */
      _write, f, address, var.type;
      address+= 4;
      _write, f, address, var.len;
      address+= 4;
      _write, f, address, var.address;
      address+= 4;
    }

    func NC_WriteArray (f, &address, ary)
    {

      type= nameof(structof(ary));
      if (type=="int") {
        ary= long(ary);
        type= "long";
      }

      type= where(type==["char","char","short","long","float","double", "",
    		     "NC_string", "NC_iarray", "NC_dim", "NC_var", "NC_attr"]);

      if (!numberof(type)) {
        if (is_void(ary))
          type= 0;

        else if (structof(ary)==string && numberof(ary)==1)

          { ary= *pointer(ary(1));  type= 2; }
        else

          error, "illegal netCDF data type: "+nameof(structof(ary));
      } else {

        type= type(1);
      }

      _write, f, address, type;
      address+= 4;
      count= long(0);

      _write, f, address, numberof(ary);
      address+= 4;

      if (type<=0 || type>12 || !numberof(ary))
        return;

      else if (type<=2)     /* byte (1) or char (2) */

        NC_WritePrim, f, address, char, 1, ary;
      else if (type==3)     /* short */

        NC_WritePrim, f, address, short, 2, ary;
      else if (type==4)     /* long */

        NC_WritePrim, f, address, long, 4, ary;
      else if (type==5)     /* float */

        NC_WritePrim, f, address, float, 4, ary;
      else if (type==6)     /* double */

        NC_WritePrim, f, address, double, 8, ary;

      else if (type==7)     /* bitfield */

        error, "netCDF bitfield data not supported";
      else if (type==8)     /* string */

        NC_WriteMulti, f, address, string, NC_WriteString, ary;
      else if (type==9)     /* iarray */

        NC_WriteMulti, f, address, pointer, NC_WriteInts, ary;

      else if (type==10)    /* dimension */

        NC_WriteMulti, f, address, NC_dim, NC_WriteDim, ary;

      else if (type==11)    /* variable */

        NC_WriteMulti, f, address, NC_var, NC_WriteVar, ary;

      else if (type==12)    /* attribute */

        NC_WriteMulti, f, address, NC_attr, NC_WriteAttr, ary;
    }

    func NC_WritePrim (f, &address, type, size, data)
    {
      _write, f, address, data;

      address+= 4*((size*numberof(data)+3)/4);
    }

    func NC_WriteMulti (f, &address, type, Writer, data)
    {
      count= numberof(data);
      for (i=1 ; i<=count ; ++i) Writer, f, address, data(i);
    }

    /* ------------- end of netCDF routines --------------------------------- */