Gigas002/GTiff2Tiles

View on GitHub
GTiff2Tiles.GUI/ViewModels/MainViewModel.cs

Summary

Maintainability
A
0 mins
Test Coverage
#pragma warning disable IDE0079 // Remove unnecessary suppression
#pragma warning disable CA1031 // Do not catch general exception types
#pragma warning disable CA1308 // Normalize strings to uppercase

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text.Json;
using System.Windows;
using System.Windows.Threading;
using GTiff2Tiles.Core;
using Prism.Mvvm;
using Prism.Commands;
using GTiff2Tiles.Core.Constants;
using GTiff2Tiles.Core.Enums;
using GTiff2Tiles.Core.GeoTiffs;
using GTiff2Tiles.Core.Helpers;
using GTiff2Tiles.Core.TileMapResource;
using GTiff2Tiles.Core.Tiles;
using GTiff2Tiles.GUI.Localization;
using GTiff2Tiles.GUI.Models;
using GTiff2Tiles.GUI.Views;
using MaterialDesignExtensions.Controls;
using MaterialDesignThemes.Wpf;
using Theme = GTiff2Tiles.GUI.Enums.Theme;
using Size = GTiff2Tiles.Core.Images.Size;

// ReSharper disable MemberCanBePrivate.Global

namespace GTiff2Tiles.GUI.ViewModels;

/// <summary>
/// ViewModel for <see cref="MainView"/>
/// </summary>
public class MainViewModel : BindableBase
{
    #region Properties

    #region DialogHost / Main grid

    private bool _isMainGridEnabled;

    /// <summary>
    /// Change main grid's state
    /// </summary>
    public bool IsMainGridEnabled
    {
        get => _isMainGridEnabled;
        set => SetProperty(ref _isMainGridEnabled, value);
    }

    #endregion

    #region Input file / Grid.Row=0

    private string _inputFilePath;

    /// <summary>
    /// Input file path
    /// </summary>
    public string InputFilePath
    {
        get => _inputFilePath;
        set => SetProperty(ref _inputFilePath, value);
    }

    /// <summary>
    /// Hint for InputFile TextBox
    /// </summary>
    public static string InputFileHint => Strings.InputFileHint;

    /// <summary>
    /// InputFileButton DelegateCommand
    /// </summary>
    public DelegateCommand InputFileButtonCommand { get; }

    #endregion

    #region Output directory / Grid.Row=2

    private string _outputDirectoryPath;

    /// <summary>
    /// Output directory path
    /// </summary>
    public string OutputDirectoryPath
    {
        get => _outputDirectoryPath;
        set => SetProperty(ref _outputDirectoryPath, value);
    }

    /// <summary>
    /// Hint for OutputDirectory TextBox
    /// </summary>
    public static string OutputDirectoryHint => Strings.OutputDirectoryHint;

    /// <summary>
    /// OutputDirectoryButton DelegateCommand
    /// </summary>
    public DelegateCommand OutputDirectoryButtonCommand { get; }

    #endregion

    #region Temp directory / Grid.Row=4

    private string _tempDirectoryPath;

    /// <summary>
    /// Temp directory path
    /// </summary>
    public string TempDirectoryPath
    {
        get => _tempDirectoryPath;
        set => SetProperty(ref _tempDirectoryPath, value);
    }

    /// <summary>
    /// Hint for TempDirectory TextBox
    /// </summary>
    public static string TempDirectoryHint => Strings.TempDirectoryHint;

    /// <summary>
    /// TempDirectoryButton DelegateCommand
    /// </summary>
    public DelegateCommand TempDirectoryButtonCommand { get; }

    #endregion

    #region Zooms / Grid.Row=6

    private int _minZ;

    /// <summary>
    /// Minimal zoom
    /// </summary>
    public int MinZ
    {
        get => _minZ;
        set => SetProperty(ref _minZ, value);
    }

    /// <summary>
    /// Hint for MinZ TextBox
    /// </summary>
    public static string MinZHint => Strings.MinZHint;

    private int _maxZ;

    /// <summary>
    /// Maximal zoom
    /// </summary>
    public int MaxZ
    {
        get => _maxZ;
        set => SetProperty(ref _maxZ, value);
    }

    /// <summary>
    /// Hint for MaxZ TextBox
    /// </summary>
    public static string MaxZHint => Strings.MaxZHint;

    #endregion

    #region Tile extension / Coordinate system / Grid.Row=8

    private TileExtension _targetTileExtension;

    /// <summary>
    /// Target tile's extension
    /// </summary>
    public TileExtension TargetTileExtension
    {
        get => _targetTileExtension;
        set => SetProperty(ref _targetTileExtension, value);
    }

    /// <summary>
    /// Collection of supported <see cref="TileExtension"/>s
    /// </summary>
    public ObservableCollection<TileExtension> TileExtensions { get; } = new();

    /// <summary>
    /// Hint for Extensions ComboBox
    /// </summary>
    public static string TileExtensionsHint => Strings.TileExtensionsHint;

    private CoordinateSystem _targetCoordinateSystem;

    /// <summary>
    /// Target tile's coordinate system
    /// </summary>
    public CoordinateSystem TargetCoordinateSystem
    {
        get => _targetCoordinateSystem;
        set => SetProperty(ref _targetCoordinateSystem, value);
    }

    /// <summary>
    /// Hint for CoordinateSystems ComboBox
    /// </summary>
    public static string CoordinateSystemsHint => Strings.CoordinateSystemsHint;

    /// <summary>
    /// Collection of supprted <see cref="CoordinateSystem"/>s
    /// </summary>
    public ObservableCollection<CoordinateSystem> CoordinateSystems { get; } = new();

    #endregion

    #region Interpolation / Bands count / Grid.Row=10

    private NetVips.Enums.Kernel _targetInterpolation;

    /// <summary>
    /// <see cref="Interpolation"/> of ready tiles
    /// </summary>
    public NetVips.Enums.Kernel TargetInterpolation
    {
        get => _targetInterpolation;
        set => SetProperty(ref _targetInterpolation, value);
    }

    /// <summary>
    /// Hint for Interpolation TextBox
    /// </summary>
    public static string InterpolationsHint => Strings.InterpolationsHint;

    /// <summary>
    /// Collection of supprted <see cref="Interpolation"/>s
    /// </summary>
    public ObservableCollection<NetVips.Enums.Kernel> Interpolations { get; } = new();

    private int _bandsCount;

    /// <summary>
    /// Number of bands in ready tiles
    /// </summary>
    public int BandsCount
    {
        get => _bandsCount;
        set => SetProperty(ref _bandsCount, value);
    }

    /// <summary>
    /// Hint for Bands TextBox
    /// </summary>
    public static string BandsHint => Strings.BandsHint;

    #endregion

    #region Tms compatible / Grid.Row=12

    private bool _tmsCompatible;

    /// <summary>
    /// Shows if you want to create tms-compatible tiles
    /// </summary>
    public bool TmsCompatible
    {
        get => _tmsCompatible;
        set => SetProperty(ref _tmsCompatible, value);
    }

    /// <summary>
    /// Text near tms check box
    /// </summary>
    public static string TmsCheckBoxContent => Strings.TmsCheckBoxContent;

    #endregion

    #region Expander with additional settings / Grid.Row=14

    /// <summary>
    /// Expander's header content
    /// </summary>
    public static string ExpanderHeader => Strings.ExpanderHeader;

    #region Theme

    private Theme _theme;

    /// <summary>
    /// Theme for DialogHost
    /// <remarks><para/>Automatically changes on set</remarks>
    /// </summary>
    public Theme Theme
    {
        get => _theme;
        set
        {
            SetProperty(ref _theme, value);

            // Set theme
            BaseDialogTheme = ThemeModel.SetTheme(value);
        }
    }

    /// <summary>
    /// Collection of supported themes
    /// </summary>
    public ObservableCollection<Theme> Themes { get; } = new();

    private BaseTheme _baseDialogTheme;

    /// <summary>
    /// Value in DialogHost only
    /// </summary>
    public BaseTheme BaseDialogTheme
    {
        get => _baseDialogTheme;
        set => SetProperty(ref _baseDialogTheme, value);
    }

    /// <summary>
    /// Hint for Themes combobox
    /// </summary>
    public static string ThemesHint => Strings.ThemesHint;

    #endregion

    #region Tile side size

    private int _tileSideSize;

    /// <summary>
    /// Size of tile's side
    /// </summary>
    public int TileSideSize
    {
        get => _tileSideSize;
        set => SetProperty(ref _tileSideSize, value);
    }

    /// <summary>
    /// Hint for TileSideSize TextBox
    /// </summary>
    public static string TileSizeHint => Strings.TileSizeHint;

    /// <summary>
    /// Ready tiles's size
    /// </summary>
    public Size TileSize => new(TileSideSize, TileSideSize);

    #endregion

    #region Threads

    private bool _isAutoThreads;

    /// <summary>
    /// Should threads be calculated automatically?
    /// </summary>
    public bool IsAutoThreads
    {
        get => _isAutoThreads;
        set
        {
            SetProperty(ref _isAutoThreads, value);
            ThreadsCountVisibility = value ? Visibility.Collapsed : Visibility.Visible;
        }
    }

    /// <summary>
    /// Hint for AutoThreads CheckBox
    /// </summary>
    public static string IsAutoThreadsContent => Strings.IsAutoThreadsContent;

    private int _threadsCount;

    /// <summary>
    /// Threads count
    /// <remarks><para/>Used only when <see cref="IsAutoThreads"/>
    /// equals <see langword="false"/></remarks>
    /// </summary>
    public int ThreadsCount
    {
        get => _threadsCount;
        set => SetProperty(ref _threadsCount, value);
    }

    /// <summary>
    /// Hint for ThreadsCount TextBox
    /// </summary>
    public static string ThreadsCountHint => Strings.ThreadsCountHint;

    private Visibility _threadsCountVisibility;

    /// <summary>
    /// Controls the ThreadsCount TextBox visibility
    /// </summary>
    public Visibility ThreadsCountVisibility
    {
        get => _threadsCountVisibility;
        set => SetProperty(ref _threadsCountVisibility, value);
    }

    #endregion

    #region Tile cache

    private int _tileCache;

    /// <summary>
    /// How much tiles would you like to store in cache?
    /// </summary>
    public int TileCache
    {
        get => _tileCache;
        set => SetProperty(ref _tileCache, value);
    }

    /// <summary>
    /// Hint for TileCache TextBox
    /// </summary>
    public static string TileCacheHint => Strings.TileCacheHint;

    #endregion

    #region Memory

    private long _memory;

    /// <summary>
    /// Max size of input tiff to store in RAM
    /// </summary>
    public long Memory
    {
        get => _memory;
        set => SetProperty(ref _memory, value);
    }

    /// <summary>
    /// Hint for Memory TextBox
    /// </summary>
    public static string MemoryHint => Strings.MemoryHint;

    #endregion

    #region TileMapResource

    private const string TmrName = "tilemapresource.xml";

    private bool _isTmr;

    /// <summary>
    /// Shows if you want to create tilemapresource.xml
    /// </summary>
    public bool IsTmr
    {
        get => _isTmr;
        set => SetProperty(ref _isTmr, value);
    }

    /// <summary>
    /// Text near tmr check box
    /// </summary>
    public static string TmrCheckBoxContent => Strings.TmrCheckBoxContent;

    #endregion

    #region Settings

    private SettingsModel _settings;

    /// <summary>
    /// Parsed <see cref="SettingsModel"/> from .json
    /// </summary>
    public SettingsModel Settings
    {
        get => _settings;
        set => SetProperty(ref _settings, value);
    }

    /// <summary>
    /// Content of SaveSettings Button
    /// </summary>
    public static string SaveSettingsButtonContent => Strings.SaveSettingsButtonContent;

    /// <summary>
    /// SaveSettings Button command delegate
    /// </summary>
    public DelegateCommand SaveSettingsButtonCommand { get; }

    #endregion

    #endregion

    #region Start button / Grid.Row=16

    /// <summary>
    /// Text inside Start button
    /// </summary>
    public static string StartButtonContent => Strings.StartButtonContent;

    /// <summary>
    /// StartButton DelegateCommand
    /// </summary>
    public DelegateCommand StartButtonCommand { get; }

    #endregion

    #region Progress bar / Grid.Row=18, 20

    private double _progressBarValue;

