Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull speed restrictions data from DynamoDB #714

Merged
merged 7 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions common/api/hooks/slowzones.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { fetchAllSlow, fetchDelayTotals, fetchSpeedRestrictions } from '../slowzones';
import { ONE_HOUR } from '../../constants/time';
import type { FetchSpeedRestrictionsOptions } from '../../types/api';

export const useSlowzoneAllData = () => {
return useQuery(['allSlow'], fetchAllSlow, { staleTime: ONE_HOUR });
Expand All @@ -10,6 +11,9 @@ export const useSlowzoneDelayTotalData = () => {
return useQuery(['delayTotals'], fetchDelayTotals, { staleTime: ONE_HOUR });
};

export const useSpeedRestrictionData = () => {
return useQuery(['speedRestrictions'], fetchSpeedRestrictions);
export const useSpeedRestrictionData = (options: FetchSpeedRestrictionsOptions) => {
return useQuery(['speedRestrictions', options], () => fetchSpeedRestrictions(options), {
enabled: true,
staleTime: ONE_HOUR,
});
};
20 changes: 16 additions & 4 deletions common/api/slowzones.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type {
SlowZoneResponse,
SpeedRestriction,
} from '../../common/types/dataPoints';
import type { FetchSpeedRestrictionsOptions } from '../types/api';
import { getGtfsRailLineId } from '../utils/lines';

export const fetchDelayTotals = (): Promise<DayDelayTotals[]> => {
const url = new URL(`/static/slowzones/delay_totals.json`, window.location.origin);
Expand All @@ -14,10 +16,20 @@ export const fetchAllSlow = (): Promise<SlowZoneResponse[]> => {
return fetch(all_slow_url.toString()).then((resp) => resp.json());
};

export const fetchSpeedRestrictions = (): Promise<SpeedRestriction[]> => {
const speed_restrictions_url = new URL(
`/static/slowzones/speed_restrictions.json`,
export const fetchSpeedRestrictions = async (
options: FetchSpeedRestrictionsOptions
): Promise<SpeedRestriction[]> => {
const { lineId, date: requestedDate } = options;
const params = new URLSearchParams({ line_id: getGtfsRailLineId(lineId), date: requestedDate });
const speedRestrictionsUrl = new URL(
'/api/speed_restrictions?' + params.toString(),
window.location.origin
);
return fetch(speed_restrictions_url.toString()).then((resp) => resp.json());
const today = new Date();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this may cause issues after 8pm EST as it'll be tomorrow in GMT. I think we try to use dayJs dates throughout the code to avoid this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good callout but in this case we're never going to display the date, we just treat it as a timestamp.

const response = await fetch(speedRestrictionsUrl.toString());
const { available, date: resolvedDate, zones } = await response.json();
if (available) {
return zones.map((zone) => ({ ...zone, currentAsOf: resolvedDate, validAsOf: today }));
}
return [];
};
11 changes: 11 additions & 0 deletions common/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,14 @@ export enum FetchRidershipParams {
startDate = 'start_date',
endDate = 'end_date',
}

export type FetchSpeedRestrictionsOptions = {
lineId: Line;
date: string;
};

export type FetchSpeedRestrictionsResponse = {
available: boolean;
date: string;
zones: Record<string, any>[];
};
5 changes: 3 additions & 2 deletions common/types/dataPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,14 @@ export type SpeedRestriction = {
line: Exclude<LineShort, 'Bus'>;
description: string;
reason: string;
status: string;
fromStopId: null | string;
toStopId: null | string;
reported: string;
cleared: string;
speedMph: number;
trackFeet: number;
currentAsOf: Date;
lineId: Line;
validAsOf: Date;
};

export type DayKind = 'weekday' | 'saturday' | 'sunday';
Expand Down
3 changes: 3 additions & 0 deletions common/types/lines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export const RIDERSHIP_KEYS = {
'line-green': 'line-Green',
};

export const GTFS_COLOR_LINE_IDS = ['line-Red', 'line-Orange', 'line-Blue', 'line-Green'] as const;
export type GtfsColorLineId = (typeof GTFS_COLOR_LINE_IDS)[number];

export const HEAVY_RAIL_LINES: Line[] = ['line-red', 'line-orange', 'line-blue'];
export const LANDING_RAIL_LINES: Line[] = ['line-red', 'line-orange', 'line-blue', 'line-green'];

Expand Down
4 changes: 3 additions & 1 deletion common/utils/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export const prettyDate = (dateString: string, withDow: boolean) => {
weekday: withDow ? 'long' : undefined,
};

const fullDate = dateString.includes('T') ? dateString : `${dateString}T00:00:00`;
const fullDate = dateString.includes('T')
? dateString
: /* Offset so that it's always past midnight in Boston */ `${dateString}T07:00:00`;

return new Date(fullDate).toLocaleDateString(
undefined, // user locale/language
Expand Down
16 changes: 11 additions & 5 deletions common/utils/lines.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { LINE_OBJECTS } from '../constants/lines';
import type { LineShort } from '../types/lines';
import type { GtfsColorLineId, Line, LineShort } from '../types/lines';
import { RAIL_LINES } from '../types/lines';

export const shortToLine = (name: LineShort): Exclude<LineShort, 'Bus'> => {
const found = Object.entries(LINE_OBJECTS).find(([, line]) => line.short === name);
return found?.[0] as Exclude<LineShort, 'Bus'>;
export const getGtfsRailLineId = (name: Line | LineShort): GtfsColorLineId => {
const normalizedColorPart = (
name.includes('line-') ? name.replace('line-', '') : name
).toLowerCase();
if (!RAIL_LINES.includes(normalizedColorPart)) {
throw new Error('Not a valid rail line ID');
}
const capped = normalizedColorPart.slice(0, 1).toUpperCase() + normalizedColorPart.slice(1);
return `line-${capped}` as GtfsColorLineId;
};
6 changes: 5 additions & 1 deletion modules/dashboard/Today.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SlowZonesMap } from '../slowzones/map';
import { WidgetDiv } from '../../common/components/widgets/WidgetDiv';
import { useSlowzoneAllData, useSpeedRestrictionData } from '../../common/api/hooks/slowzones';
import { PageWrapper } from '../../common/layouts/PageWrapper';
import type { Line } from '../../common/types/lines';
import { WidgetTitle } from './WidgetTitle';

interface TodayProps {
Expand All @@ -13,7 +14,10 @@ interface TodayProps {

export const Today: React.FC<TodayProps> = ({ lineShort }) => {
const allSlow = useSlowzoneAllData();
const speedRestrictions = useSpeedRestrictionData();
const speedRestrictions = useSpeedRestrictionData({
lineId: `line-${lineShort.toLowerCase()}` as Line,
date: new Date().toISOString().split('T')[0],
});
const canShowSlowZonesMap = lineShort !== 'Green';

return (
Expand Down
8 changes: 5 additions & 3 deletions modules/slowzones/SlowZonesDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,19 @@ import { SlowZonesWidgetTitle } from './SlowZonesWidgetTitle';
dayjs.extend(utc);

export function SlowZonesDetails() {
const delayTotals = useSlowzoneDelayTotalData();
const [direction, setDirection] = useState<Direction>('northbound');
const allSlow = useSlowzoneAllData();
const speedRestrictions = useSpeedRestrictionData();

const {
lineShort,
linePath,
line,
query: { startDate, endDate },
} = useDelimitatedRoute();

const delayTotals = useSlowzoneDelayTotalData();
const allSlow = useSlowzoneAllData();
const speedRestrictions = useSpeedRestrictionData({ lineId: line!, date: endDate! });

const startDateUTC = startDate ? dayjs.utc(startDate).startOf('day') : undefined;
const endDateUTC = endDate ? dayjs.utc(endDate).startOf('day') : undefined;
const totalSlowTimeReady =
Expand Down
2 changes: 1 addition & 1 deletion modules/slowzones/map/SlowZonesTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const SlowZonesTooltip: React.FC<SlowZonesTooltipProps> = (props) => {
const totalFeet = speedRestrictions.reduce((sum, sr) => sum + sr.trackFeet, 0);
const [oldest] = speedRestrictions
.filter((sr) => sr.reported)
.map((sr) => new Date(sr.reported).toISOString())
.map((sr) => sr.reported)
.sort();
const oldestString = prettyDate(oldest, false);
return (
Expand Down
4 changes: 2 additions & 2 deletions modules/slowzones/map/segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ export const segmentSlowZones = (options: SegmentSlowZonesOptions): Segmentation
speedRestrictions,
lineName,
effectiveDate,
(rs) => new Date(rs.cleared ?? '4000'),
(rs) => rs.line
(rs) => rs.validAsOf,
(rs) => rs.lineId.replace('line-', '') as LineShort
),
(rs) => getStationById(rs.fromStopId!),
(rs) => getStationById(rs.toStopId!)
Expand Down
27 changes: 24 additions & 3 deletions server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
secrets,
mbta_v3,
speed,
speed_restrictions,
service_levels,
ridership,
)
Expand Down Expand Up @@ -47,7 +48,9 @@ def healthcheck():
"API Key Present": (lambda: len(secrets.MBTA_V2_API_KEY) > 0),
"S3 Headway Fetching": (
lambda: "2020-11-07 10:33:40"
in json.dumps(data_funcs.headways(date(year=2020, month=11, day=7), ["70061"]))
in json.dumps(
data_funcs.headways(date(year=2020, month=11, day=7), ["70061"])
)
),
"Performance API Check": (
lambda: MbtaPerformanceAPI.get_api_data(
Expand Down Expand Up @@ -109,7 +112,9 @@ def traveltime_route(user_date):
@app.route("/api/alerts/{user_date}", cors=cors_config)
def alerts_route(user_date):
date = parse_user_date(user_date)
return json.dumps(data_funcs.alerts(date, mutlidict_to_dict(app.current_request.query_params)))
return json.dumps(
data_funcs.alerts(date, mutlidict_to_dict(app.current_request.query_params))
)


@app.route("/api/aggregate/traveltimes", cors=cors_config)
Expand Down Expand Up @@ -158,7 +163,11 @@ def dwells_aggregate_route():
def get_git_id():
# Only do this on localhost
if TM_FRONTEND_HOST == "localhost":
git_id = str(subprocess.check_output(["git", "describe", "--always", "--dirty", "--abbrev=10"]))[2:-3]
git_id = str(
subprocess.check_output(
["git", "describe", "--always", "--dirty", "--abbrev=10"]
)
)[2:-3]
return json.dumps({"git_id": git_id})
else:
raise ConflictError("Cannot get git id from serverless host")
Expand Down Expand Up @@ -204,3 +213,15 @@ def get_ridership():
line_id=line_id,
)
return json.dumps(response)


@app.route("/api/speed_restrictions", cors=cors_config)
def get_speed_restrictions():
query = app.current_request.query_params
on_date = query["date"]
line_id = query["line_id"]
response = speed_restrictions.query_speed_restrictions(
line_id=line_id,
on_date=on_date,
)
return json.dumps(response)
36 changes: 36 additions & 0 deletions server/chalicelib/speed_restrictions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from boto3.dynamodb.conditions import Key
from dynamodb_json import json_util as ddb_json

from .dynamo import dynamodb

SpeedRestrictions = dynamodb.Table("SpeedRestrictions")


def get_boundary_date(line_id: str, first: bool):
response = SpeedRestrictions.query(
KeyConditionExpression=Key("lineId").eq(line_id),
ScanIndexForward=first,
Limit=1,
)
loaded = ddb_json.loads(response["Items"])
return loaded[0]["date"]


def query_speed_restrictions(line_id: str, on_date: str):
first_sr_date = get_boundary_date(line_id=line_id, first=True)
latest_sr_date = get_boundary_date(line_id=line_id, first=False)
if on_date < first_sr_date:
return {"available": False}
if on_date > latest_sr_date:
on_date = latest_sr_date
line_condition = Key("lineId").eq(line_id)
date_condition = Key("date").eq(on_date)
condition = line_condition & date_condition
response = SpeedRestrictions.query(KeyConditionExpression=condition, Limit=1)
response_item = ddb_json.loads(response["Items"])[0]
zones = response_item.get("zones", {}).get("zones", {})
return {
"available": True,
"date": on_date,
"zones": zones,
}
Loading