-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Рушкова Ольга #29
base: master
Are you sure you want to change the base?
Рушкова Ольга #29
Changes from 24 commits
f04a193
62cdd6d
c6efa35
de85c55
95079dd
4724e74
9ae0f99
e2813f8
18df45e
a91ac06
fd67c9a
0aab977
e1dcb01
eb79cd6
c3e038c
bae5b17
088e0be
06610e8
ae9ed46
f31d772
bb62969
3de46fb
d1a91a1
7ad74fd
9733f4f
942ae93
3b31e3a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using TagCloudDI.CloudVisualize; | ||
using TagCloudDI.Data; | ||
|
||
namespace TagCloudDI | ||
{ | ||
public class CloudCreator | ||
{ | ||
private DataProvider dataProvider; | ||
private CloudVisualizer cloudVisualizer; | ||
public CloudCreator(DataProvider dataProvider, CloudVisualizer visualizer) | ||
{ | ||
this.dataProvider = dataProvider; | ||
cloudVisualizer = visualizer; | ||
} | ||
|
||
public string CreateTagCloud(string pathToFileWithWords) | ||
{ | ||
var words = dataProvider.GetPreprocessedWords(pathToFileWithWords); | ||
return cloudVisualizer.CreateImage(words); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Rectangle> 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, | ||
}; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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))); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Rectangle> storage; | ||
private readonly CloudCompressor compressor; | ||
private readonly CirclePositionDistributor distributor; | ||
|
||
public CircularCloudLayouter(): this(new Point(0,0), []) | ||
{ } | ||
|
||
private CircularCloudLayouter(Point center, List<Rectangle> storage) | ||
{ | ||
this.storage = storage; | ||
distributor = new(center); | ||
compressor = new(center, storage); | ||
} | ||
|
||
public static CircularCloudLayouter CreateLayouterWithStartRectangles(Point center, List<Rectangle> 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(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Rectangle> cloud; | ||
private int minDistanceForMoving = 1; | ||
|
||
public CloudCompressor(Point compressTo, List<Rectangle> 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<Direction> GetDirectionsForMovingForCompress(Rectangle forMoving) | ||
{ | ||
var directions = new List<Direction>(); | ||
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; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
namespace TagsCloudVisualization.CloudLayouter | ||
{ | ||
public enum Direction | ||
{ | ||
Left, | ||
Right, | ||
Top, | ||
Bottom | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
using System.Drawing; | ||
|
||
namespace TagCloudDI.CloudVisualize | ||
{ | ||
public class CloudVisualizer | ||
{ | ||
private VisualizeSettings settings; | ||
private readonly ImageSaver imageSaver; | ||
private readonly ICloudLayouter layouter; | ||
private readonly Size defaultSizeForImage = new Size(500, 500); | ||
private readonly IWordColorDistributor distributor; | ||
|
||
|
||
public CloudVisualizer(VisualizeSettings settings, ICloudLayouter cloudLayouter, IWordColorDistributor distributor) | ||
{ | ||
this.settings = settings; | ||
imageSaver = new ImageSaver(); | ||
layouter = cloudLayouter; | ||
this.distributor = distributor; | ||
} | ||
|
||
public string CreateImage((string Word, double Frequency)[] source, string? filePath = null) | ||
{ | ||
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); | ||
} | ||
imageSaver.SaveImage(image); | ||
var resizedImage = new Bitmap(image, settings.ImageSize == Size.Empty ? tmpImageSize : settings.ImageSize); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Давай попытаемся избавиться от такой логики settings.ImageSize == Size.Empty и попытаемся сделать пользовательские настройки такими, чтобы если значение не задано или задано некорректное, то использовали какие-то дефолтные настройки. Еще тут какая-то странная логика с ресайзами, не оч понял There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. По странной логике с ресайзами: я динамически рассчитываю размер картинки, чтобы облако в эту картинку точно влезло и двигаю облако внутрь этой картинки (чтобы не было ситуации, когда часть облака находится где-то за изображением). А потом уже скейлю получившуюся картинку по пользовательским настройкам Можно сделать так: дефолтно рассчитывать динамически, а при наличии корректных пользовательских настроек не скейлить вообще, но тогда как будто бы всё равно появится логика с if |
||
return imageSaver.SaveImage(resizedImage, filePath); | ||
} | ||
|
||
private IEnumerable<WordParameters> 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<Rectangle, int> selectorForMin, | ||
Func<Rectangle, int> 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(Math.Max(width, height), defaultSizeForImage.Width); | ||
return new Size(sizeForRectangles, sizeForRectangles); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Не интуитивно понятно почему CreateImage возвращает string, может будем возвращать картинку и далее её уже как-то сохранять?