All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ucar.nc2.Variable Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2;

import ucar.ma2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.iosp.IospHelper;
import ucar.nc2.util.CancelTask;
import ucar.nc2.util.Indent;
import ucar.nc2.util.rc.RC;

import java.io.OutputStream;
import java.util.*;
import java.io.IOException;
import java.nio.channels.WritableByteChannel;

/**
 * A Variable is a logical container for data. It has a dataType, a set of Dimensions that define its array shape,
 * and optionally a set of Attributes.
 * 

* The data is a multidimensional array of primitive types, Strings, or Structures. * Data access is done through the read() methods, which return a memory resident Array. *

Immutable if setImmutable() was called. * * @author caron * @see ucar.ma2.Array * @see ucar.ma2.DataType */ public class Variable extends CDMNode implements VariableIF, ProxyReader, AttributeContainer { /** * Globally permit or prohibit caching. For use during testing and debugging. *

* A {@code true} value for this field does not indicate whether a Variable * {@link #isCaching() is caching}, only that it's permitted to cache. */ static public boolean permitCaching = true; static public int defaultSizeToCache = 4000; // bytes cache any variable whose size() < defaultSizeToCache static public int defaultCoordsSizeToCache = 40 * 1000; // bytes cache coordinate variable whose size() < defaultSizeToCache static protected boolean debugCaching = false; static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Variable.class); static public String getDAPName(String name, Variable context) { if (RC.getUseGroups()) { // leave off leading '/' for root entries Group xg = context.getParentGroup(); if (!xg.isRoot()) { // Get the list of parent groups List path = Group.collectPath(xg); Formatter dapname = new Formatter(); for (int i = 1; i < path.size(); i++) { // start at 1 to skip root group Group g = path.get(i); dapname.format("/%s", g.getShortName()); } dapname.format("/%s", name); name = dapname.toString(); } } return name; } static public String getDAPName(Variable v) { return Variable.getDAPName(v.getShortName(), v); } ////////////////////////////////////////////////// // Instance data and methods protected NetcdfFile ncfile; // physical container for this Variable; where the I/O happens. may be null if Variable is self contained. protected int[] shape = new int[0]; protected Section shapeAsSection; // derived from the shape, immutable; used for every read, deferred creation protected DataType dataType; protected int elementSize; protected List dimensions = new ArrayList<>(5); protected AttributeContainerHelper attributes; protected boolean isVariableLength = false; protected boolean isMetadata = false; protected Cache cache = new Cache(); // cache cannot be null protected int sizeToCache = -1; // bytes protected ProxyReader proxyReader = this; /** * Get the data type of the Variable. */ public DataType getDataType() { return dataType; } /** * Get the shape: length of Variable in each dimension. * A scalar (rank 0) will have an int[0] shape. * * @return int array whose length is the rank of this Variable * and whose values equal the length of that Dimension. */ public int[] getShape() { int[] result = new int[shape.length]; // optimization over clone() System.arraycopy(shape, 0, result, 0, shape.length); return result; } // if scalar, return int[1], else return getShape() public int[] getShapeNotScalar() { if (isScalar()) return new int[]{1}; return getShape(); } /** * Get the size of the ith dimension * * @param index which dimension * @return size of the ith dimension */ public int getShape(int index) { return shape[index]; } /** * Get the total number of elements in the Variable. * If this is an unlimited Variable, will use the current number of elements. * If this is a Sequence, will return 1. * If variable length, will skip vlen dimensions * * @return total number of elements in the Variable. */ public long getSize() { long size = 1; for (int aShape : shape) { if (aShape >= 0) size *= aShape; } return size; } /** * Get the number of bytes for one element of this Variable. * For Variables of primitive type, this is equal to getDataType().getSize(). * Variables of String type dont know their size, so what they return is undefined. * Variables of Structure type return the total number of bytes for all the members of * one Structure, plus possibly some extra padding, depending on the underlying format. * Variables of Sequence type return the number of bytes of one element. * * @return total number of bytes for the Variable */ public int getElementSize() { return elementSize; } /** * Get the number of dimensions of the Variable. * * @return the rank */ public int getRank() { return shape.length; } /** * Get the parent group. * * @return group of this variable; if null return rootgroup */ public Group getParentGroup() { Group g = super.getParentGroup(); if (g == null) { g = ncfile.getRootGroup(); super.setParentGroup(g); } assert g != null; return g; } /** * Is this variable metadata?. True if its values need to be included explicitly in NcML output. * * @return true if Variable values need to be included in NcML */ public boolean isMetadata() { return isMetadata; } /** * Whether this is a scalar Variable (rank == 0). * * @return true if Variable has rank 0 */ public boolean isScalar() { return getRank() == 0; } /** * Does this variable have a variable length dimension? * If so, it has as one of its dimensions Dimension.VLEN. * * @return true if Variable has a variable length dimension? */ public boolean isVariableLength() { return isVariableLength; } /** * Can this variable's size grow?. * This is equivalent to saying at least one of its dimensions is unlimited. * * @return boolean true iff this variable can grow */ public boolean isUnlimited() { for (Dimension d : dimensions) { if (d.isUnlimited()) return true; } return false; } /** * Get the list of dimensions used by this variable. * The most slowly varying (leftmost for Java and C programmers) dimension is first. * For scalar variables, the list is empty. * * @return List, immutable */ public java.util.List getDimensions() { return dimensions; } /** * Get the ith dimension. * * @param i index of the dimension. * @return requested Dimension, or null if i is out of bounds. */ public Dimension getDimension(int i) { if ((i < 0) || (i >= getRank())) return null; return dimensions.get(i); } /** * Get the list of Dimension names, space delineated. * * @return Dimension names, space delineated */ public String getDimensionsString() { return Dimension.makeDimensionsString(dimensions); } /** * Find the index of the named Dimension in this Variable. * * @param name the name of the dimension * @return the index of the named Dimension, or -1 if not found. */ public int findDimensionIndex(String name) { for (int i = 0; i < dimensions.size(); i++) { Dimension d = dimensions.get(i); if (name.equals(d.getShortName())) return i; } return -1; } /** * Get the description of the Variable. * Default is to use CDM.LONG_NAME attribute value. If not exist, look for "description", "title", or * "standard_name" attribute value (in that order). * * @return description, or null if not found. */ public String getDescription() { String desc = null; Attribute att = findAttributeIgnoreCase(CDM.LONG_NAME); if ((att != null) && att.isString()) desc = att.getStringValue(); if (desc == null) { att = findAttributeIgnoreCase("description"); if ((att != null) && att.isString()) desc = att.getStringValue(); } if (desc == null) { att = findAttributeIgnoreCase(CDM.TITLE); if ((att != null) && att.isString()) desc = att.getStringValue(); } if (desc == null) { att = findAttributeIgnoreCase(CF.STANDARD_NAME); if ((att != null) && att.isString()) desc = att.getStringValue(); } return desc; } /** * Get the Unit String for the Variable. * Looks for the CDM.UNITS attribute value * * @return unit string, or null if not found. */ public String getUnitsString() { String units = null; Attribute att = findAttribute(CDM.UNITS); if (att == null) att = findAttributeIgnoreCase(CDM.UNITS); if ((att != null) && att.isString()) { units = att.getStringValue(); if (units != null) units = units.trim(); } return units; } /** * Get shape as an List of Range objects. * The List is immutable. * * @return List of Ranges, one for each Dimension. */ public List getRanges() { return getShapeAsSection().getRanges(); } /** * Get shape as a Section object. * * @return Section containing List, one for each Dimension. */ public Section getShapeAsSection() { if (shapeAsSection == null) { try { List list = new ArrayList<>(); for (Dimension d : dimensions) { int len = d.getLength(); if (len > 0) list.add(new Range(d.getShortName(), 0, len - 1)); else if (len == 0) list.add( Range.EMPTY); // LOOK empty not named else { assert d.isVariableLength(); list.add( Range.VLEN); // LOOK vlen not named } } shapeAsSection = new Section(list).makeImmutable(); } catch (InvalidRangeException e) { log.error("Bad shape in variable " + getFullName(), e); throw new IllegalStateException(e.getMessage()); } } return shapeAsSection; } public ProxyReader getProxyReader() { return proxyReader; } public void setProxyReader(ProxyReader proxyReader) { this.proxyReader = proxyReader; } /** * Create a new Variable that is a logical subsection of this Variable. * No data is read until a read method is called on it. * * @param ranges List of type ucar.ma2.Range, with size equal to getRank(). * Each Range corresponds to a Dimension, and specifies the section of data to read in that Dimension. * A Range object may be null, which means use the entire dimension. * @return a new Variable which is a logical section of this Variable. * @throws InvalidRangeException */ public Variable section(List ranges) throws InvalidRangeException { return section(new Section(ranges, shape).makeImmutable()); } /** * Create a new Variable that is a logical subsection of this Variable. * No data is read until a read method is called on it. * * @param subsection Section of this variable. * Each Range in the section corresponds to a Dimension, and specifies the section of data to read in that Dimension. * A Range object may be null, which means use the entire dimension. * @return a new Variable which is a logical section of this Variable. * @throws InvalidRangeException if section not compatible with shape */ public Variable section(Section subsection) throws InvalidRangeException { subsection = Section.fill(subsection, shape); // create a copy of this variable with a proxy reader Variable sectionV = copy(); // subclasses must override sectionV.setProxyReader(new SectionReader(this, subsection)); sectionV.shape = subsection.getShape(); sectionV.createNewCache(); // dont share the cache sectionV.setCaching(false); // dont cache // replace dimensions if needed !! LOOK not shared sectionV.dimensions = new ArrayList<>(); for (int i = 0; i < getRank(); i++) { Dimension oldD = getDimension(i); Dimension newD = (oldD.getLength() == sectionV.shape[i]) ? oldD : new Dimension(oldD.getShortName(), sectionV.shape[i], false); newD.setUnlimited(oldD.isUnlimited()); sectionV.dimensions.add(newD); } sectionV.resetShape(); return sectionV; } /** * Create a new Variable that is a logical slice of this Variable, by * fixing the specified dimension at the specified index value. This reduces rank by 1. * No data is read until a read method is called on it. * * @param dim which dimension to fix * @param value at what index value * @return a new Variable which is a logical slice of this Variable. * @throws InvalidRangeException if dimension or value is illegal */ public Variable slice(int dim, int value) throws InvalidRangeException { if ((dim < 0) || (dim >= shape.length)) throw new InvalidRangeException("Slice dim invalid= " + dim); // ok to make slice of record dimension with length 0 boolean recordSliceOk = false; if ((dim == 0) && (value == 0)) { Dimension d = getDimension(0); recordSliceOk = d.isUnlimited(); } // otherwise check slice in range if (!recordSliceOk) { if ((value < 0) || (value >= shape[dim])) throw new InvalidRangeException("Slice value invalid= " + value + " for dimension " + dim); } // create a copy of this variable with a proxy reader Variable sliceV = copy(); // subclasses must override Section slice = new Section(getShapeAsSection()); slice.replaceRange(dim, new Range(value, value)).makeImmutable(); sliceV.setProxyReader(new SliceReader(this, dim, slice)); sliceV.createNewCache(); // dont share the cache sliceV.setCaching(false); // dont cache // remove that dimension - reduce rank sliceV.dimensions.remove(dim); sliceV.resetShape(); return sliceV; } /** * Create a new Variable that is a logical view of this Variable, by * eliminating the specified dimension(s) of length 1. * No data is read until a read method is called on it. * * @param dims list of dimensions of length 1 to reduce * @return a new Variable which is a logical slice of this Variable. * @throws InvalidRangeException if dimension or value is illegal */ public Variable reduce(List dims) throws InvalidRangeException { List dimIdx = new ArrayList<>(dims.size()); for (Dimension d : dims) { assert dimensions.contains(d); assert d.getLength() == 1; dimIdx.add(dimensions.indexOf(d)); } // create a copy of this variable with a proxy reader Variable sliceV = copy(); // subclasses must override sliceV.setProxyReader(new ReduceReader(this, dimIdx)); sliceV.createNewCache(); // dont share the cache sliceV.setCaching(false); // dont cache // remove dimension(s) - reduce rank for (Dimension d : dims) sliceV.dimensions.remove(d); sliceV.resetShape(); return sliceV; } protected Variable copy() { return new Variable(this); } public NetcdfFile getNetcdfFile() { return ncfile; } ////////////////////////////////////////////////////////////////////////////// /** * Lookup the enum string for this value. * Can only be called on enum types, where dataType.isEnum() is true. * * @param val the integer value of this enum * @return the String value */ public String lookupEnumString(int val) { if (!dataType.isEnum()) throw new UnsupportedOperationException("Can only call Variable.lookupEnumVal() on enum types"); return enumTypedef.lookupEnumString(val); } private EnumTypedef enumTypedef; /** * Public by accident. * * @param enumTypedef set the EnumTypedef, only use if getDataType.isEnum() */ public void setEnumTypedef(EnumTypedef enumTypedef) { if (immutable) throw new IllegalStateException("Cant modify"); if (!dataType.isEnum()) throw new UnsupportedOperationException("Can only call Variable.setEnumTypedef() on enum types"); this.enumTypedef = enumTypedef; } /** * Get the EnumTypedef, only use if getDataType.isEnum() * * @return enumTypedef or null if !getDataType.isEnum() */ public EnumTypedef getEnumTypedef() { return enumTypedef; } ////////////////////////////////////////////////////////////////////////////// // IO // implementation notes to subclassers // all other calls use them, so override only these: // _read() // _read(Section section) // _readNestedData(Section section, boolean flatten) /** * Read a section of the data for this Variable and return a memory resident Array. * The Array has the same element type as the Variable, and the requested shape. * Note that this does not do rank reduction, so the returned Array has the same rank * as the Variable. Use Array.reduce() for rank reduction. *

* assert(origin[ii] + shape[ii]*stride[ii] <= Variable.shape[ii]); *

* * @param origin int array specifying the starting index. If null, assume all zeroes. * @param shape int array specifying the extents in each dimension. * This becomes the shape of the returned Array. * @return the requested data in a memory-resident Array */ public Array read(int[] origin, int[] shape) throws IOException, InvalidRangeException { if ((origin == null) && (shape == null)) return read(); if (origin == null) return read(new Section(shape)); if (shape == null) return read(new Section(origin, this.shape)); return read(new Section(origin, shape)); } /** * Read data section specified by a "section selector", and return a memory resident Array. Uses * Fortran 90 array section syntax. * * @param sectionSpec specification string, eg "1:2,10,:,1:100:10". May optionally have (). * @return the requested data in a memory-resident Array * @see ucar.ma2.Section for sectionSpec syntax */ public Array read(String sectionSpec) throws IOException, InvalidRangeException { return read(new Section(sectionSpec)); } /** * Read a section of the data for this Variable from the netcdf file and return a memory resident Array. * * @param ranges list of Range specifying the section of data to read. * @return the requested data in a memory-resident Array * @throws IOException if error * @throws InvalidRangeException if ranges is invalid * @see #read(Section) */ public Array read(List ranges) throws IOException, InvalidRangeException { if (null == ranges) return _read(); return read(new Section(ranges)); } /** * Read a section of the data for this Variable from the netcdf file and return a memory resident Array. * The Array has the same element type as the Variable, and the requested shape. * Note that this does not do rank reduction, so the returned Array has the same rank * as the Variable. Use Array.reduce() for rank reduction. *

* If the Variable is a member of an array of Structures, this returns only the variable's data * in the first Structure, so that the Array shape is the same as the Variable. * To read the data in all structures, use ncfile.readSectionSpec(). *

* Note this only allows you to specify a subset of this variable. * If the variable is nested in a array of structures and you want to subset that, use * NetcdfFile.read(String sectionSpec, boolean flatten); * * @param section list of Range specifying the section of data to read. * Must be null or same rank as variable. * If list is null, assume all data. * Each Range corresponds to a Dimension. If the Range object is null, it means use the entire dimension. * @return the requested data in a memory-resident Array * @throws IOException if error * @throws InvalidRangeException if section is invalid */ public Array read(ucar.ma2.Section section) throws java.io.IOException, ucar.ma2.InvalidRangeException { return (section == null) ? _read() : _read(Section.fill(section, shape)); } /** * Read all the data for this Variable and return a memory resident Array. * The Array has the same element type and shape as the Variable. *

* If the Variable is a member of an array of Structures, this returns only the variable's data * in the first Structure, so that the Array shape is the same as the Variable. * To read the data in all structures, use ncfile.readSection(). * * @return the requested data in a memory-resident Array. */ public Array read() throws IOException { return _read(); } /** * ********************************************************************* */ // scalar reading /** * Get the value as a byte for a scalar Variable. May also be one-dimensional of length 1. * * @throws IOException if theres an IO Error * @throws UnsupportedOperationException if not a scalar Variable or one-dimensional of length 1. * @throws ForbiddenConversionException if data type not convertible to byte */ public byte readScalarByte() throws IOException { Array data = getScalarData(); return data.getByte(Index.scalarIndexImmutable); } /** * Get the value as a short for a scalar Variable. May also be one-dimensional of length 1. * * @throws IOException if theres an IO Error * @throws UnsupportedOperationException if not a scalar Variable or one-dimensional of length 1. * @throws ForbiddenConversionException if data type not convertible to short */ public short readScalarShort() throws IOException { Array data = getScalarData(); return data.getShort(Index.scalarIndexImmutable); } /** * Get the value as a int for a scalar Variable. May also be one-dimensional of length 1. * * @throws IOException if theres an IO Error * @throws UnsupportedOperationException if not a scalar Variable or one-dimensional of length 1. * @throws ForbiddenConversionException if data type not convertible to int */ public int readScalarInt() throws IOException { Array data = getScalarData(); return data.getInt(Index.scalarIndexImmutable); } /** * Get the value as a long for a scalar Variable. May also be one-dimensional of length 1. * * @throws IOException if theres an IO Error * @throws UnsupportedOperationException if not a scalar Variable * @throws ForbiddenConversionException if data type not convertible to long */ public long readScalarLong() throws IOException { Array data = getScalarData(); return data.getLong(Index.scalarIndexImmutable); } /** * Get the value as a float for a scalar Variable. May also be one-dimensional of length 1. * * @throws IOException if theres an IO Error * @throws UnsupportedOperationException if not a scalar Variable or one-dimensional of length 1. * @throws ForbiddenConversionException if data type not convertible to float */ public float readScalarFloat() throws IOException { Array data = getScalarData(); return data.getFloat(Index.scalarIndexImmutable); } /** * Get the value as a double for a scalar Variable. May also be one-dimensional of length 1. * * @throws IOException if theres an IO Error * @throws UnsupportedOperationException if not a scalar Variable or one-dimensional of length 1. * @throws ForbiddenConversionException if data type not convertible to double */ public double readScalarDouble() throws IOException { Array data = getScalarData(); return data.getDouble(Index.scalarIndexImmutable); } /** * Get the value as a String for a scalar Variable. May also be one-dimensional of length 1. * May also be one-dimensional of type CHAR, which wil be turned into a scalar String. * * @throws IOException if theres an IO Error * @throws UnsupportedOperationException if not a scalar or one-dimensional. * @throws ClassCastException if data type not DataType.STRING or DataType.CHAR. */ public String readScalarString() throws IOException { Array data = getScalarData(); if (dataType == DataType.STRING) return (String) data.getObject(Index.scalarIndexImmutable); else if (dataType == DataType.CHAR) { ArrayChar dataC = (ArrayChar) data; return dataC.getString(); } else throw new IllegalArgumentException("readScalarString not STRING or CHAR " + getFullName()); } protected Array getScalarData() throws IOException { Array scalarData = (cache.data != null) ? cache.data : read(); scalarData = scalarData.reduce(); if ((scalarData.getRank() == 0) || ((scalarData.getRank() == 1) && dataType == DataType.CHAR)) return scalarData; throw new java.lang.UnsupportedOperationException("not a scalar variable =" + this); } /////////////// // internal reads: all other calls go through these. // subclasses must override, so that NetcdfDataset wrapping will work. // non-structure-member Variables. protected Array _read() throws IOException { // caching overrides the proxyReader // check if already cached if (cache.data != null) { if (debugCaching) System.out.println("got data from cache " + getFullName()); return cache.data.copy(); } Array data = proxyReader.reallyRead(this, null); // optionally cache it if (isCaching()) { setCachedData(data); if (debugCaching) System.out.println("cache " + getFullName()); return cache.data.copy(); // dont let users get their nasty hands on cached data } else { return data; } } /** * public by accident, do not call directly. * * @return Array * @throws IOException on error */ @Override public Array reallyRead(Variable client, CancelTask cancelTask) throws IOException { if (isMemberOfStructure()) { // LOOK should be UnsupportedOperationException ?? List memList = new ArrayList<>(); memList.add(this.getShortName()); Structure s = getParentStructure().select(memList); ArrayStructure as = (ArrayStructure) s.read(); return as.extractMemberArray(as.findMember(getShortName())); } try { return ncfile.readData(this, getShapeAsSection()); } catch (InvalidRangeException e) { e.printStackTrace(); throw new IOException(e.getMessage()); // cant happen haha } } // section of non-structure-member Variable // assume filled, validated Section protected Array _read(Section section) throws IOException, InvalidRangeException { // check if its really a full read if ((null == section) || section.computeSize() == getSize()) return _read(); // full read was cached if (isCaching()) { if (cache.data == null) { setCachedData(_read()); // read and cache entire array if (debugCaching) System.out.println("cache " + getFullName()); } if (debugCaching) System.out.println("got data from cache " + getFullName()); return cache.data.sectionNoReduce(section.getRanges()).copy(); // subset it, return copy } return proxyReader.reallyRead(this, section, null); } /** * public by accident, do not call directly. * * @return Array * @throws IOException on error */ @Override public Array reallyRead(Variable client, Section section, CancelTask cancelTask) throws IOException, InvalidRangeException { if (isMemberOfStructure()) { throw new UnsupportedOperationException("Cannot directly read section of Member Variable=" + getFullName()); } // read just this section return ncfile.readData(this, section); } /* structure-member Variable; section has a Range for each array in the parent // stuctures(s) and for the Variable. private Array _readMemberData(List section, boolean flatten) throws IOException, InvalidRangeException { /*Variable useVar = (ioVar != null) ? ioVar : this; NetcdfFile useFile = (ncfileIO != null) ? ncfileIO : ncfile; return useFile.readMemberData(useVar, section, flatten); } */ public long readToByteChannel(Section section, WritableByteChannel wbc) throws IOException, InvalidRangeException { if ((ncfile == null) || hasCachedData()) return IospHelper.copyToByteChannel(read(section), wbc); return ncfile.readToByteChannel(this, section, wbc); } public long readToStream(Section section, OutputStream out) throws IOException, InvalidRangeException { if ((ncfile == null) || hasCachedData()) return IospHelper.copyToOutputStream(read(section), out); return ncfile.readToOutputStream(this, section, out); } /*******************************************/ /* nicely formatted string representation */ /** * Get the display name plus the dimensions, eg 'float name(dim1, dim2)' * * @return display name plus the dimensions */ public String getNameAndDimensions() { Formatter buf = new Formatter(); getNameAndDimensions(buf, true, false); return buf.toString(); } /** * Get the display name plus the dimensions, eg 'float name(dim1, dim2)' * * @param strict strictly comply with ncgen syntax, with name escaping. otherwise, get extra info, no escaping * @return display name plus the dimensions */ public String getNameAndDimensions(boolean strict) { Formatter buf = new Formatter(); getNameAndDimensions(buf, false, strict); return buf.toString(); } /** * Get the display name plus the dimensions, eg 'name(dim1, dim2)' * * @param buf add info to this StringBuilder */ public void getNameAndDimensions(StringBuilder buf) { getNameAndDimensions(buf, true, false); } /** * Get the display name plus the dimensions, eg 'name(dim1, dim2)' * * @param buf add info to this StringBuffer * @deprecated use getNameAndDimensions(StringBuilder buf) */ public void getNameAndDimensions(StringBuffer buf) { Formatter proxy = new Formatter(); getNameAndDimensions(proxy, true, false); buf.append(proxy.toString()); } /** * Add display name plus the dimensions to the StringBuffer * * @param buf add info to this * @param useFullName use full name else short name. strict = true implies short name * @param strict strictly comply with ncgen syntax, with name escaping. otherwise, get extra info, no escaping */ public void getNameAndDimensions(StringBuilder buf, boolean useFullName, boolean strict) { Formatter proxy = new Formatter(); getNameAndDimensions(proxy, useFullName, strict); buf.append(proxy.toString()); } /** * Add display name plus the dimensions to the StringBuffer * * @param buf add info to this * @param useFullName use full name else short name. strict = true implies short name * @param strict strictly comply with ncgen syntax, with name escaping. otherwise, get extra info, no escaping */ public void getNameAndDimensions(Formatter buf, boolean useFullName, boolean strict) { useFullName = useFullName && !strict; String name = useFullName ? getFullName() : getShortName(); if (strict) name = NetcdfFile.makeValidCDLName(getShortName()); buf.format("%s", name); if (shape != null) { if (getRank() > 0) buf.format("("); for (int i = 0; i < dimensions.size(); i++) { Dimension myd = dimensions.get(i); String dimName = myd.getShortName(); if ((dimName != null) && strict) dimName = NetcdfFile.makeValidCDLName(dimName); if (i != 0) buf.format(", "); if (myd.isVariableLength()) { buf.format("*"); } else if (myd.isShared()) { if (!strict) buf.format("%s=%d", dimName, myd.getLength()); else buf.format("%s", dimName); } else { if (dimName != null) { buf.format("%s=", dimName); } buf.format("%d", myd.getLength()); } } if (getRank() > 0) buf.format(")"); } } /** * CDL representation of Variable, not strict. */ public String toString() { return writeCDL(false, false); } /** * CDL representation of a Variable. * * @param useFullName use full name, else use short name * @param strict strictly comply with ncgen syntax * @return CDL representation of the Variable. */ public String writeCDL(boolean useFullName, boolean strict) { Formatter buf = new Formatter(); writeCDL(buf, new Indent(2), useFullName, strict); return buf.toString(); } protected void writeCDL(Formatter buf, Indent indent, boolean useFullName, boolean strict) { buf.format("%s", indent); if (dataType == null) buf.format("Unknown"); else if (dataType.isEnum()) { if (enumTypedef == null) buf.format("enum UNKNOWN"); else buf.format("enum %s", NetcdfFile.makeValidCDLName(enumTypedef.getShortName())); } else buf.format("%s", dataType.toString()); //if (isVariableLength) buf.append("(*)"); // LOOK buf.format(" "); getNameAndDimensions(buf, useFullName, strict); buf.format(";"); if (!strict) buf.format(extraInfo()); buf.format("%n"); indent.incr(); for (Attribute att : getAttributes()) { if(Attribute.isspecial(att)) continue; buf.format("%s", indent); att.writeCDL(buf, strict, getShortName()); buf.format(";"); if (!strict && (att.getDataType() != DataType.STRING)) buf.format(" // %s", att.getDataType()); buf.format("%n"); } indent.decr(); } /** * String representation of Variable and its attributes. */ public String toStringDebug() { Formatter f = new Formatter(); f.format("Variable %s", getFullName()); if (ncfile != null) { f.format(" in file %s", getDatasetLocation()); String extra = ncfile.toStringDebug(this); if (extra != null) f.format(" %s", extra); } return f.toString(); } private static boolean showSize = false; protected String extraInfo() { return showSize ? " // " + getElementSize() + " " + getSize() : ""; } public String getDatasetLocation() { if (ncfile != null) return ncfile.getLocation(); return "N/A"; } /** * Instances which have same content are equal. */ public boolean equals(Object oo) { if (this == oo) return true; if (!(oo instanceof Variable)) return false; Variable o = (Variable) oo; if (!getShortName().equals(o.getShortName())) return false; if (isScalar() != o.isScalar()) return false; if (getDataType() != o.getDataType()) return false; if (!getParentGroup().equals(o.getParentGroup())) return false; if ((getParentStructure() != null) && !getParentStructure().equals(o.getParentStructure())) return false; if (isVariableLength() != o.isVariableLength()) return false; if (dimensions.size() != o.getDimensions().size()) return false; for (int i = 0; i < dimensions.size(); i++) if (!getDimension(i).equals(o.getDimension(i))) return false; return true; } /** * Override Object.hashCode() to implement equals. */ @Override public int hashCode() { if (hashCode == 0) { int result = 17; result = 37 * result + getShortName().hashCode(); if (isScalar()) result++; result = 37 * result + getDataType().hashCode(); result = 37 * result + getParentGroup().hashCode(); if (getParentStructure() != null) result = 37 * result + getParentStructure().hashCode(); if (isVariableLength) result++; result = 37 * result + dimensions.hashCode(); hashCode = result; } return hashCode; } public void hashCodeShow(Indent indent) { System.out.printf("%sVar hash = %d%n", indent, hashCode()); System.out.printf("%s shortName %s = %d%n", indent, getShortName(), getShortName().hashCode()); System.out.printf("%s isScalar %s%n", indent, isScalar()); System.out.printf("%s dataType %s%n", indent, getDataType()); System.out.printf("%s parentGroup %s = %d%n", indent, getParentGroup(), getParentGroup().hashCode()); System.out.printf("%s isVariableLength %s%n", indent, isVariableLength); System.out.printf("%s dimensions %d len=%d%n", indent, dimensions.hashCode(), dimensions.size()); indent.incr(); for (Dimension d : dimensions) { d.hashCodeShow(indent); } indent.decr(); if (getParentStructure() != null) { System.out.printf("%s parentStructure %d%n", indent, getParentStructure().hashCode()); getParentStructure().hashCodeShow(indent.incr()); indent.decr(); } } protected int hashCode = 0; /** * Sort by name */ public int compareTo(VariableSimpleIF o) { return getShortName().compareTo(o.getShortName()); } ///////////////////////////////////////////////////////////////////////////// protected Variable() { } /** * Create a Variable. Also must call setDataType() and setDimensions() * * @param ncfile the containing NetcdfFile. * @param group the containing group; if null, use rootGroup * @param parent parent Structure, may be null * @param shortName variable shortName, must be unique within the Group */ public Variable(NetcdfFile ncfile, Group group, Structure parent, String shortName) { super(shortName); this.ncfile = ncfile; if (parent == null) setParentGroup((group == null) ? ncfile.getRootGroup() : group); else setParentStructure(parent); attributes = new AttributeContainerHelper(shortName); } /** * Create a Variable. Also must call setDataType() and setDimensions() * * @param ncfile the containing NetcdfFile. * @param group the containing group; if null, use rootGroup * @param parent parent Structure, may be null * @param shortName variable shortName, must be unique within the Group * @param dtype the Variable's DataType * @param dims space delimited list of dimension names. may be null or "" for scalars. */ public Variable(NetcdfFile ncfile, Group group, Structure parent, String shortName, DataType dtype, String dims) { this(ncfile, group, parent, shortName, dtype, (List)null); if(group == null) group = ncfile.getRootGroup(); setDimensions(Dimension.makeDimensionsList(group, dims)); } /** * Create a Variable. Also must call setDataType() and setDimensions() * * @param ncfile the containing NetcdfFile. * @param group the containing group; if null, use rootGroup * @param parent parent Structure, may be null * @param shortName variable shortName, must be unique within the Group * @param dtype the Variable's DataType * @param dims dimension names. */ public Variable(NetcdfFile ncfile, Group group, Structure parent, String shortName, DataType dtype, List dims) { this(ncfile, group, parent, shortName); setDataType(dtype); setDimensions(dims); } /** * Copy constructor. * The returned Variable is mutable. * It shares the cache object and the iosp Object, attributes and dimensions with the original. * Does not share the proxyReader. * Use for section, slice, "logical views" of original variable. * * @param from copy from this Variable. */ public Variable(Variable from) { super(from.getShortName()); this.attributes = new AttributeContainerHelper(from.getShortName(), from.getAttributes()); this.cache = from.cache; // caller should do createNewCache() if dont want to share setDataType(from.getDataType()); this.dimensions = new ArrayList<>(from.dimensions); // dimensions are shared this.elementSize = from.getElementSize(); this.enumTypedef = from.enumTypedef; setParentGroup(from.group); setParentStructure(from.getParentStructure()); this.isMetadata = from.isMetadata; this.isVariableLength = from.isVariableLength; this.ncfile = from.ncfile; this.shape = from.getShape(); this.sizeToCache = from.sizeToCache; this.spiObject = from.spiObject; } /////////////////////////////////////////////////// // the following make this mutable /** * Set the data type * * @param dataType set to this value */ public void setDataType(DataType dataType) { if (immutable) throw new IllegalStateException("Cant modify"); this.dataType = dataType; this.elementSize = getDataType().getSize(); /* why is this needed ?? EnumTypedef etd = getEnumTypedef(); if (etd != null) { DataType etdtype = etd.getBaseType(); if (dataType != etdtype) log.error("Variable.setDataType: enum basetype mismatch: {} != {}", etdtype, dataType); /* DataType basetype = null; if (dataType == DataType.ENUM1) basetype = DataType.BYTE; else if (dataType == DataType.ENUM2) basetype = DataType.SHORT; else if (dataType == DataType.ENUM4) basetype = DataType.INT; else basetype = etdtype; if (etdtype != null && dataType != etdtype) else etd.setBaseType(basetype); } */ } /** * Set the short name, converting to valid CDM object name if needed. * * @param shortName set to this value * @return valid CDM object name */ public String setName(String shortName) { if (immutable) throw new IllegalStateException("Cant modify"); setShortName(shortName); return getShortName(); } /** * Set the parent group. * * @param group set to this value */ public void setParentGroup(Group group) { if (immutable) throw new IllegalStateException("Cant modify"); super.setParentGroup(group); } /** * Set the element size. Usually elementSize is determined by the dataType, * use this only for exceptional cases. * * @param elementSize set to this value */ public void setElementSize(int elementSize) { if (immutable) throw new IllegalStateException("Cant modify"); this.elementSize = elementSize; } ////////////////////////////////////////////////////////////////////////////////////////////////// // AttributeHelper public java.util.List getAttributes() { return attributes.getAttributes(); } public AttributeContainer getAttributeContainer() { return new AttributeContainerHelper(getFullName(), attributes.getAttributes()); } public Attribute findAttribute(String name) { return attributes.findAttribute(name); } public Attribute findAttributeIgnoreCase(String name) { return attributes.findAttributeIgnoreCase(name); } public String findAttValueIgnoreCase(String attName, String defaultValue) { return attributes.findAttValueIgnoreCase(attName, defaultValue); } public Attribute addAttribute(Attribute att) { return attributes.addAttribute(att); } public void addAll(Iterable atts) { attributes.addAll(atts); } public boolean remove(Attribute a) { return attributes.remove(a); } public boolean removeAttribute(String attName) { return attributes.removeAttribute(attName); } public boolean removeAttributeIgnoreCase(String attName) { return attributes.removeAttributeIgnoreCase(attName); } //////////////////////////////////////////////////////////////////////// /** * Set the shape with a list of Dimensions. The Dimensions may be shared or not. * Dimensions are in order, slowest varying first. Send a null for a scalar. * Technically you can use Dimensions from any group; pragmatically you should only use * Dimensions contained in the Variable's parent groups. * * @param dims list of type ucar.nc2.Dimension */ public void setDimensions(List dims) { if (immutable) throw new IllegalStateException("Cant modify"); this.dimensions = (dims == null) ? new ArrayList<>() : new ArrayList<>(dims); resetShape(); } /** * Use when dimensions have changed, to recalculate the shape. */ public void resetShape() { // if (immutable) throw new IllegalStateException("Cant modify"); LOOK allow this for unlimited dimension updating this.shape = new int[dimensions.size()]; for (int i = 0; i < dimensions.size(); i++) { Dimension dim = dimensions.get(i); shape[i] = dim.getLength(); //shape[i] = Math.max(dim.getLength(), 0); // LOOK // if (dim.isUnlimited() && (i != 0)) // LOOK only true for Netcdf-3 // throw new IllegalArgumentException("Unlimited dimension must be outermost"); if (dim.isVariableLength()) { //if (dimensions.size() != 1) // throw new IllegalArgumentException("Unknown dimension can only be used in 1 dim array"); //else isVariableLength = true; } } this.shapeAsSection = null; // recalc next time its asked for } /** * Set the dimensions using the dimensions names. The dimension is searched for recursively in the parent groups. * * @param dimString : whitespace separated list of dimension names, or '*' for Dimension.UNKNOWN, or number for anon dimension. null or empty String is a scalar. */ public void setDimensions(String dimString) { if (immutable) throw new IllegalStateException("Cant modify"); try { setDimensions(Dimension.makeDimensionsList(getParentGroup(), dimString)); //this.dimensions = Dimension.makeDimensionsList(getParentGroup(), dimString); resetShape(); } catch (IllegalStateException e) { throw new IllegalArgumentException("Variable " + getFullName() + " setDimensions = '" + dimString + "' FAILED: " + e.getMessage() + " file = " + getDatasetLocation()); } } /** * Reset the dimension array. Anonymous dimensions are left alone. * Shared dimensions are searched for recursively in the parent groups. */ public void resetDimensions() { if (immutable) throw new IllegalStateException("Cant modify"); ArrayList newDimensions = new ArrayList<>(); for (Dimension dim : dimensions) { if (dim.isShared()) { Dimension newD = getParentGroup().findDimension(dim.getShortName()); if (newD == null) throw new IllegalArgumentException("Variable " + getFullName() + " resetDimensions FAILED, dim doesnt exist in parent group=" + dim); newDimensions.add(newD); } else { newDimensions.add(dim); } } this.dimensions = newDimensions; resetShape(); } /** * Set the dimensions using all anonymous (unshared) dimensions * * @param shape defines the dimension lengths. must be > 0, or -1 for VLEN * @throws ucar.ma2.InvalidRangeException if any shape < 1 */ public void setDimensionsAnonymous(int[] shape) throws InvalidRangeException { if (immutable) throw new IllegalStateException("Cant modify"); this.dimensions = new ArrayList<>(); for (int i = 0; i < shape.length; i++) { if ((shape[i] < 1) && (shape[i] != -1)) throw new InvalidRangeException("shape[" + i + "]=" + shape[i] + " must be > 0"); Dimension anon; if (shape[i] == -1) { anon = Dimension.VLEN; isVariableLength = true; } else { anon = new Dimension(null, shape[i], false, false, false); } dimensions.add(anon); } resetShape(); } /** * Set this Variable to be a scalar */ public void setIsScalar() { if (immutable) throw new IllegalStateException("Cant modify"); this.dimensions = new ArrayList<>(); resetShape(); } /** * Replace a dimension with an equivalent one. * @param dim must have the same name, length as old one * public void replaceDimension( Dimension dim) { int idx = findDimensionIndex( dim.getName()); if (idx >= 0) dimensions.set( idx, dim); resetShape(); } */ /** * Replace a dimension with an equivalent one. * * @param idx index into dimension array * @param dim to set */ public void setDimension(int idx, Dimension dim) { if (immutable) throw new IllegalStateException("Cant modify"); dimensions.set(idx, dim); resetShape(); } /** * Make this immutable. * * @return this */ public Variable setImmutable() { super.setImmutable(); dimensions = Collections.unmodifiableList(dimensions); attributes.setImmutable(); return this; } /** * Is this Variable immutable * * @return if immutable */ public boolean isImmutable() { return immutable; } // for IOServiceProvider protected Object spiObject; /** * Should not be public. * * @return the IOSP object */ public Object getSPobject() { return spiObject; } /** * Should not be public. * * @param spiObject the IOSP object */ public void setSPobject(Object spiObject) { this.spiObject = spiObject; } //////////////////////////////////////////////////////////////////////////////////// // caching /** * If total data size is less than SizeToCache in bytes, then cache. * * @return size at which caching happens */ public int getSizeToCache() { if (sizeToCache >= 0) return sizeToCache; // it was set return isCoordinateVariable() ? defaultCoordsSizeToCache : defaultSizeToCache; } /** * Set the sizeToCache. If not set, use defaults * * @param sizeToCache size at which caching happens. < 0 means use defaults */ public void setSizeToCache(int sizeToCache) { this.sizeToCache = sizeToCache; } /** * Set whether to cache or not. Implies that the entire array will be stored, once read. * Normally this is set automatically based on size of data. * * @param caching set if caching. */ public void setCaching(boolean caching) { this.cache.isCaching = caching; this.cache.cachingSet = true; } /** * Will this Variable be cached when read. * Set externally, or calculated based on total size < sizeToCache. *

* This will always return {@code false} if {@link #permitCaching caching isn't permitted}. * * @return true is caching */ public boolean isCaching() { if (!permitCaching) { return false; } if (!this.cache.cachingSet) { cache.isCaching = !isVariableLength && (getSize() * getElementSize() < getSizeToCache()); if (debugCaching) System.out.printf(" cache %s %s %d < %d%n", getFullName(), cache.isCaching, getSize() * getElementSize(), getSizeToCache()); this.cache.cachingSet = true; } return cache.isCaching; } /** * Invalidate the data cache */ public void invalidateCache() { cache.data = null; } public void setCachedData(Array cacheData) { setCachedData(cacheData, false); } //public Array getCachedData() { // return (cache == null) ? null : cache.data; //} /** * Set the data cache * * @param cacheData cache this Array * @param isMetadata : synthesized data, set true if must be saved in NcML output (ie data not actually in the file). */ public void setCachedData(Array cacheData, boolean isMetadata) { if ((cacheData != null) && (cacheData.getElementType() != getDataType().getPrimitiveClassType())) throw new IllegalArgumentException("setCachedData type=" + cacheData.getElementType() + " incompatible with variable type=" + getDataType()); this.cache.data = cacheData; this.isMetadata = isMetadata; this.cache.cachingSet = true; this.cache.isCaching = true; } /** * Create a new data cache, use this when you dont want to share the cache. */ public void createNewCache() { this.cache = new Cache(); } /** * Has data been read and cached. * Use only on a Variable, not a subclass. * * @return true if data is read and cached */ public boolean hasCachedData() { return (null != cache.data); } // this indirection allows us to share the cache among the variable's sections and copies /** * Public by accident. */ static protected class Cache { public Array data; public boolean isCaching = false; public boolean cachingSet = false; public Cache() { } } /////////////////////////////////////////////////////////////////////// // setting variable data values /** * Generate the list of values from a starting value and an increment. * Will reshape to variable if needed. * * @param npts number of values, must = v.getSize() * @param start starting value * @param incr increment */ public void setValues(int npts, double start, double incr) { if (npts != getSize()) throw new IllegalArgumentException("bad npts = " + npts + " should be " + getSize()); Array data = Array.makeArray(getDataType(), npts, start, incr); if (getRank() != 1) data = data.reshape(getShape()); setCachedData(data, true); } /** * Set the data values from a list of Strings. * * @param values list of Strings * @throws IllegalArgumentException if values array not correct size, or values wont parse to the correct type */ public void setValues(List values) throws IllegalArgumentException { Array data = Array.makeArray(getDataType(), values); if (data.getSize() != getSize()) throw new IllegalArgumentException("Incorrect number of values specified for the Variable " + getFullName() + " needed= " + getSize() + " given=" + data.getSize()); if (getRank() != 1) // dont have to reshape for rank 1 data = data.reshape(getShape()); setCachedData(data, true); } //////////////////////////////////////////////////////////////////////// // StructureMember - could be a subclass, but that has problems /** * Get list of Dimensions, including parents if any. * * @return array of Dimension, rank of v plus all parents. */ public List getDimensionsAll() { List dimsAll = new ArrayList<>(); addDimensionsAll(dimsAll, this); return dimsAll; } private void addDimensionsAll(List result, Variable v) { if (v.isMemberOfStructure()) addDimensionsAll(result, v.getParentStructure()); for (int i = 0; i < v.getRank(); i++) result.add(v.getDimension(i)); } public int[] getShapeAll() { if (getParentStructure() == null) return getShape(); List dimAll = getDimensionsAll(); int[] shapeAll = new int[dimAll.size()]; for (int i = 0; i < dimAll.size(); i++) shapeAll[i] = dimAll.get(i).getLength(); return shapeAll; } /* * Read data in all structures for this Variable, using a string sectionSpec to specify the section. * See readAllStructures(Section section, boolean flatten) method for details. * * @param sectionSpec specification string, eg "1:2,10,:,1:100:10" * @param flatten if true, remove enclosing StructureData. * @return the requested data which has the shape of the request. * @see #readAllStructures * @deprecated * public Array readAllStructuresSpec(String sectionSpec, boolean flatten) throws IOException, InvalidRangeException { return readAllStructures(new Section(sectionSpec), flatten); } /* * Read data from all structures for this Variable. * This is used for member variables whose parent Structure(s) is not a scalar. * You must specify a Range for each dimension in the enclosing parent Structure(s). * The returned Array will have the same shape as the requested section. *

*

If flatten is false, return nested Arrays of StructureData that correspond to the nested Structures. * The innermost Array(s) will match the rank and type of the Variable, but they will be inside Arrays of * StructureData. *

*

If flatten is true, remove the Arrays of StructureData that wrap the data, and return an Array of the * same type as the Variable. The shape of the returned Array will be an accumulation of all the shapes of the * Structures containing the variable. * * @param sectionAll an array of Range objects, one for each Dimension of the enclosing Structures, as well as * for the Variable itself. If the list is null, use the full shape for everything. * If an individual Range is null, use the full shape for that dimension. * @param flatten if true, remove enclosing StructureData. Otherwise, each parent Structure will create a * StructureData container for the returned data array. * @return the requested data which has the shape of the request. * @deprecated * public Array readAllStructures(ucar.ma2.Section sectionAll, boolean flatten) throws java.io.IOException, ucar.ma2.InvalidRangeException { Section resolved; // resolve all nulls if (sectionAll == null) resolved = makeSectionAddParents(null, false); // everything else { ArrayList resultAll = new ArrayList(); makeSectionWithParents(resultAll, sectionAll.getRanges(), this); resolved = new Section(resultAll); } return _readMemberData(resolved, flatten); } // recursively create the section (list of Range) array private List makeSectionWithParents(List result, List orgSection, Variable v) throws InvalidRangeException { List section = orgSection; // do parent stuctures(s) first if (v.isMemberOfStructure()) section = makeSectionWithParents(result, orgSection, v.getParentStructure()); // process just this variable's subList List myList = section.subList(0, v.getRank()); Section mySection = new Section(myList, v.getShape()); result.addAll(mySection.getRanges()); // return section with this variable's sublist removed return section.subList(v.getRank(), section.size()); } */ /* * Composes this variable's ranges with another list of ranges, adding parent ranges; resolves nulls. * * @param section Section of this Variable, same rank as v, may have nulls or be null. * @param firstOnly if true, get first parent, else get all parrents. * @return Section, rank of v plus parents, no nulls * @throws InvalidRangeException if bad * private Section makeSectionAddParents(Section section, boolean firstOnly) throws InvalidRangeException { Section result; if (section == null) result = new Section(getRanges()); else result = new Section(section.getRanges(), getShape()); // add parents Structure p = getParentStructure(); while (p != null) { Section parentSection = p.getShapeAsSection(); for (int i = parentSection.getRank() - 1; i >= 0; i--) { // reverse Range r = parentSection.getRange(i); result.insertRange(0, firstOnly ? new Range(0, 0) : r); } p = p.getParentStructure(); } return result; } */ /* private Array readMemberOfStructureFlatten(Section section) throws InvalidRangeException, IOException { // get through first parents element Section sectionAll = makeSectionAddParents(section, true); Array data = _readMemberData(sectionAll, true); // flatten // remove parent dimensions. int n = data.getRank() - getRank(); for (int i = 0; i < n; i++) if (data.getShape()[0] == 1) data = data.reduce(0); return data; } /* structure-member Variable; section has a Range for each array in the parent // stuctures(s) and for the Variable. protected Array _readMemberData(Section section, boolean flatten) throws IOException, InvalidRangeException { return ncfile.readMemberData(this, section, flatten); } */ //////////////////////////////// /** * Calculate if this is a classic coordinate variable: has same name as its first dimension. * If type char, must be 2D, else must be 1D. * * @return true if a coordinate variable. */ public boolean isCoordinateVariable() { if ((dataType == DataType.STRUCTURE) || isMemberOfStructure()) // Structures and StructureMembers cant be coordinate variables return false; int n = getRank(); if (n == 1 && dimensions.size() == 1) { Dimension firstd = dimensions.get(0); if (getShortName().equals(firstd.getShortName())) { // : short names match return true; } } if (n == 2 && dimensions.size() == 2) { // two dimensional Dimension firstd = dimensions.get(0); if (shortName.equals(firstd.getShortName()) && // short names match (getDataType() == DataType.CHAR)) { // must be char valued (really a String) return true; } } return false; } /* public Object clone() throws CloneNotSupportedException { Variable clone = (Variable) super.clone(); // Do we need to clone these? // protected Cache cache = new Cache(); // protected int sizeToCache = -1; // bytes clone.setParentGroup(group); clone.setParentStructure(getParentStructure()); clone.setProxyReader(clone); return clone; } */ /////////////////////////////////////////////////////////////////////// // deprecated /** * @return isVariableLength() * @deprecated use isVariableLength() */ public boolean isUnknownLength() { return isVariableLength; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy