Generating an index for your automated catalog

In our ongoing series on custom development for PageBuilder, we illustrate how the PageBuilder 4 API can be used to customize the default behavior, to extend the available features, or to implement entirely new features on top of the existing functionality.

Today's topic discusses the possibility to dynamically add an index to your automated catalogs. In order to get a clear idea of what we want to achieve, here is a screenshot showing the first page of our auto-generated index:

There are two basic steps involved in generating an index: building the index and outputting it to the document. Our index is represented in memory using a Dictionary which maps keywords to sets of page numbers. Building the index is accomplished through a custom RecordPaginateAction: whenever a product is added to the catalog, the corresponding keywords and page numbers are added to the index. Outputting the index is achieved with a custom DocumentAction which is executed after all products have been added to the catalog. This is what the code for our custom CatalogBuilder looks like:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System.Collections.Generic;
using System.Linq;
using Adam.Core;
using Adam.PageBuilder.Core;
using Adam.PageBuilder.Core.Build;

using Index = System.Collections.Generic.Dictionary<string, System.Collections.Generic.HashSet<int>>;

namespace AutoIndexEngine
{
 class AutoIndexCatalogBuilder : CatalogBuilder
 {
  private readonly Index _index = new Index();

  public AutoIndexCatalogBuilder(Application application) : base(application) 
  {
   RecordPaginateAction = new BuildIndexAction(application, _index);
  }

  protected override IEnumerable<DocumentAction> Actions
  {
   get
   {
    return base.Actions.Concat(new List<DocumentAction> { new OutputIndexAction(App, _index) });
   }
  }
 }
}

The implementation of the BuildIndexAction that builds our index is also pretty straightforward. The code in the OnPlaced method is executed whenever a new product has been placed in the catalog. Both the product record and the page number can be retrieved through the PlaceTarget argument that is passed along to this method. We assume each record has a Keywords field containing a comma-separated list of keywords for the product:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System.Collections.Generic;
using System.Linq;
using Adam.Core;
using Adam.Core.Fields;
using Adam.Core.Records;
using Adam.PageBuilder.Core.Paginate;

using Index = System.Collections.Generic.Dictionary<string, System.Collections.Generic.HashSet<int>>;

namespace AutoIndexEngine
{
 class BuildIndexAction : RecordPaginateAction
 {
  private readonly Index _index;

  public BuildIndexAction(Application application, Index index) : base(application)
  {
   _index = index;
  }

  protected override void OnPlaced(PlaceTarget placeTarget)
  {
   RecordPlaceTarget productTarget = (RecordPlaceTarget)placeTarget;

   Record record = new Record(App);
   record.Load(productTarget.ResolvedRecordId.Value);

   TextField field = (TextField)record.Fields["Keywords"].MyLanguage;
   string keywords = field.Value;

   int pageNumber = placeTarget.Elements.First().Pages.Single().PageNumber;

   foreach (string keyword in keywords.Split(','))
   {
    if (!_index.ContainsKey(keyword))
    {
     _index[keyword] = new HashSet<int>();
    }
    _index[keyword].Add(pageNumber);
   }
  }
 }
}

Our OutputIndexAction implementation makes extensive use of the DocMaker 3.2 API. We start by adding the required additional pages and text boxes to the document. All text boxes are linked together (sharing the same story) so all content automatically flows from one text box to another. Most of the work here consists of calculating the bounds for each box, depending on the desired number of columns and space between the columns.

Once all page boxes have been added, we process our index structure and add content to the index story. The keywords are sorted alphabetically and grouped by first character. Then we start adding the paragraphs and formatted runs containing the first characters, keywords and aggregated page numbers:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Adam.Core;
using Adam.DocMaker.Core;
using Adam.DocMaker.Core.Geometry;
using Adam.PageBuilder.Core;

using Index = System.Collections.Generic.Dictionary<string, System.Collections.Generic.HashSet<int>>;
using IndexEntry = System.Collections.Generic.KeyValuePair<string, System.Collections.Generic.HashSet<int>>;

namespace AutoIndexEngine
{
 class OutputIndexAction : DocumentAction
 {
  private const int _pageCount = 3;
  private const int _columnCount = 4;
  private const int _boxPadding = 10;

  private const string _defaultFont = "Calibri";
  private static readonly FontColor _defaultColor = new FontColor(37, 64, 143);

  private const string _titleFont = "Harlow Solid Italic";
  private static readonly FontColor _titleColor = new FontColor(65, 173, 73);

  private const string _title = "Catalog Index";

  private readonly Index _index;

  public OutputIndexAction(Application application, Index index) : base(application)
  {
   _index = index;
  }

  protected override void OnExecute(Document document)
  {
   Story indexStory = AddPageBoxes(document);

   IEnumerable<IGrouping<char, IndexEntry>> indexEntryGroups 
    = _index.OrderBy(pair => pair.Key).GroupBy(pair => pair.Key.ToUpperInvariant().First());

   foreach (IGrouping<char, IndexEntry> indexEntryGroup in indexEntryGroups)
   {
    char firstCharacter = indexEntryGroup.Key;

    indexStory.AddParagraph().AddRun(firstCharacter.ToString(), _defaultFont, _defaultColor, 36, true);

    foreach (IndexEntry pair in indexEntryGroup)
    {
     Paragraph paragraph = indexStory.AddParagraph();

     string pageNumbers = pair.Value.Aggregate(
      new StringBuilder(),
      (builder, pageNumber) => builder.Append(", " + pageNumber),
      builder => builder.ToString().Substring(2));

     paragraph.AddRun(string.Format("{0}: ", pair.Key), _defaultFont, _defaultColor, 12, true);
     paragraph.AddRun(pageNumbers, _defaultFont, _defaultColor, 12, false);
    }
   }
  }

  private static Story AddPageBoxes(Document document)
  {
   Story story = null;
   for (int pageIndex = 0; pageIndex < _pageCount; ++pageIndex)
   {
    Page page = document.AddPage();
    page.MasterSpread = null;
    RectangleD bounds = page.ContentBounds;

    if (story == null)
    {
     // When adding the first box, we also add the title box and subtract the needed area.
     const double boxHeight = 40;
     page
      .AddBox(new RectangleD(bounds.X, bounds.Y, bounds.Width, boxHeight), null)
      .AddParagraph()
      .AddRun(_title, _titleFont, _titleColor, 24, true);

     const double offset = boxHeight + _boxPadding;
     bounds = new RectangleD(bounds.X, bounds.Y + offset, bounds.Width, bounds.Height - offset);
    }

    double textBoxWidth = (bounds.Width - (_columnCount - 1) * _boxPadding) / _columnCount;

    for (int columnIndex = 0; columnIndex < _columnCount; ++columnIndex)
    {
     double left = bounds.X + columnIndex * (textBoxWidth + _boxPadding);
     RectangleD boxBounds = new RectangleD(left, bounds.Y, textBoxWidth, bounds.Height);
     story = page.AddBox(boxBounds, story);
    }
   }
   return story;
  }
 }
}

For the sake of completeness, here is the implementation of the few extension methods that are conveniently used in the above code:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using Adam.DocMaker.Core;
using Adam.DocMaker.Core.Geometry;

namespace AutoIndexEngine
{
 static class IndexExtensions
 {
  public static Story AddBox(this Page page, RectangleD boxBounds, Story indexStory)
  {
   if (indexStory != null)
   {
    TextElement box = page.OwnerDocument.CreateTextElement(boxBounds, indexStory);
    page.OwnerSpread.Elements.Add(box);
    return indexStory;
   }
   else
   {
    TextElement box = page.OwnerDocument.CreateTextElement(boxBounds);
    page.OwnerSpread.Elements.Add(box);
    return box.Story;
   }
  }

  public static Paragraph AddParagraph(this Story story)
  {
   Paragraph paragraph = story.OwnerDocument.CreateParagraph();
   story.Paragraphs.Add(paragraph);
   return paragraph;
  }

  public static Run AddRun(this Paragraph paragraph, string value, string font, FontColor color, int size, bool bold)
  {
   Run run = paragraph.OwnerDocument.CreateRun();
   paragraph.Items.Add(run);

   run.Value = value;
   run.Formatting.FontSize = size;
   run.Formatting.Bold = bold;
   run.Formatting.FontFamily = font;
   run.Formatting.FontColor = color;

   return run;
  }
 }
}

All classes should be compiled together in a class library that is registered either in the GAC or in the ADAM database. You can then use the AutoIndexCatalogBuilder type to build your catalogs from within PageBuilder Studio, as explained in previous blog posts and in the PageBuilder 4 Developer Guide.

Of course all code in this blog post is meant for illustrative purposes only and should not be used directly in production. Note that all font properties and layout parameters are hard-coded in constant values. A more flexible approach would use the Tag property to pass these parameters from the PageBuilder Studio UI to the CatalogBuilder class.

Comments

Leave a comment
You must be logged in to post comments.
Sign in now
 
 
Technical
Business
rss feed