Due to data not being cleaned yet, today's data may not be fully accurate and may look
messy.
diff --git a/common/components/notices/TerminusNotice.tsx b/common/components/notices/TerminusNotice.tsx
index 009ca6e74..98f6e00e8 100644
--- a/common/components/notices/TerminusNotice.tsx
+++ b/common/components/notices/TerminusNotice.tsx
@@ -14,7 +14,7 @@ export const TerminusNotice: React.FC = ({ toStation, fromS
return (
-
+
Due to data collection issues at terminus stations, data is not guaranteed to be
complete.
diff --git a/common/components/widgets/WidgetPage.tsx b/common/components/widgets/WidgetPage.tsx
index 4456f4cb2..8e45da6e4 100644
--- a/common/components/widgets/WidgetPage.tsx
+++ b/common/components/widgets/WidgetPage.tsx
@@ -1,6 +1,5 @@
import classNames from 'classnames';
import React from 'react';
-import { BusDataNotice } from '../notices/BusDataNotice';
interface WidgetPageProps {
children?: React.ReactNode;
@@ -10,7 +9,6 @@ export const WidgetPage: React.FC = ({ children }) => {
return (
{children}
-
);
};
diff --git a/common/constants/dates.ts b/common/constants/dates.ts
index fc7f1062d..279cd28b3 100644
--- a/common/constants/dates.ts
+++ b/common/constants/dates.ts
@@ -36,7 +36,7 @@ const OVERVIEW_TRAIN_MIN_DATE = '2016-02-01';
const TRAIN_MIN_DATE = '2016-01-15';
const BUS_MIN_DATE = '2018-08-01';
export const BUS_MAX_DATE = '2023-12-31';
-const BUS_MAX_DAY = dayjs(BUS_MAX_DATE);
+export const BUS_MAX_DAY = dayjs(BUS_MAX_DATE);
export const BUS_MAX_DATE_MINUS_ONE_WEEK = dayjs(BUS_MAX_DATE)
.subtract(7, 'days')
.format(DATE_FORMAT);
diff --git a/modules/ridership/RidershipDetails.tsx b/modules/ridership/RidershipDetails.tsx
index e9abe39f3..3fbe3bd2f 100644
--- a/modules/ridership/RidershipDetails.tsx
+++ b/modules/ridership/RidershipDetails.tsx
@@ -18,7 +18,6 @@ export function RidershipDetails() {
} = useDelimitatedRoute();
const config = SPEED_RANGE_PARAM_MAP.week;
const lineId = getRidershipLineId(line, busRoute);
- const lineOrRoute = busRoute ? `line-${busRoute.replaceAll('/', '')}` : line;
const enabled = Boolean(startDate && endDate && lineId);
const ridership = useRidershipData(
@@ -29,7 +28,7 @@ export function RidershipDetails() {
},
enabled
);
- const ridershipDataReady = !ridership.isError && startDate && endDate && lineOrRoute && line;
+ const ridershipDataReady = !ridership.isError && startDate && endDate && line;
return (
diff --git a/modules/ridership/RidershipWidget.tsx b/modules/ridership/RidershipWidget.tsx
index 1243fb590..1a5cce7ed 100644
--- a/modules/ridership/RidershipWidget.tsx
+++ b/modules/ridership/RidershipWidget.tsx
@@ -16,13 +16,12 @@ export const RidershipWidget: React.FC = () => {
const endDate = TODAY_STRING;
const config = getSpeedGraphConfig(dayjs(startDate), dayjs(endDate));
const lineId = getRidershipLineId(line, query.busRoute);
- const lineOrRoute = query.busRoute ? `line-${query.busRoute.replaceAll('/', '')}` : line;
const ridership = useRidershipData({
line_id: lineId,
start_date: startDate,
end_date: endDate,
});
- const serviceReady = !ridership.isError && lineId && line && lineOrRoute;
+ const serviceReady = !ridership.isError && lineId && line;
return (
diff --git a/modules/tripexplorer/TripExplorer.tsx b/modules/tripexplorer/TripExplorer.tsx
index b87ec16c0..bf3546e1a 100644
--- a/modules/tripexplorer/TripExplorer.tsx
+++ b/modules/tripexplorer/TripExplorer.tsx
@@ -9,6 +9,8 @@ import { ChartPageDiv } from '../../common/components/charts/ChartPageDiv';
import { Layout } from '../../common/layouts/layoutTypes';
import { useDelimitatedRoute } from '../../common/utils/router';
import { getParentStationForStopId } from '../../common/utils/stations';
+import { BusDataNotice } from '../../common/components/notices/BusDataNotice';
+import { GobbleDataNotice } from '../../common/components/notices/GobbleDataNotice';
import { useAlertStore } from './AlertStore';
import { TripGraphs } from './TripGraphs';
@@ -37,8 +39,12 @@ export const TripExplorer = () => {
{alertsForModal?.length ? : null}
-
-
+
+
+
+
+
+
);
diff --git a/server/chalicelib/s3.py b/server/chalicelib/s3.py
index 042ce13a4..c529fa88e 100644
--- a/server/chalicelib/s3.py
+++ b/server/chalicelib/s3.py
@@ -28,16 +28,27 @@ def upload(key, bytes, compress=True):
s3.put_object(Bucket=BUCKET, Key=key, Body=bytes)
-def is_bus(stop_id):
+def is_bus(stop_id: str):
return ("-0-" in stop_id) or ("-1-" in stop_id)
-def download_one_event_file(date, stop_id):
+def get_live_folder(stop_id: str):
+ if is_bus(stop_id):
+ return "daily-bus-data"
+ else:
+ return "daily-rapid-data"
+
+
+def download_one_event_file(date, stop_id: str, use_live_data=False):
"""As advertised: single event file from s3"""
- year, month = date.year, date.month
+ year, month, day = date.year, date.month, date.day
- folder = "monthly-bus-data" if is_bus(stop_id) else "monthly-data"
- key = f"Events/{folder}/{stop_id}/Year={year}/Month={month}/events.csv.gz"
+ if use_live_data:
+ folder = get_live_folder(stop_id)
+ key = f"Events-live/{folder}/{stop_id}/Year={year}/Month={month}/Day={day}/events.csv.gz"
+ else:
+ folder = "monthly-bus-data" if is_bus(stop_id) else "monthly-data"
+ key = f"Events/{folder}/{stop_id}/Year={year}/Month={month}/events.csv.gz"
# Download events from S3
try:
@@ -47,6 +58,8 @@ def download_one_event_file(date, stop_id):
if ex.response["Error"]["Code"] == "NoSuchKey":
# raise Exception(f"Data not available on S3 for key {key} ") from None
print(f"WARNING: No data available on S3 for key: {key}")
+ if not use_live_data and is_bus(stop_id):
+ return download_one_event_file(date, stop_id, use_live_data=True)
return []
else:
raise
@@ -67,8 +80,23 @@ def parallel_download_events(datestop):
return download_one_event_file(date, stop)
-def download_events(sdate, edate, stops):
- datestops = itertools.product(parallel.month_range(sdate, edate), stops)
+def download_events(sdate, edate, stops: list):
+ # This used to be month_range but updated to date_range to support live ranges
+ # If something breaks, this may be why
+ datestops = itertools.product(parallel.date_range(sdate, edate), stops)
result = parallel_download_events(datestops)
result = filter(lambda row: sdate.strftime("%Y-%m-%d") <= row["service_date"] <= edate.strftime("%Y-%m-%d"), result)
return sorted(result, key=lambda row: row["event_time"])
+
+
+def get_all_s3_objects(s3, **base_kwargs):
+ continuation_token = None
+ while True:
+ list_kwargs = dict(MaxKeys=1000, **base_kwargs)
+ if continuation_token:
+ list_kwargs["ContinuationToken"] = continuation_token
+ response = s3.list_objects_v2(**list_kwargs)
+ yield from response.get("Contents", [])
+ if not response.get("IsTruncated"): # At the end of the list?
+ break
+ continuation_token = response.get("NextContinuationToken")
diff --git a/server/chalicelib/s3_historical.py b/server/chalicelib/s3_historical.py
index df64d4374..f0346f662 100644
--- a/server/chalicelib/s3_historical.py
+++ b/server/chalicelib/s3_historical.py
@@ -32,7 +32,7 @@ def unique_everseen(iterable, key=None):
yield element
-def dwells(stop_ids, sdate, edate):
+def dwells(stop_ids: list, sdate, edate):
rows_by_time = s3.download_events(sdate, edate, stop_ids)
dwells = []
@@ -59,7 +59,7 @@ def dwells(stop_ids, sdate, edate):
return dwells
-def headways(stop_ids, sdate, edate):
+def headways(stop_ids: list, sdate, edate):
rows_by_time = s3.download_events(sdate, edate, stop_ids)
only_departures = filter(lambda row: row["event_type"] in EVENT_DEPARTURE, rows_by_time)
@@ -88,7 +88,7 @@ def headways(stop_ids, sdate, edate):
{
"route_id": this["route_id"],
"direction": this["direction_id"],
- "current_dep_dt": this["event_time"],
+ "current_dep_dt": date_utils.return_formatted_date(this_dt),
"headway_time_sec": headway_time_sec,
"benchmark_headway_time_sec": benchmark_headway,
}
@@ -97,7 +97,7 @@ def headways(stop_ids, sdate, edate):
return headways
-def travel_times(stops_a, stops_b, sdate, edate):
+def travel_times(stops_a: list, stops_b: list, sdate, edate):
rows_by_time_a = s3.download_events(sdate, edate, stops_a)
rows_by_time_b = s3.download_events(sdate, edate, stops_b)
diff --git a/server/rapid/process_events.py b/server/rapid/process_events.py
index 6c96d0ef8..0370ab622 100644
--- a/server/rapid/process_events.py
+++ b/server/rapid/process_events.py
@@ -21,7 +21,6 @@ def process_events(input_csv, outdir, nozip=False):
input_csv,
usecols=columns,
parse_dates=["service_date"],
- infer_datetime_format=True,
dtype={
"route_id": "str",
"trip_id": "str",
diff --git a/server/rapid/setup_rapid_input.sh b/server/rapid/setup_rapid_input.sh
index 79a4af751..90994d0e4 100755
--- a/server/rapid/setup_rapid_input.sh
+++ b/server/rapid/setup_rapid_input.sh
@@ -14,7 +14,7 @@ wget -N -O data/input/2023.zip https://www.arcgis.com/sharing/rest/content/items
cd data/input
for i in `seq 2017 2023`; do
- unzip -d $i $i.zip
+ unzip -o -d $i $i.zip
done
# The following years only have single csv files
diff --git a/server/scripts/generate_line_files.py b/server/scripts/generate_line_files.py
index ee8bf5669..52f0d5f32 100644
--- a/server/scripts/generate_line_files.py
+++ b/server/scripts/generate_line_files.py
@@ -4,6 +4,15 @@
import boto3
import botocore
+from chalicelib.s3 import get_all_s3_objects
+
+"""
+To run, run as
+
+poetry shell
+python -m scripts.generate_line_files
+"""
+
MBTA_V3_API_KEY = os.environ.get("MBTA_V3_API_KEY", "")
BUCKET = "tm-mbta-performance"
@@ -20,52 +29,57 @@
"CR-Needham",
"CR-Newburyport",
"CR-Providence",
- "CR-Foxboro",
]
def get_line_stops():
s3 = boto3.client("s3", config=botocore.client.Config(max_pool_connections=15))
- objects = s3.list_objects_v2(Bucket=BUCKET, Prefix="Events-live/daily-cr-data/{}".format(LINE_KEY))
+ objects = []
+
+ for file in get_all_s3_objects(s3, Bucket=BUCKET, Prefix="Events-live/daily-cr-data/{}".format(LINE_KEY)):
+ objects.append(file)
+
+ stop_names = set()
- stop_ids = set()
+ for obj in objects:
+ stop_names.add(obj["Key"].split("/")[2])
- for obj in objects["Contents"]:
- stop_ids.add(parse_s3_cr_uri(obj)[2])
+ stop_ids = []
+
+ stop_prefix_len = len(LINE_KEY) + 3
+ for stop in stop_names:
+ stop_ids.append({"id": stop[stop_prefix_len:], "name": stop})
parent_children_map = {}
for stop in stop_ids:
+ _, direction, stop_id = parse_stop_name(stop["name"])
+
r_f = requests.get(
- "https://api-v3.mbta.com/stops/{}?include=parent_station&api_key={}".format(stop["id"], MBTA_V3_API_KEY)
+ "https://api-v3.mbta.com/stops/{}?include=parent_station&api_key={}".format(stop_id, MBTA_V3_API_KEY)
)
stop_details = r_f.json()
parent_id = stop_details["data"]["relationships"]["parent_station"]["data"]["id"]
if parent_id not in parent_children_map:
- if stop["direction"] == "0":
- parent_children_map[parent_id] = {"0": [stop["stop_id"]], "1": []}
+ if direction == "0":
+ parent_children_map[parent_id] = {"0": [stop["name"]], "1": []}
else:
- parent_children_map[parent_id] = {"0": [], "1": [stop["stop_id"]]}
+ parent_children_map[parent_id] = {"0": [], "1": [stop["name"]]}
else:
- parent_children_map[parent_id][stop["direction"]].append(stop["stop_id"])
+ parent_children_map[parent_id][direction].append(stop["name"])
return parent_children_map
-def parse_s3_cr_uri(uri: str):
+def parse_stop_name(stop_name: str):
"""
- Parse a CR s3 URI beginning with Events-live
+ Parse a CR stop id into its components
"""
- _, _, stop_name, year, month, day, _ = uri["Key"].split("/")
line, direction, stop_id = stop_name.split("_")
- return {"line": line, "direction": direction, "stop_id": stop_id, "year": year, "month": month, "day": day}
-
-
-def cr_stop_info_to_s3_prefix(line, direction, stop_id):
- return "{}_{}_{}".format(line, direction, stop_id)
+ return line, direction, stop_id
for LINE_KEY in ROUTES_CR:
@@ -74,16 +88,42 @@ def cr_stop_info_to_s3_prefix(line, direction, stop_id):
stop_layout = get_line_stops()
- stops_formatted = [
- {
- "stop_name": stop["attributes"]["name"],
- "station": stop["id"],
- "branches": None,
- "order": index + 1,
- "stops": stop_layout[stop["id"]],
- }
- for index, stop in enumerate(stops["data"])
- ]
+ stops_formatted = []
+
+ for index, stop in enumerate(stops["data"]):
+ try:
+ stops_formatted.append(
+ {
+ "stop_name": stop["attributes"]["name"],
+ "station": stop["id"],
+ "branches": None,
+ "order": index + 1,
+ "stops": stop_layout[stop["id"]],
+ }
+ )
+ except KeyError:
+ c_f = requests.get(
+ "https://api-v3.mbta.com/stops/{}?include=child_stops&api_key={}".format(stop["id"], MBTA_V3_API_KEY)
+ )
+ stop_details = c_f.json()
+
+ child_stops = [
+ child_stop["id"] for child_stop in stop_details["data"]["relationships"]["child_stops"]["data"]
+ ]
+ stops_map = {
+ "0": [f"{LINE_KEY}_0_{stop}" for stop in child_stops],
+ "1": [f"{LINE_KEY}_1_{stop}" for stop in child_stops],
+ }
+
+ stops_formatted.append(
+ {
+ "stop_name": stop["attributes"]["name"],
+ "station": stop["id"],
+ "branches": None,
+ "order": index + 1,
+ "stops": stops_map,
+ }
+ )
output = {
LINE_KEY: {
@@ -94,5 +134,5 @@ def cr_stop_info_to_s3_prefix(line, direction, stop_id):
}
out_json = json.dumps(output, indent=2)
- with open("../common/constants/cr_constants/{}.json".format(LINE_KEY.lower()), "w") as f:
+ with open("../common/constants/cr_constants/{}.json".format(LINE_KEY.lower()), "w+") as f:
f.write(out_json)
diff --git a/styles/dashboard.css b/styles/dashboard.css
index 1280f2e34..7aefb6d11 100644
--- a/styles/dashboard.css
+++ b/styles/dashboard.css
@@ -158,4 +158,9 @@
.selected-date-line-bus {
background: #FFE395 !important;
border-color: #FFE395 !important;
+}
+
+.selected-date-line-commuter-rail {
+ background: #a95896 !important;
+ border-color: #a95896 !important;
}
\ No newline at end of file