/* This program is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package org.opentripplanner.routing.trippattern;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import lombok.Getter;
import org.onebusaway.gtfs.model.StopTime;
import org.onebusaway.gtfs.model.Trip;
import org.opentripplanner.common.MavenVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A ScheduledTripTimes represents the standard published timetable for a trip provided by a transit
* data feed. When real-time stop time updates are being applied, these scheduled TripTimes can be
* wrapped in other TripTimes implementations which replace, cancel, or otherwise modify some of
* the timetable information.
*/
public class ScheduledTripTimes extends TripTimes implements Serializable {
private static final long serialVersionUID = MavenVersion.VERSION.getUID();
private static final Logger LOG = LoggerFactory.getLogger(ScheduledTripTimes.class);
@Getter private final Trip trip;
/**
* Both trip_headsign and stop_headsign (per stop on a particular trip) are optional GTFS
* fields. If the headsigns array is null, we will report the trip_headsign (which may also
* be null) at every stop on the trip. If all the stop_headsigns are the same as the
* trip_headsign we may also set the headsigns array to null to save space.
*/
private final String[] headsigns;
/**
* The time in seconds after midnight at which the vehicle begins traversing each inter-stop
* segment ("hop"). Field is non-final to support compaction.
*/ //@XmlElement
private int[] departureTimes;
/**
* The time in seconds after midnight at which the vehicle arrives at the end of each
* inter-stop segment ("hop"). A null value indicates that all dwells are 0-length, and arrival
* times are to be derived from the departure times array. Field is non-final to support
* compaction.
*/ //@XmlElement
private int[] arrivalTimes;
/** The provided stopTimes are assumed to be pre-filtered, valid, and monotonically increasing. */
public ScheduledTripTimes(Trip trip, List<StopTime> stopTimes) {
this.trip = trip;
int nStops = stopTimes.size();
int nHops = nStops - 1;
departureTimes = new int[nHops];
arrivalTimes = new int[nHops];
// this might be clearer if time array indexes were stops instead of hops
for (int hop = 0; hop < nHops; hop++) {
departureTimes[hop] = stopTimes.get(hop).getDepartureTime();
arrivalTimes[hop] = stopTimes.get(hop + 1).getArrivalTime();
}
this.headsigns = makeHeadsignsArray(stopTimes);
// If all dwell times are 0, arrival times array is not needed. Attempt to save some memory.
this.compact();
}
/**
* @return either an array of headsigns (one for each stop on this trip) or null if the
* headsign is the same at all stops (including null) and can be found in the Trip object.
*/
private String[] makeHeadsignsArray(List<StopTime> stopTimes) {
String tripHeadsign = trip.getTripHeadsign();
boolean useStopHeadsigns = false;
if (tripHeadsign == null) {
useStopHeadsigns = true;
}
else {
for (StopTime st : stopTimes) {
if ( ! (tripHeadsign.equals(st.getStopHeadsign()))) {
useStopHeadsigns = true;
break;
}
}
}
if ( ! useStopHeadsigns) {
return null; //defer to trip_headsign
}
boolean allNull = true;
int i = 0;
String[] hs = new String[stopTimes.size()];
for (StopTime st : stopTimes) {
String headsign = st.getStopHeadsign();
hs[i++] = headsign;
if (headsign != null)
allNull = false;
}
if (allNull)
return null;
else
return hs;
}
@Override
public int getNumHops() {
// The arrivals array may not be present, and the departures array may have grown by 1 due
// to compaction, so we can't directly use array lengths as an indicator of number of hops.
if (arrivalTimes == null)
return departureTimes.length - 1;
else
return arrivalTimes.length;
}
@Override
public ScheduledTripTimes getScheduledTripTimes() {
return this;
}
@Override
public int getDepartureTime(int hop) {
return departureTimes[hop];
}
@Override
public int getArrivalTime(int hop) {
if (arrivalTimes == null) // add range checking?
return departureTimes[hop + 1];
return arrivalTimes[hop];
}
/** {@inheritDoc} Replaces the arrivals array with null if all dwell times are zero. */
@Override
public boolean compact() {
if (arrivalTimes == null)
return false;
// use arrivalTimes to determine number of hops because departureTimes may have grown by 1
// due to successive compact/decompact operations
int nHops = arrivalTimes.length;
// dwell time is undefined for hop 0, because there is no arrival for hop -1
for (int hop = 1; hop < nHops; hop++) {
if (this.getDwellTime(hop) != 0) {
LOG.trace("compact failed: nonzero dwell time before hop {}", hop);
return false;
}
}
// extend departureTimes array by 1 to hold final arrival time
departureTimes = Arrays.copyOf(departureTimes, nHops+1);
departureTimes[nHops] = arrivalTimes[nHops-1];
arrivalTimes = null;
return true;
}
@SuppressWarnings("unused")
private boolean decompact() {
if (arrivalTimes != null)
return false;
int nHops = departureTimes.length;
if (nHops < 1)
throw new RuntimeException("improper array length in TripTimes");
arrivalTimes = Arrays.copyOfRange(departureTimes, 1, nHops);
return true;
}
public String toString() {
return "ScheduledTripTimes\n" + dumpTimes();
}
@Override
public String getHeadsign(int hop) {
if (headsigns == null)
return trip.getTripHeadsign();
else
return headsigns[hop];
}
}