-
Notifications
You must be signed in to change notification settings - Fork 303
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
base: master
Are you sure you want to change the base?
Холстинин Егор #190
Changes from 11 commits
9edd430
b44c2b2
2d7478c
98269c6
b4aac5f
f0e45c9
7522b53
d3d0d51
491567f
d7d051b
3485b50
8d308b8
7505905
4c45b2c
5c8dff7
b3fbaa8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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}"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
namespace TagsCloudVisualization; | ||
|
||
public interface IDullWordChecker | ||
{ | ||
public bool Check(WordAnalysis word); | ||
} |
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()); | ||
} | ||
} |
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); | ||
} |
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; | ||
} | ||
} |
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(); | ||
} |
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; | ||
} | ||
} |
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); | ||
} | ||
} |
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); | ||
} | ||
} |
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() | ||
{ | ||
var services = new ServiceCollection(); | ||
services.AddTransient<Font>(x => new Font(FontFamily, FontSize)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ладно, здесь немного со скоупом вышел перебор, пожалуй. |
||
services.AddTransient<IPalette>(x => new Palette(TextColor, BackgroundColor)); | ||
if (SqareAlgorithm) | ||
services.AddTransient<IPointGenerator, LissajousCurvePointGenerator>(); | ||
else | ||
services.AddTransient<IPointGenerator, SpiralPointGenerator>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Вот это тоже плохо выглядит, но не смог нагуглить как привязывать сервис в зависимости от условия в microsoft dependency injection There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. В целом, нет такой вещи, как зарегистрировать или не зарегистрировать в зависимости от условия. |
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Сейчас почти все сервисы добавлены синглтоном. Вопрос - почему?)
По умолчанию, нужно начинать с самого маленького скоупа, понимать, подходит ли он, если нет, подумать над большим скоупом, пока не станет ясно, какой подходит. А здесь ситуация обратная - решили сразу поставить максимальный скоуп, хотя каких-то предпосылок я здесь не вижу. Это как по умолчанию все переменные делать глобальными.