Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Мажирин Александр #252

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions cs/TagsCloudVisualization/Layouter/CircularCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using SkiaSharp;
using TagsCloudVisualization.PositionGenerator;

namespace TagsCloudVisualization.Layouter;

public class CircularCloudLayouter : ICircularCloudLayouter
{
private readonly IPositionGenerator positionGenerator;
private readonly List<SKRect> rectangles;

public CircularCloudLayouter(IPositionGenerator positionGenerator)
{
rectangles = new List<SKRect>();
this.positionGenerator = positionGenerator;
}

public SKRect PutNextRectangle(SKSize rectangleSize)
{
if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0)
throw new ArgumentException("Rectangle size must be positive", nameof(rectangleSize));

SKRect rectangle;

do
{
var centerOfRectangle = positionGenerator.GetNextPosition();
var rectanglePosition = new SKPoint(centerOfRectangle.X - rectangleSize.Width / 2,
centerOfRectangle.Y - rectangleSize.Height / 2);
rectangle = new SKRect(
rectanglePosition.X,
rectanglePosition.Y,
rectanglePosition.X + rectangleSize.Width,
rectanglePosition.Y + rectangleSize.Height);
} while (rectangles.Any(r => r.IntersectsWith(rectangle)));

rectangles.Add(rectangle);
return rectangle;
}

public SKRect[] GetRectangles()
{
return rectangles.ToArray();
}
}
10 changes: 10 additions & 0 deletions cs/TagsCloudVisualization/Layouter/ICircularCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using SkiaSharp;
using TagsCloudVisualization.PositionGenerator;

namespace TagsCloudVisualization.Layouter;

public interface ICircularCloudLayouter
{
SKRect PutNextRectangle(SKSize rectangleSize);
SKRect[] GetRectangles();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using SkiaSharp;

namespace TagsCloudVisualization.PositionGenerator;

public interface IPositionGenerator
{
SKPoint GetNextPosition();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using SkiaSharp;

namespace TagsCloudVisualization.PositionGenerator;

public class SpiralLayoutPositionGenerator : IPositionGenerator
{
private double angle;
private readonly SKPoint center;
private readonly double step;

public SpiralLayoutPositionGenerator(SKPoint center, double step = 0.01)
{
this.center = center;
this.step = step;
}

public SKPoint GetNextPosition()
{
var radius = step * angle;
var x = (float)(center.X + radius * Math.Cos(angle));
var y = (float)(center.Y + radius * Math.Sin(angle));

angle += step;

return new SKPoint(x, y);
}
}
34 changes: 34 additions & 0 deletions cs/TagsCloudVisualization/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using SkiaSharp;
using TagsCloudVisualization.Layouter;
using TagsCloudVisualization.PositionGenerator;
using TagsCloudVisualization.Renderer;

internal class Program
{
private static void Main()
{
Directory.CreateDirectory("results");

RenderCloud(GenerateRandomCloud(10), "results/cloud_10.png");
RenderCloud(GenerateRandomCloud(50), "results/cloud_50.png");
RenderCloud(GenerateRandomCloud(100), "results/cloud_100.png");
}

private static SKRect[] GenerateRandomCloud(int count)
{
var positionGenerator = new SpiralLayoutPositionGenerator(new SKPoint(500, 500));
var layouter = new CircularCloudLayouter(positionGenerator);
var rectangleSizes = Enumerable.Range(0, count)
.Select(_ => new SKSize(new Random().Next(10, 100), new Random().Next(10, 100)));
return rectangleSizes.Select(layouter.PutNextRectangle).ToArray();
}

private static void RenderCloud(SKRect[] rectangles, string path)
{
var renderer = new Renderer(new SKSize(1000, 1000));
renderer.DrawRectangles(rectangles);
var image = renderer.GetEncodedImage();
using var stream = File.OpenWrite(path);
image.SaveTo(stream);
}
}
11 changes: 11 additions & 0 deletions cs/TagsCloudVisualization/Renderer/IRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using SkiaSharp;

namespace TagsCloudVisualization.Renderer;

public interface IRenderer
{
void DrawRectangles(IEnumerable<SKRect> rectangles);

SKData GetEncodedImage();

}
49 changes: 49 additions & 0 deletions cs/TagsCloudVisualization/Renderer/Renderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using SkiaSharp;

namespace TagsCloudVisualization.Renderer;

public class Renderer : IRenderer
{
private readonly SKBitmap bitmap;
private readonly SKPaint paint;

public Renderer(SKSize size)
{
bitmap = new SKBitmap((int)size.Width, (int)size.Height);
paint = new SKPaint
{
Color = SKColors.Black,
IsStroke = true,
TextSize = 24
};
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.LightGray);
}

public void DrawRectangles(IEnumerable<SKRect> rectangles)
{
using var canvas = new SKCanvas(bitmap);
foreach (var rectangle in rectangles)
{
ValidateRectangle(rectangle);
canvas.DrawRect(rectangle, paint);
paint.Color = new SKColor((byte)(paint.Color.Red + 21), (byte)(paint.Color.Green + 43),
(byte)(paint.Color.Blue + 67));
}
}

private void ValidateRectangle(SKRect rectangle)
{
if (rectangle.Left < 0 || rectangle.Top < 0 || rectangle.Right > bitmap.Width ||
rectangle.Bottom > bitmap.Height)
throw new ArgumentException("Rectangle is out of bounds");
if (rectangle.Left >= rectangle.Right || rectangle.Top >= rectangle.Bottom)
throw new ArgumentException("Rectangle is invalid");
}

public SKData GetEncodedImage()
{
using var image = SKImage.FromBitmap(bitmap);
return image.Encode(SKEncodedImageFormat.Png, 100);
}
}
Binary file added cs/TagsCloudVisualization/Samples/cloud_10.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cs/TagsCloudVisualization/Samples/cloud_100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cs/TagsCloudVisualization/Samples/cloud_50.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions cs/TagsCloudVisualization/TagsCloudVisualization.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.88.9"/>
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9"/>
</ItemGroup>

</Project>
121 changes: 121 additions & 0 deletions cs/TagsCloudVisualizationTests/CircularCloudLayouterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using NUnit.Framework.Interfaces;
using TagsCloudVisualization.Layouter;
using TagsCloudVisualization.PositionGenerator;
using TagsCloudVisualization.Renderer;

namespace TagsCloudVisualizationTests;

[TestFixture]
public class CircularCloudLayouterTests
{
private CircularCloudLayouter layouter;
private static readonly SKPoint Center = new(500, 500);
private static readonly float Density = 0.7f;

[SetUp]
public void SetUp()
{
var positionGenerator = new SpiralLayoutPositionGenerator(Center);
layouter = new CircularCloudLayouter(positionGenerator);
}

[TearDown]
public void TearDown()
{
if (TestContext.CurrentContext.Result.Outcome.Status != TestStatus.Failed) return;


if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed)
{
var filename = "tests/layouter_" + TestContext.CurrentContext.Test.ID + ".png";
SaveImage(filename);
}
}

private void SaveImage(string filename)
{
var renderer = new Renderer(new SKSize(Center.X * 2, Center.Y * 2));
renderer.DrawRectangles(layouter.GetRectangles());
var imageData = renderer.GetEncodedImage();
Directory.CreateDirectory("tests");
var path = Path.Combine(Directory.GetCurrentDirectory(), filename);
using var stream = new FileStream(path, FileMode.Create);
imageData.SaveTo(stream);
Console.WriteLine($"Tag cloud visualization saved to file {path}");
}

private SKSize[] GetRandomSizes(int count)
{
var random = new Random();
var sizes = new SKSize[count];
for (var i = 0; i < count; i++)
{
var size = new SKSize(random.Next(10, 100), random.Next(10, 100));
sizes[i] = size;
}
return sizes;
}

[Test]
public void PutNextRectangle_ShouldThrowArgumentException_WhenSizeNotPositive()
{
Action action = () => layouter.PutNextRectangle(new SKSize(0, 100));
action.Should().Throw<ArgumentException>();
}

[Test]
public void PutNextRectangle_ShouldReturnRectangle()
{
var rectangle = layouter.PutNextRectangle(new SKSize(100, 100));

rectangle.Should().BeEquivalentTo(new SKRect(Center.X - 50, Center.Y - 50, Center.X + 50, Center.Y + 50));
}

[Test]
public void PutNextRectangle_ShouldNotIntersectRectangles()
{
var rectangles = new List<SKRect>();
for (var i = 0; i < 10; i++) rectangles.Add(layouter.PutNextRectangle(new SKSize(10, 10)));

for (var i = 0; i < rectangles.Count - 1; i++)
for (var j = i + 1; j < rectangles.Count; j++)
rectangles[i].IntersectsWith(rectangles[j]).Should().BeFalse();
}

[Test]
[Repeat(3)]
public void PutNextRectangle_ShouldGenerateDenseLayout()
{

var sizes = GetRandomSizes(150);
var rectangles = sizes.Select(size => layouter.PutNextRectangle(size)).ToArray();
var totalRectArea = rectangles.Sum(rect => rect.Width * rect.Height);
var boundingCircleRadius = rectangles.Max(DistanceToCenter);
var boundingCircleArea = Math.PI * boundingCircleRadius * boundingCircleRadius;
var density = totalRectArea / boundingCircleArea;
density.Should().BeGreaterOrEqualTo(Density);
}

[Test]
[Repeat(3)]
public void PutNextRectangle_ShouldPlaceRectanglesInCircle()
{
var sizes = GetRandomSizes(150);
var rectangles = sizes.Select(size => layouter.PutNextRectangle(size)).ToArray();


var presumedAverageSide = rectangles.Average(size => (size.Width + size.Height) / 2);
var totalAreaOfRectangles = rectangles.Sum(rect => rect.Width * rect.Height);
var circleRadius = Math.Sqrt(totalAreaOfRectangles / Density / Math.PI);
var expectedMaxDistanceFromCenter = circleRadius + presumedAverageSide / 2;
var maxDistanceFromCenter = (double)rectangles.Max(DistanceToCenter);

maxDistanceFromCenter.Should().BeLessOrEqualTo(expectedMaxDistanceFromCenter);
}

private static float DistanceToCenter(SKRect rect)
{
var rectCenter = new SKPoint(rect.MidX, rect.MidY);
return SKPoint.Distance(Center, rectCenter);
}
}
3 changes: 3 additions & 0 deletions cs/TagsCloudVisualizationTests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
global using NUnit.Framework;
global using FluentAssertions;
global using SkiaSharp;
Comment on lines +1 to +3

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В целом можно, но не рекомендуется. При появлении большего числа файлов имена зависимостей станут повторяться и станет неудобно указывать полные имена классов

Loading