Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions TagsCloud/CircularCloudLayouter/CircularCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using TagCloud;
using TagsCloud.Interfaces;

namespace TagsCloud.CircularCloudLayouter
{
public class CircularCloudLayouter : ITagLayouter
{
private Point Center { get; }
private readonly List<Rectangle> rectangles = new List<Rectangle>();
public IReadOnlyList<Rectangle> Rectangles => rectangles.AsReadOnly();

private readonly IPointGetter pointGetter;

public CircularCloudLayouter(Point center, IPointGetter pointGetter)
{
Center = center;
this.pointGetter = pointGetter;
}

public Rectangle PutNextRectangle(Size rectangleSize)
{
if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0)
throw new ArgumentException("Стороны должны иметь положительный размер", nameof(rectangleSize));

if (!rectangles.Any())
{
var location = new Point(Center.X - rectangleSize.Width / 2, Center.Y - rectangleSize.Height / 2);
var rect = new Rectangle(location, rectangleSize);
rectangles.Add(rect);
return rect;
}

for (int iter = 0; iter < 200000; iter++)
{
var p = pointGetter.GetNextPoint();
var topLeft = new Point(p.X - rectangleSize.Width / 2, p.Y - rectangleSize.Height / 2);
var candidate = new Rectangle(topLeft, rectangleSize);

if (!TagCloudUtils.IntersectsAny(candidate, rectangles))
{
candidate = MoveCloserToCenter(candidate);
rectangles.Add(candidate);
return candidate;
}
}

throw new InvalidOperationException("Превзойдено ограничение итераций");
}

private Rectangle MoveCloserToCenter(Rectangle rect)
{
var candidate = rect;
var moved = true;
while (moved)
{
moved = false;
var movedCandidate = TagCloudUtils.MoveVerticalTowardsCenter(candidate, Center);
if (!TagCloudUtils.IntersectsAny(movedCandidate, rectangles) && TagCloudUtils.DistanceToCenter(movedCandidate, Center) < TagCloudUtils.DistanceToCenter(candidate, Center))
{
candidate = movedCandidate;
moved = true;
}
movedCandidate = TagCloudUtils.MoveHorizontalTowardsCenter(candidate, Center);
if (!TagCloudUtils.IntersectsAny(movedCandidate, rectangles) && TagCloudUtils.DistanceToCenter(movedCandidate, Center) < TagCloudUtils.DistanceToCenter(candidate, Center))
{
candidate = movedCandidate;
moved = true;
}
}
return candidate;
}
}
}
10 changes: 10 additions & 0 deletions TagsCloud/CircularCloudLayouter/IPointGetter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Drawing;


namespace TagsCloud.CircularCloudLayouter
{
public interface IPointGetter
{
Point GetNextPoint();
}
}
35 changes: 35 additions & 0 deletions TagsCloud/CircularCloudLayouter/Spiral.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Drawing;


namespace TagsCloud.CircularCloudLayouter
{
public class Spiral : IPointGetter
{
private readonly Point center;
private readonly double angleStep;
private readonly double radiusStep;
private double angle;


public Spiral(Point center, double angleStep = 0.1, double radiusStep = 0.5)
{
this.center = center;
this.angleStep = angleStep;
this.radiusStep = radiusStep;
angle = 0;
}


public Point GetNextPoint()
{
double radius = radiusStep * angle;
var x = center.X + radius * Math.Cos(angle);
var y = center.Y + radius * Math.Sin(angle);
angle += angleStep;
return new Point((int)Math.Round(x), (int)Math.Round(y));
}


}
}
62 changes: 62 additions & 0 deletions TagsCloud/CircularCloudLayouter/VisualizationPrinter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;

namespace TagsCloud.CircularCloudLayouter
{
public static class VisualizationPrinter
{
public static void Print(string name, IEnumerable<Rectangle> rectangles, Point center)
{
var bmp = GetImage(rectangles, center);
var path = GetPath(name);
bmp.Save(path);
}

private static string GetPath(string name, [CallerFilePath] string callerFilePath = "")
{
var dir = Path.Combine(Path.GetDirectoryName(callerFilePath), "visualizations");
Directory.CreateDirectory(dir);
var filename = $"visualization-{name}.png";
var path = Path.Combine(dir, filename);
Console.WriteLine($"Изображение сохранено в {path}");
return path;
}
private static Bitmap GetImage(IEnumerable<Rectangle> rects, Point center)
{
var padding = 20;
var bounds = GetBounds(rects);
var bmpWidth = Math.Max(100, bounds.Width + padding * 2);
var bmpHeight = Math.Max(100, bounds.Height + padding * 2);
var bmp = new Bitmap(bmpWidth, bmpHeight);
var g = Graphics.FromImage(bmp);
g.Clear(Color.White);
var offsetX = -bounds.Left + padding;
var offsetY = -bounds.Top + padding;
var rnd = new Random(0);
foreach (var r in rects)
{
var rect = new Rectangle(r.Left + offsetX, r.Top + offsetY, r.Width, r.Height);
var brush = new SolidBrush(Color.FromArgb(200, (byte)rnd.Next(60, 230), (byte)rnd.Next(60, 230), (byte)rnd.Next(60, 230)));
g.FillRectangle(brush, rect);
g.DrawRectangle(Pens.Black, rect);
}
return bmp;
}


private static Rectangle GetBounds(IEnumerable<Rectangle> rects)
{
var left = rects.Min(r => r.Left);
var top = rects.Min(r => r.Top);
var right = rects.Max(r => r.Right);
var bottom = rects.Max(r => r.Bottom);
return new Rectangle(left, top, right - left, bottom - top);

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Drawing;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;


namespace TagsCloud.CircularCloudLayouter.tests
{
[TestFixture]
public class CircularCloudLayouterTests
{
private Point center;
private CircularCloudLayouter layouter;

[SetUp]
public void SetUp()
{
center = new Point(500, 500);
layouter = new CircularCloudLayouter(center, new Spiral(center));
}

[TearDown]
public void TearDown()
{
if (TestContext.CurrentContext.Result.Outcome.Status == NUnit.Framework.Interfaces.TestStatus.Failed)
{
VisualizationPrinter.Print(TestContext.CurrentContext.Test.Name, layouter.Rectangles, center);
}
}

[Test]
public void PutNextRectangle_ReturnsRectangleOfRequestedSize()
{
var size = new Size(30, 20);
var rect = layouter.PutNextRectangle(size);
rect.Size.Should().Be(size);
}

[Test]
public void PutNextRectangle_IsCentered_WhenPutsFirstRectagle()
{
var size = new Size(50, 30);
var rect = layouter.PutNextRectangle(size);
var rectCenter = new Point(rect.Left + rect.Width / 2, rect.Top + rect.Height / 2);
rectCenter.Should().Be(center);
}

[Test]
public void PutManyRectangles_DontIntersect()
{
var rnd = new Random(0);
var sizes = Enumerable.Range(0, 100).Select(i => new Size(rnd.Next(20, 60), rnd.Next(10, 40))).ToArray();
foreach (var s in sizes)
layouter.PutNextRectangle(s);
var rects = layouter.Rectangles;
for (int i = 0; i < rects.Count; i++)
for (int j = i + 1; j < rects.Count; j++)
rects[i].IntersectsWith(rects[j]).Should().BeFalse();
}

[Test]
public void PutManyRectangles_CloudIsApproximatelyCircular()
{
var rnd = new Random(1);
var sizes = Enumerable.Range(0, 150).Select(i => new Size(rnd.Next(10, 50), rnd.Next(8, 40))).ToArray();
foreach (var s in sizes)
layouter.PutNextRectangle(s);
var rects = layouter.Rectangles;
var left = rects.Min(r => r.Left);
var right = rects.Max(r => r.Right);
var top = rects.Min(r => r.Top);
var bottom = rects.Max(r => r.Bottom);
var width = right - left;
var height = bottom - top;
var ratio = width / (double)height;
ratio.Should().BeInRange(0.6, 1.7);
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions TagsCloud/ConsoleClient/ConsoleApplication.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using TagsCloud.Interfaces;
using System;
using CommandLine;
using Autofac;
namespace TagsCloud.ConsoleClient;

public sealed class ConsoleApplication
{
public int Run(string[] args)
{
return Parser.Default.ParseArguments<Options>(args)
.MapResult(
RunWithOptions,
_ => 1);
}

private int RunWithOptions(Options options)
{
var container = AppModule.Build(options);

using (container)
{
var generator = container.Resolve<ITagCloudGenerator>();
var result = generator.Generate(
options.Output,
options.Width,
options.Height,
options.Font);
if (result.IsFailure)
{
Console.WriteLine(result.Error);
}
}

return 0;
}
}
39 changes: 39 additions & 0 deletions TagsCloud/ConsoleClient/Options.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using CommandLine;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TagsCloud.ConsoleClient
{
public sealed class Options
{
[Option('i', "input", Required = true)]
public string Input { get; set; }

[Option('s', "stopwords", Required = false, Default = "")]
public string StopWords { get; set; }

[Option('o', "output", Required = false, Default = "visualizations/cloud.png")]
public string Output { get; set; }

[Option("width", Required = false, Default = 0)]
public int Width { get; set; }

[Option("height", Required = false, Default = 0)]
public int Height { get; set; }

[Option("font", Required = false, Default = "GenericSansSerif")]
public string Font { get; set; }

[Option("min-font", Required = false, Default = 50f)]
public float MinFont { get; set; }

[Option("max-font", Required = false, Default = 240f)]
public float MaxFont { get; set; }

[Option("format", Required = false, Default = "png")]
public string Format { get; set; }
}
}
Loading