diff --git a/TagsCloudContainer.Tests/CircularCloudContainerTests.cs b/TagsCloudContainer.Tests/CircularCloudContainerTests.cs new file mode 100644 index 00000000..e2942dfe --- /dev/null +++ b/TagsCloudContainer.Tests/CircularCloudContainerTests.cs @@ -0,0 +1,123 @@ +using System.Drawing; +using FluentAssertions; +using NUnit.Framework; +using TagsCloudContainer.CloudLayouters; +using TagsCloudContainer.Extensions; + +namespace TagsCloudContainer.Tests; + +[TestFixture] +public class CircularCloudContainerTests +{ + private CircularCloudLayouter circularCloudLayouter; + + [SetUp] + public void Setup() + { + var spiral = new ArchimedeanSpiral(Point.Empty); + circularCloudLayouter = new CircularCloudLayouter(Point.Empty, spiral); + } + + [Test] + public void Constructor_SetCenterCorrectly_WhenInitialized() + { + var center = Point.Empty; + var cloudCenter = circularCloudLayouter.Center; + + cloudCenter.Should().Be(center); + } + + [Test] + public void CloudSizeIsZero_WhenInitialized() + { + var actualSize = circularCloudLayouter.Tags.CalculateSize(); + + actualSize.Should().Be(Size.Empty); + } + + [Test] + public void CloudSizeEqualsFirstTagSize_WhenPuttingFirstTag() + { + var font = new Font("Arial", 25); + const string text = "text"; + + SizeF expectsdRectangleSize; + using (var bitmap = new Bitmap(1, 1)) + using (var graphics = Graphics.FromImage(bitmap)) + { + graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; + expectsdRectangleSize = graphics.MeasureString(text, font); + } + + circularCloudLayouter.PutNextTag(text, 3); + var actualRectangleSize = circularCloudLayouter.Tags.CalculateSize(); + + actualRectangleSize.Should().Be(expectsdRectangleSize.ToSize()); + } + + [Test] + public void CloudSizeIsCloseToCircleShape_WhenPuttingManyTags() + { + for (var i = 0; i < 100; i++) + { + circularCloudLayouter.PutNextTag("test", 4); + } + + var actualSize = circularCloudLayouter.Tags.CalculateSize(); + var aspectRatio = (double)actualSize.Width / actualSize.Height; + + aspectRatio.Should().BeInRange(0.5, 2.0); + } + + [TestCase(0, TestName = "NoTags_WhenInitialized")] + [TestCase(1, TestName = "SingleTag_WhenPuttingFirstTag")] + [TestCase(10, TestName = "MultipleTags_WhenPuttingALotOfTags")] + public void CloudContains(int rectangleCount) + { + for (var i = 0; i < rectangleCount; i++) + { + circularCloudLayouter.PutNextTag("test", 2); + } + + circularCloudLayouter.Tags.Count.Should().Be(rectangleCount); + } + + [Test] + public void PutNextRectangle_ThrowException_WhenCountIsNotPositive() + { + var putIncorrectRectangle = () => circularCloudLayouter.PutNextTag("test", -5); + + putIncorrectRectangle.Should().Throw(); + } + + [Test] + public void PutNextRectangle_PlacesFirstRectangleInCenter() + { + var center = Point.Empty; + var firstRectangle = circularCloudLayouter.PutNextTag("text", 3); + + var rectangleCenter = new Point( + firstRectangle.Left + firstRectangle.Width / 2, + firstRectangle.Top - firstRectangle.Height / 2); + + rectangleCenter.Should().Be(center); + } + + [Test] + public void PutNextRectangle_CloudTagsIsNotIntersect_WhenPuttingALotOfTags() + { + circularCloudLayouter.PutNextTag("test", 3); + circularCloudLayouter.PutNextTag("test", 2); + circularCloudLayouter.PutNextTag("test", 1); + + var tags = circularCloudLayouter.Tags; + for (var i = 0; i < tags.Count; i++) + { + var currentRectangle = tags[i].Rectangle; + tags + .Where((_, j) => j != i) + .All(otherTag => !currentRectangle.IntersectsWith(otherTag.Rectangle)) + .Should().BeTrue(); + } + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Tests/TagsCloudContainer.Tests.csproj b/TagsCloudContainer.Tests/TagsCloudContainer.Tests.csproj new file mode 100644 index 00000000..5e8d45cd --- /dev/null +++ b/TagsCloudContainer.Tests/TagsCloudContainer.Tests.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + 12 + + + + + + + + + + + + + + + + diff --git a/TagsCloudContainer/App.cs b/TagsCloudContainer/App.cs new file mode 100644 index 00000000..282dd3f3 --- /dev/null +++ b/TagsCloudContainer/App.cs @@ -0,0 +1,217 @@ +using System.Drawing; +using TagsCloudContainer.Extensions; + +namespace TagsCloudContainer; + +public static class App +{ + private const string DefaultCommand = "default"; + private const string ExitCommand = "exit"; + + private static int defaultImageWidth = 1000; + private static int defaultImageHeight = 1000; + private static string defaultFontName = "Arial"; + private static Color defaultBackgroundColor = Color.White; + private static Color defaultTextColor = Color.Black; + private static string defaultWordsFilePath = Path.Combine("..", "..", "..", "WordProcessing", "cloud.txt"); + private static string defaultExcludedWordsFilePath = Path.Combine("..", "..", "..", "WordProcessing", "excluded_words.txt"); + + private static readonly string imageDimensionsPrompt = $"Введите размер изображения (по умолчанию W: {defaultImageWidth}, H: {defaultImageHeight}):"; + private static readonly string fileNamePrompt = "Введите название файла с текстом:"; + private static readonly string excludedWordsFileNamePrompt = "Введите название файла с исключёнными словами:"; + private static readonly string fontNamePrompt = $"Введите название шрифта (по умолчанию {defaultFontName}):"; + private static readonly string backgroundColorPrompt = $"Введите цвет фона (по умолчанию {defaultBackgroundColor.Name}):"; + private static readonly string textColorPrompt = $"Введите цвет текста (по умолчанию {defaultTextColor.Name}):"; + + public static string GetDefaultExcludedWordsFilePath() + { + return defaultExcludedWordsFilePath; + } + + private static void ShowExitMessage() + { + Console.WriteLine($"Чтобы выйти из программы, напишите \"{ExitCommand}\", чтобы использовать значение по умолчанию, напишите \"{DefaultCommand}\""); + Console.WriteLine(); + } + + public static string GetFileNameFromUser() + { + return GetFileName(fileNamePrompt, defaultWordsFilePath); + } + + public static string GetExcludedWordsFileNameFromUser() + { + return GetFileName(excludedWordsFileNamePrompt, string.Empty); + } + + private static string GetFileName(string prompt, string defaultPath) + { + ShowExitMessage(); + + while (true) + { + Console.WriteLine(prompt); + var input = Console.ReadLine(); + + if (!string.IsNullOrEmpty(input) && File.Exists(input)) + { + Console.Clear(); + return input; + } + + switch (input) + { + case ExitCommand: + Environment.Exit(0); + break; + case DefaultCommand: + Console.Clear(); + return defaultPath; + } + + Console.Clear(); + ShowExitMessage(); + Console.WriteLine("Файл не найден. Попробуйте снова."); + } + } + + public static ImageDimensions GetImageDimensionsFromUser() + { + ShowExitMessage(); + + while (true) + { + Console.WriteLine(imageDimensionsPrompt); + Console.WriteLine("(в формате \"ширина высота\")"); + var input = Console.ReadLine(); + var size = input?.Split(' '); + + if (size?.Length == 2 && + int.TryParse(size[0], out var width) && + int.TryParse(size[1], out var height) && + width > 0 && height > 0) + { + Console.Clear(); + return new ImageDimensions(width, height); + } + + switch (input) + { + case ExitCommand: + Environment.Exit(0); + break; + case DefaultCommand: + Console.Clear(); + return new ImageDimensions(defaultImageWidth, defaultImageHeight); + } + + Console.Clear(); + ShowExitMessage(); + Console.WriteLine("Некорректный ввод. Убедитесь, что вы ввели два положительных целых числа."); + } + } + + public static string GetFontNameFromUser() + { + ShowExitMessage(); + + while (true) + { + Console.WriteLine(fontNamePrompt); + var input = Console.ReadLine(); + + if (input!.FontExists()) + { + Console.Clear(); + return input!; + } + + switch (input) + { + case ExitCommand: + Environment.Exit(0); + break; + case DefaultCommand: + Console.Clear(); + return defaultFontName; + } + + Console.Clear(); + ShowExitMessage(); + Console.WriteLine("Шрифт не найден. Попробуйте снова."); + } + } + + public static (Color Primary, Color? Secondary) GetBackgroundColorsFromUser() + { + return GetColorsFromUser(backgroundColorPrompt, defaultBackgroundColor); + } + + public static (Color Primary, Color? Secondary) GetTextColorsFromUser() + { + return GetColorsFromUser(textColorPrompt, defaultTextColor); + } + + private static (Color Primary, Color? Secondary) GetColorsFromUser(string prompt, Color defaultColor) + { + ShowExitMessage(); + + while (true) + { + Console.WriteLine(prompt); + Console.WriteLine("(Чтобы использовать градиент введите 2 цвета через пробел)"); + var input = Console.ReadLine(); + + switch (input) + { + case ExitCommand: + Environment.Exit(0); + break; + case DefaultCommand: + Console.Clear(); + return (defaultColor, null); + } + + var colorInputs = input?.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + if (colorInputs is { Length: > 0 }) + { + try + { + var primaryColor = ParseColor(colorInputs[0].Trim()); + + if (colorInputs.Length > 1) + { + var secondaryColor = ParseColor(colorInputs[1].Trim()); + Console.Clear(); + return (primaryColor, secondaryColor); + } + + Console.Clear(); + return (primaryColor, null); + } + catch (ArgumentException ex) + { + Console.Clear(); + ShowExitMessage(); + Console.WriteLine(ex.Message); + continue; + } + } + + Console.Clear(); + ShowExitMessage(); + Console.WriteLine("Некорректное значение. Попробуйте снова."); + } + } + + private static Color ParseColor(string input) + { + if (Enum.TryParse(input, true, out KnownColor knownColor)) + { + return Color.FromKnownColor(knownColor); + } + + throw new ArgumentException($"Некорректное название цвета: {input}"); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/CloudLayouters/CircularCloudLayouter.cs b/TagsCloudContainer/CloudLayouters/CircularCloudLayouter.cs new file mode 100644 index 00000000..c4868c7a --- /dev/null +++ b/TagsCloudContainer/CloudLayouters/CircularCloudLayouter.cs @@ -0,0 +1,72 @@ +using System.Drawing; + +namespace TagsCloudContainer.CloudLayouters; + +public class CircularCloudLayouter +{ + public readonly Point Center; + public List Tags { get; } + public (Color Primary, Color? Secondary)? BackgroundColor { get; private set; } + public (Color Primary, Color? Secondary)? TextColor { get; private set; } + private string? FontName { get; set; } + private readonly RectangleArranger arranger; + + public CircularCloudLayouter(Point center, ISpiral spiral) + { + Center = center; + Tags = []; + arranger = new RectangleArranger(spiral); + } + + public CircularCloudLayouterVisualizer CreateView(int width, int height) + { + return new CircularCloudLayouterVisualizer(this, new Size(width, height)); + } + + public CircularCloudLayouter SetFontName(string fontName) + { + FontName = fontName; + return this; + } + + public CircularCloudLayouter SetBackgroundColor((Color Primary, Color? Secondary) color) + { + BackgroundColor = color; + return this; + } + + public CircularCloudLayouter SetTextColor((Color Primary, Color? Secondary) color) + { + TextColor = color; + return this; + } + + public CircularCloudLayouter PutTags(Dictionary words) + { + var sortedWords = words.OrderByDescending(pair => pair.Value); + + foreach (var word in sortedWords) + { + PutNextTag(word.Key, word.Value); + } + + return this; + } + + public Rectangle PutNextTag(string text, int count) + { + if (count < 1) + { + throw new ArgumentException(nameof(count)); + } + + var font = new Font(FontName ?? "Arial", count * 6 + 10); + var rectangleSize = RectangleMeasurer.Measure(text, font); + + var rectangle = arranger.ArrangeRectangle(rectangleSize, Center); + var tag = new Tag(text, font, rectangle); + + Tags.Add(tag); + return rectangle; + } +} \ No newline at end of file diff --git a/TagsCloudContainer/CloudLayouters/CircularCloudLayouterVisualizer.cs b/TagsCloudContainer/CloudLayouters/CircularCloudLayouterVisualizer.cs new file mode 100644 index 00000000..d78472ec --- /dev/null +++ b/TagsCloudContainer/CloudLayouters/CircularCloudLayouterVisualizer.cs @@ -0,0 +1,66 @@ +using System.Drawing; +using System.Drawing.Drawing2D; +using TagsCloudContainer.CloudLayouters; + +namespace TagsCloudContainer; + +public class CircularCloudLayouterVisualizer +{ + private CircularCloudLayouter layouter; + private Size size; + + public CircularCloudLayouterVisualizer(CircularCloudLayouter layouter, Size bitmapSize) + { + this.layouter = layouter; + size = bitmapSize; + } + + public CircularCloudLayouterVisualizer SaveImage(string filePath) + { + using var bitmap = new Bitmap(size.Width, size.Height); + using var graphics = Graphics.FromImage(bitmap); + + graphics.Clear(layouter.BackgroundColor?.Primary ?? Color.White); + + if (layouter.BackgroundColor?.Secondary != null) + { + var bounds = new Rectangle(0, 0, size.Width, size.Height); + + using var gradientBrush = new LinearGradientBrush( + bounds, + layouter.BackgroundColor?.Primary ?? Color.White, + layouter.BackgroundColor?.Secondary ?? Color.White, + LinearGradientMode.Horizontal); + + graphics.FillRectangle(gradientBrush, bounds); + } + + var centerBitmap = new Point(size.Width / 2, size.Height / 2); + var offsetBitmap = new Point(centerBitmap.X - layouter.Center.X, centerBitmap.Y - layouter.Center.Y); + + foreach (var rectangle in layouter.Tags) + { + rectangle.Rectangle.Offset(offsetBitmap); + + if (layouter.TextColor?.Secondary != null) + { + var gradientBrush = new LinearGradientBrush( + rectangle.Rectangle, + layouter.TextColor?.Primary ?? Color.Blue, + layouter.TextColor?.Secondary ?? Color.Red, + LinearGradientMode.Horizontal); + + graphics.DrawString(rectangle.Text, rectangle.Font, gradientBrush, rectangle.Rectangle); + } + else + { + var brush = new SolidBrush(layouter.TextColor?.Primary ?? Color.Black); + graphics.DrawString(rectangle.Text, rectangle.Font, brush, rectangle.Rectangle); + } + } + + bitmap.Save(filePath); + + return this; + } +} \ No newline at end of file diff --git a/TagsCloudContainer/CloudLayouters/RectangleArranger.cs b/TagsCloudContainer/CloudLayouters/RectangleArranger.cs new file mode 100644 index 00000000..9242692b --- /dev/null +++ b/TagsCloudContainer/CloudLayouters/RectangleArranger.cs @@ -0,0 +1,53 @@ +using System.Drawing; +using TagsCloudContainer.Extensions; + +namespace TagsCloudContainer.CloudLayouters; + +public class RectangleArranger +{ + private readonly List _rectangles; + private readonly ISpiral spiral; + + public RectangleArranger(ISpiral spiral) + { + _rectangles = []; + this.spiral = spiral; + } + + public Rectangle ArrangeRectangle(Size rectangleSize, Point center) + { + Rectangle newRectangle; + + do + { + var location = spiral.GetNextPoint(); + location.Offset(-rectangleSize.Width / 2, rectangleSize.Height / 2); + newRectangle = new Rectangle(location, rectangleSize); + } + while (IsIntersectsWithAny(newRectangle)); + + return ShiftRectangleToCenter(newRectangle, center); + } + + private bool IsIntersectsWithAny(Rectangle rectangle) + { + return _rectangles.Any(existingRectangle => existingRectangle.IntersectsWith(rectangle)); + } + + private Rectangle ShiftRectangleToCenter(Rectangle rectangle, Point center) + { + var directionToCenter = rectangle.GetDirectionToCenter(center); + while (directionToCenter != Point.Empty) + { + var nextRectangle = rectangle.Move(directionToCenter); + if (IsIntersectsWithAny(nextRectangle)) + break; + + rectangle = nextRectangle; + directionToCenter = rectangle.GetDirectionToCenter(center); + } + + _rectangles.Add(rectangle); + return rectangle; + } +} \ No newline at end of file diff --git a/TagsCloudContainer/CloudLayouters/RectangleMeasurer.cs b/TagsCloudContainer/CloudLayouters/RectangleMeasurer.cs new file mode 100644 index 00000000..a0c37982 --- /dev/null +++ b/TagsCloudContainer/CloudLayouters/RectangleMeasurer.cs @@ -0,0 +1,22 @@ +using System.Drawing; + +namespace TagsCloudContainer.CloudLayouters; + +public static class RectangleMeasurer +{ + private static Bitmap bitmap; + private static Graphics graphics; + + static RectangleMeasurer() + { + bitmap = new Bitmap(1, 1); + graphics = Graphics.FromImage(bitmap); + } + public static Size Measure(string text, Font font) + { + graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; + var format = new StringFormat { FormatFlags = StringFormatFlags.MeasureTrailingSpaces }; + var rectangleFSize = graphics.MeasureString(text, font, int.MaxValue, format); + return new Size((int)rectangleFSize.Width + 2, (int)rectangleFSize.Height + 2); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/ContainerConfig.cs b/TagsCloudContainer/ContainerConfig.cs new file mode 100644 index 00000000..8e14c50d --- /dev/null +++ b/TagsCloudContainer/ContainerConfig.cs @@ -0,0 +1,42 @@ +using Autofac; +using TagsCloudContainer.CloudLayouters; +using TagsCloudContainer.Settings; +using TagsCloudContainer.WordProcessing.ExcludedWordsProvider; +using static TagsCloudContainer.App; + +namespace TagsCloudContainer; + +public static class ContainerConfig +{ + public static IContainer Configure() + { + var builder = new ContainerBuilder(); + + var imageDimensions = GetImageDimensionsFromUser(); + builder.RegisterInstance(new ImageSettings(imageDimensions.Width, imageDimensions.Height)).AsSelf(); + + var fontName = GetFontNameFromUser(); + builder.RegisterInstance(new FontSettings(fontName)).AsSelf(); + + var backgroundColor = GetBackgroundColorsFromUser(); + var textColor = GetTextColorsFromUser(); + builder.RegisterInstance(new ColorSettings(backgroundColor, textColor)).AsSelf(); + + var wordsFileName = GetFileNameFromUser(); + var excludedWordsFileName = GetExcludedWordsFileNameFromUser(); + builder.RegisterInstance(new FilesSettings(wordsFileName, excludedWordsFileName)).AsSelf(); + + builder.RegisterType() + .As() + .WithParameter("center", imageDimensions.Center) + .WithParameter("step", 1.0); + + var defaultExcludedWordsPath = GetDefaultExcludedWordsFilePath(); + builder.RegisterType().AsSelf().WithParameter("filePath", excludedWordsFileName); + builder.RegisterType().AsSelf().WithParameter("settings", new ExcludedWordsSettings(defaultExcludedWordsPath)); + builder.Register(c => new CircularCloudLayouter(imageDimensions.Center, c.Resolve())) + .AsSelf(); + + return builder.Build(); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/Extensions/FontExtensions.cs b/TagsCloudContainer/Extensions/FontExtensions.cs new file mode 100644 index 00000000..bd5d390f --- /dev/null +++ b/TagsCloudContainer/Extensions/FontExtensions.cs @@ -0,0 +1,11 @@ +using System.Drawing; + +namespace TagsCloudContainer.Extensions; + +public static class FontExtensions +{ + public static bool FontExists(this string fontName) + { + return FontFamily.Families.Any(font => font.Name.Equals(fontName, StringComparison.OrdinalIgnoreCase)); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/Extensions/RectangleExtensions.cs b/TagsCloudContainer/Extensions/RectangleExtensions.cs new file mode 100644 index 00000000..4cd48afd --- /dev/null +++ b/TagsCloudContainer/Extensions/RectangleExtensions.cs @@ -0,0 +1,30 @@ +using System.Drawing; + +namespace TagsCloudContainer.Extensions; + +public static class RectangleExtensions +{ + public static bool IsIntersectsWithAny(this IEnumerable rectangles, Rectangle rectangle) + { + return rectangles.Any(tag => tag.IntersectsWith(rectangle)); + } + + public static Point GetDirectionToCenter(this Rectangle rectangle, Point center) + { + var rectangleCenter = new Point( + rectangle.Left + rectangle.Width / 2, + rectangle.Top - rectangle.Height / 2); + + return new Point( + Math.Sign(center.X - rectangleCenter.X), + Math.Sign(center.Y - rectangleCenter.Y) + ); + } + + public static Rectangle Move(this Rectangle rectangle, Point direction) + { + return new Rectangle( + new Point(rectangle.X + direction.X, rectangle.Y + direction.Y), + rectangle.Size); + } +} diff --git a/TagsCloudContainer/Extensions/TagExtensions.cs b/TagsCloudContainer/Extensions/TagExtensions.cs new file mode 100644 index 00000000..cf987d13 --- /dev/null +++ b/TagsCloudContainer/Extensions/TagExtensions.cs @@ -0,0 +1,19 @@ +using System.Drawing; + +namespace TagsCloudContainer.Extensions; + +public static class TagExtensions +{ + public static Size CalculateSize(this List tags) + { + if (tags.Count == 0) + return Size.Empty; + + var left = tags.Select(tag => tag.Rectangle).Min(rectangle => rectangle.Left); + var right = tags.Select(tag => tag.Rectangle).Max(rectangle => rectangle.Right); + var top = tags.Select(tag => tag.Rectangle).Min(rectangle => rectangle.Top); + var bottom = tags.Select(tag => tag.Rectangle).Max(rectangle => rectangle.Bottom); + + return new Size(right - left, bottom - top); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/ImageDimensions.cs b/TagsCloudContainer/ImageDimensions.cs new file mode 100644 index 00000000..095517e5 --- /dev/null +++ b/TagsCloudContainer/ImageDimensions.cs @@ -0,0 +1,17 @@ +using System.Drawing; + +namespace TagsCloudContainer; + +public class ImageDimensions +{ + public readonly int Width; + public readonly int Height; + public readonly Point Center; + + public ImageDimensions(int width, int height) + { + Width = width; + Height = height; + Center = new Point(Width / 2, Height / 2); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/Program.cs b/TagsCloudContainer/Program.cs new file mode 100644 index 00000000..5384cedc --- /dev/null +++ b/TagsCloudContainer/Program.cs @@ -0,0 +1,42 @@ +using Autofac; +using TagsCloudContainer.CloudLayouters; +using TagsCloudContainer.ExcludedWordsProvider; +using TagsCloudContainer.Settings; +using TagsCloudContainer.WordProcessing.ExcludedWordsProvider; +using static TagsCloudContainer.App; + +namespace TagsCloudContainer; + +public static class Program +{ + public static void Main() + { + using var scope = ContainerConfig.Configure().BeginLifetimeScope(); + + var layouter = scope.Resolve(); + var imageSettings = scope.Resolve(); + var fontSettings = scope.Resolve(); + var colorSettings = scope.Resolve(); + var filesSettings = scope.Resolve(); + + var wordProcessor = scope.Resolve().Create(); + var fileExcludedWordsProvider = scope.Resolve().Create(); + + var words = wordProcessor + .GetWordsForCloud(filesSettings.Words) + .ExcludeWords(fileExcludedWordsProvider) + .DisableDefaultExclude() + .ToDictionary();; + + layouter + .SetFontName(fontSettings.FontName) + .SetBackgroundColor(colorSettings.BackgroundColor) + .SetTextColor(colorSettings.TextColor) + .PutTags(words) + .CreateView(imageSettings.Width, imageSettings.Height) + .SaveImage("cloud.jpeg") + .SaveImage("cloud.png") + .SaveImage("cloud.bmp") + .SaveImage("cloud.tiff"); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/Settings/ColorSettings.cs b/TagsCloudContainer/Settings/ColorSettings.cs new file mode 100644 index 00000000..f83b1cf0 --- /dev/null +++ b/TagsCloudContainer/Settings/ColorSettings.cs @@ -0,0 +1,15 @@ +using System.Drawing; + +namespace TagsCloudContainer.Settings; + +public class ColorSettings +{ + public (Color, Color?) BackgroundColor { get; } + public (Color, Color?) TextColor { get; } + + public ColorSettings((Color, Color?) backgroundColor, (Color, Color?) textColor) + { + BackgroundColor = backgroundColor; + TextColor = textColor; + } +} \ No newline at end of file diff --git a/TagsCloudContainer/Settings/ExcludedWordsSettings.cs b/TagsCloudContainer/Settings/ExcludedWordsSettings.cs new file mode 100644 index 00000000..2f3e51be --- /dev/null +++ b/TagsCloudContainer/Settings/ExcludedWordsSettings.cs @@ -0,0 +1,13 @@ +namespace TagsCloudContainer; + +public class ExcludedWordsSettings +{ + public string DefaultExcludedWordsPath { get; } + public bool EnableDefaultExclude { get; set; } + + public ExcludedWordsSettings(string defaultExcludedWordsPath, bool enableDefaultExclude = true) + { + DefaultExcludedWordsPath = defaultExcludedWordsPath; + EnableDefaultExclude = enableDefaultExclude; + } +} \ No newline at end of file diff --git a/TagsCloudContainer/Settings/FilesSettings.cs b/TagsCloudContainer/Settings/FilesSettings.cs new file mode 100644 index 00000000..a3d2b870 --- /dev/null +++ b/TagsCloudContainer/Settings/FilesSettings.cs @@ -0,0 +1,13 @@ +namespace TagsCloudContainer.Settings; + +public class FilesSettings +{ + public string Words { get; } + public string ExcludedWords { get; } + + public FilesSettings(string words, string excludedWords) + { + Words = words; + ExcludedWords = excludedWords; + } +} \ No newline at end of file diff --git a/TagsCloudContainer/Settings/FontSettings.cs b/TagsCloudContainer/Settings/FontSettings.cs new file mode 100644 index 00000000..a7e15790 --- /dev/null +++ b/TagsCloudContainer/Settings/FontSettings.cs @@ -0,0 +1,11 @@ +namespace TagsCloudContainer.Settings; + +public class FontSettings +{ + public string FontName { get; } + + public FontSettings(string fontName) + { + FontName = fontName; + } +} \ No newline at end of file diff --git a/TagsCloudContainer/Settings/ImageSettings.cs b/TagsCloudContainer/Settings/ImageSettings.cs new file mode 100644 index 00000000..02a3e19b --- /dev/null +++ b/TagsCloudContainer/Settings/ImageSettings.cs @@ -0,0 +1,13 @@ +namespace TagsCloudContainer.Settings; + +public class ImageSettings +{ + public int Width { get; } + public int Height { get; } + + public ImageSettings(int width, int height) + { + Width = width; + Height = height; + } +} diff --git a/TagsCloudContainer/Spiral/ArchimedeanSpiral.cs b/TagsCloudContainer/Spiral/ArchimedeanSpiral.cs new file mode 100644 index 00000000..c31016e7 --- /dev/null +++ b/TagsCloudContainer/Spiral/ArchimedeanSpiral.cs @@ -0,0 +1,26 @@ +using System; +using System.Drawing; + +namespace TagsCloudContainer; + +public class ArchimedeanSpiral : ISpiral +{ + private readonly Point center; + private double angle; + private readonly double spiralStep; + + public ArchimedeanSpiral(Point center, double spiralStep = 0.1) + { + this.center = center; + this.spiralStep = spiralStep; + angle = 0; + } + + public Point GetNextPoint() + { + var x = (int)(center.X + angle * Math.Cos(angle)); + var y = (int)(center.Y + angle * Math.Sin(angle)); + angle += spiralStep; + return new Point(x, y); + } +} diff --git a/TagsCloudContainer/Spiral/ISpiral.cs b/TagsCloudContainer/Spiral/ISpiral.cs new file mode 100644 index 00000000..8fb4ce53 --- /dev/null +++ b/TagsCloudContainer/Spiral/ISpiral.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudContainer; + +public interface ISpiral +{ + public Point GetNextPoint(); +} \ No newline at end of file diff --git a/TagsCloudContainer/Tag.cs b/TagsCloudContainer/Tag.cs new file mode 100644 index 00000000..55eba0c8 --- /dev/null +++ b/TagsCloudContainer/Tag.cs @@ -0,0 +1,17 @@ +using System.Drawing; + +namespace TagsCloudContainer; + +public class Tag +{ + public string Text { get; } + public Font Font { get; } + public Rectangle Rectangle { get; } + + public Tag(string text, Font font, Rectangle rectangle) + { + Text = text; + Font = font; + Rectangle = rectangle; + } +} \ No newline at end of file diff --git a/TagsCloudContainer/TagsCloudContainer.csproj b/TagsCloudContainer/TagsCloudContainer.csproj new file mode 100644 index 00000000..f14cbe02 --- /dev/null +++ b/TagsCloudContainer/TagsCloudContainer.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + enable + enable + 12 + + + + + + + + + + + + + + + diff --git a/TagsCloudContainer/WordProcessing/ExcludedWordsProvider/ExcludedWordsProviderFactory.cs b/TagsCloudContainer/WordProcessing/ExcludedWordsProvider/ExcludedWordsProviderFactory.cs new file mode 100644 index 00000000..0c365dc8 --- /dev/null +++ b/TagsCloudContainer/WordProcessing/ExcludedWordsProvider/ExcludedWordsProviderFactory.cs @@ -0,0 +1,25 @@ +using TagsCloudContainer.WordProcessing.Parsers; + +namespace TagsCloudContainer.WordProcessing.ExcludedWordsProvider; + +public class ExcludedWordsProviderFactory : IExcludedWordsProviderFactory +{ + private readonly string filePath; + + public ExcludedWordsProviderFactory(string filePath) + { + this.filePath = filePath; + } + + public FileExcludedWordsProvider Create() + { + var parsers = new Dictionary + { + { ".txt", new ParserTxt() }, + { ".doc", new ParserDoc() }, + { ".docx", new ParserDoc() } + }; + + return new FileExcludedWordsProvider(filePath, parsers); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/WordProcessing/ExcludedWordsProvider/FileExcludedWordsProvider.cs b/TagsCloudContainer/WordProcessing/ExcludedWordsProvider/FileExcludedWordsProvider.cs new file mode 100644 index 00000000..3c7b1cc4 --- /dev/null +++ b/TagsCloudContainer/WordProcessing/ExcludedWordsProvider/FileExcludedWordsProvider.cs @@ -0,0 +1,26 @@ +using TagsCloudContainer.ExcludedWordsProvider; + +namespace TagsCloudContainer.WordProcessing.ExcludedWordsProvider; + +public class FileExcludedWordsProvider : IExcludedWordsProvider +{ + private readonly string filePath; + private readonly IDictionary parsers; + + public FileExcludedWordsProvider(string filePath, IDictionary parsers) + { + this.filePath = filePath; + this.parsers = parsers; + } + + public IEnumerable GetExcludedWords() + { + if (!File.Exists(filePath)) + return []; + + var extension = Path.GetExtension(filePath).ToLower(); + return parsers.TryGetValue(extension, out var parser) + ? parser.GetWords(filePath) + : Enumerable.Empty(); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/WordProcessing/ExcludedWordsProvider/IExcludedWordsProvider.cs b/TagsCloudContainer/WordProcessing/ExcludedWordsProvider/IExcludedWordsProvider.cs new file mode 100644 index 00000000..3f1e3f41 --- /dev/null +++ b/TagsCloudContainer/WordProcessing/ExcludedWordsProvider/IExcludedWordsProvider.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.ExcludedWordsProvider; + +public interface IExcludedWordsProvider +{ + IEnumerable GetExcludedWords(); +} \ No newline at end of file diff --git a/TagsCloudContainer/WordProcessing/ExcludedWordsProvider/IExcludedWordsProviderFactory.cs b/TagsCloudContainer/WordProcessing/ExcludedWordsProvider/IExcludedWordsProviderFactory.cs new file mode 100644 index 00000000..78244b82 --- /dev/null +++ b/TagsCloudContainer/WordProcessing/ExcludedWordsProvider/IExcludedWordsProviderFactory.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.WordProcessing.ExcludedWordsProvider; + +public interface IExcludedWordsProviderFactory +{ + FileExcludedWordsProvider Create(); +} \ No newline at end of file diff --git a/TagsCloudContainer/WordProcessing/IWordProcessorFactory.cs b/TagsCloudContainer/WordProcessing/IWordProcessorFactory.cs new file mode 100644 index 00000000..698a533c --- /dev/null +++ b/TagsCloudContainer/WordProcessing/IWordProcessorFactory.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer; + +public interface IWordProcessorFactory +{ + WordProcessor Create(); +} \ No newline at end of file diff --git a/TagsCloudContainer/WordProcessing/Parsers/IParser.cs b/TagsCloudContainer/WordProcessing/Parsers/IParser.cs new file mode 100644 index 00000000..4d9cf53b --- /dev/null +++ b/TagsCloudContainer/WordProcessing/Parsers/IParser.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer; + +public interface IParser +{ + public List GetWords(string filePath); +} \ No newline at end of file diff --git a/TagsCloudContainer/WordProcessing/Parsers/ParserDoc.cs b/TagsCloudContainer/WordProcessing/Parsers/ParserDoc.cs new file mode 100644 index 00000000..eba8108a --- /dev/null +++ b/TagsCloudContainer/WordProcessing/Parsers/ParserDoc.cs @@ -0,0 +1,17 @@ +using Aspose.Words; + +namespace TagsCloudContainer.WordProcessing.Parsers; + +public class ParserDoc : IParser +{ + public List GetWords(string filePath) + { + var doc = new Document(filePath); + var content = doc.GetText(); + return content.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries) + .Select(w => w.ToLower()) + .Skip(1) + .SkipLast(1) + .ToList(); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/WordProcessing/Parsers/ParserTxt.cs b/TagsCloudContainer/WordProcessing/Parsers/ParserTxt.cs new file mode 100644 index 00000000..934cf544 --- /dev/null +++ b/TagsCloudContainer/WordProcessing/Parsers/ParserTxt.cs @@ -0,0 +1,11 @@ +namespace TagsCloudContainer.WordProcessing.Parsers; + +public class ParserTxt : IParser +{ + public List GetWords(string filePath) + { + return File.ReadAllLines(filePath) + .Select(word => word.ToLower()) + .ToList(); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/WordProcessing/WordProcessor.cs b/TagsCloudContainer/WordProcessing/WordProcessor.cs new file mode 100644 index 00000000..c6e62869 --- /dev/null +++ b/TagsCloudContainer/WordProcessing/WordProcessor.cs @@ -0,0 +1,60 @@ +using Aspose.Words; +using TagsCloudContainer.ExcludedWordsProvider; + +namespace TagsCloudContainer; + +public class WordProcessor +{ + private readonly List words = []; + private readonly HashSet excludedWords = []; + private readonly Dictionary parsers; + private readonly ExcludedWordsSettings settings; + + public WordProcessor(Dictionary handlers, ExcludedWordsSettings settings) + { + parsers = handlers ?? throw new ArgumentNullException(nameof(handlers)); + this.settings = settings ?? throw new ArgumentNullException(nameof(settings)); + } + + public WordProcessor GetWordsForCloud(string wordsPath) + { + if (!File.Exists(wordsPath)) return this; + + var extension = Path.GetExtension(wordsPath).ToLower(); + if (parsers.TryGetValue(extension, out var parser)) + { + words.AddRange(parser.GetWords(wordsPath)); + } + + return this; + } + + public WordProcessor ExcludeWords(IExcludedWordsProvider provider) + { + excludedWords.UnionWith(provider.GetExcludedWords()); + return this; + } + + public WordProcessor DisableDefaultExclude() + { + settings.EnableDefaultExclude = false; + return this; + } + + public Dictionary ToDictionary() + { + if (settings.EnableDefaultExclude) + { + var defaultExcludedWords = File.ReadAllLines(settings.DefaultExcludedWordsPath) + .Select(word => word.ToLower()) + .ToHashSet(); + + excludedWords.UnionWith(defaultExcludedWords); + } + + return words + .Where(word => !excludedWords.Contains(word)) + .GroupBy(word => word) + .ToDictionary(group => group.Key, group => group.Count()); + } +} diff --git a/TagsCloudContainer/WordProcessing/WordProcessorFactory.cs b/TagsCloudContainer/WordProcessing/WordProcessorFactory.cs new file mode 100644 index 00000000..43d1c66a --- /dev/null +++ b/TagsCloudContainer/WordProcessing/WordProcessorFactory.cs @@ -0,0 +1,25 @@ +using TagsCloudContainer.WordProcessing.Parsers; + +namespace TagsCloudContainer; + +public class WordProcessorFactory : IWordProcessorFactory +{ + private readonly ExcludedWordsSettings settings; + + public WordProcessorFactory(ExcludedWordsSettings settings) + { + this.settings = settings; + } + + public WordProcessor Create() + { + var parsers = new Dictionary + { + { ".txt", new ParserTxt() }, + { ".doc", new ParserDoc() }, + { ".docx", new ParserDoc() } + }; + + return new WordProcessor(parsers, settings); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/WordProcessing/cloud.doc b/TagsCloudContainer/WordProcessing/cloud.doc new file mode 100644 index 00000000..38ef07d0 Binary files /dev/null and b/TagsCloudContainer/WordProcessing/cloud.doc differ diff --git a/TagsCloudContainer/WordProcessing/cloud.docx b/TagsCloudContainer/WordProcessing/cloud.docx new file mode 100644 index 00000000..c3a86ec9 Binary files /dev/null and b/TagsCloudContainer/WordProcessing/cloud.docx differ diff --git a/TagsCloudContainer/WordProcessing/cloud.txt b/TagsCloudContainer/WordProcessing/cloud.txt new file mode 100644 index 00000000..c762b8e7 --- /dev/null +++ b/TagsCloudContainer/WordProcessing/cloud.txt @@ -0,0 +1,79 @@ +В +связи +с +логической +эквивалентностью +аппаратного +и +программного +обеспечения +упомянем +проблему +семантического +разрыва +В +контексте +изучаемой +нами +темы +под +семантическим +разрывом +понимается +различие +принципов +положенных +в +основу +программного +и +аппаратного +обеспечения +Это +проблема +возникает +на +самых +разных +уровнях +на +уровне +архитектуры +компьютера +между +средствами +формулировки +операций +и +операндов +на +языках +высокого +уровня +и +аппаратных +средств +на +уровне +прикладных +программ +и +языковыми +средствами +на +уровне +операционной +системы +на +уровне +архитектуры +процессора +между +аппаратными +средствами +и +тем +на +что +они +опираются \ No newline at end of file diff --git a/TagsCloudContainer/WordProcessing/excluded_words.txt b/TagsCloudContainer/WordProcessing/excluded_words.txt new file mode 100644 index 00000000..0e54d032 --- /dev/null +++ b/TagsCloudContainer/WordProcessing/excluded_words.txt @@ -0,0 +1,34 @@ +и +в +на +с +к +по +за +о +у +из +это +я +ты +он +она +мы +вы +они +тот +эта +эти +быть +есть +а +но +как +же +или +что +то +так +его +ее +их \ No newline at end of file diff --git a/desktop.ini b/desktop.ini new file mode 100644 index 00000000..aa58f061 --- /dev/null +++ b/desktop.ini @@ -0,0 +1,6 @@ +[.ShellClassInfo] +IconResource=C:\Users\Virtical\Downloads\favicon (5).ico,0 +[ViewState] +Mode= +Vid= +FolderType=Generic diff --git a/di.sln b/di.sln index a50991da..d60ff743 100644 --- a/di.sln +++ b/di.sln @@ -2,6 +2,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "di", "FractalPainter/di.csproj", "{4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudContainer", "TagsCloudContainer\TagsCloudContainer.csproj", "{BF97F285-E714-4A96-BAA3-56D091205F10}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudContainer.Tests", "TagsCloudContainer.Tests\TagsCloudContainer.Tests.csproj", "{244F8D38-79CC-4135-A85D-4AF82322B0DD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +16,13 @@ Global {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Release|Any CPU.Build.0 = Release|Any CPU + {BF97F285-E714-4A96-BAA3-56D091205F10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF97F285-E714-4A96-BAA3-56D091205F10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF97F285-E714-4A96-BAA3-56D091205F10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF97F285-E714-4A96-BAA3-56D091205F10}.Release|Any CPU.Build.0 = Release|Any CPU + {244F8D38-79CC-4135-A85D-4AF82322B0DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {244F8D38-79CC-4135-A85D-4AF82322B0DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {244F8D38-79CC-4135-A85D-4AF82322B0DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {244F8D38-79CC-4135-A85D-4AF82322B0DD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal