diff --git a/cs/TagsCloudVisualization/CircularCloudLayouter.cs b/cs/TagsCloudVisualization/CircularCloudLayouter.cs new file mode 100644 index 000000000..0d48696cd --- /dev/null +++ b/cs/TagsCloudVisualization/CircularCloudLayouter.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace TagsCloudVisualization +{ + public class CircularCloudLayouter + { + private readonly Point center; + public ICollection Cloud { get; private set; } + private IPointsProvider pointsProvider; + + public CircularCloudLayouter(Point center) + { + if (center.X <= 0 || center.Y <= 0) + throw new ArgumentException("Central point coordinates should be in positive"); + + this.center = center; + + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0) + throw new ArgumentException("Size width and height should be positive"); + + if (Cloud == null || !Cloud.Any()) + return new Rectangle(center, rectangleSize); + + Rectangle rectangle; + bool placingIsCorrect; + + var enumerator = pointsProvider.Points().GetEnumerator(); + enumerator.MoveNext(); + + do + { + var point = enumerator.Current; + enumerator.MoveNext(); + + rectangle = new Rectangle(new Point(point.X - rectangleSize.Width / 2, + point.Y - rectangleSize.Height / 2), + rectangleSize); + placingIsCorrect = PlacedCorrectly(rectangle, Cloud, new Size(center.X * 2, center.Y * 2)); + + } while (!placingIsCorrect); + + return rectangle; + } + + public void LayoutRectancles(List rectangleSizes) + { + Cloud = new List(); + pointsProvider = new SpiralPointsProvider(center); + + foreach (var size in rectangleSizes) + Cloud.Add(PutNextRectangle(size)); + } + + public Image ToImage() + { + var image = new Bitmap(center.X * 2, center.Y * 2); + var gr = Graphics.FromImage(image); + var pen = new Pen(Color.White); + + gr.Clear(Color.Black); + gr.DrawRectangles(pen, Cloud.ToArray()); + + return image; + } + + public bool PlacedCorrectly(Rectangle rectangle, ICollection rectanglesCloud, Size canvasSize) + { + if (rectangle.Top < 0 || rectangle.Left < 0 || rectangle.Bottom > canvasSize.Height || + rectangle.Right > canvasSize.Width) + return false; + + foreach (var previous in Cloud) + { + if (rectangle.IntersectsWith(previous)) + return false; + } + + return true; + } + } +} diff --git a/cs/TagsCloudVisualization/Examples/LandscapeCloud200Rectangles.png b/cs/TagsCloudVisualization/Examples/LandscapeCloud200Rectangles.png new file mode 100644 index 000000000..bc022b521 Binary files /dev/null and b/cs/TagsCloudVisualization/Examples/LandscapeCloud200Rectangles.png differ diff --git a/cs/TagsCloudVisualization/Examples/PortraitCloud200Rectangles.png b/cs/TagsCloudVisualization/Examples/PortraitCloud200Rectangles.png new file mode 100644 index 000000000..baf1f9522 Binary files /dev/null and b/cs/TagsCloudVisualization/Examples/PortraitCloud200Rectangles.png differ diff --git a/cs/TagsCloudVisualization/Examples/SquareCloud100Rectangles.png b/cs/TagsCloudVisualization/Examples/SquareCloud100Rectangles.png new file mode 100644 index 000000000..b32f86442 Binary files /dev/null and b/cs/TagsCloudVisualization/Examples/SquareCloud100Rectangles.png differ diff --git a/cs/TagsCloudVisualization/IPointsProvider.cs b/cs/TagsCloudVisualization/IPointsProvider.cs new file mode 100644 index 000000000..851671efc --- /dev/null +++ b/cs/TagsCloudVisualization/IPointsProvider.cs @@ -0,0 +1,11 @@ +using System; +using System.Drawing; + +namespace TagsCloudVisualization +{ + internal interface IPointsProvider + { + public IEnumerable Points(); + public void Reset(); + } +} diff --git a/cs/TagsCloudVisualization/Program.cs b/cs/TagsCloudVisualization/Program.cs new file mode 100644 index 000000000..902a00ab1 --- /dev/null +++ b/cs/TagsCloudVisualization/Program.cs @@ -0,0 +1,32 @@ +using System; +using FluentAssertions; +using NUnit.Framework; +using System.Drawing; +using TagsCloudVisualization; + +namespace TagCloudVisualisation +{ + class Program + { + public static void Main() + { + var layouter = new CircularCloudLayouter(new Point(300, 300)); + + var sizes = new List(); + var rnd = new Random(); + + for (int i = 0; i < 200; i++) + { + sizes.Add(new Size(rnd.Next(5, 50), rnd.Next(5, 50))); + } + + layouter.LayoutRectancles(sizes); + + var image = layouter.ToImage(); + + image.Save("cloud.png"); + Console.WriteLine("Image saved to file cloud.png"); + } + } +} + diff --git a/cs/TagsCloudVisualization/README.md b/cs/TagsCloudVisualization/README.md new file mode 100644 index 000000000..0fc10a25c --- /dev/null +++ b/cs/TagsCloudVisualization/README.md @@ -0,0 +1,5 @@ +# Примеры + +![](Examples/LandscapeCloud200Rectangles.png) +![](Examples/PortraitCloud200Rectangles.png) +![](Examples/SquareCloud100Rectangles.png) diff --git a/cs/TagsCloudVisualization/SpiralPointsProvider.cs b/cs/TagsCloudVisualization/SpiralPointsProvider.cs new file mode 100644 index 000000000..0ca6a7c5e --- /dev/null +++ b/cs/TagsCloudVisualization/SpiralPointsProvider.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace TagsCloudVisualization +{ + public class SpiralPointsProvider : IPointsProvider + { + private int pointNumber = 0; + private readonly float angleStep = (float)Math.PI / 21; + private readonly float SpiralRadius = 50; + private readonly Point Center; + + public SpiralPointsProvider(Point center) + { + Center = center; + } + + public IEnumerable Points() + { + while (pointNumber < 10000000) // Limit number of returned points for safety reason + { + var r = Math.Sqrt(SpiralRadius * pointNumber); + var angle = angleStep * pointNumber; + var x = r * Math.Cos(angle) + Center.X; + var y = r * Math.Sin(angle) + Center.Y; + pointNumber++; + yield return new Point((int)x, (int)y); + } + throw new ArgumentException("Reach end of placing points"); + } + + public void Reset() + { + pointNumber = 0; + } + } +} diff --git a/cs/TagsCloudVisualization/TagCloudVisualisationTest.cs b/cs/TagsCloudVisualization/TagCloudVisualisationTest.cs new file mode 100644 index 000000000..bba6622ae --- /dev/null +++ b/cs/TagsCloudVisualization/TagCloudVisualisationTest.cs @@ -0,0 +1,112 @@ +using System; +using NUnit.Framework; +using FluentAssertions; +using System.Drawing; +using NUnit.Framework.Interfaces; +using System.Security.Claims; + +namespace TagsCloudVisualization +{ + [TestFixture] + public class TagCloudVisualisationTest + { + private CircularCloudLayouter layouter; + + [TestCase(-1, -1)] + [TestCase(1, -1)] + [TestCase(-1, 1)] + [TestCase(0, 0)] + public void CircularCloudLayouterConstructor_ThrowExceptionOnIncorrectCentralPoins(int x, int y) + { + Action a = () => new CircularCloudLayouter(new Point(x, y)); + a.Should().Throw(); + } + + [TestCase(1)] + [TestCase(123)] + public void LayoutRectangles_ReturnCorrectNumberOfRectangles(int rectCount) + { + layouter = new CircularCloudLayouter(new Point(500, 500)); + + var sizes = new List(); + + for (int i = 0; i < rectCount; i++) + { + sizes.Add(new Size(1, 1)); + } + + layouter.LayoutRectancles(sizes); + + layouter.Cloud.Count().Should().Be(rectCount); + } + + [Test] + public void PutNextRectangle_ShouldReturnRectangle() + { + layouter = new CircularCloudLayouter(new Point(50, 50)); + layouter.PutNextRectangle(new Size(1, 2)).Should().BeOfType(typeof(Rectangle)); + } + + [TestCase(1, 1)] + [TestCase(20, 1)] + [TestCase(256, 255)] + [TestCase(1, 20)] + public void PutNextRectangle_ShouldReturnRectangleOfCorrectSize(int width, int height) + { + layouter = new CircularCloudLayouter(new Point(500, 500)); + var rect = layouter.PutNextRectangle(new Size(width, height)); + rect.Width.Should().Be(width); + rect.Height.Should().Be(height); + } + + [TestCase(-1, 1)] + [TestCase(1, -1)] + [TestCase(0, 1)] + [TestCase(1, 0)] + public void PutNextRectangle_ShouldThrowExceptionOnIncorrectSize(int width, int height) + { + layouter = new CircularCloudLayouter(new Point(50, 50)); + Action a = () => layouter.PutNextRectangle(new Size(width, height)); + a.Should().Throw(); + } + + [TestCase(10)] + [TestCase(20)] + [TestCase(200)] + public void LayoutRectangles_RectanglesShouldNotIntersect(int rectCount) + { + layouter = new CircularCloudLayouter(new Point(500, 500)); + var sizes = new List(); + var rnd = new Random(); + + for (int i = 0; i < rectCount; i++) + { + sizes.Add(new Size(rnd.Next(1, 50), rnd.Next(1, 50))); + } + + layouter.LayoutRectancles(sizes); + + foreach (var rectangleA in layouter.Cloud) + { + foreach (var rectangleB in layouter.Cloud) + { + if (rectangleA == rectangleB) continue; + var isIntersect = rectangleA.IntersectsWith(rectangleB); + + isIntersect.Should().BeFalse(); + } + } + } + + [TearDown] + public void SaveImageOnTestFails() + { + if (TestContext.CurrentContext.Result.Outcome == ResultState.Failure) + { + var img = layouter.ToImage(); + string filename = TestContext.CurrentContext.Test.Name + "_Failed_" + DateTime.Now.ToString("H - mm - ss") + ".png"; + img.Save(filename); + } + } + } +} diff --git a/cs/TagsCloudVisualization/TagsCloudVisualization.csproj b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj new file mode 100644 index 000000000..11493b3da --- /dev/null +++ b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + false + + + + + + + + + + + diff --git a/cs/TagsCloudVisualization/TagsCloudVisualization.sln b/cs/TagsCloudVisualization/TagsCloudVisualization.sln new file mode 100644 index 000000000..5bec6eddf --- /dev/null +++ b/cs/TagsCloudVisualization/TagsCloudVisualization.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34202.233 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudVisualization", "TagsCloudVisualization.csproj", "{85DD6D7B-7811-4117-8B54-A4ECC712D5C8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {85DD6D7B-7811-4117-8B54-A4ECC712D5C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85DD6D7B-7811-4117-8B54-A4ECC712D5C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85DD6D7B-7811-4117-8B54-A4ECC712D5C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85DD6D7B-7811-4117-8B54-A4ECC712D5C8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5BEAA608-2B32-428E-815C-F8F19E0DF0B8} + EndGlobalSection +EndGlobal