Gigas002/GTiff2Tiles

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

Summary

Maintainability
A
0 mins
Test Coverage
using GTiff2Tiles.Core.Coordinates;
using GTiff2Tiles.Core.Enums;
using GTiff2Tiles.Core.Images;
using GTiff2Tiles.Core.Localization;

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

namespace GTiff2Tiles.Core.Tiles;

/// <summary>
/// <see cref="Number"/> of <see cref="ITile"/>
/// </summary>
public class Number : IEquatable<Number>
{
    #region Properties

    /// <summary>
    /// X <see cref="Number"/> value
    /// </summary>
    public int X { get; }

    /// <summary>
    /// Y <see cref="Number"/> value
    /// </summary>
    public int Y { get; }

    /// <summary>
    /// Zoom
    /// </summary>
    public int Z { get; }

    #endregion

    #region Constructors

    /// <summary>
    /// Creates new <see cref="Number"/>
    /// </summary>
    /// <param name="x"><see cref="X"/>
    /// <remarks><para/>Should be >= 0</remarks></param>
    /// <param name="y"><see cref="Y"/>
    /// <remarks><para/>Should be >= 0</remarks></param>
    /// <param name="z">Zoom
    /// <remarks><para/>Should be >= 0</remarks></param>
    /// <exception cref="ArgumentOutOfRangeException"/>
    public Number(int x, int y, int z)
    {
        #region Preconditions checks

        if (x < 0) throw new ArgumentOutOfRangeException(nameof(x));
        if (y < 0) throw new ArgumentOutOfRangeException(nameof(y));
        if (z < 0) throw new ArgumentOutOfRangeException(nameof(z));

        #endregion

        (X, Y, Z) = (x, y, z);
    }

    #endregion

    #region Methods

    #region Flip

    /// <summary>
    /// Flips value of passed <see cref="Y"/> on specified zoom
    /// </summary>
    /// <param name="y"><see cref="Y"/> to flip</param>
    /// <param name="z">Zoom</param>
    /// <returns>Converted <see cref="Y"/></returns>
    private static int FlipY(int y, int z) => Convert.ToInt32(Math.Pow(2.0, z) - y - 1.0);

    /// <summary>
    /// Flips <see cref="Number"/>
    /// </summary>
    /// <returns>Converted <see cref="Number"/></returns>
    public Number Flip() => Flip(this);

    /// <inheritdoc cref="Flip()"/>
    /// <param name="number"><see cref="Number"/> to flip</param>
    /// <exception cref="ArgumentNullException"/>
    public static Number Flip(Number number)
    {
        #region Preconditions checks

        if (number == null) throw new ArgumentNullException(nameof(number));

        #endregion

        return new Number(number.X, FlipY(number.Y, number.Z), number.Z);
    }

    #endregion

    #region ToGeoCoordinates

    /// <summary>
    /// Convert <see cref="Number"/> to <see cref="GeodeticCoordinate"/>s
    /// </summary>
    /// <param name="tileSize"><see cref="Tile"/>'s <see cref="Size"/></param>
    /// <param name="tmsCompatible">Is tms compatible?</param>
    /// <returns><see cref="ValueTuple{T1,T2}"/> of <see cref="GeodeticCoordinate"/>s</returns>
    /// <exception cref="ArgumentNullException"/>
    public (GeodeticCoordinate minCoordinate, GeodeticCoordinate maxCoordinate) ToGeodeticCoordinates(
        Size tileSize, bool tmsCompatible) => ToGeodeticCoordinates(this, tileSize, tmsCompatible);

