让我们一起用谷歌地图和 React 实现一个类似 Uber 的汽车行驶功能——第一部分
折线
假设你是一名在 Uber 工作的工程师(除非你真的是 Uber 的工程师)。你的任务是制作一辆汽车在道路上行驶直至到达目的地的动画。你需要使用 React(Uber 的网页版就是用 React 开发的)。该怎么做呢?
我们的工具
在本指南中,我将使用 Create React App react-google-maps,它是 Google Maps 库的封装器,这样您就知道该怎么做了:
npm install react-google-maps
基本地图
我们先从一个基本的地图开始。可以这样初始化 Google Maps 库:
import React from 'react';
import { withGoogleMap, withScriptjs, GoogleMap } from 'react-google-maps'
class Map extends React.Component {
render = () => {
return (
<GoogleMap
defaultZoom={16}
defaultCenter={{ lat: 18.559008, lng: -68.388881 }}
>
</GoogleMap>
)
}
}
const MapComponent = withScriptjs(withGoogleMap(Map))
export default () => (
<MapComponent
googleMapURL="https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=geometry,drawing,places"
loadingElement={<div style={{ height: `100%` }} />}
containerElement={<div style={{ height: `400px`, width: '500px' }} />}
mapElement={<div style={{ height: `100%` }} />}
/>
)
我不会详细讲解初始化方法react-google-maps,而是重点讲解移动逻辑。如果你想了解如何设置,可以阅读他们的指南。
我主要使用的属性是defaultZoom`zoom`,它设置谷歌地图的缩放级别。缩放级别越高,地图就越接近地面;以及defaultCenter`geolocation`,它设置地图的主要地理位置。
这样应该就能在蓬塔卡纳环岛(离我家很近)加载一张基本地图了。
纬度和经度
在开始绘制地图之前,我们需要了解什么是纬度和经度。纬度和经度是表示地理位置的单位。纬度数值范围为 90 到 -90,其中 0 代表赤道;经度数值范围为 180 到 -180,其中 0 代表本初子午线。
基本上,纬度控制你的垂直位置,赤道位于中心;经度控制你的水平位置,本初子午线位于中心。
你其实不需要了解坐标的工作原理就能使用谷歌地图(感谢谷歌!)。谷歌提供了测量距离、计算物体朝向等工具,你只需要输入坐标即可。如果你想深入了解,可以阅读维基百科的相关条目。
标记
地图标记用于标识地图上的位置,通常使用我们熟知的表示位置的图标:
知道特定位置的经纬度,就可以在该位置放置标记。例如,我们可以像这样在环岛中央放置一个标记:
import React from 'react';
import { withGoogleMap, withScriptjs, GoogleMap, Marker } from 'react-google-maps'
class Map extends React.Component {
render = () => {
return (
<GoogleMap
defaultZoom={16}
defaultCenter={{ lat: 18.559008, lng: -68.388881 }}
>
<Marker position={{
lat: 18.559024,
lng: -68.388886,
}} />
</GoogleMap>
)
}
}
const MapComponent = withScriptjs(withGoogleMap(Map))
export default () => (
<MapComponent
googleMapURL="https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=geometry,drawing,places"
loadingElement={<div style={{ height: `100%` }} />}
containerElement={<div style={{ height: `400px`, width: '500px' }} />}
mapElement={<div style={{ height: `100%` }} />}
/>
)
折线
Polyline 组件根据传入的属性(一个坐标列表)在地图上绘制线条path。我们可以使用两个坐标(即线条的端点)绘制一条直线。
import React from "react";
import {
withGoogleMap,
withScriptjs,
GoogleMap,
Polyline
} from "react-google-maps";
class Map extends React.Component {
path = [
{ lat: 18.55996, lng: -68.388832 },
{ lat: 18.558028, lng: -68.388971 }
];
render = () => {
return (
<GoogleMap
defaultZoom={16}
defaultCenter={{ lat: 18.559008, lng: -68.388881 }}
>
<Polyline path={this.path} options={{ strokeColor: "#FF0000 " }} />
</GoogleMap>
);
};
}
const MapComponent = withScriptjs(withGoogleMap(Map));
export default () => (
<MapComponent
googleMapURL="https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=geometry,drawing,places"
loadingElement={<div style={{ height: `100%` }} />}
containerElement={<div style={{ height: `400px`, width: "500px" }} />}
mapElement={<div style={{ height: `100%` }} />}
/>
);
我们刚才在环岛上画了一条直线!不过我不建议开车的时候这样做。
那么曲线呢?很遗憾,曲线并不存在。它们只是由许多直线连接而成,让你产生曲线的错觉。只要放大到一定程度,它们就总是可见的。所以,让我们通过添加足够的坐标来“创造”一条曲线吧。
import React from "react";
import {
withGoogleMap,
withScriptjs,
GoogleMap,
Polyline
} from "react-google-maps";
class Map extends React.Component {
path = [
{ lat: 18.558908, lng: -68.389916 },
{ lat: 18.558853, lng: -68.389922 },
{ lat: 18.558375, lng: -68.389729 },
{ lat: 18.558032, lng: -68.389182 },
{ lat: 18.55805, lng: -68.388613 },
{ lat: 18.558256, lng: -68.388213 },
{ lat: 18.558744, lng: -68.387929 }
];
render = () => {
return (
<GoogleMap
defaultZoom={16}
defaultCenter={{ lat: 18.559008, lng: -68.388881 }}
>
<Polyline path={this.path} options={{ strokeColor: "#FF0000 " }} />
</GoogleMap>
);
};
}
const MapComponent = withScriptjs(withGoogleMap(Map));
export default () => (
<MapComponent
googleMapURL="https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=geometry,drawing,places"
loadingElement={<div style={{ height: `100%` }} />}
containerElement={<div style={{ height: `400px`, width: "500px" }} />}
mapElement={<div style={{ height: `100%` }} />}
/>
);
这就是绘制曲线的方法!通过添加更多坐标,我们可以使直线不那么明显。
动画
好戏开始了。让我们在终点添加一个标记path。这将代表我们的汽车以及它行驶的路径。
import React from "react";
import {
withGoogleMap,
withScriptjs,
GoogleMap,
Polyline,
Marker
} from "react-google-maps";
class Map extends React.Component {
path = [
{ lat: 18.558908, lng: -68.389916 },
{ lat: 18.558853, lng: -68.389922 },
{ lat: 18.558375, lng: -68.389729 },
{ lat: 18.558032, lng: -68.389182 },
{ lat: 18.55805, lng: -68.388613 },
{ lat: 18.558256, lng: -68.388213 },
{ lat: 18.558744, lng: -68.387929 }
];
render = () => {
return (
<GoogleMap
defaultZoom={16}
defaultCenter={{ lat: 18.559008, lng: -68.388881 }}
>
<Polyline path={this.path} options={{ strokeColor: "#FF0000 " }} />
<Marker position={this.path[this.path.length - 1]} />
</GoogleMap>
);
};
}
const MapComponent = withScriptjs(withGoogleMap(Map));
export default () => (
<MapComponent
googleMapURL="https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=geometry,drawing,places"
loadingElement={<div style={{ height: `100%` }} />}
containerElement={<div style={{ height: `400px`, width: "500px" }} />}
mapElement={<div style={{ height: `100%` }} />}
/>
);
现在我们需要准备动画的逻辑。我们会用到很多直线,需要把车放置在这些直线路径上。我们可以把逻辑分成四个步骤。
- 计算第一个点到每个坐标点的距离。这里假设路径中的坐标是有序的。
- 设定速度,并计算汽车在一段时间内行驶的距离。
- 利用计算距离,我们可以使用完整路径,得到汽车行驶的路径。
- 将汽车当前行驶的最后一段直线路径制作成动画。
计算距离
Google 为我们提供了计算两个坐标之间距离的工具。我们讨论的函数是:google.maps.geometry.spherical.computeDistanceBetween
我们可以在挂载组件之前执行此步骤。它会计算每个坐标到路径中第一个元素的距离:
componentWillMount = () => {
this.path = this.path.map((coordinates, i, array) => {
if (i === 0) {
return { ...coordinates, distance: 0 } // it begins here!
}
const { lat: lat1, lng: lng1 } = coordinates
const latLong1 = new window.google.maps.LatLng(lat1, lng1)
const { lat: lat2, lng: lng2 } = array[0]
const latLong2 = new window.google.maps.LatLng(lat2, lng2)
// in meters:
const distance = window.google.maps.geometry.spherical.computeDistanceBetween(
latLong1,
latLong2
)
return { ...coordinates, distance }
})
console.log(this.path)
}
设定速度并每秒计算距离。
现在进入物理部分。假设我们想让物体以每秒 5 米的速度运动。为此,我们需要初始时间和初始速度。让我们每秒用 console.log 记录这个距离。
velocity = 5
initialDate = new Date()
getDistance = () => {
// seconds between when the component loaded and now
const differentInTime = (new Date() - this.initialDate) / 1000 // pass to seconds
return differentInTime * this.velocity // d = v*t -- thanks Newton!
}
componentDidMount = () => {
this.interval = window.setInterval(this.consoleDistance, 1000)
}
componentWillUnmount = () => {
window.clearInterval(this.interval)
}
consoleDistance = () => {
console.log(this.getDistance())
}
这将通过 console.log 输出一个每秒递增 5 的数字,就像我们汽车的速度一样。
让我们把目前的进展汇总起来:
import React from 'react';
import { withGoogleMap, withScriptjs, GoogleMap, Polyline, Marker } from 'react-google-maps'
class Map extends React.Component {
path = [
{ lat: 18.558908, lng: -68.389916 },
{ lat: 18.558853, lng: -68.389922 },
{ lat: 18.558375, lng: -68.389729 },
{ lat: 18.558032, lng: -68.389182 },
{ lat: 18.558050, lng: -68.388613 },
{ lat: 18.558256, lng: -68.388213 },
{ lat: 18.558744, lng: -68.387929 },
]
velocity = 5
initialDate = new Date()
getDistance = () => {
// seconds between when the component loaded and now
const differentInTime = (new Date() - this.initialDate) / 1000 // pass to seconds
return differentInTime * this.velocity // d = v*t -- thanks Newton!
}
componentDidMount = () => {
this.interval = window.setInterval(this.consoleDistance, 1000)
}
componentWillUnmount = () => {
window.clearInterval(this.interval)
}
consoleDistance = () => {
console.log(this.getDistance())
}
componentWillMount = () => {
this.path = this.path.map((coordinates, i, array) => {
if (i === 0) {
return { ...coordinates, distance: 0 } // it begins here!
}
const { lat: lat1, lng: lng1 } = coordinates
const latLong1 = new window.google.maps.LatLng(lat1, lng1)
const { lat: lat2, lng: lng2 } = array[0]
const latLong2 = new window.google.maps.LatLng(lat2, lng2)
// in meters:
const distance = window.google.maps.geometry.spherical.computeDistanceBetween(
latLong1,
latLong2
)
return { ...coordinates, distance }
})
console.log(this.path)
}
render = () => {
return (
<GoogleMap
defaultZoom={16}
defaultCenter={{ lat: 18.559008, lng: -68.388881 }}
>
<Polyline path={this.path} options={{ strokeColor: "#FF0000 "}} />
<Marker position={this.path[this.path.length - 1]} />
</GoogleMap>
)
}
}
const MapComponent = withScriptjs(withGoogleMap(Map))
export default () => (
<MapComponent
googleMapURL="https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=geometry,drawing,places"
loadingElement={<div style={{ height: `100%` }} />}
containerElement={<div style={{ height: `400px`, width: '500px' }} />}
mapElement={<div style={{ height: `100%` }} />}
/>
)
实时渲染轨迹
现在我们需要实时渲染汽车。画面中有很多直线,汽车会停在两条直线之间。所以我们会将一些逻辑移到状态栏中,并每秒更新一次。
首先,让我们添加progress状态,并使折线和标记跟随该状态。
state = {
progress: [],
}
render = () => {
return (
<GoogleMap
defaultZoom={16}
defaultCenter={{ lat: 18.559008, lng: -68.388881 }}
>
{ this.state.progress && (
<>
<Polyline path={this.state.progress} options={{ strokeColor: "#FF0000 "}} />
<Marker position={this.state.progress[this.state.progress.length - 1]} />
</>
)}
</GoogleMap>
)
}
}
现在我们可以更改或consoleDistance提取moveObject汽车已经行驶过的路径部分:
componentDidMount = () => {
this.interval = window.setInterval(this.moveObject, 1000)
}
moveObject = () => {
const distance = this.getDistance()
if (! distance) {
return
}
const progress = this.path.filter(coordinates => coordinates.distance < distance)
this.setState({ progress })
}
综上所述,我们得到:
正如你所看到的,汽车会“跳动”,因为我们添加了已经经过的线条,但汽车位于最后一个元素progress和其余元素之间this.path。因此,为了使动画更流畅,我们需要知道汽车在这两条线之间的移动距离,然后找到它们之间的坐标。谷歌提供了一个函数来实现这一点,可以在[此处插入链接]找到google.maps.geometry.spherical.interpolate。
完成上述moveObject步骤后,我们得到:
moveObject = () => {
const distance = this.getDistance()
if (! distance) {
return
}
let progress = this.path.filter(coordinates => coordinates.distance < distance)
const nextLine = this.path.find(coordinates => coordinates.distance > distance)
if (! nextLine) {
this.setState({ progress })
return // it's the end!
}
const lastLine = progress[progress.length - 1]
const lastLineLatLng = new window.google.maps.LatLng(
lastLine.lat,
lastLine.lng
)
const nextLineLatLng = new window.google.maps.LatLng(
nextLine.lat,
nextLine.lng
)
// distance of this line
const totalDistance = nextLine.distance - lastLine.distance
const percentage = (distance - lastLine.distance) / totalDistance
const position = window.google.maps.geometry.spherical.interpolate(
lastLineLatLng,
nextLineLatLng,
percentage
)
progress = progress.concat(position)
this.setState({ progress })
}
现在看起来很光滑了!
我们的结果是:
import React from 'react';
import { withGoogleMap, withScriptjs, GoogleMap, Polyline, Marker } from 'react-google-maps'
class Map extends React.Component {
state = {
progress: [],
}
path = [
{ lat: 18.558908, lng: -68.389916 },
{ lat: 18.558853, lng: -68.389922 },
{ lat: 18.558375, lng: -68.389729 },
{ lat: 18.558032, lng: -68.389182 },
{ lat: 18.558050, lng: -68.388613 },
{ lat: 18.558256, lng: -68.388213 },
{ lat: 18.558744, lng: -68.387929 },
]
velocity = 5
initialDate = new Date()
getDistance = () => {
// seconds between when the component loaded and now
const differentInTime = (new Date() - this.initialDate) / 1000 // pass to seconds
return differentInTime * this.velocity // d = v*t -- thanks Newton!
}
componentDidMount = () => {
this.interval = window.setInterval(this.moveObject, 1000)
}
componentWillUnmount = () => {
window.clearInterval(this.interval)
}
moveObject = () => {
const distance = this.getDistance()
if (! distance) {
return
}
let progress = this.path.filter(coordinates => coordinates.distance < distance)
const nextLine = this.path.find(coordinates => coordinates.distance > distance)
if (! nextLine) {
this.setState({ progress })
return // it's the end!
}
const lastLine = progress[progress.length - 1]
const lastLineLatLng = new window.google.maps.LatLng(
lastLine.lat,
lastLine.lng
)
const nextLineLatLng = new window.google.maps.LatLng(
nextLine.lat,
nextLine.lng
)
// distance of this line
const totalDistance = nextLine.distance - lastLine.distance
const percentage = (distance - lastLine.distance) / totalDistance
const position = window.google.maps.geometry.spherical.interpolate(
lastLineLatLng,
nextLineLatLng,
percentage
)
progress = progress.concat(position)
this.setState({ progress })
}
componentWillMount = () => {
this.path = this.path.map((coordinates, i, array) => {
if (i === 0) {
return { ...coordinates, distance: 0 } // it begins here!
}
const { lat: lat1, lng: lng1 } = coordinates
const latLong1 = new window.google.maps.LatLng(lat1, lng1)
const { lat: lat2, lng: lng2 } = array[0]
const latLong2 = new window.google.maps.LatLng(lat2, lng2)
// in meters:
const distance = window.google.maps.geometry.spherical.computeDistanceBetween(
latLong1,
latLong2
)
return { ...coordinates, distance }
})
console.log(this.path)
}
render = () => {
return (
<GoogleMap
defaultZoom={16}
defaultCenter={{ lat: 18.559008, lng: -68.388881 }}
>
{ this.state.progress && (
<>
<Polyline path={this.state.progress} options={{ strokeColor: "#FF0000 "}} />
<Marker position={this.state.progress[this.state.progress.length - 1]} />
</>
)}
</GoogleMap>
)
}
}
const MapComponent = withScriptjs(withGoogleMap(Map))
export default () => (
<MapComponent
googleMapURL="https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=geometry,drawing,places"
loadingElement={<div style={{ height: `100%` }} />}
containerElement={<div style={{ height: `400px`, width: '500px' }} />}
mapElement={<div style={{ height: `100%` }} />}
/>
)
现在我们只需要调整路径和速度,就能让它看起来更完美。具体调整内容会根据路线和驾驶员的不同而有所变化。
利用这款强大的工具生成的更优路径,以 100 公里/小时的速度行驶,我们得到:
在第二部分中,我们将自定义汽车图标,使其朝向行驶方向!
有任何问题请告诉我哦 :D
文章来源:https://dev.to/zerquix18/let-s-play-with-google-maps-and-react-making-a-car-move-through-the-road-like-on-uber-part-1-4eo0
