def match_nextbus_direction(nextbus_route_config, geometry): shape_start = geometry.coords[0] shape_end = geometry.coords[-1] nextbus_dir_infos = nextbus_route_config.get_direction_infos() terminal_dists = [] for nextbus_dir_info in nextbus_dir_infos: nextbus_dir_stop_ids = nextbus_dir_info.get_stop_ids() first_stop_info = nextbus_route_config.get_stop_info(nextbus_dir_stop_ids[0]) last_stop_info = nextbus_route_config.get_stop_info(nextbus_dir_stop_ids[-1]) # Determine distance between first nextbus stop and start of GTFS shape, # plus distance between last stop and end of GTFS shape, # for all Nextbus directions for this route. start_dist = util.haver_distance(first_stop_info.lat, first_stop_info.lon, shape_start[1], shape_start[0]) end_dist = util.haver_distance(last_stop_info.lat, last_stop_info.lon, shape_end[1], shape_end[0]) terminal_dist = start_dist + end_dist terminal_dists.append(terminal_dist) terminal_dist_order = np.argsort(terminal_dists) best_nextbus_dir_index = terminal_dist_order[0] # index of the "best" shape for this direction, with the minimum terminal_dist best_nextbus_dir_info = nextbus_dir_infos[best_nextbus_dir_index] best_terminal_dist = terminal_dists[best_nextbus_dir_index] return best_nextbus_dir_info, best_terminal_dist
def get_stop_geometry(route, stop_id, xy_geometry, shape_lat, shape_lon, shape_cumulative_dist): stop_info = route.get_stop_info(stop_id) stop_dist_to_shape_coords = util.haver_distance(shape_lat, shape_lon, stop_info.lat, stop_info.lon) closest_index = int(np.argmin(stop_dist_to_shape_coords)) # determine the offset distance between the stop and the line after the closest coord, # and between the stop and the line after the closest coord. # Need to project lon/lat coords to x/y here in order for shapely to determine the distance between # a point and a line (shapely doesn't support distance for lon/lat coords) stop_xy = shapely.geometry.Point(project_xy(stop_info.lon, stop_info.lat)) geom_length = len(xy_geometry.coords) if closest_index < geom_length - 1: next_line = shapely.geometry.LineString(xy_geometry.coords[closest_index:closest_index+2]) next_line_offset = next_line.distance(stop_xy) if closest_index > 0: prev_line = shapely.geometry.LineString(xy_geometry.coords[closest_index-1:closest_index+1]) prev_line_offset = prev_line.distance(stop_xy) if closest_index == 0: stop_after_index = 0 offset = next_line_offset elif closest_index + 1 >= geom_length: stop_after_index = closest_index - 1 offset = prev_line_offset else: offset = min(next_line_offset, prev_line_offset) stop_after_index = closest_index if next_line_offset < prev_line_offset else closest_index - 1 stop_dist = shape_cumulative_dist[stop_after_index] + stop_dist_to_shape_coords[stop_after_index] return { 'distance': int(stop_dist), # total distance in meters along the route shape to this stop 'after_index': stop_after_index, # the index of the coordinate of the shape just before this stop 'offset': int(offset) # distance in meters between this stop and the closest line segment of shape }
print(f"Route: {route_id} ({route_config.title})") dir_infos = route_config.get_direction_infos() for dir_info in dir_infos: print(f"Direction: {dir_info.title} ({dir_info.id})") prev_stop_info = None for dir_index, stop_id in enumerate(dir_info.get_stop_ids()): stop_info = route_config.get_stop_info(stop_id) if prev_stop_info is not None: delta_dist = util.haver_distance(stop_info.lat, stop_info.lon, prev_stop_info.lat, prev_stop_info.lon) else: delta_dist = np.nan stop_arrivals = df[(df['SID'] == stop_info.id) & (df['DID'] == dir_info.id)] dwell_time = (stop_arrivals['DEPARTURE_TIME'] - stop_arrivals['TIME']) if not stop_arrivals.empty: min_dist_quantiles = np.quantile(stop_arrivals['DIST'], [0, 0.5, 1]) dwell_time_quantiles = np.quantile(dwell_time, [0, 0.5, 1]) else: min_dist_quantiles = []
# In order to determine where a Nextbus stop appears along a GTFS shape, this finds the closest coordinate of the # GTFS shape to the Nextbus stop, then determines whether the stop is closer to the line between that GTFS shape coordinate # and the previous GTFS coordinate, or the line between that GTFS shape coordinate and the next GTFS coordinate. # (This may not always be correct for shapes that loop back on themselves.) # # Currently the script just overwrites the one S3 path, but this process could be extended in the future to # store different paths for different dates, to allow fetching historical data for route configurations. # agency_id = 'sf-muni' gtfs_url = 'http://gtfs.sfmta.com/transitdata/google_transit.zip' center_lat = 37.772 center_lon = -122.442 version = 'v2' deg_lat_dist = util.haver_distance(center_lat, center_lon, center_lat-0.1, center_lon)*10 deg_lon_dist = util.haver_distance(center_lat, center_lon, center_lat, center_lon-0.1)*10 # projection function from lon/lat coordinates in degrees (z ignored) to x/y coordinates in meters. # satisfying the interface of shapely.ops.transform (https://shapely.readthedocs.io/en/stable/manual.html#shapely.ops.transform). # This makes it possible to use shapely methods to calculate the distance in meters between geometries def project_xy(lon, lat, z=None): return (round((lon - center_lon) * deg_lon_dist, 1), round((lat - center_lat) * deg_lat_dist, 1)) def get_stop_geometry(route, stop_id, xy_geometry, shape_lat, shape_lon, shape_cumulative_dist): stop_info = route.get_stop_info(stop_id) stop_dist_to_shape_coords = util.haver_distance(shape_lat, shape_lon, stop_info.lat, stop_info.lon) closest_index = int(np.argmin(stop_dist_to_shape_coords))
def add_direction(id, gtfs_shape_id, gtfs_direction_id, stop_ids, title = None): if title is None: default_direction_info = agency.default_directions.get(gtfs_direction_id, {}) title_prefix = default_direction_info.get('title_prefix', None) last_stop_id = stop_ids[-1] last_stop = stops_map[last_stop_id] if title_prefix is not None: title = f"{title_prefix} to {last_stop.stop_name}" else: title = f"To {last_stop.stop_name}" print(f' title = {title}') dir_data = { 'id': id, 'title': title, 'gtfs_shape_id': gtfs_shape_id, 'gtfs_direction_id': gtfs_direction_id, 'stops': stop_ids, 'stop_geometry': {}, } route_data['directions'].append(dir_data) for stop_id in stop_ids: stop = stops_map[stop_id] stop_data = { 'id': stop_id, 'lat': round(stop.geometry.y, 5), # stop_lat in gtfs 'lon': round(stop.geometry.x, 5), # stop_lon in gtfs 'title': stop.stop_name, 'url': stop.stop_url if hasattr(stop, 'stop_url') and isinstance(stop.stop_url, str) else None, } route_data['stops'][stop_id] = stop_data geometry = shapes_df[shapes_df['shape_id'] == gtfs_shape_id]['geometry'].values[0] # partridge returns GTFS geometries for each shape_id as a shapely LineString # (https://shapely.readthedocs.io/en/stable/manual.html#linestrings). # Each coordinate is an array in [lon,lat] format (note: longitude first, latitude second) dir_data['coords'] = [ { 'lat': round(coord[1], 5), 'lon': round(coord[0], 5) } for coord in geometry.coords ] if agency.provider == 'nextbus': # match nextbus direction IDs with GTFS direction IDs best_nextbus_dir_info, best_terminal_dist = match_nextbus_direction(nextbus_route_config, geometry) print(f' {direction_id} = {best_nextbus_dir_info.id} (terminal_dist={int(best_terminal_dist)}) {" (questionable match)" if best_terminal_dist > 300 else ""}') # dir_data['title'] = best_nextbus_dir_info.title dir_data['nextbus_direction_id'] = best_nextbus_dir_info.id start_lat = geometry.coords[0][1] start_lon = geometry.coords[0][0] #print(f" start_lat = {start_lat} start_lon = {start_lon}") deg_lat_dist = util.haver_distance(start_lat, start_lon, start_lat-0.1, start_lon)*10 deg_lon_dist = util.haver_distance(start_lat, start_lon, start_lat, start_lon-0.1)*10 # projection function from lon/lat coordinates in degrees (z ignored) to x/y coordinates in meters. # satisfying the interface of shapely.ops.transform (https://shapely.readthedocs.io/en/stable/manual.html#shapely.ops.transform). # This makes it possible to use shapely methods to calculate the distance in meters between geometries def project_xy(lon, lat, z=None): return (round((lon - start_lon) * deg_lon_dist, 1), round((lat - start_lat) * deg_lat_dist, 1)) xy_geometry = shapely.ops.transform(project_xy, geometry) shape_lon_lat = np.array(geometry).T shape_lon = shape_lon_lat[0] shape_lat = shape_lon_lat[1] shape_prev_lon = np.r_[shape_lon[0], shape_lon[:-1]] shape_prev_lat = np.r_[shape_lat[0], shape_lat[:-1]] # shape_cumulative_dist[i] is the cumulative distance in meters along the shape geometry from 0th to ith coordinate shape_cumulative_dist = np.cumsum(util.haver_distance(shape_lon, shape_lat, shape_prev_lon, shape_prev_lat)) shape_lines_xy = [shapely.geometry.LineString(xy_geometry.coords[i:i+2]) for i in range(0, len(xy_geometry.coords) - 1)] # this is the total distance of the GTFS shape, which may not be exactly the same as the # distance along the route between the first and last Nextbus stop dir_data['distance'] = int(shape_cumulative_dist[-1]) print(f" distance = {dir_data['distance']}") # Find each stop along the route shape, so that the frontend can draw line segments between stops along the shape start_index = 0 for stop_id in stop_ids: stop_info = route_data['stops'][stop_id] # Need to project lon/lat coords to x/y in order for shapely to determine the distance between # a point and a line (shapely doesn't support distance for lon/lat coords) stop_xy = shapely.geometry.Point(project_xy(stop_info['lon'], stop_info['lat'])) stop_geometry = get_stop_geometry(stop_xy, shape_lines_xy, shape_cumulative_dist, start_index) if stop_geometry['offset'] > 100: print(f" !! bad geometry for stop {stop_id}: {stop_geometry['offset']} m from route line segment") continue dir_data['stop_geometry'][stop_id] = stop_geometry start_index = stop_geometry['after_index']