All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.rgi.g2t.RawImageTileReader Maven / Gradle / Ivy

The newest version!
/* The MIT License (MIT)
 *
 * Copyright (c) 2015 Reinventing Geospatial, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.rgi.g2t;

import com.rgi.common.BoundingBox;
import com.rgi.common.Dimensions;
import com.rgi.common.Range;
import com.rgi.common.coordinate.Coordinate;
import com.rgi.common.coordinate.CoordinateReferenceSystem;
import com.rgi.common.coordinate.CrsCoordinate;
import com.rgi.common.coordinate.referencesystem.profile.CrsProfile;
import com.rgi.common.coordinate.referencesystem.profile.CrsProfileFactory;
import com.rgi.common.tile.TileOrigin;
import com.rgi.common.tile.scheme.TileMatrixDimensions;
import com.rgi.common.tile.scheme.TileScheme;
import com.rgi.common.tile.scheme.ZoomTimesTwo;
import com.rgi.common.util.FileUtility;
import com.rgi.store.tiles.TileHandle;
import com.rgi.store.tiles.TileStoreException;
import com.rgi.store.tiles.TileStoreReader;
import org.gdal.gdal.Dataset;
import utility.GdalUtility;

import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import javax.naming.OperationNotSupportedException;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.zip.DataFormatException;

/**
 * TileStoreReader implementation for GDAL-readable image files
 *
 * @author Steven D. Lander
 * @author Luke D. Lambert
 *
 */
public class RawImageTileReader implements TileStoreReader
{
    /**
     * Constructor
     *
     * @param rawImage
     *             A raster image
     * @param tileSize
     *             A {@link Dimensions} that describes what an individual tile
     *             looks like
     * @param noDataColor
     *             The {@link Color} of the NODATA fields within the raster image
     * @throws TileStoreException
     *             Thrown when GDAL could not get the correct
     *             {@link CoordinateReferenceSystem} of the input raster OR if
     *             the raw image could not be loaded as a {@link Dataset}
     */
    public RawImageTileReader(final File                rawImage,
                              final Dimensions tileSize,
                              final Color               noDataColor) throws TileStoreException
    {
        this(rawImage,
             tileSize,
             noDataColor,
             null);
    }

    /**
     * Constructor
     *
     * @param rawImage
     *             A raster image {@link File}
     * @param tileSize
     *             A {@link Dimensions} that describes what an individual tile
     *             looks like
     * @param noDataColor
     *             The {@link Color} of the NODATA fields within the raster image
     * @param coordinateReferenceSystem
     *             The {@link CoordinateReferenceSystem} the tiles should be
     *             output in
     * @throws TileStoreException
     *             Thrown when GDAL could not get the correct
     *             {@link CoordinateReferenceSystem} of the input raster OR if
     *             the raw image could not be loaded as a {@link Dataset}
     */
    public RawImageTileReader(final File                      rawImage,
                              final Dimensions       tileSize,
                              final Color                     noDataColor,
                              final CoordinateReferenceSystem coordinateReferenceSystem) throws TileStoreException
    {
        this(rawImage,
             GdalUtility.open(rawImage, coordinateReferenceSystem),
             tileSize,
             noDataColor,
             coordinateReferenceSystem);
    }


    public RawImageTileReader(final File                      rawImage,
                              final Dataset                   dataset,
                              final Dimensions       tileSize,
                              final Color                     noDataColor,
                              final CoordinateReferenceSystem coordinateReferenceSystem) throws TileStoreException
    {
        if(rawImage == null || !rawImage.canRead())
        {
            throw new IllegalArgumentException("Raw image may not be null, and must represent a valid file on disk.");
        }

        if(tileSize == null)
        {
            throw new IllegalArgumentException("Tile size may not be null.");
        }

        this.rawImage = rawImage;
        this.tileSize = tileSize;

        // TODO check noDataColor for null when the feature is implemented
        // this.noDataColor = noDataColor;

        this.dataset = dataset;

        try
        {
            if(this.dataset.GetRasterCount() == 0)
            {
                throw new IllegalArgumentException("Input file has no raster bands");
            }

            if(this.dataset.GetRasterBand(1).GetColorTable() != null)
            {
                System.out.println("expand this raster to RGB/RGBA"); // TODO: make a temporary vrt with gdal_translate to expand this to RGB/RGBA
            }

            // We cannot tile an image with no geo referencing information
            if(!GdalUtility.hasGeoReference(this.dataset))
            {
                throw new IllegalArgumentException("Input raster image has no georeference.");
            }

            this.coordinateReferenceSystem = GdalUtility.getCoordinateReferenceSystem(GdalUtility.getSpatialReference(this.dataset));

            if(this.coordinateReferenceSystem == null)
            {
                throw new IllegalArgumentException("Image file is not in a recognized coordinate reference system");
            }

            this.profile = CrsProfileFactory.create(this.coordinateReferenceSystem);

            this.tileScheme = new ZoomTimesTwo(0, MAX_ZOOM_LEVEL, 1, 1);    // Use absolute tile numbering

            final BoundingBox datasetBounds = GdalUtility.getBounds(this.dataset);

            this.tileRanges = GdalUtility.calculateTileRanges(this.tileScheme,
                                                              datasetBounds,
                                                              this.profile.getBounds(),
                                                              this.profile,
                                                              RawImageTileReader.Origin);

            //final int minimumZoom = GdalUtility.getMinimalZoom(this.dataset, this.tileRanges, Origin, this.tileScheme, tileSize);
            final int minimumZoom = GdalUtility.getMinimalZoom(this.tileRanges);
            final int maximumZoom = GdalUtility.getMaximalZoom(this.dataset, this.tileRanges, Origin, this.tileScheme, tileSize);

            // The bounds of the dataset is **almost never** the bounds of the
            // data.  The bounds of the dataset fit inside the bounds of the
            // data because the bounds of the data must align to the tile grid.
            // The minimum zoom level is selected such that the entire dataset
            // fits inside a single tile.  that single tile is the minimum data
            // bounds.

            final Coordinate minimumTile = this.tileRanges.get(minimumZoom).getMinimum();  // The minimum and maximum for the range returned from tileRanges.get() should be identical (it's a single tile)

            this.dataBounds = this.getTileBoundingBox(minimumTile.getX(),
                                                      minimumTile.getY(),
                                                      this.tileScheme.dimensions(minimumZoom));

            this.zoomLevels = IntStream.rangeClosed(minimumZoom, maximumZoom)
                                       .boxed()
                                       .collect(Collectors.toSet());

            this.tileCount = IntStream.rangeClosed(minimumZoom, maximumZoom)
                                      .map(zoomLevel -> { final Range> range = this.tileRanges.get(zoomLevel);

                                                          return (range.getMaximum().getX() - range.getMinimum().getX() + 1) *
                                                                 (range.getMinimum().getY() - range.getMaximum().getY() + 1);
                                                    })
                                      .sum();
        }
        catch(final DataFormatException dfe)
        {
            this.close();
            throw new TileStoreException(dfe);
        }

        this.cachedTiles = new HashMap<>();
    }

    @SuppressWarnings("unchecked")
    @Override
    public final void close() throws TileStoreException
    {
        // Remove temporary reprojected file
        if(this.dataset != null)
        {
            final Iterable files = this.dataset.GetFileList();
            this.dataset.delete();
            for (final String file : files)
            {
                if(file.startsWith((System.getProperty("java.io.tmpdir"))))
                {
                    try
                    {
                        Files.delete(Paths.get(file));
                    }
                    catch (final IOException e)
                    {
                        throw new TileStoreException(e);
                    }
                }
            }
        }
        // Remove final temporary cached tile(s)
        try
        {
            this.cachedTiles.values().stream().forEach(tile -> {
                                                                   try
                                                                   {
                                                                       Files.delete(tile);
                                                                   }
                                                                   catch (final IOException e)
                                                                   {
                                                                       // Break the stream if a tile cannot
                                                                       // be deleted
                                                                       //noinspection ThrowCaughtLocally
                                                                       throw new RuntimeException(e);
                                                                   }
                                                               });
        }
        catch(final RuntimeException e)
        {
            // Catch the exception when a tile cannot be deleted and throw a new TileStoreException
            throw new TileStoreException(e);
        }
    }

    @Override
    public BoundingBox getBounds()
    {
        return this.dataBounds;
    }

    @Override
    public long countTiles()
    {
        return this.tileCount;
    }

    @Override
    public long getByteSize()
    {
        return this.rawImage.length();
    }

    @Override
    public BufferedImage getTile(final int column, final int row, final int zoomLevel) throws TileStoreException
    {
        throw new TileStoreException(new OperationNotSupportedException(NOT_SUPPORTED_MESSAGE));
    }

