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

базовая реализация структуры, алгоритма распределения и визуализации #247

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions cs/TagsCloudVisualization/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Drawing;
using TagsCloudVisualization.TagCloud;

var l = new CircularCloudLayouter(new Point(250,250),new CircularPositionCalculator(new Point(250,250)));
for (int i = 0; i < 100; i++)
{
var rand = new Random();
l.PutNextRectangle(new Size(rand.Next(10,70),rand.Next(10,70)));
}

var drawer = new BaseCloudDrawer();
var bmp = drawer.DrawCloud(l.Rectangles,500,500);
drawer.SaveToFile(bmp);
21 changes: 21 additions & 0 deletions cs/TagsCloudVisualization/TagCloud/CircularCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Drawing;

namespace TagsCloudVisualization.TagCloud;

public class CircularCloudLayouter(Point center, IPositionCalculator calculator)
{
public List<Rectangle> Rectangles { get; private set; } = [];

public Rectangle PutNextRectangle(Size rectangleSize)
{
if (rectangleSize.Width <= 0)
throw new ArgumentException("Size width must be positive number");
if (rectangleSize.Height <= 0)
throw new ArgumentException("Size height must be positive number");

var temp = calculator.CalculateNextPosition(rectangleSize)
.First(rectangle => calculator.IsRectanglePositionValid(Rectangles, rectangle));
Rectangles.Add(temp);
return temp;
}
}
34 changes: 34 additions & 0 deletions cs/TagsCloudVisualization/TagCloud/CloudDrawer/BaseCloudDrawer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;

namespace TagsCloudVisualization.TagCloud;

public class BaseCloudDrawer : ICloudDrawer
{
public Bitmap DrawCloud(List<Rectangle> rectangles, int imageWidth, int imageHeight)
{
if (imageWidth <= 0)
throw new ArgumentException("Width must be positive number");
if (imageHeight <= 0)
throw new ArgumentException("Height must be positive number");
var bitmap = new Bitmap(imageWidth, imageHeight);
var graphics = Graphics.FromImage(bitmap);
graphics.Clear(Color.White);
foreach (var rectangle in rectangles)
{
graphics.DrawRectangle(new Pen(Color.Black), rectangle);
}

return bitmap;
}

public void SaveToFile(Bitmap bitmap, string? fileName = null, string? path = null, ImageFormat format = null)
{
path ??= Environment.CurrentDirectory;
fileName ??= DateTime.Now.ToOADate().ToString(CultureInfo.InvariantCulture);
format ??= ImageFormat.Png;
var fullPath = Path.Combine(path, fileName + ".png");
bitmap.Save(fullPath);
}
}
10 changes: 10 additions & 0 deletions cs/TagsCloudVisualization/TagCloud/CloudDrawer/ICloudDrawer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Drawing;
using System.Drawing.Imaging;

namespace TagsCloudVisualization.TagCloud;

public interface ICloudDrawer
{
public Bitmap DrawCloud(List<Rectangle> rectangles, int imageWidth, int imageHeight);
public void SaveToFile(Bitmap bitmap, string? fileName, string? path, ImageFormat format);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Drawing;

namespace TagsCloudVisualization.TagCloud;

public class CircularPositionCalculator(Point center, double offsetDelta = 2.0, double angleDelta = 0.1) : IPositionCalculator
{
private double currentAngle = 0.0;
private double currentOffset = 0.0;
private const double fullRoundAngle = Math.PI * 2;

public IEnumerable<Rectangle> CalculateNextPosition (Size nextRectangleSize)
{
while (true)
{
var newRectangle = MakeRectangleFromSize(nextRectangleSize);
currentAngle += angleDelta;
if (currentAngle >= fullRoundAngle)
{
currentAngle = 0;
currentOffset += offsetDelta;
}

yield return newRectangle;
}
}

private Rectangle MakeRectangleFromSize(Size nextRectangleSize)

Choose a reason for hiding this comment

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

По-хорошему логика с прямоугольниками должна была лежать в CircularCloudLayouter. Классу CircularPositionCalculator по идее не нужно ничего знать о прямоугольниках. либо о каких-то других фигурах, которые мы захотим размещать на окружностях. Если отталкиваться от S в SOLID, то ответственность класса CircularPositionCalculator заключается в нахождении следующей точки, на которую потенциально можно поставить прямоугольник или любую другую фигуру, а сами фигуры и операции над ними должны лежать на уровень выше - в CircularCloudLayouter.

Текущая реализация создает несколько проблем:

