diff --git a/cs/TagsCloudVisiualizationTests/CircularCloudLayoutTests.cs b/cs/TagsCloudVisiualizationTests/CircularCloudLayoutTests.cs new file mode 100644 index 000000000..10ce5148b --- /dev/null +++ b/cs/TagsCloudVisiualizationTests/CircularCloudLayoutTests.cs @@ -0,0 +1,91 @@ +using System.Drawing; +using System.Drawing.Imaging; +using FluentAssertions; +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using TagsCloudVisiualizationTests.Utils; +using TagsCloudVisualization; +using TagsCloudVisualization.LayoutRectanglesInCloudAlgorithms; +using TagsCloudVisualization.Visualization; + +namespace TagsCloudVisiualizationTests; + +[TestFixture] +public class CircularCloudLayoutTests +{ + private ICircularCloudLayouter cloudLayouter; + private List addedRectangles; + + [SetUp] + public void Setup() + { + var center = new Point(0, 0); + cloudLayouter = new CircularCloudLayouter(new CircularLayoutAlgorithm(center)); + addedRectangles = []; + } + + [TearDown] + public void TearDown() + { + if (TestContext.CurrentContext.Result.Outcome.Status != TestStatus.Failed) + return; + + var pathImageStored = TestContext.CurrentContext.TestDirectory + @"\imageFailedTests"; + + if (!Directory.Exists(pathImageStored)) + { + Directory.CreateDirectory(pathImageStored); + } + + var testName = TestContext.CurrentContext.Test.Name; + + var bitmap = ImageDrawer.DrawLayout(addedRectangles, 10); + + ImageSaver.Save(bitmap, pathImageStored, $"{testName}.png", ImageFormat.Png); + + Console.WriteLine($@"Tag cloud visualization saved to file {pathImageStored}\{testName}.png"); + } + + [TestCase(10, 5, 15)] + [TestCase(50, 30, 100)] + [TestCase(100, 5, 50)] + public void PutNextRectangle_ShouldAddedRectanglesDoNotIntersect(int countRectangles, int minSideLength, + int maxSideLength) + { + var rectangleSizes = GeometryUtils.GenerateRectangleSizes(countRectangles, minSideLength, maxSideLength); + + addedRectangles.AddRange(rectangleSizes.Select(t => cloudLayouter.PutNextRectangle(t))); + + for (var i = 0; i < addedRectangles.Count-1; i++) + { + addedRectangles + .Skip(i + 1) + .Any(addedRectangle => addedRectangle.IntersectsWith(addedRectangles[i])) + .Should() + .BeFalse(); + } + } + + [TestCase(10, 5, 15)] + [TestCase(50, 30, 100)] + [TestCase(100, 5, 50)] + public void CircleShape_ShouldBeCloseToCircle_WhenAddMultipleRectangles(int countRectangles, int minSideLength, + int maxSideLength) + { + var rectangleSizes = GeometryUtils.GenerateRectangleSizes(countRectangles, minSideLength, maxSideLength); + + addedRectangles.AddRange(rectangleSizes.Select(t => cloudLayouter.PutNextRectangle(t))); + + var distances = addedRectangles + .Select(rectangle => + GeometryUtils.CalculateDistanceBetweenCenterRectangleAndCenterCloud(rectangle, new Point(0, 0))) + .ToArray(); + + for (var i = 1; i < distances.Length; i++) + { + var distanceBetweenRectangles = + GeometryUtils.CalculateDistanceBetweenRectangles(addedRectangles[i], addedRectangles[i - 1]); + distances[i].Should().BeApproximately(distances[i - 1], distanceBetweenRectangles); + } + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisiualizationTests/CircularLayoutAlgorithmTests.cs b/cs/TagsCloudVisiualizationTests/CircularLayoutAlgorithmTests.cs new file mode 100644 index 000000000..43d00e428 --- /dev/null +++ b/cs/TagsCloudVisiualizationTests/CircularLayoutAlgorithmTests.cs @@ -0,0 +1,91 @@ +using System.Drawing; +using FluentAssertions; +using NUnit.Framework; +using TagsCloudVisiualizationTests.Utils; +using TagsCloudVisualization.LayoutRectanglesInCloudAlgorithms; + +namespace TagsCloudVisiualizationTests; + +[TestFixture] +public class CircularLayoutAlgorithmTests +{ + [TestCase(0)] + [TestCase(-5)] + public void Constructor_ShouldArgumentException_WhenStepIncreasingRadiusHasInvalidValue(int stepIncreasingRadius) + { + var action = () => + { + _ = new CircularLayoutAlgorithm(new Point(0, 0), stepIncreasingRadius: stepIncreasingRadius); + }; + + action + .Should() + .Throw() + .WithMessage("The parameter 'stepIncreasingRadius' is less than or equal to zero"); + } + + [Test] + public void Constructor_ShouldArgumentException_WhenStepIncreasingAngleIsZero() + { + var action = () => + { + _ = new CircularLayoutAlgorithm(new Point(0, 0), stepIncreasingAngle: 0); + }; + + action + .Should() + .Throw() + .WithMessage("The parameter 'stepIncreasingAngle' is zero"); + } + + [TestCase(0, 0)] + [TestCase(-4, 5)] + public void CalculateNextPoint_ShouldPointIsCenter_WhenCalculateFirstPoint(int centerCoordinateX, int centerCoordinateY) + { + var center = new Point(centerCoordinateY, centerCoordinateY); + var circularLayoutAlgorithm = new CircularLayoutAlgorithm(center); + + var nextPoint = circularLayoutAlgorithm.CalculateNextPoint(); + + nextPoint.Should().Be(center); + } + + [TestCase(2)] + [TestCase(7)] + public void CalculateNextPoint_ShouldIncreaseRadius_WhenCalculateTwoPoints(int stepIncreasingRadius) + { + var circularLayoutAlgorithm = + new CircularLayoutAlgorithm(new Point(0, 0), stepIncreasingRadius: stepIncreasingRadius); + + var firstPoint = circularLayoutAlgorithm.CalculateNextPoint(); + var secondPoint = circularLayoutAlgorithm.CalculateNextPoint(); + var distanceBetweenPoints = GeometryUtils.CalculateDistanceBetweenPoints(firstPoint, secondPoint); + + distanceBetweenPoints.Should().Be(stepIncreasingRadius); + } + + [TestCase(Math.PI / 4)] + [TestCase(Math.PI / 2)] + public void CalculateNextPoint_ShouldIncreaseAngle_WhenCalculateThreePoints(double stepIncreasingAngle) + { + var stepIncreasingRadius = 2; + var center = new Point(0, 0); + var circularLayoutAlgorithm = new CircularLayoutAlgorithm( + center, + stepIncreasingRadius: stepIncreasingRadius, + stepIncreasingAngle: stepIncreasingAngle + ); + + _ = circularLayoutAlgorithm.CalculateNextPoint(); + var secondPoint = circularLayoutAlgorithm.CalculateNextPoint(); + var thirdPoint = circularLayoutAlgorithm.CalculateNextPoint(); + var distanceBetweenCenterAndSecondPoint = GeometryUtils.CalculateDistanceBetweenPoints(center, secondPoint); + var distanceBetweenCenterAndThirdPoint = GeometryUtils.CalculateDistanceBetweenPoints(center, thirdPoint); + + // проверяем, что точки не равны + secondPoint.Should().NotBe(thirdPoint); + // проверяем, что точки в пределах круга с одним радиусом + distanceBetweenCenterAndSecondPoint.Should().BeLessThanOrEqualTo(stepIncreasingRadius); + distanceBetweenCenterAndThirdPoint.Should().BeLessThanOrEqualTo(stepIncreasingRadius); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisiualizationTests/TagsCloudVisiualizationTests.csproj b/cs/TagsCloudVisiualizationTests/TagsCloudVisiualizationTests.csproj new file mode 100644 index 000000000..dd1db7c96 --- /dev/null +++ b/cs/TagsCloudVisiualizationTests/TagsCloudVisiualizationTests.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/cs/TagsCloudVisiualizationTests/Utils/GeometryUtils.cs b/cs/TagsCloudVisiualizationTests/Utils/GeometryUtils.cs new file mode 100644 index 000000000..d4ec01cef --- /dev/null +++ b/cs/TagsCloudVisiualizationTests/Utils/GeometryUtils.cs @@ -0,0 +1,40 @@ +using System.Drawing; + +namespace TagsCloudVisiualizationTests.Utils; + +public static class GeometryUtils +{ + public static double CalculateDistanceBetweenCenterRectangleAndCenterCloud(Rectangle rectangle, Point center) + { + var centerRectangle = new Point(rectangle.X + rectangle.Width / 2, rectangle.Y + rectangle.Height / 2); + + return CalculateDistanceBetweenPoints(centerRectangle, center); + } + + public static double CalculateDistanceBetweenRectangles(Rectangle rectangle1, Rectangle rectangle2) + { + var centerRectangle1 = new Point(rectangle1.X + rectangle1.Width / 2, rectangle1.Y + rectangle1.Height / 2); + var centerRectangle2 = new Point(rectangle2.X + rectangle2.Width / 2, rectangle2.Y + rectangle2.Height / 2); + + return CalculateDistanceBetweenPoints(centerRectangle1, centerRectangle2); + } + + public static double CalculateDistanceBetweenPoints(Point point1, Point point2) + { + return Math.Sqrt(Math.Pow(point1.X - point2.X, 2) + Math.Pow(point1.Y - point2.Y, 2)); + } + + public static List GenerateRectangleSizes(int countRectangles, int minSideLength, int maxSideLength) + { + var random = new Random(); + + var generatedSizes = Enumerable.Range(0, countRectangles) + .Select(_ => new Size( + random.Next(minSideLength, maxSideLength), + random.Next(minSideLength, maxSideLength)) + ) + .ToList(); + + return generatedSizes; + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/CircularCloudLayouter.cs b/cs/TagsCloudVisualization/CircularCloudLayouter.cs new file mode 100644 index 000000000..36501df94 --- /dev/null +++ b/cs/TagsCloudVisualization/CircularCloudLayouter.cs @@ -0,0 +1,39 @@ +using System.Drawing; +using TagsCloudVisualization.LayoutRectanglesInCloudAlgorithms; + +namespace TagsCloudVisualization; + +public class CircularCloudLayouter : ICircularCloudLayouter +{ + private readonly ILayoutAlgorithm layoutAlgorithm; + private readonly List addedRectangles = []; + + public CircularCloudLayouter(ILayoutAlgorithm layoutAlgorithm) + { + this.layoutAlgorithm = layoutAlgorithm; + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + Rectangle rectangle; + + do + { + var nextPoint = layoutAlgorithm.CalculateNextPoint(); + + var rectangleLocation = nextPoint - rectangleSize / 2; + + rectangle = new Rectangle(rectangleLocation, rectangleSize); + + } while (IntersectWithAddedRectangles(rectangle)); + + addedRectangles.Add(rectangle); + + return rectangle; + } + + private bool IntersectWithAddedRectangles(Rectangle rectangle) + { + return addedRectangles.Any(addedRectangle => addedRectangle.IntersectsWith(rectangle)); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/ICircularCloudLayouter.cs b/cs/TagsCloudVisualization/ICircularCloudLayouter.cs new file mode 100644 index 000000000..106b0068c --- /dev/null +++ b/cs/TagsCloudVisualization/ICircularCloudLayouter.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public interface ICircularCloudLayouter +{ + Rectangle PutNextRectangle(Size rectangleSize); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/LayoutRectanglesInCloudAlgorithms/CircularLayoutAlgorithm.cs b/cs/TagsCloudVisualization/LayoutRectanglesInCloudAlgorithms/CircularLayoutAlgorithm.cs new file mode 100644 index 000000000..e82a44892 --- /dev/null +++ b/cs/TagsCloudVisualization/LayoutRectanglesInCloudAlgorithms/CircularLayoutAlgorithm.cs @@ -0,0 +1,43 @@ +using System.Drawing; + +namespace TagsCloudVisualization.LayoutRectanglesInCloudAlgorithms; + +public class CircularLayoutAlgorithm : ILayoutAlgorithm +{ + private readonly Point center; + private readonly double stepIncreasingAngle; + private readonly int stepIncreasingRadius; + private double currentAngleOfCircle; + private double currentRadiusOfCircle; + private const double OneDegree = Math.PI / 180; + private const double FullCircleRotation = 2 * Math.PI; + + public CircularLayoutAlgorithm(Point center, double stepIncreasingAngle = OneDegree, int stepIncreasingRadius = 1) + { + if (stepIncreasingRadius <= 0) + throw new ArgumentException("The parameter 'stepIncreasingRadius' is less than or equal to zero"); + if (stepIncreasingAngle == 0) + throw new ArgumentException("The parameter 'stepIncreasingAngle' is zero"); + + this.center = center; + this.stepIncreasingAngle = stepIncreasingAngle; + this.stepIncreasingRadius = stepIncreasingRadius; + } + + public Point CalculateNextPoint() + { + var x = center.X + (int)(currentRadiusOfCircle * Math.Cos(currentAngleOfCircle)); + var y = center.Y + (int)(currentRadiusOfCircle * Math.Sin(currentAngleOfCircle)); + + currentAngleOfCircle += stepIncreasingAngle; + + // проверяем не прошли ли целый круг или равен ли текущий радиус нулю + if (currentAngleOfCircle > FullCircleRotation || currentRadiusOfCircle == 0) + { + currentAngleOfCircle = 0; + currentRadiusOfCircle += stepIncreasingRadius; + } + + return new Point(x, y); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/LayoutRectanglesInCloudAlgorithms/ILayoutAlgorithm.cs b/cs/TagsCloudVisualization/LayoutRectanglesInCloudAlgorithms/ILayoutAlgorithm.cs new file mode 100644 index 000000000..55c4c2804 --- /dev/null +++ b/cs/TagsCloudVisualization/LayoutRectanglesInCloudAlgorithms/ILayoutAlgorithm.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.LayoutRectanglesInCloudAlgorithms; + +public interface ILayoutAlgorithm +{ + Point CalculateNextPoint(); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/README.md b/cs/TagsCloudVisualization/README.md new file mode 100644 index 000000000..dca4a86c1 --- /dev/null +++ b/cs/TagsCloudVisualization/README.md @@ -0,0 +1,3 @@ +![Визуализация десяти прямоугольников](images/visualization10Rectangles.png) +![Визуализация пятидесяти прямоугольников](images/visualization50Rectangles.png) +![Визуализация ста прямоугольников](images/visualization100Rectangles.png) \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagsCloudVisualization.csproj b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj new file mode 100644 index 000000000..21f20a861 --- /dev/null +++ b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/cs/TagsCloudVisualization/Visualization/ImageDrawer.cs b/cs/TagsCloudVisualization/Visualization/ImageDrawer.cs new file mode 100644 index 000000000..9578a66c7 --- /dev/null +++ b/cs/TagsCloudVisualization/Visualization/ImageDrawer.cs @@ -0,0 +1,41 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Visualization; + +#pragma warning disable CA1416 +public class ImageDrawer +{ + public static Bitmap DrawLayout(List rectangles, int paddingFromBorders) + { + var minX = rectangles.Min(rectangle => rectangle.Left); + var minY = rectangles.Min(rectangle => rectangle.Top); + var maxX = rectangles.Max(rectangle => rectangle.Right); + var maxY = rectangles.Max(rectangle => rectangle.Bottom); + var width = maxX - minX + paddingFromBorders; + var height = maxY - minY + paddingFromBorders; + + var random = new Random(); + + var bitmap = new Bitmap(width, height); + using var graphics = Graphics.FromImage(bitmap); + + graphics.Clear(Color.White); + + foreach (var rectangle in rectangles) + { + var shiftedRectangle = rectangle with + { + X = rectangle.X - minX + paddingFromBorders, + Y = rectangle.Y - minY + paddingFromBorders + }; + + var randomColor = Color.FromArgb(random.Next(256), random.Next(256), random.Next(256)); + + var brush = new SolidBrush(randomColor); + graphics.FillRectangle(brush, shiftedRectangle); + graphics.DrawRectangle(Pens.Black, shiftedRectangle); + } + + return bitmap; + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Visualization/ImageSaver.cs b/cs/TagsCloudVisualization/Visualization/ImageSaver.cs new file mode 100644 index 000000000..57113ba21 --- /dev/null +++ b/cs/TagsCloudVisualization/Visualization/ImageSaver.cs @@ -0,0 +1,13 @@ +using System.Drawing; +using System.Drawing.Imaging; + +#pragma warning disable CA1416 +namespace TagsCloudVisualization.Visualization; + +public class ImageSaver +{ + public static void Save(Bitmap bitmap, string filePath, string fileName, ImageFormat imageFormat) + { + bitmap.Save(Path.Combine(filePath, fileName), imageFormat); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/images/visualization100Rectangles.png b/cs/TagsCloudVisualization/images/visualization100Rectangles.png new file mode 100644 index 000000000..dcab3fbea Binary files /dev/null and b/cs/TagsCloudVisualization/images/visualization100Rectangles.png differ diff --git a/cs/TagsCloudVisualization/images/visualization10Rectangles.png b/cs/TagsCloudVisualization/images/visualization10Rectangles.png new file mode 100644 index 000000000..c4aad3df0 Binary files /dev/null and b/cs/TagsCloudVisualization/images/visualization10Rectangles.png differ diff --git a/cs/TagsCloudVisualization/images/visualization50Rectangles.png b/cs/TagsCloudVisualization/images/visualization50Rectangles.png new file mode 100644 index 000000000..7c844e29c Binary files /dev/null and b/cs/TagsCloudVisualization/images/visualization50Rectangles.png differ diff --git a/cs/tdd.sln b/cs/tdd.sln index c8f523d63..eca0aa98a 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", "{E344A7C8-C0E8-4231-BAAD-E2774B855CDF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudVisiualizationTests", "TagsCloudVisiualizationTests\TagsCloudVisiualizationTests.csproj", "{86B722F4-6F6A-4773-B2E6-233B10DB7492}" +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 + {E344A7C8-C0E8-4231-BAAD-E2774B855CDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E344A7C8-C0E8-4231-BAAD-E2774B855CDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E344A7C8-C0E8-4231-BAAD-E2774B855CDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E344A7C8-C0E8-4231-BAAD-E2774B855CDF}.Release|Any CPU.Build.0 = Release|Any CPU + {86B722F4-6F6A-4773-B2E6-233B10DB7492}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86B722F4-6F6A-4773-B2E6-233B10DB7492}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86B722F4-6F6A-4773-B2E6-233B10DB7492}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86B722F4-6F6A-4773-B2E6-233B10DB7492}.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