diff --git a/common/components/maps/LineMap.module.css b/common/components/maps/LineMap.module.css deleted file mode 100644 index a2a55e3a9..000000000 --- a/common/components/maps/LineMap.module.css +++ /dev/null @@ -1,20 +0,0 @@ -.container { - display: flex; - justify-content: center; - font-family: sans-serif; - position: relative; -} - -.tooltipContainer { - position: absolute; -} - -@media screen and (min-width: 770px) { - .tooltipContainer { - z-index: 1; - } -} - -.inner { - overflow-x: scroll; -} diff --git a/common/components/maps/LineMap.stories.tsx b/common/components/maps/LineMap.stories.tsx deleted file mode 100644 index 3c1db63fe..000000000 --- a/common/components/maps/LineMap.stories.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; - -import type { SegmentRenderOptions } from './LineMap'; -import { LineMap } from './LineMap'; -import { createDefaultDiagramForLine } from '.'; - -export default { - title: 'LineMap', - component: LineMap, -}; - -const redLine = createDefaultDiagramForLine('Red'); -const redLineSegments: SegmentRenderOptions[] = [ - { - location: { - toStationId: 'place-cntsq', - fromStationId: 'place-pktrm', - }, - strokes: [ - { offset: 1, stroke: 'red', opacity: 0.1 }, - { offset: -1, stroke: 'red', opacity: 0.3 }, - { offset: -2, stroke: 'red', opacity: 0.6 }, - { offset: -3, stroke: 'red', opacity: 0.8 }, - ], - labels: [ - { - mapSide: '0', - boundingSize: 40, - content: () => ( -
- Greetings amigos thank you for inviting me into your SVG -
- ), - }, - { - mapSide: '1', - boundingSize: 40, - content: () => ( -
And on this side too! I also like being on this side!
- ), - }, - ], - }, - { - location: { - toStationId: 'place-pktrm', - fromStationId: 'place-shmnl', - }, - strokes: [ - { offset: 1, stroke: 'red', opacity: 0.3 }, - { offset: 2, stroke: 'red', opacity: 0.6 }, - { offset: 3, stroke: 'red', opacity: 0.8 }, - ], - }, -]; - -export const Testing = () => { - return ( - <> - options.stationId} - strokeOptions={{ stroke: 'red' }} - getSegments={() => redLineSegments} - /> - options.stationId} - strokeOptions={{ stroke: 'red' }} - getSegments={() => redLineSegments} - /> - - ); -}; diff --git a/common/components/maps/LineMap.tsx b/common/components/maps/LineMap.tsx deleted file mode 100644 index 271c21624..000000000 --- a/common/components/maps/LineMap.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import React, { useMemo } from 'react'; - -import type { Station } from '../../types/stations'; - -import type { Path, Diagram, SegmentLocation } from './diagrams'; -import { useDiagramCoordinates } from './useDiagramCoordinates'; -import { useLineTooltip } from './useLineTooltip'; - -import styles from './LineMap.module.css'; - -type MapSide = '0' | '1'; - -type StrokeOptions = { - stroke: string; - strokeWidth: number; - opacity: number; -}; - -type Rect = ReturnType['getBounds']>; - -type OffsetStrokeOptions = StrokeOptions & { offset?: number }; - -export type SegmentLabel = { - mapSide: MapSide; - boundingSize?: number; - offset?: { x: number; y: number }; - content: (size: { width: number; height: number }) => React.ReactNode; -}; - -export type SegmentRenderOptions = { - location: SegmentLocation; - strokes?: Partial[]; - labels?: SegmentLabel[]; -}; - -export type TooltipSide = 'left' | 'right' | 'top'; - -type TooltipRenderer = (props: { - segmentLocation: SegmentLocation; - side: TooltipSide; -}) => React.ReactNode; - -type TooltipOptions = { - render: TooltipRenderer; - snapToSegment?: boolean; - maxDistance?: number; -}; - -export interface LineMapProps { - diagram: Diagram; - direction?: 'vertical' | 'horizontal' | 'horizontal-on-desktop'; - strokeOptions?: Partial; - tooltip?: TooltipOptions; - getStationLabel?: (options: { stationId: string; stationName: string }) => string; - getScaleBasis?: (viewport: { width: null | number; height: null | number }) => number; - getSegments?: (options: { isHorizontal: boolean }) => SegmentRenderOptions[]; -} - -const getPropsForStrokeOptions = (options: Partial) => { - return { - fill: 'transparent', - stroke: 'black', - strokeWidth: 1, - opacity: 1, - ...options, - }; -}; - -const getSegmentLabelBounds = ( - segmentBounds: Rect, - segmentLabel: SegmentLabel, - isHorizontal: boolean -) => { - const { top, left, right, width, height } = segmentBounds; - const { boundingSize = 10, offset: providedOffset, mapSide } = segmentLabel; - const offset = providedOffset || { x: 0, y: 0 }; - if (isHorizontal) { - const moveAcross = mapSide === '0'; - return { - x: top + offset.x, - y: 0 - left - (moveAcross ? boundingSize + width : 0) + offset.y, - width: height, - height: boundingSize, - } as const; - } - const moveAcross = mapSide === '1'; - return { - x: right - (moveAcross ? boundingSize + width : 0) + offset.x, - y: top + offset.y, - height, - width: boundingSize, - }; -}; - -export const LineMap: React.FC = ({ - diagram, - direction = 'horizontal-on-desktop', - getStationLabel, - getScaleBasis, - strokeOptions = {}, - tooltip, - getSegments, -}) => { - const { - svgRef, - svgProps, - containerRef, - isHorizontal, - viewportCoordsToContainer, - viewportCoordsToDiagram, - diagramCoordsToViewport, - } = useDiagramCoordinates({ - getScaleBasis, - direction, - }); - - const { - viewportCoordinates: tooltipViewportCoordinates, - segmentLocation: tooltipSegmentLocation, - } = useLineTooltip({ - diagram, - diagramCoordsToViewport, - viewportCoordsToDiagram, - snapToSegment: !!tooltip?.snapToSegment, - enabled: !!tooltip, - maxDistance: tooltip?.maxDistance, - }); - - const pathDirective = useMemo(() => diagram.toSVG(), [diagram]); - - const stationsById = useMemo(() => { - const index: Record = {}; - diagram.getStations().forEach((station) => { - index[station.station] = station; - }); - return index; - }, [diagram]); - - const stationPositions = useMemo(() => { - const positions: Record = {}; - for (const station of diagram.getStations()) { - positions[station.station] = diagram.getStationPosition(station.station); - } - return positions; - }, [diagram]); - - const computedSegmentExtras = useMemo(() => { - const segments = getSegments ? getSegments({ isHorizontal }) : []; - return segments.map((segment) => { - const { - location: { fromStationId, toStationId }, - strokes = [], - labels = [], - } = segment; - - const path = diagram.getPathBetweenStations(fromStationId, toStationId); - const bounds = path.getBounds(); - - const computedStrokes = strokes.map((stroke) => { - const pathDirective = path.offset(stroke.offset ?? 0).toSVG(); - return { pathDirective, stroke }; - }); - - const computedLabels = labels.map((label, index) => { - const { x, y, width, height } = getSegmentLabelBounds(bounds, label, isHorizontal); - return ( - - - {label.content({ width, height })} - - ); - }); - - return { computedStrokes, computedLabels }; - }); - }, [getSegments, diagram, isHorizontal]); - - const renderStationDots = () => { - const strokeProps = getPropsForStrokeOptions(strokeOptions); - return Object.entries(stationPositions).map(([stationId, pos]) => { - return ( - - ); - }); - }; - - const renderStationLabels = () => { - return Object.entries(stationPositions).map(([stationId, pos]) => { - const stationName = stationsById[stationId].stop_name; - const stationLabel = getStationLabel?.({ stationId, stationName }) ?? stationName; - if (stationLabel) { - return ( - - ); - } - return null; - }); - }; - - const renderLine = () => { - return ; - }; - - const renderComputedStrokes = () => { - return computedSegmentExtras - .map((segment, segmentIndex) => { - return segment.computedStrokes.map(({ pathDirective, stroke }, strokeIndex) => { - return ( - - ); - }); - }) - .flat(); - }; - - const renderComputedLabels = () => { - const transform = isHorizontal ? 'rotate(90)' : undefined; - return ( - - {computedSegmentExtras.map((segment) => segment.computedLabels).flat()} - - ); - }; - - const renderTooltip = () => { - const tooltipX = tooltipViewportCoordinates?.x; - const tooltipOnRightSide = tooltipX && tooltipX / window.innerWidth <= 0.5; - const tooltipSide = isHorizontal ? 'top' : tooltipOnRightSide ? 'right' : 'left'; - const tooltipContents = - tooltipSegmentLocation && - tooltip?.render({ segmentLocation: tooltipSegmentLocation, side: tooltipSide }); - if (tooltipViewportCoordinates && tooltipContents) { - const { x, y } = viewportCoordsToContainer(tooltipViewportCoordinates); - return ( -
- {tooltipContents} -
- ); - } - }; - - return ( -
-
- - - {renderLine()} - {renderComputedStrokes()} - {renderComputedLabels()} - {renderStationDots()} - {renderStationLabels()} - - -
- {renderTooltip()} -
- ); -}; diff --git a/common/components/maps/diagrams/commands.ts b/common/components/maps/diagrams/commands.ts deleted file mode 100644 index 05a650bc7..000000000 --- a/common/components/maps/diagrams/commands.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { CurveCommand, LineCommand, RangeNames, WiggleCommand } from './types'; - -export const line = (length: number, ranges: RangeNames = []): LineCommand => { - return { - type: 'line', - length, - ranges, - }; -}; - -export const curve = (length: number, angle: number, ranges: RangeNames = []): CurveCommand => { - return { - type: 'curve', - length, - angle, - ranges, - }; -}; - -export const wiggle = (length: number, width: number, ranges: RangeNames = []): WiggleCommand => { - return { - type: 'wiggle', - length, - width, - ranges, - }; -}; diff --git a/common/components/maps/diagrams/diagram.ts b/common/components/maps/diagrams/diagram.ts deleted file mode 100644 index 8b9eaf4d2..000000000 --- a/common/components/maps/diagrams/diagram.ts +++ /dev/null @@ -1,158 +0,0 @@ -import type { Point } from 'bezier-js'; - -import type { Station } from '../../../types/stations'; - -import type { Path } from './path'; -import type { DiagramProjection, PathProjection, SegmentLocation } from './types'; - -type StationDisplacementMap = Map>; - -const indexRangesNamesByStationId = (stationsByRangeName: Record) => { - const index: Record = {}; - for (const [rangeName, stations] of Object.entries(stationsByRangeName)) { - for (const station of stations) { - index[station.station] = rangeName; - } - } - return index; -}; - -const getStationDisplacementMap = ( - paths: Path[], - stationsByRangeName: Record -) => { - const map: StationDisplacementMap = new Map(); - for (const path of paths) { - const pathIndex: Record = {}; - map.set(path, pathIndex); - for (const range of path.getRanges()) { - const stations = stationsByRangeName[range]; - if (stations) { - for (let i = 0; i < stations.length; i++) { - const station = stations[i]; - const fraction = i / (stations.length - 1); - const displacement = path.getDisplacementFromRangeLookup({ fraction, range }); - pathIndex[station.station] = displacement; - } - } - } - } - return map; -}; - -export class Diagram { - private paths: Path[]; - private stationsByRangeName: Record; - private stations: Station[]; - private readonly rangeNamesByStationId: Record; - private readonly stationDisplacementMap: StationDisplacementMap; - - constructor(paths: Path[], stationsByRangeName: Record) { - this.paths = paths; - this.stationsByRangeName = stationsByRangeName; - this.rangeNamesByStationId = indexRangesNamesByStationId(stationsByRangeName); - this.stationDisplacementMap = getStationDisplacementMap(paths, stationsByRangeName); - this.stations = [...new Set(Object.values(this.stationsByRangeName).flat())]; - } - - private getPathWithStationIds(stationIds: string[]) { - const rangeNames = stationIds.map((stationId) => this.rangeNamesByStationId[stationId]); - for (const path of this.paths) { - if (rangeNames.every((rangeName) => path.hasRange(rangeName))) { - return path; - } - } - throw new Error(`No path has all stations by id: ${stationIds}`); - } - - private getAdjacentStationId(path: Path, displacement: number, after: boolean): null | string { - const stations = this.stationDisplacementMap.get(path)!; - let closestStationId: null | string = null; - let closestDistance = Infinity; - for (const [stationId, stationDisplacement] of Object.entries(stations)) { - const isOnCorrectSide = after - ? stationDisplacement >= displacement - : displacement >= stationDisplacement; - if (isOnCorrectSide) { - const distance = Math.abs(stationDisplacement - displacement); - if (distance < closestDistance) { - closestStationId = stationId; - closestDistance = distance; - } - } - } - return closestStationId; - } - - getStations() { - return this.stations; - } - - getAdjacentSegmentLocations() { - const pairs: SegmentLocation[] = []; - const seenPairKeys: Set = new Set(); - for (const stationIndex of this.stationDisplacementMap.values()) { - const stationIds = Object.keys(stationIndex); - for (let i = 0; i < stationIds.length - 1; i++) { - const fromStationId = stationIds[i]; - const toStationId = stationIds[i + 1]; - const pairKey = `${fromStationId}__${toStationId}`; - if (!seenPairKeys.has(pairKey)) { - seenPairKeys.add(pairKey); - pairs.push({ fromStationId: stationIds[i], toStationId: stationIds[i + 1] }); - } - } - } - return pairs; - } - - getStationPosition(stationId: string) { - const pathWithStationId = this.getPathWithStationIds([stationId]); - const { [stationId]: displacement } = this.stationDisplacementMap.get(pathWithStationId)!; - return pathWithStationId.getPointFromDisplacement(displacement); - } - - getPathBetweenStations(fromStationId: string, toStationId: string) { - const path = this.getPathWithStationIds([fromStationId, toStationId]); - const { [fromStationId]: fromDisplacement, [toStationId]: toDisplacement } = - this.stationDisplacementMap.get(path)!; - return path.cut(fromDisplacement, toDisplacement); - } - - toSVG() { - return this.paths.map((path) => path.toSVG()).reduce((a, b) => `${a} ${b}`, ''); - } - - project(point: Point): null | DiagramProjection { - let closestPath: null | Path = null; - let closestProjection: null | PathProjection = null; - for (const path of this.paths) { - const projection = path.project(point); - if (projection && (!closestProjection || projection.distance < closestProjection.distance)) { - closestProjection = projection; - closestPath = path; - } - } - if (closestPath && closestProjection) { - const fromStationId = this.getAdjacentStationId( - closestPath, - closestProjection.displacement, - false - ); - const toStationId = this.getAdjacentStationId( - closestPath, - closestProjection.displacement, - true - ); - return { - segmentProjection: closestProjection.segmentProjection, - path: closestPath, - segmentLocation: { - fromStationId, - toStationId: toStationId !== fromStationId ? toStationId : null, - }, - }; - } - return null; - } -} diff --git a/common/components/maps/diagrams/execute.ts b/common/components/maps/diagrams/execute.ts deleted file mode 100644 index d4d242709..000000000 --- a/common/components/maps/diagrams/execute.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Bezier } from 'bezier-js'; - -import { Path } from './path'; -import type { - Command, - CommandPath, - CommandResult, - CurveCommand, - LineCommand, - RangeNames, - Turtle, - WiggleCommand, -} from './types'; - -const d2r = (degrees: number) => degrees * (Math.PI / 180); -const sind = (theta: number) => Math.sin(d2r(theta)); -const cosd = (theta: number) => Math.cos(d2r(theta)); -const tand = (theta: number) => Math.tan(d2r(theta)); - -const executeLine = (command: LineCommand, turtle: Turtle): CommandResult => { - const { length } = command; - const { x, y, theta } = turtle; - const x2 = x + length * cosd(theta); - const y2 = y + length * sind(theta); - return { - curve: new Bezier([ - { x, y }, - { x: (x + x2) / 2, y: (y + y2) / 2 }, - { x: x2, y: y2 }, - ]), - turtle: { x: x2, y: y2, theta }, - }; -}; - -const executeCurve = (command: CurveCommand, turtle: Turtle): CommandResult => { - const { length, angle } = command; - const { x: x1, y: y1, theta } = turtle; - const nextTheta = theta + angle; - const x2 = x1 + length * cosd(theta + angle / 2); - const y2 = y1 + length * sind(theta + angle / 2); - // Slope of tangent passing through turtle - const m1 = tand(theta); - // Slope of tangent passing through output point - const m2 = tand(nextTheta); - // Calculate control point, which is the intersection of these two tangent lines - let xc: number, yc: number; - if (Math.abs(theta % 360) === 90) { - // tan(theta) = infinity, so the line through (x1, y1) is vertical, and xc = x1 - xc = x1; - yc = m2 * (xc - x2) + y2; - } else { - xc = (y1 - x1 * m1 - y2 + x2 * m2) / (m2 - m1); - yc = m1 * (xc - x1) + y1; - } - return { - curve: new Bezier([ - { x: x1, y: y1 }, - { x: xc, y: yc }, - { x: x2, y: y2 }, - ]), - turtle: { - x: x2, - y: y2, - theta: nextTheta, - }, - }; -}; - -const executeWiggle = (command: WiggleCommand, turtle: Turtle): CommandResult => { - const { x: x1, y: y1, theta } = turtle; - const { length, width } = command; - const nextTheta = theta; // + 0 - const x2 = x1 + length * cosd(theta) + width * cosd(theta - 90); - const y2 = y1 + length * sind(theta) + width * sind(theta - 90); - // The first control point is parallel to the turtle's incoming line, and is half the total - // distance between (x1, y1) and (x2, y2). - const halfDist = 0.5 * Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2)); - const xp1 = x1 + halfDist * cosd(theta); - const yp1 = y1 + halfDist * sind(theta); - // The second control point is parallel to the turtle's outgoing line, and is also half the - // total point-point distance. - const xp2 = x2 - halfDist * cosd(nextTheta); - const yp2 = y2 - halfDist * sind(nextTheta); - return { - curve: new Bezier([ - { x: x1, y: y1 }, - { x: xp1, y: yp1 }, - { x: xp2, y: yp2 }, - { x: x2, y: y2 }, - ]), - turtle: { x: x2, y: y2, theta: nextTheta }, - }; -}; - -const executeCommand = (command: Command, turtle: Turtle) => { - if (command.type === 'line') { - return executeLine(command, turtle); - } - if (command.type === 'curve') { - return executeCurve(command, turtle); - } - return executeWiggle(command, turtle); -}; - -export const execute = (path: CommandPath) => { - const { start, commands, ranges: sharedRanges = [] } = path; - const { curves, ranges } = commands.reduce( - (state, command) => { - const { turtle, curve } = executeCommand(command, state.turtle); - return { - curves: [...state.curves, curve], - ranges: [...state.ranges, [...sharedRanges, ...command.ranges]], - turtle, - }; - }, - { - curves: [] as Bezier[], - ranges: [] as RangeNames[], - turtle: start, - } - ); - return new Path(curves, ranges); -}; diff --git a/common/components/maps/diagrams/index.ts b/common/components/maps/diagrams/index.ts deleted file mode 100644 index 29a6c74bf..000000000 --- a/common/components/maps/diagrams/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './commands'; -export * from './diagram'; -export * from './execute'; -export * from './lines'; -export * from './path'; -export * from './types'; diff --git a/common/components/maps/diagrams/lines.ts b/common/components/maps/diagrams/lines.ts deleted file mode 100644 index e894d2188..000000000 --- a/common/components/maps/diagrams/lines.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { stations } from '../../../constants/stations'; - -import { line, wiggle } from './commands'; -import { Diagram } from './diagram'; -import { execute } from './execute'; -import type { Turtle } from './types'; - -type DiagrammableLineName = 'Red' | 'Orange' | 'Blue'; - -type CreateDiagramOptions = { - /** Number of pixels between each station */ - pxPerStation?: number; -}; - -const DEFAULT_PX_PER_STATION = 10; - -const getStationsForLine = (line: DiagrammableLineName, branch?: string) => { - const stationsForLine = stations[line].stations; - return stationsForLine - .filter((station) => !branch || !station.branches || station.branches?.includes(branch)) - .sort((a, b) => a.order - b.order); -}; - -export const createRedLineDiagram = (options: CreateDiagramOptions = {}) => { - const { pxPerStation = DEFAULT_PX_PER_STATION } = options; - const start: Turtle = { x: 0, y: 0, theta: 90 }; - const stationsA = getStationsForLine('Red', 'A'); - const stationsB = getStationsForLine('Red', 'B'); - const splitIndex = stationsA.findIndex((station) => station.station === 'place-jfk'); - const stationsTrunk = stationsA.slice(0, splitIndex + 1); - const stationsABranch = stationsA.slice(splitIndex + 1); - const stationsBBranch = stationsB.slice(splitIndex + 1); - const trunk = line(pxPerStation * (1 + stationsTrunk.length), ['trunk']); - const pathA = execute({ - start, - ranges: ['branch-a'], - commands: [ - trunk, - wiggle(15, -20), - line(10), - line(pxPerStation * stationsABranch.length, ['branch-a-stations']), - ], - }); - const pathB = execute({ - start, - ranges: ['branch-b'], - commands: [ - trunk, - wiggle(15, 20), - line(60), - line(pxPerStation * stationsBBranch.length, ['branch-b-stations']), - ], - }); - return new Diagram([pathA, pathB], { - trunk: stationsTrunk, - 'branch-a-stations': stationsABranch, - 'branch-b-stations': stationsBBranch, - }); -}; - -const createStraightLineDiagram = ( - lineName: DiagrammableLineName, - options: CreateDiagramOptions = {} -) => { - const { pxPerStation = DEFAULT_PX_PER_STATION } = options; - const start: Turtle = { x: 0, y: 0, theta: 90 }; - const stations = getStationsForLine(lineName); - const path = execute({ - start, - ranges: ['main'], - commands: [line(pxPerStation * stations.length)], - }); - return new Diagram([path], { main: stations }); -}; - -export const createDefaultDiagramForLine = ( - lineName: DiagrammableLineName, - options: CreateDiagramOptions = {} -) => { - switch (lineName) { - case 'Red': - return createRedLineDiagram(options); - default: - return createStraightLineDiagram(lineName, options); - } -}; diff --git a/common/components/maps/diagrams/path.ts b/common/components/maps/diagrams/path.ts deleted file mode 100644 index 03ad08713..000000000 --- a/common/components/maps/diagrams/path.ts +++ /dev/null @@ -1,218 +0,0 @@ -import type { Bezier, Point, Projection } from 'bezier-js'; -import type { RangeNames, RangeLookup, PathProjection } from './types'; - -type Range = [number, number]; -type RangesByName = Record; - -const getRangesIndex = (ranges: RangeNames[]): RangesByName => { - const index: RangesByName = {}; - const allNames = new Set(ranges.flat()); - for (const name of allNames) { - const firstIndexWithName = ranges.findIndex((range) => range.includes(name)); - const lastIndexInRestWithoutName = ranges - .slice(firstIndexWithName + 1) - .findIndex((range) => !range.includes(name)); - const lastIndexWithName = - lastIndexInRestWithoutName === -1 - ? ranges.length - 1 - : firstIndexWithName + lastIndexInRestWithoutName; - index[name] = [firstIndexWithName, lastIndexWithName]; - } - return index; -}; - -const getAccumulatedLengths = (segments: Bezier[]): number[] => { - return segments.reduce((lengths, segment) => { - const accumulated = lengths[lengths.length - 1] ?? 0; - return [...lengths, accumulated + segment.length()]; - }, []); -}; - -export class Path { - private segments: Bezier[]; - private readonly ranges: RangeNames[]; - private readonly rangesIndex: RangesByName; - private readonly accumulatedLengths: number[]; - readonly length: number; - - constructor(segments: Bezier[], ranges: RangeNames[] = []) { - this.segments = segments; - this.ranges = ranges; - this.rangesIndex = getRangesIndex(this.ranges); - this.accumulatedLengths = getAccumulatedLengths(segments); - this.length = this.accumulatedLengths[this.accumulatedLengths.length - 1]; - } - - private seek(target: number): { segment: Bezier; index: number; displacement: number } { - let displacement = 0; - for (let index = 0; index < this.segments.length; index++) { - const segment = this.segments[index]; - const segmentLength = segment.length(); - if (displacement + segmentLength >= target) { - return { - index, - segment, - displacement: target - displacement, - }; - } else { - displacement += segmentLength; - } - } - throw new Error('Failed to seek'); - } - - concat(curve: Bezier, ranges: RangeNames = []) { - return new Path([...this.segments, curve], [...this.ranges, ranges]); - } - - slice(from: number, to: number) { - return new Path(this.segments.slice(from, to), this.ranges.slice(from, to)); - } - - getRanges(): string[] { - return Object.keys(this.rangesIndex); - } - - hasRange(name: string) { - return !!this.rangesIndex[name]; - } - - getRange(name: string) { - const range = this.rangesIndex[name]; - if (range) { - return range; - } - throw new Error(`No range by name ${name}`); - } - - sliceRange(name: string) { - const [from, to] = this.getRange(name); - return this.slice(from, to + 1); - } - - getDisplacementFromRangeLookup(lookup: RangeLookup) { - const { range, fraction } = lookup; - const [from, to] = this.getRange(range); - const fromLength = from === 0 ? 0 : this.accumulatedLengths[from - 1]; - const toLength = this.accumulatedLengths[to]; - return fromLength + fraction * (toLength - fromLength); - } - - getPointFromRangeLookup(lookup: RangeLookup) { - const displacement = this.getDisplacementFromRangeLookup(lookup); - return this.getPointFromFraction(displacement / this.length); - } - - getPointFromDisplacement(displacement: number) { - const { segment, displacement: segmentDisplacement } = this.seek(displacement); - return segment.get(segmentDisplacement / segment.length()); - } - - getPointFromFraction(fraction: number) { - const fractionalDisplacement = fraction * this.length; - const { segment, displacement } = this.seek(fractionalDisplacement); - return segment.get(displacement / segment.length()); - } - - cut(fromDisplacement: number, toDisplacement: number) { - if (fromDisplacement > toDisplacement) { - const intermediate = toDisplacement; - toDisplacement = fromDisplacement; - fromDisplacement = intermediate; - } - const { - segment: fromSegment, - index: fromIndex, - displacement: displacementInFromSegment, - } = this.seek(fromDisplacement); - const { - segment: toSegment, - index: toIndex, - displacement: displacementInToSegment, - } = this.seek(toDisplacement); - const fromSegmentLength = fromSegment.length(); - const toSegmentLength = toSegment.length(); - if (fromSegment === toSegment) { - return new Path([ - fromSegment.split( - displacementInFromSegment / fromSegmentLength, - displacementInToSegment / fromSegmentLength - ), - ]); - } - const fractionInFromSegment = displacementInFromSegment / fromSegmentLength; - const partOfFromSegment = fromSegment.split(fractionInFromSegment, 1); - const intermediate = this.segments.slice(fromIndex + 1, toIndex); - const fractionInToSegment = displacementInToSegment / toSegmentLength; - const partOfToSegment = toSegment.split(0, fractionInToSegment); - const includedParts = [ - fractionInFromSegment !== 1 && partOfFromSegment, - ...intermediate, - fractionInToSegment !== 0 && partOfToSegment, - ].filter((x): x is Bezier => !!x); - return new Path(includedParts); - } - - offset(distance: number) { - if (distance === 0) { - return this; - } - return new Path( - this.segments.map((segment) => segment.offset(distance) as unknown as Bezier).flat() - ); - } - - toSVG() { - return this.segments.map((segment) => segment.toSVG()).reduce((a, b) => `${a} ${b}`, ''); - } - - getBounds() { - let maxLeft = Infinity; - let maxRight = -Infinity; - let maxTop = Infinity; - let maxBottom = -Infinity; - for (const segment of this.segments) { - const { - x: { min: left, max: right }, - y: { min: top, max: bottom }, - } = segment.bbox(); - maxLeft = Math.min(maxLeft, left); - maxRight = Math.max(maxRight, right); - maxTop = Math.min(maxTop, top); - maxBottom = Math.max(maxBottom, bottom); - } - return { - left: maxLeft, - right: maxRight, - top: maxTop, - bottom: maxBottom, - width: maxRight - maxLeft, - height: maxBottom - maxTop, - }; - } - - project(point: Point): null | PathProjection { - let closestProjection: null | Projection = null; - let closestSegment: null | Bezier = null; - for (const segment of this.segments) { - const projection = segment.project(point); - if (projection.d && (!closestProjection || projection.d < closestProjection.d!)) { - closestProjection = projection; - closestSegment = segment; - } - } - if (closestProjection && closestSegment) { - const indexOfSegment = this.segments.indexOf(closestSegment); - const displacementBeforeSegment = - indexOfSegment === 0 ? 0 : this.accumulatedLengths[indexOfSegment - 1]; - const displacementWithinSegment = closestProjection.t! * closestSegment.length(); - const totalDisplacement = displacementBeforeSegment + displacementWithinSegment; - return { - segmentProjection: closestProjection, - distance: closestProjection.d!, - displacement: totalDisplacement, - }; - } - return null; - } -} diff --git a/common/components/maps/diagrams/types.ts b/common/components/maps/diagrams/types.ts deleted file mode 100644 index 5b9f5c7df..000000000 --- a/common/components/maps/diagrams/types.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { Bezier, Projection } from 'bezier-js'; - -import type { Path } from './path'; - -export type Turtle = { - x: number; - y: number; - theta: number; -}; - -export type BaseCommand = { - ranges: RangeNames; -}; - -export type LineCommand = BaseCommand & { - type: 'line'; - length: number; -}; - -export type CurveCommand = BaseCommand & { - type: 'curve'; - length: number; - angle: number; -}; - -export type WiggleCommand = BaseCommand & { - type: 'wiggle'; - length: number; - width: number; -}; - -export type Command = LineCommand | CurveCommand | WiggleCommand; - -export type CommandPath = { - start: Turtle; - commands: Command[]; - ranges?: RangeNames; -}; - -export type CommandResult = { - turtle: Turtle; - curve: Bezier; -}; - -export type SegmentLocation = { - fromStationId: (Nullable extends true ? null : never) | string; - toStationId: (Nullable extends true ? null : never) | string; -}; - -export type RangeNames = string[]; - -export type RangeLookup = { range: string; fraction: number }; - -export type PathProjection = { - segmentProjection: Projection; - distance: number; - displacement: number; -}; - -export type DiagramProjection = { - segmentProjection: Projection; - path: Path; - segmentLocation: SegmentLocation; -}; diff --git a/common/components/maps/index.ts b/common/components/maps/index.ts deleted file mode 100644 index 252c56e46..000000000 --- a/common/components/maps/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { LineMap } from './LineMap'; -export * from './diagrams'; diff --git a/common/components/maps/useDiagramCoordinates.ts b/common/components/maps/useDiagramCoordinates.ts deleted file mode 100644 index e4be4fb51..000000000 --- a/common/components/maps/useDiagramCoordinates.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { useCallback, useLayoutEffect, useState } from 'react'; - -import { useBreakpoint } from '../../hooks/useBreakpoint'; -import { useViewport } from '../../hooks/useViewport'; - -import type { LineMapProps as LineMapProps } from './LineMap'; - -type Options = Pick; - -type SvgProps = { - width: number; - height: number; - viewBox: string; -}; - -type Point = { - x: number; - y: number; -}; - -export type CoordinateTransform = (p: Point) => Point; - -const defaultGetScaleBasis = (viewport: { width: null | number; height: null | number }) => { - const { width, height } = viewport; - const MAX_SCALE_BASIS = 3.5; - if (width && height) { - if (width > 750) { - return MAX_SCALE_BASIS; - } - return Math.min(MAX_SCALE_BASIS, height / 100); - } - return MAX_SCALE_BASIS; -}; - -export const useDiagramCoordinates = (options: Options) => { - const { direction, getScaleBasis = defaultGetScaleBasis } = options; - const [svg, setSvg] = useState(null); - const [container, setContainer] = useState(null); - const [svgProps, setSvgProps] = useState(null); - - const { viewportWidth, viewportHeight } = useViewport(); - const isMobile = !useBreakpoint('lg'); - const isHorizontal = - direction === 'horizontal-on-desktop' ? !isMobile : direction === 'horizontal'; - - useLayoutEffect(() => { - if (svg) { - const paddingX = 2; - const paddingY = 2; - const bbox = svg.getBBox(); - const x = Math.round(bbox.x - paddingX); - const width = Math.round(bbox.width + paddingX * 2); - const y = Math.round(bbox.y - paddingY); - const height = Math.round(bbox.height + paddingY * 2); - if (isHorizontal && container) { - const containerWidth = container.getBoundingClientRect().width; - const mapWidth = Math.min(4 * width, Math.max(Math.max(3 * width, containerWidth))); - const aspectRatio = width / height; - setSvgProps({ - viewBox: `${x} ${y} ${width} ${height}`, - width: mapWidth, - height: mapWidth / aspectRatio, - }); - } else { - const scaleBasis = getScaleBasis({ width: viewportWidth, height: viewportHeight }); - setSvgProps({ - viewBox: `${x} ${y} ${width} ${height}`, - width: Math.round(width * scaleBasis), - height: Math.round(height * scaleBasis), - }); - } - } - }, [svg, container, viewportWidth, viewportHeight, getScaleBasis, isHorizontal]); - - const viewportCoordsToDiagram: CoordinateTransform = useCallback( - (viewportPoint: Point) => { - if (svg) { - const rotation = isHorizontal ? -90 : 0; - const pt = svg.createSVGPoint(); - pt.x = viewportPoint.x; - pt.y = viewportPoint.y; - const transformed = pt.matrixTransform(svg.getScreenCTM()!.rotate(rotation).inverse()); - return { - x: transformed.x, - y: transformed.y, - }; - } - return { x: 0, y: 0 }; - }, - [svg, isHorizontal] - ); - - const diagramCoordsToViewport: CoordinateTransform = useCallback( - (mapPoint: Point) => { - if (svg) { - const rotation = isHorizontal ? -90 : 0; - const pt = svg.createSVGPoint(); - pt.x = mapPoint.x; - pt.y = mapPoint.y; - const transformed = pt.matrixTransform(svg.getScreenCTM()!.rotate(rotation)); - return { - x: transformed.x, - y: transformed.y, - }; - } - return { x: 0, y: 0 }; - }, - [svg, isHorizontal] - ); - - const viewportCoordsToContainer: CoordinateTransform = useCallback( - (viewportPoint: Point) => { - if (container) { - const rect = container.getBoundingClientRect(); - const containerX = viewportPoint.x - rect.left; - const containerY = viewportPoint.y - rect.top; - return { x: containerX, y: containerY }; - } - return { x: 0, y: 0 }; - }, - [container] - ); - - return { - svgProps, - svgRef: setSvg, - containerRef: setContainer, - isHorizontal, - viewportCoordsToDiagram, - viewportCoordsToContainer, - diagramCoordsToViewport, - }; -}; diff --git a/common/components/maps/useLineTooltip.ts b/common/components/maps/useLineTooltip.ts deleted file mode 100644 index bcbb1ee6c..000000000 --- a/common/components/maps/useLineTooltip.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; - -import type { Diagram, DiagramProjection } from './diagrams'; -import type { CoordinateTransform } from './useDiagramCoordinates'; - -type Options = { - diagram: Diagram; - viewportCoordsToDiagram: CoordinateTransform; - diagramCoordsToViewport: CoordinateTransform; - snapToSegment: boolean; - enabled?: boolean; - maxDistance?: number; -}; - -export const useLineTooltip = (options: Options) => { - const { - diagram, - viewportCoordsToDiagram, - diagramCoordsToViewport, - snapToSegment, - enabled = true, - maxDistance, - } = options; - const [projection, setProjection] = useState(null); - - const mapCoordinates = useMemo(() => { - if (projection) { - if (typeof maxDistance === 'number' && projection.segmentProjection.d! > maxDistance) { - return null; - } - if (snapToSegment) { - const { - segmentLocation: { fromStationId, toStationId }, - } = projection; - if (fromStationId && toStationId) { - const segmentPath = diagram.getPathBetweenStations(fromStationId, toStationId); - const midpoint = segmentPath.getPointFromFraction(0.5); - return midpoint; - } - } - return projection.segmentProjection; - } - return null; - }, [diagram, projection, maxDistance, snapToSegment]); - - const viewportCoordinates = useMemo( - () => mapCoordinates && diagramCoordsToViewport(mapCoordinates), - [mapCoordinates, diagramCoordsToViewport] - ); - - useEffect(() => { - if (enabled) { - const handleMouseOver = (evt: MouseEvent) => { - const mapCoords = viewportCoordsToDiagram({ - x: evt.clientX, - y: evt.clientY, - }); - setProjection(diagram.project(mapCoords)); - }; - window.addEventListener('mousemove', handleMouseOver); - return () => window.removeEventListener('mousemove', handleMouseOver); - } - }, [enabled, diagram, viewportCoordsToDiagram, diagramCoordsToViewport]); - - return { viewportCoordinates, mapCoordinates, segmentLocation: projection?.segmentLocation }; -}; diff --git a/modules/slowzones/SlowZonesDetails.tsx b/modules/slowzones/SlowZonesDetails.tsx index aac41404e..a02430374 100644 --- a/modules/slowzones/SlowZonesDetails.tsx +++ b/modules/slowzones/SlowZonesDetails.tsx @@ -20,6 +20,7 @@ import { ButtonGroup } from '../../common/components/general/ButtonGroup'; import { PageWrapper } from '../../common/layouts/PageWrapper'; import { ChartPageDiv } from '../../common/components/charts/ChartPageDiv'; import { Layout } from '../../common/layouts/layoutTypes'; +import { useBreakpoint } from '../../common/hooks/useBreakpoint'; import { SlowZonesSegmentsWrapper } from './SlowZonesSegmentsWrapper'; import { TotalSlowTimeWrapper } from './TotalSlowTimeWrapper'; import { SlowZonesMap } from './map'; @@ -48,6 +49,7 @@ export function SlowZonesDetails() { !delayTotals.isError && delayTotals.data && startDateUTC && endDateUTC && lineShort && line; const segmentsReady = !allSlow.isError && allSlow.data && startDateUTC && lineShort; const canShowSlowZonesMap = lineShort === 'Red' || lineShort === 'Blue' || lineShort === 'Orange'; + const isDesktop = useBreakpoint('lg'); if (!endDateUTC || !startDateUTC) { return ( @@ -95,7 +97,7 @@ export function SlowZonesDetails() { slowZones={isArray(allSlow.data) ? allSlow.data : allSlow.data.data} speedRestrictions={speedRestrictions.data} lineName={lineShort} - direction="horizontal-on-desktop" + direction={isDesktop ? 'horizontal' : 'vertical'} /> ) : (
diff --git a/modules/slowzones/SystemSlowZonesDetails.tsx b/modules/slowzones/SystemSlowZonesDetails.tsx index 133cda3cd..99aa0fff7 100644 --- a/modules/slowzones/SystemSlowZonesDetails.tsx +++ b/modules/slowzones/SystemSlowZonesDetails.tsx @@ -39,6 +39,7 @@ export function SystemSlowZonesDetails({ showTitle = false }: SystemSlowZonesDet const [lineShort, setLineShort] = useState('Red'); const line = `line-${lineShort.toLowerCase()}` as Line; const canShowSlowZonesMap = lineShort === 'Red' || lineShort === 'Blue' || lineShort === 'Orange'; + const isDesktop = useBreakpoint('lg'); const { query: { startDate, endDate }, @@ -100,7 +101,7 @@ export function SystemSlowZonesDetails({ showTitle = false }: SystemSlowZonesDet slowZones={isArray(allData.data) ? allData.data : allData.data.data} speedRestrictions={speedRestrictions.data} lineName={lineShort} - direction="horizontal-on-desktop" + direction={isDesktop ? 'horizontal' : 'vertical'} /> ) : (
diff --git a/modules/slowzones/map/SlowZonesMap.tsx b/modules/slowzones/map/SlowZonesMap.tsx index 96c804435..4954076d8 100644 --- a/modules/slowzones/map/SlowZonesMap.tsx +++ b/modules/slowzones/map/SlowZonesMap.tsx @@ -1,12 +1,13 @@ import React, { useMemo } from 'react'; +import type { SegmentLocation, SegmentLabel, TooltipSide } from '@transitmatters/stripmap'; +import { LineMap, createDefaultDiagramForLine } from '@transitmatters/stripmap'; + +import '@transitmatters/stripmap/dist/style.css'; import { LINE_OBJECTS } from '../../../common/constants/lines'; -import type { SegmentLocation } from '../../../common/components/maps'; -import { LineMap, createDefaultDiagramForLine } from '../../../common/components/maps'; import type { SlowZoneResponse, SpeedRestriction } from '../../../common/types/dataPoints'; import type { SlowZonesLineName } from '../types'; -import type { SegmentLabel, TooltipSide } from '../../../common/components/maps/LineMap'; import { getSlowZoneOpacity } from '../../../common/utils/slowZoneUtils'; import { useDelimitatedRoute } from '../../../common/utils/router'; import { TODAY_STRING } from '../../../common/constants/dates'; diff --git a/modules/slowzones/map/SlowZonesTooltip.tsx b/modules/slowzones/map/SlowZonesTooltip.tsx index 4bc0234d0..81e95cd75 100644 --- a/modules/slowzones/map/SlowZonesTooltip.tsx +++ b/modules/slowzones/map/SlowZonesTooltip.tsx @@ -1,11 +1,11 @@ import React, { useMemo } from 'react'; import classNames from 'classnames'; +import type { TooltipSide } from '@transitmatters/stripmap'; import { getParentStationForStopId } from '../../../common/utils/stations'; import { BasicWidgetDataLayout } from '../../../common/components/widgets/internal/BasicWidgetDataLayout'; import { DeltaTimeWidgetValue } from '../../../common/types/basicWidgets'; -import type { TooltipSide } from '../../../common/components/maps/LineMap'; import type { SlowZoneResponse, SpeedRestriction } from '../../../common/types/dataPoints'; import { prettyDate } from '../../../common/utils/date'; diff --git a/modules/slowzones/map/segment.ts b/modules/slowzones/map/segment.ts index a7ed07ff1..3b13aa5ab 100644 --- a/modules/slowzones/map/segment.ts +++ b/modules/slowzones/map/segment.ts @@ -1,4 +1,5 @@ -import type { Diagram, SegmentLocation } from '../../../common/components/maps'; +import type { Diagram, SegmentLocation } from '@transitmatters/stripmap'; + import type { SlowZoneResponse, SpeedRestriction } from '../../../common/types/dataPoints'; import type { LineShort } from '../../../common/types/lines'; import type { Station } from '../../../common/types/stations'; diff --git a/package-lock.json b/package-lock.json index 45568af3e..481f4b1c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@tanstack/react-query": "^5.32.0", "@tanstack/react-query-devtools": "^5.32.0", "@tippyjs/react": "^4.2.6", + "@transitmatters/stripmap": "^0.1.9", "@types/react-flatpickr": "^3.8.11", "bezier-js": "^6.1.4", "chart.js": "4.4.3", @@ -2120,6 +2121,11 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", @@ -6171,6 +6177,18 @@ "react-dom": ">=16.8" } }, + "node_modules/@transitmatters/stripmap": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@transitmatters/stripmap/-/stripmap-0.1.9.tgz", + "integrity": "sha512-ccD1wmbsOjCM6GY9vddqKeFXQrClcn5qUahpiYVU0I141Xq86SsuYPBR90za0EK1Se1rfD6fh3ubH1yShAkQ9Q==", + "dependencies": { + "@vanilla-extract/css": "^1.15.3", + "bezier-js": "^6.1.4" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -6739,6 +6757,42 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vanilla-extract/css": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.15.3.tgz", + "integrity": "sha512-mxoskDAxdQAspbkmQRxBvolUi1u1jnyy9WZGm+GeH8V2wwhEvndzl1QoK7w8JfA0WFevTxbev5d+i+xACZlPhA==", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@vanilla-extract/private": "^1.0.5", + "css-what": "^6.1.0", + "cssesc": "^3.0.0", + "csstype": "^3.0.7", + "dedent": "^1.5.3", + "deep-object-diff": "^1.1.9", + "deepmerge": "^4.2.2", + "media-query-parser": "^2.0.2", + "modern-ahocorasick": "^1.0.0", + "picocolors": "^1.0.0" + } + }, + "node_modules/@vanilla-extract/css/node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/@vanilla-extract/private": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.5.tgz", + "integrity": "sha512-6YXeOEKYTA3UV+RC8DeAjFk+/okoNz/h88R+McnzA2zpaVqTR/Ep+vszkWYlGBcMNO7vEkqbq5nT/JMMvhi+tw==" + }, "node_modules/@vitest/expect": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", @@ -9754,7 +9808,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "bin": { "cssesc": "bin/cssesc" }, @@ -9977,6 +10030,11 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deep-object-diff": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", + "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -15193,6 +15251,14 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" }, + "node_modules/media-query-parser": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz", + "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==", + "dependencies": { + "@babel/runtime": "^7.12.5" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -15454,6 +15520,11 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, + "node_modules/modern-ahocorasick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.0.1.tgz", + "integrity": "sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index f36d53d62..b772c420a 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@tanstack/react-query": "^5.32.0", "@tanstack/react-query-devtools": "^5.32.0", "@tippyjs/react": "^4.2.6", + "@transitmatters/stripmap": "^0.1.9", "@types/react-flatpickr": "^3.8.11", "bezier-js": "^6.1.4", "chart.js": "4.4.3",