/*******************************************************************************
* Copyright 2012 Geoscience Australia
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package au.gov.ga.earthsci.worldwind.common.util.io;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import au.gov.ga.earthsci.worldwind.common.util.Validate;
/**
* A class that can read a specified number of 32bit float values
* from a binary source.
* <p/>
* Values are read in groups; and an offset, separation and gap can be specified to control
* the pattern of bytes that are read. This allows complex striding patterns to be specified
* as required.
* <p/>
* This implementation is threadsafe <em>if all access to the underlying stream is performed through this class's methods</em>.
* If the underlying stream is accessed outside of this class behaviour is indeterminate.
*
* @author James Navin (james.navin@ga.gov.au)
*/
public class FloatReader
{
/** The input stream to read bytes from */
private final InputStream is;
/** The offset to start reading from in the provided input stream */
private final int offset;
/** The number of floats to read in each call to {@link #readNextValues} */
private final int groupSize;
/** The number of bytes to skip between groups */
private final int groupSeparation;
/** The number of bytes to skip between elements of a single group */
private final int groupValueGap;
/** The format of floats to read */
private final FloatFormat format;;
/** The byte order of the stream being read */
private final ByteOrder byteOrder;
/**
* Create a new float reader that reads floats one at a time from the provided input stream.
* <p/>
* Convenience constructor. For more configuration options, use the provided Builder class.
*/
public FloatReader(InputStream is) throws IOException
{
this(is, 0, 1, 0, 0, FloatFormat.IEEE, ByteOrder.LITTLE_ENDIAN);
}
/**
* Private constructor used to set all configuration parameters.
* <p/>
* Use the builder class to instantiate fully configured readers.
*/
private FloatReader(InputStream is, int offset, int groupSize, int groupSeparation, int groupValueGap, FloatFormat format, ByteOrder byteOrder) throws IOException
{
Validate.notNull(is, "An input stream is required");
this.is = is;
this.offset = offset;
this.groupSize = groupSize;
this.groupSeparation = groupSeparation;
this.groupValueGap = groupValueGap;
this.format = format;
this.byteOrder = byteOrder;
skipToStart();
}
/**
* Read the next group of values from the input and place them in the provided
* values array.
* <p/>
* Read values will be written into the values array from index 0.
* <p/>
* If the provided array does not have enough capacity to store the read values,
* an {@link IllegalArgumentException} will be thrown.
* <p/>
* If there are not enough bytes left in the input stream to read the float group the remainder of the group
* will be set to NaN.
* <p/>
* After reading the input stream will be positioned at the start of the <em>next</em> value group.
*
* @param values An array to store the read float values in.
*
* @throws IllegalArgumentException if the provided array does not have enough capacity to store the read values
* @throws IOException if there is a problem reading from the underlying stream
*/
synchronized public void readNextValues(float[] values) throws IOException
{
Validate.notNull(values, "A values array is required");
Validate.isTrue(values.length >= groupSize, "Provided values array has length " + values.length + ". Must have at least " + groupSize + " elements to read float group");
for (int i = 0; i < groupSize; i++)
{
values[i] = readFloat();
if (i != groupSize - 1)
{
skipToNextValueInGroup();
}
}
skipToStartOfNextGroup();
}
/**
* Read the next group of values from the input and returns them in the result array.
* <p/>
* Provided as a convenience method. This method creates a new result array on every call.
* The alternative {@link #readNextValues(float[])} is more efficient as the
* same array can be reused by the client.
*
* @see #readNextValues(float[])
*/
synchronized public float[] readNextValues() throws IOException
{
float[] values = new float[groupSize];
readNextValues(values);
return values;
}
/**
* Skip ahead to the start of the next value group.
* <p/>
* Has the same effect as {@link #readNextValues(float[])} except no
* values are read from the input stream.
*
* @throws IOException if there is a problem reading from the underlying stream
*/
synchronized public void skipToNextGroup() throws IOException
{
int skipCount = groupSize * 4 + (groupSize - 1) * groupValueGap + groupSeparation;
skip(skipCount);
}
/**
* Skip ahead by the provided number of bytes, or to the end of the stream if there are fewer bytes in the stream
* than are to be skipped.
*
* @param numBytes The number of bytes to skip
*
* @throws IOException If there is a problem accessing the underling stream
*/
synchronized public void skip(long numBytes) throws IOException
{
is.skip(numBytes);
}
/**
* @return The next float value in the stream
*/
private float readFloat() throws IOException
{
int b0, b1, b2, b3;
if (byteOrder == ByteOrder.LITTLE_ENDIAN)
{
b3 = is.read();
b2 = is.read();
b1 = is.read();
b0 = is.read();
}
else
{
b0 = is.read();
b1 = is.read();
b2 = is.read();
b3 = is.read();
}
return format.bytesToFloat(b0, b1, b2, b3);
}
/**
* Skip forward to the start of the next value in the current value group
*/
private void skipToNextValueInGroup() throws IOException
{
if (groupValueGap > 0)
{
skip(groupValueGap);
}
}
/**
* Skip forward to the start of the next value group
*/
private void skipToStartOfNextGroup() throws IOException
{
if (groupSeparation > 0)
{
skip(groupSeparation);
}
}
/**
* Skip forward to the start of the first value group
*/
private void skipToStart() throws IOException
{
skip(offset);
}
/** An enumeration of supported floating point formats */
public static enum FloatFormat
{
// Enum strategy pattern
IEEE {
@Override
public float bytesToFloat(int b0, int b1, int b2, int b3)
{
return Float.intBitsToFloat((b0) | (b1 << 8) | (b2 << 16) | b3 << 24);
}
},
IBM {
@Override
public float bytesToFloat(int b0, int b1, int b2, int b3)
{
byte S = (byte) ((b3 & 0x80) >> 7);
int E = (b3 & 0x7f);
long F = (b2 << 16) + (b1 << 8) + b0;
if (S == 0 && E == 0 && F == 0)
{
return 0;
}
double A = 16.0;
double B = 64.0;
double e24 = 16777216.0; // 2^24
double M = F / e24;
double F1 = S == 0 ? 1.0 : -1.0;
return (float) (F1 * M * Math.pow(A, E - B));
}
};
public abstract float bytesToFloat(int b0, int b1, int b2, int b3);
}
/**
* A Builder used to construct fully configured {@link FloatReader} instances
*
* @author James Navin (james.navin@ga.gov.au)
*/
public static class Builder
{
private Builder(){};
private InputStream is;
private int offset = 0;
private int groupSize = 1;
private int groupSeparation = 0;
private int groupValueGap = 0;
private FloatFormat format = FloatFormat.IEEE;
private ByteOrder byteOrder = ByteOrder.LITTLE_ENDIAN;
/** Create a new builder for a {@link FloatReader} that wraps the provided {@link InputStream} */
public static Builder newFloatReaderForStream(InputStream s)
{
Builder result = new Builder();
result.is = s;
return result;
}
/** Configure the offset to start reading from in the input stream */
public Builder withOffset(int offset)
{
this.offset = offset;
return this;
}
/** Configure the number of floats to read in each call to {@link FloatReader#readNextValues} */
public Builder withGroupSize(int groupSize)
{
this.groupSize = groupSize;
return this;
}
/** Configure the number of bytes to skip between groups */
public Builder withGroupSeparation(int groupSeparation)
{
this.groupSeparation = groupSeparation;
return this;
}
/** Configure the number of bytes to skip between elements of a single group */
public Builder withGroupValueGap(int groupValueGap)
{
this.groupValueGap = groupValueGap;
return this;
}
/** Configure the format of floats to read */
public Builder withFormat(FloatFormat format)
{
this.format = format;
return this;
}
/** Configure the byte order of the stream being read */
public Builder withByteOrder(ByteOrder byteOrder)
{
this.byteOrder = byteOrder;
return this;
}
/** Construct a {@link FloatReader} using the configured parameters */
public FloatReader build() throws IOException
{
return new FloatReader(is, offset, groupSize, groupSeparation, groupValueGap, format, byteOrder);
}
}
public int getOffset()
{
return offset;
}
public int getGroupSize()
{
return groupSize;
}
public int getGroupSeparation()
{
return groupSeparation;
}
public int getGroupValueGap()
{
return groupValueGap;
}
public FloatFormat getFormat()
{
return format;
}
public ByteOrder getByteOrder()
{
return byteOrder;
}
}