/* (c) 2015 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.web.netcdf;
import ucar.ma2.DataType;
/**
* Class used to identify the DataPacking to be applied to NetCDF values
* to be stored. DataPacking on write is used to reduce the disk usage
* (As an instance, storing 64 bit Double data as 16 bit Shorts).
* Data values are rescaled (packed) using a formula:
*
* packed_value = nearestint((original_value - add_offset) / scale_factor)
*
* add_offset and scale_factor attributes are added to the Variable when
* data packing occurs. They are computed on top of the dataset's min and max.
*
* add_offset = (max + min) / 2
* scale_factor = (max - min) / (2^n - 2)
*
* [where n is the number of bits of the output packed data type.]
* Supported packing types are Byte, Short, Integer.
*
* Data can be read back with the formula:
* unpacked_value = packed_value * scale_factor + add_offset
*
* @author Daniele Romagnoli, GeoSolutions SAS
*/
public enum DataPacking {
NONE {
@Override
public DataType getDataType() {
// No packing required
return null;
}
@Override
public DataPacker getDataPacker(DataStats stats) {
// No packing required
return null;
}
@Override
public Integer getDenominator() {
// No packing required
return null;
}
@Override
public Integer getReservedValue() {
// No packing required
return null;
}
},
BYTE {
@Override
public DataType getDataType() {
return DataType.BYTE;
}
@Override
public Integer getDenominator() {
return BYTE_DENOMINATOR;
}
@Override
public Integer getReservedValue() {
return ZERO;
}
@Override
public DataPacker getDataPacker(DataStats stats) {
double min = stats.getMin();
double max = stats.getMax();
double scale = (max - min) / getDenominator();
double offset = (min - scale);
return new DataPacker(offset, scale, getReservedValue());
}
},
SHORT {
@Override
public DataType getDataType() {
return DataType.SHORT;
}
@Override
public Integer getDenominator() {
return SHORT_DENOMINATOR;
}
@Override
public Integer getReservedValue() {
return SHORT_RESERVED;
}
},
INT {
@Override
public DataType getDataType() {
return DataType.INT;
}
@Override
public Integer getDenominator() {
return INT_DENOMINATOR;
}
@Override
public Integer getReservedValue() {
return INT_RESERVED;
}
};
public static DataPacking getDefault() {
return NONE;
}
private final static Integer ZERO = 0;
/**
* Constants to be used for dataPacking formula, depending on the dataPacking dataType.
* scale_factor = (max - min) / (2^n - 2).
*
* (2^n - 2) is a precomputed denominator.
*/
private final static Integer BYTE_DENOMINATOR = ((int) Math.pow(2, 8)) - 2;
private final static Integer SHORT_DENOMINATOR = ((int) Math.pow(2, 16)) - 2;
private final static Integer INT_DENOMINATOR = ((int) Math.pow(2, 32)) - 2;
/**
* Reserved special values dataType related.
* They will be used to remap a specific input value which can't be represented
* in the output type. As an instance, a -1E-20 input fillValue can't be
* represented as a short, so we need a reserved value to represent it.
* The reserved value formula is:
* -(2^(n-1)) where n is the output dataType's number of bits.
*/
private final static Integer SHORT_RESERVED = -((int) Math.pow(2, 15));
private final static Integer INT_RESERVED = -((int) Math.pow(2, 31));
/** Return the denominator to be used in computing the scale_factor */
public abstract Integer getDenominator();
/** Return a reserved value (it can be used to represent fillValue) */
public abstract Integer getReservedValue();
/** Return the Variable's {@link DataType} for the specific packing */
public abstract DataType getDataType();
/** Get the default DataPacker */
public DataPacker getDataPacker(DataStats stats) {
double min = stats.getMin();
double max = stats.getMax();
double offset = (min + max) / 2;
double scale = (max - min) / getDenominator();
return new DataPacker(offset, scale, getReservedValue());
}
/**
* A {@link DataPacker} instance used to pack data.
*/
public static class DataPacker {
private double offset;
private double scale;
private int reservedValue;
public DataPacker(double offset, double scale, int reservedValue) {
this.offset = offset;
this.scale = scale;
this.reservedValue = reservedValue;
}
public double getScale() {
return scale;
}
public double getOffset() {
return offset;
}
/** Return the reservedValue. */
public int getReservedValue() {
return reservedValue;
}
/**
* Pack the sample using the dataPacking formula:
*
* packed_value = nearestint((original_value - add_offset) / scale_factor)
*
* @param sample the sample to be packed
* @return the packed value
*/
public int pack(double sample) {
double unrounded = ((sample - offset) / scale);
return (int) (unrounded + 0.5);
}
}
/**
* Stores and update data stats which will be used to get a specific
* {@link DataPacker} instance. Indeed, computed offset and scale
* depend on the data statistics (min and max)
*/
public static class DataStats {
public DataStats() {
min = Double.POSITIVE_INFINITY;
max = Double.NEGATIVE_INFINITY;
}
public double getMin() {
return min;
}
public void setMin(double min) {
this.min = min;
}
public double getMax() {
return max;
}
public void setMax(double max) {
this.max = max;
}
private double min;
private double max;
public void update(double updatingMin, double updatingMax) {
if (!Double.isNaN(updatingMin)) {
min = min > updatingMin ? updatingMin : min;
}
if (!Double.isNaN(updatingMax)) {
max = max < updatingMax ? updatingMax : max;
}
}
}
public static String ADD_OFFSET = "add_offset";
public static String SCALE_FACTOR = "scale_factor";
}