  1. При добавлении другого алгоритма расстановки фигур нам потребуется не просто алгоритм подсчета точки написать в новом классе, но и продублировать всю логику с прямоугольниками.
  2. Если мы захотим размещать, допустим, ромбы, а не прямоугольники, то нам придется вносить изменения прямо в CircularPositionCalculator, т.к. он сильно завязан на сами фигуры.
  3. Покрывать тестами этот класс сложнее, чем если бы он отвечал только за получение точек.

{
var x = (int)(center.X + currentOffset * Math.Cos(currentAngle));
var y = (int)(center.Y + currentOffset * Math.Sin(currentAngle));
var newRectangle = new Rectangle(new Point(x, y), nextRectangleSize);
return newRectangle;
}

public bool IsRectanglePositionValid(List<Rectangle> rectangles, Rectangle currentRectangle)
{
return !rectangles.Any(r => r.IntersectsWith(currentRectangle));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Drawing;

namespace TagsCloudVisualization.TagCloud;

public interface IPositionCalculator
{
public IEnumerable<Rectangle> CalculateNextPosition(Size nextRectangleSize);
public bool IsRectanglePositionValid(List<Rectangle> rectangles, Rectangle currentRectangle);
}
14 changes: 14 additions & 0 deletions cs/TagsCloudVisualization/TagsCloudVisualization.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="9.0.0-rc.2.24474.1" />
</ItemGroup>

</Project>
115 changes: 115 additions & 0 deletions cs/TagsCloudVisualizationTest/BaseCloudDrawer_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using NUnit.Framework.Interfaces;

namespace BaseCloudDrawer_Tests;

[TestFixture]
public class BaseCloudDrawer_Tests

Choose a reason for hiding this comment

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

На классы, которые формируются какие-то файлы, лучше всего писать Approval-тесты, обычные unit тут не очень хорошо подходят

{
private BaseCloudDrawer drawer;
private string testDirectory;

[SetUp]
public void SetUp()
{
drawer = new BaseCloudDrawer();
testDirectory = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestImages");
Directory.CreateDirectory(testDirectory);
}

[TearDown]
public void TearDown()
{
var status = TestContext.CurrentContext.Result.Outcome.Status;
var files = Directory.GetFiles(testDirectory);

if (status == TestStatus.Passed &&
files.Length > 0) //удаляем в случае успешного прохождения, пишем в консоль при падении теста
{
File.Delete(files.OrderByDescending(f => new FileInfo(f).CreationTime).First());
}
else if (status == TestStatus.Failed)
{
Console.WriteLine($"Test failed. Image saved to {testDirectory}/{TestContext.CurrentContext.Test.Name}");
}
}

[Test]
public void DrawCloud_ThrowsArgumentException_WhenWidthIsNonPositive()
{
var rectangles = new List<Rectangle> { new Rectangle(0, 0, 10, 10) };

Action act = () => drawer.DrawCloud(rectangles, 0, 100);

act.Should().Throw<ArgumentException>().WithMessage("Width must be positive number");
}

[Test]
public void DrawCloud_ThrowsArgumentException_WhenHeightIsNonPositive()
{
var rectangles = new List<Rectangle> { new Rectangle(0, 0, 10, 10) };

Action act = () => drawer.DrawCloud(rectangles, 100, 0);

act.Should().Throw<ArgumentException>().WithMessage("Height must be positive number");
}


[Test]
public void DrawCloud_ReturnsBitmapWithCorrectDimensions()
{
var rectangles = new List<Rectangle> { new Rectangle(0, 0, 10, 10) };
var imageWidth = 100;
var imageHeight = 100;

var bitmap = drawer.DrawCloud(rectangles, imageWidth, imageHeight);

var fileName = TestContext.CurrentContext.Test.Name;
var format = ImageFormat.Png;
drawer.SaveToFile(bitmap, fileName, testDirectory, format);
bitmap.Width.Should().Be(imageWidth);
bitmap.Height.Should().Be(imageHeight);
}

[Test]
public void DrawCloud_DrawsAllRectangles()
{
var rectangles = new List<Rectangle>
{
new Rectangle(0, 0, 10, 10),
new Rectangle(20, 20, 10, 10)
};
var imageWidth = 100;
var imageHeight = 100;

var bitmap = drawer.DrawCloud(rectangles, imageWidth, imageHeight);

using (var graphics = Graphics.FromImage(bitmap))
{
foreach (var rectangle in rectangles)
{
var pixelColor = bitmap.GetPixel(rectangle.X + 1, rectangle.Y + 1);
pixelColor.Should().NotBe(Color.White);
}
}

var fileName = TestContext.CurrentContext.Test.Name;
var format = ImageFormat.Png;
drawer.SaveToFile(bitmap, fileName, testDirectory, format);
}

[Test]
public void SaveToFile_SavesBitmapToSpecifiedPath()
{
var bitmap = new Bitmap(10, 10);
var fileName = "test_image";
var format = ImageFormat.Png;

drawer.SaveToFile(bitmap, fileName, testDirectory, format);

Choose a reason for hiding this comment

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

На этот метод тесты можно было не писать, т.к. он внутри себя не содержит какой-то логики, а просто вызывает стандартный метод из шарповой библиотеки, тесты на который должны писать разработчики этой библиотеки. Мы, если ее используем, ей доверяем и дополнительные тесты можем не писать


var fullPath = Path.Combine(testDirectory, fileName + ".png");
File.Exists(fullPath).Should().BeTrue();
}
}
61 changes: 61 additions & 0 deletions cs/TagsCloudVisualizationTest/CircularCloudLayouter_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
namespace TagsCloudVisualizationTest;

public class CircularCloudLayouter_Tests
{
[TestFixture]
public class CircularCloudLayouterTests
{
private CircularCloudLayouter layouter;
private IPositionCalculator calculator;
private Point center;

[SetUp]
public void SetUp()
{
center = new Point(0, 0);
calculator = new CircularPositionCalculator(center);
layouter = new CircularCloudLayouter(center, calculator);
}

[Test]
public void PutNextRectangle_ThrowsArgumentException_WhenWidthIsNonPositive()
{
var size = new Size(0, 10);

Action act = () => layouter.PutNextRectangle(size);

act.Should().Throw<ArgumentException>().WithMessage("Size width must be positive number");
}

[Test]
public void PutNextRectangle_ThrowsArgumentException_WhenHeightIsNonPositive()
{
var size = new Size(10, 0);

Action act = () => layouter.PutNextRectangle(size);

act.Should().Throw<ArgumentException>().WithMessage("Size height must be positive number");
}

[Test]
public void PutNextRectangle_AddsRectangleToList_WhenValidSize()
{
var size = new Size(10, 10);

var result = layouter.PutNextRectangle(size);

layouter.Rectangles.Should().Contain(result);
}

[Test]
public void PutNextRectangle_ReturnsNonIntersectingRectangle_WhenRectanglesExist()
{
var size = new Size(10, 10);
layouter.PutNextRectangle(size);

var result = layouter.PutNextRectangle(size);

result.IntersectsWith(layouter.Rectangles[0]).Should().BeFalse();

Choose a reason for hiding this comment

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

Мы для тестов поле Rectangles сделали публичным, что не очень хорошо, т.к. оно стал публичным не только для тестов, но и для любого пользователя нашего класса. Я бы тут предложил самостоятельно складывать прямоугольники в коллекцию в самом тесте и проверять в конце, что они не пересекаются.

}
}
}
5 changes: 5 additions & 0 deletions cs/TagsCloudVisualizationTest/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Global using directives

global using System.Drawing;
global using FluentAssertions;
global using TagsCloudVisualization.TagCloud;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading