Upgrade origin-src to google transit feed 1.2.6
[bus.git] / origin-src / transitfeed-1.2.6 / transitfeed / trip.py
blob:a/origin-src/transitfeed-1.2.6/transitfeed/trip.py -> blob:b/origin-src/transitfeed-1.2.6/transitfeed/trip.py
--- a/origin-src/transitfeed-1.2.6/transitfeed/trip.py
+++ b/origin-src/transitfeed-1.2.6/transitfeed/trip.py
@@ -1,1 +1,732 @@
-
+#!/usr/bin/python2.5
+
+# Copyright (C) 2007 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import warnings
+
+from gtfsobjectbase import GtfsObjectBase
+import problems as problems_module
+import util
+
+class Trip(GtfsObjectBase):
+  _REQUIRED_FIELD_NAMES = ['route_id', 'service_id', 'trip_id']
+  _FIELD_NAMES = _REQUIRED_FIELD_NAMES + [
+    'trip_headsign', 'direction_id', 'block_id', 'shape_id'
+    ]
+  _TABLE_NAME= "trips"
+
+  def __init__(self, headsign=None, service_period=None,
+               route=None, trip_id=None, field_dict=None):
+    self._schedule = None
+    self._headways = []  # [(start_time, end_time, headway_secs)]
+    if not field_dict:
+      field_dict = {}
+      if headsign is not None:
+        field_dict['trip_headsign'] = headsign
+      if route:
+        field_dict['route_id'] = route.route_id
+      if trip_id is not None:
+        field_dict['trip_id'] = trip_id
+      if service_period is not None:
+        field_dict['service_id'] = service_period.service_id
+      # Earlier versions of transitfeed.py assigned self.service_period here
+      # and allowed the caller to set self.service_id. Schedule.Validate
+      # checked the service_id attribute if it was assigned and changed it to a
+      # service_period attribute. Now only the service_id attribute is used and
+      # it is validated by Trip.Validate.
+      if service_period is not None:
+        # For backwards compatibility
+        self.service_id = service_period.service_id
+    self.__dict__.update(field_dict)
+
+  def GetFieldValuesTuple(self):
+    return [getattr(self, fn) or '' for fn in self._FIELD_NAMES]
+
+  def AddStopTime(self, stop, problems=None, schedule=None, **kwargs):
+    """Add a stop to this trip. Stops must be added in the order visited.
+
+    Args:
+      stop: A Stop object
+      kwargs: remaining keyword args passed to StopTime.__init__
+
+    Returns:
+      None
+    """
+    if problems is None:
+      # TODO: delete this branch when StopTime.__init__ doesn't need a
+      # ProblemReporter
+      problems = problems_module.default_problem_reporter
+    stoptime = self.GetGtfsFactory().StopTime(
+        problems=problems, stop=stop, **kwargs)
+    self.AddStopTimeObject(stoptime, schedule)
+
+  def _AddStopTimeObjectUnordered(self, stoptime, schedule):
+    """Add StopTime object to this trip.
+
+    The trip isn't checked for duplicate sequence numbers so it must be
+    validated later."""
+    stop_time_class = self.GetGtfsFactory().StopTime
+    cursor = schedule._connection.cursor()
+    insert_query = "INSERT INTO stop_times (%s) VALUES (%s);" % (
+       ','.join(stop_time_class._SQL_FIELD_NAMES),
+       ','.join(['?'] * len(stop_time_class._SQL_FIELD_NAMES)))
+    cursor = schedule._connection.cursor()
+    cursor.execute(
+        insert_query, stoptime.GetSqlValuesTuple(self.trip_id))
+
+  def ReplaceStopTimeObject(self, stoptime, schedule=None):
+    """Replace a StopTime object from this trip with the given one.
+
+    Keys the StopTime object to be replaced by trip_id, stop_sequence
+    and stop_id as 'stoptime', with the object 'stoptime'.
+    """
+
+    if schedule is None:
+      schedule = self._schedule
+
+    new_secs = stoptime.GetTimeSecs()
+    cursor = schedule._connection.cursor()
+    cursor.execute("DELETE FROM stop_times WHERE trip_id=? and "
+                   "stop_sequence=? and stop_id=?",
+                   (self.trip_id, stoptime.stop_sequence, stoptime.stop_id))
+    if cursor.rowcount == 0:
+      raise problems_module.Error, 'Attempted replacement of StopTime object which does not exist'
+    self._AddStopTimeObjectUnordered(stoptime, schedule)
+
+  def AddStopTimeObject(self, stoptime, schedule=None, problems=None):
+    """Add a StopTime object to the end of this trip.
+
+    Args:
+      stoptime: A StopTime object. Should not be reused in multiple trips.
+      schedule: Schedule object containing this trip which must be
+      passed to Trip.__init__ or here
+      problems: ProblemReporter object for validating the StopTime in its new
+      home
+
+    Returns:
+      None
+    """
+    if schedule is None:
+      schedule = self._schedule
+    if schedule is None:
+      warnings.warn("No longer supported. _schedule attribute is used to get "
+                    "stop_times table", DeprecationWarning)
+    if problems is None:
+      problems = schedule.problem_reporter
+
+    new_secs = stoptime.GetTimeSecs()
+    cursor = schedule._connection.cursor()
+    cursor.execute("SELECT max(stop_sequence), max(arrival_secs), "
+                   "max(departure_secs) FROM stop_times WHERE trip_id=?",
+                   (self.trip_id,))
+    row = cursor.fetchone()
+    if row[0] is None:
+      # This is the first stop_time of the trip
+      stoptime.stop_sequence = 1
+      if new_secs == None:
+        problems.OtherProblem(
+            'No time for first StopTime of trip_id "%s"' % (self.trip_id,))
+    else:
+      stoptime.stop_sequence = row[0] + 1
+      prev_secs = max(row[1], row[2])
+      if new_secs != None and new_secs < prev_secs:
+        problems.OtherProblem(
+            'out of order stop time for stop_id=%s trip_id=%s %s < %s' %
+            (util.EncodeUnicode(stoptime.stop_id), 
+             util.EncodeUnicode(self.trip_id),
+             util.FormatSecondsSinceMidnight(new_secs),
+             util.FormatSecondsSinceMidnight(prev_secs)))
+    self._AddStopTimeObjectUnordered(stoptime, schedule)
+
+  def GetTimeStops(self):
+    """Return a list of (arrival_secs, departure_secs, stop) tuples.
+
+    Caution: arrival_secs and departure_secs may be 0, a false value meaning a
+    stop at midnight or None, a false value meaning the stop is untimed."""
+    return [(st.arrival_secs, st.departure_secs, st.stop) for st in
+            self.GetStopTimes()]
+
+  def GetCountStopTimes(self):
+    """Return the number of stops made by this trip."""
+    cursor = self._schedule._connection.cursor()
+    cursor.execute(
+        'SELECT count(*) FROM stop_times WHERE trip_id=?', (self.trip_id,))
+    return cursor.fetchone()[0]
+
+  def GetTimeInterpolatedStops(self):
+    """Return a list of (secs, stoptime, is_timepoint) tuples.
+
+    secs will always be an int. If the StopTime object does not have explict
+    times this method guesses using distance. stoptime is a StopTime object and
+    is_timepoint is a bool.
+
+    Raises:
+      ValueError if this trip does not have the times needed to interpolate
+    """
+    rv = []
+
+    stoptimes = self.GetStopTimes()
+    # If there are no stoptimes [] is the correct return value but if the start
+    # or end are missing times there is no correct return value.
+    if not stoptimes:
+      return []
+    if (stoptimes[0].GetTimeSecs() is None or
+        stoptimes[-1].GetTimeSecs() is None):
+      raise ValueError("%s must have time at first and last stop" % (self))
+
+    cur_timepoint = None
+    next_timepoint = None
+    distance_between_timepoints = 0
+    distance_traveled_between_timepoints = 0
+
+    for i, st in enumerate(stoptimes):
+      if st.GetTimeSecs() != None:
+        cur_timepoint = st
+        distance_between_timepoints = 0
+        distance_traveled_between_timepoints = 0
+        if i + 1 < len(stoptimes):
+          k = i + 1
+          distance_between_timepoints += util.ApproximateDistanceBetweenStops(stoptimes[k-1].stop, stoptimes[k].stop)
+          while stoptimes[k].GetTimeSecs() == None:
+            k += 1
+            distance_between_timepoints += util.ApproximateDistanceBetweenStops(stoptimes[k-1].stop, stoptimes[k].stop)
+          next_timepoint = stoptimes[k]
+        rv.append( (st.GetTimeSecs(), st, True) )
+      else:
+        distance_traveled_between_timepoints += util.ApproximateDistanceBetweenStops(stoptimes[i-1].stop, st.stop)
+        distance_percent = distance_traveled_between_timepoints / distance_between_timepoints
+        total_time = next_timepoint.GetTimeSecs() - cur_timepoint.GetTimeSecs()
+        time_estimate = distance_percent * total_time + cur_timepoint.GetTimeSecs()
+        rv.append( (int(round(time_estimate)), st, False) )
+
+    return rv
+
+  def ClearStopTimes(self):
+    """Remove all stop times from this trip.
+
+    StopTime objects previously returned by GetStopTimes are unchanged but are
+    no longer associated with this trip.
+    """
+    cursor = self._schedule._connection.cursor()
+    cursor.execute('DELETE FROM stop_times WHERE trip_id=?', (self.trip_id,))
+
+  def GetStopTimes(self, problems=None):
+    """Return a sorted list of StopTime objects for this trip."""
+    # In theory problems=None should be safe because data from database has been
+    # validated. See comment in _LoadStopTimes for why this isn't always true.
+    cursor = self._schedule._connection.cursor()
+    cursor.execute(
+        'SELECT arrival_secs,departure_secs,stop_headsign,pickup_type,'
+        'drop_off_type,shape_dist_traveled,stop_id,stop_sequence FROM '
+        'stop_times WHERE '
+        'trip_id=? ORDER BY stop_sequence', (self.trip_id,))
+    stop_times = []
+    stoptime_class = self.GetGtfsFactory().StopTime
+    for row in cursor.fetchall():
+      stop = self._schedule.GetStop(row[6])
+      stop_times.append(stoptime_class(problems=problems,
+                                       stop=stop,
+                                       arrival_secs=row[0],
+                                       departure_secs=row[1],
+                                       stop_headsign=row[2],
+                                       pickup_type=row[3],
+                                       drop_off_type=row[4],
+                                       shape_dist_traveled=row[5],
+                                       stop_sequence=row[7]))
+    return stop_times
+
+  def GetHeadwayStopTimes(self, problems=None):
+    """Deprecated. Please use GetFrequencyStopTimes instead."""
+    warnings.warn("No longer supported. The HeadwayPeriod class was renamed to "
+                  "Frequency, and all related functions were renamed "
+                  "accordingly.", DeprecationWarning)
+    return self.GetFrequencyStopTimes(problems)
+
+  def GetFrequencyStopTimes(self, problems=None):
+    """Return a list of StopTime objects for each headway-based run.
+
+    Returns:
+      a list of list of StopTime objects. Each list of StopTime objects
+      represents one run. If this trip doesn't have headways returns an empty
+      list.
+    """
+    stoptimes_list = [] # list of stoptime lists to be returned
+    stoptime_pattern = self.GetStopTimes()
+    first_secs = stoptime_pattern[0].arrival_secs # first time of the trip
+    stoptime_class = self.GetGtfsFactory().StopTime
+    # for each start time of a headway run
+    for run_secs in self.GetFrequencyStartTimes():
+      # stop time list for a headway run
+      stoptimes = []
+      # go through the pattern and generate stoptimes
+      for st in stoptime_pattern:
+        arrival_secs, departure_secs = None, None # default value if the stoptime is not timepoint
+        if st.arrival_secs != None:
+          arrival_secs = st.arrival_secs - first_secs + run_secs
+        if st.departure_secs != None:
+          departure_secs = st.departure_secs - first_secs + run_secs
+        # append stoptime
+        stoptimes.append(stoptime_class(problems=problems, stop=st.stop,
+                                        arrival_secs=arrival_secs,
+                                        departure_secs=departure_secs,
+                                        stop_headsign=st.stop_headsign,
+                                        pickup_type=st.pickup_type,
+                                        drop_off_type=st.drop_off_type,
+                                        shape_dist_traveled= \
+                                            st.shape_dist_traveled,
+                                        stop_sequence=st.stop_sequence))
+      # add stoptimes to the stoptimes_list
+      stoptimes_list.append ( stoptimes )
+    return stoptimes_list
+
+  def GetStartTime(self, problems=problems_module.default_problem_reporter):
+    """Return the first time of the trip. TODO: For trips defined by frequency
+    return the first time of the first trip."""
+    cursor = self._schedule._connection.cursor()
+    cursor.execute(
+        'SELECT arrival_secs,departure_secs FROM stop_times WHERE '
+        'trip_id=? ORDER BY stop_sequence LIMIT 1', (self.trip_id,))
+    (arrival_secs, departure_secs) = cursor.fetchone()
+    if arrival_secs != None:
+      return arrival_secs
+    elif departure_secs != None:
+      return departure_secs
+    else:
+      problems.InvalidValue('departure_time', '',
+                            'The first stop_time in trip %s is missing '
+                            'times.' % self.trip_id)
+
+  def GetHeadwayStartTimes(self):
+    """Deprecated. Please use GetFrequencyStartTimes instead."""
+    warnings.warn("No longer supported. The HeadwayPeriod class was renamed to "
+                  "Frequency, and all related functions were renamed "
+                  "accordingly.", DeprecationWarning)
+    return self.GetFrequencyStartTimes()
+
+  def GetFrequencyStartTimes(self):
+    """Return a list of start time for each headway-based run.
+
+    Returns:
+      a sorted list of seconds since midnight, the start time of each run. If
+      this trip doesn't have headways returns an empty list."""
+    start_times = []
+    # for each headway period of the trip
+    for start_secs, end_secs, headway_secs in self.GetFrequencyTuples():
+      # reset run secs to the start of the timeframe
+      run_secs = start_secs
+      while run_secs < end_secs:
+        start_times.append(run_secs)
+        # increment current run secs by headway secs
+        run_secs += headway_secs
+    return start_times
+
+  def GetEndTime(self, problems=problems_module.default_problem_reporter):
+    """Return the last time of the trip. TODO: For trips defined by frequency
+    return the last time of the last trip."""
+    cursor = self._schedule._connection.cursor()
+    cursor.execute(
+        'SELECT arrival_secs,departure_secs FROM stop_times WHERE '
+        'trip_id=? ORDER BY stop_sequence DESC LIMIT 1', (self.trip_id,))
+    (arrival_secs, departure_secs) = cursor.fetchone()
+    if departure_secs != None:
+      return departure_secs
+    elif arrival_secs != None:
+      return arrival_secs
+    else:
+      problems.InvalidValue('arrival_time', '',
+                            'The last stop_time in trip %s is missing '
+                            'times.' % self.trip_id)
+
+  def _GenerateStopTimesTuples(self):
+    """Generator for rows of the stop_times file"""
+    stoptimes = self.GetStopTimes()
+    for i, st in enumerate(stoptimes):
+      yield st.GetFieldValuesTuple(self.trip_id)
+
+  def GetStopTimesTuples(self):
+    results = []
+    for time_tuple in self._GenerateStopTimesTuples():
+      results.append(time_tuple)
+    return results
+
+  def GetPattern(self):
+    """Return a tuple of Stop objects, in the order visited"""
+    stoptimes = self.GetStopTimes()
+    return tuple(st.stop for st in stoptimes)
+
+  def AddHeadwayPeriodObject(self, headway_period, problem_reporter):
+    """Deprecated. Please use AddFrequencyObject instead."""
+    warnings.warn("No longer supported. The HeadwayPeriod class was renamed to "
+                  "Frequency, and all related functions were renamed "
+                  "accordingly.", DeprecationWarning)
+    self.AddFrequencyObject(frequency, problem_reporter)
+
+  def AddFrequencyObject(self, frequency, problem_reporter):
+    """Add a Frequency object to this trip's list of Frequencies."""
+    if frequency is not None:
+      self.AddFrequency(frequency.StartTime(),
+                        frequency.EndTime(),
+                        frequency.HeadwaySecs(),
+                        problem_reporter)
+
+  def AddHeadwayPeriod(self, start_time, end_time, headway_secs,
+      problem_reporter=problems_module.default_problem_reporter):
+    """Deprecated. Please use AddFrequency instead."""
+    warnings.warn("No longer supported. The HeadwayPeriod class was renamed to "
+                  "Frequency, and all related functions were renamed "
+                  "accordingly.", DeprecationWarning)
+    self.AddFrequency(start_time, end_time, headway_secs, problem_reporter)
+
+  def AddFrequency(self, start_time, end_time, headway_secs,
+      problem_reporter=problems_module.default_problem_reporter):
+    """Adds a period to this trip during which the vehicle travels
+    at regular intervals (rather than specifying exact times for each stop).
+
+    Args:
+      start_time: The time at which this headway period starts, either in
+          numerical seconds since midnight or as "HH:MM:SS" since midnight.
+      end_time: The time at which this headway period ends, either in
+          numerical seconds since midnight or as "HH:MM:SS" since midnight.
+          This value should be larger than start_time.
+      headway_secs: The amount of time, in seconds, between occurences of
+          this trip.
+      problem_reporter: Optional parameter that can be used to select
+          how any errors in the other input parameters will be reported.
+    Returns:
+      None
+    """
+    if start_time == None or start_time == '':  # 0 is OK
+      problem_reporter.MissingValue('start_time')
+      return
+    if isinstance(start_time, basestring):
+      try:
+        start_time = util.TimeToSecondsSinceMidnight(start_time)
+      except problems_module.Error:
+        problem_reporter.InvalidValue('start_time', start_time)
+        return
+    elif start_time < 0:
+      problem_reporter.InvalidValue('start_time', start_time)
+
+    if end_time == None or end_time == '':
+      problem_reporter.MissingValue('end_time')
+      return
+    if isinstance(end_time, basestring):
+      try:
+        end_time = util.TimeToSecondsSinceMidnight(end_time)
+      except problems_module.Error:
+        problem_reporter.InvalidValue('end_time', end_time)
+        return
+    elif end_time < 0:
+      problem_reporter.InvalidValue('end_time', end_time)
+      return
+
+    if not headway_secs:
+      problem_reporter.MissingValue('headway_secs')
+      return
+    try:
+      headway_secs = int(headway_secs)
+    except ValueError:
+      problem_reporter.InvalidValue('headway_secs', headway_secs)
+      return
+
+    if headway_secs <= 0:
+      problem_reporter.InvalidValue('headway_secs', headway_secs)
+      return
+
+    if end_time <= start_time:
+      problem_reporter.InvalidValue('end_time', end_time,
+                                    'should be greater than start_time')
+
+    self._headways.append((start_time, end_time, headway_secs))
+
+  def ClearFrequencies(self):
+    self._headways = []
+
+  def _HeadwayOutputTuple(self, headway):
+      return (self.trip_id,
+              util.FormatSecondsSinceMidnight(headway[0]),
+              util.FormatSecondsSinceMidnight(headway[1]),
+              unicode(headway[2]))
+
+  def GetFrequencyOutputTuples(self):
+    tuples = []
+    for headway in self._headways:
+      tuples.append(self._HeadwayOutputTuple(headway))
+    return tuples
+
+  def GetFrequencyTuples(self):
+    return self._headways
+
+  def __getattr__(self, name):
+    if name == 'service_period':
+      assert self._schedule, "Must be in a schedule to get service_period"
+      return self._schedule.GetServicePeriod(self.service_id)
+    elif name == 'pattern_id':
+      if '_pattern_id' not in self.__dict__:
+        self.__dict__['_pattern_id'] = hash(self.GetPattern())
+      return self.__dict__['_pattern_id']
+    else:
+      return GtfsObjectBase.__getattr__(self, name)
+
+  def ValidateRouteId(self, problems):
+    if util.IsEmpty(self.route_id):
+      problems.MissingValue('route_id')
+  
+  def ValidateServicePeriod(self, problems):
+    if 'service_period' in self.__dict__:
+      # Some tests assign to the service_period attribute. Patch up self before
+      # proceeding with validation. See also comment in Trip.__init__.
+      self.service_id = self.__dict__['service_period'].service_id
+      del self.service_period
+    if util.IsEmpty(self.service_id):
+      problems.MissingValue('service_id')
+      
+  def ValidateTripId(self, problems):
+    if util.IsEmpty(self.trip_id):
+      problems.MissingValue('trip_id')
+      
+  def ValidateDirectionId(self, problems):
+    if hasattr(self, 'direction_id') and (not util.IsEmpty(self.direction_id)) \
+        and (self.direction_id != '0') and (self.direction_id != '1'):
+      problems.InvalidValue('direction_id', self.direction_id,
+                            'direction_id must be "0" or "1"')
+
+  def ValidateShapeIdsExistInShapeList(self, problems):
+    if self._schedule:
+      if self.shape_id and self.shape_id not in self._schedule._shapes:
+        problems.InvalidValue('shape_id', self.shape_id)
+      
+  def ValidateRouteIdExistsInRouteList(self, problems):
+    if self._schedule:
+      if self.route_id and self.route_id not in self._schedule.routes:
+        problems.InvalidValue('route_id', self.route_id)
+      
+  def ValidateServiceIdExistsInServiceList(self, problems):
+    if self._schedule:
+      if (self.service_id and
+          self.service_id not in self._schedule.service_periods):
+        problems.InvalidValue('service_id', self.service_id)
+
+  def Validate(self, problems, validate_children=True):
+    """Validate attributes of this object.
+
+    Check that this object has all required values set to a valid value without
+    reference to the rest of the schedule. If the _schedule attribute is set
+    then check that references such as route_id and service_id are correct.
+
+    Args:
+      problems: A ProblemReporter object
+      validate_children: if True and the _schedule attribute is set than call
+                         ValidateChildren
+    """
+    self.ValidateRouteId(problems)
+    self.ValidateServicePeriod(problems)
+    self.ValidateDirectionId(problems)
+    self.ValidateTripId(problems)
+    self.ValidateShapeIdsExistInShapeList(problems)
+    self.ValidateRouteIdExistsInRouteList(problems)
+    self.ValidateServiceIdExistsInServiceList(problems)
+    if self._schedule and validate_children:
+      self.ValidateChildren(problems)
+
+  def ValidateNoDuplicateStopSequences(self, problems):
+    cursor = self._schedule._connection.cursor()
+    cursor.execute("SELECT COUNT(stop_sequence) AS a FROM stop_times "
+                   "WHERE trip_id=? GROUP BY stop_sequence HAVING a > 1",
+                   (self.trip_id,))
+    for row in cursor:
+      problems.InvalidValue('stop_sequence', row[0],
+                            'Duplicate stop_sequence in trip_id %s' %
+                            self.trip_id)
+
+  def ValidateTripStartAndEndTimes(self, problems, stoptimes):
+    if stoptimes:
+      if stoptimes[0].arrival_time is None and stoptimes[0].departure_time is None:
+        problems.OtherProblem(
+          'No time for start of trip_id "%s""' % (self.trip_id))
+      if stoptimes[-1].arrival_time is None and stoptimes[-1].departure_time is None:
+        problems.OtherProblem(
+          'No time for end of trip_id "%s""' % (self.trip_id))
+
+  def ValidateStopTimesSequenceHasIncreasingTimeAndDistance(self,
+                                                            problems,
+                                                            stoptimes):
+    if stoptimes:
+      route_class = self.GetGtfsFactory().Route
+      # Checks that the arrival time for each time point is after the departure 
+      # time of the previous. Assumes a stoptimes sorted by sequence
+      prev_departure = 0
+      prev_stop = None
+      prev_distance = None
+      try:
+        route_type = self._schedule.GetRoute(self.route_id).route_type
+        max_speed = route_class._ROUTE_TYPES[route_type]['max_speed']
+      except KeyError, e:
+        # If route_type cannot be found, assume it is 0 (Tram) for checking
+        # speeds between stops.
+        max_speed = route_class._ROUTE_TYPES[0]['max_speed']
+      for timepoint in stoptimes:
+        # Distance should be a nonnegative float number, so it should be 
+        # always larger than None.
+        distance = timepoint.shape_dist_traveled
+        if distance is not None:
+          if distance > prev_distance and distance >= 0:
+            prev_distance = distance
+          else:
+            if distance == prev_distance:
+              type = problems_module.TYPE_WARNING
+            else:
+              type = problems_module.TYPE_ERROR
+            problems.InvalidValue('stoptimes.shape_dist_traveled', distance,
+                  'For the trip %s the stop %s has shape_dist_traveled=%s, '
+                  'which should be larger than the previous ones. In this '
+                  'case, the previous distance was %s.' % 
+                  (self.trip_id, timepoint.stop_id, distance, prev_distance),
+                  type=type)
+
+        if timepoint.arrival_secs is not None:
+          self._CheckSpeed(prev_stop, timepoint.stop, prev_departure,
+                           timepoint.arrival_secs, max_speed, problems)
+
+          if timepoint.arrival_secs >= prev_departure:
+            prev_departure = timepoint.departure_secs
+            prev_stop = timepoint.stop
+          else:
+            problems.OtherProblem('Timetravel detected! Arrival time '
+                                  'is before previous departure '
+                                  'at sequence number %s in trip %s' %
+                                  (timepoint.stop_sequence, self.trip_id))
+
+  def ValidateShapeDistTraveledSmallerThanMaxShapeDistance(self,
+                                                           problems, 
+                                                           stoptimes):
+    if stoptimes:
+      if self.shape_id and self.shape_id in self._schedule._shapes:
+        shape = self._schedule.GetShape(self.shape_id)
+        max_shape_dist = shape.max_distance
+        st = stoptimes[-1]
+        if (st.shape_dist_traveled and
+            st.shape_dist_traveled > max_shape_dist):
+          problems.OtherProblem(
+              'In stop_times.txt, the stop with trip_id=%s and '
+              'stop_sequence=%d has shape_dist_traveled=%f, which is larger '
+              'than the max shape_dist_traveled=%f of the corresponding '
+              'shape (shape_id=%s)' %
+              (self.trip_id, st.stop_sequence, st.shape_dist_traveled,
+               max_shape_dist, self.shape_id), 
+               type=problems_module.TYPE_WARNING)
+
+  def ValidateDistanceFromStopToShape(self, problems, stoptimes):
+    if stoptimes:
+      if self.shape_id and self.shape_id in self._schedule._shapes:
+        shape = self._schedule.GetShape(self.shape_id)
+        max_shape_dist = shape.max_distance
+        st = stoptimes[-1]
+        # shape_dist_traveled is valid in shape if max_shape_dist larger than
+        # 0.
+        if max_shape_dist > 0:
+          for st in stoptimes:
+            if st.shape_dist_traveled is None:
+              continue
+            pt = shape.GetPointWithDistanceTraveled(st.shape_dist_traveled)
+            if pt:
+              stop = self._schedule.GetStop(st.stop_id)
+              distance = util.ApproximateDistance(stop.stop_lat, stop.stop_lon,
+                                             pt[0], pt[1])
+              if distance > problems_module.MAX_DISTANCE_FROM_STOP_TO_SHAPE:
+                problems.StopTooFarFromShapeWithDistTraveled(
+                    self.trip_id, stop.stop_name, stop.stop_id, pt[2],
+                    self.shape_id, distance,
+                    problems_module.MAX_DISTANCE_FROM_STOP_TO_SHAPE)
+
+  def ValidateFrequencies(self, problems):
+    # O(n^2), but we don't anticipate many headway periods per trip
+    for headway_index, headway in enumerate(self._headways[0:-1]):
+      for other in self._headways[headway_index + 1:]:
+        if (other[0] < headway[1]) and (other[1] > headway[0]):
+          problems.OtherProblem('Trip contains overlapping headway periods '
+                                '%s and %s' %
+                                (self._HeadwayOutputTuple(headway),
+                                 self._HeadwayOutputTuple(other)))
+
+  def ValidateChildren(self, problems):
+    """Validate StopTimes and headways of this trip."""
+    assert self._schedule, "Trip must be in a schedule to ValidateChildren"
+    # TODO: validate distance values in stop times (if applicable)
+
+    self.ValidateNoDuplicateStopSequences(problems)
+    stoptimes = self.GetStopTimes(problems)
+    stoptimes.sort(key=lambda x: x.stop_sequence)
+    self.ValidateTripStartAndEndTimes(problems, stoptimes)
+    self.ValidateStopTimesSequenceHasIncreasingTimeAndDistance(problems,
+                                                               stoptimes)
+    self.ValidateShapeDistTraveledSmallerThanMaxShapeDistance(problems,
+                                                              stoptimes)
+    self.ValidateDistanceFromStopToShape(problems, stoptimes)
+    self.ValidateFrequencies(problems)
+    
+  def ValidateBeforeAdd(self, problems):
+    return True
+
+  def ValidateAfterAdd(self, problems):
+    self.Validate(problems)
+
+  def _CheckSpeed(self, prev_stop, next_stop, depart_time,
+                  arrive_time, max_speed, problems):
+    # Checks that the speed between two stops is not faster than max_speed
+    if prev_stop != None:
+      try:
+        time_between_stops = arrive_time - depart_time
+      except TypeError:
+        return
+
+      try:
+        dist_between_stops = \
+          util.ApproximateDistanceBetweenStops(next_stop, prev_stop)
+      except TypeError, e:
+          return
+
+      if time_between_stops == 0:
+        # HASTUS makes it hard to output GTFS with times to the nearest second;
+        # it rounds times to the nearest minute. Therefore stop_times at the
+        # same time ending in :00 are fairly common. These times off by no more
+        # than 30 have not caused a problem. See
+        # http://code.google.com/p/googletransitdatafeed/issues/detail?id=193
+        # Show a warning if times are not rounded to the nearest minute or
+        # distance is more than max_speed for one minute.
+        if depart_time % 60 != 0 or dist_between_stops / 1000 * 60 > max_speed:
+          problems.TooFastTravel(self.trip_id,
+                                 prev_stop.stop_name,
+                                 next_stop.stop_name,
+                                 dist_between_stops,
+                                 time_between_stops,
+                                 speed=None,
+                                 type=problems_module.TYPE_WARNING)
+        return
+      # This needs floating point division for precision.
+      speed_between_stops = ((float(dist_between_stops) / 1000) /
+                                (float(time_between_stops) / 3600))
+      if speed_between_stops > max_speed:
+        problems.TooFastTravel(self.trip_id,
+                               prev_stop.stop_name,
+                               next_stop.stop_name,
+                               dist_between_stops,
+                               time_between_stops,
+                               speed_between_stops,
+                               type=problems_module.TYPE_WARNING)
+
+  def AddToSchedule(self, schedule, problems):
+    schedule.AddTripObject(self, problems)
+