Files
Aaru/Aaru.Gui/Controls/BlockMap.cs

489 lines
16 KiB
C#
Raw Normal View History

2020-03-11 21:56:55 +00:00
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : BlockMap.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI custom controls.
//
// --[ Description ] ----------------------------------------------------------
//
// Draws a block map.
//
// --[ License ] --------------------------------------------------------------
//
// 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/>.
//
// ----------------------------------------------------------------------------
2022-02-18 10:02:53 +00:00
// Copyright © 2011-2022 Natalia Portillo
2020-03-11 21:56:55 +00:00
// ****************************************************************************/
2022-03-07 07:36:44 +00:00
namespace Aaru.Gui.Controls;
using System;
2020-04-17 05:23:17 +01:00
using System.Collections.Generic;
using System.Collections.Specialized;
2020-04-17 05:23:17 +01:00
using Avalonia;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
2021-09-12 22:42:09 +01:00
using Avalonia.Visuals.Media.Imaging;
2020-07-22 13:20:25 +01:00
using JetBrains.Annotations;
2022-03-06 13:29:38 +00:00
// TODO: Partially fill clusters
// TODO: React to size changes
// TODO: Optimize block size to viewport
// TODO: Writing one more than it should
public sealed class BlockMap : ItemsControl
{
const int BLOCK_SIZE = 15;
public static readonly StyledProperty<ulong> BlocksProperty =
AvaloniaProperty.Register<Border, ulong>(nameof(Blocks));
2022-03-06 13:29:38 +00:00
public static readonly StyledProperty<IBrush> SuperFastColorProperty =
AvaloniaProperty.Register<Border, IBrush>(nameof(SuperFastColor), Brushes.LightGreen);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public static readonly StyledProperty<IBrush> FastColorProperty =
AvaloniaProperty.Register<Border, IBrush>(nameof(FastColor), Brushes.Green);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public static readonly StyledProperty<IBrush> AverageColorProperty =
AvaloniaProperty.Register<Border, IBrush>(nameof(AverageColor), Brushes.DarkGreen);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public static readonly StyledProperty<IBrush> SlowColorProperty =
AvaloniaProperty.Register<Border, IBrush>(nameof(SlowColor), Brushes.Yellow);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public static readonly StyledProperty<IBrush> SuperSlowColorProperty =
AvaloniaProperty.Register<Border, IBrush>(nameof(SuperSlowColor), Brushes.Orange);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public static readonly StyledProperty<IBrush> ProblematicColorProperty =
AvaloniaProperty.Register<Border, IBrush>(nameof(ProblematicColor), Brushes.Red);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public static readonly StyledProperty<double> SuperFastMaxTimeProperty =
AvaloniaProperty.Register<Border, double>(nameof(SuperFastMaxTime), 3);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public static readonly StyledProperty<double> FastMaxTimeProperty =
AvaloniaProperty.Register<Border, double>(nameof(FastMaxTime), 10);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public static readonly StyledProperty<double> AverageMaxTimeProperty =
AvaloniaProperty.Register<Border, double>(nameof(AverageMaxTime), 50);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public static readonly StyledProperty<double> SlowMaxTimeProperty =
AvaloniaProperty.Register<Border, double>(nameof(SlowMaxTime), 150);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public static readonly StyledProperty<double> SuperSlowMaxTimeProperty =
AvaloniaProperty.Register<Border, double>(nameof(SuperSlowMaxTime), 500);
RenderTargetBitmap _bitmap;
ulong _clusterSize;
ulong _maxBlocks;
2022-03-06 13:29:38 +00:00
public double SuperFastMaxTime
{
get => GetValue(SuperFastMaxTimeProperty);
set => SetValue(SuperFastMaxTimeProperty, value);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public double FastMaxTime
{
get => GetValue(FastMaxTimeProperty);
set => SetValue(FastMaxTimeProperty, value);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public double AverageMaxTime
{
get => GetValue(AverageMaxTimeProperty);
set => SetValue(AverageMaxTimeProperty, value);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public double SlowMaxTime
{
get => GetValue(SlowMaxTimeProperty);
set => SetValue(SlowMaxTimeProperty, value);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public double SuperSlowMaxTime
{
get => GetValue(SuperSlowMaxTimeProperty);
set => SetValue(SuperSlowMaxTimeProperty, value);
}
2022-03-06 13:29:38 +00:00
public IBrush SuperFastColor
{
get => GetValue(SuperFastColorProperty);
set => SetValue(SuperFastColorProperty, value);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public IBrush FastColor
{
get => GetValue(FastColorProperty);
set => SetValue(FastColorProperty, value);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public IBrush AverageColor
{
get => GetValue(AverageColorProperty);
set => SetValue(AverageColorProperty, value);
}
2022-03-06 13:29:38 +00:00
public IBrush SlowColor
{
get => GetValue(SlowColorProperty);
set => SetValue(SlowColorProperty, value);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public IBrush SuperSlowColor
{
get => GetValue(SuperSlowColorProperty);
set => SetValue(SuperSlowColorProperty, value);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public IBrush ProblematicColor
{
get => GetValue(ProblematicColorProperty);
set => SetValue(ProblematicColorProperty, value);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
public ulong Blocks
{
get => GetValue(BlocksProperty);
set => SetValue(BlocksProperty, value);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
protected override void OnPropertyChanged<T>([NotNull] AvaloniaPropertyChangedEventArgs<T> e)
{
base.OnPropertyChanged(e);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
switch(e.Property.Name)
{
case nameof(Blocks):
if(_maxBlocks == 0)
_maxBlocks = (ulong)(Width / BLOCK_SIZE * (Height / BLOCK_SIZE));
2020-02-29 18:03:35 +00:00
2022-03-06 13:29:38 +00:00
if(Blocks > _maxBlocks)
{
_clusterSize = Blocks / _maxBlocks;
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
if(Blocks % _maxBlocks > 0)
_clusterSize++;
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
if(Blocks / _clusterSize < _maxBlocks)
2020-04-17 05:23:17 +01:00
{
2022-03-06 13:29:38 +00:00
_maxBlocks = Blocks / _clusterSize;
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
if(Blocks % _clusterSize > 0)
_maxBlocks++;
}
}
else
{
_clusterSize = 1;
_maxBlocks = Blocks;
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
CreateBitmap();
DrawGrid();
RedrawAll();
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
break;
case nameof(SuperFastMaxTime):
case nameof(FastMaxTime):
case nameof(AverageMaxTime):
case nameof(SlowMaxTime):
case nameof(SuperSlowMaxTime):
case nameof(SuperFastColor):
case nameof(FastColor):
case nameof(AverageColor):
case nameof(SlowColor):
case nameof(SuperSlowColor):
case nameof(ProblematicColor):
2020-04-17 05:23:17 +01:00
CreateBitmap();
2022-03-06 13:29:38 +00:00
DrawGrid();
RedrawAll();
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
2021-09-12 22:42:09 +01:00
2022-03-06 13:29:38 +00:00
break;
2020-04-17 05:23:17 +01:00
}
2022-03-06 13:29:38 +00:00
}
2022-03-17 23:54:41 +00:00
public override void Render(DrawingContext context)
2022-03-06 13:29:38 +00:00
{
if((int?)_bitmap?.Size.Height != (int)Height ||
(int?)_bitmap?.Size.Width != (int)Width)
{
2022-03-06 13:29:38 +00:00
_maxBlocks = (ulong)(Width / BLOCK_SIZE * (Height / BLOCK_SIZE));
CreateBitmap();
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
context.DrawImage(_bitmap, new Rect(0, 0, Width, Height), new Rect(0, 0, Width, Height),
BitmapInterpolationMode.HighQuality);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
base.Render(context);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
protected override void ItemsCollectionChanged(object sender, [NotNull] NotifyCollectionChangedEventArgs e)
{
base.ItemsCollectionChanged(sender, e);
2022-03-06 13:29:38 +00:00
switch(e.Action)
{
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Replace:
{
if(!(e.NewItems is {} items))
throw new ArgumentException("Invalid list of items");
2022-03-06 13:29:38 +00:00
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
using var ctx = new DrawingContext(ctxi, false);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
foreach(object item in items)
2020-04-17 05:23:17 +01:00
{
2022-03-06 13:29:38 +00:00
if(!(item is ValueTuple<ulong, double> block))
throw new ArgumentException("Invalid item in list", nameof(Items));
2022-03-06 13:29:38 +00:00
DrawCluster(block.Item1, block.Item2, false, ctx);
}
2022-03-06 13:29:38 +00:00
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
2022-03-06 13:29:38 +00:00
break;
}
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Move:
{
if(!(e.NewItems is {} newItems) ||
!(e.OldItems is {} oldItems))
throw new ArgumentException("Invalid list of items");
2022-03-06 13:29:38 +00:00
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
using var ctx = new DrawingContext(ctxi, false);
2022-03-06 13:29:38 +00:00
foreach(object item in oldItems)
{
if(!(item is ValueTuple<ulong, double> block))
throw new ArgumentException("Invalid item in list", nameof(Items));
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
DrawCluster(block.Item1, block.Item2, false, ctx);
2020-04-17 05:23:17 +01:00
}
2020-02-29 18:03:35 +00:00
2022-03-06 13:29:38 +00:00
foreach(object item in newItems)
{
if(!(item is ValueTuple<ulong, double> block))
throw new ArgumentException("Invalid item in list", nameof(Items));
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
DrawCluster(block.Item1, block.Item2, false, ctx);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
break;
2020-04-17 05:23:17 +01:00
}
2022-03-06 13:29:38 +00:00
case NotifyCollectionChangedAction.Reset:
CreateBitmap();
DrawGrid();
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
break;
default: throw new ArgumentOutOfRangeException();
2020-04-17 05:23:17 +01:00
}
2022-03-06 13:29:38 +00:00
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
void RedrawAll()
{
if(Items is null)
return;
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
using var ctx = new DrawingContext(ctxi, false);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
foreach(object item in Items)
{
if(!(item is ValueTuple<ulong, double> block))
throw new ArgumentException("Invalid item in list", nameof(Items));
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
DrawCluster(block.Item1, block.Item2, false, ctx);
2020-04-17 05:23:17 +01:00
}
2022-03-06 13:29:38 +00:00
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
}
void DrawCluster(ulong block, double duration, bool clear = false, DrawingContext ctx = null)
{
if(double.IsNegative(duration) ||
double.IsInfinity(duration))
throw new ArgumentException("Duration cannot be negative or infinite", nameof(duration));
bool newContext = ctx is null;
ulong clustersPerRow = (ulong)Width / BLOCK_SIZE;
ulong cluster = block / _clusterSize;
ulong row = cluster / clustersPerRow;
ulong column = cluster % clustersPerRow;
ulong x = column * BLOCK_SIZE;
ulong y = row * BLOCK_SIZE;
var pen = new Pen(Foreground);
IBrush brush;
if(clear)
brush = Background;
else if(duration < SuperFastMaxTime)
brush = SuperFastColor;
else if(duration >= SuperFastMaxTime &&
duration < FastMaxTime)
brush = FastColor;
else if(duration >= FastMaxTime &&
duration < AverageMaxTime)
brush = AverageColor;
else if(duration >= AverageMaxTime &&
duration < SlowMaxTime)
brush = SlowColor;
else if(duration >= SlowMaxTime &&
duration < SuperSlowMaxTime)
brush = SuperSlowColor;
else if(duration >= SuperSlowMaxTime ||
double.IsNaN(duration))
brush = ProblematicColor;
else
brush = Background;
if(newContext)
2020-04-17 05:23:17 +01:00
{
2022-03-06 13:29:38 +00:00
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
ctx = new DrawingContext(ctxi, false);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
ctx.FillRectangle(brush, new Rect(x, y, BLOCK_SIZE, BLOCK_SIZE));
ctx.DrawRectangle(pen, new Rect(x, y, BLOCK_SIZE, BLOCK_SIZE));
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
if(double.IsNaN(duration))
{
ctx.DrawLine(pen, new Point(x, y), new Point(x + BLOCK_SIZE, y + BLOCK_SIZE));
ctx.DrawLine(pen, new Point(x, y + BLOCK_SIZE), new Point(x + BLOCK_SIZE, y));
2020-04-17 05:23:17 +01:00
}
2022-03-06 13:29:38 +00:00
if(newContext)
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
protected override void ItemsChanged([NotNull] AvaloniaPropertyChangedEventArgs e)
{
if(e.NewValue != null &&
!(e.NewValue is IList<(ulong, double)>))
throw new
ArgumentException("Items must be a IList<(ulong, double)> with ulong being the block and double being the time spent reading it, or NaN for an error.");
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
base.ItemsChanged(e);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
CreateBitmap();
DrawGrid();
RedrawAll();
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
void CreateBitmap()
{
if(_maxBlocks == 0)
_maxBlocks = (ulong)(Width / BLOCK_SIZE * (Height / BLOCK_SIZE));
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
_bitmap?.Dispose();
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
_bitmap = new RenderTargetBitmap(new PixelSize((int)Width, (int)Height), new Vector(96, 96));
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
using var ctx = new DrawingContext(ctxi, false);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
ctx.FillRectangle(Background, new Rect(0, 0, Width, Height));
}
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
void DrawGrid()
{
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
using var ctx = new DrawingContext(ctxi, false);
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
ulong clustersPerRow = (ulong)Width / BLOCK_SIZE;
2020-04-17 05:23:17 +01:00
2022-03-07 07:36:44 +00:00
var allBlocksDrawn = false;
2020-04-17 05:23:17 +01:00
2022-03-06 13:29:38 +00:00
for(ulong y = 0; y < Height && !allBlocksDrawn; y += BLOCK_SIZE)
2021-07-13 21:59:52 -07:00
{
2022-03-06 13:29:38 +00:00
for(ulong x = 0; x < Width; x += BLOCK_SIZE)
2021-07-13 21:59:52 -07:00
{
2022-03-07 07:36:44 +00:00
ulong currentBlockValue = y * clustersPerRow / BLOCK_SIZE + x / BLOCK_SIZE;
2021-08-17 21:23:10 +01:00
2022-03-06 13:29:38 +00:00
if(currentBlockValue >= _maxBlocks ||
currentBlockValue >= Blocks)
2021-07-13 21:59:52 -07:00
{
2022-03-06 13:29:38 +00:00
allBlocksDrawn = true;
break;
2021-07-13 21:59:52 -07:00
}
2022-03-06 13:29:38 +00:00
ctx.DrawRectangle(new Pen(Foreground), new Rect(x, y, BLOCK_SIZE, BLOCK_SIZE));
2021-07-13 21:59:52 -07:00
}
}
2022-03-06 13:29:38 +00:00
}
2021-07-13 21:59:52 -07:00
2022-03-06 13:29:38 +00:00
void DrawSquares(Color[] colors, int borderWidth, int sideLength)
{
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
using var ctx = new DrawingContext(ctxi, false);
2022-03-07 07:36:44 +00:00
int squareWidth = (sideLength - 2 * borderWidth) / colors.Length;
2022-03-06 13:29:38 +00:00
int squareHeight = squareWidth;
2022-03-07 07:36:44 +00:00
var x = 0;
var y = 0;
2022-03-06 13:29:38 +00:00
foreach(Color color in colors)
{
ctx.FillRectangle(new SolidColorBrush(color), new Rect(x, y, squareWidth, squareHeight));
2022-03-07 07:36:44 +00:00
x += squareWidth + 2 * borderWidth;
if(x < sideLength)
continue;
x = 0;
y += squareHeight + 2 * borderWidth;
}
2022-03-06 13:29:38 +00:00
}
2020-02-29 18:03:35 +00:00
2022-03-06 13:29:38 +00:00
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
if(Width < 1 ||
Height < 1 ||
double.IsNaN(Width) ||
double.IsNaN(Height))
2020-04-17 05:23:17 +01:00
{
2022-03-06 13:29:38 +00:00
base.OnAttachedToLogicalTree(e);
return;
2020-04-17 05:23:17 +01:00
}
2022-03-06 13:29:38 +00:00
CreateBitmap();
DrawGrid();
base.OnAttachedToLogicalTree(e);
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_bitmap.Dispose();
_bitmap = null;
base.OnDetachedFromLogicalTree(e);
}
}