Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
68 changes: 68 additions & 0 deletions cs/TagsCloudVisualization/CircularCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Drawing;

namespace TagsCloudVisualization;

public class CircularCloudLayouter
{
private List<Rectangle> Rects { get; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rects -> Rectangles


private Size imageSize;

private readonly PointGenerator pointGenerator;

private readonly Random random = new Random();

public CircularCloudLayouter(Point center, int imageWidth = 1920, int imageHeight = 1080)
{
if (imageWidth <= 0 || imageHeight <= 0)
throw new ArgumentException("Image width or height must be positive");
if (center.X <= 0 || center.Y <= 0)
throw new ArgumentException("Center should be greater than 0");
if (center.X >= imageWidth || center.Y >= imageHeight)
throw new ArgumentException("Center must be inside image bounds");
Rects = new List<Rectangle>();
imageSize = new Size(imageWidth, imageHeight);
pointGenerator = new PointGenerator(center, imageSize);
}


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

var points = pointGenerator.GetPointsOnSpiral();
foreach (var point in points)
{
var rectangle = new Rectangle(point, rectangleSize);
if (Rects.Any(rectangle.IntersectsWith))
continue;
Rects.Add(rectangle);
return rectangle;
}

throw new InvalidOperationException($"Failed to find place for the {Rects.Count} rectangle");
}

public IEnumerable<Rectangle> GetCircularCloudRectangles(int count, Size rectangleSize, double coefficient = 0)
{
if (count <= 0)
throw new ArgumentException("Count must be positive");
if (coefficient == 0)
{
for (var i = 0; i < count; i++)
yield return PutNextRectangle(rectangleSize);
yield break;
}

if (coefficient is < 0 or > 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Что за коэффициент? Из названия не понятно?

throw new ArgumentException("Coefficient must be between 0 and 1");
for (var i = 0; i < count; i++)
{
var rnd = random.NextDouble() + coefficient;
var rndWidth = (int)Math.Round(rectangleSize.Width * rnd);
var rndHeight = (int)Math.Round(rectangleSize.Height * rnd);
yield return PutNextRectangle(new Size(rndWidth, rndHeight));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Лучше вынести из лейаутера во внешний провайдер прямоугольников (или позиций прямоугольников)

}
Binary file added cs/TagsCloudVisualization/Image/cloud1.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/Image/cloud2.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/ImageSaver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.Diagnostics;

namespace TagsCloudVisualization;

public static class ImageSaver
{
public static void ImageSave(Bitmap bitmap, string fileName)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ImageSave -> SaveImag

{
var projectDir = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
var imagesDir = Path.Combine(projectDir, "Image");
var path = Path.Combine(imagesDir, fileName);
bitmap.Save(path, ImageFormat.Png);
}
}
39 changes: 39 additions & 0 deletions cs/TagsCloudVisualization/PointGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Collections;
using System.Drawing;

namespace TagsCloudVisualization;

public class PointGenerator

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Необходимо вынести интерфейс

{
private readonly Point center;
private readonly Size imageSize;

public PointGenerator(Point center, Size imageSize)
{
if (center.X < 0 || center.Y < 0)
throw new ArgumentException("Center coordinates must be non-negative");
if (imageSize.Width <= 0 || imageSize.Height <= 0)
throw new ArgumentException("Image size must be positive");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Эти проверки повторяются в двух местах, может их лучше оставить только там, где эти параметры используются?

this.center = center;
this.imageSize = imageSize;
}

public IEnumerable<Point> GetPointsOnSpiral(double angle = double.Pi / 24, double radius = 10)
{
if (radius <= 0)
throw new ArgumentException("Radius must be positive");

var currentAngle = 0.0;
var currentRadius = 0.0;
var x = 0;
var y = 0;
while (x < imageSize.Width || y < imageSize.Height)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Точно ли мы тут должны идти по ширине и высоте изображения? А если мы захотим отрисовать все прямоугольники, а потом определить ширину и высоту изображения, по крайним точкам получившегося круга?

{
var vector = radius * currentAngle / (2 * Math.PI);
x = center.X + (int)Math.Round(Math.Cos(currentAngle) * vector);
y = center.Y + (int)Math.Round(Math.Sin(currentAngle) * vector);
yield return new Point(x, y);
currentAngle += angle;
}
}
}
20 changes: 20 additions & 0 deletions cs/TagsCloudVisualization/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Drawing;


namespace TagsCloudVisualization;

public static class Program
{
static void Main()
{
var imageCenter = new Point(1920/2, 1080/2);
var rectangleSize = new Size(200, 85);
var imageSize = new Size(1920, 1080);

var circularCloudLayouter = new CircularCloudLayouter(imageCenter);
var rectangles = circularCloudLayouter.GetCircularCloudRectangles(40,rectangleSize, 0.5);
var visualizer = new Render(imageSize);
var image = visualizer.CreateRectangleCloud(rectangles);
ImageSaver.ImageSave(image, "cloud.png");
}
}
2 changes: 2 additions & 0 deletions cs/TagsCloudVisualization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
![cloud1.png](Image/cloud1.png)
![cloud2.png](Image/cloud2.png)
19 changes: 19 additions & 0 deletions cs/TagsCloudVisualization/Render.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Drawing;

namespace TagsCloudVisualization;

public class Render(Size imageSize)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Render -> Renderer или TagCloudRenderer. Необходимо вынести интерфейс ICloudRenderer

{
public Bitmap CreateRectangleCloud(IEnumerable<Rectangle> rectangles)
{
var bitmap = new Bitmap(imageSize.Width, imageSize.Height);

using var graphics = Graphics.FromImage(bitmap);
graphics.Clear(Color.FromArgb(0, 34, 43));
var pen = new Pen(Color.FromArgb(212,85,0), 2);
foreach (var rect in rectangles)
graphics.DrawRectangle(pen, rect);

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

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

<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
</ItemGroup>

<ItemGroup>
<Folder Include="Image\" />
</ItemGroup>

</Project>
171 changes: 171 additions & 0 deletions cs/TagsCloudVisualizationTest/CircularCloudLayouterTest.cs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Здесь стоит добавить тест на то что прямоугольники распологаются по форме круга, а также на их плотность расположения

Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
using System.Drawing;
using FluentAssertions;
using TagsCloudVisualization;

namespace TagsCloudVisualizationTest;

public class CircularCloudLayouterTest
{
private Point validCenter;

[SetUp]
public void Setup()
{
validCenter = new Point(1920 / 2, 1080 / 2);
}


[TestCase(-1920, 1080)]
[TestCase(1920, -1080)]
public void CircularCloudLayouter_ShouldThrowException_WhenInvalidWindowSize(int imageWidth, int imageHeight)
{
var act = () => new CircularCloudLayouter(validCenter, imageWidth, imageHeight);

act.Should().Throw<ArgumentException>()
.WithMessage("Image width or height must be positive");
}

[TestCase(-500, 500)]
[TestCase(500, -500)]
public void CircularCloudLayouter_ShouldThrowException_WhenInvalidCenter(int centerX, int centerY)
{
var invalidCenter = new Point(centerX, centerY);
var act = () => new CircularCloudLayouter(invalidCenter);

act.Should().Throw<ArgumentException>()
.WithMessage("Center should be greater than 0");
;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Лишнее

}


[TestCase(1930, 500)]
[TestCase(500, 1090)]
public void CircularCloudLayouter_ShouldThrowException_WhenCenterIsOutsideImage(int centerX, int centerY)
{
var center = new Point(centerX, centerY);
var act = () => new CircularCloudLayouter(center);

act.Should().Throw<ArgumentException>()
.WithMessage("Center must be inside image bounds");
;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Лишнее

}

[TestCase(-100, 100)]
[TestCase(100, -100)]
[TestCase(0, 100)]
[TestCase(100, 0)]
public void PutNextRectangle_ShouldThrowException_WhenInvalidRectangleSize(int rectangleWidth, int rectangleHeight)
{
var circularCloud = new CircularCloudLayouter(validCenter);
var act = () => circularCloud.PutNextRectangle(new Size(rectangleWidth, rectangleHeight));

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

[Test]
public void PutNextRectangle_ShouldThrowException_WhenNoPlaceForNextRectangle()
{
var center = new Point(200, 200);
var circularCloud = new CircularCloudLayouter(center, 400, 400);
var rectangles = new List<Rectangle>();
var rectangleSize = new Size(40, 40);
for (var i = 0; i < 111; i++)
{
var rect = circularCloud.PutNextRectangle(rectangleSize);
rectangles.Add(rect);
}

var act = () => circularCloud.PutNextRectangle(rectangleSize);
act.Should().Throw<InvalidOperationException>()
.WithMessage($"Failed to find place for the * rectangle");
}

[Test]
public void PutNextRectangle_ShouldNotProduceIntersectingRectangles_WhenCalledMultipleTimes()
{
var circularCloud = new CircularCloudLayouter(validCenter);
var rectangles = new List<Rectangle>();
var rectangleSize = new Size(20, 10);
for (var i = 0; i < 50; i++)
{
var rect = circularCloud.PutNextRectangle(rectangleSize);
rectangles.Add(rect);
}

for (var i = 0; i < rectangles.Count; i++)
{
for (var j = i + 1; j < rectangles.Count; j++)
{
rectangles[i].IntersectsWith(rectangles[j])
.Should().BeFalse($"rect #{i} should not intersect rect #{j}");
}
}
}

[TestCase(-1)]
[TestCase(0)]
public void GetCircularCloudRectangles_ShouldThrowException_WhenCountIsNotPositive(int count)
{
var circularCloud = new CircularCloudLayouter(validCenter);
var rectangleSize = new Size(40, 40);
var act = () => circularCloud.GetCircularCloudRectangles(count, rectangleSize).ToList();

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

[Test]
public void GetCircularCloudRectangles_ShouldNotProduceIntersectingRectangles_WhenCalledMultipleTimes()
{
var circularCloud = new CircularCloudLayouter(validCenter);
var rectangleSize = new Size(20, 10);
var rectangles = circularCloud
.GetCircularCloudRectangles(100, rectangleSize)
.ToList();

for (var i = 0; i < rectangles.Count; i++)
{
for (var j = i + 1; j < rectangles.Count; j++)
{
rectangles[i].IntersectsWith(rectangles[j])
.Should().BeFalse($"rect #{i} should not intersect rect #{j}");
}
}
}

[TestCase(-1)]
[TestCase(2)]
public void GetCircularCloudRectangles_ShouldThrowException_WhenInvalidCoefficient(double coefficient)
{
var circularCloud = new CircularCloudLayouter(validCenter);
var rectangleSize = new Size(20, 10);
var act = () => circularCloud
.GetCircularCloudRectangles(100, rectangleSize, coefficient)
.ToList();
act.Should().Throw<ArgumentException>()
.WithMessage("Coefficient must be between 0 and 1");
}

[TestCase(0.1)]
[TestCase(0.2)]
[TestCase(0.5)]
[TestCase(0.9)]
public void GetCircularCloudRectangles_ShouldNotProduceIntersectingRectangles_WhenValidCoefficient(double coefficient)
{
var circularCloud = new CircularCloudLayouter(validCenter);
var rectangleSize = new Size(20, 10);
var rectangles = circularCloud
.GetCircularCloudRectangles(100, rectangleSize)
.ToList();

for (var i = 0; i < rectangles.Count; i++)
{
for (var j = i + 1; j < rectangles.Count; j++)
{
rectangles[i].IntersectsWith(rectangles[j])
.Should().BeFalse($"rect #{i} should not intersect rect #{j}");
}
}
}
}
Loading