-
Notifications
You must be signed in to change notification settings - Fork 307
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
base: master
Are you sure you want to change the base?
Changes from all commits
2e2ace4
88ed57d
ed697d1
6268d51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); |
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; | ||
} | ||
} |
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); | ||
} | ||
} |
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) | ||
{ | ||
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); | ||
} |
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> |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. На этот метод тесты можно было не писать, т.к. он внутри себя не содержит какой-то логики, а просто вызывает стандартный метод из шарповой библиотеки, тесты на который должны писать разработчики этой библиотеки. Мы, если ее используем, ей доверяем и дополнительные тесты можем не писать |
||
|
||
var fullPath = Path.Combine(testDirectory, fileName + ".png"); | ||
File.Exists(fullPath).Should().BeTrue(); | ||
} | ||
} |
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Мы для тестов поле Rectangles сделали публичным, что не очень хорошо, т.к. оно стал публичным не только для тестов, но и для любого пользователя нашего класса. Я бы тут предложил самостоятельно складывать прямоугольники в коллекцию в самом тесте и проверять в конце, что они не пересекаются. |
||
} | ||
} | ||
} |
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; |
There was a problem hiding this comment.
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.
Текущая реализация создает несколько проблем: