EPG View

Role

An EPG component allows to integrate a classic Electronic Program Guide in a screen. It is designed with a main EpgView composed with a program grid (known as EpgGridView), a vertical channels list (named EpgChannelListView) and a timeline (known as EpgTimelineListView).

The component will manage the navigation between programs and channels. It is also responsible for knowing which programs must be loaded and shown in its program grid.

$EpgView
$EpgChannelListView
$EpgTimelineListView
$EpgGridView
$EpgTimelineListItemView
$EpgChannelListItemView
$EpgTileView

How to use it?

Define the component

defines
a view
defines...
Screen
Screen
extends
extends
MyEpgView
MyEpgView
has child
has child
has child
has child
has child
has child
EpgView
EpgView
EpgTimelineListView
EpgTimelineListView
has children
has children
EpgChannelListView
EpgChannelListView
has children
has children
EpgGridView
EpgGridView
list of
EpgChannelListItemView
list of...
list of
EpgTileView
list of...
Text is not SVG - cannot display

Create your EpgView

Even if EpgView could be used directly, you will certainly need to create your own implementation of EPG. Therefore, just like the example below, you just have to start to create a new class that inherits from EpgView.

You should also need to dimension and position those three components: gridView, channelListView, timelineView.

import $EpgView from "@EpgView";

export default $EpgView.declare("MyEpgView", {
  statics: /** @lends MyEpgView */ {
    REFERENCE_X_POSITION: 300, // x position of gridView and timelineListView
    REFERENCE_Y_POSITION: 50 // y position of gridView and channelListView
  },

  style: {
    width: 1600,
    height: 850,
    gridView: {
      width: ({parent}) => parent.width - parent.statics.REFERENCE_X_POSITION,
      height: ({parent}) => parent.height - parent.statics.REFERENCE_Y_POSITION
      // Never set gridView x nor y, it's managed by the component itself with REFERENCE_X/Y_POSITION
    },
    channelListView: {
      width: 200,
      height: ({parent}) => parent.height - parent.statics.REFERENCE_Y_POSITION,
      x: 0
      // Never set channelListView y, it's managed by the component itself with REFERENCE_Y_POSITION
    },
    timelineView: {
      width: ({parent}) => parent.width - parent.statics.REFERENCE_X_POSITION,
      height: 40,
      y: 0
      // Never set timelineView x, it's managed by the component itself with REFERENCE_X_POSITION
    }
  }
});

Configure time dimension properties

Within your EpgView definition, you must define and give a value to these properties:

export default $EpgView.declare("MyEpgView", {
  properties: /** @lends MyEpgView.prototype */ {
    ratio: null,
    frameDuration: null,
    nbFramesBefore: null,
    nbFramesAfter: null,
  }
});

The ratio property is the pixels/milliseconds ratio. If you want that one hour of programs to occupy 500 pixels in the view, you have to set it to 500 / (60 * 60 * 1000).

The next three properties are correlated with the notion of time β€œframes”.

A frame is a short period of time with a duration defined by the frameDuration parameter. The component will cut out the whole duration covered by the Epg (as set by configure(...)) into as many frames as it needs.

Info

The NEED_DATA events will only request periods of time that are a frame, or a concatenation of frames. Thus start and end parameters will always be aligned to frames.

The nbFramesBefore and nbFramesAfter properties will define how many frames must be renderered in the program grid. The total number of rendered frames is then nbFramesBefore + 1 + nbFramesAFter (1 is the current frame).

Warning

These values (nbFramesBefore, nbFramesAfter, frameDuration) will affect directly the number of programs drawn (hence the number of $Views and $Primitives in your screen).

Configure channels dimension properties

Within your EpgView, you must define and give a value to these properties:

export default $EpgView.declare("MyEpgView", {
  properties: /** @lends MyEpgView.prototype */ {
    nbVisibleChannels: null,
    nbMarginChannels: null,
    defaultFocusedChannelPosition: null,
    channelHeight: null,
  }
});

where:

  • channelHeight defines the total height in pixels between two channels.

  • nbVisibleChannels is the number of channels in the visible area of the screen.

  • nbMarginChannels is defining the number of channels above and below the visible channels that we want to preload.

  • defaultFocusedChannelPosition defines the position within the visible channels, of the focused channel.

    Info

    This is a default position. In non-cyclic mode, the component will change the position of the focused channel to avoid having empty channels within the β€œvisible” ones. For example when the first channel is focused, its position will be 0.

$EpgChannelListView
defaultFocusedChannelPosition = 2
nbvisibleChannels = 5
nbMarginChannels = 1
channelListAdditionalNbMarginChannels = 1
nbMarginChannels = 1
channelListAdditionalNbMarginChannels = 1
channel position 0
channel position 1
channel position 2
channel position 3
channel position 4
$EpgGridView

You can also define channelListAdditionalNbMarginChannels which allows to have more margin between channels in the channel list than in the program grid.

Additional configurable properties

Within your EpgView, you must define and give a value to these properties:

export default $EpgView.declare("MyEpgView", {
  properties: /** @lends MyEpgView.prototype */ {
    isCyclic: true,
    channelsTransition: {duration: 150, easing: $Easing.linear},
    timeTransition: {duration: 150, easing: $Easing.linear}
  }
});
  • isCyclic will define whether the component is cyclic on the defined channels axis. Not that the component will force a non-cyclic mode when the number of channels is lesser than the number of channels drawn in the components.
  • channelsTransition and timeTransition are defining the transition used when moving on the channel and the time axis.

Add Epg component to your screen

Add children and initialize it

Simply add your EpgView as a children. You must also indicate to the component what is the list of channels, and what are the start and end boundaries of the component, with the configure method called only once.

import $MyEpgView from "@MyEpgView";

export default $Screen.declare("MyEpgScreen", {

  children: {
    epgView: {class: $MyEpgView}
  },

  methods: /** @lends MyEpgScreen.prototype */ {
    prepareData: function (context) {
      // should get channels, start and end time boundaries
    },

    connectData: function (context) {
      if (!this._epgInitialized) {
        this.epgView.configure(this.data.channels, this.data.epgStartTime, this.data.epgEndTime);
        this._epgInitialized = true;
      }
    },
  }
});

Warning

Currently the configure method can only be called once. Meaning that further calls will fail.

Manage programs loading

Unlike using standard views, the loading of programs to feed the Epg component, is driven by itself. It means that you will not have to call an epgview.setData(...) method in the connectData of a screen.

Instead, the Epg component triggers $EpgView.NEED_DATA events when it knows that it needs programs to be shown in its grid. The screen must then listen to these events, load the programs and give them to the epg component.

RC actionScreen$EpgViewmove somewheredispatch NEED_DATA eventindicating the channels, start and end timeof the needed programs<<service>>
Check after move whether
programs that should be
shown are missing
load programs
programs loaded
add data (programs)
Draw new programsΒ 
in program grid

The NEED_DATA event payload is an object with four properties:

  • requestId as the identifier of the triggered event. It must be given back (with the loaded programs) to identify the event (i.e. more than one NEED_DATA event could be active at a time).
  • channelIds holding an array of $Channel identifiers. For each identifier in this array, a list of program must be given to the Epg component.
  • start and end are defining the period on which the programs must be loaded. These values are timestamps.

Once loaded, the programs must be given to the Epg component through the addData(requestId, programsByChannel) method.

Here is an example for such event with this kind of payload:

const event = {
  requestId: 3521,
  channelIds: ["abc1", "fr_2"],
  start: 1681315200000,
  end: 1681326000000
}

Then addData() must be called with such parameters:

addData(3521, [ // requestId
    [<$GridProgram instance>, <$GridProgram instance>, ...], // programs of channel "abc1" from 1681315200000 to 1681326000000
    [<$GridProgram instance>, <$GridProgram instance>, ...]  // programs of channel "fr_2" from 1681315200000 to 1681326000000
])

And at last, in your screen, you need to listen to NEED_DATA events and implement the listener function to add programs:

export default $Screen.declare("MyEpgScreen", {
  listen: [
    $MyEpgView.NEED_DATA
  ],

  methods: /** @lends MyEpgScreen.prototype */ {
    onEpgViewNeedData: function (context) {
      const {requestId, channelIds, start, end} = context;

      const loadPrograms = channelIds.map(channelId => {
        return this.myProgramService.loadPrograms(channelId, start, end);
      });

      Promise.all(loadPrograms)
        .then(programsByChannels => {
          this.epgView.addData(requestId, programsByChannels);
        });
    }
  }
});

Info

The first program of a requested channel programs list, must start exactly or before the given start parameter value.

The last program must end exactly or after the given end parameter value.

There must be no gaps between two consecutive programs.

Warning

The component doesn’t check the consistency of programs and neither if the three rules above, are respected. And that must be done by the controller or the service.

Manage navigation

You can also manage navigation. Just adapt it to your current screen structure:

export default $Screen.declare("MyEpgScreen", {
  methods: /** @lends MyEpgScreen.prototype */ {
    onKeypressDOWN: function () {
      this.epgView.nextChannel();
    },
    onKeypressUP: function () {
      this.epgView.previousChannel();
    },
    onKeypressRIGHT: function () {
      this.epgView.nextProgram();
    },
    onKeypressLEFT: function () {
      this.epgView.previousProgram();
    }
  }
});

Select a channel and a program

A common use case will be to open the Epg on the first channel and on the live program. You can do it with such code:

export default $Screen.declare("MyEpgScreen", {
  methods: /** @lends MyEpgScreen.prototype */ {
    connectData: function (context) {
      if (!this._epgInitialized) {
        this.epgView.configure(this.data.channels, this.data.epgStartTime, this.data.epgEndTime);
        this._epgInitialized = true;
      }

      this.epgView.select(this.data.channels[0].id, Date.now());
    }
  }
});

Info

Please notice that Epg component navigation methods currently don’t return any JavaScript promise.

Create implementations of item views

Each of the channel list, timeline, and program grid are using item views to render, respectively, a channel, a time, a program. Such EpgView component is using three default implementations (for each of them) which are very basic ones.

You can define your own items view class simply by defining an implementation of the corresponding abstract item view:

  • For the channel list:
import $AbstractEpgChannelListItemView from "@AbstractEpgChannelListItemView";

/**
 * @name MyEpgChannelListItemView
 * @class
 * @extends AbstractEpgChannelListItemView
 * @implementation
 */
export default $AbstractEpgChannelListItemView.declare("MyEpgChannelListItemView", {
     _setData: function (data, options) { // data will be a $Channel
         // set data to your primitives
     }
});
  • For the timeline:
import $AbstractEpgTimelineListItemView from "@AbstractEpgTimelineListItemView";

/**
 * @name MyEpgTimelineListItemView
 * @class
 * @extends AbstractEpgTimelineListItemView
 * @implementation
 */
export default $AbstractEpgTimelineListItemView.declare("MyEpgTimelineListItemView", {
     _setData: function (data, options) { // data will be a timestamp
         // set data to your primitives
     }
});
  • For the program grid:
import $AbstractEpgTileView from "@AbstractEpgChannelTileView";

/**
 * @name MyEpgTileView
 * @class
 * @extends AbstractEpgTileView
 * @implementation
 */
export default $AbstractEpgTileView.declare("MyEpgTileView", {
     /*
      * data is a "tile model" object following this structure:
      * {
      *    id: <epg component internal program id>,
      *    program: <$GridProgram model>,
      *    width: 257,
      *    x: 6852,
      *    y: -2760,
      *    frames: 1,
      *    channelIndex: -23
      * }
      */
     setData: function (data) {
        $AbstractEpgTileView.prototype.setData.apply(this, arguments); // parent will manage x, y and width
         // set data to your primitives
     }
});

Advanced Configuration

Horizontal navigation (manage program’s position on screen)

This EpgView component uses a default algorithm to position the timeline and program grid to a defined position when navigating through programs of the same channel. This algorithm simply moves the timeline and the program grid, to let one hour margin between the start of the program grid view, and the start of the current focused program.

$EpgGridView component
$EpgTileView items
the current focused program
1 hour

This default behavior can be modified by overriding the _getGridPosition method:

export default $EpgView.declare("MyEpgView", {
  methods: {
    /**
     * Called automatically when current program changed on horizontal navigation or when the Epg is initialized.
     * The method must return currentTime + referenceTime used in the Epg, according to current focused program.
     * Default implementation will set currentTime to the program' start and defines the reference time 1H before.
     *
     * @param {$EpgTileModel} targetProgramTile the program tile that will be focused
     * @param {number} direction Is set to -1 when browsing towards previous channel,
     *                           1 when browsing towards next channel,
     *                           or 0 when grid is just drawn/reset with no movement.
     * @returns {object} An object holding those two values: currentTime and referenceTime
     */
    _getGridPosition: function (targetProgramTile, direction) {
      return {
        referenceTime: targetProgramTile.program.start - 1 * $DateUtils.ONE_HOUR,
        currentTime: targetProgramTile.program.start
      };
    }
  }
});

This method returns two values:

  • referenceTime which is the time shown on the leftmost position of the program grid component.
  • currentTime holding the time currently β€œselected” by the component.
the current focused program
1 hour
referenceTime
currentTime
$EpgGridView

The currentTime is used when navigating through channels: the program to focus on a newly focused channel, is always the one active at currentTime. Also the horizontal axis position will never change when navigating on the vertical axis.

In the schema below, we have moved to the next channel:

new current focused program
referenceTime
currentTime
former focused program

And currentTime is only updated through the _getGridPosition() method, which is called when:

  • we go to previous/next program on the same channel.

  • we select another time.

    Info

    The Epg component always try to align the horizontal position of the timeline with the one from the program grid.

Dynamic channel position

The EpgView component defines the position of the focused channel within visible channels with the _getChannelPosition() method. The default behavior will position the focused channel at index defaultFocusedChannelPosition.

$EpgGridView
$EpgChannelListView
defaultFocusedChannelPosition = 2
channel position 0
channel position 1
channel position 2
channel position 3
channel position 4

You can change its value by overriding the method:

export default $EpgView.declare("MyEpgView", {
  methods: {
    /**
     * Called automatically when the current channel has changed.
     * The method must return the position of the focused channel within visible channels.
     * (i.e. the position from the reference channel)
     * Default implementation will return the defaultFocusedChannelPosition, except on non-cyclic mode
     * where it would lead to show empty channels, only at start or end of channels list.
     *
     * @param {number} channelIndex index of the channel to focus. This is the data index
     * @param {number} direction Is set to -1 when browsing towards previous channel,
     *                           1 when browsing towards next channel,
     *                           or 0 when grid is just drawn/reset with no movement.
     * @returns {number} the position of the channel
     */
    _getChannelPosition: function (channelIndex, direction) {
      return this.defaultChannelPosition
    }
  }
});

This method must return the wanted position for the focused channel.

Refine program tile geometry

For each program to draw in the program grid, the Epg component will create an object containing all data needed to manage and draw that program:

const tileGeometry = {
    id: "<epg component internal program id>",
    program: "<$GridProgram model>",
    width: 257,
    x: 6852,
    y: -2760,
    frames: 1,
    channelIndex: -23
};

In such object, we can find the x, y and width properties which are used by the tile to position and dimension itself.

These properties are defined in a specific method which might be overridden if we need to tweak the program geometry:

export default $EpgView.declare("MyEpgView", {
  methods: {
    /**
     * Compute geometry of the program tile.
     * The method must set the x, y and width property inside the given tile model.
     *
     * @param {$EpgTileModel} tileModel the EPG program tile model to update
     */
    _setProgramTileGeometry: function (tileModel) {
      const program = tileModel.program;
      const offsetStart = Math.round((program.start - this.$$startTime) * this.ratio);
      const offsetEnd = Math.round((program.start + program.duration - this.$$startTime) * this.ratio);

      tileModel.x = offsetStart;
      tileModel.y = tileModel.channelIndex * this.channelHeight;
      tileModel.width = offsetEnd - offsetStart;
    }
  }
});

API Reference

This UI component is part of Dana’s vendor named @dana/vendor-components which includes a JSDoc description.