diff --git a/TagCloudDI/CloudCreator.cs b/TagCloudDI/CloudCreator.cs new file mode 100644 index 00000000..fcbfdd5d --- /dev/null +++ b/TagCloudDI/CloudCreator.cs @@ -0,0 +1,25 @@ +using TagCloudDI.CloudVisualize; +using TagCloudDI.Data; + +namespace TagCloudDI +{ + public class CloudCreator + { + private DataProvider dataProvider; + private CloudVisualizer cloudVisualizer; + private readonly DefaultImageSaver imageSaver; + public CloudCreator(DataProvider dataProvider, CloudVisualizer visualizer) + { + this.dataProvider = dataProvider; + cloudVisualizer = visualizer; + imageSaver = new DefaultImageSaver(); + } + + public string CreateTagCloud(string pathToFileWithWords) + { + var words = dataProvider.GetPreprocessedWords(pathToFileWithWords); + var image = cloudVisualizer.CreateImage(words); + return imageSaver.SaveImage(image); + } + } +} \ No newline at end of file diff --git a/TagCloudDI/CloudLayouter/BruteForceNearestFinder.cs b/TagCloudDI/CloudLayouter/BruteForceNearestFinder.cs new file mode 100644 index 00000000..24716170 --- /dev/null +++ b/TagCloudDI/CloudLayouter/BruteForceNearestFinder.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace TagsCloudVisualization.CloudLayouter +{ + public static class BruteForceNearestFinder + { + public static Rectangle? FindNearestByDirection(Rectangle r, Direction direction, List rectangles) + { + if (rectangles.FirstOrDefault() == default) + return null; + var nearestByDirection = rectangles + .Select(possibleNearest => + (Distance: CalculateMinDistanceBy(direction, possibleNearest, r), Nearest: possibleNearest)) + .Where(el => el.Distance >= 0) + .ToList(); + + return nearestByDirection.Count > 0 ? nearestByDirection.MinBy(el => el.Distance).Nearest : null; + } + + + public static int CalculateMinDistanceBy(Direction direction, + Rectangle possibleNearest, Rectangle rectangleForFind) + { + return direction switch + { + Direction.Left => rectangleForFind.Left - possibleNearest.Right, + Direction.Right => possibleNearest.Left - rectangleForFind.Right, + Direction.Top => rectangleForFind.Top - possibleNearest.Bottom, + Direction.Bottom => possibleNearest.Top - rectangleForFind.Bottom, + }; + } + } +} diff --git a/TagCloudDI/CloudLayouter/CirclePositionDistributor.cs b/TagCloudDI/CloudLayouter/CirclePositionDistributor.cs new file mode 100644 index 00000000..2f8142ce --- /dev/null +++ b/TagCloudDI/CloudLayouter/CirclePositionDistributor.cs @@ -0,0 +1,34 @@ +using System; +using System.Drawing; + +namespace TagsCloudVisualization; + +public class CirclePositionDistributor +{ + public Point Center { get; } + public int Radius { get; private set; } + + private const int DeltaRadius = 5; + private int currentPositionDegrees; + + + public CirclePositionDistributor(Point center) + { + Center = center; + Radius = 5; + } + + public Point GetNextPosition() + { + currentPositionDegrees += 1; + if (currentPositionDegrees > 360) + { + currentPositionDegrees = 0; + Radius += DeltaRadius; + } + var positionAngleInRadians = currentPositionDegrees * Math.PI / 180.0; + return new Point( + Center.X + (int)Math.Ceiling(Radius * Math.Cos(positionAngleInRadians)), + Center.Y + (int)Math.Ceiling(Radius * Math.Sin(positionAngleInRadians))); + } +} \ No newline at end of file diff --git a/TagCloudDI/CloudLayouter/CircularCloudLayouter.cs b/TagCloudDI/CloudLayouter/CircularCloudLayouter.cs new file mode 100644 index 00000000..01bb4042 --- /dev/null +++ b/TagCloudDI/CloudLayouter/CircularCloudLayouter.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using TagCloudDI; + +namespace TagsCloudVisualization.CloudLayouter +{ + public class CircularCloudLayouter : ICloudLayouter + { + private readonly List storage; + private readonly CloudCompressor compressor; + private readonly CirclePositionDistributor distributor; + + public CircularCloudLayouter(): this(new Point(0,0), []) + { } + + private CircularCloudLayouter(Point center, List storage) + { + this.storage = storage; + distributor = new(center); + compressor = new(center, storage); + } + + public static CircularCloudLayouter CreateLayouterWithStartRectangles(Point center, List storage) + { + return new CircularCloudLayouter(center, storage); + } + + public Rectangle PutNextRectangle(Size nextRectangle) + { + ValidateRectangleSize(nextRectangle); + + var inserted = PutRectangleWithoutIntersection(nextRectangle); + var rectangleWithOptimalPosition = compressor.CompressCloudAfterInsertion(inserted); + + storage.Add(rectangleWithOptimalPosition); + + return rectangleWithOptimalPosition; + } + + public Rectangle PutRectangleWithoutIntersection(Size forInsertionSize) + { + bool isIntersected; + Rectangle forInsertion; + do + { + var possiblePosition = distributor.GetNextPosition(); + forInsertion = new Rectangle(possiblePosition, forInsertionSize); + isIntersected = forInsertion.IntersectedWithAnyFrom(storage); + } + while (isIntersected); + + return forInsertion; + } + + private static void ValidateRectangleSize(Size forInsertion) + { + if (forInsertion.Width <= 0 || forInsertion.Height <= 0) + throw new ArgumentException($"Rectangle has incorrect size: width = {forInsertion.Width}, height = {forInsertion.Height}"); + } + + public void Clear() + { + storage.Clear(); + } + } +} \ No newline at end of file diff --git a/TagCloudDI/CloudLayouter/CloudCompressor.cs b/TagCloudDI/CloudLayouter/CloudCompressor.cs new file mode 100644 index 00000000..64e3e58c --- /dev/null +++ b/TagCloudDI/CloudLayouter/CloudCompressor.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Drawing; +using TagsCloudVisualization.CloudLayouter; + +namespace TagsCloudVisualization +{ + internal class CloudCompressor + { + private readonly Point compressionPoint; + private readonly List cloud; + private int minDistanceForMoving = 1; + + public CloudCompressor(Point compressTo, List cloud) + { + compressionPoint = compressTo; + this.cloud = cloud; + } + + + public Rectangle CompressCloudAfterInsertion(Rectangle forInsertion) + { + var toCompressionPoint = GetDirectionsForMovingForCompress(forInsertion); + var beforeIntersection = forInsertion; + var prevDirectionHasIntersection = false; + for (var i = 0; i < toCompressionPoint.Count; i++) + { + var direction = toCompressionPoint[i]; + + while (!forInsertion.IntersectedWithAnyFrom(cloud) + && !IsIntersectCompressionPointAxis(direction, forInsertion)) + { + beforeIntersection = forInsertion; + var distance = GetDistanceForMoving(forInsertion, direction); + forInsertion.Location = MoveByDirection(forInsertion.Location, + distance == 0 ? minDistanceForMoving : distance, direction); + } + + var wasIntersection = !IsIntersectCompressionPointAxis(direction, forInsertion); + if (!prevDirectionHasIntersection && wasIntersection) + { + forInsertion = beforeIntersection; + prevDirectionHasIntersection = true; + } + } + return beforeIntersection; + } + + + private int GetDistanceForMoving(Rectangle forMoving, Direction toCompressionPoint) + { + var nearest = BruteForceNearestFinder.FindNearestByDirection(forMoving, toCompressionPoint, cloud); + if (nearest == null) return minDistanceForMoving; + return BruteForceNearestFinder.CalculateMinDistanceBy(toCompressionPoint, nearest.Value, forMoving); + } + + private bool IsIntersectCompressionPointAxis(Direction toCompressionPoint, Rectangle current) + { + return toCompressionPoint switch + { + Direction.Left => current.Left < compressionPoint.X, + Direction.Right => current.Right > compressionPoint.X, + Direction.Top => current.Top < compressionPoint.Y, + Direction.Bottom => current.Bottom > compressionPoint.Y + }; + } + + private static Point MoveByDirection(Point forMoving, int distance, Direction whereMoving) + { + var factorForDistanceByX = whereMoving switch + { + Direction.Left => -1, + Direction.Right => 1, + _ => 0 + }; + var factorForDistanceByY = whereMoving switch + { + Direction.Top => -1, + Direction.Bottom => 1, + _ => 0 + }; + forMoving.X += distance * factorForDistanceByX; + forMoving.Y += distance * factorForDistanceByY; + + return forMoving; + } + + private List GetDirectionsForMovingForCompress(Rectangle forMoving) + { + var directions = new List(); + if (forMoving.Bottom <= compressionPoint.Y) directions.Add(Direction.Bottom); + if (forMoving.Top >= compressionPoint.Y) directions.Add(Direction.Top); + if (forMoving.Left >= compressionPoint.X) directions.Add(Direction.Left); + if (forMoving.Right <= compressionPoint.X) directions.Add(Direction.Right); + + return directions; + } + } +} diff --git a/TagCloudDI/CloudLayouter/Direction.cs b/TagCloudDI/CloudLayouter/Direction.cs new file mode 100644 index 00000000..7401d100 --- /dev/null +++ b/TagCloudDI/CloudLayouter/Direction.cs @@ -0,0 +1,10 @@ +namespace TagsCloudVisualization.CloudLayouter +{ + public enum Direction + { + Left, + Right, + Top, + Bottom + } +} diff --git a/TagCloudDI/CloudVisualize/CloudVisualizer.cs b/TagCloudDI/CloudVisualize/CloudVisualizer.cs new file mode 100644 index 00000000..4276dbad --- /dev/null +++ b/TagCloudDI/CloudVisualize/CloudVisualizer.cs @@ -0,0 +1,90 @@ +using System.Drawing; + +namespace TagCloudDI.CloudVisualize +{ + public class CloudVisualizer + { + private VisualizeSettings settings; + private readonly ICloudLayouter layouter; + private readonly IWordColorDistributor distributor; + + + public CloudVisualizer(VisualizeSettings settings, ICloudLayouter cloudLayouter, IWordColorDistributor distributor) + { + this.settings = settings; + layouter = cloudLayouter; + this.distributor = distributor; + } + + public Bitmap CreateImage((string Word, double Frequency)[] source) + { + var words = LayoutWords(source).ToArray(); + var tmpImageSize = CalculateImageSize(words); + words = PlaceCloudInImage(words, tmpImageSize); + using var image = new Bitmap(tmpImageSize.Width, tmpImageSize.Height); + using var graphics = Graphics.FromImage(image); + + graphics.Clear(settings.BackgroundColor); + + for (var i = 0; i < words.Length; i++) + { + var currentWord = words[i]; + var font = new Font(settings.FontFamily, currentWord.FontSize); + var color = distributor.GetColor(settings.WordColors); + graphics.DrawRectangle(new Pen(color), currentWord.WordBorder); + graphics.DrawString(currentWord.Word, font, new SolidBrush(color), currentWord.WordBorder); + } + var resizedImage = new Bitmap(image, settings.ImageSize); + return resizedImage; + } + + private IEnumerable LayoutWords((string Word, double Frequency)[] words) + { + var g = Graphics.FromImage(new Bitmap(1, 1)); + foreach (var word in words) + { + var fontSize = Math.Max((float)(settings.MaxFontSize * word.Frequency), settings.MinFontSize); + var wordSize = g.MeasureString(word.Word, new Font(settings.FontFamily, fontSize)); + var wordSizeInt = new Size((int)Math.Ceiling(wordSize.Width), (int)Math.Ceiling(wordSize.Height)); + var border = layouter.PutNextRectangle(wordSizeInt); + yield return new WordParameters(word.Word, border, fontSize); + } + layouter.Clear(); + } + + private WordParameters[] PlaceCloudInImage(WordParameters[] words, Size tmpImageSize) + { + var deltaForX = CalculateDeltaForMoveByAxis(words, r => r.Left, r => r.Right, tmpImageSize.Width); + var deltaForY = CalculateDeltaForMoveByAxis(words, r => r.Top, r => r.Bottom, tmpImageSize.Height); + foreach (var word in words) + { + word.MoveBorderToNewLocation(new Point(word.WordBorder.Left + deltaForX, word.WordBorder.Y + deltaForY)); + } + return words; + } + + private int CalculateDeltaForMoveByAxis( + WordParameters[] words, + Func selectorForMin, + Func selectorForMax, + int sizeByAxis) + { + if (words.Length == 0) return 0; + var minByAxis = words.Min(w => selectorForMin(w.WordBorder)); + var maxByAxis = words.Max(w => selectorForMax(w.WordBorder)); + return minByAxis < 0 + ? -1 * minByAxis + : maxByAxis > sizeByAxis + ? sizeByAxis - maxByAxis + : 0; + } + + private Size CalculateImageSize(WordParameters[] words) + { + var width = words.Max(w => w.WordBorder.Right) - words.Min(w => w.WordBorder.Left); + var height = words.Max(w => w.WordBorder.Bottom) - words.Min(w => w.WordBorder.Top); + var sizeForRectangles = Math.Max(width, height); + return new Size(sizeForRectangles, sizeForRectangles); + } + } +} \ No newline at end of file diff --git a/TagCloudDI/CloudVisualize/DefaultImageSaver.cs b/TagCloudDI/CloudVisualize/DefaultImageSaver.cs new file mode 100644 index 00000000..0369cef2 --- /dev/null +++ b/TagCloudDI/CloudVisualize/DefaultImageSaver.cs @@ -0,0 +1,23 @@ +using System.Drawing.Imaging; +using System.Drawing; + +namespace TagCloudDI.CloudVisualize +{ + internal class DefaultImageSaver: IImageSaver + { + private ImageFormat format = ImageFormat.Png; + + public string SaveImage(Bitmap image, string? filePath = null) + { + var rnd = new Random(); + filePath ??= Path.Combine(Path.GetTempPath(), $"tagCloud{rnd.Next()}.{FormatToString()}"); + image.Save(filePath, format); + return filePath; + } + + private string? FormatToString() + { + return new ImageFormatConverter().ConvertToString(format)?.ToLower(); + } + } +} \ No newline at end of file diff --git a/TagCloudDI/CloudVisualize/IImageSaver.cs b/TagCloudDI/CloudVisualize/IImageSaver.cs new file mode 100644 index 00000000..6a433d41 --- /dev/null +++ b/TagCloudDI/CloudVisualize/IImageSaver.cs @@ -0,0 +1,9 @@ +using System.Drawing; + +namespace TagCloudDI.CloudVisualize +{ + public interface IImageSaver + { + public string SaveImage(Bitmap image, string filename); + } +} diff --git a/TagCloudDI/CloudVisualize/IWordColorDistributor.cs b/TagCloudDI/CloudVisualize/IWordColorDistributor.cs new file mode 100644 index 00000000..59baafdc --- /dev/null +++ b/TagCloudDI/CloudVisualize/IWordColorDistributor.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudDI.CloudVisualize +{ + public interface IWordColorDistributor + { + public Color GetColor(Color[] possibleColors); + } +} diff --git a/TagCloudDI/CloudVisualize/RandomWordColorDistributor.cs b/TagCloudDI/CloudVisualize/RandomWordColorDistributor.cs new file mode 100644 index 00000000..f759047b --- /dev/null +++ b/TagCloudDI/CloudVisualize/RandomWordColorDistributor.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudDI.CloudVisualize +{ + public class RandomWordColorDistributor : IWordColorDistributor + { + public Color GetColor(Color[] possibleColors) + { + var rnd = new Random(); + return possibleColors[rnd.Next(0, possibleColors.Length - 1)]; + } + } +} diff --git a/TagCloudDI/CloudVisualize/VisualizeSettings.cs b/TagCloudDI/CloudVisualize/VisualizeSettings.cs new file mode 100644 index 00000000..2906d18e --- /dev/null +++ b/TagCloudDI/CloudVisualize/VisualizeSettings.cs @@ -0,0 +1,18 @@ +using System.Drawing; + +namespace TagCloudDI.CloudVisualize +{ + public class VisualizeSettings + { + public FontFamily FontFamily { get; set; } = new FontFamily("Arial"); + public Color BackgroundColor { get; set; } = Color.White; + public Size ImageSize { get; set; } = new Size(5000, 5000); + public Color[] WordColors { get; set; } = new Color[] + { + Color.Blue, Color.Yellow, Color.Green, Color.Orange, Color.Orchid + }; + + public int MaxFontSize { get; set; } = 100; + public int MinFontSize { get; set; } = 11; + } +} \ No newline at end of file diff --git a/TagCloudDI/CloudVisualize/WordParameters.cs b/TagCloudDI/CloudVisualize/WordParameters.cs new file mode 100644 index 00000000..e0961c00 --- /dev/null +++ b/TagCloudDI/CloudVisualize/WordParameters.cs @@ -0,0 +1,23 @@ +using System.Drawing; + +namespace TagCloudDI.CloudVisualize +{ + public class WordParameters + { + public float FontSize { get; } + public Rectangle WordBorder { get; private set; } + public string Word { get; } + + public WordParameters(string word, Rectangle border, float fontSize) + { + WordBorder = border; + Word = word; + FontSize = fontSize; + } + + public void MoveBorderToNewLocation(Point location) + { + WordBorder = new Rectangle(location, WordBorder.Size); + } + } +} diff --git a/TagCloudDI/ConsoleInterface/App.cs b/TagCloudDI/ConsoleInterface/App.cs new file mode 100644 index 00000000..5e240135 --- /dev/null +++ b/TagCloudDI/ConsoleInterface/App.cs @@ -0,0 +1,107 @@ +using TagCloudDI.CloudVisualize; +using McMaster.Extensions.CommandLineUtils; +using System.Drawing; +using System.Diagnostics; + +namespace TagCloudDI.ConsoleInterface +{ + public class App + { + private CommandLineApplication app; + private CloudCreator creator; + private VisualizeSettings settings; + public App(CloudCreator creator, VisualizeSettings visualizeSettings) + { + this.creator = creator; + settings = visualizeSettings; + app = new CommandLineApplication(); + + app.HelpOption(); + app.Command("createTagCloud", ConfigureCreateCloudCommand); + app.Command("configure", ConfigureChangeImageParameterCommand); + } + + private void ConfigureCreateCloudCommand(CommandLineApplication cmd) + { + cmd.Description = "Create a tag cloud based on the transmitted text file"; + cmd.HelpOption(); + var filePath = cmd.Argument("file", "Path to file").IsRequired(); + cmd.OnExecute(() => + { + if (File.Exists(filePath.Value)) + { + var pathToImage = creator.CreateTagCloud(filePath.Value); + Console.WriteLine("Path to cloud generation result: " + pathToImage); + OpenImage(pathToImage); + } + else Console.WriteLine("Try read non existed file"); + + }); + } + + private void OpenImage(string pathToImage) + { + var p = new Process(); + p.StartInfo = new ProcessStartInfo(pathToImage) { UseShellExecute = true }; + p.Start(); + } + + private void ConfigureChangeImageParameterCommand(CommandLineApplication cmd) + { + var sizeSeparator = ";"; + cmd.Description = "Configure settings for tag cloud visualization"; + cmd.HelpOption(); + var font = cmd.Option("-f|--font ", "The font for words", CommandOptionType.SingleValue); + var fontSizeMin = cmd.Option("-fs-min|--fontSize ", "The min for font size", CommandOptionType.SingleValue); + var fontSizeMax = cmd.Option("-fs-max|--fontSize ", "The max for font size", CommandOptionType.SingleValue); + var wordsColor = cmd.Option("-wc|--word-color ", "The words colors", CommandOptionType.MultipleValue); + var backgroundColor = cmd.Option("-bg|--background-color ", "The background color", CommandOptionType.SingleValue); + var imageSize = cmd.Option($"-i|--imageSize ", "The size for image in pixel", CommandOptionType.SingleValue); + cmd.OnExecute(() => + { + if (font.HasValue()) + settings.FontFamily = new FontFamily(font.Value()); + if (fontSizeMin.HasValue()) + { + settings.MinFontSize = int.TryParse(fontSizeMin.Value(), out var min) ? min : settings.MinFontSize; + } + if (fontSizeMax.HasValue()) + { + settings.MaxFontSize = int.TryParse(fontSizeMax.Value(), out var max) ? max : settings.MaxFontSize; + } + if (imageSize.HasValue()) + { + var values = imageSize.Value().Split(sizeSeparator); + var hasWidth = int.TryParse(values[0], out var width); + var hasHeight = int.TryParse(values[1], out var height); + settings.ImageSize = hasHeight && hasWidth ? new Size(width, height) : Size.Empty; + } + if (wordsColor.HasValue()) + { + settings.WordColors = wordsColor.Values.Select(c => Color.FromName(c)).ToArray(); + } + if (backgroundColor.HasValue()) + { + var color = Color.FromName(backgroundColor.Value()); + settings.BackgroundColor = color; + } + }); + } + + public void Run() + { + while (true) + { + var args = Console.ReadLine(); + try + { + app.Execute(args.Split(' ')); + } + catch (UnrecognizedCommandParsingException e) + { + Console.WriteLine(e.Message); + } + } + } + } +} diff --git a/TagCloudDI/Data/DataProvider.cs b/TagCloudDI/Data/DataProvider.cs new file mode 100644 index 00000000..e6af5762 --- /dev/null +++ b/TagCloudDI/Data/DataProvider.cs @@ -0,0 +1,63 @@ +using TagCloudDI.WordHandlers; + +namespace TagCloudDI.Data +{ + public class DataProvider + { + private Func getSource; + private IEnumerable transformers; + private IEnumerable filters; + private IDataParser parser; + public DataProvider( + Func factory, + IEnumerable transformers, + IEnumerable filters, + IDataParser parser) + { + this.transformers = transformers; + this.filters = filters; + getSource = factory; + this.parser = parser; + } + + public (string Word, double Frequency)[] GetPreprocessedWords(string filePath) + { + var source = getSource(Path.GetExtension(filePath)); + var text = source.GetData(filePath); + var words = parser.Parse(text); + return PreprocessData(words); + } + + private (string Word, double Frequency)[] PreprocessData(string[] words) + { + var wordInfos = WordInfo.GetInfoFromWords(words); + var preprocessedWords = wordInfos + .Where(w => filters.All(f => f.Accept(w))) + .Select(ApplyTransformers); + var wordsWithFrequency = CalculateFrequency(preprocessedWords); + return wordsWithFrequency.Select(kvp => (Word : kvp.Key, Frequency : kvp.Value)).ToArray(); + } + + private Dictionary CalculateFrequency(IEnumerable words) + { + var frequency = new Dictionary(); + foreach (var word in words) + { + if (!frequency.ContainsKey(word)) + frequency.Add(word, 0); + frequency[word]++; + } + var maxFrequency = frequency.Max(kvp => kvp.Value); + return frequency.ToDictionary(kvp => kvp.Key, kvp => kvp.Value / maxFrequency); + } + + private string ApplyTransformers(WordInfo word) + { + foreach (var transformer in transformers) + { + word = transformer.Apply(word); + } + return word.InitialForm; + } + } +} diff --git a/TagCloudDI/Data/DefaultDataSource.cs b/TagCloudDI/Data/DefaultDataSource.cs new file mode 100644 index 00000000..1aa12963 --- /dev/null +++ b/TagCloudDI/Data/DefaultDataSource.cs @@ -0,0 +1,11 @@ +namespace TagCloudDI.Data +{ + internal class DefaultDataSource : IFileDataSource + { + public string GetData(string filePath) + { + byte[] bytes = File.ReadAllBytes(filePath); + return System.Text.Encoding.UTF8.GetString(bytes); + } + } +} diff --git a/TagCloudDI/Data/DocFileDataSource.cs b/TagCloudDI/Data/DocFileDataSource.cs new file mode 100644 index 00000000..a76715e5 --- /dev/null +++ b/TagCloudDI/Data/DocFileDataSource.cs @@ -0,0 +1,27 @@ +using System.Text; +using Spire.Doc; +using Spire.Doc.Documents; + +namespace TagCloudDI.Data +{ + public class DocFileDataSource : IFileDataSource + { + public string GetData(string filePath) + { + var text = new StringBuilder(); + var doc = new Document(); + doc.LoadFromFile(filePath); + + foreach (Section section in doc.Sections) + { + foreach (Paragraph paragraph in section.Paragraphs) + { + text.AppendLine(paragraph.Text); + } + } + + return text.ToString().Trim(); + } + } + +} diff --git a/TagCloudDI/Data/DocxFileDataSource.cs b/TagCloudDI/Data/DocxFileDataSource.cs new file mode 100644 index 00000000..292c2367 --- /dev/null +++ b/TagCloudDI/Data/DocxFileDataSource.cs @@ -0,0 +1,17 @@ +using System.Text; +using Xceed.Words.NET; +namespace TagCloudDI.Data +{ + public class DocxFileDataSource : IFileDataSource + { + public string GetData(string filePath) + { + var text = new StringBuilder(); + using (var document = DocX.Load(filePath)) + { + text.Append(document.Text); + } + return text.ToString(); + } + } +} diff --git a/TagCloudDI/Data/IDataParser.cs b/TagCloudDI/Data/IDataParser.cs new file mode 100644 index 00000000..dc42c369 --- /dev/null +++ b/TagCloudDI/Data/IDataParser.cs @@ -0,0 +1,7 @@ +namespace TagCloudDI.Data +{ + public interface IDataParser + { + public string[] Parse(string text); + } +} diff --git a/TagCloudDI/Data/IFileDataSource.cs b/TagCloudDI/Data/IFileDataSource.cs new file mode 100644 index 00000000..aa153c8e --- /dev/null +++ b/TagCloudDI/Data/IFileDataSource.cs @@ -0,0 +1,7 @@ +namespace TagCloudDI.Data +{ + public interface IFileDataSource + { + public string GetData(string filePath); + } +} \ No newline at end of file diff --git a/TagCloudDI/Data/LiteratureTextParser.cs b/TagCloudDI/Data/LiteratureTextParser.cs new file mode 100644 index 00000000..4b6a7235 --- /dev/null +++ b/TagCloudDI/Data/LiteratureTextParser.cs @@ -0,0 +1,18 @@ +namespace TagCloudDI.Data +{ + internal class LiteratureTextParser : IDataParser + { + public string[] Parse(string text) + { + var punctuation = text + .Where(char.IsPunctuation) + .Distinct() + .ToArray(); + var words = text.Split() + .Select(x => x.Trim(punctuation)) + .Where(x => x.Length > 0) + .ToArray(); + return words; + } + } +} diff --git a/TagCloudDI/Data/TxtFileDataSource.cs b/TagCloudDI/Data/TxtFileDataSource.cs new file mode 100644 index 00000000..44f803c3 --- /dev/null +++ b/TagCloudDI/Data/TxtFileDataSource.cs @@ -0,0 +1,11 @@ +namespace TagCloudDI.Data +{ + public class TxtFileDataSource: IFileDataSource + { + public string GetData(string filePath) + { + using var fileStream = new StreamReader(filePath); + return fileStream.ReadToEnd(); + } + } +} diff --git a/TagCloudDI/Data/WordInfo.cs b/TagCloudDI/Data/WordInfo.cs new file mode 100644 index 00000000..b1eeb7f0 --- /dev/null +++ b/TagCloudDI/Data/WordInfo.cs @@ -0,0 +1,77 @@ +namespace TagCloudDI.Data +{ + public enum SpeechPart + { + Verb, + Noun, + Adjective, + Adverb, + Numb, + Pronoun, + Preposition, + Interjection, + Part, + Conjunction + } + public class WordInfo + { + private static Dictionary getSpeechPart = new Dictionary + { + {"S", SpeechPart.Noun}, + {"V", SpeechPart.Verb}, + {"A", SpeechPart.Adjective}, + {"ADV", SpeechPart.Adverb }, + {"PR", SpeechPart.Preposition}, + {"SPRO", SpeechPart.Pronoun}, + {"APRO", SpeechPart.Pronoun}, + {"NUM", SpeechPart.Numb}, + {"PART", SpeechPart.Part}, + {"INTJ", SpeechPart.Interjection }, + {"CONJ", SpeechPart.Conjunction }, + }; + + public SpeechPart SpeechPart { get; } + public string InitialForm { get; set; } + + public WordInfo(SpeechPart speechPart, string initialForm) + { + SpeechPart = speechPart; + InitialForm = initialForm; + } + + public static WordInfo[] GetInfoFromWords(string[] words) + { + var text = string.Join("\n", words); + var analysedText = MyStem.MyStem.AnalyseWords(text); + var analysedWords = analysedText.Split('\n').Where(w => w.Length > 0); + var result = new List(); + foreach (var word in analysedWords) + { + result.Add(new WordInfo(GetSpeechPart(word), GetInitForm(word))); + } + return result.ToArray(); + } + + private static SpeechPart GetSpeechPart(string analysedWord) + { + var start = analysedWord.IndexOf('=') + 1; + var endSymbols = new HashSet { '=', ',' }; + if (start > 0 && endSymbols.Count > 0) + { + var end = start + 1; + while(end < analysedWord.Length && !endSymbols.Contains(analysedWord[end])) end++; + if (getSpeechPart.TryGetValue(analysedWord.Substring(start, end - start), out var part)) + return part; + } + return SpeechPart.Interjection; + } + + private static string GetInitForm(string analysedWord) + { + var start = analysedWord.IndexOf('{') + 1; + var end = analysedWord.IndexOf('='); + var initialForm = start > 0 && end - start > 0 ? analysedWord.Substring(start, end - start) : ""; + return initialForm; + } + } +} diff --git a/TagCloudDI/DependencyModules/CloudCreatorModule.cs b/TagCloudDI/DependencyModules/CloudCreatorModule.cs new file mode 100644 index 00000000..1320f886 --- /dev/null +++ b/TagCloudDI/DependencyModules/CloudCreatorModule.cs @@ -0,0 +1,24 @@ +using Autofac; +using TagCloudDI.CloudVisualize; +using TagsCloudVisualization.CloudLayouter; + +namespace TagCloudDI.DependencyModules +{ + public class CloudCreatorModule: Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType().As(); + RegisterVisualizerDependencies(builder); + } + + private void RegisterVisualizerDependencies(ContainerBuilder builder) + { + builder.RegisterType().SingleInstance(); + builder.RegisterType().As(); + builder.RegisterType().As(); + } + } +} diff --git a/TagCloudDI/DependencyModules/DataModule.cs b/TagCloudDI/DependencyModules/DataModule.cs new file mode 100644 index 00000000..74e406a9 --- /dev/null +++ b/TagCloudDI/DependencyModules/DataModule.cs @@ -0,0 +1,39 @@ +using Autofac; +using TagCloudDI.Data; + +namespace TagCloudDI.DependencyModules +{ + internal class DataModule : Module + { + protected override void Load(ContainerBuilder builder) + { + RegisterMainDependencies(builder); + RegisterDataParser(builder); + RegisterFileDataSource(builder); + } + + private void RegisterMainDependencies(ContainerBuilder builder) + { + builder.RegisterType(); + builder.Register>(c => + { + var ctx = c.Resolve(); + return p => ctx.IsRegisteredWithKey(p) + ? ctx.ResolveKeyed(p) + : new DefaultDataSource(); + }); + } + + private void RegisterDataParser(ContainerBuilder builder) + { + builder.RegisterType().As(); + } + + private void RegisterFileDataSource(ContainerBuilder builder) + { + builder.RegisterType().Keyed(".txt"); + builder.RegisterType().Keyed(".doc"); + builder.RegisterType().Keyed(".docx"); + } + } +} diff --git a/TagCloudDI/DependencyModules/WordHandlersModule.cs b/TagCloudDI/DependencyModules/WordHandlersModule.cs new file mode 100644 index 00000000..d16c79e7 --- /dev/null +++ b/TagCloudDI/DependencyModules/WordHandlersModule.cs @@ -0,0 +1,24 @@ +using Autofac; +using TagCloudDI.WordHandlers; + +namespace TagCloudDI.DependencyModules +{ + internal class WordHandlersModule : Module + { + protected override void Load(ContainerBuilder builder) + { + RegisterWordTransformers(builder); + RegisterWordFilters(builder); + } + + private void RegisterWordFilters(ContainerBuilder builder) + { + builder.RegisterType().As(); + builder.RegisterType().As(); + } + private void RegisterWordTransformers(ContainerBuilder builder) + { + builder.RegisterType().As(); + } + } +} diff --git a/TagCloudDI/Examples/Text.doc b/TagCloudDI/Examples/Text.doc new file mode 100644 index 00000000..a102e28c --- /dev/null +++ b/TagCloudDI/Examples/Text.doc @@ -0,0 +1,18 @@ +Алёша, оставшись один, начал рассматривать своё зернышко и не мог им налюбоваться. Теперь-то он совершенно спокоен был насчёт урока, и вчерашнее горе не оставило в нём никаких следов. Он с радостью думал о том, как будут все удивляться, когда он безошибочно проговорит двадцать страниц, и мысль, что он опять возьмёт верх над товарищами, которые не хотели с ним говорить, ласкала его самолюбие. Об исправлении самого себя он хотя и не забыл, но думал, что это не может быть так трудно, как говорила Чернушка. «Будто не от меня зависит исправиться! — мыслил он. — Стоит только захотеть, и все опять меня любить будут...» + +Увы! бедный Алёша не знал, что для исправления самого себя необходимо начать тем, чтоб откинуть самолюбие и излишнюю самонадеянность. +Когда поутру собрались дети в классы, Алёшу позвали наверх. Он вошёл с весёлым и торжествующим видом. +— Знаете ли вы урок ваш? — спросил учитель, взглянув на него строго. +— Знаю, — отвечал Алёша смело. +Он начал говорить и проговорил все двадцать страниц без малейшей ошибки и остановки. Учитель был вне себя от удивления, а Алёша гордо посматривал на своих товарищей. +От глаз учителя не скрылся гордый вид Алёшин. +— Вы знаете урок свой, — сказал он ему, — это правда, но зачем вы вчера не хотели его сказать? +— Вчера я не знал его, — отвечал Алёша. +— Быть не может! — прервал его учитель. — Вчера ввечеру вы мне сказали, что знаете только две страницы, да и то плохо, а теперь без ошибки проговорили все двадцать! Когда же вы его выучили? +Алёша замолчал. Наконец дрожащим голосом сказал он: +— Я выучил его сегодня поутру! +Но тут вдруг все дети, огорчённые его надменностью, закричали в один голос: +— Он неправду говорит, он и книги в руки не брал сегодня поутру! +Алёша вздрогнул, потупил глаза в землю и не сказал ни слова. +— Отвечайте же! — продолжал учитель. — Когда выучили вы урок? +Но Алёша не прерывал молчания: он так поражён был этим неожиданным вопросом и недоброжелательством, которое оказывали ему все его товарищи, что не мог опомниться. \ No newline at end of file diff --git a/TagCloudDI/Examples/Text.txt b/TagCloudDI/Examples/Text.txt new file mode 100644 index 00000000..a102e28c --- /dev/null +++ b/TagCloudDI/Examples/Text.txt @@ -0,0 +1,18 @@ +Алёша, оставшись один, начал рассматривать своё зернышко и не мог им налюбоваться. Теперь-то он совершенно спокоен был насчёт урока, и вчерашнее горе не оставило в нём никаких следов. Он с радостью думал о том, как будут все удивляться, когда он безошибочно проговорит двадцать страниц, и мысль, что он опять возьмёт верх над товарищами, которые не хотели с ним говорить, ласкала его самолюбие. Об исправлении самого себя он хотя и не забыл, но думал, что это не может быть так трудно, как говорила Чернушка. «Будто не от меня зависит исправиться! — мыслил он. — Стоит только захотеть, и все опять меня любить будут...» + +Увы! бедный Алёша не знал, что для исправления самого себя необходимо начать тем, чтоб откинуть самолюбие и излишнюю самонадеянность. +Когда поутру собрались дети в классы, Алёшу позвали наверх. Он вошёл с весёлым и торжествующим видом. +— Знаете ли вы урок ваш? — спросил учитель, взглянув на него строго. +— Знаю, — отвечал Алёша смело. +Он начал говорить и проговорил все двадцать страниц без малейшей ошибки и остановки. Учитель был вне себя от удивления, а Алёша гордо посматривал на своих товарищей. +От глаз учителя не скрылся гордый вид Алёшин. +— Вы знаете урок свой, — сказал он ему, — это правда, но зачем вы вчера не хотели его сказать? +— Вчера я не знал его, — отвечал Алёша. +— Быть не может! — прервал его учитель. — Вчера ввечеру вы мне сказали, что знаете только две страницы, да и то плохо, а теперь без ошибки проговорили все двадцать! Когда же вы его выучили? +Алёша замолчал. Наконец дрожащим голосом сказал он: +— Я выучил его сегодня поутру! +Но тут вдруг все дети, огорчённые его надменностью, закричали в один голос: +— Он неправду говорит, он и книги в руки не брал сегодня поутру! +Алёша вздрогнул, потупил глаза в землю и не сказал ни слова. +— Отвечайте же! — продолжал учитель. — Когда выучили вы урок? +Но Алёша не прерывал молчания: он так поражён был этим неожиданным вопросом и недоброжелательством, которое оказывали ему все его товарищи, что не мог опомниться. \ No newline at end of file diff --git a/TagCloudDI/Examples/tagCloud872134912.png b/TagCloudDI/Examples/tagCloud872134912.png new file mode 100644 index 00000000..5597f69f Binary files /dev/null and b/TagCloudDI/Examples/tagCloud872134912.png differ diff --git a/TagCloudDI/Extensions/PointExtensions.cs b/TagCloudDI/Extensions/PointExtensions.cs new file mode 100644 index 00000000..b6530428 --- /dev/null +++ b/TagCloudDI/Extensions/PointExtensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization.Extensions +{ + public static class PointExtensions + { + public static int CalculateDistanceBetween(this Point current, Point other) + { + return (int)Math.Ceiling(Math.Sqrt( + (current.X - other.X) * (current.X - other.X) + + (current.Y - other.Y) * (current.Y - other.Y))); + } + } +} diff --git a/TagCloudDI/Extensions/RectangleExtensions.cs b/TagCloudDI/Extensions/RectangleExtensions.cs new file mode 100644 index 00000000..f763ceb4 --- /dev/null +++ b/TagCloudDI/Extensions/RectangleExtensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagsCloudVisualization +{ + public static class RectangleExtensions + { + public static bool IntersectedWithAnyFrom(this Rectangle forInsertion, List storage) + { + if (storage.Count == 0) return false; + return storage.FirstOrDefault(r => forInsertion != r + && forInsertion.IntersectsWith(r)) != default; + } + } +} diff --git a/TagCloudDI/ICloudLayouter.cs b/TagCloudDI/ICloudLayouter.cs new file mode 100644 index 00000000..415f1dcd --- /dev/null +++ b/TagCloudDI/ICloudLayouter.cs @@ -0,0 +1,10 @@ +using System.Drawing; + +namespace TagCloudDI +{ + public interface ICloudLayouter + { + public Rectangle PutNextRectangle(Size forInsertion); + public void Clear(); + } +} \ No newline at end of file diff --git a/TagCloudDI/MyStem/MyStem.cs b/TagCloudDI/MyStem/MyStem.cs new file mode 100644 index 00000000..f7ad1d98 --- /dev/null +++ b/TagCloudDI/MyStem/MyStem.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using System.Text; + +namespace TagCloudDI.MyStem +{ + public static class MyStem + { + public static string AnalyseWords(string words) + { + var directory = ".\\MyStem"; + var inputFile = Path.Combine(directory, "input.txt"); + var outputFile = Path.Combine(directory, "output.txt"); + var mySteam = Path.Combine(directory, "mystem.exe"); + using (var writer = new StreamWriter(inputFile)) + { + writer.Write(words); + } + var arguments = string.Format("-in {0} {1}", inputFile, outputFile); + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = mySteam, + Arguments = arguments, + RedirectStandardInput = true, + RedirectStandardOutput = true + } + }; + process.Start(); + process.WaitForExit(); + using var reader = new StreamReader(outputFile, Encoding.UTF8); + return reader.ReadToEnd(); + } + } +} diff --git a/TagCloudDI/MyStem/input.txt b/TagCloudDI/MyStem/input.txt new file mode 100644 index 00000000..19d4df1f --- /dev/null +++ b/TagCloudDI/MyStem/input.txt @@ -0,0 +1 @@ +проверка проверку \ No newline at end of file diff --git a/TagCloudDI/MyStem/mystem.exe b/TagCloudDI/MyStem/mystem.exe new file mode 100644 index 00000000..e7158ff1 Binary files /dev/null and b/TagCloudDI/MyStem/mystem.exe differ diff --git a/TagCloudDI/MyStem/output.txt b/TagCloudDI/MyStem/output.txt new file mode 100644 index 00000000..936ad7a7 --- /dev/null +++ b/TagCloudDI/MyStem/output.txt @@ -0,0 +1 @@ +проверка{проверка}проверка{проверка} \ No newline at end of file diff --git a/TagCloudDI/Program.cs b/TagCloudDI/Program.cs new file mode 100644 index 00000000..17475a00 --- /dev/null +++ b/TagCloudDI/Program.cs @@ -0,0 +1,29 @@ +using Autofac; +using TagCloudDI.ConsoleInterface; +using TagCloudDI.DependencyModules; + +namespace TagCloudDI +{ + internal class Program + { + static void Main(string[] args) + { + var container = RegisterDependencies(); + using (var scope = container.BeginLifetimeScope()) + { + var app = scope.Resolve(); + app.Run(); + } + } + + public static IContainer? RegisterDependencies() + { + var builder = new ContainerBuilder(); + builder.RegisterModule(); + builder.RegisterModule(); + builder.RegisterModule(); + builder.RegisterType().SingleInstance(); + return builder.Build(); + } + } +} diff --git a/TagCloudDI/Properties/launchSettings.json b/TagCloudDI/Properties/launchSettings.json new file mode 100644 index 00000000..48e045c4 --- /dev/null +++ b/TagCloudDI/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "TagCloudDI": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/TagCloudDI/TagCloudDI.csproj b/TagCloudDI/TagCloudDI.csproj new file mode 100644 index 00000000..6930ef9a --- /dev/null +++ b/TagCloudDI/TagCloudDI.csproj @@ -0,0 +1,35 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + Always + + + Always + + + Always + + + + + + + + diff --git a/TagCloudDI/WordHandlers/BoredSpeechPartFilter.cs b/TagCloudDI/WordHandlers/BoredSpeechPartFilter.cs new file mode 100644 index 00000000..8a807bc7 --- /dev/null +++ b/TagCloudDI/WordHandlers/BoredSpeechPartFilter.cs @@ -0,0 +1,17 @@ +using TagCloudDI.Data; +using static TagCloudDI.MyStem.MyStem; + +namespace TagCloudDI.WordHandlers +{ + internal class BoredSpeechPartFilter : IWordFilter + { + private HashSet boredSpeechPart = + [ + SpeechPart.Part, SpeechPart.Conjunction, SpeechPart.Pronoun, SpeechPart.Interjection, SpeechPart.Preposition + ]; + public bool Accept(WordInfo word) + { + return !boredSpeechPart.Contains(word.SpeechPart); + } + } +} diff --git a/TagCloudDI/WordHandlers/EmptyWordsFilter.cs b/TagCloudDI/WordHandlers/EmptyWordsFilter.cs new file mode 100644 index 00000000..669026d2 --- /dev/null +++ b/TagCloudDI/WordHandlers/EmptyWordsFilter.cs @@ -0,0 +1,12 @@ +using TagCloudDI.Data; + +namespace TagCloudDI.WordHandlers +{ + internal class EmptyWordsFilter : IWordFilter + { + public bool Accept(WordInfo word) + { + return !string.IsNullOrEmpty(word.InitialForm); + } + } +} diff --git a/TagCloudDI/WordHandlers/IWordFilter.cs b/TagCloudDI/WordHandlers/IWordFilter.cs new file mode 100644 index 00000000..9be6ba76 --- /dev/null +++ b/TagCloudDI/WordHandlers/IWordFilter.cs @@ -0,0 +1,9 @@ +using TagCloudDI.Data; + +namespace TagCloudDI.WordHandlers +{ + public interface IWordFilter + { + public bool Accept(WordInfo word); + } +} \ No newline at end of file diff --git a/TagCloudDI/WordHandlers/IWordTransformer.cs b/TagCloudDI/WordHandlers/IWordTransformer.cs new file mode 100644 index 00000000..807577a6 --- /dev/null +++ b/TagCloudDI/WordHandlers/IWordTransformer.cs @@ -0,0 +1,9 @@ +using TagCloudDI.Data; + +namespace TagCloudDI.WordHandlers +{ + public interface IWordTransformer + { + public WordInfo Apply(WordInfo word); + } +} \ No newline at end of file diff --git a/TagCloudDI/WordHandlers/LowerCaseTransformer.cs b/TagCloudDI/WordHandlers/LowerCaseTransformer.cs new file mode 100644 index 00000000..c360a991 --- /dev/null +++ b/TagCloudDI/WordHandlers/LowerCaseTransformer.cs @@ -0,0 +1,13 @@ +using TagCloudDI.Data; + +namespace TagCloudDI.WordHandlers +{ + internal class LowerCaseTransformer : IWordTransformer + { + public WordInfo Apply(WordInfo word) + { + word.InitialForm = word.InitialForm.ToLower(); + return word; + } + } +} diff --git a/TagCloudDITests/CloudLayouter/BruteForceNearestFinderTests.cs b/TagCloudDITests/CloudLayouter/BruteForceNearestFinderTests.cs new file mode 100644 index 00000000..5b26d9a3 --- /dev/null +++ b/TagCloudDITests/CloudLayouter/BruteForceNearestFinderTests.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using NUnit.Framework; +using FluentAssertions; +using System.Drawing; +using TagsCloudVisualization.CloudLayouter; + +namespace TagCloudDITests.CloudLayouter +{ + public class BruteForceNearestFinderTests + { + [Test] + public void FindNearest_ShouldReturnNull_OnEmptyRectangles() + { + var rectangleForFind = new Rectangle(new Point(5, 7), new Size(4, 2)); + + BruteForceNearestFinder.FindNearestByDirection(rectangleForFind, Direction.Top, []).Should().BeNull(); + } + + [TestCase(4, 10, Direction.Top)] + [TestCase(2, 7, Direction.Top, true)] + [TestCase(2, 7, Direction.Right)] + [TestCase(0, 0, Direction.Right, true)] + [TestCase(0, 0, Direction.Bottom, true)] + [TestCase(7, 4, Direction.Bottom)] + [TestCase(10, 11, Direction.Left)] + [TestCase(7, 4, Direction.Left, true)] + public void FindNearest_ShouldReturnNearestRectangleByDirection_ForArgumentRectangle(int x, int y, Direction direction, bool isFirstNearest = false) + { + var addedRectangle1 = new Rectangle(new Point(2, 2), new Size(3, 4)); + var addedRectangle2 = new Rectangle(new Point(5, 7), new Size(4, 2)); + var rectangleForFind = new Rectangle(new Point(x, y), new Size(2, 1)); + var rectangles = new List { addedRectangle1, addedRectangle2 }; + + var nearest = BruteForceNearestFinder.FindNearestByDirection(rectangleForFind, direction, rectangles); + + nearest.Should().Be(isFirstNearest ? addedRectangle1 : addedRectangle2); + } + } +} diff --git a/TagCloudDITests/CloudLayouter/CirclePositionDistributorTests.cs b/TagCloudDITests/CloudLayouter/CirclePositionDistributorTests.cs new file mode 100644 index 00000000..f36f9d55 --- /dev/null +++ b/TagCloudDITests/CloudLayouter/CirclePositionDistributorTests.cs @@ -0,0 +1,45 @@ +using System.Drawing; +using FluentAssertions; +using NUnit.Framework; +using TagsCloudVisualization; +using TagsCloudVisualization.Extensions; + +namespace TagCloudDITests.CloudLayouter; + +public class CirclePositionDistributorTests +{ + private CirclePositionDistributor currentLayer; + + [SetUp] + public void SetUp() + { + var center = new Point(5, 5); + currentLayer = new CirclePositionDistributor(center); + } + + [Test] + public void GetNextRectanglePosition_ShouldTryPutRectangleOnCircle() + { + var firstRectangleLocation = currentLayer.GetNextPosition(); + var secondRectangleLocation = currentLayer.GetNextPosition(); + + var radius = currentLayer.Center.CalculateDistanceBetween(firstRectangleLocation); + + radius.Should().Be(currentLayer.Center.CalculateDistanceBetween(secondRectangleLocation)); + } + + [Test] + public void GetNextRectanglePosition_MustExpandCircle_WhenTryPutRectangleOnFullCircle() + { + var firstPoint = currentLayer.GetNextPosition(); + for (int i = 0; i < 360; i++) + { + currentLayer.GetNextPosition(); + } + + var finalPoint = currentLayer.GetNextPosition(); + + finalPoint.CalculateDistanceBetween(currentLayer.Center).Should() + .BeGreaterThan(firstPoint.CalculateDistanceBetween(currentLayer.Center)); + } +} \ No newline at end of file diff --git a/TagCloudDITests/CloudLayouter/CircularCloudLayouterTests.cs b/TagCloudDITests/CloudLayouter/CircularCloudLayouterTests.cs new file mode 100644 index 00000000..5a69a078 --- /dev/null +++ b/TagCloudDITests/CloudLayouter/CircularCloudLayouterTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using FluentAssertions; +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using TagsCloudVisualization; +using TagsCloudVisualization.CloudLayouter; + +namespace TagCloudDITests.CloudLayouter; + +public class CircularCloudLayouterTests +{ + private CircularCloudLayouter layouter; + private Point defaultCenter; + private List storage; + + public static IEnumerable IntersectionTestsData + { + get + { + yield return new TestCaseData(new Size[] + { + new(1, 1), new(1, 1), new(1, 1), new(400, 400), + new(1, 4), new(6, 6) + }) + .SetName("WhenAddedSmallRectanglesWithOneVeryBig"); + yield return new TestCaseData(new Size[] + { + new(100, 100), new(123, 121), new(100, 100), new(400, 400), + new(100, 400), new(600, 128) + }) + .SetName("WhenAddedBigRectangles"); + yield return new TestCaseData(new Size[] { new(4, 4), new(4, 4), new(4, 4), new(4, 4), new(4, 4) }) + .SetName("WhenAddedRectanglesHasSameSize"); + } + } + + [SetUp] + public void SetUp() + { + defaultCenter = new Point(5, 5); + storage = []; + layouter = CircularCloudLayouter.CreateLayouterWithStartRectangles(defaultCenter, storage); + } + + [TestCase(0, 4, TestName = "WhenWidthZero")] + [TestCase(3, 0, TestName = "WhenHeightZero")] + [TestCase(-3, 4, TestName = "WhenWidthIsNegative")] + [TestCase(3, -4, TestName = "WhenHeightNegative")] + [TestCase(-3, -4, TestName = "WhenWidthAndHeightNegative")] + [TestCase(0, 0, TestName = "WhenWidthAndHeightIsZero")] + public void Insert_ShouldThrow(int width, int height) + { + var inCorrectSize = new Size(width, height); + + Action act = () => layouter.PutNextRectangle(inCorrectSize); + + act.Should().Throw(); + } + + [TestCaseSource(nameof(IntersectionTestsData))] + public void PutRectangleOnCircleWithoutIntersection_ShouldPutRectangleWithoutIntersection(Size[] sizes) + { + layouter = InsertFewRectangles(layouter, sizes, storage); + var last = layouter.PutRectangleWithoutIntersection(new Size(3, 3)); + + storage.Where(addedRectangle => addedRectangle.IntersectsWith(last)).Should().BeEmpty(); + } + + private static CircularCloudLayouter InsertFewRectangles(CircularCloudLayouter layouter, Size[] sizes, + List storage) + { + for (var i = 0; i < sizes.Length; i++) + { + var forInsertion = layouter.PutNextRectangle(sizes[i]); + storage.Add(forInsertion); + } + + return layouter; + } + + [Test] + public void PutNextRectangle_ShouldTryMoveRectangleCloserToCenter_WhenItPossible() + { + var firstRectangleSize = new Size(6, 4); + var secondRectangleSize = new Size(4, 4); + + var first = layouter.PutNextRectangle(firstRectangleSize); + var second = layouter.PutNextRectangle(secondRectangleSize); + var hasEqualBound = first.Right == second.Left || first.Top == second.Bottom + || second.Right == first.Left || second.Top == first.Bottom; + + first.IntersectsWith(second).Should().BeFalse(); + hasEqualBound.Should().BeTrue(); + } +} \ No newline at end of file diff --git a/TagCloudDITests/Data/DataProviderTests.cs b/TagCloudDITests/Data/DataProviderTests.cs new file mode 100644 index 00000000..ea5405c8 --- /dev/null +++ b/TagCloudDITests/Data/DataProviderTests.cs @@ -0,0 +1,77 @@ +using TagCloudDI.Data; +using FakeItEasy; +using TagCloudDI.WordHandlers; +using FluentAssertions; + +namespace TagCloudDITests.Data +{ + public class DataProviderTests + { + private DataProvider provider; + private IDataParser fakeParser; + [SetUp] + public void SetUp() + { + var fakeDataSource = A.Fake(); + var sourceFactory = A.Fake>(); + A.CallTo(() => sourceFactory(A.Ignored)).Returns(fakeDataSource); + A.CallTo(() => fakeDataSource.GetData(A.Ignored)).Returns(string.Empty); + fakeParser = A.Fake(); + A.CallTo(() => fakeParser.Parse(A.Ignored)).Returns(new string[] { }); + provider = new DataProvider(sourceFactory, Array.Empty(), Array.Empty(), fakeParser); + } + + [Test] + public void ShouldCorrectCalculateFrequency_WhenDifferentWords() + { + A.CallTo(() => fakeParser.Parse(A.Ignored)).Returns(new string[] { + "праздник", + "праздник", + "трагедия", + "праздник"}); + + var result = provider.GetPreprocessedWords(""); + + result.Should().BeEquivalentTo(new (string Word, double Frequency)[] + { + (Word: "праздник", Frequency: 1), + (Word: "трагедия", Frequency: 1/3.0), + }); + + } + [Test] + public void ShouldCorrectCalculateFrequency_WhenSameWord() + { + A.CallTo(() => fakeParser.Parse(A.Ignored)).Returns(new string[] { + "праздник", + "праздник", + "праздник", + "праздник"}); + + var result = provider.GetPreprocessedWords(""); + + result.Should().BeEquivalentTo(new (string Word, double Frequency)[] + { + (Word: "праздник", Frequency: 1), + }); + + } + [Test] + public void ShouldCorrectCalculateFrequency_WhenDifferentWordsWithSameFrequency() + { + A.CallTo(() => fakeParser.Parse(A.Ignored)).Returns(new string[] { + "праздник", + "праздник", + "трагедия", + "трагедия"}); + + var result = provider.GetPreprocessedWords(""); + + result.Should().BeEquivalentTo(new (string Word, double Frequency)[] + { + (Word: "праздник", Frequency: 1), + (Word: "трагедия", Frequency: 1), + }); + } + } +} diff --git a/TagCloudDITests/Data/FileDataSourceTests.cs b/TagCloudDITests/Data/FileDataSourceTests.cs new file mode 100644 index 00000000..aa894307 --- /dev/null +++ b/TagCloudDITests/Data/FileDataSourceTests.cs @@ -0,0 +1,49 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudDI.Data; + +namespace TagCloudDITests.Data +{ + public class FileDataSourceTests + { + private const string RelativePath = "..\\..\\..\\Data\\ForCheck"; + private const string CorrectText = "Всё работает корректно!"; + + [Test] + public void TxtFileDataSource_ShouldReadFileCorrect_WhenExtensionIsTxt() + { + var source = new TxtFileDataSource(); + var extension = ".txt"; + + var result = source.GetData(RelativePath + extension); + + result.Should().Be(CorrectText); + } + + [Test] + public void DocFileDataSource_ShouldReadFileCorrect_WhenExtensionIsDoc() + { + var source = new DocFileDataSource(); + var extension = ".doc"; + + var result = source.GetData(RelativePath + extension); + + result.Should().Be(CorrectText); + } + + [Test] + public void DocFileDataSource_ShouldReadFileCorrect_WhenExtensionIsDocx() + { + var source = new DocxFileDataSource(); + var extension = ".docx"; + + var result = source.GetData(RelativePath + extension); + + result.Should().Be(CorrectText); + } + } +} diff --git a/TagCloudDITests/Data/ForCheck.doc b/TagCloudDITests/Data/ForCheck.doc new file mode 100644 index 00000000..bad4db87 Binary files /dev/null and b/TagCloudDITests/Data/ForCheck.doc differ diff --git a/TagCloudDITests/Data/ForCheck.docx b/TagCloudDITests/Data/ForCheck.docx new file mode 100644 index 00000000..10729251 Binary files /dev/null and b/TagCloudDITests/Data/ForCheck.docx differ diff --git a/TagCloudDITests/Data/ForCheck.txt b/TagCloudDITests/Data/ForCheck.txt new file mode 100644 index 00000000..1087dfec --- /dev/null +++ b/TagCloudDITests/Data/ForCheck.txt @@ -0,0 +1 @@ +Всё работает корректно! \ No newline at end of file diff --git a/TagCloudDITests/Data/WordInfoTests.cs b/TagCloudDITests/Data/WordInfoTests.cs new file mode 100644 index 00000000..e92c4516 --- /dev/null +++ b/TagCloudDITests/Data/WordInfoTests.cs @@ -0,0 +1,46 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudDI.Data; + +namespace TagCloudDITests.Data +{ + public class WordInfoTests + { + [Test] + public void ParseText_ShouldCorrectParseWords() + { + var words = new string[] { "гулять", "лес", "в", "красивый" }; + + var result = WordInfo.GetInfoFromWords(words); + + result.Length.Should().Be(4); + result.Select(i => i.InitialForm).Should().BeEquivalentTo(words); + } + [Test] + public void ParseText_ShouldCorrectMapSpeechPartParseWords() + { + var words = new string[] { "гулять", "лес", "из-под", "красивый" }; + + var result = WordInfo.GetInfoFromWords(words); + + result.Length.Should().Be(4); + result.Select(i => i.SpeechPart).Should().BeEquivalentTo(new SpeechPart[] + { + SpeechPart.Verb, SpeechPart.Noun, SpeechPart.Preposition, SpeechPart.Adjective + }); + } + [Test] + public void ParseText_ShouldCorrectMapSpeech_WhenTwoOrMoreVariantsForSpeechPart() + { + var words = new string[] { "в", "к", "из-под", "за", "для", "из-за", "о", "об", "без", "с", "вроде", "до", "по" }; + + var result = WordInfo.GetInfoFromWords(words); + + result.Select(i => i.SpeechPart).Should().BeEquivalentTo(words.Select(_ => SpeechPart.Preposition)); + } + } +} diff --git a/TagCloudDITests/TagCloudDITests.csproj b/TagCloudDITests/TagCloudDITests.csproj new file mode 100644 index 00000000..9dce08d1 --- /dev/null +++ b/TagCloudDITests/TagCloudDITests.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/di.sln b/di.sln index a50991da..371d3e5e 100644 --- a/di.sln +++ b/di.sln @@ -1,6 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "di", "FractalPainter/di.csproj", "{4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}" +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34902.65 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "di", "FractalPainter\di.csproj", "{4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudDI", "TagCloudDI\TagCloudDI.csproj", "{35C8F6C7-EBA5-49C4-B4FA-80429C56052B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudDITests", "TagCloudDITests\TagCloudDITests.csproj", "{546EB431-BF98-4662-9BA4-442FF64CFECA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,5 +19,19 @@ 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 + {35C8F6C7-EBA5-49C4-B4FA-80429C56052B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35C8F6C7-EBA5-49C4-B4FA-80429C56052B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35C8F6C7-EBA5-49C4-B4FA-80429C56052B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35C8F6C7-EBA5-49C4-B4FA-80429C56052B}.Release|Any CPU.Build.0 = Release|Any CPU + {546EB431-BF98-4662-9BA4-442FF64CFECA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {546EB431-BF98-4662-9BA4-442FF64CFECA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {546EB431-BF98-4662-9BA4-442FF64CFECA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {546EB431-BF98-4662-9BA4-442FF64CFECA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {088C49D7-9ECF-4622-9618-8414234ECAF1} EndGlobalSection EndGlobal