Skip to content

Commit

Permalink
feat: show googly eyes 👀 (#212)
Browse files Browse the repository at this point in the history
Show googly eyes on the 4 green line trains that currently have them 👀
  • Loading branch information
rudiejd authored Jul 10, 2024
1 parent f89b383 commit c87953f
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 38 deletions.
30 changes: 26 additions & 4 deletions server/chalicelib/fleet.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""
fleet.py provides functions used to determine whether vehicles are "new" or not
fleet.py provides functions used to determine information about vehicles
"new" encompasses
- CRRC-built trainsets for the Orange and Red Lines
- CAF-built (Type-9) trainsets for the Green Line
- BEBs (Battery Electric Buses) for the Silver Line
"eyes" encompasses trains that have googly eyes 👀
"""

from chalicelib.routes import GREEN_ROUTE_IDS, SILVER_ROUTE_IDS
Expand All @@ -16,24 +18,44 @@
silver_is_new = lambda x: int(x) >= 1294 and int(x) <= 1299
blue_is_new = lambda _: False

red_has_eyes = lambda _: False
green_has_eyes = lambda x: int(x) in [3909, 3864, 3918, 3639]
blue_has_eyes = lambda _: False
orange_has_eyes = lambda _: False
silver_has_eyes = lambda _: False
blue_has_eyes = lambda _: False


def get_is_new_dict(route_ids, test_fn):
def get_route_test_function_dict(route_ids, test_fn):
return {route_id: test_fn for route_id in route_ids}


vehicle_is_new_func = {
"Red-A": red_is_new,
"Red-B": red_is_new,
"Orange": orange_is_new,
**get_is_new_dict(GREEN_ROUTE_IDS, green_is_new),
**get_is_new_dict(SILVER_ROUTE_IDS, silver_is_new),
**get_route_test_function_dict(GREEN_ROUTE_IDS, green_is_new),
**get_route_test_function_dict(SILVER_ROUTE_IDS, silver_is_new),
"Blue": blue_is_new,
}

vehicle_has_eyes_func = {
"Red-A": red_has_eyes,
"Red-B": red_has_eyes,
"Orange": orange_has_eyes,
**get_route_test_function_dict(GREEN_ROUTE_IDS, green_has_eyes),
**get_route_test_function_dict(SILVER_ROUTE_IDS, silver_has_eyes),
"Blue": blue_has_eyes,
}


def vehicle_is_new(route_name, car):
return vehicle_is_new_func[route_name](car)


def vehicle_array_is_new(route_name, arr):
return any(map(vehicle_is_new_func[route_name], arr))


def vehicle_array_has_eyes(route_name, arr):
return any(map(vehicle_has_eyes_func[route_name], arr))
2 changes: 2 additions & 0 deletions server/chalicelib/mbta_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ async def vehicle_data_for_routes(route_ids):

# determine if vehicle is new
is_new = fleet.vehicle_array_is_new(custom_route, vehicle["label"].split("-"))
has_eyes = fleet.vehicle_array_has_eyes(custom_route, vehicle["label"].split("-"))

vehicles_to_display.append(
{
Expand All @@ -138,6 +139,7 @@ async def vehicle_data_for_routes(route_ids):
"stationId": vehicle["stop"]["parent_station"]["id"],
"tripId": vehicle["trip"]["id"],
"isNewTrain": is_new,
"hasGooglyEyes": has_eyes,
"updatedAt": vehicle["updated_at"]
}
)
Expand Down
8 changes: 4 additions & 4 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import { LineStats } from './LineStats/LineStats';
import { setCssVariable } from './util';

import favicon from '../../public/images/favicon.png';
import { AgeTabPicker } from './AgeTabPicker';
import { CategoryTabPicker } from './CategoryTabPicker';
import { Line as TLine } from '../types';

import { useSearchParams } from 'react-router-dom';
import { useLineSearchParam, useAgeSearchParam } from '../hooks/searchParams';
import { useLineSearchParam, useCategorySearchParam } from '../hooks/searchParams';
import { TrophySpin } from 'react-loading-indicators';

const lineByTabId: Record<string, TLine> = {
Expand All @@ -29,7 +29,7 @@ const lineByTabId: Record<string, TLine> = {
export const App: React.FC = () => {
const [searchParams] = useSearchParams();
const [lineSearchParam, setLineSearchParam] = useLineSearchParam();
const [ageSearchParam] = useAgeSearchParam();
const [ageSearchParam] = useCategorySearchParam();

const api = useMbtaApi(Object.values(lineByTabId), ageSearchParam);
const selectedLine = lineByTabId[lineSearchParam];
Expand Down Expand Up @@ -77,7 +77,7 @@ export const App: React.FC = () => {
const renderControls = () => {
return (
<div className={'selectors'}>
<AgeTabPicker tabColor={selectedLine.color} />
<CategoryTabPicker tabColor={selectedLine.color} />
{api.trainsByRoute && (
<LineTabPicker
lines={Object.values(lineByTabId)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { useRef, useLayoutEffect } from 'react';
import { TabList, Tab, TabProvider } from '@ariakit/react';
import { VehiclesAge } from '../types';
import { useAgeSearchParam } from '../hooks/searchParams';
import { VehicleCategory } from '../types';
import { useCategorySearchParam } from '../hooks/searchParams';

type TrainAge = { key: VehiclesAge; label: string };
type TrainCategory = { key: VehicleCategory; label: string };

const trainTypes: TrainAge[] = [
const trainTypes: TrainCategory[] = [
{ key: 'old_vehicles', label: 'Old' },
{ key: 'new_vehicles', label: 'New' },
{ key: 'googly_eyes_vehicles', label: '👀' },
{ key: 'vehicles', label: 'All' },
];

interface AgeTabPickerProps {
interface CategoryTabPickerProps {
tabColor: string;
}

export const AgeTabPicker: React.FC<AgeTabPickerProps> = ({ tabColor }) => {
export const CategoryTabPicker: React.FC<CategoryTabPickerProps> = ({ tabColor }) => {
// Get train age ID from serach params
const [ageSearchParam, setAgeSearchParam] = useAgeSearchParam();
const [categorySearchParam, setCategorySearchParam] = useCategorySearchParam();

const wrapperRef = useRef<HTMLDivElement>(null);
const selectedIndicatorRef = useRef<HTMLDivElement>(null);
Expand All @@ -26,14 +27,16 @@ export const AgeTabPicker: React.FC<AgeTabPickerProps> = ({ tabColor }) => {
const { current: wrapper } = wrapperRef;
const { current: selectedIndicator } = selectedIndicatorRef;
if (wrapper && selectedIndicator) {
const selectedEl = wrapper.querySelector(`#${ageSearchParam}`) as HTMLElement | null;
const selectedEl = wrapper.querySelector(
`#${categorySearchParam}`
) as HTMLElement | null;
if (selectedEl) {
selectedIndicator.style.width = selectedEl.clientWidth + 'px';
selectedIndicator.style.transform = `translateX(${selectedEl.offsetLeft}px)`;
selectedIndicator.style.backgroundColor = tabColor;
}
}
}, [tabColor, ageSearchParam]);
}, [tabColor, categorySearchParam]);

return (
<TabProvider>
Expand All @@ -48,7 +51,7 @@ export const AgeTabPicker: React.FC<AgeTabPickerProps> = ({ tabColor }) => {
key={trainType.key}
data-color={tabColor}
onClick={() => {
setAgeSearchParam(trainType.key);
setCategorySearchParam(trainType.key);
}}
>
<div
Expand Down
13 changes: 7 additions & 6 deletions src/components/Line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,25 @@ import classNames from 'classnames';
import { prerenderLine } from '../prerender';
import { renderTextTrainlabel } from '../labels';

import { Train } from './Train';
import { TrainDisplay } from './TrainDisplay';
import { PopoverContainerContext, getTrainRoutePairsForLine, setCssVariable } from './util';
import { Line as TLine, Pair, StationPositions, VehiclesAge } from '../types';
import { Line as TLine, Pair, StationPositions, VehicleCategory } from '../types';
import { MBTAApi } from '../hooks/useMbtaApi';
import { useLastSightingByLine } from '../hooks/useLastSighting';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';

const AGE_WORD_MAP = new Map<VehiclesAge, string>([
const AGE_WORD_MAP = new Map<VehicleCategory, string>([
['new_vehicles', ' new '],
['old_vehicles', ' old '],
['googly_eyes_vehicles', ' googly-eyed '],
['vehicles', ' '],
]);

interface LineProps {
api: MBTAApi;
line: TLine;
age: VehiclesAge;
age: VehicleCategory;
}

dayjs.extend(relativeTime);
Expand Down Expand Up @@ -49,7 +50,7 @@ const sortTrainRoutePairsByDistance = (pairs: Pair[], stationPositions: StationP
return pairs.sort((a, b) => distanceMap.get(a) - distanceMap.get(b));
};

const EmptyNoticeForLine: React.FC<{ line: string; age: VehiclesAge }> = ({ line, age }) => {
const EmptyNoticeForLine: React.FC<{ line: string; age: VehicleCategory }> = ({ line, age }) => {
const sightingForLine = useLastSightingByLine(line);

const ageWord = AGE_WORD_MAP.get(age);
Expand Down Expand Up @@ -151,7 +152,7 @@ export const Line: React.FC<LineProps> = ({ api, line, age }) => {

const renderTrains = () => {
return sortedTrainRoutePairs.map(({ train, route }, index) => (
<Train
<TrainDisplay
focusOnMount={shouldFocusOnFirstTrain && index === 0}
key={train.label}
train={train}
Expand Down
43 changes: 39 additions & 4 deletions src/components/Train.tsx → src/components/TrainDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { elementScrollIntoView } from 'seamless-scroll-polyfill';
import { interpolateTrainOffset } from '../interpolation';
import { PopoverContainerContext, prefersReducedMotion } from './util';
import { TrainPopover } from './TrainPopover';
import { Route, Train } from '../types';

const getSpringConfig = () => {
if (prefersReducedMotion()) {
Expand Down Expand Up @@ -37,7 +38,23 @@ const drawEquilateralTriangle = (radius) =>
.reduce((a, b) => `${a} ${b}`)
.trim();

export const Train = ({ train, route, colors, focusOnMount, labelPosition, onFocus, onBlur }) => {
export const TrainDisplay = ({
train,
route,
colors,
focusOnMount,
labelPosition,
onFocus,
onBlur,
}: {
train: Train;
route: Route;
colors: Record<string, string>;
focusOnMount: boolean;
labelPosition: string | undefined;
onFocus: () => void;
onBlur: () => void;
}) => {
const { direction } = train;
const { pathInterpolator, stations } = route;

Expand Down Expand Up @@ -78,7 +95,7 @@ export const Train = ({ train, route, colors, focusOnMount, labelPosition, onFoc
}
}, [element, shouldAutoFocus]);

const renderTrainMarker = () => {
const renderTrainMarker = (hasGooglyEyes: boolean) => {
return (
<g>
<circle
Expand All @@ -87,16 +104,34 @@ export const Train = ({ train, route, colors, focusOnMount, labelPosition, onFoc
r={3.326}
fill={colors.train}
stroke={isTracked ? 'white' : undefined}
textAnchor="middle"
/>
<polygon points={drawEquilateralTriangle(2)} fill={'white'} />
{hasGooglyEyes ? (
<>
<circle
cx={2}
cy={3}
r={2}
fill={'black'}
stroke={isTracked ? 'white' : undefined}
textAnchor="middle"
/>
<text fontSize={3} x={1} y={-1} transform="rotate(90)">
👀
</text>
</>
) : (
<></>
)}
</g>
);
};

return (
<Spring to={{ offset }} config={getSpringConfig()}>
{(spring) => {
const { x, y, theta } = pathInterpolator(spring.offset);
const { x, y, theta } = pathInterpolator!(spring.offset);
const correctedTheta = direction === 1 ? 180 + theta : theta;
return (
<>
Expand All @@ -110,7 +145,7 @@ export const Train = ({ train, route, colors, focusOnMount, labelPosition, onFoc
onClick={() => element?.focus()}
onBlur={handleBlur}
>
{renderTrainMarker()}
{renderTrainMarker(train.hasGooglyEyes)}
</g>
{popoverContainer && element && (
<TrainPopover
Expand Down
12 changes: 6 additions & 6 deletions src/hooks/searchParams.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useSearchParams } from 'react-router-dom';
import { Line, VehiclesAge } from '../types';
import { Line, VehicleCategory } from '../types';

// Read and update line ID search parameters
export const useLineSearchParam = (): [string, (newLine: Line) => void] => {
Expand All @@ -16,15 +16,15 @@ export const useLineSearchParam = (): [string, (newLine: Line) => void] => {
};

// Read and update train age search parameters
export const useAgeSearchParam = (): [VehiclesAge, (newAge: VehiclesAge) => void] => {
export const useCategorySearchParam = (): [VehicleCategory, (newAge: VehicleCategory) => void] => {
const [searchParams, setSearchParams] = useSearchParams();

const age = searchParams.get('age') || 'new_vehicles';
const age = searchParams.get('category') || 'new_vehicles';

const setAge = (newAge: VehiclesAge) => {
searchParams.set('age', newAge);
const setAge = (newAge: VehicleCategory) => {
searchParams.set('category', newAge);
setSearchParams(searchParams);
};

return [age as VehiclesAge, setAge];
return [age as VehicleCategory, setAge];
};
15 changes: 12 additions & 3 deletions src/hooks/useMbtaApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ File that contains a React hook that provides data from the MBTA API

import { useEffect, useState, useCallback } from 'react';

import { Line, Route, Station, Train, VehiclesAge } from '../types';
import { Line, Route, Station, Train, VehicleCategory } from '../types';
import { APP_DATA_BASE_PATH } from '../constants';

export interface MBTAApi {
Expand All @@ -28,12 +28,18 @@ const filterOld = (trains: Train[] | undefined) => {
return trains?.filter((train) => !train.isNewTrain);
};

const filterTrains = (trains: Train[] | undefined, vehiclesAge: VehiclesAge) => {
const filterGoogly = (trains: Train[] | undefined) => {
return trains?.filter((train) => train.hasGooglyEyes);
};

const filterTrains = (trains: Train[] | undefined, vehiclesAge: VehicleCategory) => {
let selectedTrains: Train[] | undefined = [];
if (vehiclesAge === 'new_vehicles') {
selectedTrains = filterNew(trains);
} else if (vehiclesAge === 'old_vehicles') {
selectedTrains = filterOld(trains);
} else if (vehiclesAge === 'googly_eyes_vehicles') {
selectedTrains = filterGoogly(trains);
} else {
selectedTrains = trains;
}
Expand All @@ -48,7 +54,10 @@ const getRoutesInfo = (routes: string[]) => {
return fetch(`${APP_DATA_BASE_PATH}/routes/${routes.join(',')}`).then((res) => res.json());
};

export const useMbtaApi = (lines: Line[], vehiclesAge: VehiclesAge = 'new_vehicles'): MBTAApi => {
export const useMbtaApi = (
lines: Line[],
vehiclesAge: VehicleCategory = 'new_vehicles'
): MBTAApi => {
const routeNames = lines
.map((line) => Object.keys(line.routes))
.reduce((a, b) => [...a, ...b], [])
Expand Down
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface Route {
stationPositions?: StationPositions;
derivedFromRouteId?: string;
pathDirective?: string;
pathInterpolator?: (any) => any;

Check warning on line 19 in src/types.ts

View workflow job for this annotation

GitHub Actions / frontend (20, 3.12)

Unexpected any. Specify a different type
id?: string;
}

Expand All @@ -32,6 +33,7 @@ export interface Train {
currentStatus: CurrentStatus;
direction: number;
isNewTrain: boolean;
hasGooglyEyes: boolean;
label: string;
latitude: number;
longitude: number;
Expand Down Expand Up @@ -85,7 +87,7 @@ export interface Pair {
train: Train;
}

export type VehiclesAge = 'vehicles' | 'new_vehicles' | 'old_vehicles';
export type VehicleCategory = 'vehicles' | 'new_vehicles' | 'old_vehicles' | 'googly_eyes_vehicles';

export interface Prediction {
departure_time: Date;
Expand Down

0 comments on commit c87953f

Please sign in to comment.