作者最近在做智能驾驶的项目,目前前端展示通过ol加载二维瓦片,地图上渲染的元素也不够酷炫。在项目启动之初正好是uber开源streetscape.gl之时,甚是欣喜,能有机会一睹大厂之作,算得上是享受。一直到最近几周才有功夫看看streetscape.gl的源码,想把获得的信息梳理一下,形成笔记,后面自己的控制台升级可以参照。
二话不说,先上个demo栗子镇楼。
可以说页面风格很简洁,作为智能驾驶辅助测试展示平台足矣。
设计内容主要分这几块:
- 地图展示
- 时间进度条
- 摄像头拍摄
- 传感器图表
- 数据流树装列表
先从我最关心的地图展示源码解读开始
streetscape.gl将地图上展示交互都打包成一个控件,它就是LogViewer (React Component)。
PS:通过介绍我们可以知道,是将XVIZ log数据进行渲染的控件。而XVIZ log数据是什么呢,就是streetscape.gl要求的后端数据交互格式数据,详见xviz。
LogViewer是啥样
接下来就看看LogViewer的源码,它是怎么样实现将XVIZ log数据在控件中渲染的,同时海提供hover和click的交互,并产生相应的tooltip和popup。
将源码下载到本地,在./modules/core/src/components/log-viewer.js中,我们可以看到这个组件的定义。
class LogViewer extends PureComponent {
static propTypes = {
...Core3DViewer.propTypes,
// Props from loader
frame: PropTypes.object,
metadata: PropTypes.object,
// Rendering options
renderTooltip: PropTypes.func,
style: PropTypes.object,
// Callbacks
onSelectObject: PropTypes.func,
onContextMenu: PropTypes.func,
onViewStateChange: PropTypes.func,
onObjectStateChange: PropTypes.func,
// Optional: to use with external state management (e.g. Redux)
viewState: PropTypes.object,
viewOffset: PropTypes.object,
objectStates: PropTypes.object
};
...
}
首先定义的是组件类自身的属性,在开发模式下,当提供一个不合法的值作为prop时,控制台会出现警告。这是一个很好的开发习惯,可以采用。
PS:React在15.5版本之后, 代替使用 PropTypes 直接从 React 对象这种导入方式, 安装一个新的包 prop-types 并且使用如下的方式进行导入,所以在package.json文件dependencies中需要添加一条依赖"prop-types": "^15.6.2"
LogViewer的属性一部分来自Core3DViewer,LogViewer组件由Core3DViewer和HoverTooltip这两个重要组件组成。
- frame、metadata属性是为了接收来自与后端XVIZ log数据
-
renderTooltip、style属性是为了在LogViewer中渲染tooltip,产生这样的效果
- onSelectObject、onContextMenu、onViewStateChange、onObjectStateChange属性是为了和地图上交互而进行回调响应
- viewState、viewOffset、objectStates属性分别记录当前组件所对应的视图状态和目标物状态,以便和其他组件进行交互
static defaultProps = {
...Core3DViewer.defaultProps,
style: {},
onViewStateChange: noop,
onObjectStateChange: noop,
onSelectObject: noop,
onContextMenu: noop,
getTransformMatrix: (streamName, context) => null
};
constructor(props) {
super(props);
this.state = {
viewState: {
width: 1,
height: 1,
longitude: 0,
latitude: 0,
...DEFAULT_VIEW_STATE,
...props.viewMode.initialViewState
},
viewOffset: {
x: 0,
y: 0,
bearing: 0
},
objectStates: {},
hoverInfo: null
};
}
这段是定义属性的默认值以及构造函数。
中间一段定义了一段事件,让我们看看最后的render
render() {
const viewState = this.props.viewState || this.state.viewState;
const viewOffset = this.props.viewOffset || this.state.viewOffset;
const objectStates = this.props.objectStates || this.state.objectStates;
return (
<div onContextMenu={preventDefault}>
<Core3DViewer
{...this.props}
onViewStateChange={this._onViewStateChange}
onClick={this._onClickObject}
onHover={this._onHoverObject}
onContextMenu={this._onContextMenu}
viewState={viewState}
viewOffset={viewOffset}
objectStates={objectStates}
>
{this._renderTooltip()}
</Core3DViewer>
</div>
);
}
render最后返回的是一个div所包裹的Core3DViewer,这里我们可以看看作者是如何实现在地图上渲染Tooltip的。
_renderTooltip() {
const {showTooltip, style, renderTooltip} = this.props;
const {hoverInfo} = this.state;
return (
showTooltip &&
hoverInfo && (
<HoverTooltip info={hoverInfo} style={style.tooltip} renderContent={renderTooltip} />
)
);
}
_renderTooltip最后返回的HoverTooltip组件的实例。
const getLogState = log => ({
frame: log.getCurrentFrame(),
metadata: log.getMetadata()
});
export default connectToLog({getLogState, Component: LogViewer});
这里作者通过connectToLog将log的state和组件进行绑定,我们可以看一下connectToLog这个function是如何定义的
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import XVIZLoaderInterface from '../loaders/xviz-loader-interface';
export default function connectToLog({getLogState, Component}) {
class WrappedComponent extends PureComponent {
static propTypes = {
log: PropTypes.instanceOf(XVIZLoaderInterface)
};
state = {
logVersion: -1
};
componentDidMount() {
const {log} = this.props;
if (log) {
log.subscribe(this._update);
}
}
componentWillReceiveProps(nextProps) {
const {log} = this.props;
const nextLog = nextProps.log;
if (log !== nextLog) {
if (log) {
log.unsubscribe(this._update);
}
if (nextLog) {
nextLog.subscribe(this._update);
}
}
}
componentWillUnmount() {
const {log} = this.props;
if (log) {
log.unsubscribe(this._update);
}
}
_update = logVersion => {
this.setState({logVersion});
};
render() {
const {log, ...otherProps} = this.props;
const logState = log && getLogState(log, otherProps);
return <Component {...otherProps} {...logState} log={log} />;
}
}
return WrappedComponent;
}
该函数的作用是将LogState绑定到组件上来,其中采用了React的HOC高阶组件通过属性代理进行属性的绑定。
顺路看看HoverTooltip
在剖析Core3DViewer之前,我们先看看HoverTooltip,在./modules/core/src/components/hove-tooltip.js中,我们可以看到这个组件的定义。
const TooltipContainer = styled.div(props => ({
...props.theme.__reset__,
position: 'absolute',
pointerEvents: 'none',
margin: props.theme.spacingNormal,
padding: props.theme.spacingNormal,
maxWidth: 320,
overflow: 'hidden',
background: props.theme.background,
color: props.theme.textColorPrimary,
zIndex: 100001,
...evaluateStyle(props.userStyle, props)
}));
首先通过@emotion/styled来创建一个div,作为tooltip的容器。
render() {
const {theme, info, style, renderContent = this._renderContent} = this.props;
return (
<TooltipContainer theme={theme} style={{left: info.x, top: info.y}} userStyle={style}>
{renderContent(info)}
</TooltipContainer>
);
}
然后运用这个容器,并传递属性。
export default withTheme(HoverTooltip);
最后将器包装成ThemedComponent组件输出。
在 uber-web/monochrome我们可以看到withTheme的定义,如下:
export function withTheme(Component) {
class ThemedComponent extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{_theme => <Component {...this.props} theme={_theme} />}
</ThemeContext.Consumer>
);
}
}
ThemedComponent.propTypes = Component.propTypes;
ThemedComponent.defaultProps = Component.defaultProps;
return ThemedComponent;
}
该方法功能是让组件带上Theme主题,这样只要修改Theme就能直接修改前端组件的风格。
先写到这,以上只是个人见解,如若有不对地方,还请帮忙指出。
接下来会介绍重量级组件Core3DViewer,希望有研究的小伙伴一起交流。