/*
* Geotools2 - OpenSource mapping toolkit
* http://geotools.org
* (C) 2003-2006, GeoTools Project Managment Committee (PMC)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
*/
package gtmig.org.geotools.gce.arcgrid;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StreamTokenizer;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import javax.media.jai.RasterFactory;
import org.geotools.gce.arcgrid.ArcGridFormat;
import org.geotools.resources.NIOUtilities;
/**
* Martin Schmitz (2009-09-02):
* This class is taken 1:1 from {@code gt2-2.3.0-M0} because of the following reasons:
*
* - class not included in later GT versions
* - using {@code ArcGridReader} from "standard" GT makes problems with
* raster colorization (no default style, so "everything green" without SLD;
* no data value specified in meta data not displayed transparent).
* - XULU needs to read only the raster meta data (without reading the whole
* raster). Don't know how to realize this with {@link ArcGridFormat}.
* Maybe: {@code ArcGridReader.getMetaDataValue(..)}
*
* - To support negative cell values, the constants {@code Double.MIN_VALUE}
* and {@code Double.MAX_VALUE} were replaced with {@code Double.NEGATIVE_INFINITY}
* and {@code Double.POSITIVE_INFINITY}.
* - BugFix: Because on writing the raster the min- and max-values are NOT recalculated
* it makes no sense to initialize these variables. So these lines
* were outlined (line 566-568).
*
*
* Nevertheless we will try to avoid this class... but later.
* @deprecated
*
* Class user for parsing an ArcGrid header (.arc, .asc) file.
*
* @author Christiaan ten Klooster
* @author Andrea Aime
* @author Simone Giannecchini (simboss)
* @source $URL$
*/
public class ArcGridRaster {
/** Column number tag in the header file */
public static final String NCOLS = "NCOLS";
/** Row number tag in the header file */
public static final String NROWS = "NROWS";
/** x corner coordinate tag in the header file */
public static final String XLLCORNER = "XLLCORNER";
/** y corner coordinate tag in the header file */
public static final String YLLCORNER = "YLLCORNER";
/** cell size tag in the header file */
public static final String CELLSIZE = "CELLSIZE";
/** no data tag in the header file */
public static final String NODATA_VALUE = "NODATA_VALUE";
/** header or data file url */
private URL srcURL;
/** max value found in the file */
protected double maxValue = Double.NEGATIVE_INFINITY;
/** min value found in the file */
protected double minValue = Double.POSITIVE_INFINITY;
protected double xllCorner = Double.NaN;
protected double yllCorner = Double.NaN;
protected double cellSize = Double.NaN;
protected double noData = -9999;
protected int nCols = -1;
protected int nRows = -1;
private Reader reader = null;
private PrintWriter writer = null;
private boolean compress;
/**
* Creates a new instance of ArcGridRaster.
*
* @param srcURL
* URL of a ArcGridRaster.
*
* @throws IOException
* DOCUMENT ME!
*/
public ArcGridRaster(URL srcURL) {
this.srcURL = srcURL;
if (srcURL.getFile().endsWith(".gz")) {
compress = true;
}
}
/**
* Creates a new instance of ArcGridRaster. Used Exclusively by
* ArcGridReader.
*
* @param reader
* reader to be used for reading the Raster.
* @param compress
* DOCUMENT ME!
*
* @throws IOException
* DOCUMENT ME!
*/
public ArcGridRaster(Reader reader, boolean compress) {
this.reader = reader;
this.compress = compress;
}
/**
* Creates a new instance of ArcGridRaster. Used Exclusively by
* ArcGridWriter.
*
* @param writer
* writer to be used for writing the Raster.
*
* @throws IOException
* DOCUMENT ME!
*/
public ArcGridRaster(PrintWriter writer) {
this.writer = writer;
}
/**
* Max value.
*
* @return the max value contained in the data file
*/
public double getMaxValue() {
return maxValue;
}
/**
* Min value.
*
* @return the min value contained in the data file
*/
public final double getMinValue() {
return minValue;
}
/**
* Returns the number of rows contained in the file.
*
* @return number of rows
*/
public final int getNRows() {
return nRows;
}
/**
* Returns the number of columns contained in the file.
*
* @return number of columns
*/
public final int getNCols() {
return nCols;
}
/**
* Returns the x cordinate of the lower left corner.
*
* @return x cordinate of the lower left corner.
*/
public final double getXlCorner() {
return xllCorner;
}
/**
* Returns the y cordinate of the lower left corner.
*
* @return y cordinate of the lower left corner.
*/
public final double getYlCorner() {
return yllCorner;
}
/**
* Returns the cell size.
*
* @return cell size
*/
public final double getCellSize() {
return cellSize;
}
/**
* Returns the no data (null) value.
*
* @return no data (null) value.
*/
public final double getNoData() {
return noData;
}
/**
* Parses the reader for the known properties.
*
* @throws IOException
* for reading errors
*/
public final void parseHeader() throws IOException {
parseHeader(new StreamTokenizer(openReader()));
}
/**
* Parse the header of an ascii grid file.
*
* @param st
* StringTokenizer to be used to parse this header.
* @throws IOException
*/
protected void parseHeader(StreamTokenizer st) throws IOException {
// make sure tokenizer is set up right
st.resetSyntax();
st.eolIsSignificant(true);
st.whitespaceChars(0, ' ');
st.wordChars('a', 'z');
st.wordChars('A', 'Z');
st.wordChars('_', '_');
st.parseNumbers();
String key = null;
// read lines while the next token is not a number
while (st.nextToken() != StreamTokenizer.TT_NUMBER) {
if (st.ttype == StreamTokenizer.TT_WORD) {
key = st.sval;
if (st.nextToken() != StreamTokenizer.TT_NUMBER) {
throw new IOException("Expected number after " + key);
}
double val = st.nval;
if (NCOLS.equalsIgnoreCase(key)) {
nCols = (int) val;
} else if (NROWS.equalsIgnoreCase(key)) {
nRows = (int) val;
} else if (XLLCORNER.equalsIgnoreCase(key)) {
xllCorner = readHeaderDouble(st);
} else if (YLLCORNER.equalsIgnoreCase(key)) {
yllCorner = readHeaderDouble(st);
} else if (CELLSIZE.equalsIgnoreCase(key)) {
cellSize = readHeaderDouble(st);
} else if (NODATA_VALUE.equalsIgnoreCase(key)) {
noData = readHeaderDouble(st);
} else {
// ignore extra fields for now
// are there ever any?
}
if (st.nextToken() != StreamTokenizer.TT_EOL) {
throw new IOException("Expected new line, not " + st.sval);
}
} else {
throw new IOException("Expected word token");
}
}
st.pushBack();
}
protected double readHeaderDouble(StreamTokenizer st) throws IOException {
double val = st.nval;
if ((st.nextToken() == StreamTokenizer.TT_WORD)
&& st.sval.startsWith("E")) {
val = val * Math.pow(10, Integer.parseInt(st.sval.substring(1)));
} else {
st.pushBack();
}
return val;
}
/**
* Obtain the best reader for the situation
*
* @return A reader to read this file.
*
* @throws IOException
* DOCUMENT ME!
*/
protected Reader openReader() throws IOException {
if (reader != null) {
return reader;
}
// gzipped source, may be remote URL
if (compress) {
InputStream in = new java.util.zip.GZIPInputStream(srcURL
.openStream());
return new InputStreamReader(new java.io.BufferedInputStream(in));
}
// file based, non zipped - lets use memory-mapped reader
if (srcURL.getProtocol().equals("file")) {
return new MemoryMappedReader(new File(java.net.URLDecoder.decode(
srcURL.getPath(), "UTF-8")));
}
// default URL
return new InputStreamReader(srcURL.openStream());
}
/**
* Open the best writer for the situation.
*
* @param compress
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*
* @throws IOException
* DOCUMENT ME!
*/
protected PrintWriter openWriter(boolean compress) throws IOException {
if (writer != null) {
return writer;
}
java.io.OutputStream out;
if (srcURL.getProtocol().equals("file")) {
out = new java.io.BufferedOutputStream(
new java.io.FileOutputStream(new File(java.net.URLDecoder
.decode(srcURL.getPath(), "UTF-8"))));
} else {
out = srcURL.openConnection().getOutputStream();
}
if (compress) {
out = new java.util.zip.GZIPOutputStream(out);
}
return new PrintWriter(out);
}
/**
* Returns the WritableRaster of the raster. This method first parses the
* header then reads all the data.
*
* @return RenderedImage
*
* @throws IOException
* DOCUMENT ME!
*/
public WritableRaster readRaster() throws IOException {
// open reader and make tokenizer
Reader reader = openReader();
StreamTokenizer st = new StreamTokenizer(reader);
// parse header
parseHeader(st);
// reconfigure tokenizer
st.resetSyntax();
st.parseNumbers();
st.whitespaceChars(' ', ' ');
st.whitespaceChars(' ', '\t');
st.whitespaceChars('\n', '\n');
st.whitespaceChars('\r', '\r'); // linefeed (on windows only?)
st.whitespaceChars('\f', '\f'); // form feed (on printers????)
st.eolIsSignificant(false);
st.ordinaryChars('E', 'E');
// allocate raster, for now this is done with floating point data,
// though eventually it should be configurable
WritableRaster raster = RasterFactory.createBandedRaster(
java.awt.image.DataBuffer.TYPE_FLOAT, getNCols(), getNRows(),
1, null);
// Read values from grid and put into raster.
// Values must be numbers, which may be simple , or expressed
// in scientific notation E.
// The following loop can read both, even if mixed.
// The loop expects a token to be read already
st.nextToken();
for (int y = 0; y < getNRows(); y++) {
for (int x = 0; x < getNCols(); x++) {
// this call always reads the next token
double d = readCell(st, x, y);
// mask no data values with NaN
if (d == getNoData()) {
d = Double.NaN;
} else {
minValue = Math.min(minValue, d);
maxValue = Math.max(maxValue, d);
}
// set the value at x,y in band 0 to the parsed value
raster.setSample(x, y, 0, (float) d);
}
}
reader.close();
return raster;
}
/**
* Parse a number.
*
* @param st
* DOCUMENT ME!
* @param x
* DOCUMENT ME!
* @param y
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*
* @throws IOException
* DOCUMENT ME!
*/
private double readCell(StreamTokenizer st, int x, int y)
throws IOException {
double d = 0;
// read a token, expected: a number
switch (st.ttype) {
case StreamTokenizer.TT_NUMBER:
d = (float) st.nval;
break;
case StreamTokenizer.TT_EOF:
throw new IOException("Unexpected EOF at " + x + "," + y);
default:
throw new IOException("Unknown token " + st.ttype);
}
// read another. May be an exponent of this number.
// If its not an exponent, its the next number. Fall through
// and token is prefetched for next loop...
switch (st.nextToken()) {
case 'e':
case 'E':
// now read the exponent
st.nextToken();
if (st.ttype != StreamTokenizer.TT_NUMBER) {
throw new IOException("Expected exponent at " + x + "," + y);
}
// calculate
d = d * Math.pow(10.0, st.nval);
// prefetch for next loop
st.nextToken();
break;
case StreamTokenizer.TT_NUMBER:
case StreamTokenizer.TT_EOF:
break;
default:
throw new IOException("Expected Number or EOF");
}
return d;
}
/**
* Print n spaces to the PrintWriter
*
* @param p
* DOCUMENT ME!
* @param n
* DOCUMENT ME!
*/
protected void spaces(PrintWriter p, int n) {
for (int i = 0; i < n; i++) {
p.print(' ');
}
}
/**
* Write out the given raster..
*
* @param raster
* DOCUMENT ME!
* @param xl
* DOCUMENT ME!
* @param yl
* DOCUMENT ME!
* @param cellsize
* DOCUMENT ME!
* @param compress
* DOCUMENT ME!
*
* @throws IOException
* DOCUMENT ME!
*/
public void writeRaster(Raster raster, double xl, double yl,
double cellsize, boolean compress) throws IOException {
// open writer
PrintWriter out = openWriter(compress);
// output header and assign header fields
out.print(NCOLS);
spaces(out, 9);
out.println(nCols = raster.getWidth());
out.print(NROWS);
spaces(out, 9);
out.println(nRows = raster.getHeight());
out.print(XLLCORNER);
spaces(out, 5);
out.println(xllCorner = xl);
out.print(YLLCORNER);
spaces(out, 5);
out.println(yllCorner = yl);
out.print(CELLSIZE);
spaces(out, 6);
out.println(cellSize = cellsize);
out.print(NODATA_VALUE);
spaces(out, 2);
out.println(noData);
//MS-01.so
// // reset min and max
// minValue = Double.MIN_VALUE;
// maxValue = Double.MAX_VALUE;
//MS-01.eo
// a buffer to flush each line to
// this technique makes things a bit quicker because buffer.append()
// internally calls new FloatingDecimal(double).appendTo(StringBuffer)
// instead of creating a new String each time (as would be done with
// PrintWriter, ie. print(new FloatingDecimal(double).toString())
StringBuffer buffer = new StringBuffer(raster.getWidth() * 4);
for (int i = 0, ii = raster.getHeight(); i < ii; i++) {
// clear buffer
buffer.delete(0, buffer.length());
// write row to buffer
for (int j = 0, jj = raster.getWidth(); j < jj; j++) {
double v = raster.getSampleDouble(j, i, 0);
// no data masking
if (Double.isNaN(v)) {
v = noData;
}
// append value and possible spacer
buffer.append(v);
if ((j + 1) < jj) {
buffer.append(' ');
}
}
// flush out row
out.write(buffer.toString());
out.println();
}
out.flush();
out.close();
}
public String toString() {
java.lang.reflect.Field[] f = getClass().getDeclaredFields();
String s = "";
for (int i = 0, ii = f.length; i < ii; i++) {
if (!java.lang.reflect.Modifier.isStatic(f[i].getModifiers())) {
try {
s += (f[i].getName() + " : " + f[i].get(this));
if ((i + 1) < f.length) {
s += " ";
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
return s;
}
/**
* This is a slight optimization over using a BufferedReader.
* StreamTokenizer makes single character read calls.
*/
static class MemoryMappedReader extends java.io.Reader {
ByteBuffer map;
CharBuffer chars;
CharsetDecoder decoder = Charset.forName("US-ASCII").newDecoder();
FileChannel channel;
public MemoryMappedReader(File f) throws IOException {
channel = new FileInputStream(f).getChannel();
map = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
chars = CharBuffer.allocate(16 * 1028);
fill();
}
public void close() throws IOException {
if (channel != null) {
channel.close();
}
NIOUtilities.clean(map);
channel = null;
map = null;
}
void fill() {
decoder.decode(map, chars, false);
chars.flip();
}
public int read() {
if (chars.remaining() == 0) {
chars.flip();
fill();
if (chars.remaining() == 0) {
return -1;
}
}
return chars.get();
}
public int read(char[] cbuf, int off, int len) throws IOException {
throw new IOException("Expected single character read");
}
}
}