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); }
/// <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; }