    /// <summary>
    /// Progress bar value
    /// </summary>
    public double ProgressBarValue
    {
        get => _progressBarValue;
        set
        {
            if (Math.Abs(_progressBarValue - value) < double.Epsilon) return;

            SetProperty(ref _progressBarValue, value);
        }
    }

    /// <summary>
    /// Text in progress's TextBlock (e.g. "Progress:")
    /// </summary>
    public static string ProgressTextBlock => Strings.ProgressTextBlock;

    #endregion

    #region Time passed / Grid.Row=20

    private DispatcherTimer _timer;

    public static string TimePassedTextBlock => Strings.TimePassedTextBlock;

    private string _timePassedValue;

    /// <summary>
    /// Shows how much time passed since process started
    /// </summary>
    public string TimePassedValue
    {
        get => _timePassedValue;
        set
        {
            if (_timePassedValue == value) return;

            SetProperty(ref _timePassedValue, value);
        }
    }

    #endregion

    #region Meta info / Grid.Row=22

    /// <summary>
    /// Copyright string
    /// </summary>
    public static string Copyright => "© Gigas002 2023";

    /// <summary>
    /// Info about current version
    /// <remarks><para/>Pattern: {MAJOR}.{MINOR}.{PATCH}.{BUILD}</remarks>
    /// </summary>
    public static string Version => Assembly.GetExecutingAssembly().GetName().Version?.ToString();

    #endregion

    #endregion

    #region Constructor

    /// <summary>
    /// Initialize all needed properties
    /// </summary>
    public MainViewModel()
    {
        IsMainGridEnabled = true;

        try
        {
            Settings = JsonSerializer.Deserialize<SettingsModel>(File.ReadAllBytes(SettingsModel.Location));
        }
        catch (Exception)
        {
            // ignored
        }

        Settings ??= new SettingsModel();

        InputFilePath = Settings.InputFilePath;
        InputFileButtonCommand = new DelegateCommand(async () => await InputFileButtonAsync().ConfigureAwait(true));

        OutputDirectoryPath = Settings.OutputDirectoryPath;
        OutputDirectoryButtonCommand = new DelegateCommand(async () => await OutputDirectoryButtonAsync().ConfigureAwait(true));

        TempDirectoryPath = Settings.TempDirectoryPath;
        TempDirectoryButtonCommand = new DelegateCommand(async () => await TempDirectoryButtonAsync().ConfigureAwait(true));

        MinZ = Settings.MinZ;
        MaxZ = Settings.MaxZ;

        TileExtensions.Add(TileExtension.Png);
        TileExtensions.Add(TileExtension.Jpg);
        TileExtensions.Add(TileExtension.Webp);
        TargetTileExtension = Settings.TargetTileExtension;
        CoordinateSystems.Add(CoordinateSystem.Epsg3857);
        CoordinateSystems.Add(CoordinateSystem.Epsg4326);
        TargetCoordinateSystem = Settings.TargetCoordinateSystem;

        Interpolations.Add(NetVips.Enums.Kernel.Linear);
        Interpolations.Add(NetVips.Enums.Kernel.Nearest);
        Interpolations.Add(NetVips.Enums.Kernel.Cubic);
        Interpolations.Add(NetVips.Enums.Kernel.Lanczos2);
        Interpolations.Add(NetVips.Enums.Kernel.Lanczos3);
        Interpolations.Add(NetVips.Enums.Kernel.Mitchell);
        TargetInterpolation = Settings.TargetInterpolation;

        BandsCount = Settings.BandsCount;

        TmsCompatible = Settings.TmsCompatible;
        IsTmr = Settings.IsTmr;

        Themes.Add(Theme.Dark);
        Themes.Add(Theme.Light);
        Theme = Settings.TargetTheme;
        TileSideSize = Settings.TileSideSize;
        IsAutoThreads = Settings.IsAutoThreads;
        ThreadsCount = Settings.ThreadsCount;
        TileCache = Settings.TileCache;
        Memory = Settings.Memory;
        SaveSettingsButtonCommand = new DelegateCommand(async () => await SaveSettingsAsync().ConfigureAwait(true));

        StartButtonCommand = new DelegateCommand(async () => await StartButtonAsync().ConfigureAwait(true));

        ProgressBarValue = 0.0;
        TimePassedValue = "0 00:00:00";
    }

    #endregion

    #region Methods

    #region Buttons

    /// <summary>
    /// Input directory button
    /// </summary>
    /// <returns></returns>
    public async ValueTask InputFileButtonAsync()
    {
        try
        {
            OpenFileDialogResult dialogResult = await OpenFileDialog.ShowDialogAsync(dialogHostName: null, new OpenFileDialogArguments())
                                                                    .ConfigureAwait(true);

            InputFilePath = dialogResult.Canceled ? InputFilePath : dialogResult.FileInfo.FullName;
        }
        catch (Exception exception)
        {
            await Helpers.ErrorHelper.ShowExceptionAsync(exception).ConfigureAwait(true);
        }
    }

    /// <summary>
    /// Output directory button
    /// </summary>
    /// <returns></returns>
    public async ValueTask OutputDirectoryButtonAsync()
    {
        try
        {
            OpenDirectoryDialogArguments args = new() { CreateNewDirectoryEnabled = true };
            OpenDirectoryDialogResult dialogResult = await OpenDirectoryDialog.ShowDialogAsync(dialogHostName: null, args).ConfigureAwait(true);

            OutputDirectoryPath = dialogResult.Canceled ? OutputDirectoryPath : dialogResult.Directory;
        }
        catch (Exception exception)
        {
            await Helpers.ErrorHelper.ShowExceptionAsync(exception).ConfigureAwait(true);
        }
    }

    /// <summary>
    /// Temp directory button
    /// </summary>
    /// <returns></returns>
    public async ValueTask TempDirectoryButtonAsync()
    {
        try
        {
            OpenDirectoryDialogArguments args = new() { CreateNewDirectoryEnabled = true };
            OpenDirectoryDialogResult dialogResult = await OpenDirectoryDialog.ShowDialogAsync(dialogHostName: null, args).ConfigureAwait(true);

            TempDirectoryPath = dialogResult.Canceled ? TempDirectoryPath : dialogResult.Directory;
        }
        catch (Exception exception)
        {
            await Helpers.ErrorHelper.ShowExceptionAsync(exception).ConfigureAwait(true);
        }
    }

