docs: def locate_trips(feed: "Feed", date: str, times: List[str]) -> DataFrame:
      """
      Return the positions of all trips active on the
      given date and times
  
      Parameters
      ----------
      feed : Feed
      date : string
          YYYYMMDD date string
      times : list
          HH:MM:SS time strings, possibly with HH > 23
  
      Returns
      -------
      DataFrame
          Columns are:
  
          - ``'trip_id'``
          - ``'route_id'``
          - ``'direction_id'``: all NaNs if ``feed.trips.direction_id`` is
            missing
          - ``'time'``
          - ``'rel_dist'``: number between 0 (start) and 1 (end)
            indicating the relative distance of the trip along its path
          - ``'lon'``: longitude of trip at given time
          - ``'lat'``: latitude of trip at given time
  
          Assume ``feed.stop_times`` has an accurate
          ``shape_dist_traveled`` column.
  
      Notes
      -----
      Assume the following feed attributes are not ``None``:
  
      - ``feed.trips``
      - Those used in :func:`.stop_times.get_stop_times`
      - Those used in :func:`.shapes.build_geometry_by_shape`
  
      """
      if not hp.is_not_null(feed.stop_times, "shape_dist_traveled"):
          raise ValueError(
              "feed.stop_times needs to have a non-null shape_dist_traveled "
              "column. You can create it, possibly with some inaccuracies, "
              "via feed2 = feed.append_dist_to_stop_times()."
          )
  
      if "shape_id" not in feed.trips.columns:
          raise ValueError("feed.trips.shape_id must exist.")
  
      # Start with stop times active on date
      f = feed.get_stop_times(date)
      f["departure_time"] = f["departure_time"].map(hp.timestr_to_seconds)
  
      # Compute relative distance of each trip along its path
      # at the given time times.
      # Use linear interpolation based on stop departure times and
      # shape distance traveled.
      geometry_by_shape = feed.build_geometry_by_shape(use_utm=False)
      sample_times = np.array([hp.timestr_to_seconds(s) for s in times])
  
      def compute_rel_dist(group):
          dists = sorted(group["shape_dist_traveled"].values)
          times = sorted(group["departure_time"].values)
          ts = sample_times[
              (sample_times >= times[0]) & (sample_times <= times[-1])
          ]
          ds = np.interp(ts, times, dists)
          return pd.DataFrame({"time": ts, "rel_dist": ds / dists[-1]})
  
      # return f.groupby('trip_id', group_keys=False).\
      #   apply(compute_rel_dist).reset_index()
      g = f.groupby("trip_id").apply(compute_rel_dist).reset_index()
  
      # Delete extraneous multi-index column
      del g["level_1"]
  
      # Convert times back to time strings
      g["time"] = g["time"].map(lambda x: hp.timestr_to_seconds(x, inverse=True))
  
      # Merge in more trip info and
      # compute longitude and latitude of trip from relative distance
      t = feed.trips.copy()
      if "direction_id" not in t.columns:
          t["direction_id"] = np.nan
  
      h = pd.merge(g, t[["trip_id", "route_id", "direction_id", "shape_id"]])
      if not h.shape[0]:
          # Return a DataFrame with the promised headers but no data.
          # Without this check, result below could be an empty DataFrame.
          h["lon"] = pd.Series()
          h["lat"] = pd.Series()
          return h
  
      def get_lonlat(group):
          shape = group["shape_id"].iat[0]
          linestring = geometry_by_shape[shape]
          lonlats = [
              linestring.interpolate(d, normalized=True).coords[0]
              for d in group["rel_dist"].values
          ]
          group["lon"], group["lat"] = zip(*lonlats)
          return group
  
      return h.groupby("shape_id").apply(get_lonlat)