diff --git a/1.bmp b/1.bmp new file mode 100644 index 000000000..3c9626826 Binary files /dev/null and b/1.bmp differ diff --git a/2.bmp b/2.bmp new file mode 100644 index 000000000..b22179568 Binary files /dev/null and b/2.bmp differ diff --git a/3.bmp b/3.bmp new file mode 100644 index 000000000..5f92188e7 Binary files /dev/null and b/3.bmp differ diff --git a/TagsCloudVisualization.Tests/CircularCloudLayouter_Should.cs b/TagsCloudVisualization.Tests/CircularCloudLayouter_Should.cs new file mode 100644 index 000000000..c7e6ad24a --- /dev/null +++ b/TagsCloudVisualization.Tests/CircularCloudLayouter_Should.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using FluentAssertions; +using System.Reflection; +using System.Drawing; +using TagsCloudVisualization.Tests; +using System.IO; +using System.Runtime.InteropServices; +using FluentAssertions.Execution; +using NUnit.Framework.Interfaces; + + +namespace TagsCloudVisualization +{ + [TestFixture] + public class CircularCloudLayouter_Should + { + private CircularCloudLayouter layout; + [TestCase(1, 2, TestName = "Odd coordinate value results in an even size value")] + [TestCase(2, 5, TestName = "Even coordinate value results in an odd size value")] + public void CircularCloudLayouter_MakeRightSizeLayout(int coordinateValue, int sizeValue) + { + var center = new Point(coordinateValue, coordinateValue); + var size = new Size(sizeValue, sizeValue); + + layout = new CircularCloudLayouter(center, new ArchemedianSpiral()); + + layout.Size.Should().BeEquivalentTo(size); + } + + [TestCase(-1, 1, TestName = "Negative X")] + [TestCase(1, -1, TestName = "Negative Y")] + [TestCase(0, 1, TestName = "Zero X")] + [TestCase(1, 0, TestName = "Zero Y")] + public void CircularCloudLayouter_GetsOnlyPositiveCenterCoordinates(int x, int y) + { + Action makeLayout = () => new CircularCloudLayouter(new Point(x, y), new ArchemedianSpiral()); + + makeLayout.Should().Throw() + .WithMessage("Center coordinates values have to be greater than Zero"); + } + + [Test] + public void PutNextRectangle_ShouldKeepEnteredSize() + { + layout = new CircularCloudLayouter(new Point(5, 5), new ArchemedianSpiral()); + var enteredSize = new Size(3, 4); + var returnedSize = layout.PutNextRectangle(enteredSize).Size; + + returnedSize.Should().BeEquivalentTo(enteredSize); + } + + [Test] + public void PutNextRectangle_HasNotEnoughPoints() + { + + layout = new CircularCloudLayouter(new Point(100, 100), new TestPointGenerator()); + var rectangleSizes = new RandomSizeRectangle().GenerateRectangles(5); + + var makeRectangles = () => { + foreach (var size in rectangleSizes) + layout.PutNextRectangle(size); + }; + + makeRectangles.Should().Throw() + .WithMessage("Not Enough Points Generated"); + } + + [TearDown] + public void TearDown() + { + if (TestContext.CurrentContext.Result.Outcome != ResultState.Failure) + return; + + var image = new Bitmap(layout.Size.Width, layout.Size.Height); + foreach (var rectangle in layout.Rectangles) + Drawings.DrawRectangle(image, rectangle); + + var fileName = string.Format("{0}.bmp", TestContext.CurrentContext.Test.Name); + var path = TestContext.CurrentContext.WorkDirectory; + var fullPath = Path.Combine(path, fileName); + var message = string.Format("Tag cloud visualization saved to file {0}", fullPath); + + image.Save(fileName); + Console.WriteLine(message); + } + } +} diff --git a/TagsCloudVisualization.Tests/IPointGenerator_Should.cs b/TagsCloudVisualization.Tests/IPointGenerator_Should.cs new file mode 100644 index 000000000..55e6c6c58 --- /dev/null +++ b/TagsCloudVisualization.Tests/IPointGenerator_Should.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using FluentAssertions; +using FluentAssertions.Execution; + +namespace TagsCloudVisualization +{ + [TestFixture] + public class IPointGenerator_Should + { + [TestCaseSource(nameof(TestCases))] + public void GeneratePoints_MovingAwayFromTheStartFor(IPointGenerator pointGenerator) + { + var start = new Point(0, 0); + var points = pointGenerator.GeneratePoints(start); + var nearPoint = points.ElementAt(100); + var farPoint = points.ElementAt(1000); + + DistanceBetween(start, nearPoint).Should().BeLessThan(DistanceBetween(start, farPoint)); + } + + [TestCaseSource(nameof(TestCases))] + public void GeneratePoints_ReturnsStartAsFirstPointFor(IPointGenerator pointGenerator) + { + var start = new Point(100, 100); + var firstReturned = pointGenerator.GeneratePoints(start) + .First(); + + firstReturned.Should().BeEquivalentTo(start); + } + + public static IEnumerable TestCases() + { + yield return new ArchemedianSpiral(); + yield return new HeartShaped(); + yield return new DeltaSHaped(); + } + + public int DistanceBetween(Point start, Point destination) + { + return (int)(Math.Sqrt((start.X - destination.X) * (start.X - destination.X) + + (start.Y - destination.Y) * (start.Y - destination.Y))); + } + } +} diff --git a/TagsCloudVisualization.Tests/TagsCloudVisualization.Tests.csproj b/TagsCloudVisualization.Tests/TagsCloudVisualization.Tests.csproj new file mode 100644 index 000000000..4203c996a --- /dev/null +++ b/TagsCloudVisualization.Tests/TagsCloudVisualization.Tests.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + diff --git a/TagsCloudVisualization.Tests/TestPointGenerator.cs b/TagsCloudVisualization.Tests/TestPointGenerator.cs new file mode 100644 index 000000000..1507fde75 --- /dev/null +++ b/TagsCloudVisualization.Tests/TestPointGenerator.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization.Tests +{ + internal class TestPointGenerator : IPointGenerator + { + public IEnumerable GeneratePoints(Point start) + { + var points = new[] { new Point(0, 0), new Point(1, 1), new Point(2, 2) }; + foreach (var point in points) + yield return point; + } + } +} diff --git a/TagsCloudVisualization/ArchemedianSpiral.cs b/TagsCloudVisualization/ArchemedianSpiral.cs new file mode 100644 index 000000000..635b6c5c7 --- /dev/null +++ b/TagsCloudVisualization/ArchemedianSpiral.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization +{ + public class ArchemedianSpiral : IPointGenerator + { + public IEnumerable GeneratePoints(Point start) + { + var zoom = 1; + var spiralStep = 0.0; + yield return start; + while (true) + { + spiralStep += Math.PI / 180; + var x = start.X + (int)(zoom * spiralStep * Math.Cos(spiralStep)); + var y = start.Y + (int)(zoom * spiralStep * Math.Sin(spiralStep)); + var next = new Point(x, y); + yield return next; + } + } + } +} diff --git a/TagsCloudVisualization/CircularCloudLayouter.cs b/TagsCloudVisualization/CircularCloudLayouter.cs new file mode 100644 index 000000000..cd60e2ee5 --- /dev/null +++ b/TagsCloudVisualization/CircularCloudLayouter.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization +{ + public class CircularCloudLayouter + { + public readonly Point Center; + public readonly Size Size; + private readonly IEnumerable _points; + public List Rectangles { get; set; } + + + public CircularCloudLayouter(Point center, IPointGenerator pointGenerator) + { + if (center.X <=0 || center.Y <=0) + throw new ArgumentException("Center coordinates values have to be greater than Zero"); + Center = center; + Size = CountSize(center); + Rectangles = []; + _points = pointGenerator.GeneratePoints(Center); + } + + + private Size CountSize(Point center) + { + var width = (center.X % 2 == 0) ? center.X * 2 + 1 : Center.X * 2; + var height = (center.Y % 2 == 0) ? center.Y * 2 + 1 : center.Y * 2; + return new Size(width, height); + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + foreach (var point in _points) + { + var supposed = new Rectangle(new Point(point.X - rectangleSize.Width / 2, point.Y - rectangleSize.Height / 2), + rectangleSize); + if (IntersectsWithAnyOther(supposed, Rectangles)) + continue; + Rectangles.Add(supposed); + return supposed; + } + throw new ArgumentException("Not Enough Points Generated"); + } + + public static bool IntersectsWithAnyOther(Rectangle supposed, List others) + { + return others.Any(x => x.IntersectsWith(supposed)); + } + } +} diff --git a/TagsCloudVisualization/DeltaShaped.cs b/TagsCloudVisualization/DeltaShaped.cs new file mode 100644 index 000000000..e79242c7d --- /dev/null +++ b/TagsCloudVisualization/DeltaShaped.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization +{ + public class DeltaSHaped : IPointGenerator + { + public IEnumerable GeneratePoints(Point start) + { + var zoom = 5; + yield return start; + while (true) + { + foreach (var pair in Heart()) + { + var x = start.X + (int)(zoom * pair.Item1); + var y = start.Y + (int)(zoom * pair.Item2); + var next = new Point(x, y); + yield return next; + } + zoom += 2; + } + } + + public IEnumerable<(double, double)> Heart() + { + for (var t = 0.0; t < 2 * Math.PI; t += Math.PI / 180) + { + var x = 2 * Math.Cos(t) + Math.Cos(2*t); + var y = 2 * Math.Sin(t) - Math.Sin(2 * t); + yield return (x, y); + } + } + } +} diff --git a/TagsCloudVisualization/Drawings.cs b/TagsCloudVisualization/Drawings.cs new file mode 100644 index 000000000..59a82a463 --- /dev/null +++ b/TagsCloudVisualization/Drawings.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization +{ + public class Drawings + { + public static void DrawPicture(IRectangleGenerator rectangleGenerator, int quantity, + IPointGenerator pointGenerator, Point startPoint, string filename) + { + var rectangleSizes = rectangleGenerator. + GenerateRectangles(quantity).ToArray(); + var layout = new CircularCloudLayouter(startPoint, pointGenerator); + var image = new Bitmap(layout.Size.Width, layout.Size.Height); + foreach (var size in rectangleSizes) + { + DrawRectangle(image, layout.PutNextRectangle(size)); + } + image.Save(filename); + } + + public static void DrawRectangle(Bitmap image, Rectangle rectangle) + { + var brush = new SolidBrush(GetRandomColor()); + var formGraphics = Graphics.FromImage(image); + formGraphics.FillRectangle(brush, rectangle); + brush.Dispose(); + formGraphics.Dispose(); + } + + private static Color GetRandomColor() + { + var random = new Random(); + return Color.FromArgb(random.Next(0, 255), random.Next(0, 255), random.Next(0, 255), random.Next(0, 255)); + } + } +} diff --git a/TagsCloudVisualization/HeartShaped.cs b/TagsCloudVisualization/HeartShaped.cs new file mode 100644 index 000000000..994dbeeaa --- /dev/null +++ b/TagsCloudVisualization/HeartShaped.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization +{ + public class HeartShaped : IPointGenerator + { + public IEnumerable GeneratePoints(Point start) + { + var zoom = 1; + yield return start; + while (true) + { + foreach (var pair in Heart()) + { + var x = start.X + (int)(zoom * pair.Item1); + var y = start.Y + (int)(zoom * pair.Item2); + var next = new Point(x, y); + yield return next; + } + zoom += 1; + } + } + + public IEnumerable<(double, double)> Heart() + { + for (var t = 0.0; t < 2 * Math.PI; t += Math.PI / 180) + { + var x = 16 * Math.Sin(t) * Math.Sin(t) * Math.Sin(t); + var y = -13 * Math.Cos(t) + 5 * Math.Cos(2 * t) + 2 * Math.Cos(3 * t) + Math.Cos(4 * t); + yield return (x, y); + } + } + } +} diff --git a/TagsCloudVisualization/IPointGenerator.cs b/TagsCloudVisualization/IPointGenerator.cs new file mode 100644 index 000000000..86c7570c1 --- /dev/null +++ b/TagsCloudVisualization/IPointGenerator.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization +{ + public interface IPointGenerator + { + IEnumerable GeneratePoints(Point start); + } +} diff --git a/TagsCloudVisualization/IRectangleGenerator.cs b/TagsCloudVisualization/IRectangleGenerator.cs new file mode 100644 index 000000000..16de52112 --- /dev/null +++ b/TagsCloudVisualization/IRectangleGenerator.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization +{ + public interface IRectangleGenerator + { + IEnumerable GenerateRectangles(int quantity); + } +} diff --git a/TagsCloudVisualization/Program.cs b/TagsCloudVisualization/Program.cs new file mode 100644 index 000000000..3d4bcb20b --- /dev/null +++ b/TagsCloudVisualization/Program.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + + +namespace TagsCloudVisualization +{ + internal class Program + { + public static void Main() + { + Drawings.DrawPicture(new RandomSizeRectangle(), 300, new ArchemedianSpiral(), new Point(1000, 1000), "spiral.bmp"); + Drawings.DrawPicture(new RandomSizeRectangle(), 300, new HeartShaped(), new Point(1000, 1000), "heart.bmp"); + Drawings.DrawPicture(new RandomSizeRectangle(), 300, new DeltaSHaped(), new Point(1000, 1000), "delta.bmp"); + } + } +} diff --git a/TagsCloudVisualization/RandomSizeRectangle.cs b/TagsCloudVisualization/RandomSizeRectangle.cs new file mode 100644 index 000000000..c18e75650 --- /dev/null +++ b/TagsCloudVisualization/RandomSizeRectangle.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization +{ + public class RandomSizeRectangle : IRectangleGenerator + { + public IEnumerable GenerateRectangles(int quantity) + { + var rnd = new Random(); + for (var i = 0; i < quantity; i++) + yield return new Size(rnd.Next(5, 30), rnd.Next(5, 30)); + } + } +} diff --git a/TagsCloudVisualization/TagsCloudVisualization.csproj b/TagsCloudVisualization/TagsCloudVisualization.csproj new file mode 100644 index 000000000..e4869d68a --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + TagsCloudVisualization.Program + + + + + + + + + + + diff --git a/TagsCloudVisualization/TagsCloudVisualization.sln b/TagsCloudVisualization/TagsCloudVisualization.sln new file mode 100644 index 000000000..443e63485 --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35327.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagsCloudVisualization", "TagsCloudVisualization.csproj", "{50CAB9A6-5557-483A-9D3F-5112AF5EBC25}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudVisualization.Tests", "..\TagsCloudVisualization.Tests\TagsCloudVisualization.Tests.csproj", "{C546C823-59B5-44B3-9306-FFD3ECAD5C9E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {50CAB9A6-5557-483A-9D3F-5112AF5EBC25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50CAB9A6-5557-483A-9D3F-5112AF5EBC25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50CAB9A6-5557-483A-9D3F-5112AF5EBC25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50CAB9A6-5557-483A-9D3F-5112AF5EBC25}.Release|Any CPU.Build.0 = Release|Any CPU + {C546C823-59B5-44B3-9306-FFD3ECAD5C9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C546C823-59B5-44B3-9306-FFD3ECAD5C9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C546C823-59B5-44B3-9306-FFD3ECAD5C9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C546C823-59B5-44B3-9306-FFD3ECAD5C9E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {12858792-A448-4387-BC16-634AECA81D66} + EndGlobalSection +EndGlobal