    /// <summary>
    /// Start button
    /// </summary>
    /// <returns></returns>
    public async ValueTask StartButtonAsync()
    {
        #region Preconditions checks

        if (!await CheckPropertiesAsync().ConfigureAwait(true)) return;

        #endregion

        // Start timer after checks passed
        Stopwatch stopwatch = Stopwatch.StartNew();
        _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1.0) };
        _timer.Tick += (_, _) =>
            TimePassedValue = string.Format(CultureInfo.InvariantCulture, Strings.TimePassedValue, stopwatch.Elapsed.Days,
                                            stopwatch.Elapsed.Hours, stopwatch.Elapsed.Minutes,
                                            stopwatch.Elapsed.Seconds);
        _timer.Start();

        // Create temp directory object
        string tempDirectoryPath = Path.Combine(TempDirectoryPath, DateTime.Now.ToString(DateTimePatterns.LongWithMs, CultureInfo.InvariantCulture));
        CheckHelper.CheckDirectory(tempDirectoryPath, true);

        // Create progress reporter
        IProgress<double> progress = new Progress<double>(value => ProgressBarValue = Math.Round(value, 4));

        // Because we need to check input file it's better to use temprorary value
        string inputFilePath = InputFilePath;

        // Threads should be calculated automatically if checked
        int threadsCount = IsAutoThreads ? 0 : ThreadsCount;

        // Run tiling asynchroniously
        try
        {
            if (!await CheckHelper.CheckInputFileAsync(inputFilePath, TargetCoordinateSystem).ConfigureAwait(true))
            {
                string tempFilePath = Path.Combine(tempDirectoryPath, GdalWorker.TempFileName);

                await GdalWorker.ConvertGeoTiffToTargetSystemAsync(inputFilePath, tempFilePath,
                                                                   TargetCoordinateSystem, progress)
                                .ConfigureAwait(true);
                inputFilePath = tempFilePath;
            }

            using Raster image = new(inputFilePath, TargetCoordinateSystem);

            // Generate tiles
            await image.WriteTilesToDirectoryAsync(OutputDirectoryPath, MinZ, MaxZ, TmsCompatible, TileSize,
                                                   TargetTileExtension, TargetInterpolation, BandsCount, TileCache,
                                                   threadsCount, progress).ConfigureAwait(true);

            // Generate tilemapresource if needed
            if (IsTmr)
            {
                IEnumerable<TileSet> tileSets = TileSets.GenerateTileSetCollection(MinZ, MaxZ, TileSize, TargetCoordinateSystem);
                TileMap tileMap = new(image.MinCoordinate, image.MaxCoordinate, TileSize, TargetTileExtension, tileSets,
                                      TargetCoordinateSystem);

                string xmlPath = $"{OutputDirectoryPath}/{TmrName}";
                using FileStream fs = File.OpenWrite(xmlPath);
                tileMap.Serialize(fs);
            }
        }
        catch (Exception exception)
        {
            await Helpers.ErrorHelper.ShowExceptionAsync(exception).ConfigureAwait(true);

            return;
        }
        finally
        {
            // Enable controls and stop timer
            IsMainGridEnabled = true;
            stopwatch.Stop();
            _timer.Stop();
        }

        await DialogHost.Show(new MessageBoxDialogViewModel(Strings.Done)).ConfigureAwait(true);
    }

    /// <summary>
    /// Update the settings.json
    /// </summary>
    /// <returns></returns>
    public Task SaveSettingsAsync()
    {
        Settings = new SettingsModel
        {
            InputFilePath = InputFilePath,
            OutputDirectoryPath = OutputDirectoryPath,
            TempDirectoryPath = TempDirectoryPath,
            MinZ = MinZ, MaxZ = MaxZ,
            TileExtension = Tile.GetExtensionString(TargetTileExtension),
            CoordinateSystem = SettingsModel.ParseCoordinateSystem(TargetCoordinateSystem),
            Interpolation = TargetInterpolation.ToString(),
            BandsCount = BandsCount,
            TmsCompatible = TmsCompatible, Theme = ThemeModel.GetTheme(Theme),
            IsTmr = IsTmr,
            TileSideSize = TileSideSize,
            IsAutoThreads = IsAutoThreads, ThreadsCount = ThreadsCount,
            TileCache = TileCache, Memory = Memory
        };

        return SettingsModel.SaveAsync(Settings);
    }

    #endregion

    #region Check properties

    /// <summary>
    /// Checks properties for errors and set some before starting
    /// </summary>
    /// <returns><see langword="true"/> if no errors occured;
    /// <see langword="false"/> otherwise</returns>
    private ValueTask<bool> CheckPropertiesAsync()
    {
        try
        {
            // Check paths
            CheckHelper.CheckFile(InputFilePath, true, FileExtensions.Tif);
            CheckHelper.CheckDirectory(OutputDirectoryPath, true);
            CheckHelper.CheckDirectory(TempDirectoryPath);

            // Required params
            if (MinZ < 0) throw new ArgumentOutOfRangeException(nameof(MinZ));
            if (MaxZ < MinZ) throw new ArgumentOutOfRangeException(nameof(MaxZ));
            if (BandsCount < 1 || BandsCount > 4) throw new ArgumentOutOfRangeException(nameof(BandsCount));

            // Optional params
            if (TileSideSize <= 0) TileSideSize = Tile.DefaultSize.Width;
            if (ThreadsCount <= 0) ThreadsCount = Environment.ProcessorCount;
            if (TileCache < 0) TileCache = 1000;

            // Need to set explicitly
            if (Memory <= 0) throw new ArgumentOutOfRangeException(nameof(Memory));
        }
        catch (Exception exception)
        {
            return Helpers.ErrorHelper.ShowExceptionAsync(exception);
        }

        // Disable grid while working if no errors in args
        IsMainGridEnabled = false;

        // Set default progress bar value for each run
        ProgressBarValue = 0.0;

        return ValueTask.FromResult(true);
    }

    #endregion

    #endregion
}

#pragma warning restore CA1031 // Do not catch general exception types
#pragma warning restore CA1308 // Normalize strings to uppercase
#pragma warning restore IDE0079 // Remove unnecessary suppression