diff --git a/TagsCloudVisualization/App/ConsoleApp.cs b/TagsCloudVisualization/App/ConsoleApp.cs new file mode 100644 index 00000000..ba7cd84a --- /dev/null +++ b/TagsCloudVisualization/App/ConsoleApp.cs @@ -0,0 +1,36 @@ +using TagsCloudVisualization.ConsoleCommands; +using TagsCloudVisualization.FileReaders; +using TagsCloudVisualization.Layouters; +using TagsCloudVisualization.Renderers; +using TagsCloudVisualization.WordPreprocessors; + +namespace TagsCloudVisualization.App; + +public class ConsoleApp : IApp +{ + private readonly IWordPreprocessor wordPreprocessor; + private readonly FileReaderFactory fileReaderFactory; + private readonly ICloudLayouter cloudLayouter; + private readonly ICloudRenderer cloudRenderer; + private readonly string inputFilePath; + + public ConsoleApp(IWordPreprocessor wordPreprocessor, + FileReaderFactory fileReaderFactory, + ICloudLayouter cloudLayouter, + ICloudRenderer cloudRenderer, + Options options) + { + this.wordPreprocessor = wordPreprocessor; + this.fileReaderFactory = fileReaderFactory; + this.cloudLayouter = cloudLayouter; + this.cloudRenderer = cloudRenderer; + inputFilePath = options.InputFilePath; + } + + public void Run() + { + var text = fileReaderFactory.GetFileReader(inputFilePath).Read(inputFilePath); + var words = wordPreprocessor.ProcessTextToWords(text); + cloudRenderer.Render(cloudLayouter.CreateTagsCloud(words)); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/App/IApp.cs b/TagsCloudVisualization/App/IApp.cs new file mode 100644 index 00000000..8ea41fb6 --- /dev/null +++ b/TagsCloudVisualization/App/IApp.cs @@ -0,0 +1,6 @@ +namespace TagsCloudVisualization.App; + +public interface IApp +{ + public void Run(); +} \ No newline at end of file diff --git a/TagsCloudVisualization/BitmapProcessors/BitmapProcessorFactory.cs b/TagsCloudVisualization/BitmapProcessors/BitmapProcessorFactory.cs new file mode 100644 index 00000000..53b9dfc5 --- /dev/null +++ b/TagsCloudVisualization/BitmapProcessors/BitmapProcessorFactory.cs @@ -0,0 +1,20 @@ +using Autofac; +using TagsCloudVisualization.Enums; + +namespace TagsCloudVisualization.BitmapProcessors; + +public class BitmapProcessorFactory +{ + private readonly IComponentContext context; + + public BitmapProcessorFactory(IComponentContext context) + => this.context = context; + + public IBitmapProcessor GetBitmapProcessor(OutputImageFormat option) + { + if (context.IsRegisteredWithKey(option)) + return context.ResolveKeyed(option); + + throw new NotSupportedException($"Image format {option.ToString()} is not supported."); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/BitmapProcessors/DefaultBitmapProcessor.cs b/TagsCloudVisualization/BitmapProcessors/DefaultBitmapProcessor.cs new file mode 100644 index 00000000..42b5be48 --- /dev/null +++ b/TagsCloudVisualization/BitmapProcessors/DefaultBitmapProcessor.cs @@ -0,0 +1,19 @@ +using System.Drawing; +using TagsCloudVisualization.ConsoleCommands; +using TagsCloudVisualization.Enums; + +namespace TagsCloudVisualization.BitmapProcessors; + +public class DefaultBitmapProcessor : IBitmapProcessor +{ + private readonly OutputImageFormat imageFormat; + + public DefaultBitmapProcessor(Options options) + => imageFormat = options.ImageFormat; + + public void SaveImage(Bitmap bitmap, string imageDirectory, string imageName) + { + var filePath = Path.Combine(imageDirectory, $"{imageName}.{imageFormat.ToString().ToLowerInvariant()}"); + bitmap.Save(filePath); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/BitmapProcessors/IBitmapProcessor.cs b/TagsCloudVisualization/BitmapProcessors/IBitmapProcessor.cs new file mode 100644 index 00000000..9bf28afc --- /dev/null +++ b/TagsCloudVisualization/BitmapProcessors/IBitmapProcessor.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.BitmapProcessors; + +public interface IBitmapProcessor +{ + public void SaveImage(Bitmap bitmap, string imageDirectory, string imageName); +} \ No newline at end of file diff --git a/TagsCloudVisualization/BitmapProcessors/PdfBitmapProcessor.cs b/TagsCloudVisualization/BitmapProcessors/PdfBitmapProcessor.cs new file mode 100644 index 00000000..a50f55d0 --- /dev/null +++ b/TagsCloudVisualization/BitmapProcessors/PdfBitmapProcessor.cs @@ -0,0 +1,26 @@ +using System.Drawing; +using System.Drawing.Imaging; +using PdfSharp.Drawing; +using PdfSharp.Pdf; + +namespace TagsCloudVisualization.BitmapProcessors; + +public class PdfBitmapProcessor : IBitmapProcessor +{ + public void SaveImage(Bitmap bitmap, string imageDirectory, string imageName) + { + using var memoryStream = new MemoryStream(); + bitmap.Save(memoryStream, ImageFormat.Png); + memoryStream.Position = 0; + + var document = new PdfDocument(); + var page = document.AddPage(); + var gfx = XGraphics.FromPdfPage(page); + + var image = XImage.FromStream(memoryStream); + gfx.DrawImage(image, 0, 0, page.Width, page.Height); + + var pdfPath = Path.Combine(imageDirectory, $"{imageName}.pdf"); + document.Save(pdfPath); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/ConsoleCommands/Options.cs b/TagsCloudVisualization/ConsoleCommands/Options.cs new file mode 100644 index 00000000..e21486f4 --- /dev/null +++ b/TagsCloudVisualization/ConsoleCommands/Options.cs @@ -0,0 +1,48 @@ +using CommandLine; +using TagsCloudVisualization.Enums; + +namespace TagsCloudVisualization.ConsoleCommands; + +public class Options +{ + [Option('i', "inputFilePath", Required = true, + HelpText = "Set path to a file containing words in one column (under one word per row).")] + public string InputFilePath { get; set; } + + [Option('o', "outputDirectory", Required = true, + HelpText = "Set directory for output image.")] + public string OutputDirectory { get; set; } + + [Option('f', "font", Default = "Arial", HelpText = "Set font for tags cloud words.")] + public string TagsFont { get; set; } + + [Option("minFontSize", Default = 5, HelpText = "Set min font size for tags cloud words.")] + public int MinTagsFontSize { get; set; } + + [Option("maxFontSize", Default = 80, HelpText = "Set max font size for tags cloud words.")] + public int MaxTagsFontSize { get; set; } + + [Option('h', "imageHeight", Default = 1080, HelpText = "Set output image height.")] + public int ImageHeight { get; set; } + + [Option('w', "imageWidth", Default = 1920, HelpText = "Set output image width.")] + public int ImageWidth { get; set; } + + [Option("imageFormat", Default = OutputImageFormat.Png, + HelpText = "Set output image format. Possible values: Jpeg, Jpg, Png, Tiff, Bmp, Gif, Pdf.")] + public OutputImageFormat ImageFormat { get; set; } + + [Option('b', "backgroundColor", Default = "Empty", + HelpText = "Set background color for tags cloud. Example : -b white")] + public string BackgroundColor { get; set; } + + [Option("pathToMyStem", Default = null, HelpText = "Set path to mystem.exe.")] + public string PathToMyStem { get; set; } + + [Option("numOfColors", Default = 85, HelpText = "Set number of colors for gradient color generator.")] + public int NumOfColors { get; set; } + + [Option("colorOption", Default = ColorOption.Random, + HelpText = $"Set option of color generator for words. Possible values: Random, Gradient.")] + public ColorOption ColorOption { get; set; } +} \ No newline at end of file diff --git a/TagsCloudVisualization/ContainerConfig.cs b/TagsCloudVisualization/ContainerConfig.cs new file mode 100644 index 00000000..d5aa30b0 --- /dev/null +++ b/TagsCloudVisualization/ContainerConfig.cs @@ -0,0 +1,75 @@ +using Autofac; +using TagsCloudVisualization.App; +using TagsCloudVisualization.BitmapProcessors; +using TagsCloudVisualization.ConsoleCommands; +using TagsCloudVisualization.Distributors; +using TagsCloudVisualization.Enums; +using TagsCloudVisualization.FileReaders; +using TagsCloudVisualization.Layouters; +using TagsCloudVisualization.Layouters.RectangleSizeCalculators; +using TagsCloudVisualization.MyStemWrapper; +using TagsCloudVisualization.Renderers; +using TagsCloudVisualization.Renderers.ColorGenerators; +using TagsCloudVisualization.WordPreprocessors; +using TagsCloudVisualization.WordPreprocessors.FontCreators; +using TagsCloudVisualization.WordPreprocessors.WordValidators; + +namespace TagsCloudVisualization; + +public static class ContainerConfig +{ + public static IContainer Configure(Options options) + { + var builder = new ContainerBuilder(); + builder.RegisterInstance(options).AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + ConfigureFileReaders(builder); + ConfigureColorGenerators(builder); + ConfigureBitmapProcessors(builder); + + var myStem = new MyStem + { + PathToMyStem = options.PathToMyStem ?? Path.GetFullPath("Utilities\\mystem.exe"), + Parameters = "-nig --format json" + }; + builder.RegisterInstance(myStem).AsSelf().SingleInstance(); + + return builder.Build(); + } + + private static void ConfigureFileReaders(ContainerBuilder builder) + { + builder.RegisterType().Keyed(".txt"); + builder.RegisterType().Keyed(".docx"); + builder.RegisterType().Keyed(".doc"); + } + + private static void ConfigureColorGenerators(ContainerBuilder builder) + { + builder.RegisterType().Keyed(ColorOption.Gradient); + builder.RegisterType().Keyed(ColorOption.Random); + } + + private static void ConfigureBitmapProcessors(ContainerBuilder builder) + { + builder.RegisterType() + .Keyed(OutputImageFormat.Jpg) + .Keyed(OutputImageFormat.Png) + .Keyed(OutputImageFormat.Gif) + .Keyed(OutputImageFormat.Tiff) + .Keyed(OutputImageFormat.Bmp) + .Keyed(OutputImageFormat.Jpeg); + builder.RegisterType() + .Keyed(OutputImageFormat.Pdf); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Distributors/ICloudDistribution.cs b/TagsCloudVisualization/Distributors/ICloudDistribution.cs new file mode 100644 index 00000000..a6914621 --- /dev/null +++ b/TagsCloudVisualization/Distributors/ICloudDistribution.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Distributors; + +public interface ICloudDistribution +{ + public Point GetNextPoint(); +} \ No newline at end of file diff --git a/TagsCloudVisualization/Distributors/SpiralDistribution.cs b/TagsCloudVisualization/Distributors/SpiralDistribution.cs new file mode 100644 index 00000000..b10de48b --- /dev/null +++ b/TagsCloudVisualization/Distributors/SpiralDistribution.cs @@ -0,0 +1,28 @@ +using System.Drawing; +using TagsCloudVisualization.ConsoleCommands; + +namespace TagsCloudVisualization.Distributors; + +public class SpiralDistribution(Options options) : ICloudDistribution +{ + private const double AngleStep = 0.02; + private const double RadiusStep = 0.01; + private double angle; + private double radius; + private readonly Point cloudCenter = new(options.ImageWidth / 2, options.ImageHeight / 2); + + public Point GetNextPoint() + { + var nextPoint = ConvertPolarToCartesian(); + angle += AngleStep; + radius += RadiusStep; + return nextPoint; + } + + private Point ConvertPolarToCartesian() + { + var cartesian = new Point((int)(radius * Math.Cos(angle)), (int)(radius * Math.Sin(angle))); + cartesian.Offset(cloudCenter); + return cartesian; + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Domain/Tag.cs b/TagsCloudVisualization/Domain/Tag.cs new file mode 100644 index 00000000..3ad16e9b --- /dev/null +++ b/TagsCloudVisualization/Domain/Tag.cs @@ -0,0 +1,17 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Domain; + +public class Tag +{ + public readonly Rectangle Rectangle; + public readonly Font Font; + public readonly string Content; + + public Tag(Rectangle rectangle, Font font, string content) + { + Rectangle = rectangle; + Font = font; + Content = content; + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Enums/ColorOption.cs b/TagsCloudVisualization/Enums/ColorOption.cs new file mode 100644 index 00000000..9b8b50a0 --- /dev/null +++ b/TagsCloudVisualization/Enums/ColorOption.cs @@ -0,0 +1,7 @@ +namespace TagsCloudVisualization.Enums; + +public enum ColorOption +{ + Random, + Gradient +} \ No newline at end of file diff --git a/TagsCloudVisualization/Enums/OutputImageFormat.cs b/TagsCloudVisualization/Enums/OutputImageFormat.cs new file mode 100644 index 00000000..5c89d4ff --- /dev/null +++ b/TagsCloudVisualization/Enums/OutputImageFormat.cs @@ -0,0 +1,12 @@ +namespace TagsCloudVisualization.Enums; + +public enum OutputImageFormat +{ + Jpeg, + Jpg, + Png, + Gif, + Bmp, + Tiff, + Pdf +} \ No newline at end of file diff --git a/TagsCloudVisualization/FileReaders/DocFileReader.cs b/TagsCloudVisualization/FileReaders/DocFileReader.cs new file mode 100644 index 00000000..4c39b76e --- /dev/null +++ b/TagsCloudVisualization/FileReaders/DocFileReader.cs @@ -0,0 +1,22 @@ +using System.Text; +using Spire.Doc; +using Spire.Doc.Documents; + +namespace TagsCloudVisualization.FileReaders; + +public class DocFileReader : IFileReader +{ + public string Read(string filePath) + { + var text = new StringBuilder(); + var document = new Document(); + + document.LoadFromFile(filePath); + + foreach (Section section in document.Sections) + foreach (Paragraph paragraph in section.Paragraphs) + text.Append(paragraph.Text); + + return text.ToString(); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/FileReaders/DocxFileReader.cs b/TagsCloudVisualization/FileReaders/DocxFileReader.cs new file mode 100644 index 00000000..7ac0c788 --- /dev/null +++ b/TagsCloudVisualization/FileReaders/DocxFileReader.cs @@ -0,0 +1,18 @@ +using System.Text; +using NPOI.XWPF.UserModel; + +namespace TagsCloudVisualization.FileReaders; + +public class DocxFileReader : IFileReader +{ + public string Read(string filePath) + { + var text = new StringBuilder(); + using var doc = new XWPFDocument(File.OpenRead(filePath)); + + foreach (var paragraph in doc.Paragraphs) + text.Append(paragraph.Text); + + return text.ToString(); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/FileReaders/FileReaderFactory.cs b/TagsCloudVisualization/FileReaders/FileReaderFactory.cs new file mode 100644 index 00000000..a657c2c5 --- /dev/null +++ b/TagsCloudVisualization/FileReaders/FileReaderFactory.cs @@ -0,0 +1,24 @@ +using Autofac; + +namespace TagsCloudVisualization.FileReaders; + +public class FileReaderFactory +{ + private readonly IComponentContext context; + + public FileReaderFactory(IComponentContext context) + => this.context = context; + + public IFileReader GetFileReader(string filePath) + { + if (!File.Exists(filePath)) + throw new FileNotFoundException("File not found", filePath); + + var extension = Path.GetExtension(filePath).ToLower(); + + if (context.IsRegisteredWithKey(extension)) + return context.ResolveKeyed(extension); + + throw new NotSupportedException($"File type {extension} is not supported."); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/FileReaders/IFileReader.cs b/TagsCloudVisualization/FileReaders/IFileReader.cs new file mode 100644 index 00000000..bb66735f --- /dev/null +++ b/TagsCloudVisualization/FileReaders/IFileReader.cs @@ -0,0 +1,6 @@ +namespace TagsCloudVisualization.FileReaders; + +public interface IFileReader +{ + public string Read(string filePath); +} \ No newline at end of file diff --git a/TagsCloudVisualization/FileReaders/TextFileReader.cs b/TagsCloudVisualization/FileReaders/TextFileReader.cs new file mode 100644 index 00000000..19970982 --- /dev/null +++ b/TagsCloudVisualization/FileReaders/TextFileReader.cs @@ -0,0 +1,9 @@ +namespace TagsCloudVisualization.FileReaders; + +public class TextFileReader : IFileReader +{ + public string Read(string filePath) + { + return File.ReadAllText(filePath); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Layouters/CircularCloudLayouter.cs b/TagsCloudVisualization/Layouters/CircularCloudLayouter.cs new file mode 100644 index 00000000..f4ebaa01 --- /dev/null +++ b/TagsCloudVisualization/Layouters/CircularCloudLayouter.cs @@ -0,0 +1,52 @@ +using System.Drawing; +using TagsCloudVisualization.Distributors; +using TagsCloudVisualization.Domain; +using TagsCloudVisualization.Layouters.RectangleSizeCalculators; +using TagsCloudVisualization.WordPreprocessors.FontCreators; + +namespace TagsCloudVisualization.Layouters; + +public class CircularCloudLayouter : ICloudLayouter +{ + private readonly ICloudDistribution distribution; + private readonly IFontCreator fontCreator; + private readonly IRectangleSizeCalculator rectangleSizeCalculator; + + public CircularCloudLayouter(ICloudDistribution distribution, + IFontCreator fontCreator, + IRectangleSizeCalculator rectangleSizeCalculator) + { + this.distribution = distribution; + this.fontCreator = fontCreator; + this.rectangleSizeCalculator = rectangleSizeCalculator; + } + + public IEnumerable CreateTagsCloud(IEnumerable> wordsCollection) + { + List tags = []; + + foreach (var (word, wordCount) in wordsCollection) + { + var tagFont = fontCreator.CreateFont(wordCount); + var rectangleSize = rectangleSizeCalculator.ConvertWordToRectangleSize(word, tagFont); + var rectangle = GetNextRectangle(tags, rectangleSize); + + tags.Add(new Tag(rectangle, tagFont, word)); + } + + return tags; + } + + private Rectangle GetNextRectangle(List tags, Size rectangleSize) + { + if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0) + throw new ArgumentException("The rectangle size must be greater than zero."); + + var newRectangle = new Rectangle(distribution.GetNextPoint(), rectangleSize); + + while (tags.Any(r => r.Rectangle.IntersectsWith(newRectangle))) + newRectangle.Location = distribution.GetNextPoint(); + + return newRectangle; + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Layouters/ICloudLayouter.cs b/TagsCloudVisualization/Layouters/ICloudLayouter.cs new file mode 100644 index 00000000..7b2c6e6b --- /dev/null +++ b/TagsCloudVisualization/Layouters/ICloudLayouter.cs @@ -0,0 +1,8 @@ +using TagsCloudVisualization.Domain; + +namespace TagsCloudVisualization.Layouters; + +public interface ICloudLayouter +{ + public IEnumerable CreateTagsCloud(IEnumerable> wordsCollection); +} \ No newline at end of file diff --git a/TagsCloudVisualization/Layouters/RectangleSizeCalculators/DefaultRectangleSizeCalculator.cs b/TagsCloudVisualization/Layouters/RectangleSizeCalculators/DefaultRectangleSizeCalculator.cs new file mode 100644 index 00000000..62a610ec --- /dev/null +++ b/TagsCloudVisualization/Layouters/RectangleSizeCalculators/DefaultRectangleSizeCalculator.cs @@ -0,0 +1,13 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Layouters.RectangleSizeCalculators; + +public class DefaultRectangleSizeCalculator : IRectangleSizeCalculator +{ + public Size ConvertWordToRectangleSize(string word, Font font) + { + using var graphic = Graphics.FromImage(new Bitmap(1, 1)); + var sizeF = graphic.MeasureString(word, font); + return new Size((int)Math.Ceiling(sizeF.Width), (int)Math.Ceiling(sizeF.Height)); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Layouters/RectangleSizeCalculators/IRectangleSizeCalculator.cs b/TagsCloudVisualization/Layouters/RectangleSizeCalculators/IRectangleSizeCalculator.cs new file mode 100644 index 00000000..139cd2ca --- /dev/null +++ b/TagsCloudVisualization/Layouters/RectangleSizeCalculators/IRectangleSizeCalculator.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Layouters.RectangleSizeCalculators; + +public interface IRectangleSizeCalculator +{ + public Size ConvertWordToRectangleSize(string word, Font font); +} \ No newline at end of file diff --git a/TagsCloudVisualization/MyStemWrapper/MyStem.cs b/TagsCloudVisualization/MyStemWrapper/MyStem.cs new file mode 100644 index 00000000..22d4f57d --- /dev/null +++ b/TagsCloudVisualization/MyStemWrapper/MyStem.cs @@ -0,0 +1,52 @@ +using System.Diagnostics; +using System.Text; + +namespace TagsCloudVisualization.MyStemWrapper; + +public class MyStem +{ + public string PathToMyStem { get; set; } = "mystem.exe"; + + public string Parameters { get; set; } = string.Empty; + + public string Analysis(string text) + { + if (!File.Exists(PathToMyStem)) + throw new FileNotFoundException("Path to MyStem.exe is not valid! Change 'PathToMyStem' properties or move MyStem.exe in appropriate folder."); + try + { + return GetResults(CreateProcess(), text); + } + catch + { + throw new FormatException("Invalid parameters! Look at https://tech.yandex.ru/mystem/doc/index-docpage"); + } + } + + private string GetResults(Process process, string text) + { + byte[] bytes = Encoding.UTF8.GetBytes(text); + process.StandardInput.BaseStream.Write(bytes, 0, bytes.Length); + process.StandardInput.BaseStream.Flush(); + process.StandardInput.BaseStream.Close(); + string end = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + return end; + } + + private Process CreateProcess() + { + return Process.Start(new ProcessStartInfo() + { + FileName = PathToMyStem, + Arguments = Parameters ?? string.Empty, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + StandardOutputEncoding = Encoding.UTF8 + }); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/MyStemWrapper/MyStemDto.cs b/TagsCloudVisualization/MyStemWrapper/MyStemDto.cs new file mode 100644 index 00000000..6c7ab02b --- /dev/null +++ b/TagsCloudVisualization/MyStemWrapper/MyStemDto.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace TagsCloudVisualization.MyStemWrapper; + +public class MyStemDto +{ + [JsonPropertyName("analysis")] + public List Analysis { get; set; } + + [JsonPropertyName("text")] + public string Text { get; set; } +} + +public class WordInfo +{ + [JsonPropertyName("lex")] + public string Lemma { get; set; } + + [JsonPropertyName("gr")] + public string Grammeme { get; set; } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Program.cs b/TagsCloudVisualization/Program.cs new file mode 100644 index 00000000..cd256b27 --- /dev/null +++ b/TagsCloudVisualization/Program.cs @@ -0,0 +1,25 @@ +using Autofac; +using CommandLine; +using TagsCloudVisualization.App; +using TagsCloudVisualization.ConsoleCommands; + +namespace TagsCloudVisualization; + +public class Program +{ + static void Main(string[] args) + { + try + { + var options = Parser.Default.ParseArguments(args).Value; + var container = ContainerConfig.Configure(options); + + using var scope = container.BeginLifetimeScope(); + scope.Resolve().Run(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Renderers/ColorGenerators/ColorGeneratorFactory.cs b/TagsCloudVisualization/Renderers/ColorGenerators/ColorGeneratorFactory.cs new file mode 100644 index 00000000..dce95c6a --- /dev/null +++ b/TagsCloudVisualization/Renderers/ColorGenerators/ColorGeneratorFactory.cs @@ -0,0 +1,20 @@ +using Autofac; +using TagsCloudVisualization.Enums; + +namespace TagsCloudVisualization.Renderers.ColorGenerators; + +public class ColorGeneratorFactory +{ + private readonly IComponentContext context; + + public ColorGeneratorFactory(IComponentContext context) + => this.context = context; + + public IColorGenerator GetColorGenerator(ColorOption option) + { + if (context.IsRegisteredWithKey(option)) + return context.ResolveKeyed(option); + + throw new NotSupportedException($"Color generator {option.ToString()} is not supported."); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Renderers/ColorGenerators/DefaultColorGenerator.cs b/TagsCloudVisualization/Renderers/ColorGenerators/DefaultColorGenerator.cs new file mode 100644 index 00000000..dba02380 --- /dev/null +++ b/TagsCloudVisualization/Renderers/ColorGenerators/DefaultColorGenerator.cs @@ -0,0 +1,13 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Renderers.ColorGenerators; + +public class DefaultColorGenerator : IColorGenerator +{ + private readonly Random random = new(); + + public Color GetColor() + { + return Color.FromArgb(random.Next(0, 255), random.Next(0, 255), random.Next(0, 255)); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Renderers/ColorGenerators/GradientColorGenerator.cs b/TagsCloudVisualization/Renderers/ColorGenerators/GradientColorGenerator.cs new file mode 100644 index 00000000..0e0284e6 --- /dev/null +++ b/TagsCloudVisualization/Renderers/ColorGenerators/GradientColorGenerator.cs @@ -0,0 +1,48 @@ +using System.Drawing; +using TagsCloudVisualization.ConsoleCommands; + +namespace TagsCloudVisualization.Renderers.ColorGenerators; + +public class GradientColorGenerator : IColorGenerator +{ + private readonly Color startColor = Color.FromArgb(69, 10, 92); + private readonly Color middleColor = Color.FromArgb(31, 158, 136); + private readonly Color endColor = Color.FromArgb(243, 229, 37); + private readonly int numOfColors; + private float stepCount; + + public GradientColorGenerator(Options options) + => numOfColors = options.NumOfColors; + + public Color GetColor() + { + var color = endColor; + + if (stepCount >= numOfColors) + return color; + + var halfCount = numOfColors / 2; + + color = stepCount < halfCount + ? GetNextColor(startColor, middleColor, stepCount / (halfCount - 1)) + : GetNextColor(middleColor, endColor, (stepCount - halfCount) / (numOfColors - halfCount - 1)); + + stepCount++; + + return color; + } + + private static Color GetNextColor(Color start, Color end, float ratio) + { + var red = Interpolate(start.R, end.R, ratio); + var green = Interpolate(start.G, end.G, ratio); + var blue = Interpolate(start.B, end.B, ratio); + + return Color.FromArgb(red, green, blue); + } + + private static int Interpolate(int start, int end, float ratio) + { + return (int)(start + (end - start) * ratio); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Renderers/ColorGenerators/IColorGenerator.cs b/TagsCloudVisualization/Renderers/ColorGenerators/IColorGenerator.cs new file mode 100644 index 00000000..9509942f --- /dev/null +++ b/TagsCloudVisualization/Renderers/ColorGenerators/IColorGenerator.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.Renderers.ColorGenerators; + +public interface IColorGenerator +{ + public Color GetColor(); +} \ No newline at end of file diff --git a/TagsCloudVisualization/Renderers/DefaultRenderer.cs b/TagsCloudVisualization/Renderers/DefaultRenderer.cs new file mode 100644 index 00000000..ce4e0e0b --- /dev/null +++ b/TagsCloudVisualization/Renderers/DefaultRenderer.cs @@ -0,0 +1,37 @@ +using System.Drawing; +using System.Drawing.Imaging; +using TagsCloudVisualization.BitmapProcessors; +using TagsCloudVisualization.ConsoleCommands; +using TagsCloudVisualization.Domain; +using TagsCloudVisualization.Renderers.ColorGenerators; + +namespace TagsCloudVisualization.Renderers; + +public class DefaultRenderer(ColorGeneratorFactory colorGeneratorFactory, + BitmapProcessorFactory bitmapProcessorFactory, + Options options) : ICloudRenderer +{ + private readonly IColorGenerator colorGenerator = colorGeneratorFactory.GetColorGenerator(options.ColorOption); + private readonly IBitmapProcessor bitmapProcessor = bitmapProcessorFactory.GetBitmapProcessor(options.ImageFormat); + private readonly string outputDirectory = options.OutputDirectory; + private readonly Size imageSize = new(options.ImageWidth, options.ImageHeight); + private readonly Color backgroundColor = Color.FromName(options.BackgroundColor); + + public void Render(IEnumerable tags) + { + if (!tags.Any()) + throw new ArgumentException("The cloud layout is empty"); + + using var bitmap = new Bitmap(imageSize.Width, imageSize.Height); + using var graphic = Graphics.FromImage(bitmap); + graphic.Clear(backgroundColor); + foreach (var tag in tags) + { + var color = colorGenerator.GetColor(); + var brush = new SolidBrush(color); + graphic.DrawString(tag.Content, tag.Font, brush, tag.Rectangle.Location); + } + + bitmapProcessor.SaveImage(bitmap, outputDirectory, $"cloud_{tags.Count()}"); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Renderers/ICloudRenderer.cs b/TagsCloudVisualization/Renderers/ICloudRenderer.cs new file mode 100644 index 00000000..7d9bbaf7 --- /dev/null +++ b/TagsCloudVisualization/Renderers/ICloudRenderer.cs @@ -0,0 +1,8 @@ +using TagsCloudVisualization.Domain; + +namespace TagsCloudVisualization.Renderers; + +public interface ICloudRenderer +{ + public void Render(IEnumerable tags); +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization.csproj b/TagsCloudVisualization/TagsCloudVisualization.csproj new file mode 100644 index 00000000..7ec829cd --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + enable + disable + + + + + + + + + + + + + + Always + + + + diff --git a/TagsCloudVisualization/Utilities/mystem.exe b/TagsCloudVisualization/Utilities/mystem.exe new file mode 100644 index 00000000..e7158ff1 Binary files /dev/null and b/TagsCloudVisualization/Utilities/mystem.exe differ diff --git a/TagsCloudVisualization/WordPreprocessors/DefaultWordPreprocessor.cs b/TagsCloudVisualization/WordPreprocessors/DefaultWordPreprocessor.cs new file mode 100644 index 00000000..d2e5ce66 --- /dev/null +++ b/TagsCloudVisualization/WordPreprocessors/DefaultWordPreprocessor.cs @@ -0,0 +1,37 @@ +using System.Text.Json; +using TagsCloudVisualization.MyStemWrapper; +using TagsCloudVisualization.WordPreprocessors.WordValidators; + +namespace TagsCloudVisualization.WordPreprocessors; + +public class DefaultWordPreprocessor : IWordPreprocessor +{ + private IWordValidator wordValidator; + private MyStem myStem; + + public DefaultWordPreprocessor(IWordValidator wordValidator, MyStem myStem) + { + this.wordValidator = wordValidator; + this.myStem = myStem; + } + + public IEnumerable> ProcessTextToWords(string text) + { + var analysis = myStem.Analysis(text).Split("\r\n", StringSplitOptions.RemoveEmptyEntries); + var result = new Dictionary(); + + foreach (var wordInfo in analysis) + { + var dto = JsonSerializer.Deserialize(wordInfo); + var word = dto.Analysis.First(); + + if (!wordValidator.IsValid(word)) continue; + + if (!result.TryAdd(word.Lemma, 1)) + result[word.Lemma] += 1; + } + + return result.Select(x => new Tuple(x.Key, x.Value)) + .OrderByDescending(x => x.Item2); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/WordPreprocessors/FontCreators/DefaultFontCreator.cs b/TagsCloudVisualization/WordPreprocessors/FontCreators/DefaultFontCreator.cs new file mode 100644 index 00000000..f8199683 --- /dev/null +++ b/TagsCloudVisualization/WordPreprocessors/FontCreators/DefaultFontCreator.cs @@ -0,0 +1,25 @@ +using System.Drawing; +using TagsCloudVisualization.ConsoleCommands; + +namespace TagsCloudVisualization.WordPreprocessors.FontCreators; + +public class DefaultFontCreator : IFontCreator +{ + private readonly string fontName; + private readonly int maxFontSize; + private readonly int minFontSize; + + public DefaultFontCreator(Options options) + { + fontName = options.TagsFont; + maxFontSize = options.MaxTagsFontSize; + minFontSize = options.MinTagsFontSize; + } + + public Font CreateFont(int fontSizeFactor) + { + var fontSize = Math.Min(Math.Max(minFontSize, fontSizeFactor), maxFontSize); + + return new Font(fontName, fontSize); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/WordPreprocessors/FontCreators/IFontCreator.cs b/TagsCloudVisualization/WordPreprocessors/FontCreators/IFontCreator.cs new file mode 100644 index 00000000..0bdf5048 --- /dev/null +++ b/TagsCloudVisualization/WordPreprocessors/FontCreators/IFontCreator.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudVisualization.WordPreprocessors.FontCreators; + +public interface IFontCreator +{ + public Font CreateFont(int fontSizeFactor); +} \ No newline at end of file diff --git a/TagsCloudVisualization/WordPreprocessors/IWordPreprocessor.cs b/TagsCloudVisualization/WordPreprocessors/IWordPreprocessor.cs new file mode 100644 index 00000000..45c5bbcd --- /dev/null +++ b/TagsCloudVisualization/WordPreprocessors/IWordPreprocessor.cs @@ -0,0 +1,6 @@ +namespace TagsCloudVisualization.WordPreprocessors; + +public interface IWordPreprocessor +{ + public IEnumerable> ProcessTextToWords(string text); +} \ No newline at end of file diff --git a/TagsCloudVisualization/WordPreprocessors/WordValidators/DefaultWordValidator.cs b/TagsCloudVisualization/WordPreprocessors/WordValidators/DefaultWordValidator.cs new file mode 100644 index 00000000..0464d410 --- /dev/null +++ b/TagsCloudVisualization/WordPreprocessors/WordValidators/DefaultWordValidator.cs @@ -0,0 +1,15 @@ +using TagsCloudVisualization.MyStemWrapper; + +namespace TagsCloudVisualization.WordPreprocessors.WordValidators; + +public class DefaultWordValidator : IWordValidator +{ + public bool IsValid(WordInfo wordInfo) + { + return !(wordInfo.Grammeme.Contains("CONJ") || + wordInfo.Grammeme.Contains("INTJ") || + wordInfo.Grammeme.Contains("PART") || + wordInfo.Grammeme.Contains("PR") || + wordInfo.Grammeme.Contains("SPRO")); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/WordPreprocessors/WordValidators/IWordValidator.cs b/TagsCloudVisualization/WordPreprocessors/WordValidators/IWordValidator.cs new file mode 100644 index 00000000..59c8a1cf --- /dev/null +++ b/TagsCloudVisualization/WordPreprocessors/WordValidators/IWordValidator.cs @@ -0,0 +1,8 @@ +using TagsCloudVisualization.MyStemWrapper; + +namespace TagsCloudVisualization.WordPreprocessors.WordValidators; + +public interface IWordValidator +{ + public bool IsValid(WordInfo wordInfo); +} \ No newline at end of file diff --git a/TagsCloudVisualization/source/cloud_85.pdf b/TagsCloudVisualization/source/cloud_85.pdf new file mode 100644 index 00000000..de9e583d Binary files /dev/null and b/TagsCloudVisualization/source/cloud_85.pdf differ diff --git a/TagsCloudVisualization/source/cloud_85.png b/TagsCloudVisualization/source/cloud_85.png new file mode 100644 index 00000000..d5310bf1 Binary files /dev/null and b/TagsCloudVisualization/source/cloud_85.png differ diff --git a/TagsCloudVisualization/source/input.doc b/TagsCloudVisualization/source/input.doc new file mode 100644 index 00000000..9929bc2a Binary files /dev/null and b/TagsCloudVisualization/source/input.doc differ diff --git a/TagsCloudVisualization/source/input.docx b/TagsCloudVisualization/source/input.docx new file mode 100644 index 00000000..1dbb23d1 Binary files /dev/null and b/TagsCloudVisualization/source/input.docx differ diff --git a/TagsCloudVisualization/source/input.txt b/TagsCloudVisualization/source/input.txt new file mode 100644 index 00000000..1157ae43 --- /dev/null +++ b/TagsCloudVisualization/source/input.txt @@ -0,0 +1,305 @@ +Самолет +Книга +Книгу +Книг +Книга +Книга +Книга +Книга +Книга +Книга +Книга +Книга +Книга +Книга +Книга +Книга +Книга +Книга +Река +Звезда +Ящер +Лошадь +Камень +Метеор +Дом +Метеор +Дом +Метеор +Дом +Метеор +Дом +Метеор +Дом +Метеор +Дом +Метеор +Астронавт +Цветок +Металл +Призрак +Музыка +Одежда +Цвет +Лошадь +Лошадь +Камень +Метеор +Дом +Астронавт +Цветок +Металл +Призрак +Музыка +Одежда +Цвет +Камень +Метеор +Дом +Астронавт +Цветок +Металл +Призрак +Музыка +Одежда +Цвет +Камень +Метеор +Дом +Астронавт +Цветок +Металл +Призрак +Музыка +Одежда +Цвет +Писатель +Лес +Звук +Волшебница +Морозильник +Минерал +Монстр +Конфета +Опыт +Рынок +Фрукт +Книжка +Ветер +Рынок +Фрукт +Книжка +Ветер +Рынок +Фрукт +Книжка +Ветер +Рынок +Фрукт +Книжка +Ветер +Рынок +Фрукт +Книжка +Ветер +Рынок +Фрукт +Книжка +Ветер +Рынок +Фрукт +Книжка +Ветер +Мир +Музыка +Дерево +Животное +Полоса +Строитель +Камень +Птица +Ветряк +Камень +Птица +Ветряк +Камень +Птица +Ветряк +Камень +Птица +Ветряк +Камень +Птица +Ветряк +Животное +Надежда +Парашют +Космос +Ножницы +Динозавр +Железо +Фонтан +Вода +Ящерица +Цветок +Животное +Шампунь +Путешествие +Камень +Фрукт +Пузырь +Украшение +Змей +Страна +Жемчужина +Дождь +Игрушка +Сладость +Лавина +Мандарин +Изобретатель +Спикер +Полет +Радио +Спутник +Ураган +Шапка +Футболка +Холст +Насекомое +Личность +Игра +Этюд +Ягода +Аквариум +Пляж +Магия +Гоблин +Взрыв +Ель +Металл +Полоса +Цветок +Машина +Напиток +Мороженое +Одежда +Аплодисменты +Птица +Угол +Винт +Монстр +Ветер +Фантазия +Ящерица +Шляпа +Цветок +Машина +Напиток +Мороженое +Одежда +Аплодисменты +Птица +Угол +Винт +Монстр +Ветер +Фантазия +Ящерица +Шляпа +Цветок +Машина +Напиток +Мороженое +Одежда +Аплодисменты +Птица +Угол +Винт +Монстр +Ветер +Фантазия +Ящерица +Шляпа +Шляпа +Цветок +Машина +Напиток +Шляпа +Цветок +Машина +Напиток +Шляпа +Цветок +Машина +Напиток +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он +он \ No newline at end of file diff --git a/TagsCloudVisualizationTests/BitmapProcessorFactoryTests.cs b/TagsCloudVisualizationTests/BitmapProcessorFactoryTests.cs new file mode 100644 index 00000000..4b021309 --- /dev/null +++ b/TagsCloudVisualizationTests/BitmapProcessorFactoryTests.cs @@ -0,0 +1,46 @@ +using Autofac; +using FluentAssertions; +using TagsCloudVisualization; +using TagsCloudVisualization.BitmapProcessors; +using TagsCloudVisualization.ConsoleCommands; +using TagsCloudVisualization.Enums; + +namespace TagsCloudVisualizationTests; + +[TestFixture] +public class BitmapProcessorFactoryTests +{ + private ILifetimeScope Scope { get; set; } + + [SetUp] + public void Setup() + { + var options = new Options + { + InputFilePath = "/path/in/test/not/needed", + OutputDirectory = "/path/in/test/not/needed", + }; + var container = ContainerConfig.Configure(options); + Scope = container.BeginLifetimeScope(); + } + + [TearDown] + public void TearDown() + { + Scope.Dispose(); + } + + [TestCase(OutputImageFormat.Jpeg, typeof(DefaultBitmapProcessor))] + [TestCase(OutputImageFormat.Jpg, typeof(DefaultBitmapProcessor))] + [TestCase(OutputImageFormat.Png, typeof(DefaultBitmapProcessor))] + [TestCase(OutputImageFormat.Gif, typeof(DefaultBitmapProcessor))] + [TestCase(OutputImageFormat.Bmp, typeof(DefaultBitmapProcessor))] + [TestCase(OutputImageFormat.Tiff, typeof(DefaultBitmapProcessor))] + [TestCase(OutputImageFormat.Pdf, typeof(PdfBitmapProcessor))] + public void GetBitmapProcessor_ShouldReturnCorrectBitmapProcessor(OutputImageFormat option, Type expectedType) + { + var bitmapFactory = Scope.Resolve(); + + bitmapFactory.GetBitmapProcessor(option).Should().BeOfType(expectedType); + } +} \ No newline at end of file diff --git a/TagsCloudVisualizationTests/BitmapProcessorTests.cs b/TagsCloudVisualizationTests/BitmapProcessorTests.cs new file mode 100644 index 00000000..6ecc71d5 --- /dev/null +++ b/TagsCloudVisualizationTests/BitmapProcessorTests.cs @@ -0,0 +1,55 @@ +using System.Drawing; +using Autofac; +using TagsCloudVisualization; +using TagsCloudVisualization.BitmapProcessors; +using TagsCloudVisualization.ConsoleCommands; +using TagsCloudVisualization.Enums; + +namespace TagsCloudVisualizationTests; + +[TestFixture] +public class BitmapProcessorTests +{ + private ILifetimeScope Scope { get; set; } + private string directory = Path.GetFullPath("rendered"); + + [SetUp] + public void Setup() + { + var options = new Options + { + InputFilePath = directory, + OutputDirectory = "/path/in/test/not/needed", + }; + var container = ContainerConfig.Configure(options); + Directory.CreateDirectory(directory); + Scope = container.BeginLifetimeScope(); + } + + [TearDown] + public void TearDown() + { + Scope.Dispose(); + + if (Directory.Exists(directory)) + Directory.Delete(directory, true); + } + + [TestCase(OutputImageFormat.Jpeg,".jpeg")] + [TestCase(OutputImageFormat.Jpg, ".jpg")] + [TestCase(OutputImageFormat.Png, ".png")] + [TestCase(OutputImageFormat.Gif, ".gif")] + [TestCase(OutputImageFormat.Bmp, ".bmp")] + [TestCase(OutputImageFormat.Tiff, ".tiff")] + [TestCase(OutputImageFormat.Pdf, ".pdf")] + public void Read_ShouldReturnCorrectStringFromFile(OutputImageFormat format, string expected) + { + var imageName = "output"; + var expectedPath = Path.Combine(directory, $"{imageName}.{expected}"); + var bitmapFactory = Scope.Resolve(); + + bitmapFactory.GetBitmapProcessor(format).SaveImage(new Bitmap(1, 1), directory, imageName); + + File.Exists(expectedPath); + } +} \ No newline at end of file diff --git a/TagsCloudVisualizationTests/CloudLayouterTests.cs b/TagsCloudVisualizationTests/CloudLayouterTests.cs new file mode 100644 index 00000000..b4d2309a --- /dev/null +++ b/TagsCloudVisualizationTests/CloudLayouterTests.cs @@ -0,0 +1,95 @@ +using System.Drawing; +using FluentAssertions; +using TagsCloudVisualization.ConsoleCommands; +using TagsCloudVisualization.Distributors; +using TagsCloudVisualization.Domain; +using TagsCloudVisualization.Layouters; +using TagsCloudVisualization.Layouters.RectangleSizeCalculators; +using TagsCloudVisualization.WordPreprocessors.FontCreators; +using Telerik.JustMock; +using Telerik.JustMock.Helpers; + +namespace TagsCloudVisualizationTests; + +[TestFixture] +public class CloudLayouterTests +{ + private ICloudLayouter Layouter { get; set; } + private IFontCreator FakeFontCreator { get; set; } + private IRectangleSizeCalculator FakeRectangleSizeCalculator { get; set; } + private Size imageSize = new(50, 50); + + [SetUp] + public void Setup() + { + FakeFontCreator = Mock.Create(); + FakeRectangleSizeCalculator = Mock.Create(); + + Layouter = new CircularCloudLayouter( + new SpiralDistribution(new Options {ImageWidth = imageSize.Width, ImageHeight = imageSize.Height}), + FakeFontCreator, + FakeRectangleSizeCalculator + ); + } + + [Test] + public void CreateTagsCloud_ShouldReturnCorrectFirstTag() + { + Mock.Arrange(() => FakeFontCreator.CreateFont(Arg.AnyInt)) + .Returns(new Font("Arial", 12)); + Mock.Arrange(() => FakeRectangleSizeCalculator.ConvertWordToRectangleSize(Arg.AnyString, Arg.IsAny())) + .Returns(new Size(10, 10)); + + Layouter.CreateTagsCloud([new Tuple("text", 10)]) + .First() + .Should().BeEquivalentTo(new Tag( + new Rectangle(new Point(imageSize.Width / 2, imageSize.Height / 2), new Size(10, 10)), + new Font("Arial", 12), + "text")); + } + + [Test] + public void CreateTagsCloud_ShouldReturnFirstTagInImageCenter() + { + Mock.Arrange(() => FakeFontCreator.CreateFont(Arg.AnyInt)) + .Returns(new Font("Arial", 12, FontStyle.Regular)); + Mock.Arrange(() => FakeRectangleSizeCalculator.ConvertWordToRectangleSize(Arg.AnyString, Arg.IsAny())) + .Returns(new Size(10, 10)); + + Layouter.CreateTagsCloud([new Tuple("text", 10)]) + .First() + .Rectangle.Location + .Should().Be(new Point(imageSize.Width / 2, imageSize.Height / 2)); + } + + [TestCase(16)] + [TestCase(64)] + [TestCase(256)] + public void CreateTagsCloud_GeneratesTagsWithoutIntersects(int wordCount) + { + Mock.Arrange(() => FakeFontCreator.CreateFont(Arg.AnyInt)) + .Returns(new Font("Arial", 12, FontStyle.Regular)); + Mock.Arrange(() => FakeRectangleSizeCalculator.ConvertWordToRectangleSize(Arg.AnyString, Arg.IsAny())) + .ReturnsMany(Enumerable.Range(0, wordCount) + .Select(_ => new Size(10, 10)) + .ToArray()); + + var tags = Layouter.CreateTagsCloud( + Enumerable.Range(0, wordCount).Select(_ => new Tuple("text", 10))); + + HasIntersectedRectangles(tags.ToList()).Should().Be(false); + } + + private static bool HasIntersectedRectangles(List tags) + { + for (var i = 0; i < tags.Count - 1; i++) + { + for (var j = i + 1; j < tags.Count; j++) + { + if (tags[i].Rectangle.IntersectsWith(tags[j].Rectangle)) + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/TagsCloudVisualizationTests/ColorGeneratorFactoryTests.cs b/TagsCloudVisualizationTests/ColorGeneratorFactoryTests.cs new file mode 100644 index 00000000..1769a20c --- /dev/null +++ b/TagsCloudVisualizationTests/ColorGeneratorFactoryTests.cs @@ -0,0 +1,41 @@ +using Autofac; +using FluentAssertions; +using TagsCloudVisualization; +using TagsCloudVisualization.ConsoleCommands; +using TagsCloudVisualization.Enums; +using TagsCloudVisualization.Renderers.ColorGenerators; + +namespace TagsCloudVisualizationTests; + +[TestFixture] +public class ColorGeneratorFactoryTests +{ + private ILifetimeScope Scope { get; set; } + + [SetUp] + public void Setup() + { + var options = new Options + { + InputFilePath = "/path/in/test/not/needed", + OutputDirectory = "/path/in/test/not/needed", + }; + var container = ContainerConfig.Configure(options); + Scope = container.BeginLifetimeScope(); + } + + [TearDown] + public void TearDown() + { + Scope.Dispose(); + } + + [TestCase(ColorOption.Random, typeof(DefaultColorGenerator))] + [TestCase(ColorOption.Gradient, typeof(GradientColorGenerator))] + public void GetColorGenerator_ShouldReturnCorrectColorGenerator(ColorOption option, Type expectedType) + { + var colorGeneratorFactory = Scope.Resolve(); + + colorGeneratorFactory.GetColorGenerator(option).Should().BeOfType(expectedType); + } +} \ No newline at end of file diff --git a/TagsCloudVisualizationTests/ConsoleAppTests.cs b/TagsCloudVisualizationTests/ConsoleAppTests.cs new file mode 100644 index 00000000..543919af --- /dev/null +++ b/TagsCloudVisualizationTests/ConsoleAppTests.cs @@ -0,0 +1,84 @@ +using System.Drawing; +using Autofac; +using TagsCloudVisualization; +using TagsCloudVisualization.App; +using TagsCloudVisualization.ConsoleCommands; +using TagsCloudVisualization.Domain; +using TagsCloudVisualization.Enums; +using TagsCloudVisualization.FileReaders; +using TagsCloudVisualization.Layouters; +using TagsCloudVisualization.Renderers; +using TagsCloudVisualization.WordPreprocessors; +using Telerik.JustMock; + +namespace TagsCloudVisualizationTests; + +[TestFixture] +public class ConsoleAppTests +{ + private ConsoleApp ConsoleApp { get; set; } + private ILifetimeScope Scope { get; set; } + private IWordPreprocessor WordPreprocessor { get; set; } + private ICloudLayouter Layouter { get; set; } + private Options Options { get; set; } + + [SetUp] + public void SetUp() + { + SetUpScope(); + WordPreprocessor = Mock.Create(); + Layouter = Mock.Create(); + ConsoleApp = new ConsoleApp( + WordPreprocessor, + Scope.Resolve(), + Layouter, + Scope.Resolve(), + Options + ); + } + + [TearDown] + public void TearDown() + { + Scope.Dispose(); + + if (Directory.Exists(Options.OutputDirectory)) + Directory.Delete(Options.OutputDirectory, true); + } + + private void SetUpScope() + { + Options = new Options + { + InputFilePath = Path.GetFullPath("source\\input.txt"), + OutputDirectory = Path.GetFullPath("rendered"), + ColorOption = ColorOption.Random, + ImageFormat = OutputImageFormat.Png, + ImageWidth = 50, + ImageHeight = 50, + BackgroundColor = "#FF0000" + }; + Directory.CreateDirectory(Options.OutputDirectory); + var container = ContainerConfig.Configure(Options); + Scope = container.BeginLifetimeScope(); + } + + [Test] + public void Run_ShouldSaveFileToOutputDirectory() + { + Mock.Arrange(() => WordPreprocessor.ProcessTextToWords(Arg.AnyString)).Returns(() => []); + Mock.Arrange(() => Layouter.CreateTagsCloud(Arg.IsAny>>())) + .Returns( + new List + { + new(new Rectangle(new Point(0, 0), new Size(50, 50)), + new Font("Arial", 1), + "fake") + } + ); + + ConsoleApp.Run(); + + File.Exists(Path.GetFullPath("rendered\\cloud_1.png")); + } +} \ No newline at end of file diff --git a/TagsCloudVisualizationTests/FileReaderFactoryTests.cs b/TagsCloudVisualizationTests/FileReaderFactoryTests.cs new file mode 100644 index 00000000..1b213c88 --- /dev/null +++ b/TagsCloudVisualizationTests/FileReaderFactoryTests.cs @@ -0,0 +1,60 @@ +using Autofac; +using FluentAssertions; +using TagsCloudVisualization; +using TagsCloudVisualization.ConsoleCommands; +using TagsCloudVisualization.FileReaders; + +namespace TagsCloudVisualizationTests; + +[TestFixture] +public class FileReaderFactoryTests +{ + private ILifetimeScope Scope { get; set; } + + [SetUp] + public void Setup() + { + var options = new Options + { + InputFilePath = "/path/in/test/not/needed", + OutputDirectory = "/path/in/test/not/needed", + }; + var container = ContainerConfig.Configure(options); + Scope = container.BeginLifetimeScope(); + } + + [TearDown] + public void TearDown() + { + Scope.Dispose(); + } + + [TestCase("source\\input.txt", typeof(TextFileReader))] + [TestCase("source\\input.doc", typeof(DocFileReader))] + [TestCase("source\\input.docx", typeof(DocxFileReader))] + public void GetFileReader_ShouldReturnCorrectFileReader(string path, Type expectedType) + { + var absolutePath = Path.GetFullPath(path); + var fileReaderFactory = Scope.Resolve(); + + fileReaderFactory.GetFileReader(absolutePath).Should().BeOfType(expectedType); + } + + [Test] + public void GetFileReader_ShouldThrowFileNotFoundException_WhenFileDoesNotExist() + { + var fileReaderFactory = Scope.Resolve(); + var action = () => fileReaderFactory.GetFileReader("any/path"); + + action.Should().Throw(); + } + + [Test] + public void GetFileReader_ShouldThrowNotSupportedException_WhenFileIsNotSupported() + { + var fileReaderFactory = Scope.Resolve(); + var action = () => fileReaderFactory.GetFileReader("Utilities\\mystem.exe"); + + action.Should().Throw(); + } +} \ No newline at end of file diff --git a/TagsCloudVisualizationTests/FileReaderTests.cs b/TagsCloudVisualizationTests/FileReaderTests.cs new file mode 100644 index 00000000..5f344788 --- /dev/null +++ b/TagsCloudVisualizationTests/FileReaderTests.cs @@ -0,0 +1,44 @@ +using Autofac; +using FluentAssertions; +using TagsCloudVisualization; +using TagsCloudVisualization.ConsoleCommands; +using TagsCloudVisualization.FileReaders; + +namespace TagsCloudVisualizationTests; + +[TestFixture] +public class FileReaderTests +{ + private ILifetimeScope Scope { get; set; } + + [SetUp] + public void Setup() + { + var options = new Options + { + InputFilePath = "/path/in/test/not/needed", + OutputDirectory = "/path/in/test/not/needed", + }; + var container = ContainerConfig.Configure(options); + Scope = container.BeginLifetimeScope(); + } + + [TearDown] + public void TearDown() + { + Scope.Dispose(); + } + + [TestCase("source\\input.txt", "Собака\r\nСобака\r\nСобака\r\nКошку\r\nКошки\r\nОн\r\nВ\r\nТвое")] + [TestCase("source\\input.doc", "Собака\vСобака\vСобака\vКошку\vКошки\vОн\vВ\vТвое")] + [TestCase("source\\input.docx", "Собака\nСобака\nСобака\nКошку\nКошки\nОн\nВ\nТвое")] + public void Read_ShouldReturnCorrectStringFromFile(string path, string expected) + { + var absolutePath = Path.GetFullPath(path); + var fileReaderFactory = Scope.Resolve(); + + var result = fileReaderFactory.GetFileReader(absolutePath).Read(absolutePath); + + result.Should().BeEquivalentTo(expected); + } +} \ No newline at end of file diff --git a/TagsCloudVisualizationTests/TagsCloudVisualizationTests.csproj b/TagsCloudVisualizationTests/TagsCloudVisualizationTests.csproj new file mode 100644 index 00000000..bc10eea9 --- /dev/null +++ b/TagsCloudVisualizationTests/TagsCloudVisualizationTests.csproj @@ -0,0 +1,44 @@ + + + + net8.0 + enable + enable + + false + true + TagsCloudVisualizationTests + + + + + + + + + + + + + + + + + + + + Always + + + + + + Always + + + + + + + + diff --git a/TagsCloudVisualizationTests/Utilities/mystem.exe b/TagsCloudVisualizationTests/Utilities/mystem.exe new file mode 100644 index 00000000..e7158ff1 Binary files /dev/null and b/TagsCloudVisualizationTests/Utilities/mystem.exe differ diff --git a/TagsCloudVisualizationTests/WordPreprocessorTests.cs b/TagsCloudVisualizationTests/WordPreprocessorTests.cs new file mode 100644 index 00000000..98e52452 --- /dev/null +++ b/TagsCloudVisualizationTests/WordPreprocessorTests.cs @@ -0,0 +1,71 @@ +using Autofac; +using FluentAssertions; +using TagsCloudVisualization; +using TagsCloudVisualization.ConsoleCommands; +using TagsCloudVisualization.FileReaders; +using TagsCloudVisualization.WordPreprocessors; + +namespace TagsCloudVisualizationTests; + +[TestFixture] +public class WordPreprocessorTests +{ + private static readonly string PathToTestFile = Path.GetFullPath("source\\input.txt"); + private ILifetimeScope Scope { get; set; } + + [SetUp] + public void Setup() + { + var options = new Options + { + InputFilePath = PathToTestFile, + OutputDirectory = "/path/in/test/not/needed", + }; + var container = ContainerConfig.Configure(options); + Scope = container.BeginLifetimeScope(); + } + + [TearDown] + public void TearDown() + { + Scope.Dispose(); + } + + [Test] + public void ProcessTextToWords_ShouldContainOnlyNotBoringWordsReducedToInitialFormWithLowerCase() + { + var words = ReadTestFile(); + var result = new List> + { + new("кошка", 2), + new("собака", 3) + }; + + var wordProcessor = Scope.Resolve(); + wordProcessor.ProcessTextToWords(words).Should().BeEquivalentTo(result); + + } + + [Test] + public void ProcessTextToWords_ShouldContainCorrectCountOfWords() + { + var words = ReadTestFile(); + + var wordProcessor = Scope.Resolve(); + wordProcessor.ProcessTextToWords(words).Should().HaveCount(2); + } + + [Test] + public void ProcessTextToWords_ShouldBeOrderedByNumberOfWords() + { + var words = ReadTestFile(); + + var wordProcessor = Scope.Resolve(); + wordProcessor.ProcessTextToWords(words).Should().BeInDescendingOrder(word => word.Item2); + } + + private string ReadTestFile() => + Scope.Resolve() + .GetFileReader(PathToTestFile) + .Read(PathToTestFile); +} \ No newline at end of file diff --git a/TagsCloudVisualizationTests/WordValidatorTests.cs b/TagsCloudVisualizationTests/WordValidatorTests.cs new file mode 100644 index 00000000..809a8f17 --- /dev/null +++ b/TagsCloudVisualizationTests/WordValidatorTests.cs @@ -0,0 +1,27 @@ +using FluentAssertions; +using TagsCloudVisualization.MyStemWrapper; +using TagsCloudVisualization.WordPreprocessors.WordValidators; + +namespace TagsCloudVisualizationTests; + +[TestFixture] +public class WordValidatorTests +{ + [TestCaseSource(nameof(WordValidatorSourceTestCases))] + public void IsValid_ShouldValidateCorrectly(WordInfo wordInfo, bool isValid) + { + new DefaultWordValidator().IsValid(wordInfo).Should().Be(isValid); + } + + private static IEnumerable WordValidatorSourceTestCases() + { + yield return new TestCaseData(new WordInfo { Grammeme = "CONJ", Lemma = "text" }, false); + yield return new TestCaseData(new WordInfo { Grammeme = "INTJ", Lemma = "text" }, false); + yield return new TestCaseData(new WordInfo { Grammeme = "PART", Lemma = "text" }, false); + yield return new TestCaseData(new WordInfo { Grammeme = "PR", Lemma = "text" }, false); + yield return new TestCaseData(new WordInfo { Grammeme = "SPRO", Lemma = "text" }, false); + yield return new TestCaseData(new WordInfo { Grammeme = "V", Lemma = "text" }, true); + yield return new TestCaseData(new WordInfo { Grammeme = "NUM", Lemma = "text" }, true); + yield return new TestCaseData(new WordInfo { Grammeme = "A", Lemma = "text" }, true); + } +} \ No newline at end of file diff --git a/TagsCloudVisualizationTests/source/input.doc b/TagsCloudVisualizationTests/source/input.doc new file mode 100644 index 00000000..dfb7aec6 Binary files /dev/null and b/TagsCloudVisualizationTests/source/input.doc differ diff --git a/TagsCloudVisualizationTests/source/input.docx b/TagsCloudVisualizationTests/source/input.docx new file mode 100644 index 00000000..a53c4692 Binary files /dev/null and b/TagsCloudVisualizationTests/source/input.docx differ diff --git a/TagsCloudVisualizationTests/source/input.txt b/TagsCloudVisualizationTests/source/input.txt new file mode 100644 index 00000000..4a15adee --- /dev/null +++ b/TagsCloudVisualizationTests/source/input.txt @@ -0,0 +1,8 @@ +Собака +Собака +Собака +Кошку +Кошки +Он +В +Твое \ No newline at end of file diff --git a/di.sln b/di.sln index a50991da..7b09b488 100644 --- a/di.sln +++ b/di.sln @@ -2,6 +2,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "di", "FractalPainter/di.csproj", "{4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudVisualization", "TagsCloudVisualization\TagsCloudVisualization.csproj", "{C23F742B-3481-4425-9447-6F7EB35EF6CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudVisualizationTests", "TagsCloudVisualizationTests\TagsCloudVisualizationTests.csproj", "{BEFE089A-DDA0-43B8-8D46-5AB3FFD0D9C5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +16,13 @@ Global {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Release|Any CPU.Build.0 = Release|Any CPU + {C23F742B-3481-4425-9447-6F7EB35EF6CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C23F742B-3481-4425-9447-6F7EB35EF6CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C23F742B-3481-4425-9447-6F7EB35EF6CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C23F742B-3481-4425-9447-6F7EB35EF6CE}.Release|Any CPU.Build.0 = Release|Any CPU + {BEFE089A-DDA0-43B8-8D46-5AB3FFD0D9C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BEFE089A-DDA0-43B8-8D46-5AB3FFD0D9C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEFE089A-DDA0-43B8-8D46-5AB3FFD0D9C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BEFE089A-DDA0-43B8-8D46-5AB3FFD0D9C5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal