diff options
Diffstat (limited to '.idea/gtfs-realtime-book/ch-07-consuming-vehicle-positions.md')
-rw-r--r-- | .idea/gtfs-realtime-book/ch-07-consuming-vehicle-positions.md | 509 |
1 files changed, 509 insertions, 0 deletions
diff --git a/.idea/gtfs-realtime-book/ch-07-consuming-vehicle-positions.md b/.idea/gtfs-realtime-book/ch-07-consuming-vehicle-positions.md new file mode 100644 index 0000000..1b745d0 --- /dev/null +++ b/.idea/gtfs-realtime-book/ch-07-consuming-vehicle-positions.md @@ -0,0 +1,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. + |