diff --git a/packages/base-map/src/index.tsx b/packages/base-map/src/index.tsx index 4de52a213..759578b17 100644 --- a/packages/base-map/src/index.tsx +++ b/packages/base-map/src/index.tsx @@ -1,13 +1,16 @@ +import maplibregl, { + MapLayerMouseEvent, + MapLayerTouchEvent +} from "maplibre-gl"; import React, { useCallback, useState } from "react"; -import { Map, MapProps } from "react-map-gl/maplibre"; -import maplibregl, { MapLayerMouseEvent } from "maplibre-gl"; +import { Map, MapProps, ViewStateChangeEvent } from "react-map-gl/maplibre"; import { useIntl } from "react-intl"; -import * as Styled from "./styled"; -import * as util from "./util"; import MarkerWithPopup from "./MarkerWithPopup"; import generateMapControlTranslations from "./mapControlLocale"; +import * as Styled from "./styled"; +import * as util from "./util"; /** * The BaseMap component renders a MapLibre map @@ -42,7 +45,7 @@ type Props = React.ComponentPropsWithoutRef & { /** A callback method which is fired when the layer selector component is clicked with the left mouse button/tapped */ onClickLayerSelector?: () => void; /** A callback method which is fired when the map is clicked with the right mouse button/long tapped */ - onContextMenu?: (e: MapLayerMouseEvent) => void; + onContextMenu?: (e: MapLayerMouseEvent | MapLayerTouchEvent) => void; /** A callback method which is fired when the map zoom or map bounds change */ onViewportChanged?: (e: State) => void; /** When set to true, all hidden layers will be removed. No layers will be uncheckable until @@ -80,7 +83,7 @@ const BaseMap = ({ // Firefox and Safari on iOS: hover is not triggered when the user touches the layer selector // (unlike Firefox or Chromium on Android), so we have to detect touch and trigger hover ourselves. const [fakeMobileHover, setFakeHover] = useState(false); - const [longPressTimer, setLongPressTimer] = useState(null); + const [longPressTimer, setLongPressTimer] = useState(); const toggleableLayers = Array.isArray(children) ? children @@ -108,12 +111,14 @@ const BaseMap = ({ typeof baseLayer === "object" ? baseLayer?.[0] : baseLayer ); - const clearLongPressTimer = useCallback(() => clearTimeout(longPressTimer), [ - longPressTimer - ]); + const clearLongPressTimer = useCallback(() => { + if (longPressTimer) { + clearTimeout(longPressTimer); + } + }, [longPressTimer]); const handleViewportChange = useCallback( - evt => { + (evt: ViewStateChangeEvent) => { onViewportChanged && onViewportChanged(evt.viewState); clearLongPressTimer(); }, @@ -153,17 +158,8 @@ const BaseMap = ({ // Start detecting long presses on screens when there is only one touch point. // If the user is pinching the map or does other multi-touch actions, cancel long-press detection. const touchPointCount = e.points.length; - if (touchPointCount === 1) { - setLongPressTimer( - setTimeout( - // In practice, MapLayerTouchEvent and MapTouchEvent behave as if they were - // subclasses of MapLayerMouseEvent and MapMouseEvent, respectively, - // so the conversion from touch to mouse event works, with the caveat that - // the `type` prop takes different string values between mouse and touch events. - () => onContextMenu((e as unknown) as MapLayerMouseEvent), - 600 - ) - ); + if (touchPointCount === 1 && onContextMenu) { + setLongPressTimer(window.setTimeout(() => onContextMenu(e), 600)); } else { clearLongPressTimer(); } diff --git a/packages/base-map/src/mapControlLocale.tsx b/packages/base-map/src/mapControlLocale.tsx index 9193a109f..9162c3518 100644 --- a/packages/base-map/src/mapControlLocale.tsx +++ b/packages/base-map/src/mapControlLocale.tsx @@ -23,7 +23,7 @@ const MAP_CONTROL_TRANSLATION_KEYS = [ const generateMapControlTranslations = ( intl: IntlShape ): Record => { - const languageObject = {}; + const languageObject: Record = {}; MAP_CONTROL_TRANSLATION_KEYS.forEach(x => { languageObject[x] = intl.formatMessage({ diff --git a/packages/base-map/tsconfig.json b/packages/base-map/tsconfig.json index 1501a23ae..88f577299 100644 --- a/packages/base-map/tsconfig.json +++ b/packages/base-map/tsconfig.json @@ -5,7 +5,8 @@ "lib": ["es2019", "dom"], "outDir": "./lib", "rootDir": "./src", - "skipLibCheck": true + "skipLibCheck": true, + "strict": true }, "include": ["src/**/*"], "references": [ diff --git a/packages/building-blocks/package.json b/packages/building-blocks/package.json index 0a9292c5d..a6ff4fc9e 100644 --- a/packages/building-blocks/package.json +++ b/packages/building-blocks/package.json @@ -9,8 +9,8 @@ "module": "esm/index.js", "private": false, "devDependencies": { - "@opentripplanner/types": "workspace:^", - "@opentripplanner/core-utils": "workspace:^" + "@opentripplanner/core-utils": "workspace:^", + "@opentripplanner/types": "workspace:^" }, "peerDependencies": { "react": "catalog:", diff --git a/packages/core-utils/package.json b/packages/core-utils/package.json index bbdf7251e..3e679b901 100644 --- a/packages/core-utils/package.json +++ b/packages/core-utils/package.json @@ -1,6 +1,6 @@ { "name": "@opentripplanner/core-utils", - "version": "16.0.4", + "version": "16.0.5", "description": "Core functionality that is shared among numerous UI components", "engines": { "node": ">=13" diff --git a/packages/core-utils/src/__tests__/__snapshots__/query.js.snap b/packages/core-utils/src/__tests__/__snapshots__/query.js.snap index 7a77e7ccc..2f3ae17c3 100644 --- a/packages/core-utils/src/__tests__/__snapshots__/query.js.snap +++ b/packages/core-utils/src/__tests__/__snapshots__/query.js.snap @@ -263,6 +263,10 @@ exports[`query parseLocationString should return location with coordinates as na exports[`query parseLocationString should return null for null input 1`] = `null`; +exports[`query parseLocationString should return null with name as name with no coords (but with commas) for input 1`] = `null`; + +exports[`query parseLocationString should return null with name as name with no coords for input 1`] = `null`; + exports[`query planParamsToQuery should parse a depart at query 1`] = ` { "bannedRoutes": "897ABC", diff --git a/packages/core-utils/src/__tests__/query.js b/packages/core-utils/src/__tests__/query.js index 8a013422d..559f1286c 100644 --- a/packages/core-utils/src/__tests__/query.js +++ b/packages/core-utils/src/__tests__/query.js @@ -218,6 +218,13 @@ describe("query", () => { parseLocationString("33.983929829,-87.3892387982") ).toMatchSnapshot(); }); + + it("should return null with name as name with no coords for input", () => { + expect(parseLocationString("1 cool street")).toMatchSnapshot(); + }); + it("should return null with name as name with no coords (but with commas) for input", () => { + expect(parseLocationString("148, se street")).toMatchSnapshot(); + }); }); describe("planParamsToQuery", () => { diff --git a/packages/core-utils/src/query.js b/packages/core-utils/src/query.js index 0cc1f8446..75aba6338 100644 --- a/packages/core-utils/src/query.js +++ b/packages/core-utils/src/query.js @@ -247,7 +247,7 @@ async function getFirstGeocodeResult( if (firstResult) { return geocoder.getLocationFromGeocodedFeature(firstResult); } - return null; + return { name: `(Geocoder failure) ${text}` }; }); } @@ -265,7 +265,7 @@ export function parseLocationString(value) { ? stringToCoords(parts[1]) : stringToCoords(parts[0]); const name = parts[1] ? parts[0] : coordsToString(coordinates); - return coordinates.length === 2 + return coordinates.length === 2 && coordinates.every(c => !!c) ? { name: name || null, lat: coordinates[0] || null, diff --git a/packages/timetable/package.json b/packages/timetable/package.json index d16017e59..904a6f799 100644 --- a/packages/timetable/package.json +++ b/packages/timetable/package.json @@ -1,6 +1,6 @@ { "name": "@opentripplanner/timetable", - "version": "1.0.0", + "version": "1.1.0", "description": "A component to display a timetable for a given route pattern", "main": "lib/index.js", "module": "esm/index.js", @@ -11,13 +11,13 @@ "private": false, "peerDependencies": { "react": "catalog:", + "react-intl": "catalog:", "styled-components": "catalog:" }, "scripts": { "tsc": "tsc" }, "dependencies": { - "react-intl": "catalog:", "toposort": "^2.0.2" }, "devDependencies": { diff --git a/packages/timetable/src/index.tsx b/packages/timetable/src/index.tsx index a88b2715b..187c9909b 100644 --- a/packages/timetable/src/index.tsx +++ b/packages/timetable/src/index.tsx @@ -15,9 +15,10 @@ const TBody = styled.tbody` white-space: nowrap; `; -const TH = styled.th` +const TH = styled.th<{ closed?: boolean }>` min-width: ${COLUMN_WIDTH}; padding: 1rem; + text-decoration: ${props => (props.closed ? "line-through" : "")}; `; const TR = styled.tr` @@ -25,8 +26,9 @@ const TR = styled.tr` vertical-align: middle; `; -const TD = styled.td` +const TD = styled.td<{ closed?: boolean }>` padding: 1rem; + text-decoration: ${props => (props.closed ? "line-through" : "")}; `; interface PatternStop { @@ -176,6 +178,8 @@ interface TimeTableProps { route: Route; /** Whether to show only timepoint stops in the timetable */ timepointsOnly: boolean; + /** A set of gtfsId values for stops that are closed and should be shown with strikethrough */ + closedStops?: Set; /** If the topological sort of the stop IDs fails for any reason, a `false` value here * will cause the timetable to use a fallback "naive" stop sorting */ @@ -194,6 +198,7 @@ interface TimeTableProps { const TimeTable = (props: TimeTableProps): JSX.Element => { const { + closedStops, directionId, errorOnStopSorting, includeDwellStops, @@ -314,27 +319,35 @@ const TimeTable = (props: TimeTableProps): JSX.Element => { }, [allTrips, comparator]); return ( - - +
+ {(showBlockId ? [{ id: "blockIdHeader", name: "Block ID" }] : []) .concat(filteredPatternStops) .map((s, index) => { return ( - ); })} - + {timetableTrips.map((t, index) => { - const rowValues: string[] = showBlockId ? [t.blockId] : []; + const rowValues: { closed: boolean; value: string }[] = showBlockId + ? [{ closed: false, value: t.blockId }] + : []; filteredPatternStops.forEach(patternStop => { const stopDetail = t.stops.get(patternStop.id); - rowValues.push( - stopDetail + rowValues.push({ + closed: closedStops?.has(patternStop.id) || false, + value: stopDetail ? intl ? intl.formatTime(stopDetail.time) : stopDetail.time.toLocaleTimeString("en-us", { @@ -344,13 +357,15 @@ const TimeTable = (props: TimeTableProps): JSX.Element => { minute: "2-digit" }) : "-" - ); + }); }); return ( - + {rowValues.map((r, rowIndex) => ( - + ))} ); diff --git a/packages/timetable/src/stories/Timetable.story.tsx b/packages/timetable/src/stories/Timetable.story.tsx index d3c4a76a7..87e990dfa 100644 --- a/packages/timetable/src/stories/Timetable.story.tsx +++ b/packages/timetable/src/stories/Timetable.story.tsx @@ -1,5 +1,6 @@ // eslint-disable-next-line prettier/prettier import type { Meta, StoryObj } from "@storybook/react-vite" +import React from "react"; import TimeTable from ".." @@ -7,13 +8,33 @@ import twinCitiesRouteMock from "../../__mocks__/route-mock.json" const meta = { component: TimeTable, - title: "Timetable" + title: "Timetable", + argTypes: { + closedStops: { + options: ["Grand St NE & 29th Ave NE (Direction: 1)", "46th St & I-35W (Direction: 0)"], + control: { type: "check" }, + mapping: { + "Grand St NE & 29th Ave NE (Direction: 1)": "2:14634", + "46th St & I-35W (Direction: 0)": "2:53545" + } + }, + directionId: { + control: { type: "radio" }, + options: [0, 1] + } + } } satisfies Meta; export default meta; type Story = StoryObj; export const Default: Story = { + // eslint-disable-next-line react/display-name + render: (args) => { + const { closedStops } = args; + // eslint-disable-next-line react/jsx-props-no-spreading + return + }, args: { directionId: 0, route: twinCitiesRouteMock.data.route, diff --git a/packages/timetable/src/stories/__snapshots__/Timetable.story.tsx.snap b/packages/timetable/src/stories/__snapshots__/Timetable.story.tsx.snap index f56e14bd7..e0e87a1ac 100644 --- a/packages/timetable/src/stories/__snapshots__/Timetable.story.tsx.snap +++ b/packages/timetable/src/stories/__snapshots__/Timetable.story.tsx.snap @@ -1,199 +1,199 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Timetable Default smoke-test 1`] = ` -
+ {s.name}
{r} + {r.value} +
- + - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - + - - - - - - - - - - - - - - + - - - - - - - - - - - - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e414afbfa..c6f256c7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5261,6 +5261,7 @@ packages: conventional-changelog-core@5.0.1: resolution: {integrity: sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A==} engines: {node: '>=14'} + deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead. conventional-changelog-preset-loader@3.0.0: resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==} @@ -6455,7 +6456,7 @@ packages: git-raw-commits@3.0.0: resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==} engines: {node: '>=14'} - deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead. + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true git-remote-origin-url@2.0.0: @@ -6465,7 +6466,7 @@ packages: git-semver-tags@5.0.1: resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==} engines: {node: '>=14'} - deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead. + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true git-up@6.0.0: @@ -20083,7 +20084,7 @@ snapshots: dependencies: find-up-simple: 1.0.0 read-pkg: 9.0.1 - type-fest: 4.26.1 + type-fest: 4.41.0 read-package-up@12.0.0: dependencies: @@ -21779,11 +21780,11 @@ snapshots: wait-on@7.2.0: dependencies: - axios: 1.7.7 + axios: 1.15.2 joi: 17.13.3 - lodash: 4.17.21 + lodash: 4.18.1 minimist: 1.2.8 - rxjs: 7.8.1 + rxjs: 7.8.2 transitivePeerDependencies: - debug
Pleasant Ave & 50th St / Busch Terrace Nicollet Ave S & 46th St E 46th St & I-35W 4th Ave S & 38th St E 4th Ave S & Lake St E 3rd Ave S & Franklin Ave E Nicollet Mall & Alice Rainville Pl Nicollet Mall & 7th St S Nicollet Mall & 3rd St S Hennepin Ave E & Wilder St Lowry Ave NE & 2nd St / 1st St NE Grand St NE & 29th Ave NE Columbia Heights Transit Center C
+
- + 06:00 + 06:02 + 06:06 + 06:11 + 06:16 + 06:20 + 06:24 + 06:28 + 06:31 + 06:39 + 06:42 + 06:52
+
16:10 + - + 16:26 + 16:31 + 16:36 + - + - + - + - + - + - + - + -
+
- + 01:37 + 01:39 + 01:43 + 01:47 + 01:52 + 01:56 + 02:00 + 02:13 + 02:15 + 02:23 + 02:26 + 02:36