美文网首页开源
streetscape.gl学习笔记(三)

streetscape.gl学习笔记(三)

作者: questionuncle | 来源:发表于2020-02-09 15:44 被阅读0次

    很久没有更新博客了,原因总有许多。但是每每都会回来看看有没有信息,看到写文章中还有不少只写了开头未发布的文章,心中又有不舍。这是关于streetscape.gl的第三篇笔记,或许后面还会有更新,亦或许没有更新换做其他领域的文章。不管怎么说,把streetscape.gl的一个闭环讲完也算是了却自己心头一件事。
    这篇主要讲进度条,在各类看板中进度条是必备的。播放控制、滑块定位、刻度展示等等。当然我们也可以以此为模板进行扩展,例如:添加倍速、标记关键时间节点、添加前至进度条等等。在对历史数据回放时,汽车工程师和进度条之间的交互就愈发频繁,所以进度条准确、便利给客户带来的感受提升不亚于在地图页面。
    streetscape.gl demo上的进度条由两部分组成:一个我称之为主进度条、另一个我管它叫前至进度条(举个例子:当前主进度条播放到10s处,如果前至进度条设置为2s,则在LogViewer中不仅能看到10s处的数据也可以看到12s处的数据。为什么有这样的功能,可能只有客户知道吧)。当然streetscape.gl是通过组合的方式提供的一个组件。


    image.png

    如何使用

    在streetscape.gl提供的get-started我们可以看到它是这样被加入进来的。

    import {
      LogViewer,
      PlaybackControl,
      StreamSettingsPanel,
      MeterWidget,
      TrafficLightWidget,
      TurnSignalWidget,
      XVIZPanel,
      VIEW_MODE
    } from 'streetscape.gl';
    render() {
        const {log, settings} = this.state;
    
        return (
        ...
              <div id="timeline">
                <PlaybackControl
                  width="100%"
                  log={log}
                  formatTimestamp={x => new Date(x * TIMEFORMAT_SCALE).toUTCString()}
                />
              </div>
        ...
        );
      }
    

    可以看到使用方便,只要输入相关属性即可。

    如何实现

    在./modules/core/src/components/playback-control中,我们可以看到这个组件的定义。其中index.js文件定义的PlaybackControl组件,我们可以视其为容器组件,主要负责组件的逻辑部分;而dual-playback-control.js文件定义的DualPlaybackControl组件,其组合了streetscape.gl/monochrome中的PlaybackControl组件,我们可以视其为UI组件,主要负责页面渲染部分。
    先看index.js中PlaybackControl组件

    class PlaybackControl extends PureComponent {
    ...
    }
    

    这里插入一下PureComponent和Component之间的区别。
    得益于作者这两天看的《React状态管理及同构实战》这本书,对React中的部分细节有所感悟,边看该框架的同时,也看看它运用React的最佳实践,精华部分当然应该吸收借鉴。
    一句话概括就是:PureComponent通过props和state的浅对比来实现 shouldComponentUpate(),而Component没有,而shouldComponentUpdate是决定界面是否需要更新的钥匙。
    再看其属性的定义:

      static propTypes = {
        // from log
        timestamp: PropTypes.number,
        lookAhead: PropTypes.number,
        startTime: PropTypes.number,
        endTime: PropTypes.number,
        buffered: PropTypes.array,
    
        // state override
        isPlaying: PropTypes.bool,
    
        // config
        width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
        style: PropTypes.object,
        compact: PropTypes.bool,
        className: PropTypes.string,
        step: PropTypes.number,
        padding: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
        tickSpacing: PropTypes.number,
        markers: PropTypes.arrayOf(PropTypes.object),
        formatTick: PropTypes.func,
        formatTimestamp: PropTypes.func,
        // dual playback control config
        maxLookAhead: PropTypes.number,
        formatLookAhead: PropTypes.func,
    
        // callbacks
        onPlay: PropTypes.func,
        onPause: PropTypes.func,
        onSeek: PropTypes.func,
        onLookAheadChange: PropTypes.func
      };
    

    从属性的定义上,我们可以看到,有部分属性来自log以获得这批数据的起止时间、时间间隔、前至多久等,还有当前播放状态,以及在页面上的样式用以显示。
    我们从React几个状态函数入手看看,组件各个阶段都做了什么。先存一张图,react组件各状态流程(不能烂熟于心只能一一对照着看了)


    react.png

    先看看componentWillReceiveProps

      componentWillReceiveProps(nextProps) {
        if ('isPlaying' in nextProps) {
          this.setState({isPlaying: Boolean(nextProps.isPlaying)});
        }
      }
    

    当组件运行时接到新属性,根据新属性更新播放状态。这个相对单一,再看看componentDidUpdate

      componentDidUpdate(prevProps, prevState) {
        const {isPlaying} = this.state;
        if (isPlaying && prevState.isPlaying !== isPlaying) {
          this._lastAnimationUpdate = Date.now();
          this._animationFrame = window.requestAnimationFrame(this._animate);
        }
      }
    

    当状态或者属性发生改变,引起组件的重新渲染并触发。
    当播放状态为true同时上一播放状态为false时,让浏览器在下一次重绘之前执行_animate动画,同时它返回一个long值_animationFrame,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。
    再看看_animate这个动画

      _animate = () => {
        if (this.state.isPlaying) {
          const now = Date.now();
          const {startTime, endTime, buffered, timestamp} = this.props;
          const {timeScale} = this.state;
          const lastUpdate = this._lastAnimationUpdate;
          const {PLAYBACK_FRAME_RATE, TIME_WINDOW} = getXVIZConfig();
    
          // avoid skipping frames - cap delta at resolution
          let timeDeltaMs = lastUpdate > 0 ? now - lastUpdate : 0;
          timeDeltaMs = Math.min(timeDeltaMs, 1000 / PLAYBACK_FRAME_RATE);
    
          let newTimestamp = timestamp + timeDeltaMs * timeScale;
          if (newTimestamp > endTime) {
            newTimestamp = startTime;
          }
    
          // check buffer availability
          if (buffered.some(r => newTimestamp >= r[0] && newTimestamp <= r[1] + TIME_WINDOW)) {
            // only move forward if buffer is loaded
            // otherwise pause and wait
            this._onTimeChange(newTimestamp);
          }
    
          this._lastAnimationUpdate = now;
          this._animationFrame = window.requestAnimationFrame(this._animate);
        }
      };
    

    当播放状态为true时,开始执行该动画。
    先计算当前时间和最后一次更新时间的时间差timeDeltaMs。
    通过时间差timeDeltaMs和属性中的该log对应的timestamp求和获得新的newTimestamp。
    检查buffered中是否有区间正好卡住newTimestamp,如果有则向前移动否则就暂停并等待。(这段估计是为了控制进度条中的slider)。
    设置最近的动画更新时间点为now。
    为了在浏览器下次重绘之前继续更新下一帧动画,那么回调函数_animate中自身必须再次调用window.requestAnimationFrame()。
    这里调用了一个_onTimeChange函数

      _onTimeChange = timestamp => {
        const {log, onSeek} = this.props;
        if (!onSeek(timestamp) && log) {
          log.seek(timestamp);
        }
      };
    

    该函数主要是让log数据seek到指定的timestamp。这里的log是 XVIZStreamLoaderXVIZLiveLoaderXVIZFileLoader中的一种,其都继承于XVIZLoaderInterface这部分计划再单开一片笔记讲解一下
    再看看componentWillUnmount,这一阶段主要将注册的动画取消掉。

      componentWillUnmount() {
        if (this._animationFrame) {
          window.cancelAnimationFrame(this._animationFrame);
        }
      }
    

    该组件的主要生命周期函数介绍完后,再看看render函数。

      render() {
        const {startTime, endTime, timestamp, lookAhead, buffered, ...otherProps} = this.props;
    
        if (!Number.isFinite(timestamp) || !Number.isFinite(startTime)) {
          return null;
        }
    
        const bufferRange = buffered.map(r => ({
          startTime: Math.max(r[0], startTime),
          endTime: Math.min(r[1], endTime)
        }));
    
        return (
          <DualPlaybackControl
            {...otherProps}
            bufferRange={bufferRange}
            currentTime={timestamp}
            lookAhead={lookAhead}
            startTime={startTime}
            endTime={endTime}
            isPlaying={this.state.isPlaying}
            formatTick={this._formatTick}
            formatTimestamp={this._formatTimestamp}
            formatLookAhead={this._formatLookAhead}
            onSeek={this._onSeek}
            onPlay={this._onPlay}
            onPause={this._onPause}
            onLookAheadChange={this._onLookAheadChange}
          />
        );
      }
    }
    

    该处功能也很明确,首先是判断timestamp和startTime是否为有穷数,如若不是直接返回为null。该部分有可能是针对Live数据源,对于直接是实时的数据源就不用安排上进度条了。然后将属性、事件,通过props向子组件传递,其中对bufferRange属性值和全局的startTime、endTime进行比较,使其落入到最小范围区间内。
    接着又定义了一个getLogState的函数,主要是从log数据体中获得timestamp、lookAhead、startTime等数据。

    const getLogState = log => ({
      timestamp: log.getCurrentTime(),
      lookAhead: log.getLookAhead(),
      startTime: log.getLogStartTime(),
      endTime: log.getLogEndTime(),
      buffered: log.getBufferedTimeRanges()
    });
    

    这个函数是用于connectToLog这个高阶组件包装函数中的。

    export default connectToLog({getLogState, Component: PlaybackControl});
    

    这里,作者把所有需要绑定log相应属性生成高阶组件,将其抽出成一个高阶组件生成函数——connectToLog,你可以在源码的很多地方瞧见它。是一个很不错的实践,可供参考。
    这部分先讲到这,再看看DualPlaybackControl这个UI组件,在dual-playback-control.js文件中。

    const LookAheadContainer = styled.div(props => ({
      display: 'flex',
      alignItems: 'center',
      width: 200,
      '>div': {
        flexGrow: 1
      },
      ...evaluateStyle(props.userStyle, props)
    }));
    
    const LookAheadTimestamp = styled.span(props => ({
      marginLeft: props.theme.spacingNormal,
      marginRight: props.theme.spacingNormal,
      ...evaluateStyle(props.userStyle, props)
    }));
    

    先是定义了两个dom的容器,一个用以存放前至进度条,另一个用以存放前至多久数值。

    const lookAheadMarkerStyle = props => ({
      position: 'absolute',
      boxSizing: 'content-box',
      borderStyle: 'solid',
      marginTop: 6,
      marginLeft: -6,
      borderWidth: 6,
      borderLeftColor: 'transparent',
      borderRightColor: 'transparent',
      borderTopColor: '#888',
      borderBottomStyle: 'none',
    
      transitionProperty: 'left',
      transitionDuration: props.isPlaying ? '0s' : props.theme.transitionDuration,
    
      ...evaluateStyle(props.userStyle, props)
    });
    

    这部分用以定义主进度条前至滑块的样式。其中evaluateStyle(在modules\monochrome\src\shared\theme.js文件中)函数定义如下

    export function evaluateStyle(userStyle, props) {
      if (!userStyle) {
        return null;
      }
      if (typeof userStyle === 'function') {
        return userStyle(props);
      }
      return userStyle;
    }
    

    获取用户自定义的样式。
    接着进入到组件的定义部分,该组件职责很单一,负责UI的展示,所以状态函数很少,只有render函数

      render() {
        const {
          theme,
          isPlaying,
          markers: userMarkers,
          style,
          children,
          currentTime,
          lookAhead,
          endTime
        } = this.props;
        const lookAheadTime = Math.min(endTime, currentTime + lookAhead);
    
        const markers = userMarkers.concat({
          time: lookAheadTime,
          style: lookAheadMarkerStyle({theme, isPlaying, userStyle: style.lookAheadMarker})
        });
    
        return (
          <PlaybackControl {...this.props} markers={markers}>
            {children}
            <div style={{flexGrow: 1}} />
            {this._renderLookAheadSlider()}
          </PlaybackControl>
        );
      }
    

    首先通过比较结束时间与当前时间和前至时间的大小,获得当前前至到什么时刻。
    设置前至marker属性,主要是时间及样式,为PlaybackControl组件的markers属性准备好数据。
    最后将PlaybackControl组件和前至进度条组合后返回。
    这里用到了_renderLookAheadSlider函数,来渲染前至进度条。

      _renderLookAheadSlider() {
        const {theme, style, isPlaying, lookAhead, formatLookAhead, maxLookAhead, step} = this.props;
    
        return (
          <LookAheadContainer theme={theme} isPlaying={isPlaying} userStyle={style.lookAhead}>
            <LookAheadTimestamp
              theme={theme}
              isPlaying={isPlaying}
              userStyle={style.lookAheadTimestamp}
            >
              Look ahead: {formatLookAhead(lookAhead)}
            </LookAheadTimestamp>
            <Slider
              style={style.lookAheadSlider}
              value={lookAhead}
              min={0}
              max={maxLookAhead}
              step={step}
              size={16}
              onChange={this.props.onLookAheadChange}
            />
          </LookAheadContainer>
        );
      }
    

    其构成也很简单,由一个span和一个Slider构成,分别存放文本Look ahead: *s和Slider滑块。
    DualPlaybackControl组件就介绍到这,可以看出该组件是有个组合组件,将由streetscape.gl/monochrome中的PlaybackControl组件主进度条和由Slider组件前至进度条构成,同时用以承接streetscape.gl中逻辑组件PlaybackControl的属性及事件。
    那接下来就继续深挖,看看streetscape.gl/monochrome为我们提供的基础组件——PlaybackControl和Slider。


    先写到这,待更新!

    相关文章

      网友评论

        本文标题:streetscape.gl学习笔记(三)

        本文链接:https://www.haomeiwen.com/subject/reczkctx.html