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

Леонид Рыбин #251

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Drawing;
using TagsCloudVisualization.Layouters;

namespace TagsCloudVisualization.Extensions;

public static class ICircularCloudLayouterExtensions
{
private const int MinRectangleSize = 40;
private const int MaxRectangleSize = 70;

public static Rectangle[] GenerateCloud(this ICloudLayouter layouter, int rectanglesNumber = 1000, int minRectangleSize = MinRectangleSize, int maxRectangleSize = MaxRectangleSize)
{
var random = new Random();
return Enumerable.Range(1, rectanglesNumber)
.Select(_ => new Size(
random.Next(minRectangleSize, maxRectangleSize),
random.Next(minRectangleSize, maxRectangleSize)))
.Select(size => layouter.PutNextRectangle(size))
.ToArray();
}
}
Binary file added cs/TagsCloudVisualization/Images/100_TagCloud.jpg
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/Images/50_TagCloud.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions cs/TagsCloudVisualization/Layouters/CircularCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Drawing;
using TagsCloudVisualization.PointGenerators;

namespace TagsCloudVisualization.Layouters;

public class CircularCloudLayouter : ICloudLayouter
{
private const double DefaultRadius = 1;
private const double DefaultAngleOffset = 10;
private readonly Point center;
public readonly List<Rectangle> rectangles;
private readonly CircularSpiralPointGenerator pointsGenerator;

public CircularCloudLayouter(Point center) : this(center, DefaultRadius, DefaultAngleOffset) {}

public CircularCloudLayouter(Point center, double radius, double angleOffset)
{
if (center.X < 0 || center.Y < 0)
throw new ArgumentException("X or Y must be positive");

this.center = center;
rectangles = new List<Rectangle>();

pointsGenerator = new CircularSpiralPointGenerator(radius, angleOffset, center);

This comment was marked as resolved.

}

public Rectangle PutNextRectangle(Size rectangleSize)
{
if (rectangleSize.Height <= 0 || rectangleSize.Width <= 0)
throw new ArgumentException("Height or Width is negative!");

Rectangle rectangle;

do
{
var rectangleCenterPos = pointsGenerator.GetPoint();
rectangle = CreateRectangle(rectangleCenterPos, rectangleSize);
}
while (rectangles.Any(rectangle.IntersectsWith));

rectangles.Add(rectangle);

return rectangle;
}

private static Rectangle CreateRectangle(Point center, Size rectangleSize)
{
var x = center.X - rectangleSize.Width / 2;
var y = center.Y - rectangleSize.Height / 2;
return new Rectangle(x, y, rectangleSize.Width, rectangleSize.Height);
}
}
8 changes: 8 additions & 0 deletions cs/TagsCloudVisualization/Layouters/ICloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Drawing;

namespace TagsCloudVisualization.Layouters;

public interface ICloudLayouter
{
public Rectangle PutNextRectangle(Size rectangleSize);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Drawing;

namespace TagsCloudVisualization.PointGenerators;

public class CircularSpiralPointGenerator : IPointGenerator
{
private double angleOffset;
private double radius;
private double angle = 0;
private Point center;

public CircularSpiralPointGenerator(double radius, double angleOffset, Point center)

This comment was marked as resolved.

Copy link
Author

Choose a reason for hiding this comment

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

В CircularSpiralPointGenerator выполняется построение спирали и получение точек из неё, а в CircularCloudLayouter идёт работа с прямоугольниками и проверка того что они не пересекаются. Логично вынести этот класс, иначе CircularCloudLayouter будет перегружен.

{
if (radius <= 0)
throw new ArgumentException("radius must be greater than 0");
if (angleOffset <= 0)
throw new ArgumentException("angleOffset must be greater than 0");

this.radius = radius;
this.angleOffset = angleOffset * Math.PI / 180;
this.center = center;
}

public Point GetPoint()
{
var radiusVector = (radius / (2 * Math.PI)) * angle;

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

angle += angleOffset;

return new Point(x, y);
}
}
8 changes: 8 additions & 0 deletions cs/TagsCloudVisualization/PointGenerators/IPointGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Drawing;

namespace TagsCloudVisualization.PointGenerators;

public interface IPointGenerator
{
public Point GetPoint();
}
38 changes: 38 additions & 0 deletions cs/TagsCloudVisualization/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Drawing;
using System.Drawing.Imaging;
using TagsCloudVisualization.Extensions;
using TagsCloudVisualization.Layouters;
using TagsCloudVisualization.Visualizers;

namespace TagsCloudVisualization;

internal class Program
{
private const int ImageWidth = 1920;
private const int ImageHeight = 1080;

private const int RectanglesNumber = 100;

private const string ImagesDirectory = "images";

public static void Main(string[] args)
{
var center = new Point(ImageWidth / 2, ImageHeight / 2);
var cloudLayouter = new CircularCloudLayouter(center);

var rectangles = cloudLayouter.GenerateCloud(RectanglesNumber);

var visualizer = new Visualizer();
var bitmap = visualizer.CreateBitmap(rectangles, new Size(ImageWidth, ImageHeight));
Directory.CreateDirectory(ImagesDirectory);

bitmap.Save(GetPathToImages(), ImageFormat.Jpeg);
}

private static string GetPathToImages()
{
var filename = $"{RectanglesNumber}_TagCloud.jpg";
return Path.Combine(ImagesDirectory, filename);
}

}
5 changes: 5 additions & 0 deletions cs/TagsCloudVisualization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### 50 Прямоугольников
![result](https://i.imgur.com/nbMnER4.jpeg)
---
### 100 Прямоугольников
![result1](https://i.imgur.com/l3GB61c.jpeg)
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>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

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

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

</Project>
8 changes: 8 additions & 0 deletions cs/TagsCloudVisualization/Visualizers/IVisualizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Drawing;

namespace TagsCloudVisualization.Visualizers;

public interface IVisualizer
{
public Bitmap CreateBitmap(IEnumerable<Rectangle> rectangles, Size bitmapSize);
}
20 changes: 20 additions & 0 deletions cs/TagsCloudVisualization/Visualizers/Visualizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Drawing;

namespace TagsCloudVisualization.Visualizers;

public class Visualizer : IVisualizer
{
public Bitmap CreateBitmap(IEnumerable<Rectangle> rectangles, Size bitmapSize)
{
var bitmap = new Bitmap(bitmapSize.Width, bitmapSize.Height);

using var graphics = Graphics.FromImage(bitmap);
foreach (var rectangle in rectangles)
{
var pen = new Pen(Color.Blue);
graphics.DrawRectangle(pen, rectangle);
}

return bitmap;
}
}
136 changes: 136 additions & 0 deletions cs/TagsCloudVisualizationTests/CircularCloudLayouterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using FluentAssertions;
using NUnit.Framework.Interfaces;
using System.Drawing;
using System.Drawing.Imaging;
using TagsCloudVisualization.Extensions;
using TagsCloudVisualization.Layouters;
using TagsCloudVisualization.Visualizers;

namespace TagsCloudVisualizationTests;

[TestFixture]
public class CircularCloudLayouterTests
{
private Rectangle[] rectangles;
private const string ImagesDirectory = "TestImages";

private static readonly Point OptimalCenter = new(0, 0);
private readonly double OptimalRadius = 1;
private readonly double OptimalAngleOffset = 0.5;

[TearDown]
public void TearDown()
{
var currentContext = TestContext.CurrentContext;
if (currentContext.Result.Outcome.Status != TestStatus.Failed)
return;

var visualizer = new Visualizer();
var bitmap = visualizer.CreateBitmap(rectangles, GetLayoutSize());
Directory.CreateDirectory(ImagesDirectory);
bitmap.Save(Path.Combine(ImagesDirectory, currentContext.Test.Name + ".jpg"), ImageFormat.Jpeg);

TestContext.Out
.WriteLine($"Tag cloud visualization saved to file " +
$"{Path.Combine(ImagesDirectory, currentContext.Test.Name + ".jpg")}");
}

[Test]
public void CircularCloudLayouter_WhenCorrectArgs_NotThrowArgumentException()
{
var act = () => new CircularCloudLayouter(new Point(5, 15));

act.Should().NotThrow<ArgumentException>();
}

[TestCase(-5, -5, TestName = "WithNegativeXAndY")]
[TestCase(-5, 5, TestName = "WithNegativeX")]
[TestCase(5, -5, TestName = "WithNegativeY")]
public void CircularCloudLayouter_WhenIncorrectArgs_ThrowArgumentException(int x, int y)
{
var act = () => new CircularCloudLayouter(new Point(x, y));

act.Should().Throw<ArgumentException>();
}

[TestCase(-5, -5, TestName = "WithNegativeWidthAndHeight")]
[TestCase(-5, 5, TestName = "WithNegativeWidth")]
[TestCase(5, -5, TestName = "WithNegativeHeight")]
[TestCase(0, 0, TestName = "WithZeroWidthAndHeight")]
public void PutNextRectangle_WhenIncorrectArgs_ThrowArgumentException(int width, int height)
{
var circularCloudLayouter = new CircularCloudLayouter(new Point(600, 600), 1, 90);

var act = () => circularCloudLayouter.PutNextRectangle(new Size(width, height));

act.Should().Throw<ArgumentException>();
}

[Test]
public void PutNextRectangle_ShouldReturnRectanglesWithoutIntersections()
{
var rectanglesNumber = 100;
var circularCloudLayouter = new CircularCloudLayouter(OptimalCenter);

rectangles = circularCloudLayouter.GenerateCloud(rectanglesNumber);

IsIntersectionBetweenRectangles(rectangles).Should().BeFalse();
}

private static bool IsIntersectionBetweenRectangles(Rectangle[] rectangles)
{
for (var i = 0; i < rectangles.Length; i++)
{
for (var j = i + 1; j < rectangles.Length; j++)
{
if (rectangles[i].IntersectsWith(rectangles[j]))
return true;
}
}

return false;
}

[Test]
public void TagsCloud_ShouldBeShapeOfCircularCloud_WhenOptimalParameters()
{
var rectanglesNumber = 1000;
var circularCloudLayouter = new CircularCloudLayouter(OptimalCenter, OptimalRadius, OptimalAngleOffset);

Choose a reason for hiding this comment

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

Так везде надо new CircularCloudLayouter(OptimalCenter) сделать, я просто не стал в каждом месте писать.
И тогда от полей OptimalRadius, OptimalAngleOffset можно будет избавиться.


rectangles = circularCloudLayouter.GenerateCloud(rectanglesNumber, 10, 25);
var layoutSize = GetLayoutSize();
var diametr = Math.Max(layoutSize.Height, layoutSize.Width);
var circleArea = Math.PI* Math.Pow(diametr, 2) / 4;
var rectanglesArea = (double)rectangles
.Select(rectangle => rectangle.Height * rectangle.Width)
.Sum();
var accuracy = circleArea / rectanglesArea;

accuracy.Should().BeApproximately(1, 0.35);
}

private Size GetLayoutSize()
{
var layoutWidth = rectangles.Max(rectangle => rectangle.Right) -
rectangles.Min(rectangle => rectangle.Left);
var layoutHeight = rectangles.Max(rectangle => rectangle.Top) -
rectangles.Min(rectangle => rectangle.Bottom);
return new Size(layoutWidth, layoutHeight);
}

[Test]
public void TagsCloud_ShouldBeDense_WhenOptimalParameters()
{
var rectanglesNumber = 1000;
var circularCloudLayouter = new CircularCloudLayouter(OptimalCenter, OptimalRadius, OptimalAngleOffset);
rectangles = circularCloudLayouter.GenerateCloud(rectanglesNumber, 10, 10);

var expectedRectanglesArea = (double)rectanglesNumber * 10 * 10;
var rectanglesArea = (double)rectangles
.Select(rectangle => rectangle.Height * rectangle.Width)
.Sum();
var accuracy = expectedRectanglesArea / rectanglesArea;

accuracy.Should().BeApproximately(1, 0.2);
}
}

This comment was marked as resolved.

Loading