Gigas002/GTiff2Tiles

View on GitHub
GTiff2Tiles.Core/Tiles/RasterTile.cs

Summary

Maintainability
A
0 mins
Test Coverage
using System.Threading.Channels;
using GTiff2Tiles.Core.Args;
using GTiff2Tiles.Core.Coordinates;
using GTiff2Tiles.Core.Enums;
using GTiff2Tiles.Core.GeoTiffs;
using GTiff2Tiles.Core.Images;
using NetVips;

// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable UnusedMember.Global

namespace GTiff2Tiles.Core.Tiles;

/// <summary>
/// <see cref="Raster"/> <see cref="Tile"/>
/// </summary>
public class RasterTile : Tile
{
    /// <summary>
    /// <see cref="BandsCount"/> backing field
    /// </summary>
    private int _bandsCount = DefaultBandsCount;

    #region Properties/Constants

    /// <summary>
    /// Default count of bands
    /// </summary>
    public const int DefaultBandsCount = 4;

    /// <summary>
    /// Count of bands in <see cref="RasterTile"/>
    /// </summary>
    public int BandsCount
    {
        get => _bandsCount;
        set
        {
            if (value <= 0 || value > 4) throw new ArgumentOutOfRangeException(nameof(value));

            _bandsCount = value;
        }
    }

    /// <summary>
    /// Interpolation of this <see cref="RasterTile"/>
    /// </summary>
    public NetVips.Enums.Kernel Interpolation { get; set; } = NetVips.Enums.Kernel.Lanczos3;

    #endregion

    #region Constructors

    /// <inheritdoc cref="Tile(Number,CoordinateSystem,Size,bool)"/>
    /// <param name="number"></param>
    /// <param name="coordinateSystem"></param>
    /// <param name="size"></param>
    /// <param name="tmsCompatible"></param>
    /// <exception cref="ArgumentOutOfRangeException"/>
    public RasterTile(Number number, CoordinateSystem coordinateSystem, Size size = null, bool tmsCompatible = false) :
        base(number, coordinateSystem, size, tmsCompatible) { }

    /// <inheritdoc cref="Tile(GeoCoordinate,GeoCoordinate,int,Size,bool)"/>
    /// <param name="minCoordinate"></param>
    /// <param name="maxCoordinate"></param>
    /// <param name="zoom"></param>
    /// <param name="size"></param>
    /// <param name="tmsCompatible"></param>
    /// <exception cref="ArgumentOutOfRangeException"/>
    public RasterTile(GeoCoordinate minCoordinate, GeoCoordinate maxCoordinate, int zoom, Size size = null,
                      bool tmsCompatible = false) : base(minCoordinate, maxCoordinate, zoom, size, tmsCompatible) { }

    #endregion

    #region Methods

    // TODO: TESTS!

    /// <summary>
    /// Create <see cref="Image"/> for one <see cref="RasterTile"/>
    /// from input <see cref="Image"/> or tile cache
    /// </summary>
    /// <param name="sourceGeoTiff">Source <see cref="IGeoTiff"/></param>
    /// <param name="tileCache">Source <see cref="Image"/>
    /// or tile cache</param>
    /// <param name="readArea">Source area</param>
    /// <param name="writeArea">Tile write area</param>
    /// <returns>Ready <see cref="Image"/> for <see cref="RasterTile"/></returns>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="ArgumentException"/>
    public Image CreateImage(IGeoTiff sourceGeoTiff, Image tileCache, Area readArea, Area writeArea)
    {
        // TODO: check geotiff?

        #region Preconditions checks

        if (tileCache == null) throw new ArgumentNullException(nameof(tileCache));
        if (readArea == null) throw new ArgumentNullException(nameof(readArea));
        if (writeArea == null) throw new ArgumentNullException(nameof(writeArea));

        #endregion


        // Scaling calculations
        double xScale = (double)writeArea.Size.Width / readArea.Size.Width;
        double yScale = (double)writeArea.Size.Height / readArea.Size.Height;

        // Crop and resize tile
        Image tempTileImage = tileCache.Crop((int)readArea.OriginCoordinate.X, (int)readArea.OriginCoordinate.Y,
                                             readArea.Size.Width, readArea.Size.Height)
                                       .Resize(xScale, Interpolation, yScale);

        // Add alpha channel if needed
        Band.AddDefaultBands(ref tempTileImage, BandsCount);

        // Make transparent image and insert tile
        return Image.Black(Size.Width, Size.Height).NewFromImage(new int[BandsCount])
                    .Insert(tempTileImage, (int)writeArea.OriginCoordinate.X, (int)writeArea.OriginCoordinate.Y);
    }

    /// <inheritdoc />
    public override void WriteToFile(IGeoTiff sourceGeoTiff, IWriteTilesArgs args)
    {
        // Get postitions and sizes for current tile
        (Area readArea, Area writeArea)? areas = Area.GetAreas(sourceGeoTiff, this);

        if (areas == null) return;

        (Area readArea, Area writeArea) = areas.Value;
        WriteRasterTilesArgs rasterArgs = (WriteRasterTilesArgs)args;

        using Image tileImage = CreateImage(sourceGeoTiff, rasterArgs.TileCache, readArea, writeArea);

        tileImage.WriteToFile(Path);
    }

    /// <inheritdoc />
    public override IEnumerable<byte> WriteToEnumerable(IGeoTiff sourceGeoTiff, IWriteTilesArgs args)
    {
        // Get postitions and sizes for current tile
        (Area readArea, Area writeArea)? areas = Area.GetAreas(sourceGeoTiff, this);

        if (areas == null) return null;

        (Area readArea, Area writeArea) = areas.Value;

        WriteRasterTilesArgs rasterArgs = args as WriteRasterTilesArgs;

        using Image tileImage = CreateImage(sourceGeoTiff, rasterArgs.TileCache, readArea, writeArea);

        return tileImage.WriteToBuffer(GetExtensionString());
    }

    /// <inheritdoc />
    public override bool WriteToChannel<T>(IGeoTiff sourceGeoTiff, ChannelWriter<T> tileWriter, IWriteTilesArgs args)
    {
        Bytes = WriteToEnumerable(sourceGeoTiff, args);

        return Bytes != null && Validate(false) && tileWriter.TryWrite(this as T);
    }

    /// <inheritdoc />
    public override IEnumerable<byte> WriteOverviewTileBytes<T>(T[] allBaseTiles)
    {
        Number[] numbers = Number.GetLowerNumbers();
        T[] lowerTiles = new T[4];
        lowerTiles[0] = allBaseTiles.FirstOrDefault(t => t.Number == numbers[0]);
        lowerTiles[1] = allBaseTiles.FirstOrDefault(t => t.Number == numbers[1]);
        lowerTiles[2] = allBaseTiles.FirstOrDefault(t => t.Number == numbers[2]);
        lowerTiles[3] = allBaseTiles.FirstOrDefault(t => t.Number == numbers[3]);
        bool isBuffered = lowerTiles[0].Bytes != null;

        Image image = WriteOverviewTileImage(lowerTiles, isBuffered);

        byte[] result = image?.WriteToBuffer(GetExtensionString(Extension));
        image?.Dispose();

        return result;
    }

    /// <summary>
    /// Create <see cref="Image"/> from 4 underlying lower tiles
    /// </summary>
    /// <typeparam name="T">Inheritors of <see cref="ITile"/></typeparam>
    /// <param name="fourBaseTiles">4 lower tiles</param>
    /// <param name="isBuffered">Do base tiles have bytes or they use paths?</param>
    /// <returns>Upper tile <see cref="Image"/></returns>
    public Image WriteOverviewTileImage<T>(T[] fourBaseTiles, bool isBuffered) where T : class, ITile
    {
        if (!isBuffered)
        {
            foreach (T tile in fourBaseTiles) tile.Bytes = File.ReadAllBytes(tile.Path);
        }

        Image[] images = new Image[4];

        bool empty = true;

        for (int i = 0; i < 4; i++)
        {
            Size size = new(fourBaseTiles[i].Size.Width / 2, fourBaseTiles[i].Size.Height / 2);
            byte[] bytes = fourBaseTiles[i].Bytes?.ToArray();

            if (bytes.Any())
            {
                empty = false;
                images[i] = Image.NewFromBuffer(bytes).ThumbnailImage(size.Width, size.Height);
            }
            else { images[i] = Image.Black(size.Width, size.Height, BandsCount); }
        }

        return empty ? null : Image.Arrayjoin(images, 2);
    }

    #endregion
}