diff --git a/cs/TagsCloudVisualization/CloudLayouters/CircularCloudLayouter.cs b/cs/TagsCloudVisualization/CloudLayouters/CircularCloudLayouter.cs new file mode 100644 index 000000000..9bce31e6f --- /dev/null +++ b/cs/TagsCloudVisualization/CloudLayouters/CircularCloudLayouter.cs @@ -0,0 +1,37 @@ +using System.Drawing; +using TagsCloudVisualization.Extensions; +using TagsCloudVisualization.PointsGenerators; + +namespace TagsCloudVisualization.CloudLayouters; + +public class CircularCloudLayouter(Point layoutCenter, IPointsGenerator pointsGenerator) : ICircularCloudLayouter +{ + private const string FiniteGeneratorExceptionMessage = + "В конструктор CircularCloudLayouter был передан конечный генератор точек"; + private readonly IEnumerator pointEnumerator = pointsGenerator + .GeneratePoints(layoutCenter) + .GetEnumerator(); + private readonly List layoutRectangles = []; + + public CircularCloudLayouter(Point layoutCenter, double radius, double angleOffset) : + this(layoutCenter, new FermatSpiralPointsGenerator(radius, angleOffset)) + { + + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + var rectangle = pointEnumerator + .ToIEnumerable() + .Select(point => new Rectangle() + .CreateRectangleWithCenter(point, rectangleSize)) + .FirstOrDefault(rectangle => !layoutRectangles.Any(rectangle.IntersectsWith)); + + if (rectangle.IsEmpty) + throw new InvalidOperationException(FiniteGeneratorExceptionMessage); + + layoutRectangles.Add(rectangle); + return rectangle; + } + +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/CloudLayouters/ICircularCloudLayouter.cs b/cs/TagsCloudVisualization/CloudLayouters/ICircularCloudLayouter.cs new file mode 100644 index 000000000..b215b54ad --- /dev/null +++ b/cs/TagsCloudVisualization/CloudLayouters/ICircularCloudLayouter.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.CloudLayouters; + +public interface ICircularCloudLayouter +{ + public Rectangle PutNextRectangle(Size rectangleSize); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Extensions/EnumeratorExtension.cs b/cs/TagsCloudVisualization/Extensions/EnumeratorExtension.cs new file mode 100644 index 000000000..13329c079 --- /dev/null +++ b/cs/TagsCloudVisualization/Extensions/EnumeratorExtension.cs @@ -0,0 +1,10 @@ +namespace TagsCloudVisualization.Extensions; + +public static class EnumeratorExtension +{ + public static IEnumerable ToIEnumerable(this IEnumerator enumerator) { + while ( enumerator.MoveNext() ) { + yield return enumerator.Current; + } + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Extensions/RandomExtension.cs b/cs/TagsCloudVisualization/Extensions/RandomExtension.cs new file mode 100644 index 000000000..a0ed6325a --- /dev/null +++ b/cs/TagsCloudVisualization/Extensions/RandomExtension.cs @@ -0,0 +1,21 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Extensions; + +public static class RandomExtension +{ + public static Size RandomSize(this Random random, int minValue=1, int maxValue=int.MaxValue) + { + if (minValue <= 0) + throw new ArgumentOutOfRangeException(nameof(minValue), "minValue must be positive"); + if (minValue > maxValue) + throw new ArgumentOutOfRangeException(nameof(minValue), "minValue must be less than maxValue"); + + return new Size(random.Next(minValue, maxValue), random.Next(minValue, maxValue)); + } + + public static Point RandomPoint(this Random random, int minValue=int.MinValue, int maxValue=int.MaxValue) + { + return new Point(random.Next(minValue, maxValue), random.Next(minValue, maxValue)); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Extensions/RectangleExtension.cs b/cs/TagsCloudVisualization/Extensions/RectangleExtension.cs new file mode 100644 index 000000000..c014f0f95 --- /dev/null +++ b/cs/TagsCloudVisualization/Extensions/RectangleExtension.cs @@ -0,0 +1,25 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Extensions; + +public static class RectangleExtension +{ + public static Rectangle CreateRectangleWithCenter(this Rectangle rectangle, 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); + } + + public static double GetDistanceToMostRemoteCorner(this Rectangle rectangle, Point startingPoint) + { + Point[] corners = [ + new(rectangle.X, rectangle.Y), + new(rectangle.X + rectangle.Width, rectangle.Y), + new(rectangle.X, rectangle.Y + rectangle.Height), + new(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height)]; + return corners.Max(corner => + Math.Sqrt((startingPoint.X - corner.X) * (startingPoint.X - corner.X) + + (startingPoint.Y - corner.Y) * (startingPoint.Y - corner.Y))); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/PointsGenerators/FermatSpiralPointsGenerator.cs b/cs/TagsCloudVisualization/PointsGenerators/FermatSpiralPointsGenerator.cs new file mode 100644 index 000000000..dc9817dcc --- /dev/null +++ b/cs/TagsCloudVisualization/PointsGenerators/FermatSpiralPointsGenerator.cs @@ -0,0 +1,45 @@ +using System.Drawing; + +namespace TagsCloudVisualization.PointsGenerators; + +public class FermatSpiralPointsGenerator : IPointsGenerator +{ + private readonly double angleOffset; + private readonly double radius; + + private double OffsetPerRadian => radius / (2 * Math.PI); + + public FermatSpiralPointsGenerator(double radius, double angleOffset) + { + if (radius <= 0) + throw new ArgumentException("radius must be greater than 0", nameof(radius)); + if (angleOffset <= 0) + throw new ArgumentException("angleOffset must be greater than 0", nameof(angleOffset)); + + this.angleOffset = angleOffset * Math.PI / 180; + this.radius = radius; + } + + public IEnumerable GeneratePoints(Point spiralCenter) + { + double angle = 0; + + while (true) + { + yield return GetPointByPolarCoordinates(spiralCenter, angle); + angle += angleOffset; + } + } + + private Point GetPointByPolarCoordinates(Point spiralCenter, double angle) + { + var radiusVector = OffsetPerRadian * angle; + + var x = (int)Math.Round( + radiusVector * Math.Cos(angle) + spiralCenter.X); + var y = (int)Math.Round( + radiusVector * Math.Sin(angle) + spiralCenter.Y); + + return new Point(x, y); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/PointsGenerators/IPointsGenerator.cs b/cs/TagsCloudVisualization/PointsGenerators/IPointsGenerator.cs new file mode 100644 index 000000000..32e5dee0f --- /dev/null +++ b/cs/TagsCloudVisualization/PointsGenerators/IPointsGenerator.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.PointsGenerators; + +public interface IPointsGenerator +{ + public IEnumerable GeneratePoints(Point startPoint); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Program.cs b/cs/TagsCloudVisualization/Program.cs new file mode 100644 index 000000000..77037c556 --- /dev/null +++ b/cs/TagsCloudVisualization/Program.cs @@ -0,0 +1,45 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Globalization; +using TagsCloudVisualization.CloudLayouters; +using TagsCloudVisualization.Extensions; +using TagsCloudVisualization.Savers; +using TagsCloudVisualization.Visualizers; + +namespace TagsCloudVisualization; + +public static class Program +{ + private const int ImageWidth = 1920; + private const int ImageHeight = 1080; + + private const int RectanglesNumber = 1000; + private const int MinRectangleSize = 10; + private const int MaxRectangleSize = 50; + + private const double LayoutRadius = 1; + private const double LayoutAngleOffset = 0.5; + + private const string ImagesDirectory = "images"; + + public static void Main() + { + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; + + var center = new Point(ImageWidth / 2, ImageHeight / 2); + var cloudLayouter = new CircularCloudLayouter(center, LayoutRadius, LayoutAngleOffset); + var random = new Random(); + var rectangles = Enumerable + .Range(0, RectanglesNumber) + .Select(_ => cloudLayouter.PutNextRectangle(random.RandomSize + (MinRectangleSize, MaxRectangleSize))) + .ToArray(); + + var visualizer = new DefaultVisualizer(new Size(ImageWidth, ImageHeight)); + var bitmap = visualizer.CreateBitmap(rectangles); + var saver = new DefaultBitmapSaver(ImagesDirectory); + + saver.SaveBitmap(bitmap, + $"{RectanglesNumber}_{LayoutRadius}_{LayoutAngleOffset}_TagCloud.jpg"); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/README.MD b/cs/TagsCloudVisualization/README.MD new file mode 100644 index 000000000..75299b792 --- /dev/null +++ b/cs/TagsCloudVisualization/README.MD @@ -0,0 +1,6 @@ +1000 4-угольников, радиус=1, шаг спирали равен 0.5 + +1000 4-угольников, радиус=1, шаг спирали равен 5 + +1000 4-угольников, радиус=100, шаг спирали равен 0.5 + \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Savers/DefaultBitmapSaver.cs b/cs/TagsCloudVisualization/Savers/DefaultBitmapSaver.cs new file mode 100644 index 000000000..19aa3e994 --- /dev/null +++ b/cs/TagsCloudVisualization/Savers/DefaultBitmapSaver.cs @@ -0,0 +1,13 @@ +using System.Drawing; +using System.Drawing.Imaging; + +namespace TagsCloudVisualization.Savers; + +public class DefaultBitmapSaver(string bitmapDirectory="images") : IBitmapSaver +{ + public void SaveBitmap(Bitmap bitmap, string filename) + { + Directory.CreateDirectory(bitmapDirectory); + bitmap.Save(Path.Combine(bitmapDirectory, filename), ImageFormat.Jpeg); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Savers/IBitmapSaver.cs b/cs/TagsCloudVisualization/Savers/IBitmapSaver.cs new file mode 100644 index 000000000..d0bf6550c --- /dev/null +++ b/cs/TagsCloudVisualization/Savers/IBitmapSaver.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Savers; + +public interface IBitmapSaver +{ + public void SaveBitmap(Bitmap bitmap, string filename); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagsCloudVisualization.csproj b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj new file mode 100644 index 000000000..3577db871 --- /dev/null +++ b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/cs/TagsCloudVisualization/Visualizers/DefaultVisualizer.cs b/cs/TagsCloudVisualization/Visualizers/DefaultVisualizer.cs new file mode 100644 index 000000000..229fbd4c6 --- /dev/null +++ b/cs/TagsCloudVisualization/Visualizers/DefaultVisualizer.cs @@ -0,0 +1,33 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Visualizers; + +public class DefaultVisualizer(Size bitmapSize) : IVisualizer +{ + private Point bitmapCenter = new(bitmapSize.Width / 2, bitmapSize.Height / 2); + + public Bitmap CreateBitmap(IEnumerable rectangles) + { + var bitmap = new Bitmap(bitmapSize.Width, bitmapSize.Height); + + var deltaWidth = bitmapCenter.X - rectangles.First().X; + var deltaHeight = bitmapCenter.Y - rectangles.First().Y; + + using var graphics = Graphics.FromImage(bitmap); + foreach (var rectangle in rectangles) + { + var pen = new Pen(Color.DeepPink); + graphics.DrawRectangle(pen, + CenterRectangle(rectangle, deltaWidth, deltaHeight)); + } + + return bitmap; + } + + private Rectangle CenterRectangle(Rectangle rectangle, int deltaWidth, int deltaHeight) + { + var centeredRectanglePos = new Point(rectangle.Location.X + deltaWidth, + rectangle.Location.Y + deltaHeight); + return new Rectangle(centeredRectanglePos, rectangle.Size); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Visualizers/IVisualizer.cs b/cs/TagsCloudVisualization/Visualizers/IVisualizer.cs new file mode 100644 index 000000000..547df523a --- /dev/null +++ b/cs/TagsCloudVisualization/Visualizers/IVisualizer.cs @@ -0,0 +1,7 @@ +namespace TagsCloudVisualization.Visualizers; +using System.Drawing; + +public interface IVisualizer +{ + public Bitmap CreateBitmap(IEnumerable rectangles); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/images/1000_100_0,5_TagCloud.jpg b/cs/TagsCloudVisualization/images/1000_100_0,5_TagCloud.jpg new file mode 100644 index 000000000..ac718bc20 Binary files /dev/null and b/cs/TagsCloudVisualization/images/1000_100_0,5_TagCloud.jpg differ diff --git a/cs/TagsCloudVisualization/images/1000_1_0,5_TagCloud.jpg b/cs/TagsCloudVisualization/images/1000_1_0,5_TagCloud.jpg new file mode 100644 index 000000000..fd1770ab3 Binary files /dev/null and b/cs/TagsCloudVisualization/images/1000_1_0,5_TagCloud.jpg differ diff --git a/cs/TagsCloudVisualization/images/1000_1_5_TagCloud.jpg b/cs/TagsCloudVisualization/images/1000_1_5_TagCloud.jpg new file mode 100644 index 000000000..859654fa2 Binary files /dev/null and b/cs/TagsCloudVisualization/images/1000_1_5_TagCloud.jpg differ diff --git a/cs/TagsCloudVisualizationTests/CircularCloudLayouterTest.cs b/cs/TagsCloudVisualizationTests/CircularCloudLayouterTest.cs new file mode 100644 index 000000000..125fe640e --- /dev/null +++ b/cs/TagsCloudVisualizationTests/CircularCloudLayouterTest.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using TagsCloudVisualization.CloudLayouters; +using TagsCloudVisualization.Extensions; +using TagsCloudVisualization.PointsGenerators; +using TagsCloudVisualization.Savers; +using TagsCloudVisualization.Visualizers; + +namespace TagsCloudVisualizationTests; + +[TestFixture] +[TestOf(typeof(CircularCloudLayouter))] +public class CircularCloudLayouterTest +{ + private readonly Random randomizer = new(); + private Rectangle[] testRectangles = null!; + private Point layoutCenter; + private const string ImagesDirectory = "testImages"; + private const string RectanglesDirectory = "testRectangles"; + + [TearDown] + public void TearDown() + { + var currentContext = TestContext.CurrentContext; + if (currentContext.Result.Outcome.Status != TestStatus.Failed) + return; + + var visualizer = new DefaultVisualizer(GetLayoutSize(testRectangles)); + + using var bitmap = visualizer.CreateBitmap(testRectangles); + var saver = new DefaultBitmapSaver(ImagesDirectory); + saver.SaveBitmap(bitmap, currentContext.Test.Name + ".jpg"); + + SaveInformationAboutTestRectangles(currentContext); + + TestContext.Out + .WriteLine($"Tag cloud visualization saved to file " + + $"{Path.Combine(ImagesDirectory, currentContext.Test.Name + ".jpg")}"); + TestContext.Out + .WriteLine($"Information about testRectangles saved to file " + + $"{Path.Combine(RectanglesDirectory, currentContext.Test.Name + "txt")}"); + } + + [Test] + public void PutNextRectangle_ShouldPutRectangle() + { + testRectangles = []; + var circularCloudLayouter = SetupLayouterWithRandomParameters(); + var rectangleSize = randomizer.RandomSize(); + + var rectangle = circularCloudLayouter.PutNextRectangle(rectangleSize); + testRectangles = [rectangle]; + + GetLayoutSize(testRectangles).Should().Be(rectangleSize); + } + + [Test] + public void PutNextRectangle_ShouldThrowInvalidOperationException_IfFiniteGenerator() + { + var finiteGenerator = new FinitePointsGenerator(0); + var circularCloudLayouter = new CircularCloudLayouter(new Point(0, 0), finiteGenerator); + var invoke = () => circularCloudLayouter.PutNextRectangle(new Size(1, 1)); + + invoke.Should().Throw(); + + } + + [Test] + [Repeat(10)] + public void PutNextRectangle_ShouldReturnRectangleInCenter_IfFirstInvoke() + { + testRectangles = []; + var rectangleSize = randomizer.RandomSize(); + var circularCloudLayouter = SetupLayouterWithRandomParameters(); + + var actualRectangle = circularCloudLayouter + .PutNextRectangle(rectangleSize); + var expectedRectangle = new Rectangle() + .CreateRectangleWithCenter(layoutCenter, rectangleSize); + + testRectangles = [actualRectangle]; + actualRectangle.Should().BeEquivalentTo(expectedRectangle); + } + + [Test] + public void PutNextRectangle_ShouldReturnRectangleWithRightSize() + { + testRectangles = []; + var rectangleSize = randomizer.RandomSize(); + var circularCloudLayouter = SetupLayouterWithRandomParameters(); + + var actualRectangle = circularCloudLayouter.PutNextRectangle(rectangleSize); + + testRectangles = [actualRectangle]; + actualRectangle.Size.Should().Be(rectangleSize); + } + + [Test] + [Repeat(10)] + public void PutNextRectangle_ShouldReturnRectanglesWithoutIntersections() + { + testRectangles = []; + var rectanglesNumber = randomizer.Next(100, 250); + var circularCloudLayouter = SetupLayouterWithRandomParameters(); + + testRectangles = PutRectanglesInLayouter(rectanglesNumber, circularCloudLayouter); + + IsIntersectionBetweenRectangles(testRectangles).Should().BeFalse(); + } + + [Test] + [Repeat(10)] + public void ShouldGenerateLayoutThatHasHighTightnessAndShapeOfCircularCloud_WhenOptimalParametersAreUsed() + { + testRectangles = []; + const double allowableDelta = 0.38; + + var circularCloudLayouter = SetupLayouterWithOptimalParameters(); + testRectangles = PutRectanglesInLayouter(randomizer.Next(800, 1200), circularCloudLayouter); + + var circumcircleRadius = testRectangles + .Max(r => r + .GetDistanceToMostRemoteCorner(layoutCenter)); + var circumcircleArea = Math.PI * Math.Pow(circumcircleRadius, 2); + var rectanglesArea = (double)testRectangles + .Select(rectangle => rectangle.Height * rectangle.Width) + .Sum(); + + var areasFraction = circumcircleArea / rectanglesArea; + areasFraction.Should().BeApproximately(1, allowableDelta); + } + + private Rectangle[] PutRectanglesInLayouter(int rectanglesNumber, + CircularCloudLayouter circularCloudLayouter) + { + return Enumerable + .Range(0, rectanglesNumber) + .Select(_ => randomizer.RandomSize(10, 25)) + .Select(circularCloudLayouter.PutNextRectangle) + .ToArray(); + } + + private CircularCloudLayouter SetupLayouterWithOptimalParameters() + { + layoutCenter = new Point(0, 0); + return new CircularCloudLayouter(layoutCenter, 1, 0.5); + } + + private CircularCloudLayouter SetupLayouterWithRandomParameters() + { + var radius = randomizer.Next(1, 10); + var angleOffset = randomizer.Next(1, 10); + layoutCenter = randomizer.RandomPoint(-10, 10); + + return new CircularCloudLayouter(layoutCenter, radius, angleOffset); + } + + private bool IsIntersectionBetweenRectangles(Rectangle[] rectangles) + { + for (var i = 0; i < rectangles.Length; i++) + { + for (var j = i + 1; j < rectangles.Length; j++) + { + if (rectangles[i].IntersectsWith(rectangles[j])) + return true; + } + } + + return false; + } + + private void SaveInformationAboutTestRectangles(TestContext currentContext) + { + var rectanglesPath = Path.Combine(RectanglesDirectory, + currentContext.Test.Name + ".txt"); + Directory.CreateDirectory(RectanglesDirectory); + File.WriteAllLines(rectanglesPath, + testRectangles.Select(rectangle => rectangle.ToString()).ToArray()); + } + + private Size GetLayoutSize(IEnumerable rectangles) + { + var layoutWidth = rectangles.Max(rectangle => rectangle.Right) - + rectangles.Min(rectangle => rectangle.Left); + var layoutHeight = rectangles.Max(rectangle => rectangle.Bottom) + - rectangles.Min(rectangle => rectangle.Top); + return new Size(layoutWidth, layoutHeight); + } + + class FinitePointsGenerator(int end) : IPointsGenerator + { + public IEnumerable GeneratePoints(Point startPoint) + { + return Enumerable.Range(0, end) + .Select(x => new Point(x, x)); + } + } + +} \ No newline at end of file diff --git a/cs/TagsCloudVisualizationTests/Extensions/EnumeratorExtensionTest.cs b/cs/TagsCloudVisualizationTests/Extensions/EnumeratorExtensionTest.cs new file mode 100644 index 000000000..6a0aa8e1a --- /dev/null +++ b/cs/TagsCloudVisualizationTests/Extensions/EnumeratorExtensionTest.cs @@ -0,0 +1,44 @@ +using System.Drawing; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using TagsCloudVisualization.Extensions; +using TagsCloudVisualization.PointsGenerators; + +namespace TagsCloudVisualizationTests.Extensions; + +[TestFixture] +[TestOf(typeof(EnumeratorExtension))] +public class EnumeratorExtensionTest +{ + + [Test] + public void ToIEnumerable_ShouldReturnExpectedEnumerable() + { + const int elementsNumber = 1000; + var centerPoint = new Point(0, 0); + var expectedEnumerable = new FermatSpiralPointsGenerator(1, 0.5) + .GeneratePoints(centerPoint); + var actualEnumerable = expectedEnumerable + .GetEnumerator() + .ToIEnumerable(); + + actualEnumerable.Take(elementsNumber).Should() + .BeEquivalentTo(expectedEnumerable.Take(elementsNumber)); + } + + [Test] + public void ToIEnumerable_ShouldContinueEnumeration() + { + const int elementsNumber = 10; + + var startEnumerable = Enumerable.Range(0, elementsNumber); + using var movedEnumerator = startEnumerable.GetEnumerator(); + movedEnumerator.MoveNext(); + var actualEnumerable = movedEnumerator.ToIEnumerable(); + var expectedEnumerable = Enumerable.Range(1, elementsNumber - 1); + + actualEnumerable.Should().BeEquivalentTo(expectedEnumerable); + + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualizationTests/Extensions/RandomExtensionTest.cs b/cs/TagsCloudVisualizationTests/Extensions/RandomExtensionTest.cs new file mode 100644 index 000000000..804418bc9 --- /dev/null +++ b/cs/TagsCloudVisualizationTests/Extensions/RandomExtensionTest.cs @@ -0,0 +1,62 @@ +using System; +using System.Drawing; +using FluentAssertions; +using NUnit.Framework; +using TagsCloudVisualization.Extensions; + +namespace TagsCloudVisualizationTests.Extensions; + +[TestFixture] +[TestOf(typeof(RandomExtension))] +public class RandomExtensionTest +{ + private const int Seed = 123452; + private readonly Random random = new(Seed); + + [TestCase(0, 5, Description = "Min value is zero")] + [TestCase(-1, 5, Description = "Min value is negative")] + [TestCase(10, 5, Description = "Min value is greater than max")] + public void RandomSize_ShouldThrowArgumentOutOfRangeException_IfWrongParameters(int minValue, int maxValue) + { + var randomSizeInvoke = () => random.RandomSize(minValue, maxValue); + randomSizeInvoke.Should().Throw(); + } + + [Test] + [Repeat(10)] + public void RandomSize_ShouldReturnExpectedRandomSize() + { + var seed = this.random.Next(); + var randomSize = new Random(seed); + var randomExpected = new Random(seed); + + var actualSize = randomSize.RandomSize(); + var expectedSize = new Size( + randomExpected.Next(1, int.MaxValue), + randomExpected.Next(1, int.MaxValue)); + + actualSize.Should().BeEquivalentTo(expectedSize); + } + + [Test] + public void RandomPoint_ShouldReturnPoint() + { + this.random.RandomPoint().Should().BeOfType(); + } + + [Test] + [Repeat(10)] + public void RandomPoint_ShouldReturnExpectedRandomPoint() + { + var seed = this.random.Next(); + var pointRandomizer = new Random(seed); + var expectedRandomizer = new Random(seed); + + var actualPoint = pointRandomizer.RandomPoint(); + var expectedPoint = new Point( + expectedRandomizer.Next(int.MinValue, int.MaxValue), + expectedRandomizer.Next(int.MinValue, int.MaxValue)); + + actualPoint.Should().BeEquivalentTo(expectedPoint); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualizationTests/Extensions/RectangleExtensionTest.cs b/cs/TagsCloudVisualizationTests/Extensions/RectangleExtensionTest.cs new file mode 100644 index 000000000..c5a05326d --- /dev/null +++ b/cs/TagsCloudVisualizationTests/Extensions/RectangleExtensionTest.cs @@ -0,0 +1,44 @@ +using System; +using System.Drawing; +using FluentAssertions; +using NUnit.Framework; +using TagsCloudVisualization.Extensions; + +namespace TagsCloudVisualizationTests.Extensions; + +[TestFixture] +[TestOf(typeof(RectangleExtension))] +public class RectangleExtensionTest +{ + + [TestCase(1, 1)] + [TestCase(2, 2)] + [TestCase(3, 3)] + [TestCase(2, 4)] + [TestCase(4, 2)] + public void GetDistanceToMostRemoteCorner_ShouldReturnExpectedValues( + int width, int height) + { + var rectangle = new Rectangle(new Point(0, 0), new Size(width, height)); + var expectedDistance = Math.Sqrt(width * width + height * height); + var actualDistance = rectangle.GetDistanceToMostRemoteCorner(new Point(0, 0)); + + actualDistance.Should().Be(expectedDistance); + } + + [TestCase(1, 1, 2, 2)] + [TestCase(1, 1, 7, 7)] + [TestCase(-1, -2, 4, 3)] + [TestCase(3, -1, 4, 3)] + [TestCase(-1, -1, 2, 2)] + public void CreateRectangleInCenter_ShouldCreateExpectedRectangle + (int x, int y, int width, int height) + { + var actualRectangle = new Rectangle().CreateRectangleWithCenter( + new Point(x, y), new Size(width, height)); + var expectedRectangle = new Rectangle( + new Point(x - width / 2, y - height / 2), new Size(width, height)); + + actualRectangle.Should().BeEquivalentTo(expectedRectangle); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualizationTests/FermatSpiralPointsGeneratorTest.cs b/cs/TagsCloudVisualizationTests/FermatSpiralPointsGeneratorTest.cs new file mode 100644 index 000000000..1721fcbee --- /dev/null +++ b/cs/TagsCloudVisualizationTests/FermatSpiralPointsGeneratorTest.cs @@ -0,0 +1,46 @@ +using System; +using System.Drawing; +using System.Linq; +using NUnit.Framework; +using FluentAssertions; +using TagsCloudVisualization.PointsGenerators; + +namespace TagsCloudVisualizationTests; + +[TestFixture] +[TestOf(typeof(FermatSpiralPointsGenerator))] +public class FermatSpiralPointsGeneratorTest +{ + + [TestCase(-1, 1, Description = "Negative radius")] + [TestCase(0, 1, Description = "Radius is zero")] + [TestCase(1, 0, Description = "AngleOffset is zero")] + [TestCase(1, -1, Description = "Negative angleOffset")] + public void ShouldThrowArgumentException_AfterWrongCreation(double radius, double angleOffset) + { + var creation = () => new FermatSpiralPointsGenerator(radius, angleOffset); + creation.Should().Throw(); + + } + + [TestCaseSource(nameof(GeneratePointsTestCases))] + public Point GeneratePoints_ShouldReturnCorrectPoint(double radius, double angleOffset, int pointNumber) + { + var pointsGenerator = new FermatSpiralPointsGenerator(radius, angleOffset); + var actualPoint = pointsGenerator + .GeneratePoints(new Point(0, 0)) + .Skip(pointNumber) + .First(); + return actualPoint; + } + + public static TestCaseData[] GeneratePointsTestCases = + [ + new TestCaseData(1, 125, 0).Returns(new Point(0, 0)), + new TestCaseData(10, 125, 1).Returns(new Point(-2, 3)), + new TestCaseData(1, 360, 1).Returns(new Point(1, 0)), + new TestCaseData(2, 180, 1).Returns(new Point(-1, 0)), + new TestCaseData(4, 90, 1).Returns(new Point(0, 1)), + new TestCaseData(4, 300, 1).Returns(new Point(2, -3)) + ]; +} \ No newline at end of file diff --git a/cs/TagsCloudVisualizationTests/Savers/DefaultBitmapSaverTest.cs b/cs/TagsCloudVisualizationTests/Savers/DefaultBitmapSaverTest.cs new file mode 100644 index 000000000..a2119de52 --- /dev/null +++ b/cs/TagsCloudVisualizationTests/Savers/DefaultBitmapSaverTest.cs @@ -0,0 +1,56 @@ +using System.Drawing; +using System.IO; +using FluentAssertions; +using NUnit.Framework; +using TagsCloudVisualization.Savers; + +namespace TagsCloudVisualizationTests.Savers; + +[TestFixture] +[TestOf(typeof(DefaultBitmapSaver))] +public class DefaultBitmapSaverTest +{ + + [Test] + public void SaveBitmap_ShouldCreatesDirectory() + { + var directory = "TestDirectory"; + try + { + if (Directory.Exists(directory)) + Directory.Delete(directory, true); + var saver = new DefaultBitmapSaver(directory); + var bitmap = new Bitmap(100, 100); + + saver.SaveBitmap(bitmap, directory); + + Directory.Exists(directory).Should().BeTrue(); + } + finally + { + Directory.Delete(directory, true); + } + } + + + [Test] + public void SaveBitmap_ShouldCreatesJpegFile() + { + const string directory = "TestDirectory"; + const string filename = "TestFilename.jpg"; + try + { + if (Directory.Exists(directory)) + Directory.Delete(directory, true); + var saver = new DefaultBitmapSaver(directory); + var bitmap = new Bitmap(100, 100); + + saver.SaveBitmap(bitmap, filename); + File.Exists(Path.Combine(directory, filename)).Should().BeTrue(); + } + finally + { + Directory.Delete(directory, true); + } + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualizationTests/TagsCloudVisualizationTests.csproj b/cs/TagsCloudVisualizationTests/TagsCloudVisualizationTests.csproj new file mode 100644 index 000000000..4254438b7 --- /dev/null +++ b/cs/TagsCloudVisualizationTests/TagsCloudVisualizationTests.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + + false + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cs/tdd.sln b/cs/tdd.sln index c8f523d63..e66200e2a 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", "{01ADC663-2353-458D-9769-790C224690B2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudVisualizationTests", "TagsCloudVisualizationTests\TagsCloudVisualizationTests.csproj", "{EE74CFE7-F089-4D9E-861E-19C4D4854974}" +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 + {01ADC663-2353-458D-9769-790C224690B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01ADC663-2353-458D-9769-790C224690B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01ADC663-2353-458D-9769-790C224690B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01ADC663-2353-458D-9769-790C224690B2}.Release|Any CPU.Build.0 = Release|Any CPU + {EE74CFE7-F089-4D9E-861E-19C4D4854974}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE74CFE7-F089-4D9E-861E-19C4D4854974}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE74CFE7-F089-4D9E-861E-19C4D4854974}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE74CFE7-F089-4D9E-861E-19C4D4854974}.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