/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2014, Geomatys * * 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.geotoolkit.image.io.plugin; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; import javax.imageio.stream.ImageInputStream; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferShort; import java.awt.image.WritableRaster; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.util.Locale; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.sis.measure.Units; import org.apache.sis.internal.storage.ChannelImageInputStream; import org.geotoolkit.image.io.SpatialImageReader; import org.opengis.metadata.content.TransferFunctionType; import org.opengis.metadata.spatial.CellGeometry; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.datum.PixelInCell; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.referencing.CommonCRS; import org.geotoolkit.coverage.grid.GridEnvelope2D; import org.geotoolkit.coverage.grid.GridGeometry2D; import org.geotoolkit.image.io.metadata.ReferencingBuilder; import org.geotoolkit.image.io.metadata.SpatialMetadata; import org.geotoolkit.internal.image.io.DimensionAccessor; import org.geotoolkit.internal.image.io.GridDomainAccessor; /** * An image reader to read SRTM data in .hgt format. * * /!\ WARNING : Ugly hacks are used for input management. Extended RawImageReader needs a stream, but we build it only * at reading time because we encountered unclosed streams problems. * * @author Alexis Manin (Geomatys) */ public class HGTReader extends SpatialImageReader { /** * HGT file name pattern. Give lower-left geographic position (CRS:84) of the current tile. */ private static final Pattern FILENAME_PATTERN = Pattern.compile("(?i)(N|S)(\\d+)(E|W)(\\d+)"); private static final ImageTypeSpecifier IMAGE_TYPE = ImageTypeSpecifier.createGrayscale(16, DataBuffer.TYPE_SHORT, true); static final int SAMPLE_SIZE = Short.SIZE / Byte.SIZE; private File fileInput; /** * Constructs a new image reader. * * @param provider the {@link javax.imageio.spi.ImageReaderSpi} that is invoking this constructor, or null. */ public HGTReader(Spi provider) { super(provider); } @Override public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { super.setInput(input, seekForwardOnly, ignoreMetadata); if (input instanceof Path) { fileInput = ((Path) input).toFile(); } else if (input instanceof File) { fileInput = (File) input; } else { fileInput = null; } } /** * {@inheritDoc} */ @Override public int getNumImages(final boolean allowSearch) throws IllegalStateException, IOException { return 1; } /** * {@inheritDoc} */ @Override public int getWidth(final int imageIndex) throws IOException { checkImageIndex(imageIndex); if (fileInput == null) { throw new IOException("No valid input set."); } return (int) Math.round(Math.sqrt(fileInput.length() / (Short.SIZE / Byte.SIZE))); } /** * {@inheritDoc} */ @Override public int getHeight(final int imageIndex) throws IOException { checkImageIndex(imageIndex); if (fileInput == null) { throw new IOException("No valid input set."); } return (int) Math.round(Math.sqrt(fileInput.length() / (Short.SIZE / Byte.SIZE))); } /** * {@inheritDoc} */ @Override public int getNumBands(final int imageIndex) throws IOException { return 1; } /** * {@inheritDoc} */ @Override protected int getRawDataType(final int imageIndex) throws IOException { checkImageIndex(imageIndex); return IMAGE_TYPE.getSampleModel().getDataType(); } /** * {@inheritDoc} */ @Override public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException { checkImageIndex(imageIndex); return IMAGE_TYPE; } /** * Returns {@code true} since random access is easy in uncompressed images. */ @Override public boolean isRandomAccessEasy(final int imageIndex) throws IOException { checkImageIndex(imageIndex); return true; } @Override protected SpatialMetadata createMetadata(int imageIndex) throws IOException { if (imageIndex < 0) return null; SpatialMetadata md = new SpatialMetadata(false, this, null); final DimensionAccessor dac = new DimensionAccessor(md); dac.selectChild(dac.appendChild()); dac.setFillSampleValues(Short.MIN_VALUE); dac.setValidSampleValue(Short.MIN_VALUE + 1, Short.MAX_VALUE); dac.setTransfertFunction(1, 0, TransferFunctionType.LINEAR); dac.setUnits(Units.METRE); if (fileInput == null) { throw new IOException("No valid input set."); } try { // Set Geo-spatial information. GridDomainAccessor accessor = new GridDomainAccessor(md); final String filename = fileInput.getName(); final Matcher matcher = FILENAME_PATTERN.matcher(filename); if (!matcher.find()) { LOGGER.log(Level.WARNING, "input name does not match : " + filename); } else { final GeographicCRS geographicCRS = CommonCRS.WGS84.normalizedGeographic(); final ReferencingBuilder builder = new ReferencingBuilder(md); builder.setCoordinateReferenceSystem(geographicCRS); final GridEnvelope2D gridEnv = new GridEnvelope2D(0, 0, getWidth(imageIndex), getHeight(imageIndex)); final GeneralEnvelope envelope = new GeneralEnvelope(geographicCRS); final int latitude = matcher.group(1).toLowerCase().startsWith("n")? Integer.parseInt(matcher.group(2)) : -Integer.parseInt(matcher.group(2)); final int longitude = matcher.group(3).toLowerCase().startsWith("e")? Integer.parseInt(matcher.group(4)) : -Integer.parseInt(matcher.group(4)); envelope.setRange(0, longitude, longitude+1); envelope.setRange(1, latitude, latitude+1); accessor.setGridGeometry(new GridGeometry2D(gridEnv, envelope), PixelInCell.CELL_CORNER, CellGeometry.POINT); } } catch (Exception e) { LOGGER.log(Level.WARNING, "Geo-spatial information cannot be retrieved from input " + input, e); } return md; } @Override public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { final int width = (int) StrictMath.round(StrictMath.sqrt(fileInput.length() / (Short.SIZE / Byte.SIZE))); final Rectangle srcRegion = new Rectangle(); final Rectangle dstRegion = new Rectangle(); final BufferedImage image = getDestination(param, getImageTypes(imageIndex), width, width); computeRegions(param, width, width, image, srcRegion, dstRegion); readLayer(image.getRaster(), param, srcRegion, dstRegion, width); return image; } /** * Processes to the image reading, and stores the pixels in the given raster.<br/> * Process fill raster from informations stored in stripOffset made. * * @param raster The raster where to store the pixel values. * @param param Parameters used to control the reading process, or {@code null}. * @param srcRegion The region to read in source image. * @param dstRegion The region to write in the given raster. * @throws IOException If an error occurred while reading the image. */ private void readLayer(final WritableRaster raster, final ImageReadParam param, final Rectangle srcRegion, final Rectangle dstRegion, int srcImgWidth) throws IOException { try (final ImageInputStream imageStream = getImageInputStream()) { final DataBufferShort dataBuffer = (DataBufferShort) raster.getDataBuffer(); final short[] data = dataBuffer.getData(0); final double stepX = (param == null)? 1 : param.getSourceXSubsampling(); final double stepY = (param == null)? 1 : param.getSourceYSubsampling(); // Current position (in byte) from source file. long srcBuffPos = (srcRegion.y * srcImgWidth + srcRegion.x) * SAMPLE_SIZE; long srcScanLineStride = srcImgWidth * SAMPLE_SIZE; // Current position in destination array (short) int destPosition = dstRegion.y * raster.getWidth() + dstRegion.x; int destScanLineStride = raster.getWidth(); final int srcMaxY = srcRegion.y + srcRegion.height; if (stepX == 1) { for (int y = srcRegion.y; y < srcMaxY; y += stepY) { imageStream.seek(srcBuffPos); imageStream.readFully(data, destPosition, srcRegion.width); // Prepare to go on the next line of source image srcBuffPos += srcScanLineStride * stepY; // Prepare to go on the next line of destination image destPosition += destScanLineStride; } } else { for (int y = srcRegion.y; y < srcMaxY; y += stepY) { int tmpDestPosition = destPosition; for (int i = 0; i < srcRegion.width; i += stepX, tmpDestPosition++) { // data[tmpDestPosition] = tmpLine[i]; imageStream.seek(srcBuffPos+(i*SAMPLE_SIZE)); imageStream.readFully(data, tmpDestPosition, 1); } // Prepare to go on the next line of source image srcBuffPos += srcScanLineStride * stepY; // Prepare to go on the next line of destination image destPosition += destScanLineStride; } } } } private ImageInputStream getImageInputStream() throws IOException { return new ChannelImageInputStream(null, openChannel(fileInput), ByteBuffer.allocateDirect(8192), false); } private static SeekableByteChannel openChannel(Object input) throws IOException { final Path inputPath; if (input instanceof File) { inputPath = ((File) input).toPath(); } else if (input instanceof Path) { inputPath = (Path) input; } else { throw new IOException("Input object is not a valid file or path."); } return Files.newByteChannel(inputPath); } public static class Spi extends SpatialImageReader.Spi { /** * The list of valid input types. */ private static final Class<?>[] INPUT_TYPES = new Class<?>[] {File.class, Path.class}; /** * Default list of file extensions. */ private static final String[] SUFFIXES = new String[] {"hgt"}; private static final String[] MIME_TYPES = new String[] {"application/x-ogc-srtmhgt"}; /** * Constructs a default {@code RawImageReader.Spi}. The fields are initialized as * documented in the <a href="#skip-navbar_top">class javadoc</a>. Subclasses can * modify those values if desired. * <p> * For efficiency reasons, the fields are initialized to shared arrays. * Subclasses can assign new arrays, but should not modify the default array content. */ public Spi() { names = SUFFIXES; suffixes = SUFFIXES; MIMETypes = MIME_TYPES; inputTypes = INPUT_TYPES; pluginClassName = HGTReader.class.getName(); // This reader does not support any metadata. nativeStreamMetadataFormatName = null; nativeImageMetadataFormatName = null; } @Override public String getDescription(Locale locale) { return "NASA HGT format for SRTM distribution."; } @Override public boolean canDecodeInput(Object source) throws IOException { final Path temp; if (source instanceof File) { temp = ((File) source).toPath(); } else if (source instanceof Path) { temp = (Path) source; } else { return false; } return (FILENAME_PATTERN.matcher(temp.getFileName().toString()).find() && Files.isReadable(temp)); } @Override public ImageReader createReaderInstance(final Object extension) throws IOException { return new HGTReader(this); } } }