Skip to content

Commit

Permalink
feat: add connection points to finished detour panel (#2473)
Browse files Browse the repository at this point in the history
* refactor(ex/skate/missed_stops): convert return type for missed stops to struct

* feat(ex/skate/missed_stops): return connection segments from `missed_segments/2`

* feat(ex/skate/missed_stops): return connection stops from API

* feat(ts/models/finishedDetour): get connection points from API response

* feat(ts/components/diversionPage): add connection points to text area and `useDetour` hook
  • Loading branch information
firestack authored Mar 6, 2024
1 parent e1d3e98 commit ccd0f27
Show file tree
Hide file tree
Showing 13 changed files with 372 additions and 34 deletions.
13 changes: 12 additions & 1 deletion assets/src/components/detours/diversionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const DiversionPage = ({

missedStops,
routeSegments,
connectionPoints,

canUndo,
undo,
Expand All @@ -60,11 +61,21 @@ export const DiversionPage = ({
"Turn-by-Turn Directions:",
...(directions?.map((v) => v.instruction) ?? []),
,
"Connection Points:",
connectionPoints?.start?.name ?? "N/A",
connectionPoints?.end?.name ?? "N/A",
,
`Missed Stops (${missedStops?.length}):`,
...(missedStops?.map(({ name }) => name) ?? ["no stops"]),
].join("\n")
)
}, [originalRoute, directions, missedStops])
}, [
originalRoute,
directions,
missedStops,
connectionPoints?.start?.name,
connectionPoints?.end?.name,
])

const [showConfirmCloseModal, setShowConfirmCloseModal] =
useState<boolean>(false)
Expand Down
4 changes: 4 additions & 0 deletions assets/src/hooks/useDetour.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ export const useDetour = (routePatternId: RoutePatternId) => {
* Three partial route-shape segments: before, during, and after the detour
*/
routeSegments: finishedDetour?.routeSegments,
/**
* Connection Points
*/
connectionPoints: finishedDetour?.connectionPoint,

/**
* Reports if {@link undo} will do anything.
Expand Down
4 changes: 4 additions & 0 deletions assets/src/models/detour.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,9 @@ export interface RouteSegments {

export interface FinishedDetour {
missedStops: Stop[]
connectionPoint: {
start?: Stop
end?: Stop
}
routeSegments: RouteSegments
}
14 changes: 12 additions & 2 deletions assets/src/models/finishedDetour.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { array, Infer, number, type } from "superstruct"
import { array, Infer, nullable, number, type } from "superstruct"
import { FinishedDetour } from "./detour"
import { StopData, stopsFromData } from "./stopData"
import { StopData, stopFromData, stopsFromData } from "./stopData"

const Coordinate = type({
lat: number(),
Expand All @@ -9,6 +9,8 @@ const Coordinate = type({

export const FinishedDetourData = type({
missed_stops: array(StopData),
connection_stop_start: nullable(StopData),
connection_stop_end: nullable(StopData),
route_segments: type({
before_detour: array(Coordinate),
detour: array(Coordinate),
Expand All @@ -22,6 +24,14 @@ export const finishedDetourFromData = (
): FinishedDetour => {
return {
missedStops: stopsFromData(finishedDetour.missed_stops),
connectionPoint: {
start: finishedDetour.connection_stop_start
? stopFromData(finishedDetour.connection_stop_start)
: undefined,
end: finishedDetour.connection_stop_end
? stopFromData(finishedDetour.connection_stop_end)
: undefined,
},
routeSegments: {
beforeDetour: finishedDetour.route_segments.before_detour,
detour: finishedDetour.route_segments.detour,
Expand Down
26 changes: 14 additions & 12 deletions assets/src/models/stopData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,18 @@ export const StopData = type({
})
export type StopData = Infer<typeof StopData>

export const stopFromData = (stopData: StopData): Stop => ({
id: stopData.id,
name: stopData.name,
lat: stopData.lat,
lon: stopData.lon,
locationType:
stopData.location_type === "station"
? LocationType.Station
: LocationType.Stop,
vehicleType: stopData.vehicle_type || null,
routes: stopData.routes,
})

export const stopsFromData = (stopsData: StopData[]): Stop[] =>
stopsData.map((stopData) => ({
id: stopData.id,
name: stopData.name,
lat: stopData.lat,
lon: stopData.lon,
locationType:
stopData.location_type === "station"
? LocationType.Station
: LocationType.Stop,
vehicleType: stopData.vehicle_type || null,
routes: stopData.routes,
}))
stopsData.map(stopFromData)
59 changes: 57 additions & 2 deletions assets/tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ import routeTabFactory from "./factories/routeTab"
import stopFactory from "./factories/stop"
import * as browser from "../src/models/browser"
import { string, unknown } from "superstruct"
import { LocationType, RouteType, stopsFromData } from "../src/models/stopData"
import {
LocationType,
RouteType,
stopFromData,
stopsFromData,
} from "../src/models/stopData"
import * as Sentry from "@sentry/react"
import locationSearchResultDataFactory from "./factories/locationSearchResultData"
import locationSearchResultFactory from "./factories/locationSearchResult"
Expand Down Expand Up @@ -395,7 +400,51 @@ describe("fetchShapeForRoute", () => {
})

describe("fetchFinishedDetour", () => {
test("fetches missed stops in finished detour", () => {
test("fetches a finished detour given a Route Pattern and connection points", () => {
const stopData = stopDataFactory.buildList(3)
const [connection_stop_start, connection_stop_end] =
stopDataFactory.buildList(2)

const stops = stopsFromData(stopData)

const beforeDetour = shapePointFactory.buildList(3)
const detour = shapePointFactory.buildList(3)
const afterDetour = shapePointFactory.buildList(3)

mockFetch(200, {
data: {
missed_stops: stopData,
connection_stop_start,
connection_stop_end,
route_segments: {
before_detour: beforeDetour,
detour: detour,
after_detour: afterDetour,
},
},
})

return fetchFinishedDetour(
"route_pattern_id",
shapePointFactory.build(),
shapePointFactory.build()
).then((result) => {
expect(result).toEqual({
missedStops: stops,
connectionPoint: {
start: stopFromData(connection_stop_start),
end: stopFromData(connection_stop_end),
},
routeSegments: {
beforeDetour,
detour,
afterDetour,
},
})
})
})

test("returns `undefined` for connection points if API result is `null`", () => {
const stopData = stopDataFactory.buildList(3)

const stops = stopsFromData(stopData)
Expand All @@ -407,6 +456,8 @@ describe("fetchFinishedDetour", () => {
mockFetch(200, {
data: {
missed_stops: stopData,
connection_stop_start: null,
connection_stop_end: null,
route_segments: {
before_detour: beforeDetour,
detour: detour,
Expand All @@ -422,6 +473,10 @@ describe("fetchFinishedDetour", () => {
).then((result) => {
expect(result).toEqual({
missedStops: stops,
connectionPoint: {
start: undefined,
end: undefined,
},
routeSegments: {
beforeDetour,
detour,
Expand Down
10 changes: 10 additions & 0 deletions assets/tests/components/detours/diversionPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ describe("DiversionPage", () => {

test("'Share Detour Details' screen copies text content to clipboard when clicked copy details button", async () => {
const stops = stopFactory.buildList(4)
const [start, end] = stopFactory.buildList(2)

jest.mocked(fetchDetourDirections).mockResolvedValue(
detourShapeFactory.build({
directions: [
Expand All @@ -303,6 +305,10 @@ describe("DiversionPage", () => {
)
jest.mocked(fetchFinishedDetour).mockResolvedValue({
missedStops: stops,
connectionPoint: {
start,
end,
},
routeSegments: routeSegmentsFactory.build(),
})

Expand Down Expand Up @@ -348,6 +354,10 @@ describe("DiversionPage", () => {
"Turn right on High Street",
"Turn sharp right on Broadway",
,
"Connection Points:",
start.name,
end.name,
,
`Missed Stops (${stops.length}):`,
...stops.map(({ name }) => name),
].join("\n")
Expand Down
4 changes: 4 additions & 0 deletions assets/tests/factories/finishedDetourFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ export const routeSegmentsFactory = Factory.define<RouteSegments>(() => ({
export const finishedDetourFactory = Factory.define<FinishedDetour>(() => ({
missedStops: stopFactory.buildList(3),
routeSegments: routeSegmentsFactory.build(),
connectionPoint: {
start: stopFactory.build(),
end: stopFactory.build(),
},
}))
37 changes: 37 additions & 0 deletions assets/tests/hooks/useDetour.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,26 @@ describe("useDetour", () => {
expect(result.current.routeSegments).toEqual(routeSegments)
})

test("when `endPoint` is set, `connectionPoints` is filled in", async () => {
const { result } = renderHook(useDetourWithFakeRoutePattern)

const connectionPoint = {
start: stopFactory.build(),
end: stopFactory.build(),
}

jest
.mocked(fetchFinishedDetour)
.mockResolvedValue(finishedDetourFactory.build({ connectionPoint }))

act(() => result.current.addConnectionPoint?.(shapePointFactory.build()))
act(() => result.current.addConnectionPoint?.(shapePointFactory.build()))

await waitFor(() => {
expect(result.current.connectionPoints).toEqual(connectionPoint)
})
})

test("when `endPoint` is undone, `missedStops` is cleared", async () => {
const { result } = renderHook(useDetourWithFakeRoutePattern)

Expand Down Expand Up @@ -358,6 +378,23 @@ describe("useDetour", () => {
})
})

test("when `endPoint` is undone, `connectionPoints` is cleared", async () => {
const { result } = renderHook(useDetourWithFakeRoutePattern)

act(() => result.current.addConnectionPoint?.(shapePointFactory.build()))
act(() => result.current.addConnectionPoint?.(shapePointFactory.build()))

await waitFor(() => {
expect(result.current.connectionPoints).not.toBeUndefined()
})

act(() => result.current.undo?.())

await waitFor(() => {
expect(result.current.connectionPoints).toBeUndefined()
})
})

test("initially, `finishDetour` is `undefined`", () => {
const { result } = renderHook(useDetourWithFakeRoutePattern)

Expand Down
52 changes: 45 additions & 7 deletions lib/skate/detours/missed_stops.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,21 @@ defmodule Skate.Detours.MissedStops do
@enforce_keys [:connection_start, :connection_end, :stops, :shape]
defstruct [:connection_start, :connection_end, :stops, :shape]

defmodule Result do
@moduledoc false
@type t :: %__MODULE__{
missed_stops: [Util.Location.From.t()],
connection_stop_start: Util.Location.From.t() | nil,
connection_stop_end: Util.Location.From.t() | nil
}
@enforce_keys [:missed_stops, :connection_stop_start, :connection_stop_end]
defstruct [:missed_stops, :connection_stop_start, :connection_stop_end]
end

@doc """
Returns the contiguous list of stops, from the input parameter `cfg.stops`.
"""
@spec missed_stops(cfg :: __MODULE__.t()) :: [Util.Location.From.t()]
@spec missed_stops(cfg :: __MODULE__.t()) :: __MODULE__.Result.t()
def(
missed_stops(%__MODULE__{
stops: stops,
Expand All @@ -30,24 +41,51 @@ defmodule Skate.Detours.MissedStops do
) do
segmented_shape = segment_shape_by_stops(shape, stops)

{connection_start, connection_end}
|> missed_segments(segmented_shape)
|> Enum.map(& &1.stop)
%{
missed_stops: missed_stops,
connection_start_segment: connection_start_segment,
connection_end_segment: connection_end_segment
} =
missed_segments({connection_start, connection_end}, segmented_shape)

%__MODULE__.Result{
missed_stops: Enum.map(missed_stops, & &1.stop),
connection_stop_start:
case connection_start_segment do
%Skate.Detours.ShapeSegment{stop: stop} when stop != :none -> stop
_ -> nil
end,
connection_stop_end:
case connection_end_segment do
%Skate.Detours.ShapeSegment{stop: stop} when stop != :none -> stop
_ -> nil
end
}
end

@spec missed_segments(
{connection_start :: Util.Location.From.t(), connection_end :: Util.Location.From.t()},
segmented_shape :: [Skate.Detours.ShapeSegment.t()]
) :: [Skate.Detours.ShapeSegment.t()]
) :: %{
missed_stops: [Skate.Detours.ShapeSegment.t()],
connection_start_segment: Skate.Detours.ShapeSegment.t() | nil,
connection_end_segment: Skate.Detours.ShapeSegment.t() | nil
}
defp missed_segments({connection_start, connection_end}, segmented_shape) do
%{index: start_index} = get_index_by_min_dist(segmented_shape, connection_start)

remaining_segments = Enum.drop(segmented_shape, start_index)
{first_segments, remaining_segments} = Enum.split(segmented_shape, start_index)

%{index: end_count} =
get_index_by_min_dist(remaining_segments, connection_end)

Enum.take(remaining_segments, end_count)
{missed_stop_segments, end_segments} = Enum.split(remaining_segments, end_count)

%{
missed_stops: missed_stop_segments,
connection_start_segment: List.last(first_segments),
connection_end_segment: List.first(end_segments)
}
end

@spec segment_shape_by_stops(
Expand Down
Loading

0 comments on commit ccd0f27

Please sign in to comment.