mirror of
https://github.com/aaru-dps/Aaru.git
synced 2025-12-16 19:24:25 +00:00
[GUI] Add BlockMap control for visualizing sector access times
This commit is contained in:
11
Aaru.Gui/Controls/BlockMap.axaml
Normal file
11
Aaru.Gui/Controls/BlockMap.axaml
Normal file
@@ -0,0 +1,11 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="800"
|
||||
d:DesignHeight="450"
|
||||
x:Class="Aaru.Gui.Controls.BlockMap">
|
||||
<Canvas Name="BlockCanvas"
|
||||
Background="Transparent" />
|
||||
</UserControl>
|
||||
262
Aaru.Gui/Controls/BlockMap.axaml.cs
Normal file
262
Aaru.Gui/Controls/BlockMap.axaml.cs
Normal file
@@ -0,0 +1,262 @@
|
||||
// /***************************************************************************
|
||||
// Aaru Data Preservation Suite
|
||||
// ----------------------------------------------------------------------------
|
||||
//
|
||||
// Filename : BlockMap.cs
|
||||
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
||||
//
|
||||
// Component : GUI custom controls.
|
||||
//
|
||||
// --[ Description ] ----------------------------------------------------------
|
||||
//
|
||||
// A block map control to visualize sector access times.
|
||||
//
|
||||
// --[ License ] --------------------------------------------------------------
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// ----------------------------------------------------------------------------
|
||||
// Copyright © 2011-2025 Natalia Portillo
|
||||
// ****************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Aaru.Gui.Controls;
|
||||
|
||||
public partial class BlockMap : UserControl
|
||||
{
|
||||
const int BlockSize = 4; // Size of each block in pixels
|
||||
const int BlockSpacing = 1; // Spacing between blocks
|
||||
const double MinDuration = 1.0; // Green threshold (ms)
|
||||
const double MaxDuration = 500.0; // Red threshold (ms)
|
||||
|
||||
public static readonly StyledProperty<ObservableCollection<(ulong startingSector, double duration)>>
|
||||
SectorDataProperty =
|
||||
AvaloniaProperty
|
||||
.Register<BlockMap, ObservableCollection<(ulong startingSector, double duration)>>(nameof(SectorData));
|
||||
|
||||
public static readonly StyledProperty<uint> ScanBlockSizeProperty =
|
||||
AvaloniaProperty.Register<BlockMap, uint>(nameof(ScanBlockSize), 1u);
|
||||
int _blocksPerRow;
|
||||
|
||||
readonly Canvas _canvas;
|
||||
uint _scanBlockSize = 1;
|
||||
ObservableCollection<(ulong startingSector, double duration)> _sectorData;
|
||||
int _totalBlocksDrawn;
|
||||
|
||||
public BlockMap()
|
||||
{
|
||||
InitializeComponent();
|
||||
_canvas = this.FindControl<Canvas>("BlockCanvas");
|
||||
}
|
||||
|
||||
public ObservableCollection<(ulong startingSector, double duration)> SectorData
|
||||
{
|
||||
get => GetValue(SectorDataProperty);
|
||||
set => SetValue(SectorDataProperty, value);
|
||||
}
|
||||
|
||||
public uint ScanBlockSize
|
||||
{
|
||||
get => GetValue(ScanBlockSizeProperty);
|
||||
set => SetValue(ScanBlockSizeProperty, value);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
if(change.Property == SectorDataProperty)
|
||||
{
|
||||
if(_sectorData != null) _sectorData.CollectionChanged -= OnSectorDataChanged;
|
||||
|
||||
_sectorData = change.GetNewValue<ObservableCollection<(ulong startingSector, double duration)>>();
|
||||
|
||||
if(_sectorData != null)
|
||||
{
|
||||
_sectorData.CollectionChanged += OnSectorDataChanged;
|
||||
RedrawAll();
|
||||
}
|
||||
}
|
||||
else if(change.Property == ScanBlockSizeProperty)
|
||||
{
|
||||
_scanBlockSize = change.GetNewValue<uint>();
|
||||
RedrawAll();
|
||||
}
|
||||
else if(change.Property == BoundsProperty)
|
||||
{
|
||||
CalculateBlocksPerRow();
|
||||
RedrawAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSectorDataChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if(e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null)
|
||||
{
|
||||
// Incremental draw for added items
|
||||
DrawNewBlocks(e.NewStartingIndex, e.NewItems.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Full redraw for other operations
|
||||
RedrawAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void CalculateBlocksPerRow()
|
||||
{
|
||||
if(Bounds.Width <= 0) return;
|
||||
|
||||
var availableWidth = (int)Bounds.Width;
|
||||
int blockWithSpacing = BlockSize + BlockSpacing;
|
||||
_blocksPerRow = Math.Max(1, availableWidth / blockWithSpacing);
|
||||
}
|
||||
|
||||
private void RedrawAll()
|
||||
{
|
||||
if(_canvas == null || _sectorData == null || _sectorData.Count == 0) return;
|
||||
|
||||
_canvas.Children.Clear();
|
||||
CalculateBlocksPerRow();
|
||||
_totalBlocksDrawn = 0;
|
||||
|
||||
DrawNewBlocks(0, _sectorData.Count);
|
||||
}
|
||||
|
||||
private void DrawNewBlocks(int startIndex, int count)
|
||||
{
|
||||
if(_canvas == null || _sectorData == null || _blocksPerRow == 0) return;
|
||||
|
||||
int blockWithSpacing = BlockSize + BlockSpacing;
|
||||
|
||||
for(int i = startIndex; i < startIndex + count && i < _sectorData.Count; i++)
|
||||
{
|
||||
(ulong startingSector, double duration) = _sectorData[i];
|
||||
Color color = GetColorForDuration(duration);
|
||||
|
||||
// Calculate position in grid
|
||||
int blockIndex = _totalBlocksDrawn;
|
||||
int row = blockIndex / _blocksPerRow;
|
||||
int col = blockIndex % _blocksPerRow;
|
||||
|
||||
// Create and position rectangle
|
||||
var rect = new Border
|
||||
{
|
||||
Width = BlockSize,
|
||||
Height = BlockSize,
|
||||
Background = new SolidColorBrush(color),
|
||||
BorderBrush = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0)
|
||||
};
|
||||
|
||||
Canvas.SetLeft(rect, col * blockWithSpacing);
|
||||
Canvas.SetTop(rect, row * blockWithSpacing);
|
||||
|
||||
_canvas.Children.Add(rect);
|
||||
_totalBlocksDrawn++;
|
||||
}
|
||||
|
||||
// Update canvas height based on rows needed
|
||||
int totalRows = (_totalBlocksDrawn + _blocksPerRow - 1) / _blocksPerRow;
|
||||
_canvas.Height = totalRows * blockWithSpacing;
|
||||
}
|
||||
|
||||
private Color GetColorForDuration(double duration)
|
||||
{
|
||||
// Clamp duration between min and max
|
||||
double clampedDuration = Math.Max(MinDuration, Math.Min(MaxDuration, duration));
|
||||
|
||||
// Calculate normalized position (0 = green/fast, 1 = red/slow)
|
||||
double normalized = (clampedDuration - MinDuration) / (MaxDuration - MinDuration);
|
||||
|
||||
// Interpolate through color spectrum with more gradients:
|
||||
// Green -> Lime -> Yellow -> Orange -> Red-Orange -> Dark Red
|
||||
if(normalized <= 0.17) // Green to Lime
|
||||
{
|
||||
double t = normalized / 0.17;
|
||||
|
||||
return Color.FromRgb((byte)(0 + t * 128), // R: 0 -> 128
|
||||
255, // G: stays 255
|
||||
0 // B: stays 0
|
||||
);
|
||||
}
|
||||
|
||||
if(normalized <= 0.34) // Lime to Yellow
|
||||
{
|
||||
double t = (normalized - 0.17) / 0.17;
|
||||
|
||||
return Color.FromRgb((byte)(128 + t * 127), // R: 128 -> 255
|
||||
255, // G: stays 255
|
||||
0 // B: stays 0
|
||||
);
|
||||
}
|
||||
|
||||
if(normalized <= 0.50) // Yellow to Orange
|
||||
{
|
||||
double t = (normalized - 0.34) / 0.16;
|
||||
|
||||
return Color.FromRgb(255, // R: stays 255
|
||||
(byte)(255 - t * 85), // G: 255 -> 170
|
||||
0 // B: stays 0
|
||||
);
|
||||
}
|
||||
|
||||
if(normalized <= 0.67) // Orange to Orange-Red
|
||||
{
|
||||
double t = (normalized - 0.50) / 0.17;
|
||||
|
||||
return Color.FromRgb(255, // R: stays 255
|
||||
(byte)(170 - t * 85), // G: 170 -> 85
|
||||
(byte)(0 + t * 64) // B: 0 -> 64
|
||||
);
|
||||
}
|
||||
|
||||
if(normalized <= 0.84) // Orange-Red to Red
|
||||
{
|
||||
double t = (normalized - 0.67) / 0.17;
|
||||
|
||||
return Color.FromRgb(255, // R: stays 255
|
||||
(byte)(85 - t * 85), // G: 85 -> 0
|
||||
(byte)(64 + t * 64) // B: 64 -> 128
|
||||
);
|
||||
}
|
||||
else // Red to Dark Red
|
||||
{
|
||||
double t = (normalized - 0.84) / 0.16;
|
||||
|
||||
return Color.FromRgb((byte)(255 - t * 55), // R: 255 -> 200
|
||||
0, // G: stays 0
|
||||
(byte)(128 + t * 127) // B: 128 -> 255
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,491 +0,0 @@
|
||||
// /***************************************************************************
|
||||
// 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/>.
|
||||
//
|
||||
// ----------------------------------------------------------------------------
|
||||
// Copyright © 2011-2025 Natalia Portillo
|
||||
// ****************************************************************************/
|
||||
|
||||
/* TODO: Doesn't compile with Avalonia 11.0, but it didn't work previously so pending rewriting
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using Aaru.Localization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.Visuals.Media.Imaging;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Aaru.Gui.Controls;
|
||||
|
||||
// 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));
|
||||
|
||||
public static readonly StyledProperty<IBrush> SuperFastColorProperty =
|
||||
AvaloniaProperty.Register<Border, IBrush>(nameof(SuperFastColor), Brushes.LightGreen);
|
||||
|
||||
public static readonly StyledProperty<IBrush> FastColorProperty =
|
||||
AvaloniaProperty.Register<Border, IBrush>(nameof(FastColor), Brushes.Green);
|
||||
|
||||
public static readonly StyledProperty<IBrush> AverageColorProperty =
|
||||
AvaloniaProperty.Register<Border, IBrush>(nameof(AverageColor), Brushes.DarkGreen);
|
||||
|
||||
public static readonly StyledProperty<IBrush> SlowColorProperty =
|
||||
AvaloniaProperty.Register<Border, IBrush>(nameof(SlowColor), Brushes.Yellow);
|
||||
|
||||
public static readonly StyledProperty<IBrush> SuperSlowColorProperty =
|
||||
AvaloniaProperty.Register<Border, IBrush>(nameof(SuperSlowColor), Brushes.Orange);
|
||||
|
||||
public static readonly StyledProperty<IBrush> ProblematicColorProperty =
|
||||
AvaloniaProperty.Register<Border, IBrush>(nameof(ProblematicColor), Brushes.Red);
|
||||
|
||||
public static readonly StyledProperty<double> SuperFastMaxTimeProperty =
|
||||
AvaloniaProperty.Register<Border, double>(nameof(SuperFastMaxTime), 3);
|
||||
|
||||
public static readonly StyledProperty<double> FastMaxTimeProperty =
|
||||
AvaloniaProperty.Register<Border, double>(nameof(FastMaxTime), 10);
|
||||
|
||||
public static readonly StyledProperty<double> AverageMaxTimeProperty =
|
||||
AvaloniaProperty.Register<Border, double>(nameof(AverageMaxTime), 50);
|
||||
|
||||
public static readonly StyledProperty<double> SlowMaxTimeProperty =
|
||||
AvaloniaProperty.Register<Border, double>(nameof(SlowMaxTime), 150);
|
||||
|
||||
public static readonly StyledProperty<double> SuperSlowMaxTimeProperty =
|
||||
AvaloniaProperty.Register<Border, double>(nameof(SuperSlowMaxTime), 500);
|
||||
RenderTargetBitmap _bitmap;
|
||||
ulong _clusterSize;
|
||||
ulong _maxBlocks;
|
||||
|
||||
public double SuperFastMaxTime
|
||||
{
|
||||
get => GetValue(SuperFastMaxTimeProperty);
|
||||
set => SetValue(SuperFastMaxTimeProperty, value);
|
||||
}
|
||||
|
||||
public double FastMaxTime
|
||||
{
|
||||
get => GetValue(FastMaxTimeProperty);
|
||||
set => SetValue(FastMaxTimeProperty, value);
|
||||
}
|
||||
|
||||
public double AverageMaxTime
|
||||
{
|
||||
get => GetValue(AverageMaxTimeProperty);
|
||||
set => SetValue(AverageMaxTimeProperty, value);
|
||||
}
|
||||
|
||||
public double SlowMaxTime
|
||||
{
|
||||
get => GetValue(SlowMaxTimeProperty);
|
||||
set => SetValue(SlowMaxTimeProperty, value);
|
||||
}
|
||||
|
||||
public double SuperSlowMaxTime
|
||||
{
|
||||
get => GetValue(SuperSlowMaxTimeProperty);
|
||||
set => SetValue(SuperSlowMaxTimeProperty, value);
|
||||
}
|
||||
|
||||
public IBrush SuperFastColor
|
||||
{
|
||||
get => GetValue(SuperFastColorProperty);
|
||||
set => SetValue(SuperFastColorProperty, value);
|
||||
}
|
||||
|
||||
public IBrush FastColor
|
||||
{
|
||||
get => GetValue(FastColorProperty);
|
||||
set => SetValue(FastColorProperty, value);
|
||||
}
|
||||
|
||||
public IBrush AverageColor
|
||||
{
|
||||
get => GetValue(AverageColorProperty);
|
||||
set => SetValue(AverageColorProperty, value);
|
||||
}
|
||||
|
||||
public IBrush SlowColor
|
||||
{
|
||||
get => GetValue(SlowColorProperty);
|
||||
set => SetValue(SlowColorProperty, value);
|
||||
}
|
||||
|
||||
public IBrush SuperSlowColor
|
||||
{
|
||||
get => GetValue(SuperSlowColorProperty);
|
||||
set => SetValue(SuperSlowColorProperty, value);
|
||||
}
|
||||
|
||||
public IBrush ProblematicColor
|
||||
{
|
||||
get => GetValue(ProblematicColorProperty);
|
||||
set => SetValue(ProblematicColorProperty, value);
|
||||
}
|
||||
|
||||
public ulong Blocks
|
||||
{
|
||||
get => GetValue(BlocksProperty);
|
||||
set => SetValue(BlocksProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged<T>([NotNull] AvaloniaPropertyChangedEventArgs<T> e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
|
||||
switch(e.Property.Name)
|
||||
{
|
||||
case nameof(Blocks):
|
||||
if(_maxBlocks == 0)
|
||||
_maxBlocks = (ulong)(Width / BLOCK_SIZE * (Height / BLOCK_SIZE));
|
||||
|
||||
if(Blocks > _maxBlocks)
|
||||
{
|
||||
_clusterSize = Blocks / _maxBlocks;
|
||||
|
||||
if(Blocks % _maxBlocks > 0)
|
||||
_clusterSize++;
|
||||
|
||||
if(Blocks / _clusterSize < _maxBlocks)
|
||||
{
|
||||
_maxBlocks = Blocks / _clusterSize;
|
||||
|
||||
if(Blocks % _clusterSize > 0)
|
||||
_maxBlocks++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_clusterSize = 1;
|
||||
_maxBlocks = Blocks;
|
||||
}
|
||||
|
||||
CreateBitmap();
|
||||
DrawGrid();
|
||||
RedrawAll();
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
|
||||
|
||||
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):
|
||||
|
||||
CreateBitmap();
|
||||
DrawGrid();
|
||||
RedrawAll();
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
if((int?)_bitmap?.Size.Height != (int)Height ||
|
||||
(int?)_bitmap?.Size.Width != (int)Width)
|
||||
{
|
||||
_maxBlocks = (ulong)(Width / BLOCK_SIZE * (Height / BLOCK_SIZE));
|
||||
CreateBitmap();
|
||||
}
|
||||
|
||||
context.DrawImage(_bitmap, new Rect(0, 0, Width, Height), new Rect(0, 0, Width, Height),
|
||||
BitmapInterpolationMode.HighQuality);
|
||||
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
|
||||
base.Render(context);
|
||||
}
|
||||
|
||||
protected override void ItemsCollectionChanged(object sender, [NotNull] NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
base.ItemsCollectionChanged(sender, e);
|
||||
|
||||
switch(e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
{
|
||||
if(e.NewItems is not {} items)
|
||||
throw new ArgumentException(UI.Invalid_list_of_items);
|
||||
|
||||
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
|
||||
using var ctx = new DrawingContext(ctxi, false);
|
||||
|
||||
foreach(object item in items)
|
||||
{
|
||||
if(item is not ValueTuple<ulong, double> block)
|
||||
throw new ArgumentException(UI.Invalid_item_in_list, nameof(Items));
|
||||
|
||||
DrawCluster(block.Item1, block.Item2, false, ctx);
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
|
||||
|
||||
break;
|
||||
}
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
{
|
||||
if(e.NewItems is not {} newItems ||
|
||||
e.OldItems is not {} oldItems)
|
||||
throw new ArgumentException(UI.Invalid_list_of_items);
|
||||
|
||||
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
|
||||
using var ctx = new DrawingContext(ctxi, false);
|
||||
|
||||
foreach(object item in oldItems)
|
||||
{
|
||||
if(item is not ValueTuple<ulong, double> block)
|
||||
throw new ArgumentException(UI.Invalid_item_in_list, nameof(Items));
|
||||
|
||||
DrawCluster(block.Item1, block.Item2, false, ctx);
|
||||
}
|
||||
|
||||
foreach(object item in newItems)
|
||||
{
|
||||
if(item is not ValueTuple<ulong, double> block)
|
||||
throw new ArgumentException(UI.Invalid_item_in_list, nameof(Items));
|
||||
|
||||
DrawCluster(block.Item1, block.Item2, false, ctx);
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
|
||||
|
||||
break;
|
||||
}
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
CreateBitmap();
|
||||
DrawGrid();
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
|
||||
|
||||
break;
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
void RedrawAll()
|
||||
{
|
||||
if(Items is null)
|
||||
return;
|
||||
|
||||
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
|
||||
using var ctx = new DrawingContext(ctxi, false);
|
||||
|
||||
foreach(object item in Items)
|
||||
{
|
||||
if(item is not ValueTuple<ulong, double> block)
|
||||
throw new ArgumentException(UI.Invalid_item_in_list, nameof(Items));
|
||||
|
||||
DrawCluster(block.Item1, block.Item2, false, ctx);
|
||||
}
|
||||
|
||||
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(UI.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)
|
||||
{
|
||||
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
|
||||
ctx = new DrawingContext(ctxi, false);
|
||||
}
|
||||
|
||||
ctx.FillRectangle(brush, new Rect(x, y, BLOCK_SIZE, BLOCK_SIZE));
|
||||
ctx.DrawRectangle(pen, new Rect(x, y, BLOCK_SIZE, BLOCK_SIZE));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
if(newContext)
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
protected override void ItemsChanged([NotNull] AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if(e.NewValue != null &&
|
||||
e.NewValue is not IList<(ulong, double)>)
|
||||
throw new ArgumentException(UI.Items_must_be_a_IList_ulong_double);
|
||||
|
||||
base.ItemsChanged(e);
|
||||
|
||||
CreateBitmap();
|
||||
DrawGrid();
|
||||
RedrawAll();
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
void CreateBitmap()
|
||||
{
|
||||
if(_maxBlocks == 0)
|
||||
_maxBlocks = (ulong)(Width / BLOCK_SIZE * (Height / BLOCK_SIZE));
|
||||
|
||||
_bitmap?.Dispose();
|
||||
|
||||
_bitmap = new RenderTargetBitmap(new PixelSize((int)Width, (int)Height), new Vector(96, 96));
|
||||
|
||||
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
|
||||
using var ctx = new DrawingContext(ctxi, false);
|
||||
|
||||
ctx.FillRectangle(Background, new Rect(0, 0, Width, Height));
|
||||
}
|
||||
|
||||
void DrawGrid()
|
||||
{
|
||||
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
|
||||
using var ctx = new DrawingContext(ctxi, false);
|
||||
|
||||
ulong clustersPerRow = (ulong)Width / BLOCK_SIZE;
|
||||
|
||||
bool allBlocksDrawn = false;
|
||||
|
||||
for(ulong y = 0; y < Height && !allBlocksDrawn; y += BLOCK_SIZE)
|
||||
{
|
||||
for(ulong x = 0; x < Width; x += BLOCK_SIZE)
|
||||
{
|
||||
ulong currentBlockValue = (y * clustersPerRow / BLOCK_SIZE) + (x / BLOCK_SIZE);
|
||||
|
||||
if(currentBlockValue >= _maxBlocks ||
|
||||
currentBlockValue >= Blocks)
|
||||
{
|
||||
allBlocksDrawn = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
ctx.DrawRectangle(new Pen(Foreground), new Rect(x, y, BLOCK_SIZE, BLOCK_SIZE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSquares(Color[] colors, int borderWidth, int sideLength)
|
||||
{
|
||||
using IDrawingContextImpl ctxi = _bitmap.CreateDrawingContext(null);
|
||||
using var ctx = new DrawingContext(ctxi, false);
|
||||
|
||||
int squareWidth = (sideLength - (2 * borderWidth)) / colors.Length;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
|
||||
foreach(Color color in colors)
|
||||
{
|
||||
ctx.FillRectangle(new SolidColorBrush(color), new Rect(x, y, squareWidth, squareWidth));
|
||||
x += squareWidth + (2 * borderWidth);
|
||||
|
||||
if(x < sideLength)
|
||||
continue;
|
||||
|
||||
x = 0;
|
||||
y += squareWidth + (2 * borderWidth);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
|
||||
{
|
||||
if(Width < 1 ||
|
||||
Height < 1 ||
|
||||
double.IsNaN(Width) ||
|
||||
double.IsNaN(Height))
|
||||
{
|
||||
base.OnAttachedToLogicalTree(e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CreateBitmap();
|
||||
DrawGrid();
|
||||
|
||||
base.OnAttachedToLogicalTree(e);
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
|
||||
{
|
||||
_bitmap.Dispose();
|
||||
_bitmap = null;
|
||||
base.OnDetachedFromLogicalTree(e);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user