diff --git a/cs/TagsCloudTests/CloudLayouterShould.cs b/cs/TagsCloudTests/CloudLayouterShould.cs new file mode 100644 index 000000000..78f32a586 --- /dev/null +++ b/cs/TagsCloudTests/CloudLayouterShould.cs @@ -0,0 +1,114 @@ +using FluentAssertions; +using System.Drawing; +using FluentAssertions.Execution; +using TagsCloudVisualization; +using TagsCloudVisualization.Extensions; +using TagsCloudVisualization.Interfaces; + +[assembly: Parallelizable(ParallelScope.Children)] + +namespace TagsCloudTests; + +[TestFixture] +public class CloudLayouterShould +{ + private readonly Point _defaultCenter = new(0, 0); + private readonly Random _random = new(); + private readonly ISizesGenerator _defaultSizesGenerator = new RandomSizesGenerator(); + + [Test] + [Repeat(5)] + public void PutNextRectangle_ReturnRectangleWithExpectedLocation_AfterFirstExecution() + { + var expectedCenter = new Point(_random.Next(-10, 10), _random.Next(-10, 10)); + var rectangleSize = _defaultSizesGenerator + .GenerateSize() + .Take(1) + .First(); + var cloudLayouter = new CircularCloudLayouter(expectedCenter); + + var actualRectangle = cloudLayouter.PutNextRectangle(rectangleSize); + + actualRectangle + .GetCentralPoint() + .Should() + .BeEquivalentTo(expectedCenter); + } + + [TestCase(-1, 1)] + [TestCase(1, -1)] + [TestCase(0, 0)] + public void PutNextRectangle_ThrowArgumentOutOfRangeException_AfterExecutionWith(int width, int height) + { + var rectangleSize = new Size(width, height); + var circularCloudLayouter = new CircularCloudLayouter(_defaultCenter); + + var executePutNewRectangle = () => + circularCloudLayouter + .PutNextRectangle(rectangleSize); + + executePutNewRectangle + .Should() + .Throw(); + } + + [Test] + [Repeat(5)] + public void PutNextRectangle_ReturnRectangleThatNotIntersectsWithOther_AfterManyExecution() + { + var rectangleSizes = _defaultSizesGenerator + .GenerateSize() + .Take(_random.Next(10, 200)); + var cloudLayouter = new CircularCloudLayouter(_defaultCenter); + + var rectangles = rectangleSizes + .Select(size => cloudLayouter.PutNextRectangle(size)) + .ToArray(); + + for (var i = 0; i < rectangles.Length; i++) + for (var j = i + 1; j < rectangles.Length; j++) + rectangles[i] + .IntersectsWith(rectangles[j]) + .Should() + .BeFalse(); + } + + [Test] + [Repeat(20)] + public void PutNextRectangle_ReturnRectanglesInCircle_AfterManyExecution() + { + var rectangleSizes = _defaultSizesGenerator + .GenerateSize() + .Take(_random.Next(100, 200)); + var circularCloudLayouter = new CircularCloudLayouter(_defaultCenter); + + var rectanglesList = rectangleSizes + .Select(rectangleSize => circularCloudLayouter + .PutNextRectangle(rectangleSize)) + .ToList(); + + var circleRadius = rectanglesList + .Select(rectangle => rectangle.GetCentralPoint()) + .Max(pointOnCircle => pointOnCircle.GetDistanceTo(_defaultCenter)); + var fromRectangleToCenterDistances = + rectanglesList + .Select(rectangle => rectangle + .GetCentralPoint() + .GetDistanceTo(_defaultCenter)); + + var sumRectanglesSquare = rectanglesList.Sum(rectangle => rectangle.Width * rectangle.Height); + var circleSquare = circleRadius * circleRadius * Math.PI; + var precision = circleSquare * 0.375; + + using (new AssertionScope()) + { + circleSquare + .Should() + .BeApproximately(sumRectanglesSquare, precision); + foreach (var distanceToCenter in fromRectangleToCenterDistances) + distanceToCenter + .Should() + .BeLessOrEqualTo(circleRadius); + } + } +} \ No newline at end of file diff --git a/cs/TagsCloudTests/PointExtensionShould.cs b/cs/TagsCloudTests/PointExtensionShould.cs new file mode 100644 index 000000000..de07621f1 --- /dev/null +++ b/cs/TagsCloudTests/PointExtensionShould.cs @@ -0,0 +1,40 @@ +using System.Drawing; +using FluentAssertions; +using TagsCloudVisualization.Extensions; + +namespace TagsCloudTests; + +[TestFixture] +public class PointExtensionShould +{ + [TestCase(0, 0, 1, 2)] + [TestCase(0, 0, 1, 0)] + [TestCase(3, 5, 0, 5)] + public void PointShiftTo_ReturnedMovedPoint_WhenSet(int pointX, int pointY, int shiftX, int shiftY) + { + var point = new Point(pointX, pointY); + var movementDirection = new Size(shiftX, shiftY); + + var movedPoint = Point.Add(point, movementDirection); + + MoveTo_CheckShift(point, movementDirection, movedPoint); + } + + [Test] + public void MoveTo_ReturnedNotMovedPoint_WhenSetZeroDirection() + { + var point = new Point(5, 6); + + MoveTo_CheckShift(point, new Size(0, 0), point); + } + + private static void MoveTo_CheckShift(Point point, Size movementDirection, Point expectedPoint) + { + var movedPoint = point.MoveTo(movementDirection); + + movedPoint + .Should() + .BeEquivalentTo(expectedPoint); + } +} + diff --git a/cs/TagsCloudTests/PointGeneratorShould.cs b/cs/TagsCloudTests/PointGeneratorShould.cs new file mode 100644 index 000000000..ffcc4536e --- /dev/null +++ b/cs/TagsCloudTests/PointGeneratorShould.cs @@ -0,0 +1,70 @@ +using System.Drawing; +using FluentAssertions; +using TagsCloudVisualization; +using TagsCloudVisualization.Extensions; +using TagsCloudVisualization.Interfaces; + +namespace TagsCloudTests; + +[TestFixture] +public class PointGeneratorShould +{ + private readonly Point _defaultCenter = new(1, 1); + private readonly Random _random = new(); + + [Test] + public void GetNewPoint_ReturnCenter_AfterFirstExecution() + { + var pointGenerator = new SpiralPointGenerator(_defaultCenter); + using var newPointIterator = pointGenerator + .GeneratePoint() + .GetEnumerator(); + newPointIterator.MoveNext(); + var point = newPointIterator.Current; + + point + .Should() + .BeEquivalentTo(_defaultCenter); + } + + [TestCase(0)] + [TestCase(-1)] + public void ThrowArgumentOutOfRangeException_AfterExecutionWith(double radiusStep) + { + var pointGeneratorCreate = () => new SpiralPointGenerator(_defaultCenter, radiusStep); + + pointGeneratorCreate + .Should() + .Throw(); + } + + [Test] + [Repeat(20)] + public void GetNewPoint_ReturnPointWithGreaterRadius_AfterManyExecutions() + { + var newPointGenerator = new SpiralPointGenerator(_defaultCenter); + var countOfPoints = _random.Next(10, 200); + var points = newPointGenerator + .GeneratePoint() + .Take(countOfPoints) + .ToArray(); + + var distances = points + .Select(p => p.GetDistanceTo(_defaultCenter)) + .ToArray(); + var angles = points + .Select(p => Math.Atan2(p.Y - _defaultCenter.Y, p.X - _defaultCenter.X)) + .ToArray(); + + distances + .Zip(distances.Skip(1), (a, b) => a <= b) + .All(x => x) + .Should() + .BeTrue(); + angles + .Zip(angles.Skip(1), (a, b) => a <= b || Math.Abs(a - b) < 0.1) + .All(x => x) + .Should() + .BeTrue(); + } +} \ No newline at end of file diff --git a/cs/TagsCloudTests/RectangleExtensionShould.cs b/cs/TagsCloudTests/RectangleExtensionShould.cs new file mode 100644 index 000000000..49e0f4dd4 --- /dev/null +++ b/cs/TagsCloudTests/RectangleExtensionShould.cs @@ -0,0 +1,26 @@ +using System.Drawing; +using FluentAssertions; +using TagsCloudVisualization.Extensions; + +namespace TagsCloudTests; + +[TestFixture] +public class RectangleExtensionShould +{ + [Test] + [Repeat(5)] + public void GetCentralPoint_ReturnExpectedCenter_AfterExecutionWithRandomRectangle() + { + var rectangleLocation = new Point(0, 0); + var random = new Random(); + var rectangleSize = new Size(random.Next(1, 100), random.Next(1, 100)); + var rectangle = new Rectangle(rectangleLocation, rectangleSize); + var expectedCentralPoint = new Point(rectangleLocation.X + rectangleSize.Width / 2, + rectangleLocation.Y - rectangleSize.Height / 2); + + rectangle + .GetCentralPoint() + .Should() + .Be(expectedCentralPoint); + } +} \ No newline at end of file diff --git a/cs/TagsCloudTests/TagCloudCreatorShould.cs b/cs/TagsCloudTests/TagCloudCreatorShould.cs new file mode 100644 index 000000000..390797944 --- /dev/null +++ b/cs/TagsCloudTests/TagCloudCreatorShould.cs @@ -0,0 +1,34 @@ +using System.Drawing; +using FluentAssertions; +using TagsCloudVisualization; + +namespace TagsCloudTests; + +[TestFixture] +public class TagCloudCreatorShould +{ + private readonly Point _defaultCenter = new(0, 0); + private readonly Random _random = new(); + + [Test] + [Repeat(5)] + public void Create_ReturnTagCloudEquivalentToConstructor_AfterExecution() + { + var sizesGenerator = new RandomSizesGenerator(); + var rectangleSizes = sizesGenerator + .GenerateSize() + .Take(_random.Next(10, 200)) + .ToArray(); + var currCloudLayouter = new CircularCloudLayouter(_defaultCenter); + var expectedCloudLayouter = new CircularCloudLayouter(_defaultCenter); + var defaultTagCloud = new TagCloud(currCloudLayouter); + foreach (var rectangleSize in rectangleSizes) + defaultTagCloud.AddNextRectangleWith(rectangleSize); + + var expectedTagCloud = TagCloudCreator.Create(rectangleSizes, expectedCloudLayouter); + + defaultTagCloud + .Should() + .BeEquivalentTo(expectedTagCloud); + } +} \ No newline at end of file diff --git a/cs/TagsCloudTests/TagCloudShould.cs b/cs/TagsCloudTests/TagCloudShould.cs new file mode 100644 index 000000000..191a9af67 --- /dev/null +++ b/cs/TagsCloudTests/TagCloudShould.cs @@ -0,0 +1,148 @@ +using System.Drawing; +using FluentAssertions; +using NUnit.Framework.Interfaces; +using TagsCloudVisualization; +using TagsCloudVisualization.Extensions; +using TagsCloudVisualization.Interfaces; + +namespace TagsCloudTests; + +[TestFixture] +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class TagCloudShould +{ + private readonly Random _random = new(); + private readonly Point _defaultCenter = new(1, 1); + private readonly string _failedTestsPictureFolder = "FailedPictures"; + private readonly ICloudLayouter _defaultLayouter; + private TagCloud _defaultTagCloud; + + public TagCloudShould() + { + _defaultLayouter = new CircularCloudLayouter(_defaultCenter); + _defaultTagCloud = new TagCloud(_defaultLayouter); + } + + [TearDown] + public void TearDown() + { + var context = TestContext.CurrentContext; + if (context.Result.Outcome.Status != TestStatus.Failed) + return; + + Directory.CreateDirectory(_failedTestsPictureFolder); + var fileName = $"{context.Test.MethodName}{_random.Next()}"; + var filePath = Path.Combine(_failedTestsPictureFolder, fileName); + + TagCloudVisualization.SaveTagCloudAsBitmap(_defaultTagCloud, filePath + ".bmp"); + + TestContext.WriteLine($"Tag cloud visualization saved to file {filePath}"); + } + + [Test] + [Repeat(5)] + public void PlacedInSelectedCenter_AfterCreation() + { + var x = _random.Next(0, 10); + var y = _random.Next(0, 10); + var center = new Point(x, y); + var firstRectangleSize = new Size(2, 2); + var circularCloudLayouter = new CircularCloudLayouter(center); + var tagCloud = new TagCloud(circularCloudLayouter); + + tagCloud.AddNextRectangleWith(firstRectangleSize); + + tagCloud.Rectangles[0] + .GetCentralPoint() + .Should() + .Be(center); + } + + [Test] + public void HaveZeroSize_AfterCreation() + { + _defaultTagCloud.Width + .Should() + .Be(0); + _defaultTagCloud.Height + .Should() + .Be(0); + } + + [Test] + [Repeat(5)] + public void HaveExpectedWidth_AfterAddedFirstRectangle() + { + var width = _random.Next(1, 100); + var newSize = new Size(width, 2); + + _defaultTagCloud.AddNextRectangleWith(newSize); + + _defaultTagCloud.Width + .Should() + .Be(width); + } + + [Test] + [Repeat(5)] + public void HaveExpectedHeight_AfterAddedFirstRectangle() + { + var height = _random.Next(1, 100); + var newSize = new Size(2, height); + + _defaultTagCloud.AddNextRectangleWith(newSize); + + _defaultTagCloud.Height + .Should() + .Be(height); + } + + [Test] + [Repeat(5)] + public void HaveExpectedWidth_AfterAddedManyRectangles() + { + var width = _random.Next(1, 100); + var newSize = new Size(width, 2); + var rectanglesCount = _random.Next(5, 40); + var expectedWidth = rectanglesCount * width; + + for (var i = 0; i < rectanglesCount; i++) + _defaultTagCloud.AddNextRectangleWith(newSize); + + _defaultTagCloud.Width + .Should() + .BeLessThanOrEqualTo(expectedWidth); + } + + [Test] + [Repeat(5)] + public void HaveExpectedHeight_AfterAddedManyRectangles() + { + var height = _random.Next(1, 100); + var newSize = new Size(2, height); + var rectanglesCount = _random.Next(5, 40); + var expectedHeight = rectanglesCount * height; + + for (var i = 0; i < rectanglesCount; i++) + _defaultTagCloud.AddNextRectangleWith(newSize); + + _defaultTagCloud.Height + .Should() + .BeLessThanOrEqualTo(expectedHeight); + } + + [Test] + [Repeat(5)] + public void AddNextRectangleWith_AddedExpectedNumberOfRectangles_AfterManyExecutions() + { + var expectedCount = _random.Next(0, 10); + var unionSize = new Size(2, 2); + + for (var i = 0; i < expectedCount; i++) + _defaultTagCloud.AddNextRectangleWith(unionSize); + + _defaultTagCloud.Rectangles + .Should() + .HaveCount(expectedCount); + } +} \ No newline at end of file diff --git a/cs/TagsCloudTests/TagsCloudTests.csproj b/cs/TagsCloudTests/TagsCloudTests.csproj new file mode 100644 index 000000000..901f17414 --- /dev/null +++ b/cs/TagsCloudTests/TagsCloudTests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/cs/TagsCloudVisualization/CircularCloudLayouter.cs b/cs/TagsCloudVisualization/CircularCloudLayouter.cs new file mode 100644 index 000000000..9ad840746 --- /dev/null +++ b/cs/TagsCloudVisualization/CircularCloudLayouter.cs @@ -0,0 +1,48 @@ +using System.Drawing; +using TagsCloudVisualization.Extensions; +using TagsCloudVisualization.Interfaces; + +namespace TagsCloudVisualization; + +public class CircularCloudLayouter : ICloudLayouter +{ + private readonly IEnumerator _pointGeneratorIterator; + private readonly List _rectangles = new(); + + public CircularCloudLayouter(Point center) + { + var pointGenerator = new SpiralPointGenerator(center); + _pointGeneratorIterator = pointGenerator + .GeneratePoint() + .GetEnumerator(); + } + + public Rectangle PutNextRectangle(Size size) + { + if (size.Width < 1 || size.Height < 1) + throw new ArgumentOutOfRangeException( + $"{nameof(size.Width)} and {nameof(size.Height)} should be greater than zero"); + + Rectangle newRectangle; + do newRectangle = GetNextRectangle(size); + while (_rectangles.Any(rec => rec.IntersectsWith(newRectangle))); + + _rectangles.Add(newRectangle); + return newRectangle; + } + + private Rectangle GetNextRectangle(Size rectangleSize) => + new(GetNextRectangleCenter(rectangleSize), rectangleSize); + + private Point GetNextRectangleCenter(Size rectangleSize) + { + _pointGeneratorIterator.MoveNext(); + var rectangleCenter = ShiftRectangleLocationBy(rectangleSize); + return _pointGeneratorIterator + .Current + .MoveTo(rectangleCenter); + } + + private static Size ShiftRectangleLocationBy(Size rectangleSize) => + new(-rectangleSize.Width / 2, rectangleSize.Height / 2); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Extensions/PointExtension.cs b/cs/TagsCloudVisualization/Extensions/PointExtension.cs new file mode 100644 index 000000000..39acaf6d5 --- /dev/null +++ b/cs/TagsCloudVisualization/Extensions/PointExtension.cs @@ -0,0 +1,13 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Extensions; + +public static class PointExtension +{ + public static Point MoveTo(this Point point, Size direction) => + Point.Add(point, direction); + + public static double GetDistanceTo(this Point point1, Point point2) => + Math.Sqrt(Math.Pow(point1.X - point2.X, 2) + Math.Pow(point1.Y - point2.Y, 2)); +} + diff --git a/cs/TagsCloudVisualization/Extensions/RectangleExtension.cs b/cs/TagsCloudVisualization/Extensions/RectangleExtension.cs new file mode 100644 index 000000000..df69e457c --- /dev/null +++ b/cs/TagsCloudVisualization/Extensions/RectangleExtension.cs @@ -0,0 +1,14 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Extensions; + +public static class RectangleExtension +{ + public static Point GetCentralPoint(this Rectangle rectangle) + { + var centerPoint = rectangle.Location; + centerPoint.Offset(rectangle.Width / 2, -rectangle.Height / 2); + return centerPoint; + } +} + diff --git a/cs/TagsCloudVisualization/Interfaces/ICloudLayouter.cs b/cs/TagsCloudVisualization/Interfaces/ICloudLayouter.cs new file mode 100644 index 000000000..df3549ae1 --- /dev/null +++ b/cs/TagsCloudVisualization/Interfaces/ICloudLayouter.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Interfaces; + +public interface ICloudLayouter +{ + public Rectangle PutNextRectangle(Size rectangleSize); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Interfaces/IPointGenerator.cs b/cs/TagsCloudVisualization/Interfaces/IPointGenerator.cs new file mode 100644 index 000000000..00a5c6a4d --- /dev/null +++ b/cs/TagsCloudVisualization/Interfaces/IPointGenerator.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Interfaces; + +public interface IPointGenerator +{ + public IEnumerable GeneratePoint(); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Interfaces/ISizesGenerator.cs b/cs/TagsCloudVisualization/Interfaces/ISizesGenerator.cs new file mode 100644 index 000000000..4f694e73c --- /dev/null +++ b/cs/TagsCloudVisualization/Interfaces/ISizesGenerator.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Interfaces; + +public interface ISizesGenerator +{ + public IEnumerable GenerateSize(); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Program.cs b/cs/TagsCloudVisualization/Program.cs new file mode 100644 index 000000000..ce19b8a58 --- /dev/null +++ b/cs/TagsCloudVisualization/Program.cs @@ -0,0 +1,20 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +[System.Runtime.Versioning.SupportedOSPlatform("windows")] +public static class Program +{ + public static void Main(string[] args) + { + const string tempBmpFile = "temp.bmp"; + var cloudLayouter = new CircularCloudLayouter(new Point(-5, 5)); + var sizesGenerator = new RandomSizesGenerator(); + var sizes = sizesGenerator + .GenerateSize() + .Take(500); + var tagCloud = TagCloudCreator.Create(sizes, cloudLayouter); + + TagCloudVisualization.SaveTagCloudAsBitmap(tagCloud, tempBmpFile); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/README.md b/cs/TagsCloudVisualization/README.md new file mode 100644 index 000000000..83c5e1237 --- /dev/null +++ b/cs/TagsCloudVisualization/README.md @@ -0,0 +1,10 @@ +# TagCloud Samples + +## 200 squares +![alt text](Samples/200Squares.bmp) + +## 350 rectangles +![alt text](Samples/350Rectangles.bmp) + +## 500 rectangles with random sizes +![alt text](Samples/500RectanglesWithRandomSize.bmp) \ No newline at end of file diff --git a/cs/TagsCloudVisualization/RandomSizesGenerator.cs b/cs/TagsCloudVisualization/RandomSizesGenerator.cs new file mode 100644 index 000000000..0ce7a38b6 --- /dev/null +++ b/cs/TagsCloudVisualization/RandomSizesGenerator.cs @@ -0,0 +1,19 @@ +using System.Drawing; +using TagsCloudVisualization.Interfaces; + +namespace TagsCloudVisualization; + +public class RandomSizesGenerator : ISizesGenerator +{ + public IEnumerable GenerateSize() + { + var random = new Random(); + while (true) + { + var rectangleWidth = random.Next(10, 100); + var rectangleHeight = random.Next(1, 25); + yield return new Size(rectangleWidth, rectangleHeight); + } + // ReSharper disable once IteratorNeverReturns + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Samples/200Squares.bmp b/cs/TagsCloudVisualization/Samples/200Squares.bmp new file mode 100644 index 000000000..94ec28968 Binary files /dev/null and b/cs/TagsCloudVisualization/Samples/200Squares.bmp differ diff --git a/cs/TagsCloudVisualization/Samples/350Rectangles.bmp b/cs/TagsCloudVisualization/Samples/350Rectangles.bmp new file mode 100644 index 000000000..e7b26b212 Binary files /dev/null and b/cs/TagsCloudVisualization/Samples/350Rectangles.bmp differ diff --git a/cs/TagsCloudVisualization/Samples/500RectanglesWithRandomSize.bmp b/cs/TagsCloudVisualization/Samples/500RectanglesWithRandomSize.bmp new file mode 100644 index 000000000..8b9999109 Binary files /dev/null and b/cs/TagsCloudVisualization/Samples/500RectanglesWithRandomSize.bmp differ diff --git a/cs/TagsCloudVisualization/SpiralPointGenerator.cs b/cs/TagsCloudVisualization/SpiralPointGenerator.cs new file mode 100644 index 000000000..beab94175 --- /dev/null +++ b/cs/TagsCloudVisualization/SpiralPointGenerator.cs @@ -0,0 +1,39 @@ +using System.Drawing; +using TagsCloudVisualization.Extensions; +using TagsCloudVisualization.Interfaces; + +namespace TagsCloudVisualization; + +public class SpiralPointGenerator : IPointGenerator +{ + private const double AngleStep = Math.PI / 360; + private readonly double _radiusStep; + private readonly Size _center; + + public SpiralPointGenerator(Point centerPoint, double radiusStep = 0.01) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(radiusStep); + + _radiusStep = radiusStep; + _center = new Size(centerPoint); + } + + public IEnumerable GeneratePoint() + { + var radius = 0d; + var angle = 0d; + + while (true) + { + var newX = (int)(radius * Math.Cos(angle)); + var newY = (int)(radius * Math.Sin(angle)); + var newPoint = new Point(newX, newY).MoveTo(_center); + + radius += _radiusStep; + angle += AngleStep; + + yield return newPoint; + } + // ReSharper disable once IteratorNeverReturns + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagCloud.cs b/cs/TagsCloudVisualization/TagCloud.cs new file mode 100644 index 000000000..aa6505d32 --- /dev/null +++ b/cs/TagsCloudVisualization/TagCloud.cs @@ -0,0 +1,36 @@ +using System.Drawing; +using TagsCloudVisualization.Interfaces; + +namespace TagsCloudVisualization; + +public class TagCloud(ICloudLayouter layouter) +{ + private readonly ICloudLayouter _layouter = layouter; + public List Rectangles { get; } = new(); + private int _maxRight = 0; + private int _maxBottom = 0; + private int _minLeft = int.MaxValue; + private int _minTop = int.MaxValue; + + public void AddNextRectangleWith(Size size) + { + var nextRectangle = _layouter.PutNextRectangle(size); + _maxRight = Math.Max(_maxRight, nextRectangle.Right); + _maxBottom = Math.Max(_maxBottom, nextRectangle.Bottom); + _minLeft = Math.Min(_minLeft, nextRectangle.Left); + _minTop = Math.Min(_minTop, nextRectangle.Top); + Rectangles.Add(nextRectangle); + } + + public int Width => + Rectangles.Count == 0 ? 0 : _maxRight - _minLeft; + + public int Height => + Rectangles.Count == 0 ? 0 : _maxBottom - _minTop; + + public int LeftBound => + _minLeft; + + public int TopBound => + _minTop; +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagCloudCreator.cs b/cs/TagsCloudVisualization/TagCloudCreator.cs new file mode 100644 index 000000000..417264722 --- /dev/null +++ b/cs/TagsCloudVisualization/TagCloudCreator.cs @@ -0,0 +1,17 @@ +using System.Drawing; +using TagsCloudVisualization.Interfaces; + +namespace TagsCloudVisualization; + +public class TagCloudCreator +{ + public static TagCloud Create(IEnumerable rectangleSizes, ICloudLayouter cloudLayouter) + { + var newTagCloud = new TagCloud(cloudLayouter); + + foreach (var rectangleSize in rectangleSizes) + newTagCloud.AddNextRectangleWith(rectangleSize); + + return newTagCloud; + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagCloudVisualization.cs b/cs/TagsCloudVisualization/TagCloudVisualization.cs new file mode 100644 index 000000000..57893dfd7 --- /dev/null +++ b/cs/TagsCloudVisualization/TagCloudVisualization.cs @@ -0,0 +1,42 @@ +using System.Drawing; +using TagsCloudVisualization.Extensions; + +namespace TagsCloudVisualization; + +[System.Runtime.Versioning.SupportedOSPlatform("windows")] +public class TagCloudVisualization +{ + private static readonly Random _random = new(); + + public static void SaveTagCloudAsBitmap(TagCloud tagCloud, string file) + { + const int rectangleOutline = 1; + + using var bitmap = new Bitmap( + tagCloud.Width + rectangleOutline, + tagCloud.Height + rectangleOutline); + var frameShift = new Size(-tagCloud.LeftBound, -tagCloud.TopBound); + + using (var graphics = Graphics.FromImage(bitmap)) + { + foreach (var rectangle in tagCloud.Rectangles) + { + var rectangleInFrame = MoveRectangleToImageFrame(rectangle, frameShift); + graphics.DrawRectangle(GetRandomPen(), rectangleInFrame); + } + } + + bitmap.Save(file); + } + + private static Rectangle MoveRectangleToImageFrame(Rectangle rectangle, Size imageCenter) => + new(rectangle.Location.MoveTo(imageCenter), rectangle.Size); + + private static Pen GetRandomPen() + { + return new Pen(Color.FromArgb( + _random.Next(0, 255), + _random.Next(0, 255), + _random.Next(0, 255))); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagsCloudVisualization.csproj b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj new file mode 100644 index 000000000..7916e9dd2 --- /dev/null +++ b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/cs/tdd.sln b/cs/tdd.sln index c8f523d63..c33e836ed 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.11.35327.3 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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagsCloudVisualization", "TagsCloudVisualization\TagsCloudVisualization.csproj", "{964110C5-0E37-470C-9397-F58944BCCE07}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudTests", "TagsCloudTests\TagsCloudTests.csproj", "{C3EAA3E8-CE30-40B9-AAFA-75C61A3FC4DE}" 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 + {964110C5-0E37-470C-9397-F58944BCCE07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {964110C5-0E37-470C-9397-F58944BCCE07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {964110C5-0E37-470C-9397-F58944BCCE07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {964110C5-0E37-470C-9397-F58944BCCE07}.Release|Any CPU.Build.0 = Release|Any CPU + {C3EAA3E8-CE30-40B9-AAFA-75C61A3FC4DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3EAA3E8-CE30-40B9-AAFA-75C61A3FC4DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3EAA3E8-CE30-40B9-AAFA-75C61A3FC4DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3EAA3E8-CE30-40B9-AAFA-75C61A3FC4DE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {98BDF24F-CE60-494B-9373-C5A6B8BEDCAB} + EndGlobalSection EndGlobal 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