diff --git a/Homework/TagCloud2/TagCloud2.sln b/Homework/TagCloud2/TagCloud2.sln new file mode 100644 index 000000000..62ef07f57 --- /dev/null +++ b/Homework/TagCloud2/TagCloud2.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36804.6 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudLibrary", "TagCloudLibrary\TagCloudLibrary.csproj", "{B31FD7D6-0B7C-45AE-BFA7-4B476F9BC962}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudTests", "TagCloudTests\TagCloudTests.csproj", "{58B4FC78-D0EE-422D-B095-2D53BEE0B3CC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudCLIApp", "TagCloudCLIApp\TagCloudCLIApp.csproj", "{5D39F8AF-24B7-4815-A70A-44ACF2A1E226}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B31FD7D6-0B7C-45AE-BFA7-4B476F9BC962}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B31FD7D6-0B7C-45AE-BFA7-4B476F9BC962}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B31FD7D6-0B7C-45AE-BFA7-4B476F9BC962}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B31FD7D6-0B7C-45AE-BFA7-4B476F9BC962}.Release|Any CPU.Build.0 = Release|Any CPU + {58B4FC78-D0EE-422D-B095-2D53BEE0B3CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58B4FC78-D0EE-422D-B095-2D53BEE0B3CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58B4FC78-D0EE-422D-B095-2D53BEE0B3CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58B4FC78-D0EE-422D-B095-2D53BEE0B3CC}.Release|Any CPU.Build.0 = Release|Any CPU + {5D39F8AF-24B7-4815-A70A-44ACF2A1E226}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D39F8AF-24B7-4815-A70A-44ACF2A1E226}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D39F8AF-24B7-4815-A70A-44ACF2A1E226}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D39F8AF-24B7-4815-A70A-44ACF2A1E226}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D68EB29C-EA44-4F73-99D0-0CF62E7ED807} + EndGlobalSection +EndGlobal diff --git a/Homework/TagCloud2/TagCloudCLIApp/CommandLineArguments.cs b/Homework/TagCloud2/TagCloudCLIApp/CommandLineArguments.cs new file mode 100644 index 000000000..e522a2f12 --- /dev/null +++ b/Homework/TagCloud2/TagCloudCLIApp/CommandLineArguments.cs @@ -0,0 +1,57 @@ +using CommandLine; +using TagCloudLibrary.Preprocessor; + +namespace TagCloudCLIApp; + +internal class CommandLineArguments +{ + [Option("min-font-size", Default = 20, Required = false, HelpText = "Set minimal font size in tag cloud.")] + public float MinFontSize { get; set; } + + [Option("max-font-size", Default = 100, Required = false, HelpText = "Set maximal font size in tag cloud.")] + public float MaxFontSize { get; set; } + + [Option("text-gap", Default = 12, Required = false, HelpText = "Set gap between text.")] + public float TextGap { get; set; } + + [Option("picture-border-size", Default = 20, Required = false, HelpText = "Set border around picture.")] + public int PictureBorderSize { get; set; } + + [Option("background-color", Default = "#000000", Required = false, HelpText = "Set backgroud color.")] + public string BackgroudColor { get; set; } + + [Option("foreground-color", Default = null, Required = false, HelpText = "Set text color.")] + public string? ForegroundColor { get; set; } + + [Option("font-family-name", Default = null, Required = false, HelpText = "Set font family name.")] + public string? FontFamilyName { get; set; } + + [Option("image-width", Default = null, Required = false, HelpText = "Set image width.")] + public int? ImageWidth { get; set; } + + [Option("image-height", Default = null, Required = false, HelpText = "Set image height.")] + public int? ImageHeight { get; set; } + + [Option("image-format", Default = "Png", Required = false, HelpText = "Set image format.")] + public string ImageFormat { get; set; } + + [Option( + "selected-parts-of-speech", + Default = new string[] + { + nameof(PartOfSpeech.Adjective), + nameof(PartOfSpeech.Adverb), + nameof(PartOfSpeech.Composite), + nameof(PartOfSpeech.Noun), + nameof(PartOfSpeech.Verb) + }, + Required = false, + HelpText = "Sets the selected of the following parts of speech: Adjective, Adverb, PronominalAdverb, NumeralAdjective, AdjectivePronoun, Composite, Conjunction, Interjection, Numeral, Particle, Pretext, Noun, PronounNoun, Verb.")] + public IEnumerable SelectedPartsOfSpeech { get; set; } + + [Value(0, MetaName = nameof(WordsFilePath), Required = true, HelpText = "Specifies a path to file in the .txt format.")] + public string WordsFilePath { get; set; } + + [Value(1, MetaName = nameof(ImageFilePath), Required = true, HelpText = "Specifies a path to file in the .png format.")] + public string ImageFilePath { get; set; } +} diff --git a/Homework/TagCloud2/TagCloudCLIApp/Program.cs b/Homework/TagCloud2/TagCloudCLIApp/Program.cs new file mode 100644 index 000000000..16fe0e4f2 --- /dev/null +++ b/Homework/TagCloud2/TagCloudCLIApp/Program.cs @@ -0,0 +1,151 @@ +using Autofac; +using CommandLine; +using SkiaSharp; +using TagCloudLibrary; +using TagCloudLibrary.Layouter; +using TagCloudLibrary.Preprocessor; +using TagCloudLibrary.ResultPattern; +using TagCloudLibrary.Visualizer; + +namespace TagCloudCLIApp; + +internal class Program +{ + private static IContainer Container { get; set; } + + static void Main(string[] args) + { + Parser.Default.ParseArguments(args) + .WithParsed(Run) + .WithNotParsed(HandleParseError); + } + + private static void Run(CommandLineArguments args) + { + SetUpContainer(args) + .Then(_ => Result.Of(() => GetWordsFromFile(args.WordsFilePath))) + .Then(GetTagCloudImage) + .Then(r => + SaveTagCloudImage( + args.ImageFilePath, + r, + args.ImageFormat)) + .OnFail(e => Console.Error.WriteLine($"Critical Error: {e}")); + } + + private static Result SetUpContainer(CommandLineArguments args) + { + var builder = new ContainerBuilder(); + return RegisterWordPreprocessorOptions(args, builder) + .Then(_ => Result.OfAction(() => RegisterTagCloudOptions(args, builder))) + .Then(_ => RegisterTagCloudVisualizerOptions(args, builder)) + .Then(_ => + { + + builder.RegisterType().As(); + builder.Register(c => new CircularCloudLayouter(new())).As(); + builder.RegisterType().As(); + builder.RegisterType().AsSelf(); + + Container = builder.Build(); + }); + } + + private static Result RegisterTagCloudVisualizerOptions(CommandLineArguments args, ContainerBuilder builder) + { + if (!SKColor.TryParse(args.BackgroudColor, out var backgroundColor)) + return Result.Fail("Incorrect background color."); + + if (!SKColor.TryParse(args.ForegroundColor, out var foregroundColor)) + return Result.Fail("Incorrect foreground color."); + + var tagCloudVisualizerOptions = new TagCloudVisualizerOptions( + args.ImageWidth, + args.ImageHeight, + args.PictureBorderSize, + backgroundColor, + args.ForegroundColor is null ? null : foregroundColor); + builder.RegisterInstance(tagCloudVisualizerOptions); + + return Result.Ok(); + } + + private static void RegisterTagCloudOptions(CommandLineArguments args, ContainerBuilder builder) + { + var tagCloudOptions = new TagCloudOptions( + args.FontFamilyName is null ? null : SKTypeface.FromFamilyName(args.FontFamilyName), + args.MinFontSize, + args.MaxFontSize, + args.TextGap); + builder.RegisterInstance(tagCloudOptions); + } + + private static Result RegisterWordPreprocessorOptions(CommandLineArguments args, ContainerBuilder builder) + { + var partsOfSpeech = GetAndValidatePartsOfSpeech(args.SelectedPartsOfSpeech); + var uniquePartsOfSpeech = new HashSet(); + foreach (var partOfSpeech in partsOfSpeech) + { + if (partOfSpeech.IsSuccess) + uniquePartsOfSpeech.Add(partOfSpeech.GetValueOrThrow()); + else + Console.WriteLine($"Warn: {partOfSpeech.Error}."); + } + + var wordPreprocessorOptions = new WordPreprocessorOptions(uniquePartsOfSpeech); + builder.RegisterInstance(wordPreprocessorOptions); + + return Result.Ok(); + } + + private static void HandleParseError(IEnumerable errs) + { + if (errs.IsVersion()) + { + Console.WriteLine("Version Request"); + return; + } + + if (errs.IsHelp()) + { + Console.WriteLine("Help Request"); + return; + } + Console.WriteLine("Parser Fail"); + } + + private static IEnumerable> GetAndValidatePartsOfSpeech(IEnumerable partsOfSpeech) + { + foreach (var partOfSpeech in partsOfSpeech) + yield return Enum.TryParse(partOfSpeech, out var result) + ? result + : Result.Fail( + $"The part of speech ({partOfSpeech}) is not one of the defined parts of speech."); + } + + private static Result GetTagCloudImage(string[] words) + { + using var scope = Container.BeginLifetimeScope(); + var cloud = scope.Resolve(); + + return cloud + .BuildTagTree(words) + .Then(_ => cloud.CreateImage()); + } + + private static string[] GetWordsFromFile(string path) => File.ReadAllText(path).Split(); + + private static Result SaveTagCloudImage(string path, SKImage image, string format) + { + if (!Enum.TryParse(format, out var imageFormat)) + return Result.Fail("Incorrect image encode format."); + + using var data = image.Encode(imageFormat, 100); + using var stream = File.OpenWrite(path); + if (data is null) + return Result.Fail($"Can't encode bitmap into format {format}."); + + data.SaveTo(stream); + return Result.Ok(); + } +} diff --git a/Homework/TagCloud2/TagCloudCLIApp/TagCloudCLIApp.csproj b/Homework/TagCloud2/TagCloudCLIApp/TagCloudCLIApp.csproj new file mode 100644 index 000000000..feff93f91 --- /dev/null +++ b/Homework/TagCloud2/TagCloudCLIApp/TagCloudCLIApp.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Homework/TagCloud2/TagCloudLibrary/Layouter/CircularCloudLayouter.cs b/Homework/TagCloud2/TagCloudLibrary/Layouter/CircularCloudLayouter.cs new file mode 100644 index 000000000..d6b4e04b5 --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Layouter/CircularCloudLayouter.cs @@ -0,0 +1,92 @@ +using SkiaSharp; +using TagCloudLibrary.ResultPattern; +using static System.Math; + +namespace TagCloudLibrary.Layouter; + +public class CircularCloudLayouter(SKPoint center) : ICloudLayouter +{ + private const double radiusStep = 1; + private const double angleStep = .01; + private double radius = 0; + private double angle = 0; + private readonly List placedRectangles = []; + + public Result PutNextRectangle(SKSize rectangleSize) + { + if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0) + return Result.Fail("Recatngle size should be greater then zero."); + + var rectangle = FindRectangleWithCorrectPosition(rectangleSize); + placedRectangles.Add(rectangle); + return rectangle; + } + + private SKRect FindRectangleWithCorrectPosition(SKSize size) + { + if (radius == 0) + { + radius += radiusStep; + return SKRect.Create(center - new SKSize(size.Width / 2, size.Height / 2), size); + } + + while (!CanPlaceRectangle(CreateRectangleAwayFromCenter(center, angle, radius, size))) + { + angle += angleStep; + + if (angle > 2 * PI) + { + angle = 0; + radius += radiusStep; + } + } + + return PullRectangleToCenter(size); + } + + private bool CanPlaceRectangle(SKRect rectangle) + { + foreach (var placedRectangle in placedRectangles) + if (rectangle.IntersectsWith(placedRectangle)) + return false; + + return true; + } + + /// + /// Pulls a rectangle toward the center of the cloud until it is centered (radius + circumscribingCircleRadius = 0) or intersects with another rectangle. + /// + private SKRect PullRectangleToCenter(SKSize size) + { + var currentRadius = radius; + var circumscribingCircleRadius = GetCircumscribingCircleRadius(size); + + while (currentRadius > -circumscribingCircleRadius + && CanPlaceRectangle(CreateRectangleAwayFromCenter(center, angle, currentRadius, size))) + currentRadius -= radiusStep; + + currentRadius += radiusStep; + return CreateRectangleAwayFromCenter(center, angle, currentRadius, size); + } + + /// + /// The method creates a rectangle at a distance from the cloud center to the circumscribed circle of a rectangle. + /// + private static SKRect CreateRectangleAwayFromCenter(SKPoint center, double angle, double distance, SKSize size) + { + var circumscribingCircleRadius = GetCircumscribingCircleRadius(size); + var location = CreatePointAwayFromCenter(center, angle, distance + circumscribingCircleRadius) - new SKSize(size.Width / 2, size.Height / 2); + + return SKRect.Create(location, size); + } + + private static double GetCircumscribingCircleRadius(SKSize size) + => Sqrt( + size.Width / 2 * (size.Width / 2) + + size.Height / 2 * (size.Height / 2)); + + private static SKPoint CreatePointAwayFromCenter(SKPoint center, double angle, double distance) + => new( + center.X + (float)(distance * Cos(angle)), + center.Y + (float)(distance * Sin(angle))); +} diff --git a/Homework/TagCloud2/TagCloudLibrary/Layouter/ICloudLayouter.cs b/Homework/TagCloud2/TagCloudLibrary/Layouter/ICloudLayouter.cs new file mode 100644 index 000000000..c2723a70f --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Layouter/ICloudLayouter.cs @@ -0,0 +1,9 @@ +using SkiaSharp; +using TagCloudLibrary.ResultPattern; + +namespace TagCloudLibrary.Layouter; + +public interface ICloudLayouter +{ + Result PutNextRectangle(SKSize rectangleSize); +} diff --git a/Homework/TagCloud2/TagCloudLibrary/Layouter/PlacedText.cs b/Homework/TagCloud2/TagCloudLibrary/Layouter/PlacedText.cs new file mode 100644 index 000000000..2bbe7e80a --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Layouter/PlacedText.cs @@ -0,0 +1,5 @@ +using SkiaSharp; + +namespace TagCloudLibrary.Layouter; + +public record class PlacedText(string Text, SKFont Font, SKRect Place); diff --git a/Homework/TagCloud2/TagCloudLibrary/Preprocessor/AnalysisInfo.cs b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/AnalysisInfo.cs new file mode 100644 index 000000000..2371b05a2 --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/AnalysisInfo.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace TagCloudLibrary.Preprocessor; + +public class AnalysisInfo +{ + [JsonPropertyName("lex")] + public string Lexeme { get; set; } + + [JsonPropertyName("gr")] + public string Gramme { get; set; } +} diff --git a/Homework/TagCloud2/TagCloudLibrary/Preprocessor/IWordPreprocessor.cs b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/IWordPreprocessor.cs new file mode 100644 index 000000000..ac5523495 --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/IWordPreprocessor.cs @@ -0,0 +1,8 @@ +using TagCloudLibrary.ResultPattern; + +namespace TagCloudLibrary.Preprocessor; + +public interface IWordPreprocessor +{ + Result> Process(IEnumerable words); +} diff --git a/Homework/TagCloud2/TagCloudLibrary/Preprocessor/PartOfSpeech.cs b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/PartOfSpeech.cs new file mode 100644 index 000000000..8845542f4 --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/PartOfSpeech.cs @@ -0,0 +1,37 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace TagCloudLibrary.Preprocessor; + +[JsonConverter(typeof(JsonStringEnumMemberConverter))] +public enum PartOfSpeech +{ + [EnumMember(Value = "A")] + Adjective, + [EnumMember(Value = "ADV")] + Adverb, + [EnumMember(Value = "ADVPRO")] + PronominalAdverb, + [EnumMember(Value = "ANUM")] + NumeralAdjective, + [EnumMember(Value = "APRO")] + AdjectivePronoun, + [EnumMember(Value = "COM")] + Composite, + [EnumMember(Value = "CONJ")] + Conjunction, + [EnumMember(Value = "INTJ")] + Interjection, + [EnumMember(Value = "NUM")] + Numeral, + [EnumMember(Value = "PART")] + Particle, + [EnumMember(Value = "PR")] + Pretext, + [EnumMember(Value = "S")] + Noun, + [EnumMember(Value = "SPRO")] + PronounNoun, + [EnumMember(Value = "V")] + Verb +} diff --git a/Homework/TagCloud2/TagCloudLibrary/Preprocessor/WordAnalysis.cs b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/WordAnalysis.cs new file mode 100644 index 000000000..65a770304 --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/WordAnalysis.cs @@ -0,0 +1,19 @@ +using System.Text.Json; + +namespace TagCloudLibrary.Preprocessor; + +public class WordAnalysis +{ + public List Analysis { get; set; } + public string Text { get; set; } + + public WordWithPartOfSpeech ToWordWithPartOfSpeech() + { + var firstGramme = Analysis.First().Gramme; + var endPartOfSpeechText = firstGramme.IndexOfAny([',', '=']); + var jsonOption = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var partOfSpeech = JsonSerializer.Deserialize($"\"{firstGramme[..endPartOfSpeechText]}\"", jsonOption); + + return new(Text, partOfSpeech); + } +} diff --git a/Homework/TagCloud2/TagCloudLibrary/Preprocessor/WordPreprocessor.cs b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/WordPreprocessor.cs new file mode 100644 index 000000000..e477b82cf --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/WordPreprocessor.cs @@ -0,0 +1,82 @@ +using System.Diagnostics; +using System.Text.Json; +using TagCloudLibrary.ResultPattern; + +namespace TagCloudLibrary.Preprocessor; + +public class WordPreprocessor(WordPreprocessorOptions options) : IWordPreprocessor +{ + public Result> Process(IEnumerable words) + { + return GetAnalyzedWords(words) + .Then(r => r + .Where(t => options.SelectedPartsOfSpeech.Contains(t.PartOfSpeech)) + .Select(t => t.Word.ToLower()) + .ToList()); + } + + private static Result> GetAnalyzedWords(IEnumerable words) + { + var inputFilePath = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName()); + var outputFilePath = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName()); + + return Result + .OfAction( + () => PrepareInputFile(inputFilePath, words), + $"Can't prepare input file {inputFilePath} for mystem.exe") + .Then(_ => Result.OfAction( + () => PrepareOutputFile(outputFilePath), + $"Can't prepare output file {outputFilePath} for mystem.exe")) + .Then(_ => Result.Of(() => ProcessWords(inputFilePath, outputFilePath))) + .Then(r => Result.Of(() => ParseProcessingResult(r))); + } + + private static List ParseProcessingResult(string output) + { + var jsonOption = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + var jsonOutput = output + .Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(t => JsonSerializer.Deserialize>(t, jsonOption)) + .Where(t => t.Count > 0) + .Select(t => t[0].ToWordWithPartOfSpeech()) + .ToList(); + return jsonOutput; + } + + private static string ProcessWords(string inputFilePath, string outputFilePath) + { + var startInfo = new ProcessStartInfo + { + FileName = Path.Combine(AppContext.BaseDirectory, "mystem.exe"), + Arguments = $"-i --format json \"{inputFilePath}\" \"{outputFilePath}\"", + UseShellExecute = false, + CreateNoWindow = true + }; + + string output; + try + { + using var myStemProcess = new Process { StartInfo = startInfo }; + myStemProcess.Start(); + myStemProcess.WaitForExit(); + output = File.ReadAllText(outputFilePath); + } + finally + { + File.Delete(inputFilePath); + File.Delete(outputFilePath); + } + + return output; + } + + private static void PrepareInputFile(string path, IEnumerable words) + { + File.WriteAllText(path, string.Join('\n', words)); + } + + private static void PrepareOutputFile(string path) + { + File.Create(path).Close(); + } +} diff --git a/Homework/TagCloud2/TagCloudLibrary/Preprocessor/WordPreprocessorOptions.cs b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/WordPreprocessorOptions.cs new file mode 100644 index 000000000..b1d11cc6c --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/WordPreprocessorOptions.cs @@ -0,0 +1,3 @@ +namespace TagCloudLibrary.Preprocessor; + +public record class WordPreprocessorOptions(HashSet SelectedPartsOfSpeech); diff --git a/Homework/TagCloud2/TagCloudLibrary/Preprocessor/WordWithPartOfSpeech.cs b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/WordWithPartOfSpeech.cs new file mode 100644 index 000000000..5f0fdd1cc --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Preprocessor/WordWithPartOfSpeech.cs @@ -0,0 +1,3 @@ +namespace TagCloudLibrary.Preprocessor; + +public record class WordWithPartOfSpeech(string Word, PartOfSpeech PartOfSpeech); diff --git a/Homework/TagCloud2/TagCloudLibrary/ResultPattern/None.cs b/Homework/TagCloud2/TagCloudLibrary/ResultPattern/None.cs new file mode 100644 index 000000000..8835aa2e1 --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/ResultPattern/None.cs @@ -0,0 +1,8 @@ +namespace TagCloudLibrary.ResultPattern; + +public class None +{ + private None() + { + } +} diff --git a/Homework/TagCloud2/TagCloudLibrary/ResultPattern/Result.cs b/Homework/TagCloud2/TagCloudLibrary/ResultPattern/Result.cs new file mode 100644 index 000000000..e17790cb3 --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/ResultPattern/Result.cs @@ -0,0 +1,101 @@ +namespace TagCloudLibrary.ResultPattern; + +public static class Result +{ + public static Result AsResult(this T value) + { + return Ok(value); + } + + public static Result Ok(T value) + { + return new Result(null, value); + } + public static Result Ok() + { + return Ok(null); + } + + public static Result Fail(string e) + { + return new Result(e); + } + + public static Result Of(Func f, string error = null) + { + try + { + return Ok(f()); + } + catch (Exception e) + { + return Fail(error ?? e.Message); + } + } + + public static Result OfAction(Action f, string error = null) + { + try + { + f(); + return Ok(); + } + catch (Exception e) + { + return Fail(error ?? e.Message); + } + } + + public static Result Then( + this Result input, + Func continuation) + { + return input.Then(inp => Of(() => continuation(inp))); + } + + public static Result Then( + this Result input, + Action continuation) + { + return input.Then(inp => OfAction(() => continuation(inp))); + } + + public static Result Then( + this Result input, + Action continuation) + { + return input.Then(inp => OfAction(() => continuation(inp))); + } + + public static Result Then( + this Result input, + Func> continuation) + { + return input.IsSuccess + ? continuation(input.Value) + : Fail(input.Error); + } + + public static Result OnFail( + this Result input, + Action handleError) + { + if (!input.IsSuccess) handleError(input.Error); + return input; + } + + public static Result ReplaceError( + this Result input, + Func replaceError) + { + if (input.IsSuccess) return input; + return Fail(replaceError(input.Error)); + } + + public static Result RefineError( + this Result input, + string errorMessage) + { + return input.ReplaceError(err => errorMessage + ". " + err); + } +} diff --git a/Homework/TagCloud2/TagCloudLibrary/ResultPattern/ResultT.cs b/Homework/TagCloud2/TagCloudLibrary/ResultPattern/ResultT.cs new file mode 100644 index 000000000..d5567f3cd --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/ResultPattern/ResultT.cs @@ -0,0 +1,24 @@ +namespace TagCloudLibrary.ResultPattern; + +public struct Result +{ + public Result(string error, T value = default) + { + Error = error; + Value = value; + } + + public static implicit operator Result(T v) + { + return Result.Ok(v); + } + + public string Error { get; } + internal T Value { get; } + public T GetValueOrThrow() + { + if (IsSuccess) return Value; + throw new InvalidOperationException($"No value. Only Error {Error}"); + } + public bool IsSuccess => Error == null; +} diff --git a/Homework/TagCloud2/TagCloudLibrary/TagCloud.cs b/Homework/TagCloud2/TagCloudLibrary/TagCloud.cs new file mode 100644 index 000000000..1f0d72438 --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/TagCloud.cs @@ -0,0 +1,43 @@ +using SkiaSharp; +using TagCloudLibrary.Layouter; +using TagCloudLibrary.Preprocessor; +using TagCloudLibrary.ResultPattern; +using TagCloudLibrary.Visualizer; + +namespace TagCloudLibrary; + +public class TagCloud(IWordPreprocessor preprocessor, ICloudLayouter layouter, ITagCloudVisualizer visualizer, TagCloudOptions options) +{ + private readonly List cloud = []; + + public Result BuildTagTree(IEnumerable words) + { + return preprocessor + .Process(words) + .Then(r => r + .GroupBy(t => t, (key, elements) => (Word: key, Count: elements.Count())) + .ToList()) + .Then(CreateTagsFromWords); + } + + private Result CreateTagsFromWords(List<(string Word, int Count)> preparedWords) + { + var fontCoeff = (float)(options.MaxFontSize - options.MinFontSize) / (preparedWords.Max(t => t.Count) - 1f); + + foreach (var group in preparedWords) + { + var fontSize = options.MinFontSize + fontCoeff * (group.Count - 1); + var font = new SKFont(options.Typeface ?? SKTypeface.Default, fontSize); + font.MeasureText(group.Word, out var textMeasurement); + var rectangle = layouter.PutNextRectangle(textMeasurement.Size + new SKSize(options.TextGap * 2, options.TextGap * 2)); + if (rectangle.IsSuccess) + cloud.Add(new PlacedText(group.Word, font, rectangle.Value)); + else + return Result.Fail(rectangle.Error); + } + + return Result.Ok(); + } + + public Result CreateImage() => visualizer.DrawTagCloud(cloud); +} diff --git a/Homework/TagCloud2/TagCloudLibrary/TagCloudLibrary.csproj b/Homework/TagCloud2/TagCloudLibrary/TagCloudLibrary.csproj new file mode 100644 index 000000000..eb9f3b87a --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/TagCloudLibrary.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + PreserveNewest + + + + diff --git a/Homework/TagCloud2/TagCloudLibrary/TagCloudOptions.cs b/Homework/TagCloud2/TagCloudLibrary/TagCloudOptions.cs new file mode 100644 index 000000000..ed935f9af --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/TagCloudOptions.cs @@ -0,0 +1,5 @@ +using SkiaSharp; + +namespace TagCloudLibrary; + +public record class TagCloudOptions(SKTypeface? Typeface, float MinFontSize, float MaxFontSize, float TextGap); diff --git a/Homework/TagCloud2/TagCloudLibrary/Visualizer/ITagCloudVisualizer.cs b/Homework/TagCloud2/TagCloudLibrary/Visualizer/ITagCloudVisualizer.cs new file mode 100644 index 000000000..5d98f46c5 --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Visualizer/ITagCloudVisualizer.cs @@ -0,0 +1,10 @@ +using SkiaSharp; +using TagCloudLibrary.Layouter; +using TagCloudLibrary.ResultPattern; + +namespace TagCloudLibrary.Visualizer; + +public interface ITagCloudVisualizer +{ + Result DrawTagCloud(IEnumerable tagCloud); +} diff --git a/Homework/TagCloud2/TagCloudLibrary/Visualizer/TagCloudVisualizer.cs b/Homework/TagCloud2/TagCloudLibrary/Visualizer/TagCloudVisualizer.cs new file mode 100644 index 000000000..a7ea58493 --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Visualizer/TagCloudVisualizer.cs @@ -0,0 +1,81 @@ +using SkiaSharp; +using TagCloudLibrary.Layouter; +using TagCloudLibrary.ResultPattern; + +namespace TagCloudLibrary.Visualizer; + +public class TagCloudVisualizer(TagCloudVisualizerOptions options) : ITagCloudVisualizer +{ + public Result DrawTagCloud(IEnumerable cloud) + { + var tagCloud = cloud.ToList(); + var boundingRectangle = GetBoundingRectangle(tagCloud.Select(t => t.Place)); + + if (boundingRectangle.Width + options.PictureBorderSize * 2 > options.Width + || boundingRectangle.Height + options.PictureBorderSize * 2 > options.Height) + return Result.Fail("The tag cloud extends beyond the selected image dimensions."); + + var pictureOrigin = boundingRectangle.Location - new SKSize(options.PictureBorderSize, options.PictureBorderSize); + + var imageInfo = new SKImageInfo( + width: options.Width ?? (int)Math.Round(boundingRectangle.Width + 2 * options.PictureBorderSize), + height: options.Height ?? (int)Math.Round(boundingRectangle.Height + 2 * options.PictureBorderSize), + colorType: SKColorType.Rgba8888, + alphaType: SKAlphaType.Opaque); + + using var surface = SKSurface.Create(imageInfo); + var canvas = surface.Canvas; + + canvas.Clear(options.BackgroundColor); + DrawWords(tagCloud, pictureOrigin, canvas); + + return surface.Snapshot(); + } + + private static SKRect GetBoundingRectangle(IEnumerable rects) + { + var result = default(SKRect?); + + foreach (var rectangle in rects) + result = SKRect.Union(result ?? rectangle, rectangle); + + return result.Value; + } + + private static SKColor GetRandomColor() + { + var rand = new Random(); + + var color = new byte[3]; + rand.NextBytes(color); + + var lineColor = new SKColor( + red: color[0], + green: color[1], + blue: color[2]); + + return lineColor; + } + + private void DrawWords(List tagCloud, SKPoint pictureOrigin, SKCanvas canvas) + { + foreach (var word in tagCloud) + { + var paint = new SKPaint + { + Color = options.ForegroundColor ?? GetRandomColor(), + IsAntialias = true, + Style = SKPaintStyle.Fill, + }; + + word.Font.MeasureText(word.Text, out var textMeasurement, paint); + + canvas.DrawText( + word.Text, + word.Place.Location.X + word.Place.Width / 2 - textMeasurement.Width / 2 - pictureOrigin.X, + word.Place.Location.Y + word.Place.Height / 2 + textMeasurement.Height / 2 - pictureOrigin.Y, + word.Font, + paint); + } + } +} diff --git a/Homework/TagCloud2/TagCloudLibrary/Visualizer/TagCloudVisualizerOptions.cs b/Homework/TagCloud2/TagCloudLibrary/Visualizer/TagCloudVisualizerOptions.cs new file mode 100644 index 000000000..2b250be6a --- /dev/null +++ b/Homework/TagCloud2/TagCloudLibrary/Visualizer/TagCloudVisualizerOptions.cs @@ -0,0 +1,5 @@ +using SkiaSharp; + +namespace TagCloudLibrary.Visualizer; + +public record class TagCloudVisualizerOptions(int? Width, int? Height, int PictureBorderSize, SKColor BackgroundColor, SKColor? ForegroundColor); diff --git a/Homework/TagCloud2/TagCloudLibrary/mystem.exe b/Homework/TagCloud2/TagCloudLibrary/mystem.exe new file mode 100644 index 000000000..efefe328f Binary files /dev/null and b/Homework/TagCloud2/TagCloudLibrary/mystem.exe differ diff --git a/Homework/TagCloud2/TagCloudTests/ResultPatternTests.cs b/Homework/TagCloud2/TagCloudTests/ResultPatternTests.cs new file mode 100644 index 000000000..c799a6eeb --- /dev/null +++ b/Homework/TagCloud2/TagCloudTests/ResultPatternTests.cs @@ -0,0 +1,98 @@ +using FakeItEasy; +using FluentAssertions; +using SkiaSharp; +using TagCloudLibrary; +using TagCloudLibrary.Layouter; +using TagCloudLibrary.Preprocessor; +using TagCloudLibrary.ResultPattern; +using TagCloudLibrary.Visualizer; + +namespace TagCloudTests; + +[TestFixture] +internal class ResultPatternTests +{ + [TestCase(0, 1)] + [TestCase(1, 0)] + [TestCase(0, 0)] + public void CloudLayouter_ShouldFail_WhenSizeHasNonPositiveComponent(int width, int height) + { + var layouter = new CircularCloudLayouter(new()); + var size = new SKSize(width, height); + + var result = layouter.PutNextRectangle(size); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be("Recatngle size should be greater then zero."); + } + + [TestCase(119, 119)] + [TestCase(0, 0)] + [TestCase(119, 121)] + [TestCase(121, 119)] + public void CloudVisualizer_ShouldFail_WhenTagCloudWithBorderDoNotFitOnImage(int width, int height) + { + var options = new TagCloudVisualizerOptions(width, height, 20, SKColor.Parse("#000000"), null); + var visualizer = new TagCloudVisualizer(options); + var placedText = new PlacedText(null, new SKFont(), new SKRect(0, 0, 100, 100)); + + var result = visualizer.DrawTagCloud([placedText]); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be("The tag cloud extends beyond the selected image dimensions."); + } + + [Test] + public void TagCloud_Should_PopulatePreprocessorFails() + { + var preprocessor = A.Fake(); + var words = new[] { "мама" }; + A.CallTo(() => preprocessor.Process(words)).Returns(Result.Fail>("")); + + var layouter = A.Fake(); + var visualizer = A.Fake(); + var options = new TagCloudOptions(SKTypeface.Default, 20, 100, 12); + var tagCloud = new TagCloud(preprocessor, layouter, visualizer, options); + + var result = tagCloud.BuildTagTree(words); + + result.IsSuccess.Should().BeFalse(); + } + + [Test] + public void TagCloud_Should_PopulateLayouterFails() + { + var preprocessor = A.Fake(); + var words = new[] { "мама" }; + A.CallTo(() => preprocessor.Process(words)).Returns(Result.Ok>(["мама"])); + + var layouter = A.Fake(); + var size = new SKSize(20, 20); + A.CallTo(() => layouter.PutNextRectangle(size)).WithAnyArguments().Returns(Result.Fail("")); + + var visualizer = A.Fake(); + var options = new TagCloudOptions(SKTypeface.Default, 20, 100, 12); + var tagCloud = new TagCloud(preprocessor, layouter, visualizer, options); + + var result = tagCloud.BuildTagTree(words); + + result.IsSuccess.Should().BeFalse(); + } + + [Test] + public void TagCloud_Should_PopulateVisualizerFails() + { + var preprocessor = A.Fake(); + var layouter = A.Fake(); + + var visualizer = A.Fake(); + A.CallTo(() => visualizer.DrawTagCloud(null!)).WithAnyArguments().Returns(Result.Fail("")); + + var options = new TagCloudOptions(SKTypeface.Default, 20, 100, 12); + var tagCloud = new TagCloud(preprocessor, layouter, visualizer, options); + + var result = tagCloud.CreateImage(); + + result.IsSuccess.Should().BeFalse(); + } +} diff --git a/Homework/TagCloud2/TagCloudTests/TagCloudTests.csproj b/Homework/TagCloud2/TagCloudTests/TagCloudTests.csproj new file mode 100644 index 000000000..5f5399962 --- /dev/null +++ b/Homework/TagCloud2/TagCloudTests/TagCloudTests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + diff --git a/Homework/TagCloud2/TagCloudTests/WordPreprocessorTests.cs b/Homework/TagCloud2/TagCloudTests/WordPreprocessorTests.cs new file mode 100644 index 000000000..4f5a645c3 --- /dev/null +++ b/Homework/TagCloud2/TagCloudTests/WordPreprocessorTests.cs @@ -0,0 +1,29 @@ +using FluentAssertions; +using TagCloudLibrary.Preprocessor; + +namespace TagCloudTests; + +public class WordPreprocessorTests +{ + [Test] + public void Should_ReturnSameWordCountWithoutFiltering() + { + var preprocessor = new WordPreprocessor(new(Enum.GetValues().ToHashSet())); + var words = new List { "", "", "" }; + + var output = preprocessor.Process(words).GetValueOrThrow(); + + output.Should().HaveSameCount(words); + } + + [Test] + public void Should_FilterWords() + { + var preprocessor = new WordPreprocessor(new([])); + var words = new List { "", "", "" }; + + var output = preprocessor.Process(words).GetValueOrThrow(); + + output.Should().HaveCount(0); + } +} diff --git a/Homework/TagCloud2/sample.png b/Homework/TagCloud2/sample.png new file mode 100644 index 000000000..918e684d6 Binary files /dev/null and b/Homework/TagCloud2/sample.png differ diff --git a/Homework/TagCloud2/words.txt b/Homework/TagCloud2/words.txt new file mode 100644 index 000000000..0ae137785 --- /dev/null +++ b/Homework/TagCloud2/words.txt @@ -0,0 +1,198 @@ +Душа +моя +озарена +неземной +радостью +как +эти +чудесные +весенние +утра +которыми +я +наслаждаюсь +от +всего +сердца +Я +совсем +один +и +блаженствую +в +здешнем +краю +словно +созданном +для +таких +как +я +Я +так +счастлив +мой +друг +так +упоен +ощущением +покоя +что +искусство +мое +страдает +от +этого +Ни +одного +штриха +не +мог +бы +я +сделать +а +никогда +не +был +таким +большим +художником +как +в +эти +минуты +Когда +от +милой +моей +долины +поднимается +пар +и +полдневное +солнце +стоит +над +непроницаемой +чащей +темного +леса +и +лишь +редкий +луч +проскальзывает +в +его +святая +святых +а +я +лежу +в +высокой +траве +у +быстрого +ручья +и +прильнув +к +земле +вижу +тысячи +всевозможных +былинок +и +чувствую +как +близок +моему +сердцу +крошечный +мирок +что +снует +между +стебельками +наблюдаю +эти +неисчислимые +непостижимые +разновидности +червяков +и +мошек +и +чувствую +близость +всемогущего +создавшего +нас +по +своему +подобию +веяние +вселюбящего +судившего +нам +парить +в +вечном +блаженстве +когда +взор +мой +туманится +и +все +вокруг +меня +и +небо +надо +мной +запечатлены +в +моей +душе +точно +образ +возлюбленной +тогда +дорогой +друг +меня +часто +томит +мысль +Ах +Как +бы +выразить +как +бы +вдохнуть +в +рисунок +то +что +так +полно +так +трепетно +живет +во +мне +запечатлеть +отражение +моей +души +как +душа +моя +отражение +предвечного +бога +Друг