创建和显示连续行程

背靠背行程是指一项接连发生的排他性独立行程,驾驶员承诺在完成当前行程之前接下一个消费者的行程。

背靠背行程与单目的地行程之间的主要区别在于,对于连续行程,行程运营者可以将行程分配给已分配行程的车辆。

本教程将引导您完成创建背靠背行程的过程。还展示了如何将该行程与您的消费者应用集成,以便您的客户可以通过他们的手机直观地查看行程进度。您可以使用 Consumer SDK 进行此集成。

前提条件

如需完成本教程,请务必完成以下操作:

  1. 设置 Fleet Engine。如需了解详情,请参阅 Fleet Engine:初始设置

  2. 将您的应用与驱动程序 SDK 集成。如需了解详情,请参阅适用于 Android 的初始化驱动程序 SDK 和适用于 iOS 的驱动程序 SDK 集成指南

  3. 将面向消费者的应用与消费者 SDK 集成。如需了解详情,请参阅适用于 Android 的消费者 SDK 使用入门和适用于 iOS 的消费者 SDK 使用入门

  4. 设置授权令牌。如需详细了解授权令牌,请参阅 Fleet Engine 使用入门指南中的创建 JSON Web 令牌以进行授权,以及 Fleet Engine 消费者 SDK 文档中的身份验证和授权

第 1 步:在 Fleet Engine 中创建车辆

车辆是代表车队中车辆的对象。您必须在 Fleet Engine 中创建这些集群,才能在消费者应用中跟踪它们。

您可以使用以下两种方法之一创建车辆:

gRPC
使用 CreateVehicleRequest 请求消息调用 CreateVehicle() 方法。您必须拥有 Fleet Engine 超级用户权限才能调用 CreateVehicle()
REST
调用 https://fleetengine.googleapis.com/v1/providers.vehicles.create

注意事项

制作车辆时需注意以下事项。

  • 请务必将初始车辆状态设置为 OFFLINE。这样可确保 Fleet Engine 能够发现您的车辆,以便进行行程匹配。

  • 车辆的 provider_id 必须与 Google Cloud 项目(其中包含用于调用 Fleet Engine 的服务帐号)的项目 ID 相同。虽然多个服务帐号可以访问同一拼车提供商的 Fleet Engine,但 Fleet Engine 目前不支持来自访问同一车辆的不同 Google Cloud 项目的服务帐号。

  • CreateVehicle() 返回的响应包含 Vehicle 实例。如果您尚未使用 UpdateVehicle() 更新实例,则七天后将将其删除。您应在调用 CreateVehicle() 之前调用 GetVehicle(),只是为了确认车辆尚不存在。如果 GetVehicle() 返回 NOT_FOUND 错误,则应继续调用 CreateVehicle()。如需了解详情,请参阅车辆及其生命周期

示例

以下提供程序代码示例演示了如何在 Fleet Engine 中创建车辆。

static final String PROJECT_ID = "my-rideshare-co-gcp-project";

VehicleServiceBlockingStub vehicleService = VehicleService.newBlockingStub(channel);

String parent = "providers/" + PROJECT_ID;

Vehicle vehicle = Vehicle.newBuilder()
    .setVehicleState(VehicleState.OFFLINE)  // Initial state
    .addSupportedTripTypes(TripType.EXCLUSIVE)
    .setMaximumCapacity(4)
    .setVehicleType(VehicleType.newBuilder().setCategory(VehicleType.Category.AUTO))
    .build();

CreateVehicleRequest createVehicleRequest = CreateVehicleRequest.newBuilder()
    .setParent(parent)
    .setVehicleId("8241890")  // Vehicle ID assigned by solution provider.
    .setVehicle(vehicle)      // Initial state.
    .build();

// The Vehicle is created in the OFFLINE state, and no initial position is
// provided.  When the driver app calls the rideshare provider, the state can be
// set to ONLINE, and the driver app updates the vehicle location.
try {
  Vehicle createdVehicle = vehicleService.createVehicle(createVehicleRequest);
} catch (StatusRuntimeException e) {
  Status s = e.getStatus();
  switch (s.getCode()) {
    case ALREADY_EXISTS:
      break;
    case PERMISSION_DENIED:
      break;
  }
  return;
}

如需创建支持“背靠背”行程的 Vehicle,您必须在传递给 CreateVehicleRequestVehicle 对象中将 backToBackEnabled 字段设置为 true

Vehicle vehicle = Vehicle.newBuilder()
    .setVehicleState(VehicleState.OFFLINE)
    .addSupportedTripTypes(TripType.EXCLUSIVE)
    .setMaximumCapacity(4)
    .setVehicleType(VehicleType.newBuilder().setCategory(VehicleType.Category.AUTO))
    .setBackToBackEnabled(true) // Set as 'true' so vehicle can be assigned back-to-back trips.
    .build();

第 2 步:启用位置跟踪

位置跟踪是指在行程中跟踪车辆的位置,驾驶员应用会向 Fleet Engine 发送遥测数据,其中包含车辆的当前位置。这种不断更新的位置信息流用于传达车辆沿行程路线的进度。启用位置跟踪后,驾驶员应用开始以默认频率(每 5 秒一次)发送此遥测数据。

您可以按以下步骤为 Android 和 iOS 启用位置跟踪:

示例

以下代码示例演示了如何启用位置跟踪。

Java

RidesharingVehicleReporter vehicleReporter = ...;

vehicleReporter.enableLocationTracking();

Kotlin

val vehicleReporter = ...

vehicleReporter.enableLocationTracking()

Swift

vehicleReporter.locationTrackingEnabled = true

Objective-C

_vehicleReporter.locationTrackingEnabled = YES;

第 3 步:将车辆的状态设为在线

您可以通过将车辆的状态设置为 online 来使车辆投入服务(即使其可供使用),但您必须先启用位置信息跟踪,然后才能执行此操作。

对于 Android 和 iOS,您可以将车辆的状态设置为在线,如下所示:

示例

以下代码示例演示了如何将车辆的状态设置为 ONLINE

Java

vehicleReporter.setVehicleState(VehicleState.ONLINE);

Kotlin

vehicleReporter.setVehicleState(VehicleState.ONLINE)

Swift

vehicleReporter.update(.online)

Objective-C

[_vehicleReporter updateVehicleState:GMTDVehicleStateOnline];

第 4 步:在 Fleet Engine 中创建行程

如需创建背靠背的行程,您可以创建一个 Trip 对象,就像创建单目的地行程一样。

行程是一个表示行程的对象,它是一个地理坐标点集合,包括出发地、航点和下车点。您必须为每个行程请求创建一个 Trip 对象,以便将请求与车辆匹配,然后进行跟踪。

提供必需属性

若要创建背靠背行程,您必须提供以下字段:

parent
一个包含提供方 ID 的字符串。此值必须与用于调用 Fleet Engine 的服务账号所属 Google Cloud 项目的项目 ID 相同
trip_id
您创建的字符串,用于唯一标识此行程。
trip
要创建的 Trip 对象。

必须在传递给 CreateTripRequestTrip 对象中设置以下字段:

trip_type
TripType.EXCLUSIVE
pickup_point
行程的出发地。
dropoff_point
行程的下车点。创建行程时,此字段不是必填字段,稍后可通过调用 UpdateTrip 进行设置。

示例

以下后端集成示例演示了如何创建行程并将其分配给车辆。

// A vehicle with ID 'my-vehicle' is already created and it is assigned to a trip with ID 'current-trip'.

static final String PROJECT_ID = "my-rideshare-co-gcp-project";
static final String VEHICLE_ID =" my-vehicle";
static final String TRIP_ID = "back-to-back-trip");

TripServiceBlockingStub tripService = TripService.newBlockingStub(channel);

String parent = "providers/" + PROJECT_ID;

Trip trip = Trip.newBuilder()
    .setTripType(TripType.EXCLUSIVE)
    .setPickupPoint(
        TerminalLocation.newBuilder().setPoint(
            LatLng.newBuilder()
                .setLatitude(-6.195139).setLongitude(106.820826)))
    .setDropoffPoint(
        TerminalLocation.newBuilder().setPoint(
            LatLng.newBuilder()
                .setLatitude(-6.1275).setLongitude(106.6537)))
    .setVehicleId(VEHICLE_ID)
    .build();

// Create trip request
CreateTripRequest createTripRequest = CreateTripRequest.newBuilder()
    .setParent(parent)
    .setTripId(TRIP_ID)
    .setTrip(trip)
    .build();

// Error handling.
try {
  // Fleet Engine automatically assigns a 'waypoints' list to the trip containing
  // the vehicle's current trip waypoints.
  Trip createdTrip =
      tripService.createTrip(createTripRequest);
} catch (StatusRuntimeException e) {
  Status s = e.getStatus();
  switch (s.getCode()) {
    case ALREADY_EXISTS:
      break;
    case PERMISSION_DENIED:
      break;
  }
  return;
}

第 5 步:使用车辆 ID 和航点更新行程

您必须使用车辆 ID 配置行程,以便 Fleet Engine 可以沿路线跟踪车辆。对于背靠背模式,即使已向车辆分配行程,也仍会将行程分配给相应车辆。

将行程分配给车辆后,Fleet Engine 会自动将与连续行程关联的航点添加到车辆的航点字段。行程的 remainingWaypoints 字段包含将在行程中途下车之前访问的所有航点的列表,包括并发行程中的航点。

例如,假设有两个连续的行程:行程 A行程 B。车辆已接走行程 A 的消费者,在前往下车点的途中,司机会收到为下一行程(行程 B)接其他消费者的请求。

  • 调用 getVehicle() 会返回剩余航点 (remainingWaypoints) 的列表,其中包含
    下车点B 上车B 下车
  • getTrip()行程 AonTripRemainingWaypointsUpdated 回调会返回包含“离开”事件的剩余航点 (remainingWaypoints) 列表。
  • getTrip()行程 BonTripRemainingWaypointsUpdated 回调会返回其余航点 (remainingWaypoints) 的列表,其中包含 A Drop-offB PickupB Drop-off

第 6 步:在消费者应用中监听行程更新

  • 对于 Android,您可以通过从 TripModelManager 获取 TripModel 对象并注册 TripModelCallback 监听器,来监听行程的数据更新。

  • 对于 iOS,您可以通过从 GMTCTripService 获取 GMTCTripModel 对象并注册 GMTCTripModelSubscriber 订阅者来监听行程的数据更新。

借助 TripModelCallback 监听器和 GMTCTripModelSubscriber 订阅者,您的应用可在每次刷新时根据自动刷新间隔接收定期行程进度更新。只有发生变化的值才能触发回调。否则,回调将保持静默状态。

无论数据发生怎样的变化,系统始终会调用 TripModelCallback.onTripUpdated()tripModel(_:didUpdate:updatedPropertyFields:) 方法。

示例 1

以下代码示例演示了如何从 TripModelManager/GMTCTripService 获取 TripModel 并在其上设置监听器。

Java

// Start journey sharing after a trip has been created via Fleet Engine.
TripModelManager tripModelManager = consumerApi.getTripModelManager();

// Get a TripModel object.
TripModel tripModel = tripModelManager.getTripModel(tripName);

// Register a listener on the trip.
TripModelCallback tripCallback = new TripModelCallback() {
  ...
};
tripModel.registerTripCallback(tripCallback);

// Set the refresh interval.
TripModelOptions tripModelOptions = TripModelOptions.builder()
    .setRefreshInterval(5000) // interval in milliseconds, so 5 seconds
    .build();
tripModel.setTripModelOptions(tripModelOptions);

// The trip stops auto-refreshing when all listeners are unregistered.
tripModel.unregisterTripCallback(tripCallback);

Kotlin

// Start journey sharing after a trip has been created via Fleet Engine.
val tripModelManager = consumerApi.getTripModelManager()

// Get a TripModel object.
val tripModel = tripModelManager.getTripModel(tripName)

// Register a listener on the trip.
val tripCallback = TripModelCallback() {
  ...
}

tripModel.registerTripCallback(tripCallback)

// Set the refresh interval.
val tripModelOptions =
  TripModelOptions.builder()
    .setRefreshInterval(5000) // interval in milliseconds, so 5 seconds
    .build()

tripModel.setTripModelOptions(tripModelOptions)

// The trip stops auto-refreshing when all listeners are unregistered.
tripModel.unregisterTripCallback(tripCallback)

Swift

let tripService = GMTCServices.shared().tripService

// Create a tripModel instance for listening for updates from the trip
// specified by the trip name.
let tripModel = tripService.tripModel(forTripName: tripName)

// Register for the trip update events.
tripModel.register(self)

// Set the refresh interval (in seconds).
tripModel.options.autoRefreshTimeInterval = 5

// Unregister for the trip update events.
tripModel.unregisterSubscriber(self)

Objective-C

GMTCTripService *tripService = [GMTCServices sharedServices].tripService;

// Create a tripModel instance for listening for updates from the trip
// specified by the trip name.
GMTCTripModel *tripModel = [tripService tripModelForTripName:tripName];

// Register for the trip update events.
[tripModel registerSubscriber:self];

// Set the refresh interval (in seconds).
tripModel.options.autoRefreshTimeInterval = 5;

// Unregister for the trip update events.
[tripModel unregisterSubscriber:self];

示例 2

以下代码示例演示了如何设置 TripModelCallback 监听器和 GMTCTripModelSubscriber 订阅者。

Java

// Implements a callback for the trip model so your app can listen for trip
// updates from Fleet Engine.
TripModelCallback subscriber =
  new TripModelCallback() {

    @Override
    public void onTripStatusUpdated(TripInfo tripInfo, @TripStatus int status) {
      // ...
    }

    @Override
    public void onTripActiveRouteUpdated(TripInfo tripInfo, List<LatLng> route) {
      // ...
    }

    @Override
    public void onTripVehicleLocationUpdated(
        TripInfo tripInfo, @Nullable VehicleLocation vehicleLocation) {
      // ...
    }

    @Override
    public void onTripPickupLocationUpdated(
        TripInfo tripInfo, @Nullable TerminalLocation pickup) {
      // ...
    }

    @Override
    public void onTripPickupTimeUpdated(TripInfo tripInfo, @Nullable Long timestampMillis) {
      // ...
    }

    @Override
    public void onTripDropoffLocationUpdated(
        TripInfo tripInfo, @Nullable TerminalLocation dropoff) {
      // ...
    }

    @Override
    public void onTripDropoffTimeUpdated(TripInfo tripInfo, @Nullable Long timestampMillis) {
      // ...
    }

    @Override
    public void onTripETAToNextWaypointUpdated(
        TripInfo tripInfo, @Nullable Long timestampMillis) {
      // ...
    }

    @Override
    public void onTripActiveRouteRemainingDistanceUpdated(
        TripInfo tripInfo, @Nullable Integer distanceMeters) {
      // ...
    }

    @Override
    public void onTripUpdateError(TripInfo tripInfo, TripUpdateError error) {
      // ...
    }

    @Override
    public void onTripUpdated(TripInfo tripInfo) {
      // ...
    }

    @Override
    public void onTripRemainingWaypointsUpdated(
        TripInfo tripInfo, List<TripWaypoint> waypointList) {
      // ...
    }

    @Override
    public void onTripIntermediateDestinationsUpdated(
        TripInfo tripInfo, List<TerminalLocation> intermediateDestinations) {
      // ...
    }

    @Override
    public void onTripRemainingRouteDistanceUpdated(
        TripInfo tripInfo, @Nullable Integer distanceMeters) {
      // ...
    }

    @Override
    public void onTripRemainingRouteUpdated(TripInfo tripInfo, List<LatLng> route) {
      // ...
    }
  };

Kotlin

// Implements a callback for the trip model so your app can listen for trip
// updates from Fleet Engine.
val subscriber =
  object : TripModelCallback() {
    override fun onTripStatusUpdated(tripInfo: TripInfo, status: @TripStatus Int) {
      // ...
    }

    override fun onTripActiveRouteUpdated(tripInfo: TripInfo, route: List<LatLng>) {
      // ...
    }

    override fun onTripVehicleLocationUpdated(
      tripInfo: TripInfo,
      vehicleLocation: VehicleLocation?
    ) {
      // ...
    }

    override fun onTripPickupLocationUpdated(tripInfo: TripInfo, pickup: TerminalLocation?) {
      // ...
    }

    override fun onTripPickupTimeUpdated(tripInfo: TripInfo, timestampMillis: Long?) {
      // ...
    }

    override fun onTripDropoffLocationUpdated(tripInfo: TripInfo, dropoff: TerminalLocation?) {
      // ...
    }

    override fun onTripDropoffTimeUpdated(tripInfo: TripInfo, timestampMillis: Long?) {
      // ...
    }

    override fun onTripETAToNextWaypointUpdated(tripInfo: TripInfo, timestampMillis: Long?) {
      // ...
    }

    override fun onTripActiveRouteRemainingDistanceUpdated(
      tripInfo: TripInfo,
      distanceMeters: Int?
    ) {
      // ...
    }

    override fun onTripUpdateError(tripInfo: TripInfo, error: TripUpdateError) {
      // ...
    }

    override fun onTripUpdated(tripInfo: TripInfo) {
      // ...
    }

    override fun onTripRemainingWaypointsUpdated(
      tripInfo: TripInfo,
      waypointList: List<TripWaypoint>
    ) {
      // ...
    }

    override fun onTripIntermediateDestinationsUpdated(
      tripInfo: TripInfo,
      intermediateDestinations: List<TerminalLocation>
    ) {
      // ...
    }

    override fun onTripRemainingRouteDistanceUpdated(tripInfo: TripInfo, distanceMeters: Int?) {
      // ...
    }

    override fun onTripRemainingRouteUpdated(tripInfo: TripInfo, route: List<LatLng>) {
      // ...
    }
  }