    /// <inheritdoc cref="ToGeodeticCoordinates(Size, bool)"/>
    /// <param name="number"><see cref="Number"/> to convert</param>
    /// <param name="tileSize"></param>
    /// <param name="tmsCompatible"></param>
    public static (GeodeticCoordinate minCoordinate, GeodeticCoordinate maxCoordinate) ToGeodeticCoordinates(
        Number number, Size tileSize, bool tmsCompatible)
    {
        #region Preconditions checks

        if (number == null) throw new ArgumentNullException(nameof(number));
        if (tileSize == null) throw new ArgumentNullException(nameof(tileSize));

        #endregion

        if (!tmsCompatible) number = Flip(number);

        double resolution = GeodeticCoordinate.Resolution(number.Z, tileSize);

        GeodeticCoordinate minCoordinate = new(number.X * tileSize.Width * resolution - 180.0,
                                               number.Y * tileSize.Height * resolution - 90.0);
        GeodeticCoordinate maxCoordinate = new((number.X + 1) * tileSize.Width * resolution - 180.0,
                                               (number.Y + 1) * tileSize.Height * resolution - 90.0);

        return (minCoordinate, maxCoordinate);
    }

    /// <summary>
    /// Convert <see cref="Number"/> to <see cref="MercatorCoordinate"/>s
    /// </summary>
    /// <param name="tileSize"><see cref="Tile"/>'s <see cref="Size"/></param>
    /// <param name="tmsCompatible">Is tms compatible?</param>
    /// <returns><see cref="ValueTuple{T1, T2}"/> of <see cref="MercatorCoordinate"/>s</returns>
    /// <exception cref="ArgumentNullException"/>
    public (MercatorCoordinate minCoordinate, MercatorCoordinate maxCoordinate) ToMercatorCoordinates(Size tileSize, bool tmsCompatible)
        => ToMercatorCoordinates(this, tileSize, tmsCompatible);

    /// <inheritdoc cref="ToMercatorCoordinates(Size, bool)"/>
    /// <param name="number"><see cref="Number"/> to convert</param>
    /// <param name="tileSize"></param>
    /// <param name="tmsCompatible"></param>
    public static (MercatorCoordinate minCoordinate, MercatorCoordinate maxCoordinate) ToMercatorCoordinates(
        Number number, Size tileSize, bool tmsCompatible)
    {
        #region Preconditions checks

        if (number == null) throw new ArgumentNullException(nameof(number));
        if (tileSize == null) throw new ArgumentNullException(nameof(tileSize));

        #endregion

        if (!tmsCompatible) number = Flip(number);

        PixelCoordinate minPixelCoordinate = new(number.X * tileSize.Width,
                                                 number.Y * tileSize.Height);
        PixelCoordinate maxPixelCoordinate = new((number.X + 1) * tileSize.Width,
                                                 (number.Y + 1) * tileSize.Height);
        MercatorCoordinate minCoordinate = minPixelCoordinate.ToMercatorCoordinate(CoordinateSystem.Epsg3857,
            number.Z, tileSize);
        MercatorCoordinate maxCoordinate = maxPixelCoordinate.ToMercatorCoordinate(CoordinateSystem.Epsg3857,
            number.Z, tileSize);

        return (minCoordinate, maxCoordinate);
    }

    /// <summary>
    /// Convert <see cref="Number"/> to <see cref="GeoCoordinate"/>s
    /// </summary>
    /// <param name="coordinateSystem">Desired number's coordinate system</param>
    /// <param name="tileSize"><see cref="Tile"/>'s <see cref="Size"/></param>
    /// <param name="tmsCompatible">Is tms compatible?</param>
    /// <returns><see cref="ValueTuple{T1, T2}"/> of <see cref="GeoCoordinate"/>s</returns>
    /// <exception cref="ArgumentNullException"/>
    public (GeoCoordinate minCoordinate, GeoCoordinate maxCoordinate) ToGeoCoordinates(
        CoordinateSystem coordinateSystem, Size tileSize, bool tmsCompatible) =>
        ToGeoCoordinates(this, coordinateSystem, tileSize, tmsCompatible);

    /// <inheritdoc cref="ToGeoCoordinates(CoordinateSystem,Size,bool)"/>
    /// <param name="number"><see cref="Number"/> to convert</param>
    /// <param name="coordinateSystem"></param>
    /// <param name="tileSize"></param>
    /// <param name="tmsCompatible"></param>
    /// <exception cref="NotSupportedException"/>
    public static (GeoCoordinate minCoordinate, GeoCoordinate maxCoordinate) ToGeoCoordinates(
        Number number, CoordinateSystem coordinateSystem, Size tileSize, bool tmsCompatible)
    {
        #region Preconditions checks

        if (number == null) throw new ArgumentNullException(nameof(number));

        #endregion

        switch (coordinateSystem)
        {
            case CoordinateSystem.Epsg4326:
            {
                (GeodeticCoordinate minCoordinate, GeodeticCoordinate maxCoordinate) =
                    number.ToGeodeticCoordinates(tileSize, tmsCompatible);

                return (minCoordinate, maxCoordinate);
            }
            case CoordinateSystem.Epsg3857:
            {
                (MercatorCoordinate minCoordinate, MercatorCoordinate maxCoordinate) =
                    number.ToMercatorCoordinates(tileSize, tmsCompatible);

                return (minCoordinate, maxCoordinate);
            }
            default:
            {
                string err = string.Format(Strings.Culture, Strings.NotSupported, coordinateSystem);

                throw new NotSupportedException(err);
            }
        }
    }

    #endregion

    #region GetLowerNumbers

    /// <summary>
    /// Get lower <see cref="Number"/>s for specified <see cref="Number"/> and zoom
    /// </summary>
    /// <param name="z">Zoom;
    /// <remarks><para/>Must be >= 10</remarks></param>
    /// <returns><see cref="ValueTuple{T1, T2}"/> of lower <see cref="Number"/>s</returns>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="ArgumentOutOfRangeException"/>
    public (Number minNumber, Number maxNumber) GetLowerNumbers(int z) =>
        GetLowerNumbers(this, z);

    /// <inheritdoc cref="GetLowerNumbers(int)"/>
    /// <param name="number">Base <see cref="Number"/></param>
    /// <param name="z"></param>
    public static (Number minNumber, Number maxNumber) GetLowerNumbers(Number number, int z)
    {
        #region Preconditions checks

        if (number == null) throw new ArgumentNullException(nameof(number));
        if (z < 10) throw new ArgumentOutOfRangeException(nameof(z));

        #endregion

        int resolution = Convert.ToInt32(Math.Pow(2.0, z - 10.0));

        int[] tilesXs = { number.X * resolution, (number.X + 1) * resolution - 1 };
        int[] tilesYs = { number.Y * resolution, (number.Y + 1) * resolution - 1 };

        Number minNumber = new(tilesXs.Min(), tilesYs.Min(), z);
        Number maxNumber = new(tilesXs.Max(), tilesYs.Max(), z);

        return (minNumber, maxNumber);
    }

    /// <inheritdoc cref="GetLowerNumbers(Number)"/>
    public Number[] GetLowerNumbers() => GetLowerNumbers(this);

    /// <summary>
    /// Gets 4 one zoom lower <see cref="Number"/>s
    /// </summary>
    /// <param name="number">Input <see cref="Number"/></param>
    /// <returns>4 lower <see cref="Number"/>s</returns>
    /// <exception cref="ArgumentNullException"/>
    public static Number[] GetLowerNumbers(Number number)
    {
        #region Preconditions checks

        if (number == null) throw new ArgumentNullException(nameof(number));

        #endregion

        Number[] numbers = new Number[4];

        numbers[0] = new Number(number.X * 2, number.Y * 2, number.Z + 1);
        numbers[1] = new Number(numbers[0].X + 1, numbers[0].Y, numbers[0].Z);
        numbers[2] = new Number(numbers[0].X, numbers[0].Y + 1, numbers[0].Z);
        numbers[3] = new Number(numbers[0].X + 1, numbers[0].Y + 1, numbers[0].Z);

        return numbers;
    }

    #endregion

    #region GetCount

    /// <summary>
    /// Get count of <see cref="Tile"/>s in specified region
    /// </summary>
    /// <param name="minCoordinate">Minimal <see cref="GeoCoordinate"/></param>
    /// <param name="maxCoordinate">Maximal <see cref="GeoCoordinate"/></param>
    /// <param name="minZ">Minimal zoom</param>
    /// <param name="maxZ">Maximal zoom</param>
    /// <param name="tmsCompatible">Is tms compatible?</param>
    /// <param name="tileSize"><see cref="Tile"/>'s <see cref="Size"/></param>
    /// <returns><see cref="Tile"/>s count</returns>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="ArgumentOutOfRangeException"/>
    public static int GetCount(GeoCoordinate minCoordinate, GeoCoordinate maxCoordinate,
                               int minZ, int maxZ, bool tmsCompatible, Size tileSize)
    {
        #region Preconditions checks

        // Coordinates are checked inside GeoCoordinate.GetNumbers, no need to check it here
        // Size is checked inside GeoCoordinate.GetNumbers

        if (minZ < 0) throw new ArgumentOutOfRangeException(nameof(minZ));
        if (maxZ < minZ) throw new ArgumentOutOfRangeException(nameof(maxZ));

        #endregion

        int tilesCount = 0;

        for (int zoom = minZ; zoom <= maxZ; zoom++)
        {
            (Number minNumber, Number maxNumber) = GeoCoordinate.GetNumbers(minCoordinate, maxCoordinate, zoom, tileSize, tmsCompatible);

            int xsCount = maxNumber.X - minNumber.X + 1;
            int ysCount = maxNumber.Y - minNumber.Y + 1;

            tilesCount += xsCount * ysCount;
        }

        return tilesCount;
    }

    #endregion

    #region Bool compare overrides

    /// <inheritdoc />
    public override bool Equals(object obj) => Equals(obj as Number);

    /// <inheritdoc />
    public override int GetHashCode() => HashCode.Combine(X, Y, Z);

    /// <inheritdoc />
    public bool Equals(Number other)
    {
        if (other is null) return false;
        if (ReferenceEquals(this, other)) return true;

        return X == other.X && Y == other.Y && Z == other.Z;
    }

    /// <summary>
    /// Check two <see cref="Number"/>s for equality
    /// </summary>
    /// <param name="number1"><see cref="Number"/> 1</param>
    /// <param name="number2"><see cref="Number"/> 2</param>
    /// <returns><see langword="true"/> if <see cref="Number"/>s are equal;
    /// <see langword="false"/> otherwise</returns>
    public static bool operator ==(Number number1, Number number2) => number1?.Equals(number2) ?? number2 is null;

    /// <summary>
    /// Check two <see cref="Number"/>s for non-equality
    /// </summary>
    /// <param name="number1"><see cref="Number"/> 1</param>
    /// <param name="number2"><see cref="Number"/> 2</param>
    /// <returns><see langword="true"/> if <see cref="Number"/>s are not equal;
    /// <see langword="false"/> otherwise</returns>
    public static bool operator !=(Number number1, Number number2) => !(number1 == number2);

    #endregion

    #region Math operations

    /// <summary>
    /// Sum <see cref="Number"/>s
    /// <remarks><para/>Sums <see cref="X"/> and <see cref="Y"/>
    /// only if <see cref="Z"/>'s are the same;
    /// returns <see langword="null"/> otherwise</remarks>
    /// </summary>
    /// <param name="number1"><see cref="Number"/> 1</param>
    /// <param name="number2"><see cref="Number"/> 2</param>
    /// <returns>New <see cref="Number"/>, if <see cref="Z"/>s are the same</returns>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="ArgumentException"/>
    public static Number operator +(Number number1, Number number2)
    {
        #region Preconditions checks

        if (number1 == null) throw new ArgumentNullException(nameof(number1));
        if (number2 == null) throw new ArgumentNullException(nameof(number2));

        string err = string.Format(Strings.Culture, Strings.NotEqual, nameof(number1.Z), nameof(number2.Z));

        if (number1.Z != number2.Z) throw new ArgumentException(err);

        #endregion

        return new Number(number1.X + number2.X, number1.Y + number2.Y, number1.Z);
    }

    /// <inheritdoc cref="op_Addition"/>
    /// <param name="other"><see cref="Number"/> to add</param>
    public Number Add(Number other) => this + other;

    /// <summary>
    /// Subtruct <see cref="Number"/>s
    /// <remarks><para/>Subtruct <see cref="X"/> and <see cref="Y"/>
    /// only if <see cref="Z"/>'s are the same;
    /// returns <see langword="null"/> otherwise</remarks>
    /// </summary>
    /// <param name="number1"><see cref="Number"/> 1</param>
    /// <param name="number2"><see cref="Number"/> 2</param>
    /// <returns>New <see cref="Number"/>, if <see cref="Z"/>s are the same</returns>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="ArgumentException"/>
    public static Number operator -(Number number1, Number number2)
    {
        #region Preconditions checks

        if (number1 == null) throw new ArgumentNullException(nameof(number1));
        if (number2 == null) throw new ArgumentNullException(nameof(number2));

        string err = string.Format(Strings.Culture, Strings.NotEqual, nameof(number1.Z), nameof(number2.Z));

        if (number1.Z != number2.Z) throw new ArgumentException(err);

        #endregion

        return new Number(number1.X - number2.X, number1.Y - number2.Y, number1.Z);
    }

    /// <inheritdoc cref="op_Subtraction"/>
    /// <param name="other"><see cref="Number"/> to subtract</param>
    public Number Subtract(Number other) => this - other;

    /// <summary>
    /// Multiply <see cref="Number"/>s
    /// <remarks><para/>Multiply <see cref="X"/> and <see cref="Y"/>
    /// only if <see cref="Z"/>'s are the same;
    /// returns <see langword="null"/> otherwise</remarks>
    /// </summary>
    /// <param name="number1"><see cref="Number"/> 1</param>
    /// <param name="number2"><see cref="Number"/> 2</param>
    /// <returns>New <see cref="Number"/>, if <see cref="Z"/>s are the same</returns>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="ArgumentException"/>
    public static Number operator *(Number number1, Number number2)
    {
        #region Preconditions checks

        if (number1 == null) throw new ArgumentNullException(nameof(number1));
        if (number2 == null) throw new ArgumentNullException(nameof(number2));

        string err = string.Format(Strings.Culture, Strings.NotEqual, nameof(number1.Z), nameof(number2.Z));

        if (number1.Z != number2.Z) throw new ArgumentException(err);

        #endregion

        return new Number(number1.X * number2.X, number1.Y * number2.Y, number1.Z);
    }

    /// <inheritdoc cref="op_Multiply"/>
    /// <param name="other"><see cref="Number"/> to multiply</param>
    public Number Multiply(Number other) => this * other;

    /// <summary>
    /// Divide <see cref="Number"/>s
    /// <remarks><para/>Divide <see cref="X"/> and <see cref="Y"/>
    /// only if <see cref="Z"/>'s are the same;
    /// returns <see langword="null"/> otherwise</remarks>
    /// </summary>
    /// <param name="number1"><see cref="Number"/> 1</param>
    /// <param name="number2"><see cref="Number"/> 2</param>
    /// <returns>New <see cref="Number"/>, if <see cref="Z"/>s are the same</returns>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="ArgumentException"/>
    public static Number operator /(Number number1, Number number2)
    {
        #region Preconditions checks

        if (number1 == null) throw new ArgumentNullException(nameof(number1));
        if (number2 == null) throw new ArgumentNullException(nameof(number2));

        string err = string.Format(Strings.Culture, Strings.NotEqual, nameof(number1.Z), nameof(number2.Z));

        if (number1.Z != number2.Z) throw new ArgumentException(err);

        #endregion

        return new Number(number1.X / number2.X, number1.Y / number2.Y, number1.Z);
    }

    /// <inheritdoc cref="op_Division"/>
    /// <param name="other"><see cref="Number"/> to divide on</param>
    public Number Divide(Number other) => this / other;

    #endregion

    #endregion
}