    @Override
    public BufferedImage getTile(final CrsCoordinate coordinate, final int zoomLevel) throws TileStoreException
    {
        throw new TileStoreException(new OperationNotSupportedException(NOT_SUPPORTED_MESSAGE));
    }

    @Override
    public Set getZoomLevels()
    {
        // removes the possibility of the collection being modified during return
        return Collections.unmodifiableSet(this.zoomLevels);
    }

    @Override
    public Stream stream() throws TileStoreException
    {
        final List tileHandles = new ArrayList<>();

        final Range zoomRange = new Range<>(this.zoomLevels, Integer::compare);

        // Should always start with the lowest-integer-zoom-level that has only one tile
        final Range> zoomInfo = this.tileRanges.get(zoomRange.getMinimum());

        // Get the coordinate information
        final Coordinate topLeftCoordinate     = zoomInfo.getMinimum();
        final Coordinate bottomRightCoordinate = zoomInfo.getMaximum();

        // Parse each coordinate into min/max tiles for X/Y
        final int zoomMinXTile =     topLeftCoordinate.getX();
        final int zoomMaxXTile = bottomRightCoordinate.getX();
        final int zoomMinYTile = bottomRightCoordinate.getY();
        final int zoomMaxYTile =     topLeftCoordinate.getY();

        //Make tiles for each tile at the min zoom level
        for(int x = zoomMinXTile; x <= zoomMaxXTile; x++)
        {
            for (int y = zoomMinYTile; y <= zoomMaxYTile; y++)
            {
                this.makeTiles(tileHandles, new RawImageTileReader.RawImageTileHandle(zoomRange.getMinimum(), x, y), zoomRange.getMaximum());
            }
        }

        // Sort the tile handles so that they decrement.  This ensures all base level tiles
        // are generated before the overview tiles
        tileHandles.sort((o1, o2) -> Integer.compare(o2.getZoomLevel(), o1.getZoomLevel()));
        return tileHandles.stream();
    }

    private boolean tileIntersectsData(final TileHandle tile)
    {
        final int zoom   = tile.getZoomLevel();
        final int column = tile.getColumn();
        final int row    = tile.getRow();

        final Range> zoomRange = this.tileRanges.get(zoom);

        return column >= zoomRange.getMinimum().getX() &&
               column <= zoomRange.getMaximum().getX() &&
               row    >= zoomRange.getMaximum().getY() &&
               row    <= zoomRange.getMinimum().getY();
    }

    private void makeTiles(final List tileHandles, final TileHandle tile, final int maxZoom) throws TileStoreException
    {
        if(!this.tileIntersectsData(tile))
        {
            // Do nothing if the tile does not intersect with the data bounding box
            return;
        }
        if(tile.getZoomLevel() == maxZoom)
        {
            // tell the RawImageTileHandle this is a special case: a gdalImage
            tileHandles.add(new RawImageTileReader.RawImageTileHandle(tile.getZoomLevel(), tile.getColumn(), tile.getRow(), true));
        }
        else
        {
            // calculate all the tiles below this current one
            final int zoomBelow       = tile.getZoomLevel() + 1;
            // Shift values instead of multiplying by 2 for possible performance improvement
            final int zoomColumnBelow = tile.getColumn() << 1;
            final int zoomRowBelow    = tile.getRow() << 1;

            // create handles for below tiles
            final TileHandle belowOriginTile = new RawImageTileReader.RawImageTileHandle(zoomBelow,
                                                                         zoomColumnBelow,
                                                                         zoomRowBelow);
            final TileHandle belowColumnShiftedTile = new RawImageTileReader.RawImageTileHandle(zoomBelow,
                                                                         zoomColumnBelow + 1,
                                                                         zoomRowBelow);
            final TileHandle belowRowShiftedTile = new RawImageTileReader.RawImageTileHandle(zoomBelow,
                                                                         zoomColumnBelow,
                                                                         zoomRowBelow + 1);
            final TileHandle belowBothShiftedTile = new RawImageTileReader.RawImageTileHandle(zoomBelow,
                                                                         zoomColumnBelow + 1,
                                                                         zoomRowBelow + 1);

            // recurse
            this.makeTiles(tileHandles, belowOriginTile, maxZoom);
            this.makeTiles(tileHandles, belowColumnShiftedTile, maxZoom);
            this.makeTiles(tileHandles, belowRowShiftedTile, maxZoom);
            this.makeTiles(tileHandles, belowBothShiftedTile, maxZoom);

            // finally, add this current tile
            tileHandles.add(tile);
        }
    }

