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 2 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
6 changes: 3 additions & 3 deletions cs/TagsCloudVisualization/Layouter/CircularCloudLayouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ public class CircularCloudLayouter : ICircularCloudLayouter
private readonly IPositionGenerator positionGenerator;
private readonly List<SKRect> rectangles;

public CircularCloudLayouter(SKPoint center)
public CircularCloudLayouter(IPositionGenerator positionGenerator)
{
rectangles = [];
positionGenerator = new SpiralLayoutPositionGenerator(center);
rectangles = new List<SKRect>();
this.positionGenerator = positionGenerator;
}

public SKRect PutNextRectangle(SKSize rectangleSize)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using SkiaSharp;
using TagsCloudVisualization.PositionGenerator;

namespace TagsCloudVisualization.Layouter;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ namespace TagsCloudVisualization.PositionGenerator;

public interface IPositionGenerator
{
public SKPoint GetNextPosition();
SKPoint GetNextPosition();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@

namespace TagsCloudVisualization.PositionGenerator;

public class SpiralLayoutPositionGenerator(SKPoint center, double step = 0.01) : IPositionGenerator
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()
{
Expand Down
13 changes: 8 additions & 5 deletions cs/TagsCloudVisualization/Program.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
using SkiaSharp;
using TagsCloudVisualization.Layouter;
using TagsCloudVisualization.PositionGenerator;
using TagsCloudVisualization.Renderer;

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

RenderCloud(GenerateRandomCloud(10), "results/cloud_10.png");
RenderCloud(GenerateRandomCloud(50), "results/cloud_50.png");
Expand All @@ -16,7 +16,8 @@ private static void Main()

private static SKRect[] GenerateRandomCloud(int count)
{
var layouter = new CircularCloudLayouter(new SKPoint(500, 500));
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();
Expand All @@ -25,7 +26,9 @@ private static SKRect[] GenerateRandomCloud(int count)
private static void RenderCloud(SKRect[] rectangles, string path)
{
var renderer = new Renderer(new SKSize(1000, 1000));
renderer.CreateRectangles(rectangles);
renderer.CreateImage(path);
renderer.DrawRectangles(rectangles);
var image = renderer.GetEncodedImage();
using var stream = File.OpenWrite(path);
image.SaveTo(stream);
}
}
6 changes: 4 additions & 2 deletions cs/TagsCloudVisualization/Renderer/IRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace TagsCloudVisualization.Renderer;

public interface IRenderer
{
void CreateRectangles(SKRect[] rectangles);
void CreateImage(string path);
void DrawRectangles(IEnumerable<SKRect> rectangles);

SKData GetEncodedImage();

}
27 changes: 14 additions & 13 deletions cs/TagsCloudVisualization/Renderer/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,33 @@ public Renderer(SKSize size)
TextSize = 24
};
using var canvas = new SKCanvas(bitmap);
{
canvas.Clear(SKColors.LightGray);
}
canvas.Clear(SKColors.LightGray);
}

public void CreateRectangles(SKRect[] rectangles)
public void DrawRectangles(IEnumerable<SKRect> rectangles)
{
using var canvas = new SKCanvas(bitmap);
foreach (var rectangle in rectangles)
{
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");
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));
}
}

public void CreateImage(string path)
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);
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
using var stream = File.OpenWrite(path);
data.SaveTo(stream);
return image.Encode(SKEncodedImageFormat.Png, 100);
}
}
1 change: 0 additions & 1 deletion cs/TagsCloudVisualization/TagsCloudVisualization.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.2"/>
<PackageReference Include="SkiaSharp" Version="2.88.9"/>
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9"/>
</ItemGroup>
Expand Down
97 changes: 52 additions & 45 deletions cs/TagsCloudVisualizationTests/CircularCloudLayouterTests.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,59 @@
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()
{
layouter = new CircularCloudLayouter(new SKPoint(500, 500));
var positionGenerator = new SpiralLayoutPositionGenerator(Center);
layouter = new CircularCloudLayouter(positionGenerator);
}

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

if (!Directory.Exists("tests"))
Directory.CreateDirectory("tests");
var filename = "tests/layouter_" + TestContext.CurrentContext.Test.ID + ".png";
var renderer = new Renderer(new SKSize(1000, 1000));
renderer.CreateRectangles(layouter.GetRectangles());
renderer.CreateImage(filename);


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

Choose a reason for hiding this comment

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

if (!a) return
if (a) Foo()

Похоже, логика дублируется
А ещё имя файла можно унести в тот же метод сохранения, назвав как-нибудь SaveTestResultImage()

}

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 CircularCloudLayouter layouter;

[Test]
public void Constructor_ShouldCreateLayouter()
private SKSize[] GetRandomSizes(int count)
{
layouter.Should().NotBeNull();
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;

Choose a reason for hiding this comment

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

Этот метод можно сделать ленивым, добавив yield конструкцию. Учитываю, что везде дальше он именно в таком ключе и используется. Урок

}

[Test]
Expand All @@ -45,12 +64,11 @@ public void PutNextRectangle_ShouldThrowArgumentException_WhenSizeNotPositive()
}

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

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

[Test]
Expand All @@ -59,7 +77,7 @@ 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; i++)
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();
}
Expand All @@ -68,47 +86,36 @@ public void PutNextRectangle_ShouldNotIntersectRectangles()
[Repeat(3)]
public void PutNextRectangle_ShouldGenerateDenseLayout()
{
var rectangles = new List<SKRect>();
float totalRectArea = 0;
var random = new Random();
for (var i = 0; i < 100; i++)
{
var size = new SKSize(random.Next(10, 100), random.Next(10, 100));
var rect = layouter.PutNextRectangle(size);
rectangles.Add(rect);
totalRectArea += size.Width * size.Height;
}


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 * Math.Pow(boundingCircleRadius, 2);

var boundingCircleArea = Math.PI * boundingCircleRadius * boundingCircleRadius;
var density = totalRectArea / boundingCircleArea;
density.Should().BeGreaterOrEqualTo(0.7f);
density.Should().BeGreaterOrEqualTo(Density);
}

[Test]
[Repeat(3)]
public void PutNextRectangle_ShouldPlaceRectanglesInCircle()
{
var rectangles = new List<SKRect>();
var random = new Random();
for (var i = 0; i < 100; i++)
{
var size = new SKSize(random.Next(10, 100), random.Next(10, 100));
var rect = layouter.PutNextRectangle(size);
rectangles.Add(rect);
}
var sizes = GetRandomSizes(150);
var rectangles = sizes.Select(size => layouter.PutNextRectangle(size)).ToArray();


var maxDistanceFromCenter = rectangles.Max(DistanceToCenter);
const int expectedMaxDistance = 500;
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(expectedMaxDistance);
maxDistanceFromCenter.Should().BeLessOrEqualTo(expectedMaxDistanceFromCenter);
}

private static float DistanceToCenter(SKRect rect)
{
var center = new SKPoint(500, 500);
var rectCenter = new SKPoint(rect.MidX, rect.MidY);
return SKPoint.Distance(center, rectCenter);
return SKPoint.Distance(Center, rectCenter);
}
}
45 changes: 28 additions & 17 deletions cs/TagsCloudVisualizationTests/RendererTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,64 @@ namespace TagsCloudVisualizationTests;
[TestFixture]
public class RendererTests
{
private const string DefaultFileName = "image.png";
private Renderer renderer;

[SetUp]
public void SetUp()
{
render = new Renderer(new SKSize(100, 100));
renderer = new Renderer(new SKSize(100, 100));
}

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

if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed)
{
SaveImage();
}
}

if (!Directory.Exists("tests"))
Directory.CreateDirectory("tests");
private void SaveImage()
{
var filename = "tests/renderer_" + TestContext.CurrentContext.Test.ID + ".png";
render.CreateImage(filename);
Directory.CreateDirectory("tests");
var path = Path.Combine(Directory.GetCurrentDirectory(), filename);
using var stream = new FileStream(path, FileMode.Create);
var imageData = renderer.GetEncodedImage();
imageData.SaveTo(stream);
Console.WriteLine($"Attempted to save result to file {path}");


if (File.Exists("image.png"))
File.Delete("image.png");
}

private Renderer render;

[Test]
public void CreateImage_ShouldCreateImage()
{
render.CreateImage("image.png");
Assert.That(File.Exists("image.png"));
var image = renderer.GetEncodedImage();
using var stream = File.OpenWrite(DefaultFileName);
image.SaveTo(stream);

Assert.That(File.Exists(DefaultFileName));

Choose a reason for hiding this comment

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

Даже если нам вернётся пустое изображение, файл создастся. Давай лучше проверять, что возвращаемый объект не пуст

}

[TestCase(0, 200)]
[TestCase(-1, 50)]
public void CreateRectangles_ShouldThrowException_WhenRectanglesAreOutOfBounds(int topLeft, int bottomRight)
{
Assert.Throws<ArgumentException>(() =>
render.CreateRectangles([new SKRect(topLeft, topLeft, bottomRight, bottomRight)]));
var action = () => renderer.DrawRectangles([new SKRect(topLeft, topLeft, bottomRight, bottomRight)]);
action.Should().ThrowExactly<ArgumentException>();
}

[TestCase(0, 0, 0, 0)]
[TestCase(50, 50, 0, 0)]
[TestCase(50, 0, 50, 0)]
public void CreateRectangles_ShouldThrowException_WhenRectanglesAreInvalid(int topLeft, int topRight,
int bottomLeft, int bottomRight)
public void CreateRectangles_ShouldThrowException_WhenRectanglesAreInvalid(int left, int top,
int right, int bottom)
{
Assert.Throws<ArgumentException>(() =>
render.CreateRectangles([new SKRect(topLeft, topRight, bottomLeft, bottomRight)]));
renderer.DrawRectangles([new SKRect(left, top, right, bottom)]));
}
}
Loading