发布于 2026-01-06 2 阅读
0

让我们一起用谷歌地图和 React 玩转汽车地图——让汽车像 Uber 一样在道路上行驶——第一部分:折线

让我们一起用谷歌地图和 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%` }} />}
  />
)


Enter fullscreen mode Exit fullscreen mode

我不会详细讲解初始化方法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%` }} />}
  />
)



Enter fullscreen mode Exit fullscreen mode

折线

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%` }} />}
  />
);



Enter fullscreen mode Exit fullscreen mode

我们刚才在环岛上画了一条直线!不过我不建议开车的时候这样做。

那么曲线呢?很遗憾,曲线并不存在。它们只是由许多直线连接而成,让你产生曲线的错觉。只要放大到一定程度,它们就总​​是可见的。所以,让我们通过添加足够的坐标来“创造”一条曲线吧。



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%` }} />}
  />
);



Enter fullscreen mode Exit fullscreen mode

这就是绘制曲线的方法!通过添加更多坐标,我们可以使直线不那么明显。

动画

好戏开始了。让我们在终点添加一个标记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%` }} />}
  />
);



Enter fullscreen mode Exit fullscreen mode

现在我们需要准备动画的逻辑。我们会用到很多直线,需要把车放置在这些直线路径上。我们可以把逻辑分成四个步骤。

  1. 计算第一个点到每个坐标点的距离。这里假设路径中的坐标是有序的
  2. 设定速度,并计算汽车在一段时间内行驶的距离。
  3. 利用计算距离,我们可以使用完整路径,得到汽车行驶的路径。
  4. 将汽车当前行驶的最后一段直线路径制作成动画。

计算距离

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)
  }



Enter fullscreen mode Exit fullscreen mode

设定速度并每秒计算距离。

现在进入物理部分。假设我们想让物体以每秒 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())
  }


Enter fullscreen mode Exit fullscreen mode

这将通过 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%` }} />}
  />
)



Enter fullscreen mode Exit fullscreen mode

实时渲染轨迹

现在我们需要实时渲染汽车。画面中有很多直线,汽车会停在两条直线之间。所以我们会将一些逻辑移到状态栏中,并每秒更新一次。

首先,让我们添加progress状态,并使折线和标记跟随该状态。



  state = {
    progress: [],
  }


Enter fullscreen mode Exit fullscreen mode


  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>
    )
  }
}


Enter fullscreen mode Exit fullscreen mode

现在我们可以更改或consoleDistance提取moveObject汽车已经行驶过的路径部分:



  componentDidMount = () => {
    this.interval = window.setInterval(this.moveObject, 1000)
  }


Enter fullscreen mode Exit fullscreen mode


  moveObject = () => {
    const distance = this.getDistance()
    if (! distance) {
      return
    }
    const progress = this.path.filter(coordinates => coordinates.distance < distance)
    this.setState({ progress })
  }


Enter fullscreen mode Exit fullscreen mode

综上所述,我们得到:

正如你所看到的,汽车会“跳动”,因为我们添加了已经经过的线条,但汽车位于最后一个元素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 })
  }


Enter fullscreen mode Exit fullscreen mode

现在看起来很光滑了!

我们的结果是:



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%` }} />}
  />
)



Enter fullscreen mode Exit fullscreen mode

现在我们只需要调整路径和速度,就能让它看起来更完美。具体调整内容会根据路线和驾驶员的不同而有所变化。

利用这款强大的工具生成的更优路径,以 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