/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-2012, Open Source Geospatial Foundation (OSGeo) * * 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 org.geotools.referencing.factory.gridshift; import java.io.BufferedReader; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.util.StringTokenizer; import java.util.logging.Logger; import org.geotools.factory.BufferedFactory; import org.geotools.referencing.factory.ReferencingFactory; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.geotools.util.SoftValueHashMap; import org.geotools.util.logging.Logging; import org.opengis.referencing.FactoryException; /** * Loads and caches NADCON grid shifts * * @author Andrea Aime - GeoSolutions * */ public class NADCONGridShiftFactory extends ReferencingFactory implements BufferedFactory { static final class NADCONKey { String latFile; String longFile; public NADCONKey(String latFile, String longFile) { super(); this.latFile = latFile; this.longFile = longFile; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((latFile == null) ? 0 : latFile.hashCode()); result = prime * result + ((longFile == null) ? 0 : longFile.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; NADCONKey other = (NADCONKey) obj; if (latFile == null) { if (other.latFile != null) return false; } else if (!latFile.equals(other.latFile)) return false; if (longFile == null) { if (other.longFile != null) return false; } else if (!longFile.equals(other.longFile)) return false; return true; } } /** * The number of hard references to hold internally. */ private static final int GRID_CACHE_HARD_REFERENCES = 10; /** * Logger. */ protected static final Logger LOGGER = Logging.getLogger("org.geotools.referencing"); /** * The soft cache that holds loaded grids. */ private SoftValueHashMap<NADCONKey, NADConGridShift> gridCache; /** * Constructs a factory with the default priority. */ public NADCONGridShiftFactory() { gridCache = new SoftValueHashMap<NADCONKey, NADConGridShift>(GRID_CACHE_HARD_REFERENCES); } public NADConGridShift loadGridShift(URL latGridURL, URL longGridURL) throws FactoryException { NADCONKey key = new NADCONKey(latGridURL.toExternalForm(), longGridURL.toExternalForm()); synchronized (gridCache) { // Prevent simultaneous threads trying to load same grid NADConGridShift grid = gridCache.get(key); if (grid != null) { // Cached: return grid; // - Return } else { // Not cached: grid = loadGridShiftInternal(latGridURL, longGridURL); // - Load if (grid != null) { gridCache.put(key, grid); // - Cache return grid; // - Return } } throw new FactoryException("NTv2 Grid " + latGridURL + ", " + longGridURL + " could not be created."); } } private NADConGridShift loadGridShiftInternal(URL latGridURL, URL longGridURL) throws FactoryException { // decide if text or binary grid will be used String latGridName = DataUtilities.urlToFile(latGridURL).getPath(); String longGridName = DataUtilities.urlToFile(longGridURL).getPath(); try { if ((latGridName.endsWith(".las") && longGridName.endsWith(".los")) || (latGridName.endsWith(".LAS") && longGridName.endsWith(".LOS"))) { return loadBinaryGrid(latGridURL, longGridURL); } else if ((latGridName.endsWith(".laa") && longGridName.endsWith(".loa")) || (latGridName.endsWith(".LAA") && longGridName.endsWith(".LOA"))) { return loadTextGrid(latGridURL, longGridURL); } else { throw new FactoryException(Errors.format(ErrorKeys.UNSUPPORTED_FILE_TYPE_$2, latGridName.substring(latGridName.lastIndexOf('.') + 1), longGridName.substring(longGridName.lastIndexOf('.') + 1))); // Note: the +1 above hide the dot, but also make sure that the code is // valid even if the path do not contains '.' at all (-1 + 1 == 0). } } catch (IOException exception) { final Throwable cause = exception.getCause(); if (cause instanceof FactoryException) { throw (FactoryException) cause; } throw new FactoryException(exception.getLocalizedMessage(), exception); } } /** * Reads latitude and longitude binary grid shift file data into {@link grid}. The file is * organized into records, with the first record containing the header information, followed by * the shift data. The header values are: text describing grid (64 bytes), num. columns (int), * num. rows (int), num. z (int), min x (float), delta x (float), min y (float), delta y (float) * and angle (float). Each record is num. columns 4 bytes + 4 byte separator long and the file * contains num. rows + 1 (for the header) records. The data records (with the grid shift * values) are all floats and have a 4 byte separator (0's) before the data. Row records are * organized from low y (latitude) to high and columns are orderd from low longitude to high. * Everything is written in low byte order. * * @param latGridUrl URL to the binary latitude shift file (.las extention). * @param longGridUrl URL to the binary longitude shift file (.los extention). * @throws IOException if the data files cannot be read. * @throws FactoryException if there is an inconsistency in the data */ private NADConGridShift loadBinaryGrid(final URL latGridUrl, final URL longGridUrl) throws IOException, FactoryException { final int HEADER_BYTES = 96; final int SEPARATOR_BYTES = 4; final int DESCRIPTION_LENGTH = 64; ReadableByteChannel latChannel = null; ReadableByteChannel longChannel = null; NADConGridShift gridShift = null; ByteBuffer latBuffer; ByteBuffer longBuffer; try { // ////////////////////// // setup // ////////////////////// latChannel = getReadChannel(latGridUrl); latBuffer = fillBuffer(latChannel, HEADER_BYTES); longChannel = getReadChannel(longGridUrl); longBuffer = fillBuffer(longChannel, HEADER_BYTES); // ////////////////////// // read header info // ////////////////////// // skip the header description latBuffer.position(latBuffer.position() + DESCRIPTION_LENGTH); int nc = latBuffer.getInt(); int nr = latBuffer.getInt(); int nz = latBuffer.getInt(); float xmin = latBuffer.getFloat(); float dx = latBuffer.getFloat(); float ymin = latBuffer.getFloat(); float dy = latBuffer.getFloat(); float angle = latBuffer.getFloat(); float xmax = xmin + ((nc - 1) * dx); float ymax = ymin + ((nr - 1) * dy); // skip the longitude header description longBuffer.position(longBuffer.position() + DESCRIPTION_LENGTH); // check that latitude grid header is the same as for latitude grid if ((nc != longBuffer.getInt()) || (nr != longBuffer.getInt()) || (nz != longBuffer.getInt()) || (xmin != longBuffer.getFloat()) || (dx != longBuffer.getFloat()) || (ymin != longBuffer.getFloat()) || (dy != longBuffer.getFloat()) || (angle != longBuffer.getFloat())) { throw new FactoryException(Errors.format(ErrorKeys.GRID_LOCATIONS_UNEQUAL)); } // ////////////////////// // read grid shift data into LocalizationGrid // ////////////////////// final int RECORD_LENGTH = (nc * 4) + SEPARATOR_BYTES; final int NUM_BYTES_LEFT = ((nr + 1) * RECORD_LENGTH) - HEADER_BYTES; final int START_OF_DATA = RECORD_LENGTH - HEADER_BYTES; latBuffer = fillBuffer(latChannel, NUM_BYTES_LEFT); latBuffer.position(START_OF_DATA); // start of second record (data) longBuffer = fillBuffer(longChannel, NUM_BYTES_LEFT); longBuffer.position(START_OF_DATA); gridShift = new NADConGridShift(xmin, ymin, xmax, ymax, dx, dy, nc, nr); int i = 0; int j = 0; for (i = 0; i < nr; i++) { latBuffer.position(latBuffer.position() + SEPARATOR_BYTES); // skip record separator longBuffer.position(longBuffer.position() + SEPARATOR_BYTES); for (j = 0; j < nc; j++) { gridShift.setLocalizationPoint(j, i, longBuffer.getFloat(), latBuffer.getFloat()); } } assert i == nr : i; assert j == nc : j; } finally { if(latChannel != null) { latChannel.close(); } if(longChannel != null) { longChannel.close(); } } return gridShift; } /** * Returns a new bytebuffer, of numBytes length and little endian byte order, filled from the * channel. * * @param channel the channel to fill the buffer from * @param numBytes number of bytes to read * @return a new bytebuffer filled from the channel * @throws IOException if there is a problem reading the channel * @throws EOFException if the end of the channel is reached */ private ByteBuffer fillBuffer(ReadableByteChannel channel, int numBytes) throws IOException { ByteBuffer buf = ByteBuffer.allocate(numBytes); if (fill(buf, channel) == -1) { throw new EOFException(Errors.format(ErrorKeys.END_OF_DATA_FILE)); } buf.flip(); buf.order(ByteOrder.LITTLE_ENDIAN); return buf; } /** * Fills the bytebuffer from the channel. Code was lifted from ShapefileDataStore. * * @param buffer bytebuffer to fill from the channel * @param channel channel to fill the buffer from * @return number of bytes read * @throws IOException if there is a problem reading the channel */ private int fill(ByteBuffer buffer, ReadableByteChannel channel) throws IOException { int r = buffer.remaining(); // channel reads return -1 when EOF or other error // because they a non-blocking reads, 0 is a valid return value!! while ((buffer.remaining() > 0) && (r != -1)) { r = channel.read(buffer); } if (r == -1) { buffer.limit(buffer.position()); } return r; } /** * Obtain a ReadableByteChannel from the given URL. If the url protocol is file, a FileChannel * will be returned. Otherwise a generic channel will be obtained from the urls input stream. * Code swiped from ShapefileDataStore. * * @param url URL to create the channel from * @return a new PeadableByteChannel from the input url * @throws IOException if there is a problem creating the channel */ private ReadableByteChannel getReadChannel(URL url) throws IOException { ReadableByteChannel channel = null; if (url.getProtocol().equals("file")) { File file = DataUtilities.urlToFile(url); if (!file.exists() || !file.canRead()) { throw new IOException(Errors.format(ErrorKeys.FILE_DOES_NOT_EXIST_$1, file)); } FileInputStream in = new FileInputStream(file); channel = in.getChannel(); } else { InputStream in = url.openConnection().getInputStream(); channel = Channels.newChannel(in); } return channel; } /** * Reads latitude and longitude text grid shift file data into {@link grid}. The first two lines * of the shift data file contain the header, with the first being a description of the grid. * The second line contains 8 values separated by spaces: num. columns, num. rows, num. z, min * x, delta x, min y, delta y and angle. Shift data values follow this and are also separated by * spaces. Row records are organized from low y (latitude) to high and columns are orderd from * low longitude to high. * * @param latGridUrl URL to the text latitude shift file (.laa extention). * @param longGridUrl URL to the text longitude shift file (.loa extention). * @throws IOException if the data files cannot be read. * @throws FactoryException if there is an inconsistency in the data */ private NADConGridShift loadTextGrid(URL latGridUrl, URL longGridUrl) throws IOException, FactoryException { String latLine; String longLine; StringTokenizer latSt; StringTokenizer longSt; // ////////////////////// // setup // ////////////////////// InputStreamReader latIsr = new InputStreamReader(latGridUrl.openStream()); BufferedReader latBr = new BufferedReader(latIsr); InputStreamReader longIsr = new InputStreamReader(longGridUrl.openStream()); BufferedReader longBr = new BufferedReader(longIsr); // ////////////////////// // read header info // ////////////////////// latLine = latBr.readLine(); // skip header description latLine = latBr.readLine(); latSt = new StringTokenizer(latLine, " "); if (latSt.countTokens() != 8) { throw new FactoryException(Errors.format(ErrorKeys.HEADER_UNEXPECTED_LENGTH_$1, String.valueOf(latSt.countTokens()))); } int nc = Integer.parseInt(latSt.nextToken()); int nr = Integer.parseInt(latSt.nextToken()); int nz = Integer.parseInt(latSt.nextToken()); float xmin = Float.parseFloat(latSt.nextToken()); float dx = Float.parseFloat(latSt.nextToken()); float ymin = Float.parseFloat(latSt.nextToken()); float dy = Float.parseFloat(latSt.nextToken()); float angle = Float.parseFloat(latSt.nextToken()); float xmax = xmin + ((nc - 1) * dx); float ymax = ymin + ((nr - 1) * dy); // now read long shift grid longLine = longBr.readLine(); // skip header description longLine = longBr.readLine(); longSt = new StringTokenizer(longLine, " "); if (longSt.countTokens() != 8) { throw new FactoryException(Errors.format(ErrorKeys.HEADER_UNEXPECTED_LENGTH_$1, String.valueOf(longSt.countTokens()))); } // check that latitude grid header is the same as for latitude grid if ((nc != Integer.parseInt(longSt.nextToken())) || (nr != Integer.parseInt(longSt.nextToken())) || (nz != Integer.parseInt(longSt.nextToken())) || (xmin != Float.parseFloat(longSt.nextToken())) || (dx != Float.parseFloat(longSt.nextToken())) || (ymin != Float.parseFloat(longSt.nextToken())) || (dy != Float.parseFloat(longSt.nextToken())) || (angle != Float.parseFloat(longSt.nextToken()))) { throw new FactoryException(Errors.format(ErrorKeys.GRID_LOCATIONS_UNEQUAL)); } // ////////////////////// // read grid shift data into LocalizationGrid // ////////////////////// NADConGridShift gridShift = new NADConGridShift(xmin, ymin, xmax, ymax, dx, dy, nc, nr); int i = 0; int j = 0; for (i = 0; i < nr; i++) { for (j = 0; j < nc;) { latLine = latBr.readLine(); latSt = new StringTokenizer(latLine, " "); longLine = longBr.readLine(); longSt = new StringTokenizer(longLine, " "); while (latSt.hasMoreTokens() && longSt.hasMoreTokens()) { gridShift.setLocalizationPoint(j, i, (double) Float.parseFloat(longSt.nextToken()), (double) Float.parseFloat(latSt.nextToken())); ++j; } } } assert i == nr : i; assert j == nc : j; return gridShift; } }