docs: def compute_screen_line_counts(
      feed: "Feed", linestring: LineString, dates: List[str], geo_shapes=None
  ) -> DataFrame:
      """
      Find all the Feed trips active on the given dates
      that intersect the given Shapely LineString (with WGS84
      longitude-latitude coordinates).
  
      Parameters
      ----------
      feed : Feed
      linestring : Shapely LineString
      dates : list
          YYYYMMDD date strings
  
      Returns
      -------
      DataFrame
          The columns are
  
          - ``'date'``
          - ``'trip_id'``
          - ``'route_id'``
          - ``'route_short_name'``
          - ``'crossing_time'``: time that the trip's vehicle crosses
            the linestring; one trip could cross multiple times
          - ``'orientation'``: 1 or -1; 1 indicates trip travel from the
            left side to the right side of the screen line;
            -1 indicates trip travel in the  opposite direction
  
      Notes
      -----
      - Requires GeoPandas
      - The first step is to geometrize ``feed.shapes`` via
        :func:`.shapes.geometrize_shapes`. Alternatively, use the
        ``geo_shapes`` GeoDataFrame, if given.
      - Assume ``feed.stop_times`` has an accurate
        ``shape_dist_traveled`` column.
      - Assume that trips travel in the same direction as their
        shapes. That restriction is part of GTFS, by the way.
        To calculate direction quickly and accurately, assume that
        the screen line is straight and doesn't double back on itself.
      - Probably does not give correct results for trips with
        self-intersecting shapes.
      - The algorithm works as follows
  
          1. Compute all the shapes that intersect the linestring
          2. For each such shape, compute the intersection points
          3. For each point p, scan through all the trips in the feed
             that have that shape
          4. For each date in ``dates``, restrict to trips active on the
             date and interpolate a stop time for p by assuming that the
             feed has the shape_dist_traveled field in stop times
          5. Use that interpolated time as the crossing time of the trip
             vehicle, and compute the trip orientation to the screen line
             via a cross product of a vector in the direction of the
             screen line and a tiny vector in the direction of trip travel
  
      - Assume the following feed attributes are not ``None``:
           * ``feed.shapes``, if ``geo_shapes`` is not given
  
      """
      dates = feed.restrict_dates(dates)
      if not dates:
          return pd.DataFrame()
  
      # Get all shapes that intersect the screen line
      shapes = feed.get_shapes_intersecting_geometry(
          linestring, geo_shapes, geometrized=True
      )
  
      # Convert shapes to UTM
      lat, lon = feed.shapes.loc[0, ["shape_pt_lat", "shape_pt_lon"]].values
      crs = hp.get_utm_crs(lat, lon)
      shapes = shapes.to_crs(crs)
  
      # Convert linestring to UTM
      linestring = hp.linestring_to_utm(linestring)
  
      # Get all intersection points of shapes and linestring
      shapes["intersection"] = shapes.intersection(linestring)
  
      # Make a vector in the direction of the screen line
      # to later calculate trip orientation.
      # Does not work in case of a bent screen line.
      p1 = sg.Point(linestring.coords[0])
      p2 = sg.Point(linestring.coords[-1])
      w = np.array([p2.x - p1.x, p2.y - p1.y])
  
      # Build a dictionary from the shapes DataFrame of the form
      # shape ID -> list of pairs (d, v), one for each intersection point,
      # where d is the distance of the intersection point along shape,
      # and v is a tiny vectors from the point in direction of shape.
      # Assume here that trips travel in the same direction as their shapes.
      dv_by_shape = {}
      eps = 1
      convert_dist = hp.get_convert_dist("m", feed.dist_units)
      for __, sid, geom, intersection in shapes.itertuples():
          # Get distances along shape of intersection points (in meters)
          distances = [geom.project(p) for p in intersection]
          # Build tiny vectors
          vectors = []
          for i, p in enumerate(intersection):
              q = geom.interpolate(distances[i] + eps)
              vector = np.array([q.x - p.x, q.y - p.y])
              vectors.append(vector)
          # Convert distances to units used in feed
          distances = [convert_dist(d) for d in distances]
          dv_by_shape[sid] = list(zip(distances, vectors))
  
      # Get trips with those shapes
      t = feed.trips
      t = t[t["shape_id"].isin(dv_by_shape.keys())].copy()
  
      # Merge in route short names and stop times
      t = t.merge(feed.routes[["route_id", "route_short_name"]]).merge(
          feed.stop_times
      )
  
      # Drop NaN departure times and convert to seconds past midnight
      t = t[t["departure_time"].notnull()].copy()
      t["departure_time"] = t["departure_time"].map(hp.timestr_to_seconds)
  
      # Compile crossings by date
      a = feed.compute_trip_activity(dates)
      rows = []
      for date in dates:
          # Slice to trips active on date
          ids = a.loc[a[date] == 1, "trip_id"]
          f = t[t["trip_id"].isin(ids)].copy()
  
          # For each shape find the trips that cross the screen line
          # and get crossing times and orientation
          f = f.sort_values(["trip_id", "stop_sequence"])
          for tid, group in f.groupby("trip_id"):
              sid = group["shape_id"].iat[0]
              rid = group["route_id"].iat[0]
              rsn = group["route_short_name"].iat[0]
              stop_times = group["departure_time"].values
              stop_distances = group["shape_dist_traveled"].values
              for d, v in dv_by_shape[sid]:
                  # Interpolate crossing time
                  time = np.interp(d, stop_distances, stop_times)
                  # Compute direction of trip travel relative to
                  # screen line by looking at the sign of the cross
                  # product of tiny shape vector and screen line vector
                  det = np.linalg.det(np.array([v, w]))
                  if det >= 0:
                      orientation = 1
                  else:
                      orientation = -1
                  # Update rows
                  rows.append([date, tid, rid, rsn, time, orientation])
  
      # Create DataFrame
      cols = [
          "date",
          "trip_id",
          "route_id",
          "route_short_name",
          "crossing_time",
          "orientation",
      ]
      g = pd.DataFrame(rows, columns=cols).sort_values(["date", "crossing_time"])
  
      # Convert departure times back to time strings
      g["crossing_time"] = g["crossing_time"].map(
          lambda x: hp.timestr_to_seconds(x, inverse=True)
      )
  
      return g