diff --git a/Aaru.Gui/Controls/BlockMap.axaml b/Aaru.Gui/Controls/BlockMap.axaml new file mode 100644 index 000000000..b473fe0fc --- /dev/null +++ b/Aaru.Gui/Controls/BlockMap.axaml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/Aaru.Gui/Controls/BlockMap.axaml.cs b/Aaru.Gui/Controls/BlockMap.axaml.cs new file mode 100644 index 000000000..f12b44f56 --- /dev/null +++ b/Aaru.Gui/Controls/BlockMap.axaml.cs @@ -0,0 +1,262 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : BlockMap.cs +// Author(s) : Natalia Portillo +// +// 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> + SectorDataProperty = + AvaloniaProperty + .Register>(nameof(SectorData)); + + public static readonly StyledProperty ScanBlockSizeProperty = + AvaloniaProperty.Register(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("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>(); + + if(_sectorData != null) + { + _sectorData.CollectionChanged += OnSectorDataChanged; + RedrawAll(); + } + } + else if(change.Property == ScanBlockSizeProperty) + { + _scanBlockSize = change.GetNewValue(); + 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 + ); + } + } +} \ No newline at end of file diff --git a/Aaru.Gui/Controls/BlockMap.cs b/Aaru.Gui/Controls/BlockMap.cs deleted file mode 100644 index e9aa769fe..000000000 --- a/Aaru.Gui/Controls/BlockMap.cs +++ /dev/null @@ -1,491 +0,0 @@ -// /*************************************************************************** -// Aaru Data Preservation Suite -// ---------------------------------------------------------------------------- -// -// Filename : BlockMap.cs -// Author(s) : Natalia Portillo -// -// 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 . -// -// ---------------------------------------------------------------------------- -// 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 BlocksProperty = - AvaloniaProperty.Register(nameof(Blocks)); - - public static readonly StyledProperty SuperFastColorProperty = - AvaloniaProperty.Register(nameof(SuperFastColor), Brushes.LightGreen); - - public static readonly StyledProperty FastColorProperty = - AvaloniaProperty.Register(nameof(FastColor), Brushes.Green); - - public static readonly StyledProperty AverageColorProperty = - AvaloniaProperty.Register(nameof(AverageColor), Brushes.DarkGreen); - - public static readonly StyledProperty SlowColorProperty = - AvaloniaProperty.Register(nameof(SlowColor), Brushes.Yellow); - - public static readonly StyledProperty SuperSlowColorProperty = - AvaloniaProperty.Register(nameof(SuperSlowColor), Brushes.Orange); - - public static readonly StyledProperty ProblematicColorProperty = - AvaloniaProperty.Register(nameof(ProblematicColor), Brushes.Red); - - public static readonly StyledProperty SuperFastMaxTimeProperty = - AvaloniaProperty.Register(nameof(SuperFastMaxTime), 3); - - public static readonly StyledProperty FastMaxTimeProperty = - AvaloniaProperty.Register(nameof(FastMaxTime), 10); - - public static readonly StyledProperty AverageMaxTimeProperty = - AvaloniaProperty.Register(nameof(AverageMaxTime), 50); - - public static readonly StyledProperty SlowMaxTimeProperty = - AvaloniaProperty.Register(nameof(SlowMaxTime), 150); - - public static readonly StyledProperty SuperSlowMaxTimeProperty = - AvaloniaProperty.Register(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([NotNull] AvaloniaPropertyChangedEventArgs 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 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 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 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 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); - } -} -*/ -