diff --git a/cs/TagsCloudVisualization/BruteForceNearestFinder.cs b/cs/TagsCloudVisualization/BruteForceNearestFinder.cs new file mode 100644 index 000000000..c8f3f7053 --- /dev/null +++ b/cs/TagsCloudVisualization/BruteForceNearestFinder.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace TagsCloudVisualization +{ + public class BruteForceNearestFinder + { + public Rectangle? FindNearestByDirection(Rectangle r, Direction direction, List rectangles) + { + if (rectangles.FirstOrDefault() == default) + return null; + var calculator = GetMinDistanceCalculatorBy(direction); + var nearestByDirection = rectangles + .Select(possibleNearest => (Distance: calculator(possibleNearest, r), CurrentEl: possibleNearest)) + .Where(el => el.Distance >= 0).ToList(); + + return nearestByDirection.Count > 0 ? nearestByDirection.MinBy(el => el.Distance).CurrentEl : null; + } + + public Func GetMinDistanceCalculatorBy(Direction direction) + { + return direction switch + { + Direction.Left => (possibleNearest, rectangleForFind) => rectangleForFind.Left - possibleNearest.Right, + Direction.Right => (possibleNearest, rectangleForFind) => possibleNearest.Left - rectangleForFind.Right, + Direction.Top => (possibleNearest, rectangleForFind) => rectangleForFind.Top - possibleNearest.Bottom, + _ => (possibleNearest, rectangleForFind) => possibleNearest.Top - rectangleForFind.Bottom, + }; + } + } +} diff --git a/cs/TagsCloudVisualization/CircleLayer.cs b/cs/TagsCloudVisualization/CircleLayer.cs new file mode 100644 index 000000000..edb6a94ef --- /dev/null +++ b/cs/TagsCloudVisualization/CircleLayer.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace TagsCloudVisualization; + +public class CircleLayer +{ + public enum Sector + { + TopRight, + BottomRight, + BottomLeft, + TopLeft + } + + public Point Center { get; } + public int Radius { get; private set; } + + + private Sector currentSector; + private readonly List storage; + private readonly List layerRectangles = []; + + private CircleLayer(Point center, int radius, List storage) + { + Center = center; + Radius = radius; + currentSector = Sector.TopRight; + this.storage = storage; + } + + public CircleLayer(Point center, List storage) : this(center, 0, storage) + { } + + public void OnSuccessInsertRectangle() + { + if (storage.Count == 0) throw new InvalidOperationException("Rectangle was not added"); + if (storage.Count != 1) currentSector = GetNextClockwiseSector(); + layerRectangles.Add(storage.Count - 1); + if (ShouldCreateNewLayer()) + CreateNextLayerAndChangeCurrentOnNext(); + } + + private bool ShouldCreateNewLayer() + { + return currentSector == Sector.TopRight; + } + + private Sector GetNextClockwiseSector() + { + return currentSector == Sector.TopLeft ? Sector.TopRight : currentSector + 1; + } + + private void CreateNextLayerAndChangeCurrentOnNext() + { + var nextLayer = new CircleLayer(Center, CalculateRadiusForNextLayer(), storage); + Radius = nextLayer.Radius; + currentSector = nextLayer.currentSector; + var rectanglesForNextRadius = RemoveRectangleInCircle(); + layerRectangles.Clear(); + layerRectangles.AddRange(rectanglesForNextRadius); + } + + private int CalculateRadiusForNextLayer() + { + return layerRectangles + .Select(ind => CalculateDistanceBetweenCenterAndRectangleFarCorner(storage[ind])) + .Min(); + } + + private List RemoveRectangleInCircle() + { + return layerRectangles + .Where(i => CalculateDistanceBetweenCenterAndRectangleFarCorner(storage[i]) > Radius) + .ToList(); + } + + private int CalculateDistanceBetweenCenterAndRectangleFarCorner(Rectangle rectangle) + { + var distanceToCorners = new List + { + Center.CalculateDistanceBetween(new Point(rectangle.Right, rectangle.Top)), + Center.CalculateDistanceBetween(new Point(rectangle.Right, rectangle.Bottom)), + Center.CalculateDistanceBetween(new Point(rectangle.Left, rectangle.Bottom)), + Center.CalculateDistanceBetween(new Point(rectangle.Left, rectangle.Top)) + }; + return distanceToCorners.Max(); + } + + private Point PutToCenter(Size rectangleSize) + { + var rectangleX = Center.X - rectangleSize.Width / 2; + var rectangleY = Center.Y - rectangleSize.Height / 2; + + return new Point(rectangleX, rectangleY); + } + + public Point AddNextRectangle(Size rectangleSize) + { + if (Radius == 0) return PutToCenter(rectangleSize); + var rectangleStartPositionOnCircle = GetStartSectorPointOnCircleBySector(currentSector); + return currentSector switch + { + Sector.TopRight => new Point(rectangleStartPositionOnCircle.X, + rectangleStartPositionOnCircle.Y -= rectangleSize.Height), + Sector.BottomRight => + rectangleStartPositionOnCircle, + Sector.BottomLeft => + new Point(rectangleStartPositionOnCircle.X - rectangleSize.Width, + rectangleStartPositionOnCircle.Y), + _ => + new Point(rectangleStartPositionOnCircle.X - rectangleSize.Width, + rectangleStartPositionOnCircle.Y - rectangleSize.Height) + }; + } + + private Point GetStartSectorPointOnCircleBySector(Sector s) + { + return s switch + { + Sector.TopRight => new Point(Center.X, Center.Y - Radius), + Sector.BottomRight => new Point(Center.X + Radius, Center.Y), + Sector.BottomLeft => new Point(Center.X, Center.Y + Radius), + _ => new Point(Center.X - Radius, Center.Y) + }; + } + + public Point GetRectanglePositionWithoutIntersection(Rectangle forInsertion, Rectangle intersected) + { + if (IsRectangleIntersectSymmetryAxis(new Rectangle(forInsertion.Location, forInsertion.Size))) + { + currentSector = GetNextClockwiseSector(); + if (ShouldCreateNewLayer()) CreateNextLayerAndChangeCurrentOnNext(); + forInsertion.Location = AddNextRectangle(forInsertion.Size); + } + + var nextPosition = CalculateNewPositionWithoutIntersectionBySector(currentSector, forInsertion, intersected); + + return nextPosition; + } + + private bool IsRectangleIntersectSymmetryAxis(Rectangle rectangle) + { + return (rectangle.Left < Center.X && rectangle.Right > Center.X) || + (rectangle.Bottom > Center.Y && rectangle.Top < Center.Y); + } + + private Point CalculateNewPositionWithoutIntersectionBySector(Sector whereIntersected, Rectangle forInsertion, + Rectangle intersected) + { + var isMovingAxisIsX = IsMovingAxisIsXBySector(whereIntersected); + var distanceForMoving = + CalculateDistanceForMoveClockwiseToPositionWithoutIntersection(whereIntersected, forInsertion, intersected); + + int distanceForBringBackOnCircle; + if (IsRectangleBetweenSectors(distanceForMoving, forInsertion.Location, isMovingAxisIsX)) + { + distanceForBringBackOnCircle = Radius; + } + else + { + var nearestForCenterCorner = + CalculateCornerNearestForCenterAfterMove(whereIntersected, distanceForMoving, forInsertion); + distanceForBringBackOnCircle = + CalculateDeltaForBringRectangleBackOnCircle(nearestForCenterCorner, isMovingAxisIsX, forInsertion); + } + + distanceForMoving *= CalculateMoveMultiplierForMoveClockwise(isMovingAxisIsX, forInsertion); + distanceForBringBackOnCircle *= CalculateMoveMultiplierForMoveFromCenter(!isMovingAxisIsX, forInsertion); + return isMovingAxisIsX + ? new Point(forInsertion.X + distanceForMoving, forInsertion.Y + distanceForBringBackOnCircle) + : new Point(forInsertion.X + distanceForBringBackOnCircle, forInsertion.Y + distanceForMoving); + } + + private bool IsRectangleBetweenSectors(int distanceForMoving, Point forInsertionLocation, bool isMovingAxisIsX) + { + var distanceToCenter = Math.Abs(isMovingAxisIsX + ? forInsertionLocation.X - Center.X + : forInsertionLocation.Y - Center.Y); + return distanceForMoving > distanceToCenter; + } + + private int CalculateDeltaForBringRectangleBackOnCircle(Point nearestForCenterCorner, bool isMovingAxisIsX, + Rectangle forInsertion) + { + Func getAxisForBringBackOnCircle = isMovingAxisIsX ? p => p.Y : p => p.X; + Func getStaticAxis = isMovingAxisIsX ? p => p.X : p => p.Y; + + var distanceOnStaticAxis = Math.Abs(getStaticAxis(nearestForCenterCorner) - getStaticAxis(Center)); + var distanceOnAxisForBringBackOnCircle = Math.Abs(getAxisForBringBackOnCircle(nearestForCenterCorner) - + getAxisForBringBackOnCircle(Center)); + var distanceBetweenCornerAndCenter = Center.CalculateDistanceBetween(nearestForCenterCorner); + if (distanceBetweenCornerAndCenter > Radius) + return CalculateMoveMultiplierForMoveToCenter(!isMovingAxisIsX, forInsertion) + * WhenRectangleOutsideCircle(distanceOnStaticAxis, distanceBetweenCornerAndCenter, + distanceOnAxisForBringBackOnCircle); + + return CalculateMoveMultiplierForMoveFromCenter(!isMovingAxisIsX, forInsertion) + * WhenRectangleInCircle(distanceOnStaticAxis, distanceOnAxisForBringBackOnCircle); + } + + private int WhenRectangleOutsideCircle(int distanceOnStaticAxis, int distanceBetweenCornerAndCenter, + int distanceOnAxisForBringBackOnCircle) + { + var inCircleCathetusPart = Math.Sqrt(Math.Abs(Radius * Radius - distanceOnStaticAxis * distanceOnStaticAxis)); + return CalculatePartCathetus(distanceBetweenCornerAndCenter, inCircleCathetusPart, + distanceOnAxisForBringBackOnCircle); + } + + private int WhenRectangleInCircle(int distanceOnStaticAxis, int distanceOnAxisForBringBackOnCircle) + { + return CalculatePartCathetus(Radius, distanceOnStaticAxis, distanceOnAxisForBringBackOnCircle); + } + + private int CalculatePartCathetus(int hypotenuse, double a, int b) + { + return (int)Math.Ceiling(Math.Sqrt(Math.Abs(hypotenuse * hypotenuse - a * a))) - b; + } + + private Point CalculateCornerNearestForCenterAfterMove(Sector whereIntersected, int distanceForMoving, + Rectangle forMove) + { + var isAxisForMoveIsX = IsMovingAxisIsXBySector(whereIntersected); + var moveMultiplier = CalculateMoveMultiplierForMoveClockwise(isAxisForMoveIsX, forMove); + distanceForMoving *= moveMultiplier; + var nearestCorner = GetCornerNearestForCenterBySector(whereIntersected, forMove); + return isAxisForMoveIsX + ? new Point(nearestCorner.X + distanceForMoving, nearestCorner.Y) + : new Point(nearestCorner.X, nearestCorner.Y + distanceForMoving); + } + + private int CalculateMoveMultiplierForMoveFromCenter(bool isAxisForMoveIsX, Rectangle forMove) + { + if (forMove.Bottom < Center.Y && forMove.Left > Center.X) return isAxisForMoveIsX ? 1 : -1; + if (forMove.Bottom < Center.Y && forMove.Right < Center.X) return -1; + if (forMove.Top > Center.Y && forMove.Left > Center.X) return 1; + if (forMove.Top > Center.Y && forMove.Right < Center.X) return isAxisForMoveIsX ? -1 : 1; + return isAxisForMoveIsX ? forMove.Bottom < Center.Y ? -1 : 1 + : forMove.Left > Center.X ? 1 : -1; + } + + private int CalculateMoveMultiplierForMoveToCenter(bool isAxisForMoveIsX, Rectangle forMove) + { + return CalculateMoveMultiplierForMoveFromCenter(isAxisForMoveIsX, forMove) * -1; + } + + private int CalculateMoveMultiplierForMoveClockwise(bool isAxisForMoveIsX, Rectangle forMove) + { + if (forMove.Bottom < Center.Y && forMove.Left > Center.X) return 1; + if (forMove.Bottom < Center.Y && forMove.Right < Center.X) return isAxisForMoveIsX ? 1 : -1; + if (forMove.Top > Center.Y && forMove.Left > Center.X) return isAxisForMoveIsX ? -1 : 1; + if (forMove.Top > Center.Y && forMove.Right < Center.X) return -1; + return isAxisForMoveIsX ? forMove.Bottom < Center.Y ? 1 : -1 + : forMove.Left > Center.X ? -1 : 1; + } + + private int CalculateDistanceForMoveClockwiseToPositionWithoutIntersection( + Sector whereIntersected, Rectangle forInsertion, Rectangle intersected) + { + return whereIntersected switch + { + Sector.TopRight => Math.Abs(forInsertion.Top - intersected.Bottom), + Sector.BottomRight => Math.Abs(forInsertion.Right - intersected.Left), + Sector.BottomLeft => Math.Abs(forInsertion.Bottom - intersected.Top), + _ => Math.Abs(forInsertion.Left - intersected.Right) + }; + } + + private static Point GetCornerNearestForCenterBySector(Sector rectangleLocationSector, Rectangle forInsertion) + { + return rectangleLocationSector switch + { + Sector.TopRight => new Point(forInsertion.Left, forInsertion.Bottom), + Sector.BottomRight => new Point(forInsertion.Left, forInsertion.Top), + Sector.BottomLeft => new Point(forInsertion.Right, forInsertion.Top), + _ => new Point(forInsertion.Right, forInsertion.Bottom) + }; + } + + private bool IsMovingAxisIsXBySector(Sector forInsertionRectangleSector) + { + return forInsertionRectangleSector == Sector.BottomRight || forInsertionRectangleSector == Sector.TopLeft; + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/CircularCloudLayouter.cs b/cs/TagsCloudVisualization/CircularCloudLayouter.cs new file mode 100644 index 000000000..af4fa6261 --- /dev/null +++ b/cs/TagsCloudVisualization/CircularCloudLayouter.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace TagsCloudVisualization; + +public class CircularCloudLayouter +{ + private readonly List storage; + private readonly CloudCompressor compressor; + + public CircleLayer CurrentLayer { get; } + + public CircularCloudLayouter(Point center) : this(center, []) + { } + + internal CircularCloudLayouter(Point center, List storage) + { + this.storage = storage; + CurrentLayer = new(center, storage); + compressor = new(center, storage); + } + + public Rectangle PutNextRectangle(Size nextRectangle) + { + ValidateRectangleSize(nextRectangle); + + var inserted = PutRectangleWithoutIntersection(nextRectangle); + var rectangleWithOptimalPosition = compressor.CompressCloudAfterInsertion(inserted); + + storage.Add(rectangleWithOptimalPosition); + CurrentLayer.OnSuccessInsertRectangle(); + + return rectangleWithOptimalPosition; + } + + public Rectangle PutRectangleWithoutIntersection(Size forInsertionSize) + { + var firstRectanglePosition = CurrentLayer.AddNextRectangle(forInsertionSize); + var forInsertion = new Rectangle(firstRectanglePosition, forInsertionSize); + var intersected = GetRectangleIntersection(forInsertion); + + while (intersected != null && intersected.Value != default) + { + var possiblePosition = CurrentLayer.GetRectanglePositionWithoutIntersection(forInsertion, intersected.Value); + forInsertion.Location = possiblePosition; + intersected = GetRectangleIntersection(forInsertion); + } + + return forInsertion; + } + + private static void ValidateRectangleSize(Size forInsertion) + { + if (forInsertion.Width <= 0 || forInsertion.Height <= 0) + throw new ArgumentException($"Rectangle has incorrect size: width = {forInsertion.Width}, height = {forInsertion.Height}"); + } + + private Rectangle? GetRectangleIntersection(Rectangle forInsertion) + { + if (storage.Count == 0) return null; + return storage.FirstOrDefault(r => forInsertion != r + && forInsertion.IntersectsWith(r)); + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/CircularCloudVisualizer.cs b/cs/TagsCloudVisualization/CircularCloudVisualizer.cs new file mode 100644 index 000000000..9b99d7525 --- /dev/null +++ b/cs/TagsCloudVisualization/CircularCloudVisualizer.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; + +namespace TagsCloudVisualization +{ + public class CircularCloudVisualizer + { + private readonly Color backgroundColor = Color.White; + private readonly Color rectangleColor = Color.DarkBlue; + private readonly Size imageSize; + private readonly List rectangleStorage; + + public CircularCloudVisualizer(List rectangles, Size imageSize) + { + rectangleStorage = rectangles; + this.imageSize = imageSize; + } + + public void CreateImage(string? filePath = null, bool withSaveSteps = false) + { + var rectangles = rectangleStorage.Select(r => (Rectangle)r).ToArray(); + rectangles = NormalizeSizes(rectangles); + + using var image = new Bitmap(imageSize.Width, imageSize.Height); + using var graphics = Graphics.FromImage(image); + graphics.Clear(backgroundColor); + graphics.DrawGrid(); + var pen = new Pen(rectangleColor); + + for (var i = 0; i < rectangles.Length; i++) + { + var nextRectangle = rectangles[i]; + graphics.DrawRectangle(pen, nextRectangle); + if (withSaveSteps) + { + SaveImage(image, filePath); + } + } + SaveImage(image, filePath); + } + + private void SaveImage(Bitmap image, string? filePath = null) + { + var rnd = new Random(); + filePath ??= Path.Combine(Path.GetTempPath(), $"testImage{rnd.Next()}.png"); + image.Save(filePath, ImageFormat.Png); + } + + private Rectangle[] NormalizeSizes(IEnumerable source) + { + var xLength = source.Max(r => r.Right) - source.Min(r => r.Left); + var yLength = source.Max(r => r.Bottom) - source.Min(r => r.Top); + + var factorX = GetNormalizeFactorByAxis(imageSize.Width, xLength); + var factorY = GetNormalizeFactorByAxis(imageSize.Height, yLength); + + return source.Select(r => new Rectangle( + new Point(r.X * factorX, r.Y * factorY), + new Size(r.Width * factorX, r.Height * factorY))) + .ToArray(); + } + + private static int GetNormalizeFactorByAxis(int imageSizeOnAxis, int rectanglesSizeOnAxis) + { + const int boundShift = 10; + double imageSizeOnAxisWithShiftForBounds = imageSizeOnAxis - boundShift; + var stretchFactor = (int)Math.Floor(imageSizeOnAxisWithShiftForBounds / rectanglesSizeOnAxis); + return imageSizeOnAxisWithShiftForBounds > rectanglesSizeOnAxis ? stretchFactor : 1; + } + } +} diff --git a/cs/TagsCloudVisualization/CloudCompressor.cs b/cs/TagsCloudVisualization/CloudCompressor.cs new file mode 100644 index 000000000..24addd20e --- /dev/null +++ b/cs/TagsCloudVisualization/CloudCompressor.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Drawing; + +namespace TagsCloudVisualization +{ + internal class CloudCompressor + { + private readonly BruteForceNearestFinder nearestFinder = new (); + private readonly Point compressionPoint; + private readonly List cloud; + + public CloudCompressor(Point compressTo, List cloud) + { + compressionPoint = compressTo; + this.cloud = cloud; + } + + public Rectangle CompressCloudAfterInsertion(Rectangle insertionRectangle) + { + var toCompressionPoint = GetDirectionsForMovingForCompress(insertionRectangle); + foreach (var direction in toCompressionPoint) + { + insertionRectangle.Location = CalculateRectangleLocationAfterCompress(insertionRectangle, direction); + } + return insertionRectangle; + } + + private Point CalculateRectangleLocationAfterCompress(Rectangle forMoving, Direction toCenter) + { + var nearest = nearestFinder.FindNearestByDirection(forMoving, toCenter, cloud); + if (nearest == null) return forMoving.Location; + var distanceCalculator = nearestFinder.GetMinDistanceCalculatorBy(toCenter); + var distanceForMove = distanceCalculator(nearest.Value, forMoving); + return MoveByDirection(forMoving.Location, distanceForMove, toCenter); + } + + private static Point MoveByDirection(Point forMoving, int distance, Direction whereMoving) + { + var factorForDistanceByX = whereMoving switch + { + Direction.Left => -1, + Direction.Right => 1, + _ => 0 + }; + var factorForDistanceByY = whereMoving switch + { + Direction.Top => -1, + Direction.Bottom => 1, + _ => 0 + }; + forMoving.X += distance * factorForDistanceByX; + forMoving.Y += distance * factorForDistanceByY; + + return forMoving; + } + + private List GetDirectionsForMovingForCompress(Rectangle forMoving) + { + var directions = new List(); + if (forMoving.Bottom < compressionPoint.Y) directions.Add(Direction.Bottom); + if (forMoving.Left > compressionPoint.X) directions.Add(Direction.Left); + if (forMoving.Right < compressionPoint.X) directions.Add(Direction.Right); + if (forMoving.Top > compressionPoint.Y) directions.Add(Direction.Top); + return directions; + } + } +} diff --git a/cs/TagsCloudVisualization/Dicrection.cs b/cs/TagsCloudVisualization/Dicrection.cs new file mode 100644 index 000000000..6a144ce0d --- /dev/null +++ b/cs/TagsCloudVisualization/Dicrection.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization +{ + public enum Direction + { + Left, + Right, + Top, + Bottom + } +} diff --git a/cs/TagsCloudVisualization/GraphicsExtensions.cs b/cs/TagsCloudVisualization/GraphicsExtensions.cs new file mode 100644 index 000000000..01fc09426 --- /dev/null +++ b/cs/TagsCloudVisualization/GraphicsExtensions.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 static class GraphicsExtensions + { + public static void DrawGrid(this Graphics graphics, int cellsCount = 100, int cellSize = 10) + { + Pen p = new (Color.DarkGray); + + for (int y = 0; y < cellsCount; ++y) + { + graphics.DrawLine(p, 0, y * cellSize, cellsCount * cellSize, y * cellSize); + } + + for (int x = 0; x < cellsCount; ++x) + { + graphics.DrawLine(p, x * cellSize, 0, x * cellSize, cellsCount * cellSize); + } + } + } +} diff --git a/cs/TagsCloudVisualization/Images/FIRSTa.png b/cs/TagsCloudVisualization/Images/FIRSTa.png new file mode 100644 index 000000000..774375a96 Binary files /dev/null and b/cs/TagsCloudVisualization/Images/FIRSTa.png differ diff --git a/cs/TagsCloudVisualization/Images/sECOND.png b/cs/TagsCloudVisualization/Images/sECOND.png new file mode 100644 index 000000000..a191367c5 Binary files /dev/null and b/cs/TagsCloudVisualization/Images/sECOND.png differ diff --git a/cs/TagsCloudVisualization/Images/third.png b/cs/TagsCloudVisualization/Images/third.png new file mode 100644 index 000000000..213d06531 Binary files /dev/null and b/cs/TagsCloudVisualization/Images/third.png differ diff --git a/cs/TagsCloudVisualization/PointExtensions.cs b/cs/TagsCloudVisualization/PointExtensions.cs new file mode 100644 index 000000000..51bdf6af0 --- /dev/null +++ b/cs/TagsCloudVisualization/PointExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization +{ + public static class PointExtensions + { + public static int CalculateDistanceBetween(this Point current, Point other) + { + return (int)Math.Ceiling(Math.Sqrt((current.X - other.X) * (current.X - other.X) + (current.Y - other.Y) * (current.Y - other.Y))); + } + } +} diff --git a/cs/TagsCloudVisualization/README.md b/cs/TagsCloudVisualization/README.md new file mode 100644 index 000000000..958981835 --- /dev/null +++ b/cs/TagsCloudVisualization/README.md @@ -0,0 +1,3 @@ +![Alt text](./Images/FIRSTa.png?raw=true "First") +![Alt text](./Images/sECOND.png?raw=true "Second") +![Alt text](./Images/third.png?raw=true "Third") diff --git a/cs/TagsCloudVisualization/TagsCloudVisualization.csproj b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj new file mode 100644 index 000000000..1a44a26a4 --- /dev/null +++ b/cs/TagsCloudVisualization/TagsCloudVisualization.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + disable + enable + TagsCloudVisualization.Program + + + + + + + + + + + + + + + diff --git a/cs/TagsCloudVisualization/Tests/BruteForceNearestFinderTests.cs b/cs/TagsCloudVisualization/Tests/BruteForceNearestFinderTests.cs new file mode 100644 index 000000000..8d2eb2c18 --- /dev/null +++ b/cs/TagsCloudVisualization/Tests/BruteForceNearestFinderTests.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using NUnit.Framework; +using FluentAssertions; +using System.Drawing; + +namespace TagsCloudVisualization.Tests +{ + public class BruteForceNearestFinderTests + { + private BruteForceNearestFinder finder; + [SetUp] + public void SetUp() + { + finder = new BruteForceNearestFinder(); + } + [Test] + public void FindNearest_ShouldReturnNull_OnEmptyRectangles() + { + var rectangleForFind = new Rectangle(new Point(5, 7), new Size(4, 2)); + + finder.FindNearestByDirection(rectangleForFind, Direction.Top, []).Should().BeNull(); + } + + [TestCase(4, 10, Direction.Top)] + [TestCase(2, 7, Direction.Top, true)] + [TestCase(2, 7, Direction.Right)] + [TestCase(0, 0, Direction.Right, true)] + [TestCase(0, 0, Direction.Bottom, true)] + [TestCase(7, 4, Direction.Bottom)] + [TestCase(10, 11, Direction.Left)] + [TestCase(7, 4, Direction.Left, true)] + public void FindNearest_ShouldReturnNearestRectangleByDirection_ForArgumentRectangle(int x, int y, Direction direction, bool isFirstNearest = false) + { + var addedRectangle1 = new Rectangle(new Point(2, 2), new Size(3, 4)); + var addedRectangle2 = new Rectangle(new Point(5, 7), new Size(4, 2)); + var rectangleForFind = new Rectangle(new Point(x, y), new Size(2, 1)); + var rectangles = new List { addedRectangle1, addedRectangle2 }; + + var nearest = finder.FindNearestByDirection(rectangleForFind, direction, rectangles); + + nearest.Should().Be(isFirstNearest ? addedRectangle1 : addedRectangle2); + } + } +} diff --git a/cs/TagsCloudVisualization/Tests/CircleLayerTests.cs b/cs/TagsCloudVisualization/Tests/CircleLayerTests.cs new file mode 100644 index 000000000..01ddc6ea5 --- /dev/null +++ b/cs/TagsCloudVisualization/Tests/CircleLayerTests.cs @@ -0,0 +1,214 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using static TagsCloudVisualization.CircleLayer; + +namespace TagsCloudVisualization.Tests; + +public class CircleLayerTests +{ + private CircleLayer currentLayer; + private Size defaultRectangleSize; + private List storage; + + public static IEnumerable SimpleIntersectionInSector + { + get + { + yield return new TestCaseData( + new Rectangle(new Point(5, -1), new Size(5, 1)), + new Rectangle(new Point(8, -6), new Size(8, 7)), + new Point(9, 1), 0).SetName("WhenFoundIntersectionInTopRightSector"); + yield return new TestCaseData( + new Rectangle(new Point(8, 9), new Size(5, 1)), + new Rectangle(new Point(10, 5), new Size(8, 7)), + new Point(5, 10), 1).SetName("WhenFoundIntersectionInBottomRightSector"); + yield return new TestCaseData( + new Rectangle(new Point(-3, 9), new Size(5, 3)), + new Rectangle(new Point(-7, 8), new Size(8, 7)), + new Point(-1, 5), 2).SetName("WhenFoundIntersectionInBottomLeftSector"); + yield return new TestCaseData( + new Rectangle(new Point(-3, -2), new Size(4, 3)), + new Rectangle(new Point(-7, 1), new Size(8, 7)), + new Point(1, -1), 3).SetName("WhenFoundIntersectionInTopLeftSector"); + } + } + + public static IEnumerable GetDataForIntersectionTests + { + get + { + yield return new TestCaseData(new Size[] + { new(1, 1), new(5, 8), new(4, 4), new(4, 4), new(4, 4) }, + new Rectangle(new Point(11, 5), new Size(6, 6)), + new Rectangle(new Point(10, 5), new Size(5, 8)), + new Point(4, 12)).SetName("ChangeCornerPositionForSector_WhenMoveRectangleClockwise"); + yield return new TestCaseData(new Size[] + { new(1, 1), new(1, 8), new(50, 50), new(1, 1), new(1, 1), new(1, 1) }, + new Rectangle(new Point(4, 10), new Size(1, 1)), + new Rectangle(new Point(-50, 10), new Size(50, 50)), + new Point(8, 11)).SetName("GetCorrectPosition_WhenRectanglesSidesMatch"); + yield return new TestCaseData(new Size[] + { new(6, 3), new(4, 2), new(1, 1), new(4, 4) }, + new Rectangle(new Point(5, -7), new Size(6, 5)), + new Rectangle(new Point(5, -3), new Size(6, 3)), + new Point(12, 0)).SetName("NotChangeSector_WhenRectangleForIntersectionBottomEqualCenterY_AfterMove"); + } + } + + [SetUp] + public void SetUp() + { + var center = new Point(5, 5); + storage = []; + currentLayer = new CircleLayer(center, storage); + var first = new Rectangle(currentLayer.AddNextRectangle(new Size(8, 6)), new Size(8, 6)); + storage.Add(first); + currentLayer.OnSuccessInsertRectangle(); + defaultRectangleSize = new Size(3, 4); + } + + [Test] + public void CircleLayer_InsertFirstForLayerRectangle_InTopRightSectorStart() + { + var possibleRectangleLocation = currentLayer.AddNextRectangle(defaultRectangleSize); + + possibleRectangleLocation.Should() + .Be(GetCorrectRectangleLocationByExpectedSector(Sector.TopRight, defaultRectangleSize)); + } + + [TestCase(1, Sector.BottomRight)] + [TestCase(2, Sector.BottomLeft)] + [TestCase(3, Sector.TopLeft)] + [TestCase(4, Sector.TopRight)] + [TestCase(0, Sector.TopRight)] + public void CircleLayer_InsertRectangleInNextSector_AfterSuccessInsertion(int insertionsCount, Sector expected) + { + currentLayer = GetLayerAfterFewInsertionsRectangleWithSameSize(currentLayer, insertionsCount); + + var possibleRectangleLocation = currentLayer.AddNextRectangle(defaultRectangleSize); + + possibleRectangleLocation.Should() + .Be(GetCorrectRectangleLocationByExpectedSector(expected, defaultRectangleSize)); + } + + + [Test] + public void CircleLayer_RadiusNextCircleLayer_ShouldBeIntMinDistanceFromCenterToInsertedRectangles() + { + currentLayer = GetLayerAfterFewInsertionsRectangleWithSameSize(currentLayer, 3); + var nextRectangleLocation = + GetCorrectRectangleLocationByExpectedSector(GetSectorByInsertionsCount(4), defaultRectangleSize); + storage.Add(new Rectangle(nextRectangleLocation, new Size(2, 2))); + + currentLayer.OnSuccessInsertRectangle(); + + currentLayer.Radius.Should().Be(9); + } + + private CircleLayer GetLayerAfterFewInsertionsRectangleWithSameSize(CircleLayer layer, int additionsCount) + { + layer = GetLayerAfterFewInsertionsRectangle(layer, additionsCount, + new Size[additionsCount].Select(_ => defaultRectangleSize).ToArray()); + return layer; + } + + private Sector GetSectorByInsertionsCount(int count) + { + return (Sector)((count - 1) % 4); + } + + private Point GetCorrectRectangleLocationByExpectedSector(Sector expected, Size size) + { + return expected switch + { + Sector.TopRight => new Point(currentLayer.Center.X, currentLayer.Center.Y - currentLayer.Radius - size.Height), + Sector.BottomRight => new Point(currentLayer.Center.X + currentLayer.Radius, currentLayer.Center.Y), + Sector.BottomLeft => new Point(currentLayer.Center.X - size.Width, currentLayer.Center.Y + currentLayer.Radius), + _ => new Point(currentLayer.Center.X - currentLayer.Radius - size.Width, + currentLayer.Center.Y - size.Height), + }; + } + + [Test] + public void CircleLayer_RectangleWithNewPositionAfterIntersection_ShouldNotIntersectSameRectangle() + { + var rectangleForInsertion = new Rectangle(new Point(5, -1), new Size(5, 1)); + var intersectedRectangle = new Rectangle(new Point(8, -6), new Size(8, 7)); + + var newPosition = + currentLayer.GetRectanglePositionWithoutIntersection(rectangleForInsertion, intersectedRectangle); + + new Rectangle(newPosition, rectangleForInsertion.Size).IntersectsWith(intersectedRectangle).Should() + .BeFalse(); + } + + [Test] + public void + GetPositionWithoutIntersection_ShouldPlaceBottomLeftCornerOnCircle_WhenFoundIntersectionInTopRightSector() + { + var rectangleForInsertion = new Rectangle(new Point(5, -1), new Size(5, 1)); + var intersectedRectangle = new Rectangle(new Point(8, -6), new Size(8, 7)); + + var newPosition = + currentLayer.GetRectanglePositionWithoutIntersection(rectangleForInsertion, intersectedRectangle); + var bottomLeftCorner = new Point(newPosition.X, newPosition.Y + intersectedRectangle.Height); + + CurrentLayerContainsPoint(bottomLeftCorner).Should().BeTrue(); + } + + [TestCaseSource(nameof(SimpleIntersectionInSector))] + public void GetPositionWithoutIntersection_ReturnCorrectRectanglePosition(Rectangle forInsertion, + Rectangle intersected, Point expected, int additionsCount) + { + currentLayer = GetLayerAfterFewInsertionsRectangleWithSameSize(currentLayer, additionsCount); + + var newPosition = currentLayer.GetRectanglePositionWithoutIntersection(forInsertion, intersected); + + newPosition.Should().Be(expected); + } + + private bool CurrentLayerContainsPoint(Point p) + { + return (p.X - currentLayer.Center.X) * (p.X - currentLayer.Center.X) + + (p.Y - currentLayer.Center.Y) * (p.Y - currentLayer.Center.Y) == currentLayer.Radius * currentLayer.Radius; + } + + [Test] + public void CircleLayer_RadiusNextCircleLayer_ShouldBeCeilingToInt() + { + var sizes = new Size[] + { + new(8, 1), new(7, 8), new(4, 4), new(4, 4), new(4, 4) + }; + var nextLayer = GetLayerAfterFewInsertionsRectangle(currentLayer, sizes.Length, sizes); + + nextLayer.Radius.Should().Be(10); + } + + [TestCaseSource(nameof(GetDataForIntersectionTests))] + public void GetPositionWithoutIntersection_Should(Size[] sizes, Rectangle forInsertion, Rectangle intersected, + Point expected) + { + var fullLayer = GetLayerAfterFewInsertionsRectangle(currentLayer, sizes.Length, sizes); + + var newPosition = fullLayer.GetRectanglePositionWithoutIntersection(forInsertion, intersected); + + newPosition.Should().Be(expected); + } + + private CircleLayer GetLayerAfterFewInsertionsRectangle(CircleLayer layer, int insertionsCount, Size[] sizes) + { + for (var i = 1; i <= insertionsCount; i++) + { + var location = GetCorrectRectangleLocationByExpectedSector(GetSectorByInsertionsCount(i), sizes[i - 1]); + var rectangleForInsert = new Rectangle(location, sizes[i - 1]); + storage.Add(rectangleForInsert); + layer.OnSuccessInsertRectangle(); + } + + return layer; + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Tests/CircularCloudLayouterTests.cs b/cs/TagsCloudVisualization/Tests/CircularCloudLayouterTests.cs new file mode 100644 index 000000000..cb447038b --- /dev/null +++ b/cs/TagsCloudVisualization/Tests/CircularCloudLayouterTests.cs @@ -0,0 +1,126 @@ +using System; +using System.Drawing; +using NUnit.Framework; +using FluentAssertions; +using NUnit.Framework.Interfaces; +using System.IO; +using System.Reflection; +using System.Collections.Generic; + +namespace TagsCloudVisualization.Tests +{ + public class CircularCloudLayouterTests + { + private CircularCloudLayouter layouter; + private Point defaultCenter; + private List storage; + + [SetUp] + public void SetUp() + { + defaultCenter = new Point(5, 5); + storage = []; + layouter = new CircularCloudLayouter(defaultCenter, storage); + } + + [TearDown] + public void TearDown() + { + if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed) + { + var testObj = TestContext.CurrentContext.Test.Parent?.Fixture as CircularCloudLayouterTests; + var info = typeof(CircularCloudLayouterTests) + .GetField("storage", BindingFlags.NonPublic | BindingFlags.Instance); + var st = info?.GetValue(testObj); + + var visualizer = new CircularCloudVisualizer(st as List ?? [], new Size(1000, 1000)); + var pathFile = Path.Combine(Directory.GetCurrentDirectory(), TestContext.CurrentContext.Test.Name); + visualizer.CreateImage(pathFile); + TestContext.Out.WriteLine($"Tag cloud visualization saved to file {pathFile}"); + } + } + + [TestCase(0, 4, TestName = "WhenWidthZero")] + [TestCase(3, 0, TestName = "WhenHeightZero")] + [TestCase(-3, 4, TestName = "WhenWidthIsNegative")] + [TestCase(3, -4, TestName = "WhenHeightNegative")] + [TestCase(-3, -4, TestName = "WhenWidthAndHeightNegative")] + [TestCase(0, 0, TestName = "WhenWidthAndHeightIsZero")] + public void Insert_ShouldThrow(int width, int height) + { + var inCorrectSize = new Size(width, height); + + Action act = () => layouter.PutNextRectangle(inCorrectSize); + + act.Should().Throw(); + } + + [Test] + public void PutNextRectangle_ShouldAddRectangleToCenter_WhenRectangleFirst() + { + var firstRectangleSize = new Size(6, 4); + var expected = new Rectangle(new Point(2, 3), firstRectangleSize); + + var nextRectangle = layouter.PutNextRectangle(firstRectangleSize); + + nextRectangle + .Should().Be(expected); + } + + [Test] + public void PutNextRectangle_ShouldCreateFirstCircleLayer_AfterCreation() + { + layouter.CurrentLayer.Should().NotBeNull(); + } + + [TestCase(6, 4, 4)] + [TestCase(4, 6, 4)] + [TestCase(2, 2, 2)] + [TestCase(5, 9, 6)] + public void PutNextRectangle_ShouldCreateFirstCircleLayer_WithRadiusEqualHalfDiagonalFirstRectangleRoundToInt(int height, int width, int expected) + { + var firstRectangleSize = new Size(width, height); + + layouter.PutNextRectangle(firstRectangleSize); + + layouter.CurrentLayer.Radius.Should().Be(expected); + } + + [Test] + public void PutRectangleOnCircleWithoutIntersection_ShouldPutRectangleWithoutIntersection() + { + var expected = new Point(14, 1); + + var sizes = new Size[] { new (6, 4), new(4, 7), new(4, 4), new(4, 4), new(4, 4) }; + layouter = InsertionsWithoutCompress(layouter, sizes, storage); + var rectangleLocation = layouter.PutRectangleWithoutIntersection(new(3, 3)).Location; + + rectangleLocation.Should().Be(expected); + } + + private static CircularCloudLayouter InsertionsWithoutCompress(CircularCloudLayouter layouter, Size[] sizes, List storage) + { + for (var i = 0; i < sizes.Length; i++) + { + var forInsertion = layouter.PutRectangleWithoutIntersection(sizes[i]); + storage.Add(forInsertion); + layouter.CurrentLayer.OnSuccessInsertRectangle(); + } + + return layouter; + } + + [Test] + public void PutNextRectangle_ShouldTryMoveRectangleCloserToCenter_WhenItPossible() + { + var firstRectangleSize = new Size(6, 4); + var secondRectangleSize = new Size(4, 4); + var expectedSecondRectangleLocation = new Point(5, -1); + + layouter.PutNextRectangle(firstRectangleSize); + var second = layouter.PutNextRectangle(secondRectangleSize); + + second.Location.Should().Be(expectedSecondRectangleLocation); + } + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Tests/CircularCloudLayouterVisualizationTests.cs b/cs/TagsCloudVisualization/Tests/CircularCloudLayouterVisualizationTests.cs new file mode 100644 index 000000000..b08d35f44 --- /dev/null +++ b/cs/TagsCloudVisualization/Tests/CircularCloudLayouterVisualizationTests.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using NUnit.Framework; + +namespace TagsCloudVisualization.Tests; + +public class CircularCloudLayouterVisualizationTests +{ + private Size imageSize; + private Point center; + private CircularCloudVisualizer visualizer; + + [SetUp] + public void SetUp() + { + imageSize = new(1000, 1000); + center = new Point(imageSize.Width / 2, imageSize.Height / 2); + visualizer = new CircularCloudVisualizer(GenerateRectangles(center, 100, 10, 100), imageSize); + } + + [Test] + public void GenerateImage() + { + visualizer.CreateImage(); + } + + [Test] + public void GenerateImageWithSaveEveryStep() + { + visualizer.CreateImage(withSaveSteps: true); + } + + private static List GenerateRectangles(Point center, int maxSize, int minSize, int count) + { + var rnd = new Random(); + var storage = new List(); + var layouter = new CircularCloudLayouter(center, storage); + for (var i = 0; i < count; i++) layouter.PutNextRectangle(new Size(rnd.Next(minSize, maxSize), + rnd.Next(minSize, maxSize))); + + return storage; + } +} \ No newline at end of file diff --git a/cs/TagsCloudVisualization/Tests/RectangleStorageTests.cs b/cs/TagsCloudVisualization/Tests/RectangleStorageTests.cs new file mode 100644 index 000000000..9d0b90954 --- /dev/null +++ b/cs/TagsCloudVisualization/Tests/RectangleStorageTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using NUnit.Framework; + +namespace TagsCloudVisualization.Tests +{ + public class RectangleStorageTests + { + private RectangleStorage storage; + private Rectangle defaulRectangle; + [SetUp] + public void SetUp() + { + storage = new RectangleStorage(); + defaulRectangle = new(new(2, 2), new(2, 2)); + } + + [Test] + public void GetRectangles_ShouldGetAllRectangle() + { + storage.Add(defaulRectangle); + storage.Add(defaulRectangle); + + storage.GetAll().Should().HaveCount(2); + } + + [Test] + public void AddRectangle_ShouldGetIdForRectangle() + { + var id = storage.Add(defaulRectangle); + + storage.GetById(id).Should().Be(defaulRectangle); + } + + [Test] + public void ChangeRectangle_ShouldChangeRectangleByIndex() + { + var id = storage.Add(defaulRectangle); + var rectangleForChange = storage.GetById(id); + + rectangleForChange.Size = new Size(1, 1); + + storage.GetById(id).Size.Should().Be(rectangleForChange.Size); + } + } +}