diff --git a/cs/TagCloud/CircularCloudLayouter.cs b/cs/TagCloud/CircularCloudLayouter.cs new file mode 100644 index 000000000..3d593658e --- /dev/null +++ b/cs/TagCloud/CircularCloudLayouter.cs @@ -0,0 +1,38 @@ +using System.Drawing; + +namespace TagCloud; + +public class CircularCloudLayouter : ICircularCloudLayouter +{ + private readonly IPointGenerator pointGenerator; + public IList Rectangles { get; } + + public CircularCloudLayouter(IPointGenerator pointGenerator) + { + Rectangles = new List(); + this.pointGenerator = pointGenerator; + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.Height <= 0 || rectangleSize.Width <= 0) + { + throw new ArgumentException("Rectangle size parameters should be positive"); + } + + var rectangle = new Rectangle(pointGenerator.GetNextPoint(), rectangleSize); + while (!CanPlaceRectangle(rectangle)) + { + rectangle = new Rectangle(pointGenerator.GetNextPoint() - rectangleSize / 2, rectangleSize); + } + + Rectangles.Add(rectangle); + + return rectangle; + } + + private bool CanPlaceRectangle(Rectangle newRectangle) + { + return !Rectangles.Any(rectangle => rectangle.IntersectsWith(newRectangle)); + } +} \ No newline at end of file diff --git a/cs/TagCloud/CloudDrawer.cs b/cs/TagCloud/CloudDrawer.cs new file mode 100644 index 000000000..eb1956628 --- /dev/null +++ b/cs/TagCloud/CloudDrawer.cs @@ -0,0 +1,46 @@ +using System.Drawing; + +namespace TagCloud; + +public static class CloudDrawer +{ + public static Bitmap DrawTagCloud(IList rectangles, int border = 10) + { + var imageSize = GetImageSize(rectangles, border); + var shift = GetImageShift(rectangles, border); + var image = new Bitmap(imageSize.Width, imageSize.Height); + using var graphics = Graphics.FromImage(image); + foreach (var rectangle in rectangles) + { + var shiftedCoordinates = new Point(rectangle.X - shift.Width, rectangle.Y - shift.Height); + using var brush = new SolidBrush(GetRandomColor()); + graphics.FillRectangle(brush, new Rectangle(shiftedCoordinates, rectangle.Size)); + } + + return image; + } + + private static Size GetImageShift(IList rectangles, int border) + { + var minX = rectangles.Min(rectangle => rectangle.Left); + var minY = rectangles.Min(rectangle => rectangle.Top); + + return new Size(minX - border, minY - border); + } + + private static Size GetImageSize(IList rectangles, int border) + { + var minX = rectangles.Min(rectangle => rectangle.Left); + var maxX = rectangles.Max(rectangle => rectangle.Right); + var minY = rectangles.Min(rectangle => rectangle.Top); + var maxY = rectangles.Max(rectangle => rectangle.Bottom); + + return new Size(maxX - minX + 2 * border, maxY - minY + 2 * border); + } + + private static Color GetRandomColor() + { + var random = new Random(); + return Color.FromArgb(random.Next(0, 255), random.Next(0, 255), random.Next(0, 255)); + } +} \ No newline at end of file diff --git a/cs/TagCloud/ICircularCloudLayouter.cs b/cs/TagCloud/ICircularCloudLayouter.cs new file mode 100644 index 000000000..dee68e17f --- /dev/null +++ b/cs/TagCloud/ICircularCloudLayouter.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagCloud; + +public interface ICircularCloudLayouter +{ + Rectangle PutNextRectangle(Size rectangleSize); +} \ No newline at end of file diff --git a/cs/TagCloud/IPointGenerator.cs b/cs/TagCloud/IPointGenerator.cs new file mode 100644 index 000000000..b584fd208 --- /dev/null +++ b/cs/TagCloud/IPointGenerator.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagCloud; + +public interface IPointGenerator +{ + Point GetNextPoint(); +} \ No newline at end of file diff --git a/cs/TagCloud/Program.cs b/cs/TagCloud/Program.cs new file mode 100644 index 000000000..d0d2e1331 --- /dev/null +++ b/cs/TagCloud/Program.cs @@ -0,0 +1,36 @@ +using System.Drawing; + +namespace TagCloud +{ + public class Program + { + private const int Width = 1920; + private const int Height = 1080; + + static void Main(string[] args) + { + var layouter = new CircularCloudLayouter(new SpiralGenerator(new Point(Width / 2, Height / 2), 1, 0.01)); + + var random = new Random(); + + for (var i = 0; i < 150; i++) + { + layouter.PutNextRectangle(new Size(50 + random.Next(0, 100), 50 + random.Next(0, 100))); + } + + var filename = "Sample"; + var path = @$"{Environment.CurrentDirectory}\..\..\..\Samples"; + var absPath = Path.GetFullPath(path); + + if (!Directory.Exists(absPath)) + { + Directory.CreateDirectory(absPath); + } + + absPath += @$"\{filename}.png"; + + using var bitmap = CloudDrawer.DrawTagCloud(layouter.Rectangles); + bitmap.Save(absPath); + } + } +} \ No newline at end of file diff --git a/cs/TagCloud/Samples/150 Equal rectangles.png b/cs/TagCloud/Samples/150 Equal rectangles.png new file mode 100644 index 000000000..46ef87fc8 Binary files /dev/null and b/cs/TagCloud/Samples/150 Equal rectangles.png differ diff --git a/cs/TagCloud/Samples/150 Random rectangles.png b/cs/TagCloud/Samples/150 Random rectangles.png new file mode 100644 index 000000000..c73d357a6 Binary files /dev/null and b/cs/TagCloud/Samples/150 Random rectangles.png differ diff --git a/cs/TagCloud/Samples/150 Squares.png b/cs/TagCloud/Samples/150 Squares.png new file mode 100644 index 000000000..42b07fcc6 Binary files /dev/null and b/cs/TagCloud/Samples/150 Squares.png differ diff --git a/cs/TagCloud/Samples/50 Random rectangles.png b/cs/TagCloud/Samples/50 Random rectangles.png new file mode 100644 index 000000000..c664557ac Binary files /dev/null and b/cs/TagCloud/Samples/50 Random rectangles.png differ diff --git a/cs/TagCloud/Samples/500 Random rectangles.png b/cs/TagCloud/Samples/500 Random rectangles.png new file mode 100644 index 000000000..d3657d09e Binary files /dev/null and b/cs/TagCloud/Samples/500 Random rectangles.png differ diff --git a/cs/TagCloud/Samples/README.md b/cs/TagCloud/Samples/README.md new file mode 100644 index 000000000..150966f9e --- /dev/null +++ b/cs/TagCloud/Samples/README.md @@ -0,0 +1,16 @@ +## Layout examples + +#### 150 Equal reactangles: +![150_equal_rectangles](https://github.com/HardreaM/tdd/blob/master/cs/TagCloud/Samples/150%20Equal%20rectangles.png) + +#### 50 Random reactangles: +![50_random_rectangles](https://github.com/HardreaM/tdd/blob/master/cs/TagCloud/Samples/50%20Random%20rectangles.png) + +#### 150 Random reactangles: +![150_random_rectangles](https://github.com/HardreaM/tdd/blob/master/cs/TagCloud/Samples/150%20Random%20rectangles.png) + +#### 500 Random reactangles: +![500_random_rectangles](https://github.com/HardreaM/tdd/blob/master/cs/TagCloud/Samples/500%20Random%20rectangles.png) + +#### 150 Squares: +![150_squares](https://github.com/HardreaM/tdd/blob/master/cs/TagCloud/Samples/150%20Squares.png) \ No newline at end of file diff --git a/cs/TagCloud/SpiralGenerator.cs b/cs/TagCloud/SpiralGenerator.cs new file mode 100644 index 000000000..74302738d --- /dev/null +++ b/cs/TagCloud/SpiralGenerator.cs @@ -0,0 +1,30 @@ +using System.Drawing; + +namespace TagCloud; + +public class SpiralGenerator : IPointGenerator +{ + private readonly Point startPoint; + private readonly int spiralDensity; + private readonly double angleShift; + private double currentAngle; + + public SpiralGenerator(Point startPoint, int spiralDensity = 1, double angleShift = 0.1) + { + if (startPoint.X < 0 || startPoint.Y < 0) + throw new ArgumentException("Spiral center point coordinates should be non-negative"); + this.startPoint = startPoint; + this.spiralDensity = spiralDensity; + this.angleShift = angleShift; + } + + public Point GetNextPoint() + { + var radius = spiralDensity * currentAngle; + var x = (int)(Math.Cos(currentAngle) * radius); + var y = (int)(Math.Sin(currentAngle) * radius); + currentAngle += angleShift; + + return new Point(startPoint.X + x, startPoint.Y + y); + } +} \ No newline at end of file diff --git a/cs/TagCloud/TagCloud.csproj b/cs/TagCloud/TagCloud.csproj new file mode 100644 index 000000000..30ddccc44 --- /dev/null +++ b/cs/TagCloud/TagCloud.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0-windows + enable + enable + + + + + + + diff --git a/cs/TagCloudTests/CircularCloudLayouter_Should.cs b/cs/TagCloudTests/CircularCloudLayouter_Should.cs new file mode 100644 index 000000000..5c802912f --- /dev/null +++ b/cs/TagCloudTests/CircularCloudLayouter_Should.cs @@ -0,0 +1,96 @@ +using NUnit.Framework.Interfaces; + +[TestFixture] +public class CircularCloudLayouter_Should +{ + private const int Width = 1920; + private const int Height = 1080; + private CircularCloudLayouter sut; + + + [SetUp] + public void Setup() + { + sut = new CircularCloudLayouter(new SpiralGenerator(new Point(Width / 2, Height / 2))); + } + + [TearDown] + public void TearDown() + { + if (TestContext.CurrentContext.Result.Outcome == ResultState.Failure) + { + var bitmap = CloudDrawer.DrawTagCloud(sut.Rectangles); + + var path = @$"{Environment.CurrentDirectory}\..\..\..\FailedTests\{this.GetType()}"; + var absPath = Path.GetFullPath(path); + + if (!Directory.Exists(absPath)) + { + Directory.CreateDirectory(absPath); + } + + var fileName = TestContext.CurrentContext.Test.Name; + absPath += @$"\{fileName}.png"; + + bitmap.Save(absPath); + + TestContext.Out.WriteLine($"Tag cloud visualization saved to file <{absPath}>"); + } + } + + [Test] + public void HaveEmptyRectanglesList_WhenCreated() + { + sut.Rectangles.Count.Should().Be(0); + } + + [TestCase(1, TestName = "One rectangle")] + [TestCase(10, TestName = "10 rectangles")] + public void HaveAllRectangles_WhichWerePut(int rectanglesCount) + { + for (var i = 0; i < rectanglesCount; i++) + { + sut.PutNextRectangle(new Size(2, 3)); + } + + sut.Rectangles.Count.Should().Be(rectanglesCount); + } + + [TestCase(0, 5, TestName = "Width is zero")] + [TestCase(5, 0, TestName = "Height is zero")] + [TestCase(-5, 5, TestName = "Width is negative")] + [TestCase(5, -5, TestName = "Height is negative")] + public void ThrowException_OnInvalidSizeArguments(int width, int height) + { + Action action = () => { sut.PutNextRectangle(new Size(0, 0)); }; + + action.Should().Throw(); + } + + [Test] + public void ContainNonIntersectingRectangles() + { + var random = new Random(); + + for (var i = 0; i < 50; i++) + { + sut.PutNextRectangle(new Size(50 + random.Next(0, 100), 50 + random.Next(0, 100))); + } + + HasIntersectedRectangles(sut.Rectangles).Should().Be(false); + } + + private bool HasIntersectedRectangles(IList rectangles) + { + for (var i = 0; i < rectangles.Count - 1; i++) + { + for (var j = i + 1; j < rectangles.Count; j++) + { + if (rectangles[i].IntersectsWith(rectangles[j])) + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/cs/TagCloudTests/SpiralGenerator_Should.cs b/cs/TagCloudTests/SpiralGenerator_Should.cs new file mode 100644 index 000000000..165e81e04 --- /dev/null +++ b/cs/TagCloudTests/SpiralGenerator_Should.cs @@ -0,0 +1,44 @@ +[TestFixture] +public class SpiralGenerator_Should +{ + private const int Width = 1920; + private const int Height = 1080; + private SpiralGenerator sut; + private Point startPoint; + + [SetUp] + public void Setup() + { + startPoint = new Point(Width / 2, Height / 2); + sut = new SpiralGenerator(startPoint); + } + + [Test] + public void ReturnCenterPoint_OnFirstCall() + { + sut.GetNextPoint().Should().BeEquivalentTo(startPoint); + } + + [Test] + public void ReturnDifferentPoints_AfterMultipleCalls() + { + var spiralPoints = new HashSet(); + + for (var i = 0; i < 50; i++) + { + spiralPoints.Add(sut.GetNextPoint()); + } + + spiralPoints.Count.Should().BeGreaterThan(1); + } + + [TestCase(-1, 1, TestName = "X is negative")] + [TestCase(1, -1, TestName = "Y is negative")] + [TestCase(-1, -1, TestName = "X and Y are negative")] + public void ThrowException_OnInvalidCenterPoint(int x, int y) + { + Action action = () => { new SpiralGenerator(new Point(x, y)); }; + + action.Should().Throw(); + } +} \ No newline at end of file diff --git a/cs/TagCloudTests/TagCloudTests.csproj b/cs/TagCloudTests/TagCloudTests.csproj new file mode 100644 index 000000000..d599a5d4b --- /dev/null +++ b/cs/TagCloudTests/TagCloudTests.csproj @@ -0,0 +1,21 @@ + + + + net6.0-windows + enable + enable + + false + + + + + + + + + + + + + diff --git a/cs/TagCloudTests/Usings.cs b/cs/TagCloudTests/Usings.cs new file mode 100644 index 000000000..663d37242 --- /dev/null +++ b/cs/TagCloudTests/Usings.cs @@ -0,0 +1,4 @@ +global using NUnit.Framework; +global using FluentAssertions; +global using TagCloud; +global using System.Drawing; \ No newline at end of file diff --git a/cs/tdd.sln b/cs/tdd.sln index c8f523d63..5f656a0ad 100644 --- a/cs/tdd.sln +++ b/cs/tdd.sln @@ -1,11 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32929.385 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BowlingGame", "BowlingGame\BowlingGame.csproj", "{AD0F018A-732E-4074-8527-AB2EEC8D0BF3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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}") = "Samples", "Samples\Samples.csproj", "{B5108E20-2ACF-4ED9-84FE-2A718050FC94}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloud", "TagCloud\TagCloud.csproj", "{A91CC98B-AF6A-4D7A-B583-7467A518FF21}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudTests", "TagCloudTests\TagCloudTests.csproj", "{7C05535D-9F7C-4992-BD04-8A6B702D2E3A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,8 +25,19 @@ 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 + {A91CC98B-AF6A-4D7A-B583-7467A518FF21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A91CC98B-AF6A-4D7A-B583-7467A518FF21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A91CC98B-AF6A-4D7A-B583-7467A518FF21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A91CC98B-AF6A-4D7A-B583-7467A518FF21}.Release|Any CPU.Build.0 = Release|Any CPU + {7C05535D-9F7C-4992-BD04-8A6B702D2E3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C05535D-9F7C-4992-BD04-8A6B702D2E3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C05535D-9F7C-4992-BD04-8A6B702D2E3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C05535D-9F7C-4992-BD04-8A6B702D2E3A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5F540D89-ECD8-40C7-849F-3969C9F0A03C} + EndGlobalSection EndGlobal