org.esa.s2tbx.dataio.s2.S2TileOpImage.java Source code

Java tutorial

Introduction

Here is the source code for org.esa.s2tbx.dataio.s2.S2TileOpImage.java

Source

/*
 * Copyright (C) 2014-2015 CS-SI (foss-contact@thor.si.c-s.fr)
 * Copyright (C) 2013-2015 Brockmann Consult GmbH (info@brockmann-consult.de)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 3 of the License, or (at your option)
 * any later version.
 * This program 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 General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, see http://www.gnu.org/licenses/
 */

package org.esa.s2tbx.dataio.s2;

import com.bc.ceres.core.Assert;
import com.bc.ceres.glevel.MultiLevelModel;
import org.esa.s2tbx.dataio.Utils;
import org.esa.s2tbx.dataio.jp2.TileLayout;
import org.esa.s2tbx.dataio.openjpeg.CommandOutput;
import org.esa.s2tbx.dataio.openjpeg.OpenJpegUtils;
import org.esa.snap.core.image.ResolutionLevel;
import org.esa.snap.core.image.SingleBandedOpImage;
import org.esa.snap.core.util.Guardian;
import org.esa.snap.core.util.ImageUtils;
import org.esa.snap.core.util.SystemUtils;
import org.esa.snap.core.util.io.FileUtils;

import javax.imageio.stream.FileImageInputStream;
import javax.media.jai.ImageLayout;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.operator.ConstantDescriptor;
import java.awt.*;
import java.awt.image.DataBufferUShort;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.rmi.UnexpectedException;
import java.util.Arrays;
import java.util.logging.Logger;

/**
 * @author Norman Fomferra
 * @author Nicolas Ducoin
 */
public class S2TileOpImage extends SingleBandedOpImage {

    protected TileLayout tileLayout;
    protected File imageFile;
    protected File cacheDir;

    public S2TileOpImage(File imageFile, File cacheDir, Point imagePos, TileLayout tileLayout,
            MultiLevelModel imageModel, int level) {
        super(S2Config.SAMPLE_DATA_BUFFER_TYPE, imagePos, tileLayout.width, tileLayout.height,
                getTileDimAtResolutionLevel(tileLayout.tileWidth, tileLayout.tileHeight, level), null,
                ResolutionLevel.create(imageModel, level));

        Assert.notNull(imageFile, "imageFile");
        Assert.notNull(cacheDir, "cacheDir");
        Assert.notNull(tileLayout, "tileLayout");
        Assert.notNull(imageModel, "imageModel");

        this.logger = SystemUtils.LOG;

        this.imageFile = imageFile;
        this.cacheDir = cacheDir;
        this.tileLayout = tileLayout;
    }

    protected final Logger logger;

    public static PlanarImage create(File imageFile, File cacheDir, Point imagePos, TileLayout tileLayout,
            S2Config config, MultiLevelModel imageModel, S2SpatialResolution productResolution, int level) {

        Assert.notNull(cacheDir, "cacheDir");
        Assert.notNull(tileLayout, "imageLayout");
        Assert.notNull(imageModel, "imageModel");

        if (imageFile != null) {
            SystemUtils.LOG.fine("Image layout: " + tileLayout);

            return new S2TileOpImage(imageFile, cacheDir, imagePos, tileLayout, imageModel, level);
        } else {
            SystemUtils.LOG.fine("Using empty image !");

            TileLayout tileLaoutForProductResolution = config.getTileLayout(productResolution);
            int targetWidth = getSizeAtResolutionLevel(tileLaoutForProductResolution.width, level);
            int targetHeight = getSizeAtResolutionLevel(tileLaoutForProductResolution.height, level);
            Dimension targetTileDim = getTileDimAtResolutionLevel(tileLaoutForProductResolution.tileWidth,
                    tileLaoutForProductResolution.tileHeight, level);
            SampleModel sampleModel = ImageUtils.createSingleBandedSampleModel(S2Config.SAMPLE_DATA_BUFFER_TYPE,
                    targetWidth, targetHeight);
            ImageLayout imageLayout = new ImageLayout(0, 0, targetWidth, targetHeight, 0, 0, targetTileDim.width,
                    targetTileDim.height, sampleModel, null);
            return ConstantDescriptor.create((float) imageLayout.getWidth(null),
                    (float) imageLayout.getHeight(null), new Short[] { S2Config.FILL_CODE_NO_FILE },
                    new RenderingHints(JAI.KEY_IMAGE_LAYOUT, imageLayout));
        }
    }

    @Override
    protected synchronized void computeRect(PlanarImage[] sources, WritableRaster dest, Rectangle destRect) {
        final DataBufferUShort dataBuffer = (DataBufferUShort) dest.getDataBuffer();
        final short[] tileData = dataBuffer.getData();

        final int tileWidth = this.getTileWidth();
        final int tileHeight = this.getTileHeight();
        final int tileX = destRect.x / tileWidth;
        final int tileY = destRect.y / tileHeight;

        if (tileWidth * tileHeight != tileData.length) {
            throw new IllegalStateException(
                    String.format("tileWidth (=%d) * tileHeight (=%d) != tileData.length (=%d)", tileWidth,
                            tileHeight, tileData.length));
        }

        final Dimension jp2TileDim = getDimAtResolutionLevel(tileLayout.tileWidth, tileLayout.tileHeight,
                getLevel());

        final int jp2TileWidth = jp2TileDim.width;
        final int jp2TileHeight = jp2TileDim.height;
        final int jp2TileX = destRect.x / jp2TileWidth;
        final int jp2TileY = destRect.y / jp2TileHeight;

        File outputFile = null;
        try {
            outputFile = new File(cacheDir, FileUtils.exchangeExtension(imageFile.getName(),
                    String.format("_R%d_TX%d_TY%d.pgx", getLevel(), jp2TileX, jp2TileY)));
        } catch (Exception e) {
            logger.severe(Utils.getStackTrace(e));
        }

        final File outputFile0 = getFirstComponentOutputFile(outputFile);
        // todo - outputFile0 may have already been created, although 'opj_decompress' has not finished execution.
        //        This may be the reason for party filled tiles, that sometimes occur
        if (!outputFile0.exists()) {
            logger.finest(String.format("Jp2ExeImage.readTileData(): recomputing res=%d, tile=(%d,%d)\n",
                    getLevel(), jp2TileX, jp2TileY));
            try {
                decompressTile(outputFile, jp2TileX, jp2TileY);
            } catch (IOException e) {
                logger.severe("opj_decompress process failed! :" + Utils.getStackTrace(e));
                if (outputFile0.exists() && !outputFile0.delete()) {
                    logger.severe("Failed to delete file: " + outputFile0.getAbsolutePath());
                }
            }
            if (!outputFile0.exists()) {
                Arrays.fill(tileData, S2Config.FILL_CODE_NO_FILE);
                return;
            }
        }

        try {
            logger.finest(String.format("Jp2ExeImage.readTileData(): reading res=%d, tile=(%d,%d)\n", getLevel(),
                    jp2TileX, jp2TileY));
            readTileData(outputFile0, tileX, tileY, tileWidth, tileHeight, jp2TileX, jp2TileY, jp2TileWidth,
                    jp2TileHeight, tileData, destRect);
        } catch (IOException e) {
            logger.severe("Failed to read uncompressed file data: " + Utils.getStackTrace(e));
        }
    }

    private File getFirstComponentOutputFile(File outputFile) {
        return FileUtils.exchangeExtension(outputFile, "_0.pgx");
    }

    protected static Dimension getTileDimAtResolutionLevel(int fullTileWidth, int fullTileHeight, int level) {
        int width = getSizeAtResolutionLevel(fullTileWidth, level);
        int height = getSizeAtResolutionLevel(fullTileHeight, level);
        return getTileDim(width, height);
    }

    protected static Dimension getDimAtResolutionLevel(int fullWidth, int fullHeight, int level) {
        int width = getSizeAtResolutionLevel(fullWidth, level);
        int height = getSizeAtResolutionLevel(fullHeight, level);
        return new Dimension(width, height);
    }

    /**
     * Computes a new size at a given resolution level in the style of JPEG2000.
     *
     * @param fullSize the full size
     * @param level    the resolution level
     * @return the reduced size at the given level
     */
    protected static int getSizeAtResolutionLevel(int fullSize, int level) {
        int size = fullSize >> level;
        int sizeTest = size << level;
        if (sizeTest < fullSize) {
            size++;
        }

        return size;
    }

    static Dimension getTileDim(int width, int height) {
        return new Dimension(width < S2Config.DEFAULT_JAI_TILE_SIZE ? width : S2Config.DEFAULT_JAI_TILE_SIZE,
                height < S2Config.DEFAULT_JAI_TILE_SIZE ? height : S2Config.DEFAULT_JAI_TILE_SIZE);
    }

    protected void decompressTile(final File outputFile, int jp2TileX, int jp2TileY) throws IOException {
        final int tileIndex = tileLayout.numXTiles * jp2TileY + jp2TileX;

        ProcessBuilder builder;

        if (S2Config.OPJ_DECOMPRESSOR_EXE != null) {
            if (org.apache.commons.lang.SystemUtils.IS_OS_WINDOWS) {
                String inputFileName = Utils.GetIterativeShortPathName(imageFile.getPath());
                String outputFileName = outputFile.getPath();

                if (inputFileName.length() == 0) {
                    inputFileName = imageFile.getPath();
                }

                Guardian.assertTrue("Image file exists", new File(inputFileName).exists());

                builder = new ProcessBuilder(S2Config.OPJ_DECOMPRESSOR_EXE, "-i", inputFileName, "-o",
                        outputFileName, "-r", getLevel() + "", "-t", tileIndex + "");
            } else {
                SystemUtils.LOG.fine("Writing to " + outputFile.getPath());

                Guardian.assertTrue("Image file exists", imageFile.exists());

                builder = new ProcessBuilder(S2Config.OPJ_DECOMPRESSOR_EXE, "-i", imageFile.getPath(), "-o",
                        outputFile.getPath(), "-r", getLevel() + "", "-t", tileIndex + "");
            }
        } else {
            throw new UnexpectedException("OpenJpeg decompressor is not set");
        }

        builder = builder.directory(cacheDir);

        try {
            builder.redirectErrorStream(true);
            CommandOutput result = OpenJpegUtils.runProcess(builder);

            final int exitCode = result.getErrorCode();
            if (exitCode != 0) {
                SystemUtils.LOG.severe(String.format(
                        "Failed to uncompress tile: %s, exitCode = %d, command = [%s], command stdoutput = [%s], command stderr = [%s]",
                        imageFile.getPath(), exitCode, builder.command().toString(), result.getTextOutput(),
                        result.getErrorOutput()));
            }
        } catch (InterruptedException e) {
            SystemUtils.LOG.severe("Process was interrupted, InterruptedException: " + e.getMessage());
        }
    }

    @Override
    public synchronized void dispose() {

        if (!cacheDir.delete()) {
            SystemUtils.LOG.severe("Failed to delete cache dir! :" + cacheDir.getAbsolutePath());
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        dispose();
    }

    protected void readTileData(File outputFile, int tileX, int tileY, int tileWidth, int tileHeight, int jp2TileX,
            int jp2TileY, int jp2TileWidth, int jp2TileHeight, short[] tileData, Rectangle destRect)
            throws IOException {

        // todo - we still have a synchronisation problem here: often zero areas are generated in a tile.
        // This does not happen, if we synchronise entire computeRect() on the instance, but it is less efficient.
        try (FileImageInputStream fis = new FileImageInputStream(outputFile)) {
            int jp2Width, jp2Height;
            final String[] tokens = fis.readLine().split(" ");
            long dataPos = fis.getStreamPosition();
            if (tokens.length != 6) {
                throw new IOException("Unexpected PGX tile image format");
            }

            // String pg = tokens[0];   // PG
            // String ml = tokens[1];   // ML
            // String plus = tokens[2]; // +
            try {
                // int jp2File.nbits = Integer.parseInt(tokens[3]);
                jp2Width = Integer.parseInt(tokens[4]);
                jp2Height = Integer.parseInt(tokens[5]);
            } catch (NumberFormatException e) {
                throw new IOException("Unexpected PGX tile image format");
            }
            if (jp2Width > jp2TileWidth || jp2Height > jp2TileHeight) {
                throw new IllegalStateException(
                        String.format("width (=%d) > tileWidth (=%d) || height (=%d) > tileHeight (=%d)", jp2Width,
                                jp2TileWidth, jp2Height, jp2TileHeight));
            }

            int jp2X = destRect.x - jp2TileX * jp2TileWidth;
            int jp2Y = destRect.y - jp2TileY * jp2TileHeight;
            if (jp2X < 0 || jp2Y < 0) {
                throw new IllegalStateException(String.format("jp2X (=%d) < 0 || jp2Y (=%d) < 0", jp2X, jp2Y));
            }

            if (jp2X == 0 && jp2Width == tileWidth && jp2Y == 0 && jp2Height == tileHeight
                    && tileWidth * tileHeight == tileData.length) {
                fis.seek(dataPos);
                fis.readFully(tileData, 0, tileData.length);
            } else {
                final Rectangle jp2FileRect = new Rectangle(0, 0, jp2Width, jp2Height);
                final Rectangle tileRect = new Rectangle(jp2X, jp2Y, tileWidth, tileHeight);
                final Rectangle intersection = jp2FileRect.intersection(tileRect);
                if (!intersection.isEmpty()) {
                    SystemUtils.LOG
                            .fine(String.format("%s: tile=(%d,%d): jp2FileRect=%s, tileRect=%s, intersection=%s\n",
                                    outputFile, tileX, tileY, jp2FileRect, tileRect, intersection));
                    long seekPos = dataPos
                            + S2Config.SAMPLE_BYTE_COUNT * (intersection.y * jp2Width + intersection.x);
                    int tilePos = 0;
                    for (int y = 0; y < intersection.height; y++) {
                        fis.seek(seekPos);
                        fis.readFully(tileData, tilePos, intersection.width);
                        seekPos += S2Config.SAMPLE_BYTE_COUNT * jp2Width;
                        tilePos += tileWidth;
                        for (int x = intersection.width; x < tileWidth; x++) {
                            tileData[y * tileWidth + x] = S2Config.FILL_CODE_OUT_OF_X_BOUNDS;
                        }
                    }

                    for (int y = intersection.height; y < tileHeight; y++) {
                        for (int x = 0; x < tileWidth; x++) {
                            tileData[y * tileWidth + x] = S2Config.FILL_CODE_OUT_OF_Y_BOUNDS;
                        }
                    }
                } else {
                    Arrays.fill(tileData, S2Config.FILL_CODE_NO_INTERSECTION);
                }
            }
        }
    }

}