diff --git a/cs/TagsCloudVisualization/Program.cs b/cs/TagsCloudVisualization/Program.cs new file mode 100644 index 000000000..68b8acf53 --- /dev/null +++ b/cs/TagsCloudVisualization/Program.cs @@ -0,0 +1,13 @@ +using System.Drawing; +using TagsCloudVisualization.TagCloud; + +var l = new CircularCloudLayouter(new Point(250,250),new CircularPositionCalculator(new Point(250,250))); +for (int i = 0; i < 100; i++) +{ + var rand = new Random(); + l.PutNextRectangle(new Size(rand.Next(10,70),rand.Next(10,70))); +} + +var drawer = new BaseCloudDrawer(); +var bmp = drawer.DrawCloud(l.Rectangles,500,500); +drawer.SaveToFile(bmp); diff --git a/cs/TagsCloudVisualization/TagCloud/CircularCloudLayouter.cs b/cs/TagsCloudVisualization/TagCloud/CircularCloudLayouter.cs new file mode 100644 index 000000000..ae15e78d4 --- /dev/null +++ b/cs/TagsCloudVisualization/TagCloud/CircularCloudLayouter.cs @@ -0,0 +1,21 @@ +using System.Drawing; + +namespace TagsCloudVisualization.TagCloud; + +public class CircularCloudLayouter(Point center, IPositionCalculator calculator) +{ + public List Rectangles { get; private set; } = []; + + public Rectangle PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.Width <= 0) + throw new ArgumentException("Size width must be positive number"); + if (rectangleSize.Height <= 0) + throw new ArgumentException("Size height must be positive number"); + + var temp = calculator.CalculateNextPosition(rectangleSize) + .First(rectangle => calculator.IsRectanglePositionValid(Rectangles, rectangle)); + Rectangles.Add(temp); + return temp; + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagCloud/CloudDrawer/BaseCloudDrawer.cs b/cs/TagsCloudVisualization/TagCloud/CloudDrawer/BaseCloudDrawer.cs new file mode 100644 index 000000000..b5fce6448 --- /dev/null +++ b/cs/TagsCloudVisualization/TagCloud/CloudDrawer/BaseCloudDrawer.cs @@ -0,0 +1,34 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Globalization; + +namespace TagsCloudVisualization.TagCloud; + +public class BaseCloudDrawer : ICloudDrawer +{ + public Bitmap DrawCloud(List rectangles, int imageWidth, int imageHeight) + { + if (imageWidth <= 0) + throw new ArgumentException("Width must be positive number"); + if (imageHeight <= 0) + throw new ArgumentException("Height must be positive number"); + var bitmap = new Bitmap(imageWidth, imageHeight); + var graphics = Graphics.FromImage(bitmap); + graphics.Clear(Color.White); + foreach (var rectangle in rectangles) + { + graphics.DrawRectangle(new Pen(Color.Black), rectangle); + } + + return bitmap; + } + + public void SaveToFile(Bitmap bitmap, string? fileName = null, string? path = null, ImageFormat format = null) + { + path ??= Environment.CurrentDirectory; + fileName ??= DateTime.Now.ToOADate().ToString(CultureInfo.InvariantCulture); + format ??= ImageFormat.Png; + var fullPath = Path.Combine(path, fileName + ".png"); + bitmap.Save(fullPath); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagCloud/CloudDrawer/ICloudDrawer.cs b/cs/TagsCloudVisualization/TagCloud/CloudDrawer/ICloudDrawer.cs new file mode 100644 index 000000000..cc1b9ae15 --- /dev/null +++ b/cs/TagsCloudVisualization/TagCloud/CloudDrawer/ICloudDrawer.cs @@ -0,0 +1,10 @@ +using System.Drawing; +using System.Drawing.Imaging; + +namespace TagsCloudVisualization.TagCloud; + +public interface ICloudDrawer +{ + public Bitmap DrawCloud(List rectangles, int imageWidth, int imageHeight); + public void SaveToFile(Bitmap bitmap, string? fileName, string? path, ImageFormat format); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagCloud/PositionCalculator/CircularPositionCalculator.cs b/cs/TagsCloudVisualization/TagCloud/PositionCalculator/CircularPositionCalculator.cs new file mode 100644 index 000000000..4df93ec26 --- /dev/null +++ b/cs/TagsCloudVisualization/TagCloud/PositionCalculator/CircularPositionCalculator.cs @@ -0,0 +1,39 @@ +using System.Drawing; + +namespace TagsCloudVisualization.TagCloud; + +public class CircularPositionCalculator(Point center, double offsetDelta = 2.0, double angleDelta = 0.1) : IPositionCalculator +{ + private double currentAngle = 0.0; + private double currentOffset = 0.0; + private const double fullRoundAngle = Math.PI * 2; + + public IEnumerable CalculateNextPosition (Size nextRectangleSize) + { + while (true) + { + var newRectangle = MakeRectangleFromSize(nextRectangleSize); + currentAngle += angleDelta; + if (currentAngle >= fullRoundAngle) + { + currentAngle = 0; + currentOffset += offsetDelta; + } + + yield return newRectangle; + } + } + + private Rectangle MakeRectangleFromSize(Size nextRectangleSize) + { + var x = (int)(center.X + currentOffset * Math.Cos(currentAngle)); + var y = (int)(center.Y + currentOffset * Math.Sin(currentAngle)); + var newRectangle = new Rectangle(new Point(x, y), nextRectangleSize); + return newRectangle; + } + + public bool IsRectanglePositionValid(List rectangles, Rectangle currentRectangle) + { + return !rectangles.Any(r => r.IntersectsWith(currentRectangle)); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagCloud/PositionCalculator/IPositionCalculator.cs b/cs/TagsCloudVisualization/TagCloud/PositionCalculator/IPositionCalculator.cs new file mode 100644 index 000000000..c5cbf40ed --- /dev/null +++ b/cs/TagsCloudVisualization/TagCloud/PositionCalculator/IPositionCalculator.cs @@ -0,0 +1,9 @@ +using System.Drawing; + +namespace TagsCloudVisualization.TagCloud; + +public interface IPositionCalculator +{ + public IEnumerable CalculateNextPosition(Size nextRectangleSize); + public bool IsRectanglePositionValid(List rectangles, Rectangle currentRectangle); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagsCloudVisualization.csproj b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj new file mode 100644 index 000000000..d35d934e9 --- /dev/null +++ b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/cs/TagsCloudVisualizationTest/BaseCloudDrawer_Tests.cs b/cs/TagsCloudVisualizationTest/BaseCloudDrawer_Tests.cs new file mode 100644 index 000000000..c14751944 --- /dev/null +++ b/cs/TagsCloudVisualizationTest/BaseCloudDrawer_Tests.cs @@ -0,0 +1,115 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Globalization; +using NUnit.Framework.Interfaces; + +namespace BaseCloudDrawer_Tests; + +[TestFixture] +public class BaseCloudDrawer_Tests +{ + private BaseCloudDrawer drawer; + private string testDirectory; + + [SetUp] + public void SetUp() + { + drawer = new BaseCloudDrawer(); + testDirectory = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestImages"); + Directory.CreateDirectory(testDirectory); + } + + [TearDown] + public void TearDown() + { + var status = TestContext.CurrentContext.Result.Outcome.Status; + var files = Directory.GetFiles(testDirectory); + + if (status == TestStatus.Passed && + files.Length > 0) //удаляем в случае успешного прохождения, пишем в консоль при падении теста + { + File.Delete(files.OrderByDescending(f => new FileInfo(f).CreationTime).First()); + } + else if (status == TestStatus.Failed) + { + Console.WriteLine($"Test failed. Image saved to {testDirectory}/{TestContext.CurrentContext.Test.Name}"); + } + } + + [Test] + public void DrawCloud_ThrowsArgumentException_WhenWidthIsNonPositive() + { + var rectangles = new List { new Rectangle(0, 0, 10, 10) }; + + Action act = () => drawer.DrawCloud(rectangles, 0, 100); + + act.Should().Throw().WithMessage("Width must be positive number"); + } + + [Test] + public void DrawCloud_ThrowsArgumentException_WhenHeightIsNonPositive() + { + var rectangles = new List { new Rectangle(0, 0, 10, 10) }; + + Action act = () => drawer.DrawCloud(rectangles, 100, 0); + + act.Should().Throw().WithMessage("Height must be positive number"); + } + + + [Test] + public void DrawCloud_ReturnsBitmapWithCorrectDimensions() + { + var rectangles = new List { new Rectangle(0, 0, 10, 10) }; + var imageWidth = 100; + var imageHeight = 100; + + var bitmap = drawer.DrawCloud(rectangles, imageWidth, imageHeight); + + var fileName = TestContext.CurrentContext.Test.Name; + var format = ImageFormat.Png; + drawer.SaveToFile(bitmap, fileName, testDirectory, format); + bitmap.Width.Should().Be(imageWidth); + bitmap.Height.Should().Be(imageHeight); + } + + [Test] + public void DrawCloud_DrawsAllRectangles() + { + var rectangles = new List + { + new Rectangle(0, 0, 10, 10), + new Rectangle(20, 20, 10, 10) + }; + var imageWidth = 100; + var imageHeight = 100; + + var bitmap = drawer.DrawCloud(rectangles, imageWidth, imageHeight); + + using (var graphics = Graphics.FromImage(bitmap)) + { + foreach (var rectangle in rectangles) + { + var pixelColor = bitmap.GetPixel(rectangle.X + 1, rectangle.Y + 1); + pixelColor.Should().NotBe(Color.White); + } + } + + var fileName = TestContext.CurrentContext.Test.Name; + var format = ImageFormat.Png; + drawer.SaveToFile(bitmap, fileName, testDirectory, format); + } + + [Test] + public void SaveToFile_SavesBitmapToSpecifiedPath() + { + var bitmap = new Bitmap(10, 10); + var fileName = "test_image"; + var format = ImageFormat.Png; + + drawer.SaveToFile(bitmap, fileName, testDirectory, format); + + var fullPath = Path.Combine(testDirectory, fileName + ".png"); + File.Exists(fullPath).Should().BeTrue(); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualizationTest/CircularCloudLayouter_Tests.cs b/cs/TagsCloudVisualizationTest/CircularCloudLayouter_Tests.cs new file mode 100644 index 000000000..7bf1aa703 --- /dev/null +++ b/cs/TagsCloudVisualizationTest/CircularCloudLayouter_Tests.cs @@ -0,0 +1,61 @@ +namespace TagsCloudVisualizationTest; + +public class CircularCloudLayouter_Tests +{ + [TestFixture] + public class CircularCloudLayouterTests + { + private CircularCloudLayouter layouter; + private IPositionCalculator calculator; + private Point center; + + [SetUp] + public void SetUp() + { + center = new Point(0, 0); + calculator = new CircularPositionCalculator(center); + layouter = new CircularCloudLayouter(center, calculator); + } + + [Test] + public void PutNextRectangle_ThrowsArgumentException_WhenWidthIsNonPositive() + { + var size = new Size(0, 10); + + Action act = () => layouter.PutNextRectangle(size); + + act.Should().Throw().WithMessage("Size width must be positive number"); + } + + [Test] + public void PutNextRectangle_ThrowsArgumentException_WhenHeightIsNonPositive() + { + var size = new Size(10, 0); + + Action act = () => layouter.PutNextRectangle(size); + + act.Should().Throw().WithMessage("Size height must be positive number"); + } + + [Test] + public void PutNextRectangle_AddsRectangleToList_WhenValidSize() + { + var size = new Size(10, 10); + + var result = layouter.PutNextRectangle(size); + + layouter.Rectangles.Should().Contain(result); + } + + [Test] + public void PutNextRectangle_ReturnsNonIntersectingRectangle_WhenRectanglesExist() + { + var size = new Size(10, 10); + layouter.PutNextRectangle(size); + + var result = layouter.PutNextRectangle(size); + + result.IntersectsWith(layouter.Rectangles[0]).Should().BeFalse(); + } + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualizationTest/GlobalUsings.cs b/cs/TagsCloudVisualizationTest/GlobalUsings.cs new file mode 100644 index 000000000..e7901fc9b --- /dev/null +++ b/cs/TagsCloudVisualizationTest/GlobalUsings.cs @@ -0,0 +1,5 @@ +// Global using directives + +global using System.Drawing; +global using FluentAssertions; +global using TagsCloudVisualization.TagCloud; \ No newline at end of file diff --git a/cs/TagsCloudVisualizationTest/PngExamples/45608,91380605324.png b/cs/TagsCloudVisualizationTest/PngExamples/45608,91380605324.png new file mode 100644 index 000000000..90449ad95 Binary files /dev/null and b/cs/TagsCloudVisualizationTest/PngExamples/45608,91380605324.png differ diff --git a/cs/TagsCloudVisualizationTest/PngExamples/45609.73230590278.png b/cs/TagsCloudVisualizationTest/PngExamples/45609.73230590278.png new file mode 100644 index 000000000..450708156 Binary files /dev/null and b/cs/TagsCloudVisualizationTest/PngExamples/45609.73230590278.png differ diff --git a/cs/TagsCloudVisualizationTest/SpiralPositionCalculator_Tests.cs b/cs/TagsCloudVisualizationTest/SpiralPositionCalculator_Tests.cs new file mode 100644 index 000000000..1909040d0 --- /dev/null +++ b/cs/TagsCloudVisualizationTest/SpiralPositionCalculator_Tests.cs @@ -0,0 +1,73 @@ +using NUnit.Framework; + +namespace TagsCloudVisualizationTests +{ + [TestFixture] + public class CircularPositionCalculatorTests + { + private CircularPositionCalculator calculator; + private Point center; + + [SetUp] + public void SetUp() + { + center = new Point(0, 0); + calculator = new CircularPositionCalculator(center); + } + + [Test] + public void CalculateNextPosition_ShouldReturnRectangleAtCenter_WhenNoRectanglesExist() + { + var size = new Size(10, 10); + var rectangles = new List(); + + var result = calculator.CalculateNextPosition(size).First(); + + result.Location.Should().Be(center); + } + + [Test] + public void CalculateNextPosition_ShouldIncreaseOffset_WhenAngleCompletesFullCircle() + { + var size = new Size(10, 10); + var source =calculator.CalculateNextPosition(size) + .Skip(100) + .First(); + GetDistance(source.Location, center).Should().NotBe(0); + } + + [Test] + public void ValidateRectanglePosition_ShouldReturnTrue_WhenNoIntersections() + { + var size = new Size(10, 10); + var rectangles = new List(); + var rectangle = new Rectangle(new Point(0, 0), size); + + var result = calculator.IsRectanglePositionValid(rectangles, rectangle); + + result.Should().BeTrue(); + } + + [Test] + public void ValidateRectanglePosition_ShouldReturnFalse_WhenIntersectsWithExistingRectangle() + { + var size = new Size(10, 10); + var rectangles = new List + { + new Rectangle(new Point(0, 0), size) + }; + var rectangle = new Rectangle(new Point(0, 0), size); + + var result = calculator.IsRectanglePositionValid(rectangles, rectangle); + + result.Should().BeFalse(); + } + + private static double GetDistance(Point point, Point other) + { + var dx = point.X - other.X; + var dy = point.Y - other.Y; + return Math.Sqrt(dx * dx + dy * dy); + } + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualizationTest/TagsCloudVisualizationTest.csproj b/cs/TagsCloudVisualizationTest/TagsCloudVisualizationTest.csproj new file mode 100644 index 000000000..47c063eb8 --- /dev/null +++ b/cs/TagsCloudVisualizationTest/TagsCloudVisualizationTest.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/cs/tdd.sln b/cs/tdd.sln index c8f523d63..d2db0aeeb 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}") = "TagsCloudVisualization", "TagsCloudVisualization\TagsCloudVisualization.csproj", "{D99B90EA-1C05-417E-80C6-B005531D0545}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudVisualizationTest", "TagsCloudVisualizationTest\TagsCloudVisualizationTest.csproj", "{A1FACECA-57BA-4B8F-B804-DC54B8B167C8}" +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 + {D99B90EA-1C05-417E-80C6-B005531D0545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D99B90EA-1C05-417E-80C6-B005531D0545}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D99B90EA-1C05-417E-80C6-B005531D0545}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D99B90EA-1C05-417E-80C6-B005531D0545}.Release|Any CPU.Build.0 = Release|Any CPU + {A1FACECA-57BA-4B8F-B804-DC54B8B167C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1FACECA-57BA-4B8F-B804-DC54B8B167C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1FACECA-57BA-4B8F-B804-DC54B8B167C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1FACECA-57BA-4B8F-B804-DC54B8B167C8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/cs/tdd.sln.DotSettings b/cs/tdd.sln.DotSettings index 135b83ecb..229f449d2 100644 --- a/cs/tdd.sln.DotSettings +++ b/cs/tdd.sln.DotSettings @@ -1,6 +1,9 @@  <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + True True True Imported 10.10.2016