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

namespace TagsCloudVisualization;

public class CircularCloudLayouter
{
public readonly Point Center;

This comment was marked as resolved.

public readonly List<Rectangle> PlacedRectangles = [];

This comment was marked as resolved.

private readonly Spiral spiral;

public CircularCloudLayouter(Point center)
{
Center = center;
spiral = new Spiral(center);
}

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

var rect = FindFreeRectangle(rectangleSize);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Свободный от чего? Мы там вроде место под прямоугольник ищем, нет?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FindFreeSpaceForRectangle - уже лучше, но можно даже просто FindFreeSpace и смотри, как хорошо читается:
image
"Найти свободное место для размера"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

И в месте вызова тоже
image
"Найти свободное место размером с прямоугольник"

rect = ShiftToCenter(rect);

PlacedRectangles.Add(rect);
return rect;
}

private Rectangle FindFreeRectangle(Size size)
{
while (true)
{
var point = spiral.GetNextPoint();

This comment was marked as resolved.

var rect = CreateRectangleByCenter(point, size);

if (!IntersectsWithOthers(rect))
return rect;
}
}

private Rectangle ShiftToCenter(Rectangle rect)

This comment was marked as resolved.

{
rect = ShiftAxis(rect,
dx: rect.X < Center.X ? 1 : -1,
dy: 0);

rect = ShiftAxis(rect,
dx: 0,
dy: rect.Y < Center.Y ? 1 : -1);

return rect;
}

private Rectangle ShiftAxis(Rectangle rect, int dx, int dy)

This comment was marked as resolved.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

можно сделать шаги побольше и если пересекся то уже маленькими шагами идти

{
while (true)
{
var shifted = rect with { X = rect.X + dx, Y = rect.Y + dy };

if (IntersectsWithOthers(shifted) || TouchesCorner(rect, shifted) || CrossedTheCenter(rect, dx, dy, shifted))
return rect;

rect = shifted;
}
}

private bool TouchesCorner(Rectangle oldRect, Rectangle newRect)
{
return PlacedRectangles.Any(r => !oldRect.IntersectsWith(r) && newRect.IntersectsWith(r));
}

private bool CrossedTheCenter(Rectangle oldRect, int dx, int dy, Rectangle shifted)

This comment was marked as resolved.

{
var oldCx = oldRect.X + oldRect.Width / 2;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

oldCx - C это от Coordinate или от Center?

var oldCy = oldRect.Y + oldRect.Height / 2;

var newCx = shifted.X + shifted.Width / 2;
var newCy = shifted.Y + shifted.Height / 2;

if (dx != 0)
return Math.Abs(newCx - Center.X) > Math.Abs(oldCx - Center.X);

if (dy != 0)
return Math.Abs(newCy - Center.Y) > Math.Abs(oldCy - Center.Y);

return false;
}

private bool IntersectsWithOthers(Rectangle rect)

This comment was marked as resolved.

=> PlacedRectangles.Any(r => r.IntersectsWith(rect with{ Width = rect.Width+1, Height = rect.Height+1}));

private static Rectangle CreateRectangleByCenter(Point center, Size size)
=> new(
center.X - size.Width / 2,
center.Y - size.Height / 2,
size.Width,
size.Height);
}
63 changes: 63 additions & 0 deletions cs/TagsCloudVisualization/CloudVizualizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Drawing;
using System.Runtime.Versioning;

namespace TagsCloudVisualization;

[SupportedOSPlatform("windows")]

This comment was marked as duplicate.

public static class CloudVisualizer

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ответственность этого класса - создать изборажение по заданным параметрам и сохранить в файл. При этом за пользователя очень много чего выбирается по-умолчанию:

  1. расположение центра
  2. размер изображения
  3. цвет фона
  4. цвета прямоугольников
  5. и т.д.

Хорошенько подумай, что можно захотеть настраивать в изображении!

Далее - недавно вы, кажется, проходили fluent interface'ы, помнишь такие? Давай применим новые знания и навыки: во-первых, давай создадим класс-билдер для CloudVisualizer - пусть билдер хранит разнообразные настройки и для каждой имеет fluent-метод для их изменения (например, метод SetCenter мог бы принимать параметры настраиваемого центра); во-вторых, пусть билдер имеет метод Build или Create (или что-то в этом духе), который будет создавать экземпляр CloudVisualizer с указанными настройками. Подумай, что должен делать билдер, если пользователь определил не все настройки, но вызывает метод Build? Spoiler: здесь могут быть разные решения разной сложности.

{
public static void GenerateWordsCloud(string fileName, List<Size> rectSizes)

This comment was marked as resolved.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

ну пока что rectangle)

This comment was marked as duplicate.

{
var center = new Point(0, 0);
var layouter = new CircularCloudLayouter(center);

foreach (var size in rectSizes)
{
layouter.PutNextRectangle(size);
}

SaveLayoutImage(layouter.PlacedRectangles, fileName);
}

private static void SaveLayoutImage(List<Rectangle> rectangles, string fileName, int padding = 50)
{
if (rectangles.Count == 0)

This comment was marked as resolved.

return;

var minX = rectangles.Min(r => r.Left);

This comment was marked as resolved.

var maxX = rectangles.Max(r => r.Right);
var minY = rectangles.Min(r => r.Top);
var maxY = rectangles.Max(r => r.Bottom);

var width = (maxX - minX) + padding * 2;
var height = (maxY - minY) + padding * 2;

using var bitmap = new Bitmap(width, height);
using var g = Graphics.FromImage(bitmap);

g.Clear(Color.Black);

var random = new Random();

This comment was marked as resolved.


foreach (var rect in rectangles)
{
var shifted = rect with { X = rect.Left - minX + padding, Y = rect.Top - minY + padding };
if (rect == rectangles[0])
{
g.FillRectangle(Brushes.Red, shifted);
continue;
}
var color = Color.FromArgb(
random.Next(60, 93),

This comment was marked as resolved.

0,
random.Next(164, 255));

using var brush = new SolidBrush(color);
g.FillRectangle(brush, shifted);
g.DrawRectangle(Pens.Black, shifted);
}

bitmap.Save(fileName);

This comment was marked as resolved.

}
}
5 changes: 5 additions & 0 deletions cs/TagsCloudVisualization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
![example1.png](../TagsCloudVisualizationTests/bin/Debug/net9.0/visualizer_tests/example1.png)

![example2.png](../TagsCloudVisualizationTests/bin/Debug/net9.0/visualizer_tests/example2.png)

![example3.png](../TagsCloudVisualizationTests/bin/Debug/net9.0/visualizer_tests/example3.png)
29 changes: 29 additions & 0 deletions cs/TagsCloudVisualization/Spiral.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Drawing;

namespace TagsCloudVisualization;

public class Spiral
{
private readonly Point center;
private double angle;

This comment was marked as resolved.

private readonly double angleStep;
private readonly double radiusStep;

public Spiral(Point center, double angleStep = 0.1, double radiusStep = 0.5)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

В чём здесь измеряются углы?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

радианы

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.angleStep = angleStep;
this.radiusStep = radiusStep;
angle = 0;
}

public Point GetNextPoint()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Хм, выглядит, как бесконечная коллекция. Не требую в обязательном порядке, но мне кажется было бы круто попробовать реализовать не Spiral, а какую-нибудь SpiralPoints : IEnumerable

{
var radius = radiusStep * angle;

This comment was marked as resolved.

var x = center.X + (int)(radius * Math.Cos(angle));
var y = center.Y + (int)(radius * Math.Sin(angle));

angle += angleStep;
return new Point(x, y);
}
}
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>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

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

</Project>
135 changes: 135 additions & 0 deletions cs/TagsCloudVisualizationTests/GenerateWordsCloudTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using System.Drawing;
using System.Runtime.Versioning;
using FluentAssertions;
using TagsCloudVisualization;

namespace TagsCloudVisualizationTests;

[SupportedOSPlatform("windows")]

[TestFixture]
public class CloudVisualizerTests
{
private string outputDir;

[SetUp]
public void SetUp()
{
outputDir = Path.Combine(TestContext.CurrentContext.WorkDirectory, "visualizer_tests");
Directory.CreateDirectory(outputDir);
}

[Test]
public void GenerateWordsCloud_ShouldCreateFile_WhenSizesProvided()

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 file = Path.Combine(outputDir, "cloud.png");
var sizes = new List<Size>
{
new(50, 30),
new(20, 20),
new(10, 40)
};

CloudVisualizer.GenerateWordsCloud(file, sizes);

File.Exists(file).Should().BeTrue("visualizer must create output file");
}

[Test]
public void GenerateWordsCloud_ShouldNotThrow_WhenGivenEmptyList()

This comment was marked as resolved.

{
var file = Path.Combine(outputDir, "empty.png");

var act = () => CloudVisualizer.GenerateWordsCloud(file, []);

act.Should().NotThrow("empty list shouldn't cause failures");
File.Exists(file).Should().BeFalse("empty input should not produce image");
}

[Test]
public void GenerateWordsCloud_ShouldProduceNonEmptyImage()

This comment was marked as resolved.

{
var file = Path.Combine(outputDir, "nonempty.png");
var sizes = new List<Size> { new(100, 50), new(30, 30) };

CloudVisualizer.GenerateWordsCloud(file, sizes);

var fileInfo = new FileInfo(file);

fileInfo.Exists.Should().BeTrue();
fileInfo.Length.Should().BeGreaterThan(1000, "image should not be empty");
}

[Test]
public void GenerateWordsCloud_ShouldProduceValidPng()
{
var file = Path.Combine(outputDir, "valid.png");
var sizes = new List<Size> { new(80, 40) };

CloudVisualizer.GenerateWordsCloud(file, sizes);

using var bmp = new Bitmap(file);

This comment was marked as resolved.


bmp.Width.Should().BeGreaterThan(0);

This comment was marked as resolved.

bmp.Height.Should().BeGreaterThan(0);
}

[Test]
public void GenerateWordsCloud_ShouldUseAllSizes()
{
var file = Path.Combine(outputDir, "count_check.png");
var sizes = new List<Size>
{
new(10,10),
new(20,20),
new(30,30)
};

CloudVisualizer.GenerateWordsCloud(file, sizes);

var layouter = new CircularCloudLayouter(new Point(0, 0));
foreach (var size in sizes)
layouter.PutNextRectangle(size);

layouter.PlacedRectangles.Count.Should().Be(sizes.Count);

This comment was marked as resolved.

}

[Test]
public void GenerateWordsCloud_ShouldHandleLargeAmountOfRects()
{
var file = Path.Combine(outputDir, "many.png");

var sizes = Enumerable
.Range(1, 200)

This comment was marked as resolved.

.Select(i => new Size(i % 40 + 10, i % 30 + 10))
.ToList();

var act = () => CloudVisualizer.GenerateWordsCloud(file, sizes);

act.Should().NotThrow();
File.Exists(file).Should().BeTrue();
}

[Test, Explicit]
public void VizualizeExamples()

This comment was marked as resolved.

{
var file1 = Path.Combine(outputDir, "example1.png");
var file2 = Path.Combine(outputDir, "example2.png");
var file3 = Path.Combine(outputDir, "example3.png");
var sizes1 = Enumerable
.Range(1, 500)
.Select(i => new Size(i % 40 + 10, i % 30 + 10))
.ToList();
var sizes2 = Enumerable
.Range(1, 100)
.Select(i => new Size(i % 60 + 10, i % 20 + 10))
.ToList();
var sizes3 = Enumerable
.Range(1, 50)
.Select(i => new Size(i % 40 + 10, i % 70 + 10))
.ToList();
CloudVisualizer.GenerateWordsCloud(file1, sizes1);
CloudVisualizer.GenerateWordsCloud(file2, sizes2);
CloudVisualizer.GenerateWordsCloud(file3, sizes3);
}
}
Loading