diff --git a/TagCloud/AppSettings/IAppSettings.cs b/TagCloud/AppSettings/IAppSettings.cs new file mode 100644 index 000000000..f9b922a22 --- /dev/null +++ b/TagCloud/AppSettings/IAppSettings.cs @@ -0,0 +1,17 @@ +namespace TagCloud.AppSettings; + +public interface IAppSettings +{ + public string InputPath { get; } + public string OutputPath { get; } + public string ImageExtension { get; } + public string FontType { get; } + public int CloudWidth { get; } + public int CloudHeight { get; } + public string LayouterType { get; } + public int CloudDensity { get; } + public bool UseRandomPalette { get; } + public string BackgroundColor { get; } + public string ForegroundColor { get; } + public string BoringWordsFile { get; } +} \ No newline at end of file diff --git a/TagCloud/AppSettings/Settings.cs b/TagCloud/AppSettings/Settings.cs new file mode 100644 index 000000000..5ddc38455 --- /dev/null +++ b/TagCloud/AppSettings/Settings.cs @@ -0,0 +1,42 @@ +using CommandLine; + +namespace TagCloud.AppSettings; + +public class Settings : IAppSettings +{ + [Option('s', "sourceFile", Default = "text.txt", HelpText = "Path to file with words to visualize")] + public string InputPath { get; set; } + + [Option('o', "outputPath", Default = "result", HelpText = "Path to output image file")] + public string OutputPath { get; set; } + + [Option('e', "extensionImage", Default = "png", HelpText = "Output image file format")] + public string ImageExtension { get; set; } + + [Option('f', "fontType", Default = "SansSerif", HelpText = "Font type of words")] + public string FontType { get; set; } + + [Option('W', "width", Default = 1920, HelpText = "Width of cloud")] + public int CloudWidth { get; set; } + + [Option('H', "height", Default = 1080, HelpText = "Height of cloud")] + public int CloudHeight { get; set; } + + [Option('l', "layouter", Default = "Spiral", HelpText = "Cloud layouter algorithm")] + public string LayouterType { get; set; } + + [Option('d', "density", Default = 1, HelpText = "Density of cloud")] + public int CloudDensity { get; set; } + + [Option('r', "randomPalette", Default = true, HelpText = "Use random colors")] + public bool UseRandomPalette { get; set; } + + [Option("background", Default = "White", HelpText = "Cloud layouter algorithm")] + public string BackgroundColor { get; set; } + + [Option("foreground", Default = "Black", HelpText = "Cloud layouter algorithm")] + public string ForegroundColor { get; set; } + + [Option("boringWordsFile", Default = null, HelpText = "Cloud layouter algorithm")] + public string BoringWordsFile { get; set; } +} \ No newline at end of file diff --git a/TagCloud/Configurator.cs b/TagCloud/Configurator.cs new file mode 100644 index 000000000..022973dde --- /dev/null +++ b/TagCloud/Configurator.cs @@ -0,0 +1,65 @@ +using System.Drawing; +using Autofac; +using CommandLine; +using TagCloud.AppSettings; +using TagCloud.Drawer; +using TagCloud.FileReader; +using TagCloud.FileSaver; +using TagCloud.Filter; +using TagCloud.PointGenerator; +using TagCloud.UserInterface; +using TagCloud.WordRanker; +using TagCloud.WordsPreprocessor; + +namespace TagCloud; + +public class Configurator +{ + public static IAppSettings Parse(string[] args, ContainerBuilder builder) + { + var settings = Parser.Default.ParseArguments(args).WithParsed(o => + { + if (o.UseRandomPalette) + builder.RegisterType().As(); + else + builder.Register(p => + new CustomPalette(Color.FromName(o.ForegroundColor), Color.FromName(o.BackgroundColor))); + var filter = new WordFilter().UsingFilter((word) => word.Length > 3); + if (string.IsNullOrEmpty(o.BoringWordsFile)) + builder.Register(c => filter).As(); + else + { + var boringWords = new TxtReader().ReadLines(o.BoringWordsFile); + builder.Register(c => filter.UsingFilter((word) => !boringWords.Contains(word))); + } + }); + + return settings.Value; + } + + public static ContainerBuilder BuildWithSettings(IAppSettings settings, ContainerBuilder builder) + { + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + + builder.RegisterType().As(); + + builder.Register(c => new WordFilter().UsingFilter((word) => word.Length > 3)).As(); + builder.Register(c => + new SpiralGenerator(new Point(settings.CloudWidth / 2, settings.CloudWidth / 2), settings.CloudDensity)) + .As(); + builder.Register(c => new CirclesGenerator(new Point(settings.CloudWidth / 2, settings.CloudWidth / 2))) + .As(); + builder.Register(c => new FileReaderProvider(c.Resolve>())).As(); + builder.Register(c => new PointGeneratorProvider(c.Resolve>())) + .As(); + + builder.Register(c => settings).AsImplementedInterfaces(); + + return builder; + } +} \ No newline at end of file diff --git a/TagCloud/Drawer/CloudDrawer.cs b/TagCloud/Drawer/CloudDrawer.cs new file mode 100644 index 000000000..10ca9ad19 --- /dev/null +++ b/TagCloud/Drawer/CloudDrawer.cs @@ -0,0 +1,79 @@ +using System.Drawing; +using TagCloud.AppSettings; +using TagCloud.Layouter; +using TagCloud.PointGenerator; + +namespace TagCloud.Drawer; + +public class CloudDrawer : IDrawer +{ + private readonly ILayouter layouter; + private readonly IPalette palette; + private readonly IAppSettings appSettings; + private int minimalRank; + private int maximalRank; + private const int MaximalFontSize = 50; + private const int LengthSizeMultiplier = 35; + + public CloudDrawer(IPointGeneratorProvider pointGenerator, IPalette palette, IAppSettings appSettings) + { + layouter = new CloudLayouter(pointGenerator.CreateGenerator(appSettings.LayouterType)); + this.palette = palette; + this.appSettings = appSettings; + } + + public Bitmap DrawTagCloud(IEnumerable<(string word, int rank)> words) + { + var tags = PlaceWords(words); + var imageSize = new Size(appSettings.CloudWidth, appSettings.CloudHeight); + var shift = GetImageShift(layouter.Rectangles); + var image = new Bitmap(imageSize.Width, imageSize.Height); + using var graphics = Graphics.FromImage(image); + using var background = new SolidBrush(palette.BackgroudColor); + graphics.FillRectangle(background, 0, 0, imageSize.Width, imageSize.Height); + foreach (var tag in tags) + { + var shiftedCoordinates = new PointF(tag.Position.X - shift.Width, tag.Position.Y - shift.Height); + using var brush = new SolidBrush(palette.ForegroundColor); + graphics.DrawString(tag.Value, new Font(appSettings.FontType, tag.FontSize), brush, shiftedCoordinates); + } + + return image; + } + + private IList PlaceWords(IEnumerable<(string word, int rank)> words) + { + maximalRank = words.First().rank; + minimalRank = words.Last().rank - 1; + + var tags = new List(); + + foreach (var pair in words) + { + var fontSize = CalculateFontSize(pair.rank); + var boxLength = CalculateWordBoxLength(pair.word.Length, fontSize); + var rectangle = layouter.PutNextRectangle(new Size(boxLength, fontSize)); + tags.Add(new Tag(pair.word, rectangle, fontSize)); + } + + return tags; + } + + private int CalculateFontSize(int rank) + { + return (MaximalFontSize * (rank - minimalRank)) / (maximalRank - minimalRank); + } + + private int CalculateWordBoxLength(int length, int fontSize) + { + return (int)Math.Round(length * LengthSizeMultiplier * ((double)fontSize / MaximalFontSize)); + } + + private static Size GetImageShift(IList rectangles) + { + var minX = rectangles.Min(rectangle => rectangle.Left); + var minY = rectangles.Min(rectangle => rectangle.Top); + + return new Size(minX, minY); + } +} \ No newline at end of file diff --git a/TagCloud/Drawer/CustomPalette.cs b/TagCloud/Drawer/CustomPalette.cs new file mode 100644 index 000000000..897a27399 --- /dev/null +++ b/TagCloud/Drawer/CustomPalette.cs @@ -0,0 +1,18 @@ +using System.Drawing; + +namespace TagCloud.Drawer; + +public class CustomPalette : IPalette +{ + private Color foregroundColor; + private Color backgroundColor; + + public CustomPalette(Color foregroundColor, Color backgroundColor) + { + this.foregroundColor = foregroundColor; + this.backgroundColor = backgroundColor; + } + + public Color ForegroundColor => foregroundColor; + public Color BackgroudColor => backgroundColor; +} \ No newline at end of file diff --git a/TagCloud/Drawer/IDrawer.cs b/TagCloud/Drawer/IDrawer.cs new file mode 100644 index 000000000..6524cf758 --- /dev/null +++ b/TagCloud/Drawer/IDrawer.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagCloud.Drawer; + +public interface IDrawer +{ + Bitmap DrawTagCloud(IEnumerable<(string word, int rank)> words); +} \ No newline at end of file diff --git a/TagCloud/Drawer/IPalette.cs b/TagCloud/Drawer/IPalette.cs new file mode 100644 index 000000000..91896f82e --- /dev/null +++ b/TagCloud/Drawer/IPalette.cs @@ -0,0 +1,9 @@ +using System.Drawing; + +namespace TagCloud.Drawer; + +public interface IPalette +{ + Color ForegroundColor { get; } + Color BackgroudColor { get; } +} \ No newline at end of file diff --git a/TagCloud/Drawer/RandomPalette.cs b/TagCloud/Drawer/RandomPalette.cs new file mode 100644 index 000000000..5b39a2fd9 --- /dev/null +++ b/TagCloud/Drawer/RandomPalette.cs @@ -0,0 +1,11 @@ +using System.Drawing; + +namespace TagCloud.Drawer; + +public class RandomPalette : IPalette +{ + private Random random = new(); + + public Color ForegroundColor => Color.FromArgb(random.Next(0, 255), random.Next(0, 255), random.Next(0, 255)); + public Color BackgroudColor => Color.White; +} \ No newline at end of file diff --git a/TagCloud/Drawer/Tag.cs b/TagCloud/Drawer/Tag.cs new file mode 100644 index 000000000..095a48fa6 --- /dev/null +++ b/TagCloud/Drawer/Tag.cs @@ -0,0 +1,17 @@ +using System.Drawing; + +namespace TagCloud.Drawer; + +public class Tag +{ + public string Value; + public Rectangle Position; + public int FontSize; + + public Tag(string value, Rectangle position, int fontSize) + { + Value = value; + Position = position; + FontSize = fontSize; + } +} \ No newline at end of file diff --git a/TagCloud/FileReader/DocReader.cs b/TagCloud/FileReader/DocReader.cs new file mode 100644 index 000000000..b19c19815 --- /dev/null +++ b/TagCloud/FileReader/DocReader.cs @@ -0,0 +1,19 @@ +using Spire.Doc; + +namespace TagCloud.FileReader; + +public class DocReader : IFileReader +{ + public IEnumerable ReadLines(string inputPath) + { + if (!File.Exists(inputPath)) + throw new ArgumentException("Source file doesn't exist"); + + var document = new Document(inputPath, FileFormat.Auto); + var text = document.GetText(); + + return text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(1); + } + + public IList GetAvailableExtensions() => new List() { "doc", "docx" }; +} \ No newline at end of file diff --git a/TagCloud/FileReader/FileReaderProvider.cs b/TagCloud/FileReader/FileReaderProvider.cs new file mode 100644 index 000000000..20bb8ab84 --- /dev/null +++ b/TagCloud/FileReader/FileReaderProvider.cs @@ -0,0 +1,33 @@ +namespace TagCloud.FileReader; + +public class FileReaderProvider : IFileReaderProvider +{ + private Dictionary readers; + + public FileReaderProvider(IEnumerable readers) + { + this.readers = ArrangeReaders(readers); + } + + public IFileReader CreateReader(string inputPath) + { + var extension = inputPath.Split(".").Last(); + if (readers.ContainsKey(extension)) + return readers[extension]; + throw new ArgumentException($"{extension} file type is not supported"); + } + + private Dictionary ArrangeReaders(IEnumerable readers) + { + var readersDictionary = new Dictionary(); + foreach (var reader in readers) + { + foreach (var extension in reader.GetAvailableExtensions()) + { + readersDictionary[extension] = reader; + } + } + + return readersDictionary; + } +} \ No newline at end of file diff --git a/TagCloud/FileReader/IFileReader.cs b/TagCloud/FileReader/IFileReader.cs new file mode 100644 index 000000000..6c32bfc8a --- /dev/null +++ b/TagCloud/FileReader/IFileReader.cs @@ -0,0 +1,8 @@ +namespace TagCloud.FileReader; + +public interface IFileReader +{ + IEnumerable ReadLines(string inputPath); + + IList GetAvailableExtensions(); +} \ No newline at end of file diff --git a/TagCloud/FileReader/IFileReaderProvider.cs b/TagCloud/FileReader/IFileReaderProvider.cs new file mode 100644 index 000000000..37c156359 --- /dev/null +++ b/TagCloud/FileReader/IFileReaderProvider.cs @@ -0,0 +1,6 @@ +namespace TagCloud.FileReader; + +public interface IFileReaderProvider +{ + IFileReader CreateReader(string inputPath); +} \ No newline at end of file diff --git a/TagCloud/FileReader/TxtReader.cs b/TagCloud/FileReader/TxtReader.cs new file mode 100644 index 000000000..8cd3de0cd --- /dev/null +++ b/TagCloud/FileReader/TxtReader.cs @@ -0,0 +1,21 @@ +using System.Xml.XPath; + +namespace TagCloud.FileReader; + +public class TxtReader : IFileReader +{ + private List extensions = new() { "txt" }; + + public IEnumerable ReadLines(string inputPath) + { + if (!File.Exists(inputPath)) + throw new ArgumentException("Source file doesn't exist"); + + return File.ReadLines(inputPath); + } + + public IList GetAvailableExtensions() + { + return extensions; + } +} \ No newline at end of file diff --git a/TagCloud/FileSaver/ISaver.cs b/TagCloud/FileSaver/ISaver.cs new file mode 100644 index 000000000..58b6f035e --- /dev/null +++ b/TagCloud/FileSaver/ISaver.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagCloud.FileSaver; + +public interface ISaver +{ + void Save(Bitmap bitmap, string outputPath, string imageFormat); +} \ No newline at end of file diff --git a/TagCloud/FileSaver/ImageSaver.cs b/TagCloud/FileSaver/ImageSaver.cs new file mode 100644 index 000000000..8488e4503 --- /dev/null +++ b/TagCloud/FileSaver/ImageSaver.cs @@ -0,0 +1,18 @@ +using System.Drawing; + +namespace TagCloud.FileSaver; + +public class ImageSaver : ISaver +{ + private List supportedFormats = new() { "png", "jpg", "jpeg", "bmp", "gif" }; + + public void Save(Bitmap bitmap, string outputPath, string imageFormat) + { + if (!supportedFormats.Contains(imageFormat)) + throw new ArgumentException($"{imageFormat} format is not supported"); + using (bitmap) + { + bitmap.Save($"{outputPath}.{imageFormat}"); + } + } +} \ No newline at end of file diff --git a/TagCloud/Filter/IFilter.cs b/TagCloud/Filter/IFilter.cs new file mode 100644 index 000000000..21aa1c12c --- /dev/null +++ b/TagCloud/Filter/IFilter.cs @@ -0,0 +1,8 @@ +namespace TagCloud.Filter; + +public interface IFilter +{ + IEnumerable FilterWords(IEnumerable words); + + IFilter UsingFilter(Func filter); +} \ No newline at end of file diff --git a/TagCloud/Filter/WordFilter.cs b/TagCloud/Filter/WordFilter.cs new file mode 100644 index 000000000..1b5cd5201 --- /dev/null +++ b/TagCloud/Filter/WordFilter.cs @@ -0,0 +1,18 @@ +namespace TagCloud.Filter; + +public class WordFilter : IFilter +{ + private readonly List> filters = []; + + public IEnumerable FilterWords(IEnumerable words) + { + return words.Where(word => filters.All(f => f(word))); + } + + public IFilter UsingFilter(Func filter) + { + filters.Add(filter); + + return this; + } +} \ No newline at end of file diff --git a/TagCloud/Layouter/CloudLayouter.cs b/TagCloud/Layouter/CloudLayouter.cs new file mode 100644 index 000000000..d5866c938 --- /dev/null +++ b/TagCloud/Layouter/CloudLayouter.cs @@ -0,0 +1,39 @@ +using System.Drawing; +using TagCloud.PointGenerator; + +namespace TagCloud.Layouter; + +public class CloudLayouter : ILayouter +{ + private readonly IPointGenerator pointGenerator; + public IList Rectangles { get; } + + public CloudLayouter(IPointGenerator pointGenerator) + { + Rectangles = new List(); + this.pointGenerator = pointGenerator; + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.Height <= 0 || rectangleSize.Width <= 0) + { + throw new ArgumentException("Rectangle size parameters should be positive"); + } + + var rectangle = new Rectangle(pointGenerator.GetNextPoint(), rectangleSize); + while (!CanPlaceRectangle(rectangle)) + { + rectangle = new Rectangle(pointGenerator.GetNextPoint() - rectangleSize / 2, rectangleSize); + } + + Rectangles.Add(rectangle); + + return rectangle; + } + + private bool CanPlaceRectangle(Rectangle newRectangle) + { + return !Rectangles.Any(rectangle => rectangle.IntersectsWith(newRectangle)); + } +} \ No newline at end of file diff --git a/TagCloud/Layouter/ILayouter.cs b/TagCloud/Layouter/ILayouter.cs new file mode 100644 index 000000000..a01569b93 --- /dev/null +++ b/TagCloud/Layouter/ILayouter.cs @@ -0,0 +1,9 @@ +using System.Drawing; + +namespace TagCloud.Layouter; + +public interface ILayouter +{ + IList Rectangles { get; } + Rectangle PutNextRectangle(Size rectangleSize); +} \ No newline at end of file diff --git a/TagCloud/PointGenerator/CirclesGenerator.cs b/TagCloud/PointGenerator/CirclesGenerator.cs new file mode 100644 index 000000000..80842693f --- /dev/null +++ b/TagCloud/PointGenerator/CirclesGenerator.cs @@ -0,0 +1,35 @@ +using System.Drawing; + +namespace TagCloud.PointGenerator; + +public class CirclesGenerator : IPointGenerator +{ + private readonly Point startPoint; + private int density; + private readonly double angleShift; + private double currentAngle; + + public string GeneratorName => "Circular"; + + public CirclesGenerator(Point startPoint, int density = 1, double angleShift = 0.01) + { + if (startPoint.X < 0 || startPoint.Y < 0) + throw new ArgumentException("Circle center point coordinates should be non-negative"); + this.startPoint = startPoint; + this.density = density * 200; + this.angleShift = angleShift; + } + + public Point GetNextPoint() + { + var radius = density; + var x = (int)(Math.Cos(currentAngle) * radius); + var y = (int)(Math.Sin(currentAngle) * radius); + currentAngle += angleShift; + + if (currentAngle < 2 * Math.PI && currentAngle + angleShift > 2 * Math.PI) + density += 100; + + return new Point(startPoint.X + x, startPoint.Y + y); + } +} \ No newline at end of file diff --git a/TagCloud/PointGenerator/IPointGenerator.cs b/TagCloud/PointGenerator/IPointGenerator.cs new file mode 100644 index 000000000..6bc7323d0 --- /dev/null +++ b/TagCloud/PointGenerator/IPointGenerator.cs @@ -0,0 +1,9 @@ +using System.Drawing; + +namespace TagCloud.PointGenerator; + +public interface IPointGenerator +{ + string GeneratorName { get; } + Point GetNextPoint(); +} \ No newline at end of file diff --git a/TagCloud/PointGenerator/IPointGeneratorProvider.cs b/TagCloud/PointGenerator/IPointGeneratorProvider.cs new file mode 100644 index 000000000..d32163863 --- /dev/null +++ b/TagCloud/PointGenerator/IPointGeneratorProvider.cs @@ -0,0 +1,6 @@ +namespace TagCloud.PointGenerator; + +public interface IPointGeneratorProvider +{ + IPointGenerator CreateGenerator(string generatorName); +} \ No newline at end of file diff --git a/TagCloud/PointGenerator/PointGeneratorProvider.cs b/TagCloud/PointGenerator/PointGeneratorProvider.cs new file mode 100644 index 000000000..7bcd6206f --- /dev/null +++ b/TagCloud/PointGenerator/PointGeneratorProvider.cs @@ -0,0 +1,29 @@ +namespace TagCloud.PointGenerator; + +public class PointGeneratorProvider : IPointGeneratorProvider +{ + private Dictionary registeredGenerators; + + public PointGeneratorProvider(IEnumerable generators) + { + registeredGenerators = ArrangeLayouters(generators); + } + + public IPointGenerator CreateGenerator(string generatorName) + { + if (registeredGenerators.ContainsKey(generatorName)) + return registeredGenerators[generatorName]; + throw new ArgumentException($"{generatorName} layouter is not supported"); + } + + private Dictionary ArrangeLayouters(IEnumerable generators) + { + var generatorsDictionary = new Dictionary(); + foreach (var generator in generators) + { + generatorsDictionary[generator.GeneratorName] = generator; + } + + return generatorsDictionary; + } +} \ No newline at end of file diff --git a/TagCloud/PointGenerator/SpiralGenerator.cs b/TagCloud/PointGenerator/SpiralGenerator.cs new file mode 100644 index 000000000..cbf60246f --- /dev/null +++ b/TagCloud/PointGenerator/SpiralGenerator.cs @@ -0,0 +1,32 @@ +using System.Drawing; + +namespace TagCloud.PointGenerator; + +public class SpiralGenerator : IPointGenerator +{ + private readonly Point startPoint; + private readonly int spiralDensity; + private readonly double angleShift; + private double currentAngle; + + public string GeneratorName => "Spiral"; + + public SpiralGenerator(Point startPoint, int spiralDensity = 1, double angleShift = 0.01) + { + if (startPoint.X < 0 || startPoint.Y < 0) + throw new ArgumentException("Spiral center point coordinates should be non-negative"); + this.startPoint = startPoint; + this.spiralDensity = spiralDensity; + this.angleShift = angleShift; + } + + public Point GetNextPoint() + { + var radius = spiralDensity * currentAngle; + var x = (int)(Math.Cos(currentAngle) * radius); + var y = (int)(Math.Sin(currentAngle) * radius); + currentAngle += angleShift; + + return new Point(startPoint.X + x, startPoint.Y + y); + } +} \ No newline at end of file diff --git a/TagCloud/Program.cs b/TagCloud/Program.cs new file mode 100644 index 000000000..044a854be --- /dev/null +++ b/TagCloud/Program.cs @@ -0,0 +1,18 @@ +using Autofac; +using TagCloud.UserInterface; + +namespace TagCloud; + +public class Program +{ + static void Main(string[] args) + { + var builder = new ContainerBuilder(); + var settings = Configurator.Parse(args, builder); + + builder = Configurator.BuildWithSettings(settings, builder); + + var container = builder.Build(); + container.Resolve().Run(settings); + } +} \ No newline at end of file diff --git a/TagCloud/TagCloud.csproj b/TagCloud/TagCloud.csproj new file mode 100644 index 000000000..e109de6ce --- /dev/null +++ b/TagCloud/TagCloud.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0-windows + enable + enable + + + + + + + + + + diff --git a/TagCloud/UserInterface/ConsoleUI.cs b/TagCloud/UserInterface/ConsoleUI.cs new file mode 100644 index 000000000..c1cbdf9ea --- /dev/null +++ b/TagCloud/UserInterface/ConsoleUI.cs @@ -0,0 +1,41 @@ +using TagCloud.AppSettings; +using TagCloud.Drawer; +using TagCloud.FileReader; +using TagCloud.FileSaver; +using TagCloud.Filter; +using TagCloud.WordRanker; +using TagCloud.WordsPreprocessor; + +namespace TagCloud.UserInterface; + +public class ConsoleUI : IUserInterface +{ + private readonly IFileReaderProvider readerProvider; + private readonly ISaver saver; + private readonly IDrawer drawer; + private readonly IWordRanker ranker; + private readonly IFilter filter; + private readonly IPreprocessor preprocessor; + + public ConsoleUI(IFileReaderProvider readerProvider, ISaver saver, IDrawer drawer, IWordRanker ranker, + IFilter filter, + IPreprocessor preprocessor) + { + this.readerProvider = readerProvider; + this.saver = saver; + this.drawer = drawer; + this.ranker = ranker; + this.filter = filter; + this.preprocessor = preprocessor; + } + + public void Run(IAppSettings appSettings) + { + var words = readerProvider.CreateReader($"{appSettings.InputPath}").ReadLines(appSettings.InputPath); + var preprocessed = preprocessor.HandleWords(words); + var filtered = filter.FilterWords(preprocessed); + var ranked = ranker.RankWords(filtered); + using var bitmap = drawer.DrawTagCloud(ranked); + saver.Save(bitmap, appSettings.OutputPath, appSettings.ImageExtension); + } +} \ No newline at end of file diff --git a/TagCloud/UserInterface/IUserInterface.cs b/TagCloud/UserInterface/IUserInterface.cs new file mode 100644 index 000000000..2372db3c6 --- /dev/null +++ b/TagCloud/UserInterface/IUserInterface.cs @@ -0,0 +1,8 @@ +using TagCloud.AppSettings; + +namespace TagCloud.UserInterface; + +public interface IUserInterface +{ + void Run(IAppSettings appSettings); +} \ No newline at end of file diff --git a/TagCloud/WordRanker/IWordRanker.cs b/TagCloud/WordRanker/IWordRanker.cs new file mode 100644 index 000000000..d86b8cfa3 --- /dev/null +++ b/TagCloud/WordRanker/IWordRanker.cs @@ -0,0 +1,6 @@ +namespace TagCloud.WordRanker; + +public interface IWordRanker +{ + IEnumerable<(string word, int rank)> RankWords(IEnumerable words); +} \ No newline at end of file diff --git a/TagCloud/WordRanker/WordRankerByFrequency.cs b/TagCloud/WordRanker/WordRankerByFrequency.cs new file mode 100644 index 000000000..15488d6ec --- /dev/null +++ b/TagCloud/WordRanker/WordRankerByFrequency.cs @@ -0,0 +1,10 @@ +namespace TagCloud.WordRanker; + +public class WordRankerByFrequency : IWordRanker +{ + public IEnumerable<(string word, int rank)> RankWords(IEnumerable words) + { + return words.GroupBy(word => word.Trim().ToLowerInvariant()).OrderByDescending(g => g.Count()).ToList() + .Select(g => ValueTuple.Create(g.Key, g.Count())); + } +} \ No newline at end of file diff --git a/TagCloud/WordsPreprocessor/DefaultPreprocessor.cs b/TagCloud/WordsPreprocessor/DefaultPreprocessor.cs new file mode 100644 index 000000000..35732260b --- /dev/null +++ b/TagCloud/WordsPreprocessor/DefaultPreprocessor.cs @@ -0,0 +1,9 @@ +namespace TagCloud.WordsPreprocessor; + +public class DefaultPreprocessor : IPreprocessor +{ + public IEnumerable HandleWords(IEnumerable words) + { + return words.Select(word => word.ToLower()); + } +} \ No newline at end of file diff --git a/TagCloud/WordsPreprocessor/IPreprocessor.cs b/TagCloud/WordsPreprocessor/IPreprocessor.cs new file mode 100644 index 000000000..28f70b4cf --- /dev/null +++ b/TagCloud/WordsPreprocessor/IPreprocessor.cs @@ -0,0 +1,6 @@ +namespace TagCloud.WordsPreprocessor; + +public interface IPreprocessor +{ + IEnumerable HandleWords(IEnumerable words); +} \ No newline at end of file diff --git a/TagCloud/result.png b/TagCloud/result.png new file mode 100644 index 000000000..6a5f676a7 Binary files /dev/null and b/TagCloud/result.png differ diff --git a/TagCloud/text.txt b/TagCloud/text.txt new file mode 100644 index 000000000..4f5c9e6db --- /dev/null +++ b/TagCloud/text.txt @@ -0,0 +1,385 @@ +Lorem +ipsum +dolor +sit +amet +consectetuer +adipiscing +elit +Phasellus +hendrerit +Pellentesque +aliquet +nibh +nec +urna +In +nisi +neque +aliquet +vel +dapibus +id +consectetuer +adipiscing +elit +Phasellus +hendrerit +Pellentesque +mattis +vel +nisi +Sed +pretium +ligula +sollicitudin +laoreet +viverra +tortor +libero +sodales +leo +eget +blandit +nunc +consectetuer +adipiscing +elit +Phasellus +hendrerit +Pellentesque +tortor +eu +nibh +Nullam +mollis +Ut +justo +Suspendisse +potenti +Sed +egestas +ante +et +vulputate +volutpat +eros +pede +semper +est +vitae +luctus +metus +libero +eu +augue +Morbi +purus +libero +consectetuer +adipiscing +elit +Phasellus +hendrerit +Pellentesque +faucibus +adipiscing +commodo +quis +gravida +id +est +Sed +lectus +Praesent +elementum +hendrerit +tortor +Sed +semper +lorem +at +felis +Vestibulum +volutpat +lacus +consectetuer +adipiscing +elit +Phasellus +hendrerit +Pellentesque +a +ultrices +sagittis +mi +neque +euismod +dui +eu +pulvinar +nunc +sapien +ornare +nisl +Phasellus +pede +arcu +dapibus +eu +fermentum +et +dapibus +sed +urna +hendrerit +tortor +Sed +semper +Morbi +interdum +mollis +sapien +Sed +ac +risus +Phasellus +lacinia +magna +a +ullamcorper +laoreet +lectus +arcu +pulvinar +risus +vitae +facilisis +Nullam +quis +massa +sit +amet +nibh +libero +dolor +a +purus +Sed +vel +lacus +Mauris +nibh +felis +hendrerit +tortor +Sed +semper +adipiscing +varius +adipiscing +in +lacinia +risus +vitae +facilisis +libero +dolor +vel +tellus +Suspendisse +ac +urna +Etiam +pellentesque +mauris +ut +lectus +Nunc +tellus +ante +mattis +eget +gravida +vitae +ultricies +ac +risus +Nullam +quis +massa +sit +amet +nibh +vitae +facilisis +libero +dolor +leo +Integer +leo +pede +ornare +a +lacinia +eu +vulputate +vel +nisl +Suspendisse +mauris +Fusce +accumsan +mollis +eros +risus +vitae +facilisis +libero +dolor +Pellentesque +a +diam +sit +amet +mi +ullamcorper +vehicula +Integer +adipiscing +risus +a +sem +Nullam +quis +massa +sit +amet +nibh +viverra +malesuada +Nunc +sem +lacus +accumsan +quis +faucibus +non +congue +Nullam +quis +massa +sit +amet +nibh +vel +arcu +Ut +scelerisque +hendrerit +tellus +Integer +sagittis +Vivamus +a +mauris +eget +arcu +gravida +tristique +Nunc +iaculis +mi +in +ante +Vivamus +imperdiet +nibh +feugiat +est +Ut +convallis +sem +sit +amet +interdum +consectetuer +odio +augue +aliquam +leo +nec +dapibus +tortor +nibh +sed +augue +Integer +eu +magna +sit +amet +metus +fermentum +posuere +Morbi +sit +amet +nulla +sed +dolor +elementum +imperdiet +Quisque +fermentum +Cum +sociis +natoque +penatibus +et +magnis +xdis +parturient +montes +nascetur +ridiculus +mus +Pellentesque +adipiscing +eros +ut +libero +Ut +condimentum +mi +vel +tellus +Suspendisse +laoreet +Fusce +ut +est +sed +dolor +gravida +convallis +Morbi +vitae +ante +Vivamus +ultrices +luctus +nunc +Suspendisse +et +dolor +Etiam +dignissim +Proin +malesuada +adipiscing +lacus +Donec +metus +Curabitur +gravida diff --git a/TagCloudTests/ConsoleUI_Should.cs b/TagCloudTests/ConsoleUI_Should.cs new file mode 100644 index 000000000..57a184203 --- /dev/null +++ b/TagCloudTests/ConsoleUI_Should.cs @@ -0,0 +1,54 @@ +using Autofac; +using CommandLine; +using TagCloud.AppSettings; +using TagCloud.Drawer; +using TagCloud.FileReader; +using TagCloud.Filter; +using TagCloud.UserInterface; + +namespace TagCloudTests; + +[TestFixture] +public class ConsoleUI_Should +{ + private IAppSettings settings; + private IUserInterface sut; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + settings = Parser.Default.ParseArguments(new List()).Value; + + var builder = new ContainerBuilder(); + builder = Configurator.BuildWithSettings(settings, builder); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + + var container = builder.Build(); + sut = container.Resolve(); + } + + [Test] + public void GenerateFileWithCorrectSettings() + { + sut.Run(settings); + + File.Exists($"{settings.OutputPath}.{settings.ImageExtension}").Should().BeTrue(); + + File.Delete($"{settings.OutputPath}.{settings.ImageExtension}"); + } + + private class FakeReader : IFileReader + { + public IEnumerable ReadLines(string inputPath) + { + yield return "test"; + } + + public IList GetAvailableExtensions() + { + return new List() { "txt" }; + } + } +} \ No newline at end of file diff --git a/TagCloudTests/IOTests/CloudSaver_Should.cs b/TagCloudTests/IOTests/CloudSaver_Should.cs new file mode 100644 index 000000000..1bb40a714 --- /dev/null +++ b/TagCloudTests/IOTests/CloudSaver_Should.cs @@ -0,0 +1,25 @@ +using NUnit.Framework.Internal; +using TagCloud.FileSaver; + +namespace TagCloudTests; + +[TestFixture] +public class CloudSaver_Should +{ + private ISaver sut = new ImageSaver(); + private const string filename = "test"; + private const string extension = "png"; + + + [Test] + public void SaveBitmapToFile() + { + using var bitmap = new Bitmap(50, 50); + + sut.Save(bitmap, filename, extension); + + File.Exists($"{filename}.{extension}").Should().BeTrue(); + + File.Delete($"{filename}.{extension}"); + } +} \ No newline at end of file diff --git a/TagCloudTests/IOTests/FileReader_Should.cs b/TagCloudTests/IOTests/FileReader_Should.cs new file mode 100644 index 000000000..f60156eff --- /dev/null +++ b/TagCloudTests/IOTests/FileReader_Should.cs @@ -0,0 +1,33 @@ +using Microsoft.VisualStudio.TestPlatform.Utilities; +using TagCloud.FileReader; + +namespace TagCloudTests; + +[TestFixture] +public class FileReader_Should +{ + private IFileReader sut = new TxtReader(); + private const string inputPath = "test.txt"; + private string text = $"one{Environment.NewLine}two{Environment.NewLine}three{Environment.NewLine}"; + + [SetUp] + public void SetUp() + { + using var fileStream = File.Open(inputPath, FileMode.Create); + using var writer = new StreamWriter(fileStream); + + writer.Write(text); + } + + [Test] + public void ReadWordsFromTxt() + { + var expected = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).ToList(); + + var result = sut.ReadLines(inputPath); + + result.Should().BeEquivalentTo(expected); + + File.Delete(inputPath); + } +} \ No newline at end of file diff --git a/TagCloudTests/LayouterTests/CircularCloudLayouter_Should.cs b/TagCloudTests/LayouterTests/CircularCloudLayouter_Should.cs new file mode 100644 index 000000000..0fca20ee1 --- /dev/null +++ b/TagCloudTests/LayouterTests/CircularCloudLayouter_Should.cs @@ -0,0 +1,72 @@ +using TagCloud.Layouter; +using TagCloud.PointGenerator; + +[TestFixture] +public class CircularCloudLayouter_Should +{ + private const int Width = 1920; + private const int Height = 1080; + private CloudLayouter sut; + + [SetUp] + public void Setup() + { + sut = new CloudLayouter(new SpiralGenerator(new Point(Width / 2, Height / 2))); + } + + [Test] + public void HaveEmptyRectanglesList_WhenCreated() + { + sut.Rectangles.Count.Should().Be(0); + } + + [TestCase(1, TestName = "One rectangle")] + [TestCase(10, TestName = "10 rectangles")] + public void HaveAllRectangles_WhichWerePut(int rectanglesCount) + { + for (var i = 0; i < rectanglesCount; i++) + { + sut.PutNextRectangle(new Size(2, 3)); + } + + sut.Rectangles.Count.Should().Be(rectanglesCount); + } + + [TestCase(0, 5, TestName = "Width is zero")] + [TestCase(5, 0, TestName = "Height is zero")] + [TestCase(-5, 5, TestName = "Width is negative")] + [TestCase(5, -5, TestName = "Height is negative")] + public void ThrowException_OnInvalidSizeArguments(int width, int height) + { + Action action = () => { sut.PutNextRectangle(new Size(0, 0)); }; + + action.Should().Throw(); + } + + [Test] + public void ContainNonIntersectingRectangles() + { + var random = new Random(); + + for (var i = 0; i < 50; i++) + { + sut.PutNextRectangle(new Size(50 + random.Next(0, 100), 50 + random.Next(0, 100))); + } + + HasIntersectedRectangles(sut.Rectangles).Should().Be(false); + } + + private bool HasIntersectedRectangles(IList rectangles) + { + for (var i = 0; i < rectangles.Count - 1; i++) + { + for (var j = i + 1; j < rectangles.Count; j++) + { + if (rectangles[i].IntersectsWith(rectangles[j])) + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/TagCloudTests/LayouterTests/SpiralGenerator_Should.cs b/TagCloudTests/LayouterTests/SpiralGenerator_Should.cs new file mode 100644 index 000000000..146e78bfb --- /dev/null +++ b/TagCloudTests/LayouterTests/SpiralGenerator_Should.cs @@ -0,0 +1,46 @@ +using TagCloud.PointGenerator; + +[TestFixture] +public class SpiralGenerator_Should +{ + private const int Width = 1920; + private const int Height = 1080; + private SpiralGenerator sut; + private Point startPoint; + + [SetUp] + public void Setup() + { + startPoint = new Point(Width / 2, Height / 2); + sut = new SpiralGenerator(startPoint, 1, 0.1); + } + + [Test] + public void ReturnCenterPoint_OnFirstCall() + { + sut.GetNextPoint().Should().BeEquivalentTo(startPoint); + } + + [Test] + public void ReturnDifferentPoints_AfterMultipleCalls() + { + var spiralPoints = new HashSet(); + + for (var i = 0; i < 50; i++) + { + spiralPoints.Add(sut.GetNextPoint()); + } + + spiralPoints.Count.Should().BeGreaterThan(1); + } + + [TestCase(-1, 1, TestName = "X is negative")] + [TestCase(1, -1, TestName = "Y is negative")] + [TestCase(-1, -1, TestName = "X and Y are negative")] + public void ThrowException_OnInvalidCenterPoint(int x, int y) + { + Action action = () => { new SpiralGenerator(new Point(x, y)); }; + + action.Should().Throw(); + } +} \ No newline at end of file diff --git a/TagCloudTests/TagCloudTests.csproj b/TagCloudTests/TagCloudTests.csproj new file mode 100644 index 000000000..dc210dce6 --- /dev/null +++ b/TagCloudTests/TagCloudTests.csproj @@ -0,0 +1,21 @@ + + + + net8.0-windows + enable + enable + + false + + + + + + + + + + + + + diff --git a/TagCloudTests/Usings.cs b/TagCloudTests/Usings.cs new file mode 100644 index 000000000..663d37242 --- /dev/null +++ b/TagCloudTests/Usings.cs @@ -0,0 +1,4 @@ +global using NUnit.Framework; +global using FluentAssertions; +global using TagCloud; +global using System.Drawing; \ No newline at end of file diff --git a/TagCloudTests/WordProcessorsTests/DefaultPreprocessor_Should.cs b/TagCloudTests/WordProcessorsTests/DefaultPreprocessor_Should.cs new file mode 100644 index 000000000..b46d6a7a8 --- /dev/null +++ b/TagCloudTests/WordProcessorsTests/DefaultPreprocessor_Should.cs @@ -0,0 +1,33 @@ +using TagCloud.WordsPreprocessor; + +namespace TagCloudTests.WordProcessorsTests; + +[TestFixture] +public class DefaultPreprocessor_Should +{ + private IPreprocessor sut = new DefaultPreprocessor(); + private List words = new() { "Aba", "caBC", "teSTword", "ZAZ", "word" }; + + [SetUp] + public void SetUp() + { + sut = new DefaultPreprocessor(); + } + + [Test] + public void ConvertAllWordsToLowerCase() + { + var expected = words.Select(w => w.ToLower()); + var result = sut.HandleWords(words); + + result.Should().BeEquivalentTo(expected); + } + + [Test] + public void ReturnEmpty_WhenNoWordsGiven() + { + var result = sut.HandleWords(new List()); + + result.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/TagCloudTests/WordProcessorsTests/WordFilter_Should.cs b/TagCloudTests/WordProcessorsTests/WordFilter_Should.cs new file mode 100644 index 000000000..660385913 --- /dev/null +++ b/TagCloudTests/WordProcessorsTests/WordFilter_Should.cs @@ -0,0 +1,46 @@ +using TagCloud.Filter; + +namespace TagCloudTests.WordProcessorsTests; + +[TestFixture] +public class WordFilter_Should +{ + private IFilter sut; + private List words = new() { "aba", "cabc", "testword", "zaz" }; + + [SetUp] + public void SetUp() + { + sut = new WordFilter(); + } + + [Test] + public void FilterWordList_ByGivenFilter() + { + sut = sut.UsingFilter(w => w.Length > 3); + + var result = sut.FilterWords(words); + + result.Should().Contain(new List { "cabc", "testword" }); + } + + [Test] + public void ReturnSameWords_WhenFilterIsUseless() + { + sut = sut.UsingFilter(w => true); + + var result = sut.FilterWords(words); + + result.Should().BeEquivalentTo(words); + } + + [Test] + public void ShouldReturnEmpty_WhenFilterFiltersAllWords() + { + sut = sut.UsingFilter(w => false); + + var result = sut.FilterWords(words); + + result.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/TagCloudTests/WordProcessorsTests/WordRanker_Should.cs b/TagCloudTests/WordProcessorsTests/WordRanker_Should.cs new file mode 100644 index 000000000..c009009c9 --- /dev/null +++ b/TagCloudTests/WordProcessorsTests/WordRanker_Should.cs @@ -0,0 +1,34 @@ +using TagCloud.WordRanker; + +namespace TagCloudTests.WordProcessorsTests; + +[TestFixture] +public class WordRanker_Should +{ + private IWordRanker sut; + private List words = new() { "aba", "caba", "aba", "a", "a", "a" }; + + [SetUp] + public void SetUp() + { + sut = new WordRankerByFrequency(); + } + + [Test] + public void CorrectlyRankWords() + { + var expected = words.GroupBy(word => word.Trim().ToLowerInvariant()).OrderByDescending(g => g.Count()).ToList() + .Select(g => ValueTuple.Create(g.Key, g.Count())); + var result = sut.RankWords(words); + + result.Should().BeEquivalentTo(expected); + } + + [Test] + public void ReturnEmpty_WhenNowordsGiven() + { + var result = sut.RankWords(new List()); + + result.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/di.sln b/di.sln index b27b7c05d..887024570 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}") = "FractalPainter", "FractalPainter\FractalPainter.csproj", "{4D70883B-6F8B-4166-802F-8EDC9BE93199}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloud", "TagCloud\TagCloud.csproj", "{08924E6D-5ECE-4367-A043-75EE79AE421C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudTests", "TagCloudTests\TagCloudTests.csproj", "{2B0195B5-2D91-439A-8B3C-913D41F05275}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +16,13 @@ Global {4D70883B-6F8B-4166-802F-8EDC9BE93199}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D70883B-6F8B-4166-802F-8EDC9BE93199}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D70883B-6F8B-4166-802F-8EDC9BE93199}.Release|Any CPU.Build.0 = Release|Any CPU + {08924E6D-5ECE-4367-A043-75EE79AE421C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08924E6D-5ECE-4367-A043-75EE79AE421C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08924E6D-5ECE-4367-A043-75EE79AE421C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08924E6D-5ECE-4367-A043-75EE79AE421C}.Release|Any CPU.Build.0 = Release|Any CPU + {2B0195B5-2D91-439A-8B3C-913D41F05275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B0195B5-2D91-439A-8B3C-913D41F05275}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B0195B5-2D91-439A-8B3C-913D41F05275}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B0195B5-2D91-439A-8B3C-913D41F05275}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal