Skip to content

Commit

Permalink
feat(skate/detours/turn-by-turn): add turn-by-turn directions to deto…
Browse files Browse the repository at this point in the history
…urs page (#2367)

* feat(ex/api/ors+ts/superstruct): add directions to API response

* feat(ex/open_route_service_api): filter out errors and goals

* fix:test(ex/controllers/detour_route_controller): add segments json element

* feat:test(ex/controllers/detour_route_controller): add tests for return value shape

* fix(ts/components/dummyDetourPage): remove `l-page` div

* feat(ts/components/diversionPage): add directions to detour page

* refactor:test(ex/controllers/detour_route_controller): replace `directions_json` with `Skate.Factory`

---------

Co-authored-by: Josh Larson <[email protected]>
  • Loading branch information
firestack and joshlarson authored Jan 24, 2024
1 parent 305065d commit 836a2f8
Show file tree
Hide file tree
Showing 13 changed files with 309 additions and 57 deletions.
13 changes: 11 additions & 2 deletions assets/src/components/detours/diversionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { DiversionPanel, DiversionPanelProps } from "./diversionPanel"
import { DetourMap } from "./detourMap"
import { Shape } from "../../schedule"
import { useDetour } from "../../hooks/useDetour"
import { ListGroup } from "react-bootstrap"

export const DiversionPage = ({
directions,
missedStops,
routeName,
routeDescription,
Expand All @@ -22,6 +22,7 @@ export const DiversionPage = ({
waypoints,

detourShape,
directions,

canUndo,
undoLastWaypoint,
Expand All @@ -34,7 +35,15 @@ export const DiversionPage = ({
</header>
<div className="l-diversion-page__panel bg-light">
<DiversionPanel
directions={directions}
directions={
<ListGroup as="ol">
{directions?.map((d) => (
<ListGroup.Item key={d.instruction} as="li">
{d.instruction}
</ListGroup.Item>
))}
</ListGroup>
}
missedStops={missedStops}
routeName={routeName}
routeDescription={routeDescription}
Expand Down
32 changes: 19 additions & 13 deletions assets/src/components/dummyDetourPage.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
import React, { useEffect, useState } from "react"
import { fetchShapeForRoute } from "../api"
import { Shape } from "../schedule"
import { fetchRoutePatterns } from "../api"
import { RoutePattern } from "../schedule"
import { DiversionPage } from "./detours/diversionPage"
import { useRoute } from "../contexts/routesContext"

export const DummyDetourPage = () => {
const [routeShape, setRouteShape] = useState<Shape | null>(null)
const [routePattern, setRoutePattern] = useState<RoutePattern | null>(null)

const routeNumber = "66"

useEffect(() => {
fetchShapeForRoute("39").then((shapes) => {
setRouteShape(shapes[0])
fetchRoutePatterns(routeNumber).then((routePatterns) => {
setRoutePattern(routePatterns[0])
})
}, [])
const route = useRoute(routePattern?.routeId)

return (
<div className="l-page">
{routeShape && (
<>
{routePattern && routePattern.shape && (
<DiversionPage
shape={routeShape}
routeName="39"
routeDescription="Forest Hills"
routeOrigin="from Back Bay"
routeDirection="Outbound"
shape={routePattern.shape}
routeName={routePattern.routeId}
routeDescription={routePattern.headsign || "?"}
routeOrigin={routePattern.name}
routeDirection={
route?.directionNames[routePattern.directionId] || "?"
}
/>
)}
</div>
</>
)
}
3 changes: 3 additions & 0 deletions assets/src/detour.d.ts → assets/src/detour.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ import { ShapePoint } from "./schedule"

export interface DetourShape {
coordinates: ShapePoint[]
directions: {
instruction: string
}[]
}
24 changes: 18 additions & 6 deletions assets/src/hooks/useDetour.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { useEffect, useMemo, useState } from "react"
import { ShapePoint } from "../schedule"
import { fetchDetourDirections } from "../api"
import { DetourShape } from "../detour"

const useDetourDirections = (shapePoints: ShapePoint[]) => {
const [detourShape, setDetourShape] = useState<ShapePoint[]>([])
const [directions, setDirections] = useState<
DetourShape["directions"] | undefined
>(undefined)

useEffect(() => {
let shouldUpdate = true
Expand All @@ -12,9 +16,10 @@ const useDetourDirections = (shapePoints: ShapePoint[]) => {
return
}

fetchDetourDirections(shapePoints).then((detourShape) => {
if (detourShape && shouldUpdate) {
setDetourShape(detourShape.coordinates)
fetchDetourDirections(shapePoints).then((detourInfo) => {
if (detourInfo && shouldUpdate) {
setDetourShape(detourInfo.coordinates)
setDirections(detourInfo.directions)
}
})

Expand All @@ -23,22 +28,25 @@ const useDetourDirections = (shapePoints: ShapePoint[]) => {
}
}, [shapePoints])

return detourShape
return {
detourShape,
directions,
}
}

export const useDetour = () => {
const [startPoint, setStartPoint] = useState<ShapePoint | null>(null)
const [endPoint, setEndPoint] = useState<ShapePoint | null>(null)
const [waypoints, setWaypoints] = useState<ShapePoint[]>([])

const detourShape = useDetourDirections(
const { detourShape, directions } = useDetourDirections(
useMemo(
() =>
[startPoint, ...waypoints, endPoint].filter(
(v): v is ShapePoint => !!v
),
[startPoint, waypoints, endPoint]
)
) ?? []
)

const canAddWaypoint = () => startPoint !== null && endPoint === null
Expand Down Expand Up @@ -92,6 +100,10 @@ export const useDetour = () => {
* The routing API generated detour shape.
*/
detourShape,
/**
* The turn-by-turn directions generated by the routing API.
*/
directions,

/**
* Reports if {@link undoLastWaypoint} will do anything.
Expand Down
7 changes: 6 additions & 1 deletion assets/src/models/detourShapeData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { array, Infer, number, type } from "superstruct"
import { array, Infer, number, string, type } from "superstruct"
import { DetourShape } from "../detour"

export const DetourShapeData = type({
Expand All @@ -8,6 +8,11 @@ export const DetourShapeData = type({
lon: number(),
})
),
directions: array(
type({
instruction: string(),
})
),
})
export type DetourShapeData = Infer<typeof DetourShapeData>

Expand Down
16 changes: 16 additions & 0 deletions assets/tests/factories/detourShapeFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Factory } from "fishery"
import { DetourShape } from "../../src/detour"
import { shapePointFactory } from "./shapePointFactory"

export const directionsFactory = Factory.define<DetourShape["directions"][0]>(
({ sequence }) => {
return {
instruction: `directionInstruction${sequence}`,
}
}
)

export const detourShapeFactory = Factory.define<DetourShape>(() => ({
coordinates: shapePointFactory.buildList(3),
directions: directionsFactory.buildList(3),
}))
12 changes: 12 additions & 0 deletions assets/tests/factories/shapePointFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Factory } from "fishery"
import { localGeoCoordinateFactory } from "./geoCoordinate"
import { ShapePoint } from "../../src/schedule"

/**
* Wrapper around {@link localGeoCoordinateFactory} until {@link ShapePoint} is
* updated to a global shared coordinate object
*/
export const shapePointFactory = Factory.define<ShapePoint>(() => {
const { latitude, longitude } = localGeoCoordinateFactory.build()
return { lat: latitude, lon: longitude }
})
51 changes: 31 additions & 20 deletions assets/tests/hooks/useDetour.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { fetchDetourDirections } from "../../src/api"
import { renderHook, waitFor } from "@testing-library/react"
import { useDetour } from "../../src/hooks/useDetour"
import { act } from "react-dom/test-utils"
import { detourShapeFactory } from "../factories/detourShapeFactory"
import { ShapePoint } from "../../src/schedule"

jest.mock("../../src/api")

Expand Down Expand Up @@ -41,36 +43,41 @@ describe("useDetour", () => {

act(() => result.current.addWaypoint({ lat: 0, lon: 0 }))

expect(result.current.waypoints).toEqual([])
expect(result.current.waypoints).toHaveLength(0)
})

test("when `endPoint` is set, `addWaypoint` does nothing", () => {
const start = { lat: 0, lon: 0 }
const end = { lat: 1, lon: 1 }

const { result } = renderHook(useDetour)

expect(result.current.startPoint).toBeNull()

act(() => result.current.addConnectionPoint(start))
act(() => result.current.addConnectionPoint(end))
act(() => result.current.addConnectionPoint({ lat: 0, lon: 0 }))
act(() => result.current.addConnectionPoint({ lat: 1, lon: 1 }))

act(() => result.current.addWaypoint({ lat: 0, lon: 0 }))

expect(result.current.waypoints).toEqual([])
expect(result.current.waypoints).toHaveLength(0)
})

test("when `addWaypoint` is called, `detourShape` is updated", async () => {
const start = { lat: 0, lon: 0 }
const end = { lat: 1, lon: 1 }
const apiResult = [
{ lat: -1, lon: -1 },
{ lat: -2, lon: -2 },
]
test("when `addWaypoint` is called, should update `detourShape` and `directions`", async () => {
const start: ShapePoint = { lat: -2, lon: -2 }
const end: ShapePoint = { lat: -1, lon: -1 }

const detourShape = detourShapeFactory.build({
coordinates: [
{ lat: 0, lon: 0 },
{ lat: 1, lon: 1 },
{ lat: 2, lon: 2 },
],
directions: [
{ instruction: "Turn Left onto Main St" },
{ instruction: "Turn Right onto High St" },
],
})

jest.mocked(fetchDetourDirections).mockImplementation((coordinates) => {
expect(coordinates).toStrictEqual([start, end])
return Promise.resolve({ coordinates: apiResult })
return Promise.resolve(detourShape)
})

const { result } = renderHook(useDetour)
Expand All @@ -80,14 +87,16 @@ describe("useDetour", () => {

expect(result.current.startPoint).toBe(start)

expect(jest.mocked(fetchDetourDirections)).toHaveBeenCalledTimes(1)
expect(jest.mocked(fetchDetourDirections)).toHaveBeenNthCalledWith(1, [
start,
end,
])

await waitFor(() =>
expect(result.current.detourShape).toStrictEqual(apiResult)
)
await waitFor(() => {
expect(result.current.detourShape).toStrictEqual(detourShape.coordinates)
expect(result.current.directions).toStrictEqual(detourShape.directions)
})
})

test("when `undoLastWaypoint` is called, removes the last `waypoint`", async () => {
Expand All @@ -103,7 +112,7 @@ describe("useDetour", () => {

act(() => result.current.undoLastWaypoint())

expect(result.current.waypoints).toStrictEqual([])
expect(result.current.waypoints).toHaveLength(0)
})

test("when `undoLastWaypoint` is called, should call API with updated waypoints", async () => {
Expand All @@ -130,7 +139,7 @@ describe("useDetour", () => {

act(() => result.current.addConnectionPoint({ lat: 0, lon: 0 }))

expect(result.current.waypoints).toStrictEqual([])
expect(result.current.waypoints).toHaveLength(0)
expect(result.current.canUndo).toBe(false)
})

Expand All @@ -140,6 +149,7 @@ describe("useDetour", () => {
act(() => result.current.addConnectionPoint({ lat: 0, lon: 0 }))
act(() => result.current.addWaypoint({ lat: 1, lon: 1 }))

expect(result.current.waypoints).not.toHaveLength(0)
expect(result.current.canUndo).toBe(true)
})

Expand All @@ -149,6 +159,7 @@ describe("useDetour", () => {
act(() => result.current.addConnectionPoint({ lat: 0, lon: 0 }))
act(() => result.current.addConnectionPoint({ lat: 0, lon: 0 }))

expect(result.current.endPoint).not.toBeNull()
expect(result.current.canUndo).toBe(false)
})
})
Loading

0 comments on commit 836a2f8

Please sign in to comment.