text: /**
   * @file Silder timeline view
   */
  define(function (require) {
  
      var zrUtil = require('zrender/core/util');
      var graphic = require('../../util/graphic');
      var layout = require('../../util/layout');
      var TimelineView = require('./TimelineView');
      var TimelineAxis = require('./TimelineAxis');
      var symbolUtil = require('../../util/symbol');
      var axisHelper = require('../../coord/axisHelper');
      var BoundingRect = require('zrender/core/BoundingRect');
      var matrix = require('zrender/core/matrix');
      var numberUtil = require('../../util/number');
      var modelUtil = require('../../util/model');
      var formatUtil = require('../../util/format');
      var encodeHTML = formatUtil.encodeHTML;
  
      var bind = zrUtil.bind;
      var each = zrUtil.each;
  
      var PI = Math.PI;
  
      return TimelineView.extend({
  
          type: 'timeline.slider',
  
          init: function (ecModel, api) {
  
              this.api = api;
  
              /**
               * @private
               * @type {module:echarts/component/timeline/TimelineAxis}
               */
              this._axis;
  
              /**
               * @private
               * @type {module:zrender/core/BoundingRect}
               */
              this._viewRect;
  
              /**
               * @type {number}
               */
              this._timer;
  
              /**
               * @type {module:zrende/Element}
               */
              this._currentPointer;
  
              /**
               * @type {module:zrender/container/Group}
               */
              this._mainGroup;
  
              /**
               * @type {module:zrender/container/Group}
               */
              this._labelGroup;
          },
  
          /**
           * @override
           */
          render: function (timelineModel, ecModel, api, payload) {
              this.model = timelineModel;
              this.api = api;
              this.ecModel = ecModel;
  
              this.group.removeAll();
  
              // var mainGroup = new graphic.Group();
  
              var layoutInfo = this._layout(timelineModel, api);
  
              var mainGroup = this._createGroup('mainGroup');
              var labelGroup = this._createGroup('labelGroup');
  
              /**
               * @private
               * @type {module:echarts/component/timeline/TimelineAxis}
               */
              var axis = this._axis = this._createAxis(layoutInfo, timelineModel);
  
              each(
                  ['AxisLine', 'AxisTick', 'Control', 'CurrentPointer'],
                  function (name) {
                      this['_render' + name](layoutInfo, mainGroup, axis, timelineModel);
                  },
                  this
              );
  
              this._renderAxisLabel(layoutInfo, labelGroup, axis, timelineModel);
  
              this._position(layoutInfo, timelineModel);
  
              this._doPlayStop();
          },
  
          /**
           * @override
           */
          remove: function () {
              this._clearTimer();
              this.group.removeAll();
          },
  
          /**
           * @override
           */
          dispose: function () {
              this._clearTimer();
          },
  
          _layout: function (timelineModel, api) {
              var labelPosOpt = timelineModel.get('label.normal.position');
              var orient = timelineModel.get('orient');
              var viewRect = getViewRect(timelineModel, api);
              // Auto label offset.
              if (labelPosOpt == null || labelPosOpt === 'auto') {
                  labelPosOpt = orient === 'horizontal'
                      ? ((viewRect.y + viewRect.height / 2) < api.getHeight() / 2 ? '-' : '+')
                      : ((viewRect.x + viewRect.width / 2) < api.getWidth() / 2 ? '+' : '-');
              }
              else if (isNaN(labelPosOpt)) {
                  labelPosOpt = ({
                      horizontal: {top: '-', bottom: '+'},
                      vertical: {left: '-', right: '+'}
                  })[orient][labelPosOpt];
              }
  
              // FIXME
              // 暂没有实现用户传入
              // var labelAlign = timelineModel.get('label.normal.textStyle.align');
              // var labelBaseline = timelineModel.get('label.normal.textStyle.baseline');
              var labelAlignMap = {
                  horizontal: 'center',
                  vertical: (labelPosOpt >= 0 || labelPosOpt === '+') ? 'left' : 'right'
              };
  
              var labelBaselineMap = {
                  horizontal: (labelPosOpt >= 0 || labelPosOpt === '+') ? 'top' : 'bottom',
                  vertical: 'middle'
              };
              var rotationMap = {
                  horizontal: 0,
                  vertical: PI / 2
              };
  
              // Position
              var mainLength = orient === 'vertical' ? viewRect.height : viewRect.width;
  
              var controlModel = timelineModel.getModel('controlStyle');
              var showControl = controlModel.get('show');
              var controlSize = showControl ? controlModel.get('itemSize') : 0;
              var controlGap = showControl ? controlModel.get('itemGap') : 0;
              var sizePlusGap = controlSize + controlGap;
  
              // Special label rotate.
              var labelRotation = timelineModel.get('label.normal.rotate') || 0;
              labelRotation = labelRotation * PI / 180; // To radian.
  
              var playPosition;
              var prevBtnPosition;
              var nextBtnPosition;
              var axisExtent;
              var controlPosition = controlModel.get('position', true);
              var showControl = controlModel.get('show', true);
              var showPlayBtn = showControl && controlModel.get('showPlayBtn', true);
              var showPrevBtn = showControl && controlModel.get('showPrevBtn', true);
              var showNextBtn = showControl && controlModel.get('showNextBtn', true);
              var xLeft = 0;
              var xRight = mainLength;
  
              // position[0] means left, position[1] means middle.
              if (controlPosition === 'left' || controlPosition === 'bottom') {
                  showPlayBtn && (playPosition = [0, 0], xLeft += sizePlusGap);
                  showPrevBtn && (prevBtnPosition = [xLeft, 0], xLeft += sizePlusGap);
                  showNextBtn && (nextBtnPosition = [xRight - controlSize, 0], xRight -= sizePlusGap);
              }
              else { // 'top' 'right'
                  showPlayBtn && (playPosition = [xRight - controlSize, 0], xRight -= sizePlusGap);
                  showPrevBtn && (prevBtnPosition = [0, 0], xLeft += sizePlusGap);
                  showNextBtn && (nextBtnPosition = [xRight - controlSize, 0], xRight -= sizePlusGap);
              }
              axisExtent = [xLeft, xRight];
  
              if (timelineModel.get('inverse')) {
                  axisExtent.reverse();
              }
  
              return {
                  viewRect: viewRect,
                  mainLength: mainLength,
                  orient: orient,
  
                  rotation: rotationMap[orient],
                  labelRotation: labelRotation,
                  labelPosOpt: labelPosOpt,
                  labelAlign: labelAlignMap[orient],
                  labelBaseline: labelBaselineMap[orient],
  
                  // Based on mainGroup.
                  playPosition: playPosition,
                  prevBtnPosition: prevBtnPosition,
                  nextBtnPosition: nextBtnPosition,
                  axisExtent: axisExtent,
  
                  controlSize: controlSize,
                  controlGap: controlGap
              };
          },
  
          _position: function (layoutInfo, timelineModel) {
              // Position is be called finally, because bounding rect is needed for
              // adapt content to fill viewRect (auto adapt offset).
  
              // Timeline may be not all in the viewRect when 'offset' is specified
              // as a number, because it is more appropriate that label aligns at
              // 'offset' but not the other edge defined by viewRect.
  
              var mainGroup = this._mainGroup;
              var labelGroup = this._labelGroup;
  
              var viewRect = layoutInfo.viewRect;
              if (layoutInfo.orient === 'vertical') {
                  // transfrom to horizontal, inverse rotate by left-top point.
                  var m = matrix.create();
                  var rotateOriginX = viewRect.x;
                  var rotateOriginY = viewRect.y + viewRect.height;
                  matrix.translate(m, m, [-rotateOriginX, -rotateOriginY]);
                  matrix.rotate(m, m, -PI / 2);
                  matrix.translate(m, m, [rotateOriginX, rotateOriginY]);
                  viewRect = viewRect.clone();
                  viewRect.applyTransform(m);
              }
  
              var viewBound = getBound(viewRect);
              var mainBound = getBound(mainGroup.getBoundingRect());
              var labelBound = getBound(labelGroup.getBoundingRect());
  
              var mainPosition = mainGroup.position;
              var labelsPosition = labelGroup.position;
  
              labelsPosition[0] = mainPosition[0] = viewBound[0][0];
  
              var labelPosOpt = layoutInfo.labelPosOpt;
  
              if (isNaN(labelPosOpt)) { // '+' or '-'
                  var mainBoundIdx = labelPosOpt === '+' ? 0 : 1;
                  toBound(mainPosition, mainBound, viewBound, 1, mainBoundIdx);
                  toBound(labelsPosition, labelBound, viewBound, 1, 1 - mainBoundIdx);
              }
              else {
                  var mainBoundIdx = labelPosOpt >= 0 ? 0 : 1;
                  toBound(mainPosition, mainBound, viewBound, 1, mainBoundIdx);
                  labelsPosition[1] = mainPosition[1] + labelPosOpt;
              }
  
              mainGroup.position = mainPosition;
              labelGroup.position = labelsPosition;
              mainGroup.rotation = labelGroup.rotation = layoutInfo.rotation;
  
              setOrigin(mainGroup);
              setOrigin(labelGroup);
  
              function setOrigin(targetGroup) {
                  var pos = targetGroup.position;
                  targetGroup.origin = [
                      viewBound[0][0] - pos[0],
                      viewBound[1][0] - pos[1]
                  ];
              }
  
              function getBound(rect) {
                  // [[xmin, xmax], [ymin, ymax]]
                  return [
                      [rect.x, rect.x + rect.width],
                      [rect.y, rect.y + rect.height]
                  ];
              }
  
              function toBound(fromPos, from, to, dimIdx, boundIdx) {
                  fromPos[dimIdx] += to[dimIdx][boundIdx] - from[dimIdx][boundIdx];
              }
          },
  
          _createAxis: function (layoutInfo, timelineModel) {
              var data = timelineModel.getData();
              var axisType = timelineModel.get('axisType');
  
              var scale = axisHelper.createScaleByModel(timelineModel, axisType);
              var dataExtent = data.getDataExtent('value');
              scale.setExtent(dataExtent[0], dataExtent[1]);
              this._customizeScale(scale, data);
              scale.niceTicks();
  
              var axis = new TimelineAxis('value', scale, layoutInfo.axisExtent, axisType);
              axis.model = timelineModel;
  
              return axis;
          },
  
          _customizeScale: function (scale, data) {
  
              scale.getTicks = function () {
                  return data.mapArray(['value'], function (value) {
                      return value;
                  });
              };
  
              scale.getTicksLabels = function () {
                  return zrUtil.map(this.getTicks(), scale.getLabel, scale);
              };
          },
  
          _createGroup: function (name) {
              var newGroup = this['_' + name] = new graphic.Group();
              this.group.add(newGroup);
              return newGroup;
          },
  
          _renderAxisLine: function (layoutInfo, group, axis, timelineModel) {
              var axisExtent = axis.getExtent();
  
              if (!timelineModel.get('lineStyle.show')) {
                  return;
              }
  
              group.add(new graphic.Line({
                  shape: {
                      x1: axisExtent[0], y1: 0,
                      x2: axisExtent[1], y2: 0
                  },
                  style: zrUtil.extend(
                      {lineCap: 'round'},
                      timelineModel.getModel('lineStyle').getLineStyle()
                  ),
                  silent: true,
                  z2: 1
              }));
          },
  
          /**
           * @private
           */
          _renderAxisTick: function (layoutInfo, group, axis, timelineModel) {
              var data = timelineModel.getData();
              var ticks = axis.scale.getTicks();
              var tooltipHostModel = this._prepareTooltipHostModel(data, timelineModel);
  
              each(ticks, function (value, dataIndex) {
  
                  var tickCoord = axis.dataToCoord(value);
                  var itemModel = data.getItemModel(dataIndex);
                  var itemStyleModel = itemModel.getModel('itemStyle.normal');
                  var hoverStyleModel = itemModel.getModel('itemStyle.emphasis');
                  var symbolOpt = {
                      position: [tickCoord, 0],
                      onclick: bind(this._changeTimeline, this, dataIndex)
                  };
                  var el = giveSymbol(itemModel, itemStyleModel, group, symbolOpt);
                  graphic.setHoverStyle(el, hoverStyleModel.getItemStyle());
  
                  if (itemModel.get('tooltip')) {
                      el.dataIndex = dataIndex;
                      el.hostModel = tooltipHostModel;
                  }
                  else {
                      el.dataIndex = el.hostModel = null;
                  }
  
              }, this);
          },
  
          /**
           * @private
           */
          _prepareTooltipHostModel: function (data, timelineModel) {
              var tooltipHostModel = modelUtil.createDataFormatModel(
                  {}, data, timelineModel.get('data')
              );
              var me = this;
  
              tooltipHostModel.formatTooltip = function (dataIndex) {
                  return encodeHTML(me._axis.scale.getLabel(dataIndex));
              };
  
              return tooltipHostModel;
          },
  
          /**
           * @private
           */
          _renderAxisLabel: function (layoutInfo, group, axis, timelineModel) {
              var labelModel = timelineModel.getModel('label.normal');
  
              if (!labelModel.get('show')) {
                  return;
              }
  
              var data = timelineModel.getData();
              var ticks = axis.scale.getTicks();
              var labels = axisHelper.getFormattedLabels(
                  axis, labelModel.get('formatter')
              );
              var labelInterval = axis.getLabelInterval();
  
              each(ticks, function (tick, dataIndex) {
                  if (axis.isLabelIgnored(dataIndex, labelInterval)) {
                      return;
                  }
  
                  var itemModel = data.getItemModel(dataIndex);
                  var itemTextStyleModel = itemModel.getModel('label.normal.textStyle');
                  var hoverTextStyleModel = itemModel.getModel('label.emphasis.textStyle');
                  var tickCoord = axis.dataToCoord(tick);
                  var textEl = new graphic.Text({
                      style: {
                          text: labels[dataIndex],
                          textAlign: layoutInfo.labelAlign,
                          textBaseline: layoutInfo.labelBaseline,
                          textFont: itemTextStyleModel.getFont(),
                          fill: itemTextStyleModel.getTextColor()
                      },
                      position: [tickCoord, 0],
                      rotation: layoutInfo.labelRotation - layoutInfo.rotation,
                      onclick: bind(this._changeTimeline, this, dataIndex),
                      silent: false
                  });
  
                  group.add(textEl);
                  graphic.setHoverStyle(textEl, hoverTextStyleModel.getItemStyle());
  
              }, this);
          },
  
          /**
           * @private
           */
          _renderControl: function (layoutInfo, group, axis, timelineModel) {
              var controlSize = layoutInfo.controlSize;
              var rotation = layoutInfo.rotation;
  
              var itemStyle = timelineModel.getModel('controlStyle.normal').getItemStyle();
              var hoverStyle = timelineModel.getModel('controlStyle.emphasis').getItemStyle();
              var rect = [0, -controlSize / 2, controlSize, controlSize];
              var playState = timelineModel.getPlayState();
              var inverse = timelineModel.get('inverse', true);
  
              makeBtn(
                  layoutInfo.nextBtnPosition,
                  'controlStyle.nextIcon',
                  bind(this._changeTimeline, this, inverse ? '-' : '+')
              );
              makeBtn(
                  layoutInfo.prevBtnPosition,
                  'controlStyle.prevIcon',
                  bind(this._changeTimeline, this, inverse ? '+' : '-')
              );
              makeBtn(
                  layoutInfo.playPosition,
                  'controlStyle.' + (playState ? 'stopIcon' : 'playIcon'),
                  bind(this._handlePlayClick, this, !playState),
                  true
              );
  
              function makeBtn(position, iconPath, onclick, willRotate) {
                  if (!position) {
                      return;
                  }
                  var opt = {
                      position: position,
                      origin: [controlSize / 2, 0],
                      rotation: willRotate ? -rotation : 0,
                      rectHover: true,
                      style: itemStyle,
                      onclick: onclick
                  };
                  var btn = makeIcon(timelineModel, iconPath, rect, opt);
                  group.add(btn);
                  graphic.setHoverStyle(btn, hoverStyle);
              }
          },
  
          _renderCurrentPointer: function (layoutInfo, group, axis, timelineModel) {
              var data = timelineModel.getData();
              var currentIndex = timelineModel.getCurrentIndex();
              var pointerModel = data.getItemModel(currentIndex).getModel('checkpointStyle');
              var me = this;
  
              var callback = {
                  onCreate: function (pointer) {
                      pointer.draggable = true;
                      pointer.drift = bind(me._handlePointerDrag, me);
                      pointer.ondragend = bind(me._handlePointerDragend, me);
                      pointerMoveTo(pointer, currentIndex, axis, timelineModel, true);
                  },
                  onUpdate: function (pointer) {
                      pointerMoveTo(pointer, currentIndex, axis, timelineModel);
                  }
              };
  
              // Reuse when exists, for animation and drag.
              this._currentPointer = giveSymbol(
                  pointerModel, pointerModel, this._mainGroup, {}, this._currentPointer, callback
              );
          },
  
          _handlePlayClick: function (nextState) {
              this._clearTimer();
              this.api.dispatchAction({
                  type: 'timelinePlayChange',
                  playState: nextState,
                  from: this.uid
              });
          },
  
          _handlePointerDrag: function (dx, dy, e) {
              this._clearTimer();
              this._pointerChangeTimeline([e.offsetX, e.offsetY]);
          },
  
          _handlePointerDragend: function (e) {
              this._pointerChangeTimeline([e.offsetX, e.offsetY], true);
          },
  
          _pointerChangeTimeline: function (mousePos, trigger) {
              var toCoord = this._toAxisCoord(mousePos)[0];
  
              var axis = this._axis;
              var axisExtent = numberUtil.asc(axis.getExtent().slice());
  
              toCoord > axisExtent[1] && (toCoord = axisExtent[1]);
              toCoord < axisExtent[0] && (toCoord = axisExtent[0]);
  
              this._currentPointer.position[0] = toCoord;
              this._currentPointer.dirty();
  
              var targetDataIndex = this._findNearestTick(toCoord);
              var timelineModel = this.model;
  
              if (trigger || (
                  targetDataIndex !== timelineModel.getCurrentIndex()
                  && timelineModel.get('realtime')
              )) {
                  this._changeTimeline(targetDataIndex);
              }
          },
  
          _doPlayStop: function () {
              this._clearTimer();
  
              if (this.model.getPlayState()) {
                  this._timer = setTimeout(
                      bind(handleFrame, this),
                      this.model.get('playInterval')
                  );
              }
  
              function handleFrame() {
                  // Do not cache
                  var timelineModel = this.model;
                  this._changeTimeline(
                      timelineModel.getCurrentIndex()
                      + (timelineModel.get('rewind', true) ? -1 : 1)
                  );
              }
          },
  
          _toAxisCoord: function (vertex) {
              var trans = this._mainGroup.getLocalTransform();
              return graphic.applyTransform(vertex, trans, true);
          },
  
          _findNearestTick: function (axisCoord) {
              var data = this.model.getData();
              var dist = Infinity;
              var targetDataIndex;
              var axis = this._axis;
  
              data.each(['value'], function (value, dataIndex) {
                  var coord = axis.dataToCoord(value);
                  var d = Math.abs(coord - axisCoord);
                  if (d < dist) {
                      dist = d;
                      targetDataIndex = dataIndex;
                  }
              });
  
              return targetDataIndex;
          },
  
          _clearTimer: function () {
              if (this._timer) {
                  clearTimeout(this._timer);
                  this._timer = null;
              }
          },
  
          _changeTimeline: function (nextIndex) {
              var currentIndex = this.model.getCurrentIndex();
  
              if (nextIndex === '+') {
                  nextIndex = currentIndex + 1;
              }
              else if (nextIndex === '-') {
                  nextIndex = currentIndex - 1;
              }
  
              this.api.dispatchAction({
                  type: 'timelineChange',
                  currentIndex: nextIndex,
                  from: this.uid
              });
          }
  
      });
  
      function getViewRect(model, api) {
          return layout.getLayoutRect(
              model.getBoxLayoutParams(),
              {
                  width: api.getWidth(),
                  height: api.getHeight()
              },
              model.get('padding')
          );
      }
  
      function makeIcon(timelineModel, objPath, rect, opts) {
          var icon = graphic.makePath(
              timelineModel.get(objPath).replace(/^path:\/\//, ''),
              zrUtil.clone(opts || {}),
              new BoundingRect(rect[0], rect[1], rect[2], rect[3]),
              'center'
          );
  
          return icon;
      }
  
      /**
       * Create symbol or update symbol
       */
      function giveSymbol(hostModel, itemStyleModel, group, opt, symbol, callback) {
          var symbolType = hostModel.get('symbol');
          var color = itemStyleModel.get('color');
          var symbolSize = hostModel.get('symbolSize');
          var halfSymbolSize = symbolSize / 2;
          var itemStyle = itemStyleModel.getItemStyle(['color', 'symbol', 'symbolSize']);
  
          if (!symbol) {
              symbol = symbolUtil.createSymbol(
                  symbolType, -halfSymbolSize, -halfSymbolSize, symbolSize, symbolSize, color
              );
              group.add(symbol);
              callback && callback.onCreate(symbol);
          }
          else {
              symbol.setStyle(itemStyle);
              symbol.setColor(color);
              group.add(symbol); // Group may be new, also need to add.
              callback && callback.onUpdate(symbol);
          }
  
          opt = zrUtil.merge({
              rectHover: true,
              style: itemStyle,
              z2: 100
          }, opt, true);
  
          symbol.attr(opt);
  
          return symbol;
      }
  
      function pointerMoveTo(pointer, dataIndex, axis, timelineModel, noAnimation) {
          if (pointer.dragging) {
              return;
          }
  
          var pointerModel = timelineModel.getModel('checkpointStyle');
          var toCoord = axis.dataToCoord(timelineModel.getData().get(['value'], dataIndex));
  
          if (noAnimation || !pointerModel.get('animation', true)) {
              pointer.attr({position: [toCoord, 0]});
          }
          else {
              pointer.stopAnimation(true);
              pointer.animateTo(
                  {position: [toCoord, 0]},
                  pointerModel.get('animationDuration', true),
                  pointerModel.get('animationEasing', true)
              );
          }
      }
  
  });