aboutsummaryrefslogtreecommitdiff
path: root/gtfs-realtime-book/ch-07-consuming-vehicle-positions.md
blob: 1b745d064b71292425599423d9fd3a6ed777fc1c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
## 7. Consuming Vehicle Positions

Just like when consuming service alerts, you can loop over the
`FeedEntity` objects returned from `getEntityList()` to process
vehicle positions. If an entity contains a vehicle position, you can
retrieve it using the `getVehicle()` method.

```java
for (FeedEntity entity : fm.getEntityList()) {
    if (entity.hasAlert()) {
        VehiclePosition vp = entity.getVehicle();
        processVehiclePosition(vp);
    }
}
```

You can then process the returned `VehiclePosition` object to extract
the details of the vehicle position.

### Timestamp

One of the provided values is a timestamp reading of when the vehicle
position reading was taken.

```java
if (vp.hasTimestamp()) {
    Date timestamp = new Date(vp.getTimestamp() * 1000);

}
```

***Note:** The value is multiplied by 1,000 because the java.util.Date
class accepts milliseconds, whereas GTFS-realtime uses whole seconds.*

This value is useful because the age of a reading can dictate how the
data is interpreted. For example, if your latest reading was only thirty
seconds earlier, your users would realize it is very recent and
therefore is probably quite accurate. On the other hand, if the latest
reading was ten minutes earlier, they would see it had not updated
recently and may therefore not be completely accurate.

The other way this value is useful is for determining whether to store
this new vehicle position. If your previous reading for the same vehicle
has the same timestamp, you can ignore this update, as nothing has
changed.

### Geographic Location

A `VehiclePosition` object contains a `Position` object, which
contains a vehicle's latitude and longitude, and may also include other
useful information such as its bearing and speed.

```java
public static void processVehiclePosition(VehiclePosition vp) {
    if (vp.hasPosition()) {
        Position position = vp.getPosition();

        if (position.hasLatitude() && position.hasLongitude()) {
            float latitude = position.getLatitude();
            float longitude = position.getLongitude();

            // ...
        }

        // ...
    }
}
```

Even though the `Position` element of a `VehiclePosition` is
required (according to the specification), checking for it explicitly
means you can handle its omission gracefully. Remember: when consuming
GTFS-realtime data you are likely to be relying on a third-party data
provider who may or may not follow the specification correctly.

Likewise, the latitude and longitude are also required, but it is still
prudent to ensure they are included. These values are treated as any
floating-point numbers, so technically they may not be valid geographic
coordinates.

A basic check to ensure the coordinates are valid is to ensure the
latitude is between -90 and 90 and the longitude is between -180 and
180.

```java
float latitude = position.getLatitude();
float longitude = position.getLongitude();

if (Math.abs(latitude) <= 90 && Math.abs(longitude) <= 180) {
  // Valid coordinate

}
else {
  // Invalid coordinate
  
}
```

A more advanced check would be to determine a bounding box of the data
provider's entire public transportation network from the corresponding
GTFS feed's `stops.txt`. You would then check that all received
coordinates are within or near the bounding box.

***Note:** The important lesson to take from this is that a GTFS-realtime
feed may appear to adhere to the specification, but you should still
perform your own sanity checks on received data.*

In addition to the latitude and longitude, you can also retrieve a
vehicle's speed, bearing and odometer reading.

```java
if (position.hasBearing()) {
  float bearing = position.getBearing();

  // Degrees from 0-359. 0 is North, 90 is East, 180 is South, 270 is West

}

if (position.hasOdometer()) {
  double odometer = position.getOdometer();

  // Meters
}

if (position.hasSpeed()) {
  float speed = position.getSpeed();
  
  // Meters per second
}
```

### Trip Information

In order to associate a vehicle position with a particular trip from the
corresponding GTFS feed, vehicle positions may include a trip
descriptor.

***Note:** The trip descriptor is declared as optional in the
GTFS-realtime specification. Realistically, it will be hard to provide
value to end-users without knowing which trip the position corresponds
to. At the very least, you would need to know the route (which can be
specified via the trip descriptor).*

Just like with service alerts (covered in the previous chapter), there
are a number of values you can retrieve from a trip descriptor to
determine which trip a vehicle position belongs to. The following
listing demonstrates how to access this data:

```java
if (vp.hasTrip()) {
  TripDescriptor trip = vp.getTrip();

  if (trip.hasTripId()) {
    String tripId = trip.getTripId();

  }

  if (trip.hasRouteId()) {
    String routeId = trip.getRouteId();

  }

  if (trip.hasStartDate()) {
    String startDate = trip.getStartDate();
    
  }

  if (trip.hasStartTime()) {
    String startTime = trip.getStartTime();

  }

  if (trip.hasScheduleRelationship()) {
    ScheduleRelationship sr = trip.getScheduleRelationship();

  }
}
```

### Vehicle Identifiers

There are a number of values available in a vehicle position entity by
which to identify a vehicle. You can access an internal identifier (not
for public display), a label (such as a vehicle number painted on to a
vehicle), or a license plate, as shown in the following listing:

```java
if (vp.hasVehicle()) {
  VehicleDescriptor vehicle = vp.getVehicle();

  if (vehicle.hasId()) {
    String id = vehicle.getId();

  }

  if (vehicle.hasLabel()) {
    String label = vehicle.getLabel();

  }

  if (vehicle.hasLicensePlate()) {
    String licensePlate = vehicle.getLicensePlate();

  }
}
```

The vehicle descriptor and the values contained within are all optional.
In the case where this information is not available, you can use the
trip descriptor provided with each vehicle position to match up vehicle
positions across multiple updates.

Being able to match up the trip and/or vehicle reliably over subsequent
updates allows you reliably track the ongoing position changes for a
particular vehicle. For instance, if you wanted to animate the vehicle
moving on a map as new positions were received, you would need to know
that each update corresponds to a particular vehicle.

### Current Stop

Each vehicle position record can be associated with a single stop. If
specified, this stop must appear in the corresponding GTFS feed.

The stop can be identified either by the `stop_id` value, or by using
the `current_stop_sequence` value. If you use the stop sequence, the
stop can be determined by finding the corresponding record in the GTFS
feed's `stop_times.txt` file.

```java
if (vp.hasStopId()) {
  String stopId = vp.getStopId();

}

if (vp.hasCurrentStopSequence()) {
  int sequence = vp.getCurrentStopSequence();

}
```

***Note:** It is possible for the same stop to be visited multiple times
in a single trip (consider a loop service, although there are other
instances when this may also happen). The stop sequence value can be
useful to disambiguate this case.*

On its own, knowing the stop has no meaning without context. The
`current_status` field provides this context, indicating that the
vehicle is either:

* In transit to the stop (it is the next stop but the vehicle is not yet nearby)
* About to arrive at the stop
* Currently stopped at the stop.

According to the GTFS-realtime specification, you can only make use of
`current_status` if the stop sequence is specified.

The following code shows how you can retrieve and check the value of the
stop status.

```java
if (vp.hasCurrentStopSequence()) {
  int sequence = vp.getCurrentStopSequence();

  if (vp.hasCurrentStatus()) {
    VehicleStopStatus status = vp.getCurrentStatus();

    switch (status.getNumber()) {
    case VehicleStopStatus.IN_TRANSIT_TO_VALUE:
      // ...

    case VehicleStopStatus.INCOMING_AT_VALUE:
      // ...

    case VehicleStopStatus.STOPPED_AT_VALUE:
      // ...

    }
  }
}
```

### Congestion Levels

The other two values that may be included with a vehicle position relate
to the congestion inside and outside of the vehicle.

The `congestion_level` value indicates the flow of traffic. This value does
not indicate whether or not the vehicle is running to schedule, since congestion
levels are typically accounted for in scheduling.

The following code shows how you can check the congestion level value:

```java
if (vp.hasCongestionLevel()) {
  CongestionLevel congestion = vp.getCongestionLevel();

  switch (congestion.getNumber()) {
  case CongestionLevel.UNKNOWN_CONGESTION_LEVEL_VALUE:
    // ...

  case CongestionLevel.RUNNING_SMOOTHLY_VALUE:
    // ...

  case CongestionLevel.STOP_AND_GO_VALUE:
    // ...

  case CongestionLevel.SEVERE_CONGESTION_VALUE:
    // ...

  case CongestionLevel.CONGESTION_VALUE:
    // ...
    
  }
}
```

The `occupancy_status` value indicates how full the vehicle currently
is. This can be useful to present to your users so they know what to
expect before the vehicle arrives. For instance:

* A person with a broken leg may not want to travel on a standing
  room-only bus
* Someone traveling late at night might prefer a taxi over an empty
  train for safety reasons
* If a bus is full and not accepting passengers, someone may stay at
  home for longer until a bus with seats is coming by.

You can check the occupancy status of a vehicle as follows:

```java
if (vp.hasOccupancyStatus()) {
  OccupancyStatus status = vp.getOccupancyStatus();

  switch (status.getNumber()) {
  case OccupancyStatus.EMPTY_VALUE:
    // ...

  case OccupancyStatus.MANY_SEATS_AVAILABLE_VALUE:
    // ...

  case OccupancyStatus.FEW_SEATS_AVAILABLE_VALUE:
    // ...

  case OccupancyStatus.STANDING_ROOM_ONLY_VALUE:
    // ...

  case OccupancyStatus.CRUSHED_STANDING_ROOM_ONLY_VALUE:
    // ...

  case OccupancyStatus.FULL_VALUE:
    // ...

  case OccupancyStatus.NOT_ACCEPTING_PASSENGERS_VALUE:
    // ...

  }
}
```

### Determining a Vehicle's Bearing

One of the values that can be specified in a GTFS-realtime vehicle
position update is the bearing of the vehicle being reported. This value
indicates either the direction the vehicle is facing, or the direction
towards the next stop.

Ideally, the bearing contains the actual direction that the vehicle is
facing, not the direction to the next stop, since it is possible for
this value to be inaccurate. For example, if a Northbound vehicle is
stopped at a stop (that is, directly beside it), then the calculated
bearing would indicate the vehicle was facing East, not North.

***Note:** This example assumes that the vehicle is in a country that
drives on the right-hand side of the road.*

There are several ways to calculate a vehicle's bearing if it is not
specified in a GTFS-realtime feed:

* Determine the direction towards the next stop
* Determine the direction using a previous vehicle position reading
* A combination of the above.

***Note:** The GTFS-realtime specification states that feed providers
should not include the bearing if it is calculated using previous
positions. This is because consumers of the feed can calculate this, as
shown in the remainder of this chapter.*

### Bearing to Next Stop

In order to determine the bearing from the current location to the next
stop, there are two values you need:

* **Vehicle position.** This is provided in the `latitude` and
  `longitude` fields of the vehicle position.
* **Position of the next stop.** This is provided by the `stop_id`
  or `current_stop_sequence` fields of the vehicle position.

***Note:** Many GTFS-realtime vehicle position feeds do not include
information about the next stop, and consequently this technique will
not work in those instances. If this is the case, using the previous
reading to determine the bearing would be used, as shown later in
*Bearing From Previous Position*.*

The following formula is used to determine the bearing between the
starting location and the next stop:

```
θ = atan2( sin Δλ ⋅ cos φ2 , cos φ1 ⋅ sin φ2 − sin φ1 ⋅ cos φ2 ⋅ cos Δλ )
```

In this equation, the starting point is indicated by *1*, while the next
stop is represented by *2*. Latitude is represented by *φ*, while
longitude is represented by *λ*. For example, *φ2* means the latitude of
the next stop.

The resultant value *θ* is the bearing between the two points in
*radians*, and must then be converted to degrees (0-360).

This equation can be represented in Java as follows. It accepts that
latitude and longitude of two points in degrees, and returns the bearing
in degrees.

```java
public static double calculateBearing(double lat1Deg, double lon1Deg, double lat2Deg, double lon2Deg) {

  // Convert all degrees to radians
  double lat1 = Math.toRadians(lat1Deg);
  double lon1 = Math.toRadians(lon2Deg);
  double lat2 = Math.toRadians(lat2Deg);
  double lon2 = Math.toRadians(lon2Deg);

  // sin Δλ ⋅ cos φ2
  double y = Math.sin(lon2 - lon1) * Math.cos(lat2);

  // cos φ1 ⋅ sin φ2 − sin φ1 ⋅ cos φ2 ⋅ cos Δλ
  double x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);

  // Calculate the bearing in radians
  double bearingRad = Math.atan2(y, x);

  // Convert radians to degrees
  double bearingDeg = Math.toDegrees(bearingRad);

  // Ensure x is positive, in the range of 0 <= x < 360
  if (bearingDeg < 0) {
    bearingDeg += 360;
  }

  return bearingDeg;
}
```

One thing to be aware of when using the next stop to determine bearing
is the `current_status` value of the vehicle position (if specified).
If the value is `STOPPED_AT`, then the calculated angle might be
significantly wrong, since the vehicle is likely directly next to the
stop. In this instance, you should either use the next stop, or
determine the bearing from the previous position reading.

### Bearing From Previous Position

Similar to calculating a vehicle's bearing using the direction to the
next stop, you can also use a previous reading to calculate the
vehicle's direction.

The following diagram shows two readings for the same vehicle, as well
as the calculated bearing from the first point to the second.

![Bearing From Previous Position](images/bearing-from-position.png)

To calculate the bearing in this way, you need the following data:

* **Current vehicle position.** This is provided in the `latitude`
  and `longitude` fields of the vehicle position.
* **Previous vehicle position.** This should be the most recent
  vehicle position recorded prior to receiving the current vehicle
  position. Additionally, this position must represent a different
  location to the current position. If a vehicle is stationary for
  several minutes, you may receive several locations for the vehicle
  with the same coordinates.

***Note:** In the case of multiple readings at the same location, you
should use a minimum distance threshold to decide whether or not the
location is the same. For instance, you may decide that the two previous
locations must be more than, say, 10 meters to use it for comparison.*

It is also necessary to check the age of the previous reading. If the
previous reading is more than a minute or two old, it is likely that the
vehicle has travelled far enough to render the calculated bearing
meaningless.

### Combination

These two strategies are useful to approximate a vehicle's bearing if
it is not specified in a vehicle position message, but they are still
reliant on certain data being available.

The first technique needs to know the next stop, while the second needs
a previous vehicle position reading.

Your algorithm to determine a vehicle's position could be as follows:

* Use the provided bearing value in the vehicle position if this
  available.
* Otherwise, if you have a recent previous reading, calculate the
  direction using that and the current reading.
* Otherwise, if the next stop is known, show the bearing of the
  vehicle towards the stop.