private ModTag HitTest(IEnumerable <string> tags, Graphics g, CellEventArgs e)
        {
            if (tags == null || e.SubItem == null)
            {
                return(null);
            }

            var bounds = e.SubItem.Bounds;
            var offset = new Point(bounds.X + TagRenderInfo.rX,
                                   bounds.Y + TagRenderInfo.rY);
            var tagList = AvailableTags.Where(t => tags.Select(s => s.ToLower()).Contains(t.Key)).Select(kvp => kvp.Value);

            foreach (var tag in tagList)
            {
                var tagSize    = g.MeasureString(tag.Label, e.SubItem.Font).ToSize();
                var renderInfo = new TagRenderInfo(offset, bounds, tagSize, Color.Black);

                if (renderInfo.HitBox.Contains(e.Location))
                {
                    return(tag);
                }

                offset.X += renderInfo.HitBox.Width + TagRenderInfo.rX;
                // stop drawing outside of the column bounds
                if (offset.X > bounds.Right)
                {
                    break;
                }
            }

            return(null);
        }
Example #2
0
        /// <summary>
        /// Arranges the tags to form the distinctive tag cloud layout.
        /// </summary>
        public void Arrange()
        {
            RectangleF totalBounds = RectangleF.Empty;
            Random     rnd         = IsDeterministic ? new Random(0) : new Random();

            _renderItems.Clear();
            CycleCount = 0;

            // sort tags by frequency (highest first)
            var orderedItems = Items.OrderByDescending(x => x.Frequency);

            int maxFrequency = orderedItems.Select(x => x.Frequency).DefaultIfEmpty(0).First();
            int minFrequency = orderedItems.Select(x => x.Frequency).DefaultIfEmpty(0).Last();

            foreach (TagItem tag in orderedItems)
            {
                RectangleF bestBounds = RectangleF.Empty;
                int        bestDist   = 0;

                // calculate font size to use
                float scale = (maxFrequency != minFrequency)
                                        ? ((float)(tag.Frequency - minFrequency) / (float)(maxFrequency - minFrequency))
                                        : 0;

                float fontSize = BASE_FONT_SIZE + (BASE_FONT_SIZE * (FontSizeGradient * scale));

                // measure text and calculate bounds
                SizeF sz;
                using (Font font = new Font(FontFamily, fontSize, FontStyle)) {
                    sz = TextRenderer.MeasureText(tag.Text, font, Size.Empty, TEXT_FORMAT_FLAGS);
                }

                SizeF      offset    = new SizeF(-(sz.Width / 2f), -(sz.Height / 2f));
                RectangleF tagBounds = new RectangleF(offset.ToPointF(), sz);

                // initialise rendering info with what we know so far
                TagRenderInfo info = new TagRenderInfo()
                {
                    Item = tag, FontSize = fontSize
                };

                // try a random subset of the angles between 0 and 360 degrees
                foreach (int angle in Enumerable.Range(0, 360).Shuffle(rnd).Take(90))
                {
                    int tagDist = 0;

                    while (true)
                    {
                        // measure outward from the origin
                        PointF p = PointF.Empty.GetRadialPoint(angle, tagDist);
                        tagBounds.Location = PointF.Add(p, offset);

                        // check whether tag would overlap (collide) with previously-placed tags
                        bool collision = false;
                        foreach (var other in _renderItems)
                        {
                            CycleCount++;

                            if (other.Bounds.IntersectsWith(tagBounds))
                            {
                                collision = true;
                                break;
                            }
                        }

                        // once there are no collisions this location becomes a candidate angle
                        if (!collision)
                        {
                            break;
                        }

                        // ...otherwise, increase distance from origin and try again
                        tagDist += 5;

                        // if we've already exceeded the most optimal distance, we can stop here
                        if ((bestDist != 0) && (tagDist > bestDist))
                        {
                            break;
                        }
                    }

                    // determine whether this candidate angle produces the most optimal solution
                    RectangleF tryBounds = RectangleF.Union(totalBounds, tagBounds);

                    float tryArea        = (tryBounds.Width * tryBounds.Height);
                    float bestArea       = (bestBounds.Width * bestBounds.Height);
                    float tryAspectDiff  = Math.Abs((tryBounds.Width / tryBounds.Height) - PreferredAspectRatio);
                    float bestAspectDiff = Math.Abs((bestBounds.Width / bestBounds.Height) - PreferredAspectRatio);

                    bool isBest = (bestBounds.IsEmpty || (tryArea < bestArea)) &&
                                  (bestBounds.IsEmpty || (tryAspectDiff < bestAspectDiff)) &&
                                  ((bestDist == 0) || (tagDist <= bestDist));

                    if (isBest)
                    {
                        // this becomes the new most optimal solution (until/if a better one is found)
                        bestBounds  = tryBounds;
                        bestDist    = tagDist;
                        info.Bounds = tagBounds;

                        // if the total bounds did not increase at all, skip over the remaining angles
                        if (bestBounds == totalBounds)
                        {
                            break;
                        }
                    }
                }

                // commit the current tag
                _renderItems.AddLast(info);
                totalBounds = bestBounds;
            }

            Bounds = totalBounds;
        }