Swift

class TripModelSubscriber: NSObject, GMTCTripModelSubscriber {

  func tripModel(_: GMTCTripModel, didUpdate trip: GMTSTrip?, updatedPropertyFields: GMTSTripPropertyFields) {
    // Update the UI with the new `trip` data.
    updateUI(with: trip)
    ...
  }

  func tripModel(_: GMTCTripModel, didUpdate tripStatus: GMTSTripStatus) {
    // Handle trip status did change.
  }

  func tripModel(_: GMTCTripModel, didUpdateActiveRoute activeRoute: [GMTSLatLng]?) {
    // Handle trip active route did update.
  }

  func tripModel(_: GMTCTripModel, didUpdate vehicleLocation: GMTSVehicleLocation?) {
    // Handle vehicle location did update.
  }

  func tripModel(_: GMTCTripModel, didUpdatePickupLocation pickupLocation: GMTSTerminalLocation?) {
    // Handle pickup location did update.
  }

  func tripModel(_: GMTCTripModel, didUpdateDropoffLocation dropoffLocation: GMTSTerminalLocation?) {
    // Handle drop off location did update.
  }

  func tripModel(_: GMTCTripModel, didUpdatePickupETA pickupETA: TimeInterval) {
    // Handle the pickup ETA did update.
  }

  func tripModel(_: GMTCTripModel, didUpdateDropoffETA dropoffETA: TimeInterval) {
    // Handle the drop off ETA did update.
  }

  func tripModel(_: GMTCTripModel, didUpdateRemaining remainingWaypoints: [GMTSTripWaypoint]?) {
    // Handle updates to the pickup, dropoff or intermediate destinations of the trip.
  }

  func tripModel(_: GMTCTripModel, didFailUpdateTripWithError error: Error?) {
    // Handle the error.
  }

  func tripModel(_: GMTCTripModel, didUpdateIntermediateDestinations intermediateDestinations: [GMTSTerminalLocation]?) {
    // Handle the intermediate destinations being updated.
  }

  ...
}

Objective-C

@interface TripModelSubscriber : NSObject <GMTCTripModelSubscriber>
@end

@implementation TripModelSubscriber

- (void)tripModel:(GMTCTripModel *)tripModel
            didUpdateTrip:(nullable GMTSTrip *)trip
    updatedPropertyFields:(GMTSTripPropertyFields)updatedPropertyFields {
  // Update the UI with the new `trip` data.
  [self updateUIWithTrip:trip];
  ...
}

- (void)tripModel:(GMTCTripModel *)tripModel didUpdateTripStatus:(enum GMTSTripStatus)tripStatus {
  // Handle trip status did change.
}

- (void)tripModel:(GMTCTripModel *)tripModel
    didUpdateActiveRoute:(nullable NSArray<GMTSLatLng *> *)activeRoute {
  // Handle trip route did update.
}

- (void)tripModel:(GMTCTripModel *)tripModel
    didUpdateVehicleLocation:(nullable GMTSVehicleLocation *)vehicleLocation {
  // Handle vehicle location did update.
}

- (void)tripModel:(GMTCTripModel *)tripModel
    didUpdatePickupLocation:(nullable GMTSTerminalLocation *)pickupLocation {
  // Handle pickup location did update.
}

- (void)tripModel:(GMTCTripModel *)tripModel
    didUpdateDropoffLocation:(nullable GMTSTerminalLocation *)dropoffLocation {
  // Handle drop off location did update.
}

- (void)tripModel:(GMTCTripModel *)tripModel didUpdatePickupETA:(NSTimeInterval)pickupETA {
  // Handle the pickup ETA did update.
}

- (void)tripModel:(GMTCTripModel *)tripModel
    didUpdateRemainingWaypoints:(nullable NSArray<GMTSTripWaypoint *> *)remainingWaypoints {
  // Handle updates to the pickup, dropoff or intermediate destinations of the trip.
}

- (void)tripModel:(GMTCTripModel *)tripModel didUpdateDropoffETA:(NSTimeInterval)dropoffETA {
  // Handle the drop off ETA did update.
}

- (void)tripModel:(GMTCTripModel *)tripModel didFailUpdateTripWithError:(nullable NSError *)error {
  // Handle the error.
}

- (void)tripModel:(GMTCTripModel *)tripModel
    didUpdateIntermediateDestinations:
        (nullable NSArray<GMTSTerminalLocation *> *)intermediateDestinations {
  // Handle the intermediate destinations being updated.
}
…
@end

您可以随时访问行程信息,具体操作步骤如下:

第 7 步:在消费者应用中显示历程

您可以按如下方式访问 Rides 和 Deliveries 界面元素 API:

示例

以下代码示例演示了如何开始体验共享界面。

Java

JourneySharingSession session = JourneySharingSession.createInstance(tripModel);
consumerController.showSession(session);

Kotlin

val session = JourneySharingSession.createInstance(tripModel)
consumerController.showSession(session)

Swift

let journeySharingSession = GMTCJourneySharingSession(tripModel: tripModel)
mapView.show(journeySharingSession)

Objective-C

GMTCJourneySharingSession *journeySharingSession =
    [[GMTCJourneySharingSession alloc] initWithTripModel:tripModel];
[self.mapView showMapViewSession:journeySharingSession];

示例场景

假设有一辆背靠背的行程,在这种行程中,车辆已经接上了首次行程的消费者。当前行程完成时,如果驾驶员收到接其他消费者的请求,车辆正在前往下车点。





此时,首次行程的消费者会看到以下内容。





在同一时间点,第二次行程的消费者看到以下内容。





默认情况下,Consumer SDK 仅显示路线中的活动路程,但您可以选择显示其余路程(包括目的地)。

如果要显示其他行程中的航点信息,您可以按如下方式访问与行程相关的所有航点:

第 8 步:在 Fleet Engine 中管理行程状态

您可以使用其中一个 TripStatus 枚举值指定行程状态。当行程的状态发生变化(例如,从 ENROUTE_TO_PICKUP 更改为 ARRIVED_AT_PICKUP)时,您必须通过 Fleet Engine 更新行程状态。行程状态始终以 NEW 值开始,以 COMPLETECANCELED 值结束。如需了解详情,请参阅 trip_status

示例

以下后端集成示例演示了如何更新 Fleet Engine 中的行程状态。

static final String PROJECT_ID = "my-rideshare-co-gcp-project";
static final String TRIP_ID = "trip-8241890";

String tripName = "providers/" + PROJECT_ID + "/trips/" + TRIP_ID;

TripServiceBlockingStub tripService = TripService.newBlockingStub(channel);

// Trip settings to be updated.
Trip trip = Trip.newBuilder()
    .setTripStatus(TripStatus.ARRIVED_AT_PICKUP)
    .build();

// Trip update request
UpdateTripRequest updateTripRequest = UpdateTripRequest.newBuilder()
    .setName(tripName)
    .setTrip(trip)
    .setUpdateMask(FieldMask.newBuilder().addPaths("trip_status"))
    .build();

// Error handling.
try {
  Trip updatedTrip = tripService.updateTrip(updateTripRequest);
} catch (StatusRuntimeException e) {
  Status s = e.getStatus();
  switch (s.getCode()) {
    case NOT_FOUND:            // The trip doesn't exist.
      break;
    case FAILED_PRECONDITION:  // The given trip status is invalid.
      break;
    case PERMISSION_DENIED:
      break;
  }
  return;
}