Skip to content

Commit

Permalink
feat(ts/hooks/useDetour): control waypoints with state machine (#2607)
Browse files Browse the repository at this point in the history
Co-authored-by: Josh Larson <jlarson@mbta.com>
  • Loading branch information
firestack and joshlarson authored Jun 11, 2024
1 parent 18d69ac commit ddd6728
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 45 deletions.
80 changes: 40 additions & 40 deletions assets/src/hooks/useDetour.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from "react"
import { useCallback, useMemo } from "react"
import { ShapePoint } from "../schedule"
import { fetchDetourDirections, fetchFinishedDetour } from "../api"

Expand Down Expand Up @@ -29,19 +29,31 @@ export const useDetour = (input: CreateDetourMachineInput) => {
input,
})

const { routePattern } = snapshot.context

const [startPoint, setStartPoint] = useState<ShapePoint | null>(null)
const [endPoint, setEndPoint] = useState<ShapePoint | null>(null)
const [waypoints, setWaypoints] = useState<ShapePoint[]>([])
const { routePattern, startPoint, endPoint, waypoints } = snapshot.context
const allPoints = useMemo(() => {
if (!startPoint) {
return []
} else if (!endPoint) {
return [startPoint].concat(waypoints)
} else {
return [startPoint].concat(waypoints).concat([endPoint])
}
}, [startPoint, waypoints, endPoint])

const { result: finishedDetour } = useApiCall({
apiCall: useCallback(async () => {
/* Until we have "typegen" in XState,
* we need to validate these exist for typescript
*
* > [Warning] XState Typegen does not fully support XState v5 yet. However,
* > strongly-typed machines can still be achieved without Typegen.
* > -- https://stately.ai/docs/migration#use-typestypegen-instead-of-tstypes
*/
if (routePattern && startPoint && endPoint) {
return fetchFinishedDetour(routePattern.id, startPoint, endPoint)
} else {
return null
}

return null
}, [startPoint, endPoint, routePattern]),
})

Expand All @@ -50,15 +62,7 @@ export const useDetour = (input: CreateDetourMachineInput) => {
longitude: startPoint?.lon,
})

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

const coordinates =
detourShape.result && isOk(detourShape.result)
Expand All @@ -76,38 +80,32 @@ export const useDetour = (input: CreateDetourMachineInput) => {
})
}

const canAddWaypoint = () => startPoint !== null && endPoint === null
const canAddWaypoint = () =>
snapshot.can({
type: "detour.edit.place-waypoint",
location: { lat: 0, lon: 0 },
})

const addWaypoint = canAddWaypoint()
? (p: ShapePoint) => {
setWaypoints((positions) => [...positions, p])
? (location: ShapePoint) => {
send({ type: "detour.edit.place-waypoint", location })
}
: undefined

const addConnectionPoint = (point: ShapePoint) => {
if (startPoint === null) {
setStartPoint(point)
} else if (endPoint === null) {
setEndPoint(point)
}
}
const addConnectionPoint = (point: ShapePoint) =>
send({
type: "detour.edit.place-waypoint-on-route",
location: point,
})

const canUndo =
startPoint !== null && snapshot.matches({ "Detour Drawing": "Editing" })
const canUndo = snapshot.can({ type: "detour.edit.undo" })

const undo = () => {
if (endPoint !== null) {
setEndPoint(null)
} else if (waypoints.length > 0) {
setWaypoints((positions) => positions.slice(0, positions.length - 1))
} else if (startPoint !== null) {
setStartPoint(null)
}
send({ type: "detour.edit.undo" })
}

const clear = () => {
setEndPoint(null)
setStartPoint(null)
setWaypoints([])
send({ type: "detour.edit.clear-detour" })
}

const finishDetour = () => {
Expand Down Expand Up @@ -208,7 +206,9 @@ export const useDetour = (input: CreateDetourMachineInput) => {
clear,

/** When present, puts this detour in "finished mode" */
finishDetour: endPoint !== null ? finishDetour : undefined,
finishDetour: snapshot.can({ type: "detour.edit.done" })
? finishDetour
: undefined,
/** When present, puts this detour in "edit mode" */
editDetour,
}
Expand Down
116 changes: 111 additions & 5 deletions assets/src/models/createDetourMachine.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { setup, assign, fromPromise, ActorLogicFrom, InputFrom } from "xstate"
import { ShapePoint } from "../schedule"
import { Route, RouteId, RoutePattern } from "../schedule"
import { fetchRoutePatterns } from "../api"

Expand All @@ -9,6 +10,10 @@ export const createDetourMachine = setup({
routePattern?: RoutePattern

routePatterns?: RoutePattern[]

waypoints: ShapePoint[]
startPoint: ShapePoint | undefined
endPoint: ShapePoint | undefined
}

input:
Expand All @@ -29,8 +34,6 @@ export const createDetourMachine = setup({
}

events:
| { type: "detour.edit.done" }
| { type: "detour.edit.resume" }
| { type: "detour.route-pattern.open" }
| { type: "detour.route-pattern.done" }
| { type: "detour.route-pattern.delete-route" }
Expand All @@ -39,6 +42,13 @@ export const createDetourMachine = setup({
type: "detour.route-pattern.select-pattern"
routePattern: RoutePattern
}
| { type: "detour.edit.done" }
| { type: "detour.edit.resume" }
| { type: "detour.edit.clear-detour" }
| { type: "detour.edit.place-waypoint-on-route"; location: ShapePoint }
| { type: "detour.edit.place-waypoint"; location: ShapePoint }
| { type: "detour.edit.undo" }
| { type: "detour.share.copy-detour"; detourText: string }
},
actors: {
"fetch-route-patterns": fromPromise<
Expand All @@ -63,11 +73,40 @@ export const createDetourMachine = setup({
"set.route-id": assign({
route: (_, params: { route: Route }) => params.route,
}),
"detour.add-start-point": assign({
startPoint: (_, params: { location: ShapePoint }) => params.location,
}),
"detour.remove-start-point": assign({
startPoint: undefined,
}),
"detour.add-waypoint": assign({
waypoints: ({ context }, params: { location: ShapePoint }) => [
...context.waypoints,
params.location,
],
}),
"detour.remove-last-waypoint": assign({
waypoints: ({ context }) => context.waypoints.slice(0, -1),
}),
"detour.add-end-point": assign({
endPoint: (_, params: { location: ShapePoint }) => params.location,
}),
"detour.remove-end-point": assign({
endPoint: undefined,
}),
"detour.clear": assign({
startPoint: undefined,
waypoints: [],
endPoint: undefined,
}),
},
}).createMachine({
id: "Detours Machine",
context: ({ input }) => ({
...input,
waypoints: [],
startPoint: undefined,
endPoint: undefined,
}),

initial: "Detour Drawing",
Expand Down Expand Up @@ -176,19 +215,86 @@ export const createDetourMachine = setup({
},
},
Editing: {
initial: "Pick Start Point",
on: {
"detour.route-pattern.open": {
target: "Pick Route Pattern",
},
"detour.edit.done": {
target: "Share Detour",
"detour.edit.clear-detour": {
target: ".Pick Start Point",
actions: "detour.clear",
},
},
states: {
"Pick Start Point": {
on: {
"detour.edit.place-waypoint-on-route": {
target: "Place Waypoint",
actions: {
type: "detour.add-start-point",
params: ({ event: { location } }) => ({
location,
}),
},
},
},
},
"Place Waypoint": {
on: {
"detour.edit.place-waypoint": {
actions: {
type: "detour.add-waypoint",
params: ({ event: { location } }) => ({
location,
}),
},
},
"detour.edit.place-waypoint-on-route": {
target: "Finished Drawing",
actions: {
type: "detour.add-end-point",
params: ({ event: { location } }) => ({
location,
}),
},
},
"detour.edit.undo": [
{
guard: ({ context }) => context.waypoints.length === 0,
actions: "detour.remove-start-point",
target: "Pick Start Point",
},
{
actions: "detour.remove-last-waypoint",
target: "Place Waypoint",
},
],
},
},
"Finished Drawing": {
on: {
"detour.edit.undo": {
actions: "detour.remove-end-point",
target: "Place Waypoint",
},
"detour.edit.done": {
target: "Done",
},
},
},
Done: {
type: "final",
},
},

onDone: {
target: "Share Detour",
},
},
"Share Detour": {
on: {
"detour.edit.resume": {
target: "Editing",
target: "Editing.Finished Drawing",
},
},
},
Expand Down

0 comments on commit ddd6728

Please sign in to comment.