Tuesday, September 17, 2013

Timeline component features lazy loading

The first release candidate 1.0.0.RC1 of the PrimeFaces Extensions brought one new of two planned features for the Timeline component. This component supports lazy loading of events during moving / zooming in the timeline. This makes sense when event's loading is time-consuming. Unlike Schedule component in the PrimeFaces it didn't introduce a new LazyTimelineModel. The model class stays the same - TimelineModel. Events are loaded lazy when p:ajax with event="lazyload" is attached to the pe:timeline tag. This new AJAX behaviour has several advantages in comparison to the approach with the "lazy model". It is consistent with other AJAX events such as add, delete, rangechanged, etc. You can disable / enable the "lazyload" AJAX behavior at runtime and execute a custom JS script on start / complete. Example:
<p:ajax event="lazyload" disabled="#{lazyTimelineController.disabled}"
            listener="#{lazyTimelineController.onLazyLoad}"  
            onstart="PF('dialogWidget').show()"   
            oncomplete="PF('dialogWidget').hide()"/>
And of course you can control exactly what should be executed and updated (process / update attributes). To understand how this new feature works (before posting a lot of code :-)) I sketched one diagram. Please read from top to down.


On intial page load, only events in the visible time range should be lazy loaded. After moving to the right or left direction, area on the right or left side is coming into the view. Only events in this new displayed time range should be loaded. Time range with already loaded events is cached internally (read line in the diagram). When you zoom out now, you will probably have two time ranges the events should be loaded for. This is demonstrated in the 4. use case. When you zoom in, no AJAX event for lazy loading is sent. It is very smart! As you can see, the "lazyload" listener is not invoked again when the visible time range (incl. some hidden ranges defined by preloadFactor) already has lazy loaded events.

What is preloadFactor? The preloadFactor attribute of the pe:timeline is a positive float value or 0 (default). When the lazy loading feature is active, the calculated time range for preloading will be multiplicated by the preload factor. The result of this multiplication specifies the additional time range which will be considered for the preloading during moving / zooming too. For example, if the calculated time range for preloading is 5 days and the preload factor is 0.2, the result is 5 * 0.2 = 1 day. That means, 1 day backwards and / or 1 day onwards will be added to the original calculated time range. The event's area to be preloaded is wider then. This helps to avoid frequently, time-consuming fetching of events. Note: the preload factor in the diagram above was 0.

Let me show the code now. The code is taken from this live example of the deployed showcase.
<div id="loadingText" style="font-weight:bold; margin:-5px 0 5px 0; visibility:hidden;">Loading ...</div>  
              
<pe:timeline id="timeline" value="#{lazyTimelineController.model}"  
             preloadFactor="#{lazyTimelineController.preloadFactor}"  
             zoomMax="#{lazyTimelineController.zoomMax}"  
             minHeight="170" showNavigation="true">  
    <p:ajax event="lazyload" update="@none" listener="#{lazyTimelineController.onLazyLoad}"  
            onstart="$('#loadingText').css('visibility', 'visible')"   
            oncomplete="$('#loadingText').css('visibility', 'hidden')"/>  
</pe:timeline>
You see a hidden "Loading ..." text and the timeline tag. The text is shown when a "lazyload" AJAX request is sent and gets hidden when the response is back. The bean class LazyTimelineController looks as follows
@ManagedBean
@ViewScoped
public class LazyTimelineController implements Serializable {

   private TimelineModel model;

   private float preloadFactor = 0;
   private long zoomMax;

   @PostConstruct
   protected void initialize() {
      // create empty model
      model = new TimelineModel();

      // about five months in milliseconds for zoomMax
      // this can help to avoid a long loading of events when zooming out to wide time ranges
      zoomMax = 1000L * 60 * 60 * 24 * 31 * 5;
   }

   public TimelineModel getModel() {
      return model;
   }

   public void onLazyLoad(TimelineLazyLoadEvent e) {
      try {
          // simulate time-consuming loading before adding new events
          Thread.sleep((long) (1000 * Math.random() + 100));
      } catch (Exception ex) {
          // ignore
      }

      TimelineUpdater timelineUpdater = TimelineUpdater.getCurrentInstance(":mainForm:timeline");

      Date startDate = e.getStartDateFirst(); // alias getStartDate() can be used too
      Date endDate = e.getEndDateFirst(); // alias getEndDate() can be used too

      // fetch events for the first time range
      generateRandomEvents(startDate, endDate, timelineUpdater);

      if (e.hasTwoRanges()) {
          // zooming out ==> fetch events for the second time range
          generateRandomEvents(e.getStartDateSecond(), e.getEndDateSecond(), timelineUpdater);
      }
   }

   private void generateRandomEvents(Date startDate, Date endDate, TimelineUpdater timelineUpdater) {
      Calendar cal = Calendar.getInstance();
      Date curDate = startDate;
      Random rnd = new Random();

      while (curDate.before(endDate)) {
          // create events in the given time range
          if (rnd.nextBoolean()) {
              // event with only one date
              model.add(new TimelineEvent("Event " + RandomStringUtils.randomNumeric(5), curDate), timelineUpdater);
          } else {
              // event with start and end dates
              cal.setTimeInMillis(curDate.getTime());
              cal.add(Calendar.HOUR, 18);
              model.add(new TimelineEvent("Event " + RandomStringUtils.randomNumeric(5), curDate, cal.getTime()),
                        timelineUpdater);
          }

          cal.setTimeInMillis(curDate.getTime());
          cal.add(Calendar.HOUR, 24);

          curDate = cal.getTime();
      }
   }

   public void clearTimeline() {
      // clear Timeline, so that it can be loaded again with a new preload factor
      model.clear();
   }

   public void setPreloadFactor(float preloadFactor) {
      this.preloadFactor = preloadFactor;
   }

   public float getPreloadFactor() {
      return preloadFactor;
   }

   public long getZoomMax() {
      return zoomMax;
   }
}
The listener onLazyLoad gets an event object TimelineLazyLoadEvent. The TimelineLazyLoadEvent contains one or two time ranges the events should be loaded for. Two times ranges occur when you zoom out the timeline (as in the screenshot at the end of this post). If you know these time ranges (start / end time), you can fetch events from the database or whatever. Server-side added events can be automatically updated in the UI. This happens as usally by TimelineUpdater: model.add(new TimelineEvent(...), timelineUpdater).

I hope the code is self-explained :-).


5 comments:

  1. Great component Oleg! Congratulations!

    ReplyDelete
  2. Nice effort Mr. Oleg. You put your best in this article by explaining everything using examples and graphs. I'm glad that such hard working blogger exists in the Internet world. Keep sharing informative blog posts like this.

    Regards,
    Prasant

    ReplyDelete
  3. Hey Oleg,

    Won't be a good idea to implement the same concept for unloading?
    Correct me if I'm wrong, but in your outstanding example, the TimelineModel seems to keep growing. If the user start at 2000 and keep moving till 2013 you can have a lot of data loaded on the backing bean. So an unloadFactor would be cool. You then can adjust depending on you model an system load. What do you think?

    LA

    ReplyDelete
  4. Awesome timeline component.
    Are there any plans to support timeline printing? (Maybe with the component)
    Thank.

    ReplyDelete
    Replies
    1. Printing? Do you mean with p:printer? http://www.primefaces.org/showcase/ui/printer.jsf

      Delete

Note: Only a member of this blog may post a comment.