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)
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`