diff --git a/cs/TagsCloudTests/CircularCloudLayouterTests.cs b/cs/TagsCloudTests/CircularCloudLayouterTests.cs new file mode 100644 index 000000000..bf48ec326 --- /dev/null +++ b/cs/TagsCloudTests/CircularCloudLayouterTests.cs @@ -0,0 +1,89 @@ +using System.Drawing; +using System.Drawing.Imaging; +using FluentAssertions; +using NUnit.Framework.Interfaces; +using TagsCloud; + +namespace TagsCloudTests; + +public class CircularCloudLayouterTests +{ + private ISpiral spiral; + private ICircularCloudLayouter sut; + + [SetUp] + public void SetUp() + { + spiral = new Spiral(new Point(10, 10)); + sut = new CircularCloudLayouter(spiral); + } + + [TearDown] + public void TearDown() + { + if (TestContext.CurrentContext.Result.Outcome != ResultState.Failure) return; + var workingDirectory = Environment.CurrentDirectory; + var parentDirectory = Directory.GetParent(workingDirectory)?.Parent; + + var directoryPath = parentDirectory != null + ? Path.Combine(parentDirectory.FullName, "FailedTestImages") + : Path.Combine(workingDirectory, "FailedTestImages"); + + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + var imageName = TestContext.CurrentContext.Test.Name; + var imagePath = Path.Combine(directoryPath, $"{imageName}.png"); + + RectanglesVisualizer.GetTagsCloudImage(sut.Rectangles).Save(imagePath, ImageFormat.Png); + + Console.WriteLine($"Tag cloud visualization saved to file {imagePath}"); + } + + + private static bool IsRectanglesIntersect(List rectangles) => + rectangles.Any(rectangle => rectangles.Any(nextRectangle => + nextRectangle.IntersectsWith(rectangle) && !rectangle.Equals(nextRectangle))); + + + [Test] + public void GetLocationAfterInitialization_ShouldBeEmpty() + { + var location = sut.GetRectanglesLocation(); + location.Should().BeEmpty(); + } + + [TestCase(-1, 10, TestName = "width is negative")] + [TestCase(1, -10, TestName = "height is negative")] + [TestCase(1, 0, TestName = "Zero height, correct width")] + [TestCase(0, 10, TestName = "Zero width, correct height")] + public void PutRectangleWithNegativeParams_ShouldBeThrowException(int width, int height) + { + var size = new Size(width, height); + Action action = () => sut.PutNextRectangle(size); + action.Should().Throw() + .WithMessage("Sides of the rectangle should not be non-positive"); + } + + [Test] + public void PutOneRectangle_IsNotEmpty() + { + var rectangle = sut.PutNextRectangle(new Size(10, 10)); + var location = sut.GetRectanglesLocation(); + location.Should().NotBeEmpty(); + } + + [Test] + public void Put1000Rectangles_RectanglesShouldNotIntersect() + { + for (var i = 0; i < 1000; i++) + { + var size = Utils.GetRandomSize(); + var rectangle = sut.PutNextRectangle(size); + } + + IsRectanglesIntersect(sut.Rectangles).Should().BeFalse(); + } +} \ No newline at end of file diff --git a/cs/TagsCloudTests/GlobalUsings.cs b/cs/TagsCloudTests/GlobalUsings.cs new file mode 100644 index 000000000..cefced496 --- /dev/null +++ b/cs/TagsCloudTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/cs/TagsCloudTests/RectanglesVisualizerTests.cs b/cs/TagsCloudTests/RectanglesVisualizerTests.cs new file mode 100644 index 000000000..daf6550ae --- /dev/null +++ b/cs/TagsCloudTests/RectanglesVisualizerTests.cs @@ -0,0 +1,53 @@ +using System.Drawing; +using System.Drawing.Imaging; +using FluentAssertions; +using TagsCloud; + +namespace TagsCloudTests; + +public class RectanglesVisualizerTests +{ + private const int MinCoordinate = 0; + private const int MaxCoordinate = 5000; + private static readonly Random Random = new(); + + [Test] + public void GetTagsCloudImage_DrawImageWithoutRectangles_EmptyImage() + { + var rectangles = new List(); + var expected = BitmapToByteArray(new Bitmap(100, 100)); + var result = BitmapToByteArray(RectanglesVisualizer.GetTagsCloudImage(rectangles)); + result.Should().BeEquivalentTo(expected); + } + + [Test] + public void GetTagsCloudImage_DrawSomeRectangles_AllRectanglesInImage() + { + var count = 10; + var rectangles = new List(); + for (var i = 0; i < count; i++) + { + var locate = GetRandomLocation(); + var size = Utils.GetRandomSize(); + var rect = new Rectangle(locate, size); + rectangles.Add(rect); + } + + var image = RectanglesVisualizer.GetTagsCloudImage(rectangles); + CheckImageBorders(rectangles, image).Should().BeTrue(); + } + + private static Point GetRandomLocation() => + new Point(Random.Next(MinCoordinate, MaxCoordinate), Random.Next(MinCoordinate, MaxCoordinate)); + + private static byte[] BitmapToByteArray(Bitmap bitmap) + { + using var stream = new MemoryStream(); + bitmap.Save(stream, ImageFormat.Png); + return stream.ToArray(); + } + + private static bool CheckImageBorders(List rectangles, Bitmap image) => + rectangles.Max(rectangle => rectangle.Bottom) < image.Height && + rectangles.Max(rectangle => rectangle.Right) < image.Width; +} \ No newline at end of file diff --git a/cs/TagsCloudTests/SpiralTests.cs b/cs/TagsCloudTests/SpiralTests.cs new file mode 100644 index 000000000..53b3c50b4 --- /dev/null +++ b/cs/TagsCloudTests/SpiralTests.cs @@ -0,0 +1,46 @@ +using System.Drawing; +using FluentAssertions; +using TagsCloud; + +namespace TagsCloudTests; + +public class SpiralTests +{ + private Point center; + + [SetUp] + public void Setup() + { + center = new Point(10, 10); + } + + private static IEnumerable ConstructorSpiralPoints => new[] + { + new TestCaseData(2.5f, + new Point[] { new(10, 10), new(5, 14), new(14, -2), new(16, 28), new(-11, -4), new(41, 8) }) + .SetName("AngleStep is positive"), + new TestCaseData(-2.5f, + new Point[] { new(10, 10), new(5, 6), new(14, 22), new(16, -8), new(-11, 24), new(41, 12) }) + .SetName("AngleStep is negative") + }; + + [Test] + public void Spiral_StepAngleEquals0_ShouldBeThrowException() + { + Action action = () => new Spiral(center, 0); + action.Should().Throw() + .WithMessage("the step must not be equal to 0"); + } + + [TestCaseSource(nameof(ConstructorSpiralPoints))] + public void Spiral_GetNextPoint_CreatePointsWithCustomAngle_ReturnsCorrectPoints(float angleStep, + Point[] expectedPoints) + { + var spiral = new Spiral(new Point(10, 10), angleStep); + var resultPoints = new Point[expectedPoints.Length]; + for (var i = 0; i < resultPoints.Length; i++) + resultPoints[i] = spiral.GetPoint(); + + resultPoints.Should().BeEquivalentTo(expectedPoints); + } +} \ No newline at end of file diff --git a/cs/TagsCloudTests/TagsCloudTests.csproj b/cs/TagsCloudTests/TagsCloudTests.csproj new file mode 100644 index 000000000..5a54db322 --- /dev/null +++ b/cs/TagsCloudTests/TagsCloudTests.csproj @@ -0,0 +1,22 @@ + + + + net6-windows + enable + enable + + false + true + + + + + + + + + + + + + diff --git a/cs/tagsCloud/CircularCloudLayouter.cs b/cs/tagsCloud/CircularCloudLayouter.cs new file mode 100644 index 000000000..b0dd0a203 --- /dev/null +++ b/cs/tagsCloud/CircularCloudLayouter.cs @@ -0,0 +1,46 @@ +using System.Drawing; + +namespace TagsCloud; + +public class CircularCloudLayouter : ICircularCloudLayouter +{ + public List Rectangles { get; } + + private readonly ISpiral spiral; + + public CircularCloudLayouter(ISpiral spiral) + { + this.spiral = spiral; + Rectangles = new List(); + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.Height <= 0 || rectangleSize.Width <= 0) + throw new ArgumentException("Sides of the rectangle should not be non-positive"); + var rectangles = CreateNextRectangle(rectangleSize); + Rectangles.Add(rectangles); + return rectangles; + } + + private Rectangle CreateNextRectangle(Size rectangleSize) + { + var point = spiral.GetPoint(); + var rectangles = new Rectangle(point, rectangleSize); + while (!HasNoIntersections(rectangles)) + { + point = spiral.GetPoint(); + rectangles = new Rectangle(point, rectangleSize); + } + + return rectangles; + } + + private bool HasNoIntersections(Rectangle rectangles) + { + for (var i = Rectangles.Count - 1; i >= 0; i--) + if (Rectangles[i].IntersectsWith(rectangles)) + return false; + return true; + } +} \ No newline at end of file diff --git a/cs/tagsCloud/CloudLayouterExtension.cs b/cs/tagsCloud/CloudLayouterExtension.cs new file mode 100644 index 000000000..9a2361902 --- /dev/null +++ b/cs/tagsCloud/CloudLayouterExtension.cs @@ -0,0 +1,11 @@ +using System.Drawing; + +namespace TagsCloud; + +public static class CloudLayouterExtension +{ + public static List GetRectanglesLocation(this ICircularCloudLayouter layouter) + { + return layouter.Rectangles.Select(rectangle => rectangle.Location).ToList(); + } +} \ No newline at end of file diff --git a/cs/tagsCloud/ICircularCloudLayouter.cs b/cs/tagsCloud/ICircularCloudLayouter.cs new file mode 100644 index 000000000..103619a6b --- /dev/null +++ b/cs/tagsCloud/ICircularCloudLayouter.cs @@ -0,0 +1,9 @@ +using System.Drawing; + +namespace TagsCloud; + +public interface ICircularCloudLayouter +{ + List Rectangles { get; } + Rectangle PutNextRectangle(Size rectangleSize); +} \ No newline at end of file diff --git a/cs/tagsCloud/ISpiral.cs b/cs/tagsCloud/ISpiral.cs new file mode 100644 index 000000000..cb2d4cb80 --- /dev/null +++ b/cs/tagsCloud/ISpiral.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloud; + +public interface ISpiral +{ + Point GetPoint(); +} \ No newline at end of file diff --git a/cs/tagsCloud/MainProgram.cs b/cs/tagsCloud/MainProgram.cs new file mode 100644 index 000000000..75e9ad9c8 --- /dev/null +++ b/cs/tagsCloud/MainProgram.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Drawing.Imaging; + +namespace TagsCloud; + +public class MainProgram +{ + public static void Main(string[] args) + { + var spiral = new Spiral(new Point(100, 100)); + var layout = new CircularCloudLayouter(spiral); + + for (var i = 0; i < 10000; i++) + { + var rectangle = layout.PutNextRectangle(Utils.GetRandomSize()); + } + + var workingDirectory = Environment.CurrentDirectory; + var imagesDirectoryPath = Path.Combine(workingDirectory, "images"); + + if (!Directory.Exists(imagesDirectoryPath)) + { + Directory.CreateDirectory(imagesDirectoryPath); + } + + const string imageName = "10rect"; + var imagePath = Path.Combine(imagesDirectoryPath, $"{imageName}.png"); + + using var image = RectanglesVisualizer.GetTagsCloudImage(layout.Rectangles); + image.Save(imagePath, ImageFormat.Png); + } +} \ No newline at end of file diff --git a/cs/tagsCloud/README.md b/cs/tagsCloud/README.md new file mode 100644 index 000000000..eda34029e --- /dev/null +++ b/cs/tagsCloud/README.md @@ -0,0 +1,4 @@ +![10000 random rectangles](https://github.com/PavelUd/tdd/blob/master/cs/tagsCloud/images/10000rect.png) +![1000 random rectangles](https://github.com/PavelUd/tdd/blob/master/cs/tagsCloud/images/1000rect.png) +![100 random rectangles](https://github.com/PavelUd/tdd/blob/master/cs/tagsCloud/images/100rect.png) +![10 random rectangles](https://github.com/PavelUd/tdd/blob/master/cs/tagsCloud/images/10rect.png) \ No newline at end of file diff --git a/cs/tagsCloud/RectanglesVisualizer.cs b/cs/tagsCloud/RectanglesVisualizer.cs new file mode 100644 index 000000000..2bc61a85f --- /dev/null +++ b/cs/tagsCloud/RectanglesVisualizer.cs @@ -0,0 +1,62 @@ +using System.Drawing; + +namespace TagsCloud; + +public static class RectanglesVisualizer +{ + private const int Border = 50; + + public static Bitmap GetTagsCloudImage(List rectangles) + { + if (!rectangles.Any()) return new Bitmap(100, 100); + + var extremePoints = GetRectanglesExtremePoints(rectangles); + var sizeImage = GetImageSize(extremePoints); + var shift = GetRectanglesShift(extremePoints); + var image = new Bitmap(sizeImage.Width, sizeImage.Height); + using var graphics = Graphics.FromImage(image); + var background = new SolidBrush(Color.Black); + graphics.FillRectangle(background, new Rectangle(0, 0, image.Width, image.Height)); + DrawTagsCloud(rectangles, graphics, shift); + + return image; + } + + private static (int Left, int Right, int Top, int Bottom) GetRectanglesExtremePoints(List rectangles) + { + var leftmost = rectangles.Min(rectangle => rectangle.Left); + var rightmost = rectangles.Max(rectangle => rectangle.Right); + var topmost = rectangles.Min(rectangle => rectangle.Top); + var bottommost = rectangles.Max(rectangle => rectangle.Bottom); + + return (leftmost, rightmost, topmost, bottommost); + } + + private static Point GetRectanglesShift((int Left, int Right, int Top, int Bottom) extremePoints) + { + var startX = extremePoints.Top >= 0 ? 0 : extremePoints.Top; + var startY = extremePoints.Left >= 0 ? 0 : extremePoints.Left; + + return new Point(Math.Abs(startX) + Border, Math.Abs(startY) + Border); + } + + + private static Size GetImageSize((int Left, int Right, int Top, int Bottom) extremePoints) + { + var height = Math.Abs(extremePoints.Bottom) + Math.Abs(extremePoints.Top) + 2 * Border; + var width = Math.Abs(extremePoints.Right) + Math.Abs(extremePoints.Left) + 2 * Border; + + return new Size(width, height); + } + + private static void DrawTagsCloud(List rectangles, Graphics graphics, Point shift) + { + foreach (var rectangle in rectangles) + { + var renderedRectangle = + new Rectangle(new Point(rectangle.X + shift.X, rectangle.Y + shift.Y), rectangle.Size); + using var pen = new Pen(Utils.GetRandomColor()); + graphics.DrawRectangle(pen, renderedRectangle); + } + } +} \ No newline at end of file diff --git a/cs/tagsCloud/Spiral.cs b/cs/tagsCloud/Spiral.cs new file mode 100644 index 000000000..188cc08e8 --- /dev/null +++ b/cs/tagsCloud/Spiral.cs @@ -0,0 +1,28 @@ +using System.Drawing; + +namespace TagsCloud; + +public class Spiral : ISpiral +{ + private int counter; + private readonly float step; + private readonly Point center; + + public Spiral(Point center, float step = 0.1f) + { + this.center = center; + if (step == 0) + throw new ArgumentException("the step must not be equal to 0"); + this.step = step; + } + + public Point GetPoint() + { + var angle = step * counter; + var xOffset = (float)(step * angle * Math.Cos(angle)); + var yOffset = (float)(step * angle * Math.Sin(angle)); + var point = new Point((int)Math.Round(center.X + xOffset), (int)Math.Round(center.Y + yOffset)); + counter += 1; + return point; + } +} \ No newline at end of file diff --git a/cs/tagsCloud/TagsCloud.csproj b/cs/tagsCloud/TagsCloud.csproj new file mode 100644 index 000000000..06eb4efbb --- /dev/null +++ b/cs/tagsCloud/TagsCloud.csproj @@ -0,0 +1,15 @@ + + + + net6-windows + enable + enable + Exe + TagsCloud + + + + + + + diff --git a/cs/tagsCloud/Utils.cs b/cs/tagsCloud/Utils.cs new file mode 100644 index 000000000..6a17005f2 --- /dev/null +++ b/cs/tagsCloud/Utils.cs @@ -0,0 +1,20 @@ +using System.Drawing; + +namespace TagsCloud; + +public class Utils +{ + private static readonly Random Random = new(); + private const int MinSize = 1; + private const int MaxSize = 50; + + + public static Color GetRandomColor() => Color.FromArgb(GetShade(), GetShade(), GetShade()); + + + private static int GetShade() => Random.Next(0, 256); + + + public static Size GetRandomSize() => + new Size(Random.Next(MinSize, MaxSize), Random.Next(MinSize, MaxSize)); +} \ No newline at end of file diff --git a/cs/tagsCloud/images/10000rect.png b/cs/tagsCloud/images/10000rect.png new file mode 100644 index 000000000..80fd7f1af Binary files /dev/null and b/cs/tagsCloud/images/10000rect.png differ diff --git a/cs/tagsCloud/images/1000rect.png b/cs/tagsCloud/images/1000rect.png new file mode 100644 index 000000000..cd224d94f Binary files /dev/null and b/cs/tagsCloud/images/1000rect.png differ diff --git a/cs/tagsCloud/images/100rect.png b/cs/tagsCloud/images/100rect.png new file mode 100644 index 000000000..344daf30c Binary files /dev/null and b/cs/tagsCloud/images/100rect.png differ diff --git a/cs/tagsCloud/images/10rect.png b/cs/tagsCloud/images/10rect.png new file mode 100644 index 000000000..08a77536b Binary files /dev/null and b/cs/tagsCloud/images/10rect.png differ diff --git a/cs/tdd.sln b/cs/tdd.sln index c8f523d63..80d0bb998 100644 --- a/cs/tdd.sln +++ b/cs/tdd.sln @@ -7,6 +7,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BowlingGame", "BowlingGame\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{B5108E20-2ACF-4ED9-84FE-2A718050FC94}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloud", "TagsCloud\TagsCloud.csproj", "{A748FAA4-6ABE-4157-BCC5-5C24761F948F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudTests", "TagsCloudTests\TagsCloudTests.csproj", "{E1F8AF7B-F67E-41ED-B0E7-04116CE9A273}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +25,14 @@ Global {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 + {A748FAA4-6ABE-4157-BCC5-5C24761F948F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A748FAA4-6ABE-4157-BCC5-5C24761F948F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A748FAA4-6ABE-4157-BCC5-5C24761F948F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A748FAA4-6ABE-4157-BCC5-5C24761F948F}.Release|Any CPU.Build.0 = Release|Any CPU + {E1F8AF7B-F67E-41ED-B0E7-04116CE9A273}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1F8AF7B-F67E-41ED-B0E7-04116CE9A273}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1F8AF7B-F67E-41ED-B0E7-04116CE9A273}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1F8AF7B-F67E-41ED-B0E7-04116CE9A273}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE