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

Холстинин Егор #190

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
22 changes: 22 additions & 0 deletions TagsCloudVisualization/BitmapExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Drawing;
using System.Drawing.Imaging;

namespace TagsCloudVisualization;

public static class BitmapExtensions
{
public static void SaveImage(this Bitmap bitmap, string outputFilePath, ImageFormat imageFormat)
{
outputFilePath = Path.GetFullPath(outputFilePath);
var outputFileName = Path.GetFileName(outputFilePath);
var outputFileDirectory = Path.GetDirectoryName(outputFilePath);

Directory.CreateDirectory(outputFileDirectory);

var savePath = Path.Combine(outputFileDirectory, $"{outputFileName}.{imageFormat.ToString().ToLower()}");

bitmap.Save(savePath, imageFormat);

Console.WriteLine($"Image is saved to {savePath}");
}
}
6 changes: 6 additions & 0 deletions TagsCloudVisualization/DullCheckers/IDullWordChecker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace TagsCloudVisualization;

public interface IDullWordChecker
{
public bool Check(WordAnalysis word);
}
33 changes: 33 additions & 0 deletions TagsCloudVisualization/DullCheckers/MystemDullWordChecker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Text;

namespace TagsCloudVisualization;

public class MystemDullWordChecker : IDullWordChecker
{
private HashSet<string> removedPartOfSpeech;
private HashSet<string> excludedWords = new();

public MystemDullWordChecker(HashSet<string> removedPartOfSpeech, string? excludedWordsFile)
{
this.removedPartOfSpeech = removedPartOfSpeech;
if (excludedWordsFile is null)
return;

try
{
excludedWords =
new HashSet<string>(File.ReadAllText(excludedWordsFile, Encoding.UTF8).Split(Environment.NewLine));
}
catch (FileNotFoundException e)
{
Console.WriteLine($"Could not find specified excluded words file {excludedWordsFile}. " +
$"No words will be excluded.");
}
}

public bool Check(WordAnalysis wordAnalysis)
{
return removedPartOfSpeech.Any(dullPart => wordAnalysis.GrammarAnalysis.StartsWith(dullPart))
|| excludedWords.Contains(wordAnalysis.Lexema.ToLower());
}
}
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.
9 changes: 9 additions & 0 deletions TagsCloudVisualization/IRectangleLayouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Drawing;

namespace TagsCloudVisualization;

public interface IRectangleLayouter
{
public Rectangle PutNextRectangle(Size rectangleSize);
public Rectangle PutNextRectangle(SizeF rectangleSize);
}
57 changes: 57 additions & 0 deletions TagsCloudVisualization/LayoutDrawer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Drawing.Imaging;
using System.Drawing;

namespace TagsCloudVisualization;

public class LayoutDrawer
{
private IInterestingWordsParser interestingWordsParser;
private IRectangleLayouter rectangleLayouter;
private IPalette palette;
private Font font;

public LayoutDrawer(IInterestingWordsParser interestingWordsParser,
IRectangleLayouter rectangleLayouter,
IPalette palette,
Font font)
{
this.interestingWordsParser = interestingWordsParser;
this.rectangleLayouter = rectangleLayouter;
this.palette = palette;
this.font = font;
}

public Bitmap CreateLayoutImageFromFile(string inputFilePath,
Size imageSize,
int minimumFontSize)
{
var bitmap = new Bitmap(imageSize.Width, imageSize.Height);
using var graphics = Graphics.FromImage(bitmap);

inputFilePath = Path.GetFullPath(inputFilePath);

var sortedWordsCount = interestingWordsParser.GetInterestingWords(inputFilePath)
.GroupBy(s => s)
.Select(group => new { Word = group.Key, Count = group.Count() })
.OrderByDescending(wordCount => wordCount.Count);
var mostWordOccurrencies = sortedWordsCount.Max(arg => arg.Count);

graphics.Clear(palette.GetBackgroundColor());

foreach (var wordCount in sortedWordsCount)
{
var rectangleFont = new Font(font.FontFamily,
Math.Max(font.Size * wordCount.Count / mostWordOccurrencies, minimumFontSize));
var rectangleSize = graphics.MeasureString(wordCount.Word, rectangleFont);

var textRectangle = rectangleLayouter.PutNextRectangle(rectangleSize);
var x = textRectangle.X + imageSize.Width / 2;
var y = textRectangle.Y + imageSize.Height / 2;

using var brush = new SolidBrush(palette.GetNextWordColor());
graphics.DrawString(wordCount.Word, rectangleFont, brush, x, y);
}

return bitmap;
}
}
10 changes: 10 additions & 0 deletions TagsCloudVisualization/Palletes/IPalette.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Drawing;

namespace TagsCloudVisualization;

public interface IPalette
{
public Color GetNextWordColor();

public Color GetBackgroundColor();
}
27 changes: 27 additions & 0 deletions TagsCloudVisualization/Palletes/Palette.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Drawing;

namespace TagsCloudVisualization;

public class Palette : IPalette
{
public Palette(Color[] textColor, Color backgroundColor)
{
TextColor = textColor;
BackgroundColor = backgroundColor;
}

private Color[] TextColor { get; set; }
private int currentColorId = 0;
private Color BackgroundColor { get; set; }

public Color GetNextWordColor()
{
if (currentColorId >= TextColor.Length) currentColorId = 0;
return TextColor[currentColorId++];
}

public Color GetBackgroundColor()
{
return BackgroundColor;
}
}
8 changes: 8 additions & 0 deletions TagsCloudVisualization/PointGenerators/IPointGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Drawing;

namespace TagsCloudVisualization;

public interface IPointGenerator
{
Point GetNextPoint();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Drawing;

namespace TagsCloudVisualization;

public class LissajousCurvePointGenerator : IPointGenerator
{
private int xAmplitude = 100;
private int yAmplitude = 100;
private int xConstant = 19;
private int yConstant = 20;
private double delta = Math.PI / 2;
private double parameter = 0;

public Point GetNextPoint()
{
parameter += 0.01;
var x = Math.Round(xAmplitude * Math.Sin(xConstant * parameter + delta));
var y = Math.Round(yAmplitude * Math.Sin(yConstant * parameter));

if (parameter > 20)
{
parameter = 0;
xAmplitude += 20;
yAmplitude += 20;
}

return new Point((int)x, (int)y);
}
}
26 changes: 26 additions & 0 deletions TagsCloudVisualization/PointGenerators/SpiralPointGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Drawing;

namespace TagsCloudVisualization;

public class SpiralPointGenerator : IPointGenerator
{
public Point Center { get; } = new(0, 0);
public int Radius { get; private set; }
public double Angle { get; private set; }
public int RadiusDelta { get; private set; } = 1;
public double AngleDelta { get; private set; } = Math.PI / 60;

public Point GetNextPoint()
{
var x = (int)Math.Round(Center.X + Radius * Math.Cos(Angle));
var y = (int)Math.Round(Center.Y + Radius * Math.Sin(Angle));

var nextAngle = Angle + AngleDelta;
var angleMoreThan2Pi = Math.Abs(nextAngle) >= Math.PI * 2;

Radius = angleMoreThan2Pi ? Radius + RadiusDelta : Radius;
Angle = angleMoreThan2Pi ? 0 : nextAngle;

return new Point(x, y);
}
}
99 changes: 99 additions & 0 deletions TagsCloudVisualization/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.ComponentModel.DataAnnotations;
using System.Drawing;
using System.Drawing.Imaging;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.DependencyInjection;

namespace TagsCloudVisualization;

public class Program
{
public static int Main(string[] args) => CommandLineApplication.Execute<Program>(args);

[Argument(0, Description = "Path to txt file with words")]
[Required(ErrorMessage = "Expected to get path to file with words as first positional argument." +
"\nExample: C:\\PathTo\\File.txt\nOr relative to exe: PathTo\\File.txt")]
public string InputFilePath { get; set; }

[Argument(1, Description = "Path to output file")]
[Required(ErrorMessage = "Expected to get output file path as second positional argument." +
"\nExample: C:\\PathTo\\File\nOr relative to exe: PathTo\\File")]
public string OutputFilePath { get; set; }

[Option("-w", Description = "Image width in pixels")]
private int ImageWidth { get; set; } = 1000;

[Option("-h", Description = "Image height in pixels")]
private int ImageHeight { get; set; } = 1000;

[Option("-bc", Description = "Image background color from KnownColor enum")]
private Color BackgroundColor { get; set; } = Color.Wheat;

[Option("-tc", Description = "Image words colors sequence array from KnownColor enum. " +
"Can be set multiple times for sequence. Example: -tc black -tc white")]
private Color[] TextColor { get; set; } = { Color.Black };

[Option("-ff", Description = "Font used for words")]
private string FontFamily { get; set; } = "Arial";

[Option("-fs", Description = "Max font size in em")]
private int FontSize { get; set; } = 50;

[Option("-mfs", Description = "Min font size in em")]
private int MinimalFontSize { get; set; } = 0;

[Option("-img", Description = "Output image format. Choosen from ImageFormat")]
private ImageFormat SaveImageFormat { get; set; } = ImageFormat.Png;

[Option("-ef", Description = "Txt file with words to exclude. 1 word in line. Words must be lexems.")]
private string ExcludedWordsFile { get; set; }

[Option("-rp", Description = "Parts of speech abbreviations that are excluded from parsed words. " +
"More info here https://yandex.ru/dev/mystem/doc/ru/grammemes-values")]
private HashSet<string> RemovedPartsOfSpeech { get; set; } = new()
{ "ADVPRO", "APRO", "INTJ", "CONJ", "PART", "PR", "SPRO" };

[Option("-square", Description = "Will use another algorithm to generate square tag cloud instead of circular.")]
private bool SqareAlgorithm { get; set; }


private void OnExecute()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сейчас почти все сервисы добавлены синглтоном. Вопрос - почему?)
По умолчанию, нужно начинать с самого маленького скоупа, понимать, подходит ли он, если нет, подумать над большим скоупом, пока не станет ясно, какой подходит. А здесь ситуация обратная - решили сразу поставить максимальный скоуп, хотя каких-то предпосылок я здесь не вижу. Это как по умолчанию все переменные делать глобальными.

{
var services = new ServiceCollection();
services.AddTransient<Font>(x => new Font(FontFamily, FontSize));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ладно, здесь немного со скоупом вышел перебор, пожалуй.
Вообще, по дефолту используют Scoped.

services.AddTransient<IPalette>(x => new Palette(TextColor, BackgroundColor));
if (SqareAlgorithm)
services.AddTransient<IPointGenerator, LissajousCurvePointGenerator>();
else
services.AddTransient<IPointGenerator, SpiralPointGenerator>();
Copy link
Author

@Yrwlcm Yrwlcm Jan 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вот это тоже плохо выглядит, но не смог нагуглить как привязывать сервис в зависимости от условия в microsoft dependency injection

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В целом, нет такой вещи, как зарегистрировать или не зарегистрировать в зависимости от условия.
Тебе надо зарегистрировать PointGenerator'ы все разом, а потом в отдельном классе ты их принимаешь массивом и производишь выбор в зависимости от значения параметра.

services.AddTransient<IDullWordChecker>(x =>
new MystemDullWordChecker(RemovedPartsOfSpeech, ExcludedWordsFile));
services.AddTransient<IInterestingWordsParser, MystemWordsParser>();
services.AddTransient<IRectangleLayouter, RectangleLayouter>();
services.AddTransient<LayoutDrawer>();

using var provider = services.BuildServiceProvider();

var layoutDrawer = provider.GetRequiredService<LayoutDrawer>();
try
{
layoutDrawer
.CreateLayoutImageFromFile(InputFilePath, new Size(ImageWidth, ImageHeight), MinimalFontSize)
.SaveImage(OutputFilePath, SaveImageFormat);
}
Comment on lines +77 to +81
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не уверен насколько сейчас рационально выносить эти поля в TagLayoutSettings, так как сейчас мы не прячем ничего внутрь метода

catch (Exception ex)
{
if (ex is FileNotFoundException or DirectoryNotFoundException)
{
Console.WriteLine(ex.Message);
if (!Path.IsPathRooted(InputFilePath))
Console.WriteLine("Relative paths are searched realative to .exe file. " +
"Try giving an absolute path.");
}
else
{
throw;
}
}
}
}
Loading