    @Override
    public Stream stream(final int zoomLevel) throws TileStoreException
    {
        final Range> zoomInfo = this.tileRanges.get(zoomLevel);

        // Get the coordinate information
        final Coordinate topLeftCoordinate     = zoomInfo.getMinimum();
        final Coordinate bottomRightCoordinate = zoomInfo.getMaximum();

        // Parse each coordinate into min/max tiles for X/Y
        final int zoomMinXTile =     topLeftCoordinate.getX();
        final int zoomMaxXTile = bottomRightCoordinate.getX();
        final int zoomMinYTile = bottomRightCoordinate.getY();
        final int zoomMaxYTile =     topLeftCoordinate.getY();

        // Create a tile handle list that we can append to
        final Collection tileHandles = new ArrayList<>();

        for(int tileY = zoomMaxYTile; tileY >= zoomMinYTile; --tileY)
        {
            for(int tileX = zoomMinXTile; tileX <= zoomMaxXTile; ++tileX)
            {
                tileHandles.add(new RawImageTileReader.RawImageTileHandle(zoomLevel, tileX, tileY));
            }
        }
        // Return the entire tile handle list as a stream
        return tileHandles.stream();
    }

    @Override
    public CoordinateReferenceSystem getCoordinateReferenceSystem()
    {
        return this.coordinateReferenceSystem;
    }

    @Override
    public String getName()
    {
        return FileUtility.nameWithoutExtension(this.rawImage);
    }

    @Override
    public String getImageType() throws TileStoreException
    {
        try
        {
            final MimeType mimeType = new MimeType(Files.probeContentType(this.rawImage.toPath()));

            if(mimeType.getPrimaryType().toLowerCase().equals("image"))
            {
               return mimeType.getSubType();
            }

            return null;
        }
        catch(final MimeTypeParseException | IOException ex)
        {
            throw new TileStoreException(ex);
        }
    }

    @Override
    public Dimensions getImageDimensions() throws TileStoreException
    {
        return this.tileSize;
    }

    @Override
    public TileScheme getTileScheme() throws TileStoreException
    {
        return this.tileScheme;
    }

    @Override
    public TileOrigin getTileOrigin()
    {
        return Origin;
    }

    @SuppressWarnings("NonStaticInnerClassInSecureContext")
    private class RawImageTileHandle implements TileHandle
    {
        private final TileMatrixDimensions matrix;

        private boolean gotImage;
        private boolean gdalImage;
        private Path cachedImageLocation;
        private BufferedImage image;

        private final int zoomLevel;
        private final int column;
        private final int row;

        RawImageTileHandle(final int zoom,
                           final int column,
                           final int row) throws TileStoreException
        {
            this.zoomLevel = zoom;
            this.column    = column;
            this.row       = row;
            this.matrix    = RawImageTileReader.this.getTileScheme().dimensions(this.zoomLevel);
        }

        RawImageTileHandle(final int zoom,
                           final int column,
                           final int row,
                           final Path cachedImageLocation) throws TileStoreException
        {
            this.zoomLevel           = zoom;
            this.column              = column;
            this.row                 = row;
            this.matrix              = RawImageTileReader.this.getTileScheme().dimensions(this.zoomLevel);
            this.cachedImageLocation = cachedImageLocation;
        }

        RawImageTileHandle(final int zoom,
                           final int column,
                           final int row,
                           final boolean gdalImage) throws TileStoreException
        {
            this.zoomLevel = zoom;
            this.column    = column;
            this.row       = row;
            this.matrix    = RawImageTileReader.this.getTileScheme().dimensions(this.zoomLevel);
            this.gdalImage = gdalImage;
        }

        @Override
        public int getZoomLevel()
        {
            return this.zoomLevel;
        }

        @Override
        public int getColumn()
        {
            return this.column;
        }

        @Override
        public int getRow()
        {
            return this.row;
        }

        public Path getCachedImagePath()
        {
            return this.cachedImageLocation;
        }

        @Override
        public TileMatrixDimensions getMatrix() throws TileStoreException
        {
            return this.matrix;
        }

        @Override
        public CrsCoordinate getCrsCoordinate() throws TileStoreException
        {
            return RawImageTileReader.this.tileToCrsCoordinate(this.column,
                                                               this.row,
                                                               this.matrix,
                                                               RawImageTileReader.this.getTileOrigin());
        }

        @Override
        public CrsCoordinate getCrsCoordinate(final TileOrigin corner) throws TileStoreException
        {
            return RawImageTileReader.this.tileToCrsCoordinate(this.column,
                                                               this.row,
                                                               this.matrix,
                                                               corner);
        }

        @Override
        public BoundingBox getBounds() throws TileStoreException
        {
             return RawImageTileReader.this.getTileBoundingBox(this.column,
                                                               this.row,
                                                               this.matrix);
        }

        @Override
        public BufferedImage getImage() throws TileStoreException
        {
            if(this.gotImage)
            {
                return this.image;
            }

            if(this.gdalImage)
            {
                // Build the parameters for GDAL read raster call
                final GdalUtility.GdalRasterParameters params = GdalUtility.getGdalRasterParameters(RawImageTileReader.this.dataset.GetGeoTransform(),
                                                                                                    this.getBounds(),
                                                                                                    RawImageTileReader.this.tileSize,
                                                                                                    RawImageTileReader.this.dataset);
                try
                {
                    // Read image data directly from the raster
                    final byte[] imageData = GdalUtility.readRaster(params,
                                                                    RawImageTileReader.this.dataset);

                    // TODO: logic goes here in the case that the querysize == tile size (gdalconstConstants.GRA_NearestNeighbour) (write directly)
                    final Dataset querySizeImageCanvas = GdalUtility.writeRaster(params,
                                                                                 imageData,
                                                                                 RawImageTileReader.this.dataset.GetRasterCount());

                    try
                    {
                        // Scale each band of tileDataInMemory down to the tile size (down from the query size)
                        final Dataset tileDataInMemory = GdalUtility.scaleQueryToTileSize(querySizeImageCanvas,
                                                                                          RawImageTileReader.this.tileSize);

                        this.image = GdalUtility.convert(tileDataInMemory);

                        // Write this image to disk for later overview generation
                        final Path baseTilePath = this.writeTempTile(this.image);

                        // Add the image path to the reader map
                        RawImageTileReader.this.cachedTiles.put(this.tileKey(this.zoomLevel,
                                                                             this.column,
                                                                             this.row),
                                                                baseTilePath);

                        // Clean up dataset
                        tileDataInMemory.delete();
                        this.gotImage = true;

                        return this.image;
                    }
                    catch(final TilingException | IOException ex)
                    {
                        throw new TileStoreException(ex);
                    }
                    finally
                    {
                        querySizeImageCanvas.delete();
                    }
                }
                catch(final TilingException ex)
                {
                    throw new TileStoreException(ex);
                }
                catch(final IOException ignored)
                {
                    // An IOException is thrown by GdalUtility.readRaster() when the tile boundary
                    // requested lies outside of the databounding box.  This can sometimes occur
                    // when using the lowest-integer-zoom tile boundary as the tile-able area. This
                    // should not happen as the RawImageTileReader.makeTiles() method automatically
                    // checks if a generated tile does NOT intersect with the reported input raster
                    // bounding box.
                    // In this case, if readRaster does indeed throw IOException, just return a
                    // transparent tile.
                    this.gotImage = true;
                    return this.createTransparentImage();
                }
            }

            // Make this tile by getting the tiles below it and scaling them to fit
            return this.generateScaledTileFromChildren();
        }

        private Path writeTempTile(final RenderedImage tileImage) throws IOException
        {
            final Path baseTilePath = File.createTempFile("baseTile" + this.zoomLevel
                                                                     + this.column
                                                                     + this.row,
                                                          ".png")
                                          .toPath();
            try(final ImageOutputStream fileOutputStream = ImageIO.createImageOutputStream(baseTilePath.toFile()))
            {
                ImageIO.write(tileImage, "png", baseTilePath.toFile());
            }
            return baseTilePath;
        }

        private BufferedImage createTransparentImage()
        {
            final int tileWidth = RawImageTileReader.this.tileSize.getWidth();
            final int tileHeight = RawImageTileReader.this.tileSize.getHeight();

            final BufferedImage transparentImage = new BufferedImage(tileWidth,
                                                                     tileHeight,
                                                                     BufferedImage.TYPE_INT_ARGB);
            final Graphics2D graphics = transparentImage.createGraphics();
            graphics.setComposite(AlphaComposite.Clear);
            graphics.fillRect(0, 0, tileWidth, tileHeight);
            graphics.dispose();

            // Return the transparent tile
            return transparentImage;
        }

        private BufferedImage generateScaledTileFromChildren() throws TileStoreException
        {
            final int tileWidth  = RawImageTileReader.this.tileSize.getWidth();
            final int tileHeight = RawImageTileReader.this.tileSize.getHeight();

            // Create the full-sized buffered image from the child tiles
            final BufferedImage fullCanvas = new BufferedImage(tileWidth  * 2,
                                                               tileHeight * 2,
                                                               BufferedImage.TYPE_INT_ARGB);

            // Create the full-sized graphics object
            final Graphics2D fullCanvasGraphics = fullCanvas.createGraphics();

            // Get child handles
            final List children = this.generateTileChildren();

            // Get the cached children of this tile
            final List transformedChildren = children.stream().map(tileHandle ->
            {
                final Coordinate resultCoordinate = RawImageTileReader.Origin.transform(TileOrigin.UpperLeft,
                                                                                                 tileHandle.column,
                                                                                                 tileHandle.row,
                                                                                                 this.matrix);
                try
                {
                    return new RawImageTileHandle(tileHandle.zoomLevel,
                                                  resultCoordinate.getX(),
                                                  resultCoordinate.getY(),
                                                  tileHandle.cachedImageLocation);
                } catch(TileStoreException e)
                {
                    throw new RuntimeException(e);
                }
            })
            .sorted((final TileHandle o1, final TileHandle o2) -> {
                final int columnCompare = Integer.compare(o1.getColumn(), o2.getColumn());
                   final int rowCompare    = Integer.compare(o1.getRow(),    o2.getRow());

                   // column values are the same
                   if(columnCompare == 0)
                   {
                       return rowCompare;
                   }
                   if(rowCompare == 0)
                   {
                       return columnCompare;
                   }

               // TODO: Duplicate tile case?
               return 0;
            })
            .collect(Collectors.toList());

            if(transformedChildren.size() == 4)
            {
                try
                {
                    // Origin tile
                    if(transformedChildren.get(2).cachedImageLocation != null)
                    {
                        final BufferedImage originImage = ImageIO.read(transformedChildren.get(2).cachedImageLocation.toFile());
                        fullCanvasGraphics.drawImage(originImage, null, 0, 0);
                    }

                    // Tile that is Y+1 in relation to the origin
                    if(transformedChildren.get(0).cachedImageLocation != null)
                    {
                        final BufferedImage rowShiftedImage = ImageIO.read(transformedChildren.get(0).cachedImageLocation.toFile());
                        fullCanvasGraphics.drawImage(rowShiftedImage, null, 0, tileHeight);
                    }

                    // Tile that is X+1 in relation to the origin
                    if(transformedChildren.get(3).cachedImageLocation != null)
                    {
                        final BufferedImage columnShiftedImage = ImageIO.read(transformedChildren.get(3).cachedImageLocation.toFile());
                        fullCanvasGraphics.drawImage(columnShiftedImage, null, tileWidth, 0);
                    }

                    // Tile that is both X+1 and Y+1 in relation to the origin
                    if(transformedChildren.get(1).cachedImageLocation != null)
                    {
                        final BufferedImage bothShiftedImage = ImageIO.read(transformedChildren.get(1).cachedImageLocation.toFile());
                        fullCanvasGraphics.drawImage(bothShiftedImage, null, tileWidth, tileHeight);
                    }
                }
                catch(final IOException ex)
                {
                    throw new TileStoreException(ex);
                }
            }
            else
            {
                throw new RuntimeException("Cannot create tile from child tiles.");
            }

            // Write cached tile
            try
            {
                final BufferedImage scaledTile = this.scaleToTileCanvas(fullCanvas);

                RawImageTileReader.this.cachedTiles.put(this.tileKey(this.zoomLevel, this.column, this.row), this.writeTempTile(scaledTile));
                return scaledTile;
            }
            catch(final IOException ex)
            {
                throw new TileStoreException(ex);
            }
            finally
            {
                // Clean-up step
                fullCanvasGraphics.dispose();
                children.stream().filter(Objects::nonNull).forEach(child ->
                {
                    if(child.cachedImageLocation != null)
                    {
                        child.cachedImageLocation.toFile().delete();
                    }
                    RawImageTileReader.this.cachedTiles.remove(this.tileKey(child.zoomLevel, child.column, child.row));
                });
            }
        }

        private BufferedImage scaleToTileCanvas(final BufferedImage fullCanvas)
        {
            final BufferedImage tileCanvas = new BufferedImage(RawImageTileReader.this.tileSize.getWidth(),
                                                               RawImageTileReader.this.tileSize.getHeight(),
                                                               BufferedImage.TYPE_INT_ARGB);

            final AffineTransform affineTransform = new AffineTransform();
            affineTransform.scale(AFFINE_SCALE, AFFINE_SCALE);
            final BufferedImageOp scaleOp = new AffineTransformOp(affineTransform, AffineTransformOp.TYPE_BILINEAR);

            return scaleOp.filter(fullCanvas, tileCanvas);
        }

        private String tileKey(final int zoom, final int column, final int row)
        {
            return String.format("%d/%d/%d", zoom, column, row);
        }

        private List generateTileChildren() throws TileStoreException
        {
            final int childZoom   = this.zoomLevel + 1;
            final int childColumn = this.column * 2;
            final int childRow    = this.row    * 2;

            final Path origin        = RawImageTileReader.this.cachedTiles.get(this.tileKey(childZoom, childColumn,     childRow));
            final Path columnShifted = RawImageTileReader.this.cachedTiles.get(this.tileKey(childZoom, childColumn + 1, childRow));
            final Path rowShifted    = RawImageTileReader.this.cachedTiles.get(this.tileKey(childZoom, childColumn,     childRow + 1));
            final Path bothShifted   = RawImageTileReader.this.cachedTiles.get(this.tileKey(childZoom, childColumn + 1, childRow + 1));

            final List children = new ArrayList<>();

            children.add(new RawImageTileReader.RawImageTileHandle(childZoom, childColumn,     childRow,     origin));
            children.add(new RawImageTileReader.RawImageTileHandle(childZoom, childColumn + 1, childRow,     columnShifted));
            children.add(new RawImageTileReader.RawImageTileHandle(childZoom, childColumn,     childRow + 1, rowShifted));
            children.add(new RawImageTileReader.RawImageTileHandle(childZoom, childColumn + 1, childRow + 1, bothShifted));

            return children;
        }

        @Override
        public String toString()
        {
            return this.tileKey(this.zoomLevel, this.column, this.row);
        }
    }

    private CrsCoordinate tileToCrsCoordinate(final int                  column,
                                              final int                  row,
                                              final TileMatrixDimensions tileMatrixDimensions,
                                              final TileOrigin           corner)
    {
        if(corner == null)
        {
            throw new IllegalArgumentException("Corner may not be null");
        }

        return this.profile.tileToCrsCoordinate(column + corner.getHorizontal(),
                                                row    + corner.getVertical(),
                                                this.profile.getBounds(),    // RawImageTileReader uses absolute tiling, which covers the whole globe
                                                tileMatrixDimensions,
                                                RawImageTileReader.Origin);
    }

    private BoundingBox getTileBoundingBox(final int                  column,
                                           final int                  row,
                                           final TileMatrixDimensions tileMatrixDimensions)
    {
        return this.profile.getTileBounds(column,
                                          row,
                                          this.profile.getBounds(),
                                          tileMatrixDimensions,
                                          RawImageTileReader.Origin);
    }

    private final File                                     rawImage;
    private final CoordinateReferenceSystem                coordinateReferenceSystem;
    private final Dimensions                      tileSize;
    //private final Color                                    noDataColor; // TODO implement no-data color handling
    private final Dataset                                  dataset;
    private final BoundingBox                              dataBounds;
    private final Set                             zoomLevels;
    private final ZoomTimesTwo                             tileScheme;
    private final CrsProfile                               profile;
    private final int                                      tileCount;
    private final Map>> tileRanges;
    private final Map                        cachedTiles;

    private static final int                               MAX_ZOOM_LEVEL = 31;
    private static final double                            AFFINE_SCALE = 0.5;
    private static final String                            NOT_SUPPORTED_MESSAGE = "Call to unsupported method.";

    private static final TileOrigin Origin = TileOrigin.LowerLeft;
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy