diff --git a/cs/Images/1000_0,1_0,1_TagCloud.jpg b/cs/Images/1000_0,1_0,1_TagCloud.jpg new file mode 100644 index 000000000..310ef061d Binary files /dev/null and b/cs/Images/1000_0,1_0,1_TagCloud.jpg differ diff --git a/cs/Images/1000_0,1_20_TagCloud.jpg b/cs/Images/1000_0,1_20_TagCloud.jpg new file mode 100644 index 000000000..2a7f14b85 Binary files /dev/null and b/cs/Images/1000_0,1_20_TagCloud.jpg differ diff --git a/cs/Images/1000_0,1_5_TagCloud.jpg b/cs/Images/1000_0,1_5_TagCloud.jpg new file mode 100644 index 000000000..7ad71dc18 Binary files /dev/null and b/cs/Images/1000_0,1_5_TagCloud.jpg differ diff --git a/cs/Images/1000_1_0,1_TagCloud.jpg b/cs/Images/1000_1_0,1_TagCloud.jpg new file mode 100644 index 000000000..3e1749b3f Binary files /dev/null and b/cs/Images/1000_1_0,1_TagCloud.jpg differ diff --git a/cs/Images/1000_1_1_TagCloud.jpg b/cs/Images/1000_1_1_TagCloud.jpg new file mode 100644 index 000000000..33a985799 Binary files /dev/null and b/cs/Images/1000_1_1_TagCloud.jpg differ diff --git a/cs/Images/1000_20_0,001_TagCloud.jpg b/cs/Images/1000_20_0,001_TagCloud.jpg new file mode 100644 index 000000000..0423a6f45 Binary files /dev/null and b/cs/Images/1000_20_0,001_TagCloud.jpg differ diff --git a/cs/Images/1000_20_0,1_TagCloud.jpg b/cs/Images/1000_20_0,1_TagCloud.jpg new file mode 100644 index 000000000..94e473c4d Binary files /dev/null and b/cs/Images/1000_20_0,1_TagCloud.jpg differ diff --git a/cs/TagsCloudVisualization/CloudLayouter/CircularCloudLayouter.cs b/cs/TagsCloudVisualization/CloudLayouter/CircularCloudLayouter.cs new file mode 100644 index 000000000..16eea2ed4 --- /dev/null +++ b/cs/TagsCloudVisualization/CloudLayouter/CircularCloudLayouter.cs @@ -0,0 +1,49 @@ +using System.Drawing; +using TagsCloudVisualization.PointsGenerators; + +namespace TagsCloudVisualization.CloudLayouter; + +public class CircularCloudLayouter : ICircularCloudLayouter +{ + public Point Center { get; } + public List GeneratedRectangles { get; } + private readonly IPointsGenerator spiral; + + public CircularCloudLayouter(Point center) + { + Center = center; + GeneratedRectangles = new List(); + spiral = new SpiralPointsGenerator(center); + } + + public CircularCloudLayouter(Point center, int step, int angleOffset) : this(center) + { + spiral = new SpiralPointsGenerator(center, step, angleOffset); + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0) + throw new ArgumentException($"{nameof(rectangleSize)} height and width must be greater than zero"); + Rectangle rectangle; + do + { + rectangle = GetNextRectangle(rectangleSize); + } while (GeneratedRectangles.Any(rectangle.IntersectsWith)); + GeneratedRectangles.Add(rectangle); + return rectangle; + } + + private Rectangle GetNextRectangle(Size rectangleSize) + { + var rectanglePosition = spiral.GetNextPointPosition(); + return CreateRectangle(rectanglePosition, rectangleSize); + } + + 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); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/CloudLayouter/ICircularCloudLayouter.cs b/cs/TagsCloudVisualization/CloudLayouter/ICircularCloudLayouter.cs new file mode 100644 index 000000000..6289e0961 --- /dev/null +++ b/cs/TagsCloudVisualization/CloudLayouter/ICircularCloudLayouter.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.CloudLayouter; + +public interface ICircularCloudLayouter +{ + Rectangle PutNextRectangle(Size rectangleSize); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/CloudLayouter/ICircularCloudLayouterExtensions.cs b/cs/TagsCloudVisualization/CloudLayouter/ICircularCloudLayouterExtensions.cs new file mode 100644 index 000000000..1deb2567e --- /dev/null +++ b/cs/TagsCloudVisualization/CloudLayouter/ICircularCloudLayouterExtensions.cs @@ -0,0 +1,21 @@ +using System.Drawing; + +namespace TagsCloudVisualization.CloudLayouter; + +public static class ICircularCloudLayouterExtensions +{ + public static void GenerateCloud( + this ICircularCloudLayouter cloudLayouter, + int rectanglesNumber = 1000, + int minRectangleSize = 10, + int maxRectangleSize = 50) + { + var random = new Random(); + new Rectangle[rectanglesNumber] + .Select(x => new Size( + random.Next(minRectangleSize, maxRectangleSize), + random.Next(minRectangleSize, maxRectangleSize))) + .Select(size => cloudLayouter.PutNextRectangle(size)) + .ToArray(); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Images/1000_0.01_0.01_TagCloud.jpg b/cs/TagsCloudVisualization/Images/1000_0.01_0.01_TagCloud.jpg new file mode 100644 index 000000000..d80182bc6 Binary files /dev/null and b/cs/TagsCloudVisualization/Images/1000_0.01_0.01_TagCloud.jpg differ diff --git a/cs/TagsCloudVisualization/Images/1000_0.1_0.1_TagCloud.jpg b/cs/TagsCloudVisualization/Images/1000_0.1_0.1_TagCloud.jpg new file mode 100644 index 000000000..ebce15401 Binary files /dev/null and b/cs/TagsCloudVisualization/Images/1000_0.1_0.1_TagCloud.jpg differ diff --git a/cs/TagsCloudVisualization/Images/1000_10_0.01_TagCloud.jpg b/cs/TagsCloudVisualization/Images/1000_10_0.01_TagCloud.jpg new file mode 100644 index 000000000..2bda72e25 Binary files /dev/null and b/cs/TagsCloudVisualization/Images/1000_10_0.01_TagCloud.jpg differ diff --git a/cs/TagsCloudVisualization/Images/1000_1_1_TagCloud.jpg b/cs/TagsCloudVisualization/Images/1000_1_1_TagCloud.jpg new file mode 100644 index 000000000..4f8a0425e Binary files /dev/null and b/cs/TagsCloudVisualization/Images/1000_1_1_TagCloud.jpg differ diff --git a/cs/TagsCloudVisualization/PointsGenerators/IPointsGenerator.cs b/cs/TagsCloudVisualization/PointsGenerators/IPointsGenerator.cs new file mode 100644 index 000000000..b656a7d6d --- /dev/null +++ b/cs/TagsCloudVisualization/PointsGenerators/IPointsGenerator.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.PointsGenerators; + +public interface IPointsGenerator +{ + Point GetNextPointPosition(); +} diff --git a/cs/TagsCloudVisualization/PointsGenerators/SpiralPointsGenerator.cs b/cs/TagsCloudVisualization/PointsGenerators/SpiralPointsGenerator.cs new file mode 100644 index 000000000..601bfe5bb --- /dev/null +++ b/cs/TagsCloudVisualization/PointsGenerators/SpiralPointsGenerator.cs @@ -0,0 +1,29 @@ +using System.Drawing; + +namespace TagsCloudVisualization.PointsGenerators; + +public class SpiralPointsGenerator : IPointsGenerator +{ + private readonly double step; + private readonly double angleOffset; + private readonly Point center; + private double currentAngle = 0; + + public SpiralPointsGenerator(Point center, double step = 0.1, double angleOffset = 0.1) + { + if (step == 0 || angleOffset == 0) + throw new ArgumentException($"Step and angleOffset must not be zero"); + this.center = center; + this.step = step; + this.angleOffset = angleOffset; + } + + public Point GetNextPointPosition() + { + var radius = step * currentAngle; + var x = (int)(center.X + radius * Math.Cos(currentAngle)); + var y = (int)(center.Y + radius * Math.Sin(currentAngle)); + currentAngle += angleOffset; + return new(x, y); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Program.cs b/cs/TagsCloudVisualization/Program.cs new file mode 100644 index 000000000..3b356dc06 --- /dev/null +++ b/cs/TagsCloudVisualization/Program.cs @@ -0,0 +1,22 @@ +using System.Drawing; +using TagsCloudVisualization.Visualizers; +using TagsCloudVisualization.CloudLayouter; + +namespace TagsCloudVisualization; + +public static class Program +{ + private const int imageWidth = 1500; + private const int imageHeight = 1500; + + public static void Main() + { + var imageSize = new Size(imageWidth, imageHeight); + var center = new Point(imageSize.Width / 2, imageSize.Height / 2); + var layouter = new CircularCloudLayouter(center); + layouter.GenerateCloud(); + var rectangles = layouter.GeneratedRectangles; + var visualizer = new SimpleCloudVisualizer(); + visualizer.CreateBitmap(rectangles, imageSize); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/README.md b/cs/TagsCloudVisualization/README.md new file mode 100644 index 000000000..d1eca449c --- /dev/null +++ b/cs/TagsCloudVisualization/README.md @@ -0,0 +1,4 @@ +1000 прямоугольников шаг 0.1 угловое смещение 0.1 +1000 прямоугольников шаг 0.01 угловое смещение 0.01 +1000 прямоугольников шаг 1 угловое смещение 1 +1000 прямоугольников шаг 10 угловое смещение 0.01 \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagsCloudVisualization.csproj b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj new file mode 100644 index 000000000..56ddd599d --- /dev/null +++ b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + + TagsCloudVisualization.Program + + + + + + + + + + + diff --git a/cs/TagsCloudVisualization/Tests/CircularCloudLayouterTests.cs b/cs/TagsCloudVisualization/Tests/CircularCloudLayouterTests.cs new file mode 100644 index 000000000..bb60389d9 --- /dev/null +++ b/cs/TagsCloudVisualization/Tests/CircularCloudLayouterTests.cs @@ -0,0 +1,145 @@ +using NUnit.Framework.Interfaces; +using FluentAssertions; +using System.Drawing; +using TagsCloudVisualization.CloudLayouter; +using TagsCloudVisualization.Visualizers; +using NUnit.Framework; + +namespace TagsCloudVisualization.Tests.CircularCloudLayouterTests; + +[TestFixture, NonParallelizable] +public class CircularCloudLayouterTests +{ + private CircularCloudLayouter cloudLayouter; + private const int imageWidth = 1500; + private const int imageHeight = 1500; + + [SetUp] + public void Init() + { + var center = new Point(imageWidth / 2, imageHeight / 2); + cloudLayouter = new CircularCloudLayouter(center); + cloudLayouter.GenerateCloud(100); + } + + [TearDown] + public void TearDown() + { + if (TestContext.CurrentContext.Result.Outcome.Status != TestStatus.Failed) + return; + var directory = "FailedVisualisations"; + var path = Path.Combine(directory, $"{TestContext.CurrentContext.Test.Name}_visualisation.png"); + var visuliser = new SimpleCloudVisualizer(); + visuliser.CreateBitmap(cloudLayouter.GeneratedRectangles, new(imageWidth, imageHeight), path); + Console.WriteLine($"Tag cloud visualization saved to file {path}"); + } + + [TestCase(0, 1, TestName = "WhenWidthIsZero")] + [TestCase(1, 0, TestName = "WhenHeightIsZero")] + [TestCase(-1, 1, TestName = "WhenWidthIsNegative")] + [TestCase(1, -1, TestName = "WhenHeightIsNegative")] + public void PutNextRectangle_ShouldThrowArgumentException(int width, int height) + { + var size = new Size(width, height); + + var action = () => cloudLayouter.PutNextRectangle(size); + + action.Should().Throw(); + } + + [Test] + public void PutNextRectangle_FirstRectangle_ShouldBeInCenter() + { + cloudLayouter = new CircularCloudLayouter(cloudLayouter.Center); + var rectangleSize = new Size(10, 10); + var expectedRectangle = new Rectangle( + cloudLayouter.Center.X - rectangleSize.Width / 2, + cloudLayouter.Center.Y - rectangleSize.Height / 2, + rectangleSize.Width, + rectangleSize.Height + ); + + var actualRectangle = cloudLayouter.PutNextRectangle(rectangleSize); + + actualRectangle.Should().BeEquivalentTo(expectedRectangle); + } + + [Test, Parallelizable(ParallelScope.Self)] + [Repeat(10)] + public void PutNextRectangle_Rectangles_ShouldNotHaveIntersects() => + AreRectanglesHaveIntersects(cloudLayouter.GeneratedRectangles).Should().BeFalse(); + + [Test] + [Repeat(10)] + public void PutNextRectangle_CloudCenterMust_ShouldBeInLayoterCenter() + { + var maxRectangleSize = 10; + var expectedDiscrepancy = maxRectangleSize; + var minRectangleSize = 1; + var center = cloudLayouter.Center; + + cloudLayouter.GenerateCloud(100, minRectangleSize, maxRectangleSize); + + var actualCenter = GetCenterOfAllRectangles(cloudLayouter.GeneratedRectangles); + actualCenter.X.Should().BeInRange(center.X - expectedDiscrepancy, center.X + expectedDiscrepancy); + actualCenter.Y.Should().BeInRange(center.Y - expectedDiscrepancy, center.Y + expectedDiscrepancy); + } + + [Test] + [Repeat(10)] + public void PutNextRectangle_RectanglesDensity_ShouldBeMax() + { + var expectedDensity = 0.45; + var center = cloudLayouter.Center; + var rectangles = cloudLayouter.GeneratedRectangles; + + var rectanglesArea = rectangles.Sum(rect => rect.Width * rect.Height); + + var radius = GetMaxDistanceBetweenRectangleAndCenter(rectangles); + var circleArea = Math.PI * radius * radius; + var density = rectanglesArea / circleArea; + density.Should().BeGreaterThanOrEqualTo(expectedDensity); + } + + private Point GetCenterOfAllRectangles(List rectangles) + { + var top = rectangles.Max(r => r.Top); + var right = rectangles.Max(r => r.Right); + var bottom = rectangles.Min(r => r.Bottom); + var left = rectangles.Min(r => r.Left); + var x = left + (right - left) / 2; + var y = bottom + (top - bottom) / 2; + return new(x, y); + } + + private double GetMaxDistanceBetweenRectangleAndCenter(List rectangles) + { + var center = GetCenterOfAllRectangles(rectangles); + double maxDistance = -1; + foreach (var rectangle in rectangles) + { + var corners = new Point[4] + { + new(rectangle.Top, rectangle.Left), + new(rectangle.Bottom, rectangle.Left), + new(rectangle.Top, rectangle.Right), + new(rectangle.Bottom, rectangle.Right) + }; + var distance = corners.Max(p => GetDistanceBetweenPoints(p, center)); + maxDistance = Math.Max(maxDistance, distance); + } + return maxDistance; + } + + private static bool AreRectanglesHaveIntersects(List rectangles) + { + for (var i = 0; i < rectangles.Count; i++) + for (var j = i + 1; j < rectangles.Count; j++) + if (rectangles[i].IntersectsWith(rectangles[j])) + return true; + return false; + } + + private static double GetDistanceBetweenPoints(Point point1, Point point2) + => Math.Sqrt(Math.Pow(point1.X - point2.X, 2) + Math.Pow(point1.Y - point2.Y, 2)); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Tests/SpiralPointsGeneratorTests.cs b/cs/TagsCloudVisualization/Tests/SpiralPointsGeneratorTests.cs new file mode 100644 index 000000000..bb9ef0565 --- /dev/null +++ b/cs/TagsCloudVisualization/Tests/SpiralPointsGeneratorTests.cs @@ -0,0 +1,43 @@ +using FluentAssertions; +using System.Drawing; +using TagsCloudVisualization.PointsGenerators; +using NUnit.Framework; + +namespace TagsCloudVisualization.Tests.SpiralPointsGeneratorTests; + +[TestFixture, Parallelizable(ParallelScope.All)] +public class SpiralPointsGeneratorTests +{ + [TestCase(0, 1, TestName = "WhenStepIsZero")] + [TestCase(1, 0, TestName = "WhenAngleOffsetIsZero")] + public void Constructor_ShouldThrowArgumentException(double step, double angleOffset) + { + var act = () => new SpiralPointsGenerator(new Point(0, 0), step, angleOffset); + + act.Should().Throw(); + } + + [TestCaseSource(nameof(GeneratePointsTestCases))] + public void GetNextPointPosition_ShouldReturnCorrectPoint(double step, double angleOffset, int pointNumber, Point expectedPoint) + { + var pointsGenerator = new SpiralPointsGenerator(new Point(0, 0), step, angleOffset); + + var actualPoint = pointsGenerator.GetNextPointPosition(); + for (var i = 0; i < pointNumber - 1; i++) + actualPoint = pointsGenerator.GetNextPointPosition(); + + actualPoint.Should().Be(expectedPoint); + } + + public static TestCaseData[] GeneratePointsTestCases = + { + new TestCaseData(0.1, 0.1, 1, new Point(0, 0)), + new TestCaseData(1, 1, 1, new Point(0, 0)), + new TestCaseData(1, 1, 3, new Point(0, 1)), + new TestCaseData(1, 1, 5, new Point(-2, -3)), + new TestCaseData(3, 1, 3, new Point(-2, 5)), + new TestCaseData(3, 1, 5, new Point(-7, -9)), + new TestCaseData(5, 1, 3, new Point(-4, 9)), + new TestCaseData(5, 1, 5, new Point(-13, -15)), + }; +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Visualizers/SimpleCloudVisualizer.cs b/cs/TagsCloudVisualization/Visualizers/SimpleCloudVisualizer.cs new file mode 100644 index 000000000..6b556a291 --- /dev/null +++ b/cs/TagsCloudVisualization/Visualizers/SimpleCloudVisualizer.cs @@ -0,0 +1,42 @@ +using System.Drawing; +using System.Drawing.Imaging; + +namespace TagsCloudVisualization.Visualizers; + +public class SimpleCloudVisualizer +{ + private const string imagesDirectory = "images"; + + public void CreateBitmap( + IEnumerable rectangles, + Size bitmapSize, + string path = null + ) + { + var bitmap = new Bitmap(bitmapSize.Width, bitmapSize.Height); + using (var graphics = Graphics.FromImage(bitmap)) + { + graphics.Clear(Color.White); + foreach (var rectangle in rectangles) + { + var pen = new Pen(GetRandomColor()); + graphics.DrawRectangle(pen, rectangle); + } + var currentPath = path == null ? GetPathToImages(rectangles.Count()) : path; + Directory.CreateDirectory(Path.GetDirectoryName(currentPath)); + bitmap.Save((string)currentPath, ImageFormat.Jpeg); + } + } + + private static Color GetRandomColor() + { + var random = new Random(); + return Color.FromArgb(random.Next(0, 255), random.Next(0, 255), random.Next(0, 255)); + } + + private static string GetPathToImages(int rectanglesNumber) + { + var filename = $"{rectanglesNumber}_TagCloud.jpg"; + return Path.Combine(imagesDirectory, filename); + } +} \ No newline at end of file diff --git a/cs/tdd.sln b/cs/tdd.sln index c8f523d63..6c1571ad9 100644 --- a/cs/tdd.sln +++ b/cs/tdd.sln @@ -1,11 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35327.3 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BowlingGame", "BowlingGame\BowlingGame.csproj", "{AD0F018A-732E-4074-8527-AB2EEC8D0BF3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{B5108E20-2ACF-4ED9-84FE-2A718050FC94}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagsCloudVisualization", "TagsCloudVisualization\TagsCloudVisualization.csproj", "{AADB4D6C-687B-4CED-B181-255503CCA7B2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -13,14 +11,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {AD0F018A-732E-4074-8527-AB2EEC8D0BF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD0F018A-732E-4074-8527-AB2EEC8D0BF3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD0F018A-732E-4074-8527-AB2EEC8D0BF3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD0F018A-732E-4074-8527-AB2EEC8D0BF3}.Release|Any CPU.Build.0 = Release|Any CPU - {B5108E20-2ACF-4ED9-84FE-2A718050FC94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B5108E20-2ACF-4ED9-84FE-2A718050FC94}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B5108E20-2ACF-4ED9-84FE-2A718050FC94}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B5108E20-2ACF-4ED9-84FE-2A718050FC94}.Release|Any CPU.Build.0 = Release|Any CPU + {AADB4D6C-687B-4CED-B181-255503CCA7B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AADB4D6C-687B-4CED-B181-255503CCA7B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AADB4D6C-687B-4CED-B181-255503CCA7B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AADB4D6C-687B-4CED-B181-255503CCA7B2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE