diff --git a/cs/DrawingTagsCloudVisualization/DecreasingRectangles120.png b/cs/DrawingTagsCloudVisualization/DecreasingRectangles120.png new file mode 100644 index 000000000..9590045be Binary files /dev/null and b/cs/DrawingTagsCloudVisualization/DecreasingRectangles120.png differ diff --git a/cs/DrawingTagsCloudVisualization/DrawingExamples.cs b/cs/DrawingTagsCloudVisualization/DrawingExamples.cs new file mode 100644 index 000000000..5f8f534df --- /dev/null +++ b/cs/DrawingTagsCloudVisualization/DrawingExamples.cs @@ -0,0 +1,58 @@ +using System.Drawing; +using TagsCloudVisualization; + +namespace DrawingTagsCloudVisualization; + +public class DrawingExamples +{ + static void Main() + { + DrawImage_DecreasingRectangles120(); + DrawImage_MixedRectangles320(); + DrawImage_EqualsRectangles250(); + } + + public static void DrawImage_EqualsRectangles250() + { + var tempLayouter = new CircularCloudLayouter(new Point(400, 400)); + for (int i = 0; i < 250; i++) + tempLayouter.PutNextRectangle(new Size(10, 5)); + DrawingTagsCloud drawingTagsCloud = new DrawingTagsCloud(new Point(tempLayouter.CenterCloud.X * 2, tempLayouter.CenterCloud.Y * 2), tempLayouter.GetRectangles); + drawingTagsCloud.SaveToFile("EqualsRectangles250.png"); + } + + public static void DrawImage_MixedRectangles320() + { + var tempLayouter = new CircularCloudLayouter(new Point(400, 400)); + var rectanglesSizes = new List + { + new Size(10, 5), + new Size(8, 8), + new Size(12, 3), + new Size(6, 10) + }; + for (int i = 0; i < 80; i++) + { + foreach (var size in rectanglesSizes) + tempLayouter.PutNextRectangle(size); + } + DrawingTagsCloud drawingTagsCloud = new DrawingTagsCloud(new Point(tempLayouter.CenterCloud.X * 2, tempLayouter.CenterCloud.Y * 2), tempLayouter.GetRectangles); + drawingTagsCloud.SaveToFile("MixedRectangles320.png"); + } + + public static void DrawImage_DecreasingRectangles120() + { + var tempLayouter = new CircularCloudLayouter(new Point(400, 400)); + tempLayouter.PutNextRectangle(new Size(160, 180)); + for (int i = 0; i < 80; i++) + { + tempLayouter.PutNextRectangle(new Size(60, 40)); + } + for (int i = 0; i < 39; i++) + { + tempLayouter.PutNextRectangle(new Size(20, 25)); + } + DrawingTagsCloud drawingTagsCloud = new DrawingTagsCloud(new Point(tempLayouter.CenterCloud.X * 2, tempLayouter.CenterCloud.Y * 2), tempLayouter.GetRectangles); + drawingTagsCloud.SaveToFile("DecreasingRectangles120.png"); + } +} \ No newline at end of file diff --git a/cs/DrawingTagsCloudVisualization/DrawingTagsCloud.cs b/cs/DrawingTagsCloudVisualization/DrawingTagsCloud.cs new file mode 100644 index 000000000..fef08acff --- /dev/null +++ b/cs/DrawingTagsCloudVisualization/DrawingTagsCloud.cs @@ -0,0 +1,40 @@ +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Graphics.Skia; +using System.Drawing; + +namespace DrawingTagsCloudVisualization; + +public class DrawingTagsCloud +{ + private readonly System.Drawing.Point centercloud; + private readonly List rectangles; + + public DrawingTagsCloud(System.Drawing.Point center, List rectanglesInput) + { + this.centercloud = center; + this.rectangles = rectanglesInput; + } + + public void SaveToFile(string filePath) + { + using var bitmapContext = new SkiaBitmapExportContext(800, 800, 2.0f); + + var canvas = bitmapContext.Canvas; + canvas.FontColor = Colors.Black; + canvas = Draw(canvas); + using var image = bitmapContext.Image; + using var stream = File.OpenWrite(filePath); + image.Save(stream); + } + + private ICanvas Draw(ICanvas canvas) + { + canvas.FillColor = Colors.Blue; + + foreach (var rect in rectangles) + { + canvas.FillRectangle(rect.X, rect.Y, rect.Width, rect.Height); + } + return canvas; + } +} diff --git a/cs/DrawingTagsCloudVisualization/DrawingTagsCloudVisualization.csproj b/cs/DrawingTagsCloudVisualization/DrawingTagsCloudVisualization.csproj new file mode 100644 index 000000000..e72974258 --- /dev/null +++ b/cs/DrawingTagsCloudVisualization/DrawingTagsCloudVisualization.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + false + false + Exe + + + + + + + + + + + + diff --git a/cs/DrawingTagsCloudVisualization/EqualsRectangles250.png b/cs/DrawingTagsCloudVisualization/EqualsRectangles250.png new file mode 100644 index 000000000..b88558943 Binary files /dev/null and b/cs/DrawingTagsCloudVisualization/EqualsRectangles250.png differ diff --git a/cs/DrawingTagsCloudVisualization/MixedRectangles320.png b/cs/DrawingTagsCloudVisualization/MixedRectangles320.png new file mode 100644 index 000000000..f1f7cfde8 Binary files /dev/null and b/cs/DrawingTagsCloudVisualization/MixedRectangles320.png differ diff --git a/cs/DrawingTagsCloudVisualization/README.md b/cs/DrawingTagsCloudVisualization/README.md new file mode 100644 index 000000000..1f0b35364 --- /dev/null +++ b/cs/DrawingTagsCloudVisualization/README.md @@ -0,0 +1,10 @@ +Увеличивающися по размеру прямоугольники +![alt text](DecreasingRectangles120.png) + + +Одинаковые прямоугольники +![alt text](EqualsRectangles250.png) + + +Прямоугольники разных размеров +![alt text](MixedRectangles320.png) \ No newline at end of file diff --git a/cs/Samples/Samples.csproj b/cs/Samples/Samples.csproj index 113a459cd..106124526 100644 --- a/cs/Samples/Samples.csproj +++ b/cs/Samples/Samples.csproj @@ -8,10 +8,10 @@ - - - - + + + + \ No newline at end of file diff --git a/cs/TagsCloudVisualization/ArchimedeanSpiral.cs b/cs/TagsCloudVisualization/ArchimedeanSpiral.cs new file mode 100644 index 000000000..374e8c803 --- /dev/null +++ b/cs/TagsCloudVisualization/ArchimedeanSpiral.cs @@ -0,0 +1,32 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public class ArchimedeanSpiral +{ + private readonly Point startPoint; + private readonly double radiusStep; + private readonly double angleStep; + private double currentAngle; + + public ArchimedeanSpiral(Point startPoint, double radiusStep = 1) + { + if (radiusStep <= 0) throw new ArgumentOutOfRangeException(nameof(radiusStep), "radius step must be positive"); + + this.angleStep = Math.PI / 180; + this.startPoint = startPoint; + this.radiusStep = radiusStep; + this.currentAngle = 0; + } + + public Point GetNextPoint() + { + var radius = radiusStep * currentAngle; + + var x = (int)(startPoint.X + radius * Math.Cos(currentAngle)); + var y = (int)(startPoint.Y + radius * Math.Sin(currentAngle)); + currentAngle += angleStep; + + return new Point(x, y); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/CloudTags.cs b/cs/TagsCloudVisualization/CloudTags.cs new file mode 100644 index 000000000..c64f7378b --- /dev/null +++ b/cs/TagsCloudVisualization/CloudTags.cs @@ -0,0 +1,85 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public class CircularCloudLayouter +{ + private ArchimedeanSpiral spiral; + private Point centerСloud; + + private List rectangles; + + public Point CenterCloud => centerСloud; + + public List GetRectangles => rectangles; + + public CircularCloudLayouter(Point center) + { + this.centerСloud = center; + this.spiral = new ArchimedeanSpiral(center); + this.rectangles = new List(); + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.IsEmpty) + { + throw new ArgumentNullException("rectangle is empty"); + } + if (rectangleSize.Height <= 0 || rectangleSize.Width <= 0) + { + throw new ArgumentOutOfRangeException("side less or equal zero"); + } + Rectangle tempRectangle; + do + { + Point nextPoint = spiral.GetNextPoint(); + tempRectangle = new Rectangle(new Point(nextPoint.X, nextPoint.Y), rectangleSize); + } + while (IsRectangleIntersect(tempRectangle)); + if (rectangles.Count > 1) + tempRectangle = TryToMoveRectangleNearToCenter(tempRectangle); + rectangles.Add(tempRectangle); + return tempRectangle; + } + + private Rectangle TryToMoveRectangleNearToCenter(Rectangle rectangle) + { + while (true) + { + var tempRectangle = rectangle; + if (rectangle.X != centerСloud.X) + { + var directionX = rectangle.X < centerСloud.X ? 1 : -1; + rectangle = MovingRectangleIfPossible(rectangle, true, directionX); + } + if (rectangle.Y != centerСloud.Y) + { + var directionY = rectangle.Y < centerСloud.Y ? 1 : -1; + rectangle = MovingRectangleIfPossible(rectangle, false, directionY); + } + + if (tempRectangle.Equals(rectangle)) + break; + } + return rectangle; + } + + private Rectangle MovingRectangleIfPossible(Rectangle rectangle, bool isX, int direction) + { + var shiftPoint = isX ? new Point(direction, 0) : new Point(0, direction); + var movedRectangle = rectangle with + { + Location = new Point(rectangle.X + shiftPoint.X, rectangle.Y + shiftPoint.Y) + }; + + if (!IsRectangleIntersect(movedRectangle)) + { + rectangle = movedRectangle; + } + return rectangle; + } + + private bool IsRectangleIntersect(Rectangle rectangleChecked) => + rectangles.Any(rectangleChecked.IntersectsWith); +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/TagsCloudVisualization.csproj b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj new file mode 100644 index 000000000..c710d9d32 --- /dev/null +++ b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + false + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/cs/TestsTagsCloudVisualization/TestsSpiral.cs b/cs/TestsTagsCloudVisualization/TestsSpiral.cs new file mode 100644 index 000000000..27f26b23c --- /dev/null +++ b/cs/TestsTagsCloudVisualization/TestsSpiral.cs @@ -0,0 +1,41 @@ +using NUnit.Framework; +using FluentAssertions; +using System.Drawing; +using TagsCloudVisualization; + +namespace TagsCloudVisualizationTests; + +public class TestsSpiral +{ + + ArchimedeanSpiral currentSpiral; + [SetUp] + public void SetUp() + { + currentSpiral = new ArchimedeanSpiral(new Point(0, 0)); + } + + [Test] + public void Spiral_ThrowingWhenRadiusNonPositive() + { + Action action = new Action(() => new ArchimedeanSpiral(new Point(0, 0), -9)); + action.Should().Throw().Which.Message.Should().Contain("radius step must be positive"); + } + + [Test] + public void GetNextPoint_CenterPointSetting() + { + currentSpiral.GetNextPoint().Should().Be(new Point(0, 0)); + } + + [Test] + public void GetNextPoint_SeveralPointSetting() + { + for (int i = 0; i < 180; i++) + { + currentSpiral.GetNextPoint(); + } + currentSpiral.GetNextPoint().Should().Be(new Point((int)-Math.PI, 0)); + } + +} \ No newline at end of file diff --git a/cs/TestsTagsCloudVisualization/TestsTagsCloudVisualization.cs b/cs/TestsTagsCloudVisualization/TestsTagsCloudVisualization.cs new file mode 100644 index 000000000..197374c2d --- /dev/null +++ b/cs/TestsTagsCloudVisualization/TestsTagsCloudVisualization.cs @@ -0,0 +1,142 @@ +using NUnit.Framework; +using FluentAssertions; +using System.Drawing; +using TagsCloudVisualization; +using NUnit.Framework.Interfaces; +using DrawingTagsCloudVisualization; + + +namespace TagsCloudVisualizationTests; + +public class TestsCloudVisualization +{ + private CircularCloudLayouter circularCloudLayouter; + + [SetUp] + public void SetUp() + { + circularCloudLayouter = new CircularCloudLayouter(new Point(0, 0)); + } + + [TearDown] + public void TearDown() + { + var context = TestContext.CurrentContext; + if (context.Result.Outcome == ResultState.Failure) + { + DrawingTagsCloud drawingTagsCloud = new DrawingTagsCloud(new Point(circularCloudLayouter.CenterCloud.X * 2, + circularCloudLayouter.CenterCloud.Y * 2), + circularCloudLayouter.GetRectangles); + var pathToSave = context.Test.MethodName + ".png"; + drawingTagsCloud.SaveToFile(pathToSave); //сохраняется в bin + Console.WriteLine($"Tag cloud visualization saved to file {pathToSave}"); + } + } + + [Test] + public void CircularCloudLayouter_SettingCenter() + { + CircularCloudLayouter circularLayouter = new CircularCloudLayouter(new Point(2, 4)); + circularLayouter.CenterCloud.X.Should().Be(2); + circularLayouter.CenterCloud.Y.Should().Be(4); + } + + [Test] + public void CircularCloudLayouter_StartWithoutExceptions() + { + Action action = new Action(() => new CircularCloudLayouter(new Point(2, 6))); + action.Should().NotThrow(); + } + + [Test] + public void PutNextRectangle_ThrowingWhenLengthsNegative() + { + Action action = new Action(() => circularCloudLayouter.PutNextRectangle(new Size(-1, -1))); + action.Should().Throw().Which.Message.Should().Contain("side less or equal zero"); + } + + [Test] + public void PutNextRectangle_ThrowingWhenRectangleEmpty() + { + Action action = new Action(() => circularCloudLayouter.PutNextRectangle(Size.Empty)); + action.Should().Throw().Which.Message.Should().Contain("rectangle is empty"); + } + + [Test] + public void CircularCloudLayouter_RectanglesEmptyAfterInitialization() + { + circularCloudLayouter.GetRectangles.Should().BeEmpty(); + } + + [Test] + public void PutNextRectangle_PutFirstRectangle() + { + Size rectangleSize = new Size(3, 7); + Rectangle expectedRectangle = new Rectangle(new Point(0, 0), rectangleSize); + circularCloudLayouter.PutNextRectangle(rectangleSize); + + circularCloudLayouter.GetRectangles.Should().ContainSingle(x => x == expectedRectangle); + } + + [Test] + public void PutNextRectangle_PutSeveralRectangles() + { + Size rectangleSize = new Size(3, 7); + for (int i = 0; i < 20; i++) + { + circularCloudLayouter.PutNextRectangle(rectangleSize); + } + circularCloudLayouter.GetRectangles.Should().HaveCount(20); + circularCloudLayouter.GetRectangles.Should().AllBeOfType(typeof(Rectangle)); + } + + [Test] + public void PutNextRectangle_FirstFailedTestForCheckTearDown() + { + for (int i = 0; i < 200; i++) + circularCloudLayouter.PutNextRectangle(new Size(8, 5)); + + for (int i = 0; i < 20; i++) + { + circularCloudLayouter.PutNextRectangle(new Size(3, 7)); + } + circularCloudLayouter.GetRectangles.Should().HaveCount(20); + } + + [Test] + public void PutNextRectangle_SecondFailedTestForCheckTearDown() + { + for (int i = 0; i < 150; i++) + circularCloudLayouter.PutNextRectangle(new Size(2, 4)); + + for (int i = 0; i < 200; i++) + { + circularCloudLayouter.PutNextRectangle(new Size(2, 2)); + } + circularCloudLayouter.GetRectangles.Should().HaveCount(20); + } + + [Test] + public void PutNextRectangle_SeveralRectanglesDontIntersect() + { + var rectanglesSizes = new List + { + new Size(10, 5), + new Size(8, 8), + new Size(12, 3), + new Size(6, 10) + }; + + foreach (var size in rectanglesSizes) + { + circularCloudLayouter.PutNextRectangle(size); + } + + List rectanglesTemp = circularCloudLayouter.GetRectangles; + for (int i = 0; i < rectanglesTemp.Count; i++) + { + for (int j = i + 1; j < rectanglesTemp.Count; j++) + rectanglesTemp[i].IntersectsWith(rectanglesTemp[j]).Should().BeFalse(); + } + } +} \ No newline at end of file diff --git a/cs/TestsTagsCloudVisualization/TestsTagsCloudVisualization.csproj b/cs/TestsTagsCloudVisualization/TestsTagsCloudVisualization.csproj new file mode 100644 index 000000000..21cc5750c --- /dev/null +++ b/cs/TestsTagsCloudVisualization/TestsTagsCloudVisualization.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + diff --git a/cs/tdd.sln b/cs/tdd.sln index c8f523d63..01a05c475 100644 --- a/cs/tdd.sln +++ b/cs/tdd.sln @@ -7,6 +7,12 @@ 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", "{8CBDE389-D9A9-454C-AD5E-7D04BBE79D71}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestsTagsCloudVisualization", "TestsTagsCloudVisualization\TestsTagsCloudVisualization.csproj", "{0311AD96-616E-4971-984B-22680BD6DEAB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawingTagsCloudVisualization", "DrawingTagsCloudVisualization\DrawingTagsCloudVisualization.csproj", "{75025923-D44F-4B58-AA8B-B603727B2149}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +27,18 @@ 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 + {8CBDE389-D9A9-454C-AD5E-7D04BBE79D71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CBDE389-D9A9-454C-AD5E-7D04BBE79D71}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CBDE389-D9A9-454C-AD5E-7D04BBE79D71}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CBDE389-D9A9-454C-AD5E-7D04BBE79D71}.Release|Any CPU.Build.0 = Release|Any CPU + {0311AD96-616E-4971-984B-22680BD6DEAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0311AD96-616E-4971-984B-22680BD6DEAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0311AD96-616E-4971-984B-22680BD6DEAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0311AD96-616E-4971-984B-22680BD6DEAB}.Release|Any CPU.Build.0 = Release|Any CPU + {75025923-D44F-4B58-AA8B-B603727B2149}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75025923-D44F-4B58-AA8B-B603727B2149}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75025923-D44F-4B58-AA8B-B603727B2149}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75025923-D44F-4B58-AA8B-B603727B2149}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE