diff --git a/TagsCloudVisualization/TagsCloudVisualization.Tests/CircularCloudLayouterTests.cs b/TagsCloudVisualization/TagsCloudVisualization.Tests/CircularCloudLayouterTests.cs new file mode 100644 index 000000000..aa4991c5b --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization.Tests/CircularCloudLayouterTests.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NUnit.Framework.Interfaces; + +namespace TagsCloudVisualization.Tests; + +[TestFixture] +[TestOf(typeof(CircularCloudLayouterImpl))] +public class CircularCloudLayouterTests +{ + + private static readonly string FailReportFolderPath = "./failed"; + private static readonly int MaxDistanceFromBarycenter = 20; + + private ICircularCloudLayouter _circularCloudLayouter; + + [SetUp] + public void SetUp() + { + _circularCloudLayouter = new CircularCloudLayouterImpl(Point.Empty); + } + + [OneTimeSetUp] + public void EmptyFailReportFolder() + { + if (Directory.Exists(FailReportFolderPath)) + Directory.Delete(FailReportFolderPath, true); + } + + [TearDown] + public void ReportFailures() + { + var context = TestContext.CurrentContext; + if (context.Result.Outcome.Status == TestStatus.Failed) + { + if (context.Test.MethodName == null) + { + Console.WriteLine("Teardown error: test method name is null"); + return; + } + + var testType = DetermineTestType(context.Test.MethodName); + if (testType == TestType.NoTearDown) + return; + + var rectangles = _circularCloudLayouter.Layout.ToArray(); + + var savingPath = $"{FailReportFolderPath}/{context.Test.Name}.png"; + Directory.CreateDirectory(FailReportFolderPath); + + var args = context.Test.Arguments; + var center = new Point((int) args[0]!, (int) args[1]!); + +#pragma warning disable CA1416 + + new Bitmap(1000, 1000) + .DrawFailedTestImage( + rectangles.ToArray(), + center, + ComputeBaryCenter(rectangles), + MaxDistanceFromBarycenter, + testType) + .Save(savingPath, ImageFormat.Png); + +#pragma warning restore CA1416 + + Console.WriteLine($"Failure was reported to {Path.GetFullPath(savingPath)}"); + } + } + + private static TestType DetermineTestType(string methodName) + { + if (methodName == nameof(PutNextRectangle_ThrowsOnHeightOrWidth_BeingLessOrEqualToZero)) + return TestType.NoTearDown; + + if (methodName == nameof(RectanglesCommonBarycenterIsCloseToTheProvidedCenter)) + return TestType.BarycenterTest; + + return TestType.OtherTest; + } + + [Test] + [Description("Проверяем, что поле CloudCenter бросает исключение, " + + "если процесс генерации облака уже начался")] + public void CloudCenter_ThrowsIfLayoutContainsGeneratedRectangles() + { + _circularCloudLayouter.PutNextRectangle(new Size(1, 1)); + + Action act = () => _circularCloudLayouter.CloudCenter = new Point(1, 1); + + act.Should().Throw() + .WithMessage("Can not change cloud center after generation start"); + } + + [Test] + [Description("Проверяем, что метод PutNextRectangle бросает ArgumentException, " + + "если ему передан размер прямоугольника с высотой или шириной " + + "меньше либо равной нулю")] + [TestCase(0, -4, 0, 4)] + [TestCase(0, 1, -2, 4)] + [TestCase(0, 0, 4, -2)] + [TestCase(2, 3, 2, 0)] + [TestCase(1, 2, -2, -1)] + [TestCase(-1, 0, -1, 0)] + public void PutNextRectangle_ThrowsOnHeightOrWidth_BeingLessOrEqualToZero( + int centerX, + int centerY, + int width, + int height) + { + Arrange(centerX, centerY); + + Action act = () => _circularCloudLayouter.PutNextRectangle(new Size(width, height)); + + act.Should() + .Throw(); + } + + [Test] + [Description("Проверяем, что прямоугольники не пересекаются друг с другом")] + [TestCaseSource(typeof(TestCaseData), nameof(TestCaseData.IntersectionTestSource))] + public void RectanglesShouldNotIntersectEachOther( + int centerX, + int centerY, + (int, int)[] sizes) + { + Arrange(centerX, centerY); + + var rectangles = GenerateTestLayout(sizes); + + Assert_RectanglesDoNotIntersect(rectangles); + } + + [Test] + [Description("Проверяем, что центр прямоугольника, размер которого был передан первым " + + "совпадает с центром, переданным в аргумент конструктора CircularCloudLayouter")] + [TestCaseSource(typeof(TestCaseData), nameof(TestCaseData.FirstRectanglePositionTestSource))] + public void FirstRectangleShouldBePositionedAtProvidedCenter( + int centerX, + int centerY, + (int, int)[] sizes) + { + Arrange(centerX, centerY); + + var rectangles = GenerateTestLayout(sizes); + + Assert_FirstRectangleIsPositionedAtProvidedCenter(rectangles, new Point(centerX, centerY)); + } + + [Test] + [Description("Проверяем, что прямоугольники расположены наиболее плотно, " + + "то есть максимум из попарных расстояний между центрами " + + "прямоугольников не превышает maxDistance")] + [TestCaseSource(typeof(TestCaseData), nameof(TestCaseData.DensityTestSource))] + public void RectanglesShouldBeCloseToEachOther( + int centerX, + int centerY, + int maxDistance, + (int, int)[] sizes) + { + Arrange(centerX, centerY); + + var rectangles = GenerateTestLayout(sizes); + + Assert_RectanglesArePositionedCloseToEachOther(rectangles, maxDistance); + } + + [Test] + [Description("Проверяем, что общий центр масс всех прямоугольников находится " + + "рядом с центром, переданным в конструктор CircularCloudLayouter")] + [TestCaseSource(typeof(TestCaseData), nameof(TestCaseData.CenterTestSource))] + public void RectanglesCommonBarycenterIsCloseToTheProvidedCenter( + int centerX, + int centerY, + (int, int)[] sizes) + { + Arrange(centerX, centerY); + + var rectangles = GenerateTestLayout(sizes); + + Assert_RectanglesBarycenterIsCloseToCenter(rectangles, new Point(centerX, centerY)); + } + + private void Arrange(int centerX, int centerY) + { + _circularCloudLayouter.CloudCenter = new Point(centerX, centerY); + } + + private IEnumerable GenerateTestLayout((int, int)[] sizes) + { + return _circularCloudLayouter.GenerateLayout( + sizes.Select(size => new Size(size.Item1, size.Item2)).ToArray()); + } + + private void Assert_RectanglesDoNotIntersect(IEnumerable rectangles) + { + rectangles.CheckForAllPairs(pair => !pair.Item1.IntersectsWith(pair.Item2)) + .Should() + .BeTrue(); + } + + private void Assert_FirstRectangleIsPositionedAtProvidedCenter(IEnumerable rectangles, Point center) + { + rectangles + .First() + .RectangleCenter() + .Should() + .BeEquivalentTo(center); + } + + private void Assert_RectanglesArePositionedCloseToEachOther( + IEnumerable rectangles, + int maxDistance) + { + rectangles.CheckForAllPairs(pair => pair.Item1.DistanceToOtherIsNotGreaterThan(pair.Item2, maxDistance)) + .Should() + .BeTrue(); + } + + private Point ComputeBaryCenter(IEnumerable rectangles) + { + var (totalX, totalY, count) = rectangles + .Aggregate((0, 0, 0), ((int totalX, int totalY, int count) res, Rectangle rect) => + { + var rectCenter = rect.RectangleCenter(); + return (res.totalX + rectCenter.X, res.totalY + rectCenter.Y, ++res.count); + }); + return new Point(totalX / count, totalY / count); + } + + private void Assert_RectanglesBarycenterIsCloseToCenter( + IEnumerable rectangles, + Point center) + { + var barycenter = ComputeBaryCenter(rectangles); + var deviationFromCenter = barycenter.SquaredDistanceTo(center); + deviationFromCenter.Should().BeLessOrEqualTo(MaxDistanceFromBarycenter * MaxDistanceFromBarycenter); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization.Tests/TagsCloudVisualization.Tests.csproj b/TagsCloudVisualization/TagsCloudVisualization.Tests/TagsCloudVisualization.Tests.csproj new file mode 100644 index 000000000..a41c7bb2f --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization.Tests/TagsCloudVisualization.Tests.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + + false + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization.Tests/TestBitmapExtensions.cs b/TagsCloudVisualization/TagsCloudVisualization.Tests/TestBitmapExtensions.cs new file mode 100644 index 000000000..7e4d9bb5d --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization.Tests/TestBitmapExtensions.cs @@ -0,0 +1,30 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Tests; + +internal static class TestBitmapExtensions +{ + public static Bitmap DrawFailedTestImage( + this Bitmap bitmap, + Rectangle[] rectangles, + Point centerPoint, + Point barycenterPoint, + int maxDistanceFromBarycenter, + TestType testType) + { +#pragma warning disable CA1416 + bitmap.DrawRectangles(rectangles, new Pen(Color.Blue)); + + if (testType != TestType.BarycenterTest) + return bitmap; + + bitmap.DrawEllipse( + new Rectangle(centerPoint.X, centerPoint.Y, maxDistanceFromBarycenter, maxDistanceFromBarycenter), + new Pen(Color.Lime)); + bitmap.DrawEllipse( + new Rectangle(barycenterPoint.X, barycenterPoint.Y, 1, 1), + new Pen(Color.Red)); +#pragma warning restore CA1416 + return bitmap; + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization.Tests/TestCaseData.cs b/TagsCloudVisualization/TagsCloudVisualization.Tests/TestCaseData.cs new file mode 100644 index 000000000..eb0b5e2fa --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization.Tests/TestCaseData.cs @@ -0,0 +1,161 @@ +namespace TagsCloudVisualization.Tests; + +public static class TestCaseData +{ + public static object[][] IntersectionTestSource() + { + return + [ + [ + 500, 500, + new[] + { + (20, 10), + (40, 20), + (60, 30), + (80, 40) + } + ], + [ + 500, 500, + new[] + { + (10, 10), + (10, 10), + (20, 10), + (20, 10) + } + ], + [ + 600, 600, + new[] + { + (20, 10), + (30, 15), + (50, 10), + (60, 30), + (30, 10), + (40, 20) + } + ] + ]; + } + + public static object[][] FirstRectanglePositionTestSource() + { + return + [ + [ + 500, 500, + new[] + { + (20, 30), + (30, 45), + (40, 60) + } + ], + [ + 510, 550, + new[] + { + (10, 40), + (10, 30), + (10, 20) + } + ], + [ + 300, 800, + new[] + { + (10, 30) + } + ] + ]; + } + + public static object[][] DensityTestSource() + { + return + [ + [ + 500, 500, 3025, + new[] + { + (20, 20), + (30, 30), + (40, 40) + } + ], + [ + 500, 500, 3025, + new[] + { + (20, 20), + (40, 40), + (30, 30) + } + ], + [ + 600, 400, 4225, + new[] + { + (40, 40), + (30, 30), + (20, 20), + (10, 10) + } + ], + [ + 400, 550, 3025, + new[] + { + (20, 20), + (30, 30), + (40, 40), + (10, 10) + } + ] + ]; + } + + public static object[][] CenterTestSource() + { + return + [ + [ + 500, 500, + new[] + { + (10, 20), + (10, 60), + (10, 60), + (10, 20) + } + ], + [ + 300, 500, + new[] + { + (10, 40), + (10, 50), + (20, 30), + (40, 30) + } + ], + [ + 520, 410, + new[] + { + (10, 20), + (20, 30), + (30, 40), + (40, 50), + (50, 40), + (40, 30), + (30, 20), + (20, 10) + } + ] + ]; + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization.Tests/TestType.cs b/TagsCloudVisualization/TagsCloudVisualization.Tests/TestType.cs new file mode 100644 index 000000000..53e7c0d0b --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization.Tests/TestType.cs @@ -0,0 +1,8 @@ +namespace TagsCloudVisualization.Tests; + +public enum TestType +{ + NoTearDown, + BarycenterTest, + OtherTest +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization.sln b/TagsCloudVisualization/TagsCloudVisualization.sln new file mode 100644 index 000000000..f38858b1d --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudVisualization", "TagsCloudVisualization\TagsCloudVisualization.csproj", "{B36FD3AA-D116-4E5B-AC96-83504630043E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudVisualization.Tests", "TagsCloudVisualization.Tests\TagsCloudVisualization.Tests.csproj", "{ABA2723C-4180-4BE8-AD07-0EFE58FF6912}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B36FD3AA-D116-4E5B-AC96-83504630043E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B36FD3AA-D116-4E5B-AC96-83504630043E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B36FD3AA-D116-4E5B-AC96-83504630043E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B36FD3AA-D116-4E5B-AC96-83504630043E}.Release|Any CPU.Build.0 = Release|Any CPU + {ABA2723C-4180-4BE8-AD07-0EFE58FF6912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABA2723C-4180-4BE8-AD07-0EFE58FF6912}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABA2723C-4180-4BE8-AD07-0EFE58FF6912}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABA2723C-4180-4BE8-AD07-0EFE58FF6912}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/TagsCloudVisualization/TagsCloudVisualization/BitmapExtensions.cs b/TagsCloudVisualization/TagsCloudVisualization/BitmapExtensions.cs new file mode 100644 index 000000000..deec008e3 --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization/BitmapExtensions.cs @@ -0,0 +1,26 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public static class BitmapExtensions +{ +#pragma warning disable CA1416 + + public static Bitmap DrawRectangles(this Bitmap bitmap, Rectangle[] rectangles, Pen pen) + { + var graphics = Graphics.FromImage(bitmap); + graphics.DrawRectangles(pen, rectangles); + graphics.Dispose(); + return bitmap; + } + + public static Bitmap DrawEllipse(this Bitmap bitmap, Rectangle boundingRect, Pen pen) + { + var graphics = Graphics.FromImage(bitmap); + graphics.DrawEllipse(pen, boundingRect); + graphics.Dispose(); + return bitmap; + } + +#pragma warning restore CA1416 +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization/CircularCloudLayouterExtensions.cs b/TagsCloudVisualization/TagsCloudVisualization/CircularCloudLayouterExtensions.cs new file mode 100644 index 000000000..49a1ec11e --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization/CircularCloudLayouterExtensions.cs @@ -0,0 +1,11 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public static class CircularCloudLayouterExtensions +{ + public static IEnumerable GenerateLayout(this ICircularCloudLayouter layouter, Size[] sizes) + { + return sizes.Select(layouter.PutNextRectangle); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization/CircularCloudLayouterImpl.cs b/TagsCloudVisualization/TagsCloudVisualization/CircularCloudLayouterImpl.cs new file mode 100644 index 000000000..6d6773d89 --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization/CircularCloudLayouterImpl.cs @@ -0,0 +1,177 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public class CircularCloudLayouterImpl : ICircularCloudLayouter +{ + private static readonly float TracingStep = 0.001f; + private static readonly float MaxTracingDistance = 1000f; + private static readonly int MaxCycleCount = 7; + + public Point CloudCenter + { + get => _cloudCenter; + set + { + if (_generatedLayout.Count > 0) + { + throw new InvalidOperationException("Can not change cloud center after generation start"); + } + _cloudCenter = value; + } + } + + public IEnumerable Layout => _generatedLayout.AsEnumerable(); + + private Point _cloudCenter; + private List _generatedLayout = new(); + private double _nextAngle; + private double _angleStep = Math.PI / 2; + private int _currentCycle; + private int CurrentCycle + { + get => _currentCycle; + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value); + if (value < MaxCycleCount) + { + _currentCycle = value; + _angleStep = Math.PI / Math.Pow(2, value); + } + } + } + + public CircularCloudLayouterImpl(Point center) + { + CloudCenter = center; + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(rectangleSize.Width, "Width"); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(rectangleSize.Height, "Height"); + + if (_generatedLayout.Count == 0) + { + var rectangle = new Rectangle( + CloudCenter.X - rectangleSize.Width / 2, + CloudCenter.Y - rectangleSize.Height / 2, + rectangleSize.Width, + rectangleSize.Height); + _generatedLayout.Add(rectangle); + return rectangle; + } + + var nextRectangle = GetNextRectangle(rectangleSize); + _generatedLayout.Add(nextRectangle); + return nextRectangle; + } + + private Rectangle GetNextRectangle(Size rectangleSize) + { + Rectangle result = Rectangle.Empty; + var found = false; + while (!found) + { + var direction = GetNextDirection(); + var step = 0.0f; + while (step < 1f && !found) + { + (step, var availablePos) = FindNextAvailablePosByTracingLine(direction, step); + found = TryFindGoodRectanglePosition(availablePos, rectangleSize, out result); + } + } + + return result; + } + + private bool TryFindGoodRectanglePosition(Point posToPlace, Size rectangleSize, out Rectangle result) + { + var possibleOptions = new Rectangle[] + { + new Rectangle( + posToPlace.X, + posToPlace.Y, + rectangleSize.Width, + rectangleSize.Height), + new Rectangle( + posToPlace.X - rectangleSize.Width, + posToPlace.Y, + rectangleSize.Width, + rectangleSize.Height), + new Rectangle( + posToPlace.X, + posToPlace.Y - rectangleSize.Height, + rectangleSize.Width, + rectangleSize.Height), + new Rectangle( + posToPlace.X - rectangleSize.Width, + posToPlace.Y - rectangleSize.Height, + rectangleSize.Width, + rectangleSize.Height) + }; + + foreach (var option in possibleOptions) + { + bool intersects = false; + foreach (var rectangle in _generatedLayout) + { + if (rectangle.IntersectsWith(option)) + { + intersects = true; + break; + } + } + + if (!intersects) + { + result = option; + return true; + } + } + + result = Rectangle.Empty; + return false; + } + + private (float, Point) FindNextAvailablePosByTracingLine(PointF direction, float startingStep = 0.0f) + { + var nextPos = new PointF( + CloudCenter.X + direction.X * MaxTracingDistance * TracingStep, + CloudCenter.Y + direction.Y * MaxTracingDistance * TracingStep); + var currentStep = startingStep == 0.0f ? TracingStep : startingStep; + var notInRectangle = false; + while (!notInRectangle) + { + notInRectangle = true; + foreach (var rectangle in _generatedLayout) + { + if (rectangle.ContainsFloat(nextPos)) + { + notInRectangle = false; + break; + } + } + currentStep += TracingStep; + nextPos = new PointF( + CloudCenter.X + direction.X * MaxTracingDistance * currentStep, + CloudCenter.Y + direction.Y * MaxTracingDistance * currentStep); + } + + return (currentStep, Point.Truncate(nextPos)); + } + + private PointF GetNextDirection() + { + var x = (float)Math.Cos(_nextAngle); + var y = (float)Math.Sin(_nextAngle); + _nextAngle += _angleStep; + if (Math.Abs(_nextAngle - Math.PI * 2) < 1e-12f) + { + _nextAngle = 0; + CurrentCycle++; + } + return new PointF(x, y); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization/ICircularCloudLayouter.cs b/TagsCloudVisualization/TagsCloudVisualization/ICircularCloudLayouter.cs new file mode 100644 index 000000000..7b4b58d79 --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization/ICircularCloudLayouter.cs @@ -0,0 +1,10 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public interface ICircularCloudLayouter +{ + public Rectangle PutNextRectangle(Size rectangleSize); + public Point CloudCenter { get; set; } + public IEnumerable Layout { get; } +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization/PointExtensions.cs b/TagsCloudVisualization/TagsCloudVisualization/PointExtensions.cs new file mode 100644 index 000000000..dc13cf699 --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization/PointExtensions.cs @@ -0,0 +1,11 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public static class PointExtensions +{ + public static int SquaredDistanceTo(this Point p1, Point p2) + { + return (int)(Math.Pow(p1.X - p2.X, 2) + Math.Pow(p1.Y - p2.Y, 2)); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization/Program.cs b/TagsCloudVisualization/TagsCloudVisualization/Program.cs new file mode 100644 index 000000000..89b9a6f8c --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization/Program.cs @@ -0,0 +1,25 @@ +using System.Drawing; +using System.Drawing.Imaging; +using TagsCloudVisualization; + +var sizes = new List(); + +var random = new Random(); +for (int i = 0; i < 50; i++) +{ + sizes.Add(new Size(random.Next(10, 50), random.Next(10, 50))); +} + +var layout = new CircularCloudLayouterImpl(new Point(1000, 1000)) + .GenerateLayout(sizes.ToArray()) + .ToArray(); + +#pragma warning disable CA1416 + +var filename = "./test.png"; +new Bitmap(2000, 2000) + .DrawRectangles(layout, new Pen(Color.Blue)) + .Save(filename, ImageFormat.Png); +Console.WriteLine($"File was saved to: {Path.GetFullPath(filename)}"); + +#pragma warning restore CA1416 \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization/README.md b/TagsCloudVisualization/TagsCloudVisualization/README.md new file mode 100644 index 000000000..1269415ba --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization/README.md @@ -0,0 +1,11 @@ +## Облако из 50 прямоугольников: + +![screenshot](generation_50.png) + +## Облако из 200 прямоугольников: + +![screenshot](generation_200.png) + +## Облако из 2000 прямоугольников: + +![screenshot](generation_2000.png) \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization/RectangleCollectionExtensions.cs b/TagsCloudVisualization/TagsCloudVisualization/RectangleCollectionExtensions.cs new file mode 100644 index 000000000..b0a04ffe2 --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization/RectangleCollectionExtensions.cs @@ -0,0 +1,19 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public static class RectangleCollectionExtensions +{ + public static bool CheckForAllPairs( + this IEnumerable rectangles, + Func<(Rectangle, Rectangle), bool> predicate) + { + var rectangleList = rectangles.ToList(); + return rectangleList + .SelectMany((rect1, index) => rectangleList + .GetRange(index, rectangleList.Count - index) + .Select(rect2 => (rect1, rect2))) + .Where(tuple => tuple.Item1 != tuple.Item2) + .All(predicate); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization/RectangleExtensions.cs b/TagsCloudVisualization/TagsCloudVisualization/RectangleExtensions.cs new file mode 100644 index 000000000..fe1628897 --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization/RectangleExtensions.cs @@ -0,0 +1,27 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public static class RectangleExtensions +{ + public static bool ContainsFloat(this Rectangle rectangle, PointF point) + { + return rectangle.X <= point.X + && point.X < rectangle.Right + && rectangle.Y <= point.Y + && point.Y < rectangle.Bottom; + } + + public static bool DistanceToOtherIsNotGreaterThan(this Rectangle rect, Rectangle other, int distance) + { + var center1 = RectangleCenter(rect); + var center2 = RectangleCenter(other); + var actualDistance = center1.SquaredDistanceTo(center2); + return actualDistance <= distance; + } + + public static Point RectangleCenter(this Rectangle rectangle) + { + return new Point((rectangle.X + rectangle.Right) / 2, (rectangle.Y + rectangle.Bottom) / 2); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization/TagsCloudVisualization.csproj b/TagsCloudVisualization/TagsCloudVisualization/TagsCloudVisualization.csproj new file mode 100644 index 000000000..f4aff6610 --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization/TagsCloudVisualization.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/TagsCloudVisualization/TagsCloudVisualization/generation_200.png b/TagsCloudVisualization/TagsCloudVisualization/generation_200.png new file mode 100644 index 000000000..0a8324f38 Binary files /dev/null and b/TagsCloudVisualization/TagsCloudVisualization/generation_200.png differ diff --git a/TagsCloudVisualization/TagsCloudVisualization/generation_2000.png b/TagsCloudVisualization/TagsCloudVisualization/generation_2000.png new file mode 100644 index 000000000..e067ac5a9 Binary files /dev/null and b/TagsCloudVisualization/TagsCloudVisualization/generation_2000.png differ diff --git a/TagsCloudVisualization/TagsCloudVisualization/generation_50.png b/TagsCloudVisualization/TagsCloudVisualization/generation_50.png new file mode 100644 index 000000000..fd685dc44 Binary files /dev/null and b/TagsCloudVisualization/TagsCloudVisualization/generation_50.png differ