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

Брайденбихер Виктор Николаевич #238

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,5 @@ FakesAssemblies/

*Solved.cs

.idea/
.idea/
/cs/tdd.sln.DotSettings
43 changes: 43 additions & 0 deletions cs/TagsCloudVisualization/CloudClasses/CircularCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TagsCloudVisualization.CloudClasses.Interfaces;

namespace TagsCloudVisualization.CloudClasses
{
public class CircularCloudLayouter : ICloudLayouter
{
private readonly List<Rectangle> rectangles = new List<Rectangle>();

public readonly IRayMover RayMover;

public List<Rectangle> Rectangles => rectangles;

public CircularCloudLayouter(IRayMover rayMover)
{
RayMover = rayMover;
}

public Rectangle PutNextRectangle(Size rectangleSize)
{
if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0)
throw new ArgumentException("The height and width of the Rectangle must be greater than 0");

foreach (var point in RayMover.MoveRay())
{
var location = new Point(point.X - rectangleSize.Width / 2,
point.Y - rectangleSize.Height / 2);

var rectangle = new Rectangle(location, rectangleSize);

// Проверяем, пересекается ли новый прямоугольник с уже существующими
if (!rectangles.Any(r => r.IntersectsWith(rectangle)))
{
rectangles.Add(rectangle);
return rectangle;
}
}

throw new InvalidOperationException("No suitable location found for the rectangle.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Drawing;

namespace TagsCloudVisualization.CloudClasses.Interfaces
{
public interface ICloudLayouter
{
public Rectangle PutNextRectangle(Size rectangleSize);

public List<Rectangle> Rectangles { get; }
}
}
10 changes: 10 additions & 0 deletions cs/TagsCloudVisualization/CloudClasses/Interfaces/IRayMover.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Drawing;

namespace TagsCloudVisualization.CloudClasses.Interfaces
{
// Интерфейс для класса, который перемещает луч по спирали, генерируя последовательность точек.
public interface IRayMover
{
IEnumerable<Point> MoveRay();
}
}
35 changes: 35 additions & 0 deletions cs/TagsCloudVisualization/CloudClasses/Ray.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Drawing;

namespace TagsCloudVisualization.CloudClasses
{
public class Ray
{
public double Radius { get; private set; }

//В радианах
public double Angle { get; private set; }

public readonly Point StartPoint;

public Ray(Point startPoint, int radius, int angle)
{
StartPoint = startPoint;

Radius = radius;

Angle = angle;
}

public void Update(double deltaRadius, double deltaAngle)
{
Radius += deltaRadius;
Angle += deltaAngle;
}

public Point EndPoint => new Point
{
X = StartPoint.X + (int)(Radius * Math.Cos(Angle)),
Y = StartPoint.Y + (int)(Radius * Math.Sin(Angle))
};
}
}
44 changes: 44 additions & 0 deletions cs/TagsCloudVisualization/CloudClasses/SpiralRayMover.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Drawing;
using TagsCloudVisualization.CloudClasses.Interfaces;

namespace TagsCloudVisualization.CloudClasses
{
public class SpiralRayMover : IRayMover
{
private readonly Ray spiralRay;

private readonly double radiusStep;

//В радианах
private readonly double angleStep;

private const double OneRound = Math.PI * 2;

public SpiralRayMover(Point center, double radiusStep = 1, double angleStep = 5,
int startRadius = 0, int startAngle = 0)
{
if (center.X <= 0 || center.Y <= 0)
throw new ArgumentException("SpiralRayMover center Point should have positive X and Y");

if (radiusStep <= 0 || angleStep <= 0)
throw new ArgumentException("radiusStep and angleStep should be positive");

spiralRay = new Ray(center, startRadius, startAngle);
this.radiusStep = radiusStep;

//Преобразование из градусов в радианы
this.angleStep = angleStep * Math.PI / 180;
}

public IEnumerable<Point> MoveRay()
{
while (true)
{
yield return spiralRay.EndPoint;

//Радиус увеличивается на 1 только после полного прохождения круга
spiralRay.Update(radiusStep / OneRound * angleStep, angleStep);
}
}
}
}
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
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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions cs/TagsCloudVisualization/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Drawing;
using TagsCloudVisualization.CloudClasses;
using TagsCloudVisualization.CloudClasses.Interfaces;

namespace TagsCloudVisualization.Visualisation
{
internal class Program
{
private static void Main(string[] args)
{
using var tagCloudGenerator = new TagCloudImageGenerator(new Visualiser(Color.Black, 1));

var center = new Point(500, 500);
var bitmapSize = new Size(1000, 1000);

var firstSizes = GenerateRectangleSizes(40, 60, 80, 60, 80);
var secondSizes = GenerateRectangleSizes(100, 10, 40, 20, 30);
var thirdSizes = GenerateRectangleSizes(30, 80, 130, 80, 130);

var firstLayouter = SetupCloudLayout(center, firstSizes);
var secondLayouter = SetupCloudLayout(center, secondSizes);
var thirdLayouter = SetupCloudLayout(center, thirdSizes);

using var bitmap1 = tagCloudGenerator.CreateNewBitmap(bitmapSize, firstLayouter.Rectangles);
using var bitmap2 = tagCloudGenerator.CreateNewBitmap(bitmapSize, secondLayouter.Rectangles);
using var bitmap3 = tagCloudGenerator.CreateNewBitmap(bitmapSize, thirdLayouter.Rectangles);

BitmapSaver.SaveToCorrect(bitmap1, "bitmap_1.png");
BitmapSaver.SaveToCorrect(bitmap2, "bitmap_2.png");
BitmapSaver.SaveToCorrect(bitmap3, "bitmap_3.png");
}

private static IEnumerable<Size> GenerateRectangleSizes(int count, int minWidth, int maxWidth, int minHeight, int maxHeight)
{
var sizeBuilder = SizeBuilder.Configure()
.SetCount(count)
.SetWidth(minWidth, maxWidth)
.SetHeight(minHeight, maxHeight)
.Generate();

return sizeBuilder;
}

private static CircularCloudLayouter SetupCloudLayout(Point center, IEnumerable<Size> sizes)
{
var layouter = new CircularCloudLayouter(new SpiralRayMover(center));

sizes.ToList()
.ForEach(size => layouter.PutNextRectangle(size));

return layouter;
}
}
}
32 changes: 32 additions & 0 deletions cs/TagsCloudVisualization/TagsCloudVisualization.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<StartupObject>TagsCloudVisualization.Visualisation.Program</StartupObject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
</ItemGroup>

<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>

<ItemGroup>
<Folder Include="CorrectImages\" />
<Folder Include="FailImages\" />
</ItemGroup>

</Project>
150 changes: 150 additions & 0 deletions cs/TagsCloudVisualization/Tests/CircularCloudLayouterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using FluentAssertions;
using NUnit.Framework.Interfaces;
using System.Drawing;
using TagsCloudVisualization.CloudClasses;
using TagsCloudVisualization.Visualisation;

namespace TagsCloudVisualization.Tests

Choose a reason for hiding this comment

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

Я бы разделил на два проекта: текущий, где будет находится вся логика и проект где будут тесты на логику.
Это сделает код более понятным и удобным - логика остается чистой, а тесты находятся отдельно и не мешают.

Choose a reason for hiding this comment

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

Плюс артефакты из тестов, будут складываться рядом с тестами, а не посреди логики

Copy link
Author

Choose a reason for hiding this comment

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

Я первоначально так и хотел, но когда я прочитал формулировку задания в репозитории
"Сделай форк этого репозитория. Добавь проект TagsCloudVisualization. Выполняй задания в этом проекте."
Это заставило меня передумать и создать один проект, но я понимаю, что лучше было бы это разделить

Choose a reason for hiding this comment

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

Понял, немного от формулировки задания, можно отходить если считаешь как можно сделать лучше

{
public class CircularCloudLayouterTests
{
private const int WIDTH = 1000;

private const int HEIGHT = 1000;

private CircularCloudLayouter _layouter;

private Point _center;

private IVisualiser _visualiser;

private TagCloudImageGenerator _tagCloudImageGenerator;

private Size _errorImageSize;

[SetUp]
public void Setup()
{
_center = new Point(WIDTH / 2, HEIGHT / 2);

var mover = new SpiralRayMover(_center);

_layouter = new CircularCloudLayouter(mover);

_visualiser = new Visualiser(Color.Black, 1);

_tagCloudImageGenerator = new TagCloudImageGenerator(_visualiser);

_errorImageSize = new Size(WIDTH, HEIGHT);
}

[TearDown]
public void Teardown()
{
if (TestContext.CurrentContext.Result.Outcome == ResultState.Failure)
{
using var errorBitmap = _tagCloudImageGenerator.CreateNewBitmap(_errorImageSize, _layouter.Rectangles);

var fileName = $"{TestContext.CurrentContext.Test.MethodName + Guid.NewGuid()}.png";
BitmapSaver.SaveToFail(errorBitmap, fileName);
}

_visualiser.Dispose();
_tagCloudImageGenerator.Dispose();
}

[Test]
public void CircularCloudLayouter_WhenCreated_RectanglesShoudBeEmpty()
{
_layouter.Rectangles.Should().BeEmpty();
}

[Test]
public void CircularCloudLayouter_WhenCreated_FirstPointEqualsCenter()
{
var firstPoint = _layouter.RayMover.MoveRay().First();

firstPoint.Should().BeEquivalentTo(_center);
}

[Test]
public void CircularCloudLayouter_WhenAddFirstRectangle_ContainOneAndSameRectangle()
{
var rectangle = _layouter.PutNextRectangle(new Size(20, 10));

_layouter.Rectangles
.Select(r => r).Should().HaveCount(1).And.Contain(rectangle);
}

[Test]
public void CircularCloudLayouter_WhenAddRectangle_ReturnsRectangleWithSameSize()
{
var givenSize = new Size(20, 20);

var rectangle = _layouter.PutNextRectangle(givenSize);

rectangle.Size.Should().BeEquivalentTo(givenSize);
}

[TestCase(0, 0)]
[TestCase(-2, 2)]
[TestCase(2, -2)]
[TestCase(-2, -2)]
public void CircularCloudLayouter_WhenWrongSize_ThrowArgumentException(int width, int height)
{
Assert.Throws<ArgumentException>(() => _layouter.PutNextRectangle(new Size(width, height)),
"Not valid size should be positive");
}

[Test]
public void CircularCloudLayouter_WhenAddFew_ShouldHaveSameCount()
{
var sizesList = GenerateSizes(5);
AddRectanglesToLayouter(sizesList);

_layouter.Rectangles.Should().HaveCount(5);
}

[TestCase(1, 2, 3)]
public void CircularCloudLayouter_ShouldIncreaseRectangleCountCorrectly(int countBefore, int add, int countAfter)
{
var countBeforeSizes = GenerateSizes(countBefore);
AddRectanglesToLayouter(countBeforeSizes);

var addSizes = GenerateSizes(add);
AddRectanglesToLayouter(addSizes);

_layouter.Rectangles.Should().HaveCount(countAfter);
}

[TestCase(2)]
[TestCase(30)]
public void CircularCloudLayouter_WhenAddFew_RectangleNotIntersectsWithOtherRectangles(int count)
{
var listSizes = GenerateSizes(count);
AddRectanglesToLayouter(listSizes);

foreach (var rectangle in _layouter.Rectangles)
{
_layouter.Rectangles
.Any(r => r.IntersectsWith(rectangle) && rectangle != r)
.Should().BeFalse();
}
}

private List<Size> GenerateSizes(int count)
{
return SizeBuilder.Configure()
.SetCount(count)
.SetWidth(60, 80)
.SetHeight(60, 80)
.Generate()
.ToList();
}

private void AddRectanglesToLayouter(List<Size> sizes)
{
sizes.ForEach(size => _layouter.PutNextRectangle(size));
}
}
}
Loading