Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Брозовский Максим #250

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2038a71
Basic setup of TagsCloudVisualization project
BMV989 Nov 12, 2024
f2196bf
Added SpiralCloudLayouter.cs, DefaultPointsDistributor.cs with tests
BMV989 Nov 13, 2024
f4ee9e4
added some more tests
BMV989 Nov 13, 2024
58bc738
small naming fixes
BMV989 Nov 13, 2024
38fdceb
small refactoring of CircularLayouter and SpiralPointsGenerator with …
BMV989 Nov 17, 2024
0f7a10d
add Visualizer and pictures
BMV989 Nov 17, 2024
8ff9eb8
small refactoring of Visualizer
BMV989 Nov 17, 2024
0e12a6d
small refactoring of CircularCloudLayouter
BMV989 Nov 17, 2024
eb5be16
added more tests for CircularCloudLayouterTest
BMV989 Nov 17, 2024
009dc35
added tests for RandomExtension
BMV989 Nov 17, 2024
3090cdb
added test for SpiralPointsGeneratorTest
BMV989 Nov 17, 2024
6940721
fix README.md
BMV989 Nov 17, 2024
04abe31
small naming fixes in CircularCloudLayouterTest
BMV989 Nov 17, 2024
ba4e5f7
small fix in CircularCloudLayouterTest
BMV989 Nov 17, 2024
2a33e20
added 3rd task with Saver and small refactoring
BMV989 Nov 17, 2024
ff64502
refactor of Program
BMV989 Nov 17, 2024
9236bae
added tests for RandomExtensionTest
BMV989 Nov 17, 2024
71ae050
small fix in RandomExtensionTest
BMV989 Nov 17, 2024
f2bceec
renaming of Visualizer to TagCloudVisualizer
BMV989 Nov 18, 2024
e6c398f
refactoring of CircularCloudLayouter with its test
BMV989 Nov 18, 2024
c2cb66f
using SkiaSharp everywhere
BMV989 Nov 18, 2024
8a64693
small refactoring of CircularCloudLayouter
BMV989 Nov 18, 2024
5832415
refactoring of CircularCloudLayouterTest
BMV989 Nov 18, 2024
f71a890
another small refactoring of CircularCloudLayouterTest
BMV989 Nov 18, 2024
4f257ec
small refactoring of TagCloudVisualizer
BMV989 Nov 18, 2024
c74256f
migrate to SkiaSharp in RandomExtensionTest
BMV989 Nov 18, 2024
5c25936
migrate to Random.Shared
BMV989 Nov 18, 2024
d7eda0b
added background and re-rendered pictures
BMV989 Nov 18, 2024
6a2f944
remove unnecessary package
BMV989 Nov 18, 2024
1511372
refactor one of the tests in CircularCloudLayouterTest
BMV989 Nov 18, 2024
87ebcc2
small fixes of CircularCloudLayouterTest
BMV989 Nov 18, 2024
8a67c73
refactor Saver
BMV989 Nov 18, 2024
d18367e
small fix in CircularCloudLayouterTest
BMV989 Nov 18, 2024
0b1de9a
small fixes
BMV989 Nov 18, 2024
79f342d
migrate to List
BMV989 Nov 18, 2024
687a820
At last!
BMV989 Nov 18, 2024
901ffe5
small fix in CircularCloudLayouterTest
BMV989 Nov 18, 2024
b8ce60c
another small fix
BMV989 Nov 18, 2024
cdf1d62
small cleanup
BMV989 Nov 18, 2024
dbd9352
remove useless test in RandomExtensionTest
BMV989 Nov 18, 2024
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
39 changes: 39 additions & 0 deletions cs/TagsCloudVisualization/CircularCloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using SkiaSharp;

namespace TagsCloudVisualization;

public class CircularCloudLayouter(SKPoint center) : ICloudLayouter
{
private const double OptimalRadius = 1;
private const double OptimalAngleOffset = 0.5;

private readonly List<SKRect> rectangles = new();
private readonly SpiralPointsGenerator pointsGenerator = new(center, OptimalRadius, OptimalAngleOffset);

public SKPoint Center => center;
public IEnumerable<SKRect> Rectangles => rectangles;

public SKRect PutNextRectangle(SKSize rectangleSize)
{
while (true)
{
var rectanglePosition = pointsGenerator.GetNextPoint();
var rectangle = CreateRectangleWithCenter(rectanglePosition, rectangleSize);

if (rectangles.Any(rectangle.IntersectsWith)) continue;

rectangles.Add(rectangle);

return rectangle;
}
}

private static SKRect CreateRectangleWithCenter(SKPoint center, SKSize rectangleSize)
{
var left = center.X - rectangleSize.Width / 2;
var top = center.Y - rectangleSize.Height / 2;

return new SKRect(left, top, left + rectangleSize.Width, top + rectangleSize.Height);
}
}

8 changes: 8 additions & 0 deletions cs/TagsCloudVisualization/ICloudLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using SkiaSharp;

namespace TagsCloudVisualization;

public interface ICloudLayouter
{
public SKRect PutNextRectangle(SKSize rectangleSize);
}
8 changes: 8 additions & 0 deletions cs/TagsCloudVisualization/IPointsGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using SkiaSharp;

namespace TagsCloudVisualization;

public interface IPointsGenerator
{
public SKPoint GetNextPoint();
}
30 changes: 30 additions & 0 deletions cs/TagsCloudVisualization/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using SkiaSharp;

namespace TagsCloudVisualization;

class Program
{
private const int ImageWidth = 2560;
private const int ImageHeight = 1440;

private const int NumberOfRectangles = 50;
private const int MinRectangleSize = 10;
private const int MaxRectangleSize = 50;

private const string ImageDirectory = "../../../imgs";
public static void Main(string[] args)
{
var center = new SKPoint(ImageWidth / 2f, ImageHeight / 2f);
var cloudLayouter = new CircularCloudLayouter(center);
var rectangles = Enumerable
.Range(0, NumberOfRectangles)
.Select(_ =>
cloudLayouter.PutNextRectangle(Random.Shared.NextSkSize(MinRectangleSize, MaxRectangleSize)))
.ToList();

var visualizer = new TagCloudVisualizer(ImageWidth, ImageHeight);
var bitmap = visualizer.Visualize(rectangles);

TagCloudSaver.SaveAsPng(bitmap, Path.Combine(ImageDirectory,$"{NumberOfRectangles}_TagCloud"));
}
}
8 changes: 8 additions & 0 deletions cs/TagsCloudVisualization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
### 50 прямоугольников
<img src="imgs/50_TagCloud.png">

### 100 прямоугольников
<img src="imgs/100_TagCloud.png">

### 1000 прямоугольников
<img src="imgs/1000_TagCloud.png">
19 changes: 19 additions & 0 deletions cs/TagsCloudVisualization/RandomExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using SkiaSharp;

namespace TagsCloudVisualization;

public static class RandomExtension
{
public static SKSize NextSkSize(this Random random, int minValue, int maxValue)
{
if (minValue <= 0)
throw new ArgumentOutOfRangeException(nameof(minValue), "minValue must be greater than 0");
if (maxValue < minValue)
throw new ArgumentOutOfRangeException(nameof(maxValue), "maxValue must be greater than minValue");

return new SKSize(random.Next(minValue, maxValue), random.Next(minValue, maxValue));
}

public static SKPoint NextSkPoint(this Random random, int minValue, int maxValue) =>
new (random.Next(minValue, maxValue), random.Next(minValue, maxValue));
}
41 changes: 41 additions & 0 deletions cs/TagsCloudVisualization/SpiralPointsGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using SkiaSharp;

namespace TagsCloudVisualization;

public class SpiralPointsGenerator : IPointsGenerator
{
private readonly double angleOffset;
private readonly double radius;
private readonly SKPoint start;
private double angle;

public SpiralPointsGenerator(SKPoint start, double radius, double angleOffset)
{
if (radius <= 0)
throw new ArgumentException("radius must be greater than 0");
if (angleOffset <= 0)
throw new ArgumentException("angleOffset must be greater than 0");

this.angleOffset = angleOffset * Math.PI / 180;
this.radius = radius;
this.start = start;
}

public SKPoint GetNextPoint()
{
var nextPoint = GetPointByPolarCords();
angle += angleOffset;
return nextPoint;
}

private SKPoint GetPointByPolarCords()
{
var offsetPerRadian = radius / (2 * Math.PI);
var radiusVector = offsetPerRadian * angle;

var x = (int)Math.Round(radiusVector * Math.Cos(angle) + start.X);
var y = (int)Math.Round(radiusVector * Math.Sin(angle) + start.Y);

return new SKPoint(x, y);
}
}
15 changes: 15 additions & 0 deletions cs/TagsCloudVisualization/TagCloudSaver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using SkiaSharp;

namespace TagsCloudVisualization;

public static class TagCloudSaver
{
private const int ImageQuality = 80;

public static void SaveAsPng(SKBitmap bitmap, string filePath)
{
Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
using var file = File.OpenWrite($"{filePath}.png");
bitmap.Encode(SKEncodedImageFormat.Png, ImageQuality).SaveTo(file);
}
}
42 changes: 42 additions & 0 deletions cs/TagsCloudVisualization/TagCloudVisualizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using SkiaSharp;


namespace TagsCloudVisualization;

public class TagCloudVisualizer(int width, int height)
{
public SKBitmap Visualize(List<SKRect> rectangles)
{
var layoutSize = GetLayoutSize(rectangles);
var bimapWidth = Math.Max(width, layoutSize.Width) * 2 ;
var bimapHeight = Math.Max(height, layoutSize.Height) * 2;
var bitmap = new SKBitmap(bimapWidth, bimapHeight);
var canvas = new SKCanvas(bitmap);
var paint = new SKPaint
{
Color = SKColors.Black,
Style = SKPaintStyle.Stroke
};

canvas.Clear(SKColors.White);

var xOffset = bitmap.Width / 2f - rectangles.First().Location.X;
var yOffset = bitmap.Height / 2f - rectangles.First().Location.Y;

foreach (var rectangle in rectangles)
{
rectangle.Offset(xOffset, yOffset);
canvas.DrawRect(rectangle, paint);
}

return bitmap;
}

private SKSizeI GetLayoutSize(List<SKRect> rectangles)
{
var layoutWidth = rectangles.Max(r => r.Right) - rectangles.Min(r => r.Left);
var layoutHeight = rectangles.Max(r => r.Top) - rectangles.Min(r => r.Bottom);

return new SKSize(layoutWidth, layoutHeight).ToSizeI();
}
}
14 changes: 14 additions & 0 deletions cs/TagsCloudVisualization/TagsCloudVisualization.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="SkiaSharp" Version="3.118.0-preview.1.2" />
</ItemGroup>

</Project>
Binary file added cs/TagsCloudVisualization/imgs/1000_TagCloud.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cs/TagsCloudVisualization/imgs/100_TagCloud.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cs/TagsCloudVisualization/imgs/50_TagCloud.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
110 changes: 110 additions & 0 deletions cs/TagsCloudVisualizationTests/CircularCloudLayouterTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using FluentAssertions;
using NUnit.Framework.Interfaces;
using SkiaSharp;
using TagsCloudVisualization;

namespace TagsCloudVisualizationTests;

[TestFixture]
[TestOf(typeof(CircularCloudLayouter))]
public class CircularCloudLayouterTest
{
private const string ImagesDirectory = "../../../failedTests";
private CircularCloudLayouter circularCloudLayouter;

[SetUp]
public void Setup()
{
circularCloudLayouter = new CircularCloudLayouter(new SKPoint(0, 0));
}

[TearDown]
public void TearDown()
{
var currentContext = TestContext.CurrentContext;
if (currentContext.Result.Outcome.Status != TestStatus.Failed) return;

var layoutSize = GetLayoutSize(circularCloudLayouter.Rectangles.ToList());
var visualizer = new TagCloudVisualizer((int)layoutSize.Width, (int)layoutSize.Height);
var bitmap = visualizer.Visualize(circularCloudLayouter.Rectangles.ToList());

var pathToFile = Path.Combine(ImagesDirectory, currentContext.Test.Name);
TagCloudSaver.SaveAsPng(bitmap, pathToFile);

TestContext.Out.WriteLine($"Tag cloud visualization saved to file {pathToFile}.png");
}

[Test]
public void PutNextRectangle_ShouldReturnRectangleAtCenter_WhenFirstInvoked()
{
var rectSize = Random.Shared.NextSkSize(1, int.MaxValue);

var actualRect = circularCloudLayouter.PutNextRectangle(rectSize);

var expectedRect = new SKRect(
circularCloudLayouter.Center.X - rectSize.Width / 2,
circularCloudLayouter.Center.Y - rectSize.Height / 2,
circularCloudLayouter.Center.X + rectSize.Width / 2,
circularCloudLayouter.Center.Y + rectSize.Height / 2);

actualRect.Should().BeEquivalentTo(expectedRect);
}

[Test]
public void PutNextRectangle_ShouldReturnRectangle_WithCorrectSize()
{
var rectangleSize = Random.Shared.NextSkSize(1, int.MaxValue);

var actualRectangle = circularCloudLayouter.PutNextRectangle(rectangleSize);

actualRectangle.Size.Should().Be(rectangleSize);
}

[Test]
public void PutNextRectangle_ShouldReturnRectangles_WithoutIntersections()
{
var numberOfRectangles = Random.Shared.Next(100, 300);

var rectangles = Enumerable
.Range(0, numberOfRectangles)
.Select(_ => circularCloudLayouter.PutNextRectangle(Random.Shared.NextSkSize(10, 27)))
.ToList();

bool IsIntersectionBetweenRectangles(SKRect rect) =>
rectangles.Any(otherRect => rect != otherRect && rect.IntersectsWith(otherRect));

rectangles.Any(IsIntersectionBetweenRectangles)
.Should().BeFalse();
}

[Test]
public void GeneratedLayout_ShouldHaveHighTightnessAndShapeOfCircularCloud()
{
const double eps = 0.35;
var rectangles = PutRandomRectanglesInLayouter(Random.Shared.Next(500, 1000));
var layoutSize = GetLayoutSize(rectangles);

var diameterOfCircle = Math.Max(layoutSize.Width, layoutSize.Height);
var areaOfCircle = Math.PI * Math.Pow(diameterOfCircle, 2) / 4;
var areaOfRectangles = (double)rectangles
.Select(r => r.Height * r.Width)
.Sum();
var areaRatio = areaOfCircle / areaOfRectangles;

areaRatio.Should().BeApproximately(1, eps);
}

private List<SKRect> PutRandomRectanglesInLayouter(int numberOfRectangles) =>
Enumerable
.Range(0, numberOfRectangles)
.Select(_ => circularCloudLayouter.PutNextRectangle(Random.Shared.NextSkSize(10, 27)))
.ToList();

private SKSize GetLayoutSize(List<SKRect> rectangles)
{
var layoutWidth = rectangles.Max(r => r.Right) - rectangles.Min(r => r.Left);
var layoutHeight = rectangles.Max(r => r.Top) - rectangles.Min(r => r.Bottom);

return new SKSize(layoutWidth, layoutHeight);
}
}
Loading