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

feat: show googly eyes 👀 #212

Merged
merged 5 commits into from
Jul 10, 2024
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
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 @@
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 @@
currentStatus: CurrentStatus;
direction: number;
isNewTrain: boolean;
hasGooglyEyes: boolean;
label: string;
latitude: number;
longitude: number;
Expand All @@ -58,7 +60,7 @@
commands?: null[];
start?: string;
end?: string;
get?: (frac: any) => { x; y };

Check warning on line 63 in src/types.ts

View workflow job for this annotation

GitHub Actions / frontend (20, 3.12)

Unexpected any. Specify a different type
}

type ShapeType = 'start' | 'line' | 'branch' | 'stationRange';
Expand All @@ -85,7 +87,7 @@
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
Loading