Skip to content

Commit

Permalink
refactor(ts/components/detourMap): move state up in tree (#2364)
Browse files Browse the repository at this point in the history
* refactor(ts/components/detourMap): move state out into `useDetour` hook

* refactor(ts/components/detourMap): simplify `useDetour` API

* refactor(ts/components/detourMap): merge `RouteShapeWithDetour` into `DetourMap`

* refactor(ts/components/detors): move `DetourMap` state into `DiversionPage`

---------

Co-authored-by: Josh Larson <[email protected]>
  • Loading branch information
firestack and joshlarson authored Jan 23, 2024
1 parent 5e773e4 commit 305065d
Show file tree
Hide file tree
Showing 8 changed files with 649 additions and 232 deletions.
242 changes: 112 additions & 130 deletions assets/src/components/detours/detourMap.tsx
Original file line number Diff line number Diff line change
@@ -1,152 +1,134 @@
import React, { useEffect, useState } from "react"
import { Shape, ShapePoint } from "../../schedule"
import React from "react"
import { LatLngLiteral } from "leaflet"
import { Polyline, useMapEvent } from "react-leaflet"
import { Polyline, useMapEvents } from "react-leaflet"
import Leaflet from "leaflet"
import Map from "../map"
import { CustomControl } from "../map/controls/customControl"
import { Button } from "react-bootstrap"
import { ReactMarker } from "../map/utilities/reactMarker"
import { closestPosition } from "../../util/math"
import { fetchDetourDirections } from "../../api"
import { ShapePoint } from "../../schedule"
import {
latLngLiteralToShapePoint,
shapePointToLatLngLiteral,
} from "../../util/pointLiterals"

export const DetourMap = ({ shape }: { shape: Shape }) => {
const [startPoint, setStartPoint] = useState<LatLngLiteral | null>(null)
const [endPoint, setEndPoint] = useState<LatLngLiteral | null>(null)
const [waypoints, setWaypoints] = useState<LatLngLiteral[]>([])
const [detourShapePositions, setDetourShapePositions] = useState<
LatLngLiteral[]
>([])

const onAddDetourPosition = (p: LatLngLiteral) => {
setWaypoints((positions) => [...positions, p])
}

useEffect(() => {
let shouldUpdate = true

const shapePoints: ShapePoint[] = waypoints.map(latLngLiteralToShapePoint)

fetchDetourDirections(shapePoints).then((detourShape) => {
if (detourShape && shouldUpdate) {
setDetourShapePositions(
detourShape.coordinates.map(shapePointToLatLngLiteral)
)
}
})

return () => {
shouldUpdate = false
}
}, [waypoints])

return (
<Map vehicles={[]}>
<CustomControl position="topleft" className="leaflet-bar">
<Button
variant="primary"
disabled={
startPoint === null || endPoint !== null || waypoints.length === 1
}
onClick={() =>
setWaypoints((positions) =>
positions.slice(0, positions.length - 1)
)
}
>
Clear Last Waypoint
</Button>
</CustomControl>
<RouteShapeWithDetour
originalShape={shape}
startPoint={startPoint}
onSetStartPoint={setStartPoint}
endPoint={endPoint}
onSetEndPoint={setEndPoint}
waypoints={waypoints}
onAddDetourPosition={onAddDetourPosition}
detourShapePositions={detourShapePositions}
/>
</Map>
)
interface DetourMapProps {
/**
* Coordinates to display as the original route.
*/
originalShape: ShapePoint[]
/**
* Coordinates to display as the detour line.
*/
detourShape: ShapePoint[]

/**
* Coordinate to display as the beginning connection point.
*/
startPoint?: ShapePoint

/**
* Coordinate to display as the ending connection point.
*/
endPoint?: ShapePoint

/**
* Coordinates to display as the waypoints.
*/
waypoints: ShapePoint[]

/**
* Callback fired when the {@link originalShape} is clicked.
*/
onClickOriginalShape: (point: ShapePoint) => void

/**
* Callback fired when the map is clicked.
* @param point
*/
onClickMap: (point: ShapePoint) => void

/**
* User signal to describe the state of the undo button.
*/
undoDisabled: boolean
/**
* Callback fired when the undo button is clicked.
*/
onUndoLastWaypoint: () => void
}

const RouteShapeWithDetour = ({
export const DetourMap = ({
originalShape,
detourShape,

startPoint,
onSetStartPoint,
endPoint,
onSetEndPoint,
waypoints,
onAddDetourPosition,
detourShapePositions,
}: {
originalShape: Shape
startPoint: LatLngLiteral | null
onSetStartPoint: (p: LatLngLiteral | null) => void
endPoint: LatLngLiteral | null
onSetEndPoint: (p: LatLngLiteral | null) => void
waypoints: LatLngLiteral[]
onAddDetourPosition: (p: LatLngLiteral) => void
detourShapePositions: LatLngLiteral[]
}) => {
const routeShapePositions: LatLngLiteral[] = originalShape.points.map(
shapePointToLatLngLiteral
)

useMapEvent("click", (e) => {
if (startPoint !== null && endPoint === null) {
onAddDetourPosition(e.latlng)
}
})

// points on the detour not already represented by the start and end
const uniqueWaypoints =
waypoints.length === 0
? []
: endPoint === null
? waypoints.slice(1)
: waypoints.slice(1, -1)

return (
<>
<Polyline
positions={routeShapePositions}
className="c-detour_map--original-route-shape"
eventHandlers={{
click: (e) => {
if (startPoint === null) {
const { position } =
closestPosition(routeShapePositions, e.latlng) ?? {}

position && onSetStartPoint(position)
position && onAddDetourPosition(position)
} else if (endPoint === null) {
const { position } =
closestPosition(routeShapePositions, e.latlng) ?? {}

position && onSetEndPoint(position)
position && onAddDetourPosition(position)
}
},
}}
bubblingMouseEvents={false}
/>
{startPoint && <StartMarker position={startPoint} />}
{endPoint && <EndMarker position={endPoint} />}
<Polyline
positions={detourShapePositions}
className="c-detour_map--detour-route-shape"

onClickOriginalShape,
onClickMap,

undoDisabled,
onUndoLastWaypoint,
}: DetourMapProps) => (
<Map vehicles={[]}>
<CustomControl position="topleft" className="leaflet-bar">
<Button
variant="primary"
disabled={undoDisabled}
onClick={onUndoLastWaypoint}
>
Clear Last Waypoint
</Button>
</CustomControl>

<MapEvents
click={(e) => {
onClickMap(latLngLiteralToShapePoint(e.latlng))
}}
/>

{startPoint && (
<StartMarker position={shapePointToLatLngLiteral(startPoint)} />
)}

{waypoints.map((position) => (
<DetourPointMarker
key={JSON.stringify(position)}
position={shapePointToLatLngLiteral(position)}
/>
{uniqueWaypoints.map((position) => (
<DetourPointMarker key={position.toString()} position={position} />
))}
</>
)
))}

{endPoint && <EndMarker position={shapePointToLatLngLiteral(endPoint)} />}

<Polyline
positions={detourShape.map(shapePointToLatLngLiteral)}
className="c-detour_map--detour-route-shape"
/>

<Polyline
positions={originalShape.map(shapePointToLatLngLiteral)}
className="c-detour_map--original-route-shape"
bubblingMouseEvents={false}
eventHandlers={{
click: (e) => {
const { position } =
closestPosition(
originalShape.map(shapePointToLatLngLiteral),
e.latlng
) ?? {}
position && onClickOriginalShape(latLngLiteralToShapePoint(position))
},
}}
/>
</Map>
)

const MapEvents = (props: Leaflet.LeafletEventHandlerFnMap) => {
useMapEvents(props)
return null
}

const StartMarker = ({ position }: { position: LatLngLiteral }) => (
Expand Down
67 changes: 47 additions & 20 deletions assets/src/components/detours/diversionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react"
import { DiversionPanel, DiversionPanelProps } from "./diversionPanel"
import { DetourMap } from "./detourMap"
import { Shape } from "../../schedule"
import { useDetour } from "../../hooks/useDetour"

export const DiversionPage = ({
directions,
Expand All @@ -11,23 +12,49 @@ export const DiversionPage = ({
routeDirection,
routeOrigin,
shape,
}: DiversionPanelProps & { shape: Shape }) => (
<article className="l-diversion-page h-100 border-box">
<header className="l-diversion-page__header text-bg-light border-bottom">
<h1 className="h3 text-center">Create Detour</h1>
</header>
<div className="l-diversion-page__panel bg-light">
<DiversionPanel
directions={directions}
missedStops={missedStops}
routeName={routeName}
routeDescription={routeDescription}
routeOrigin={routeOrigin}
routeDirection={routeDirection}
/>
</div>
<div className="l-diversion-page__map">
<DetourMap shape={shape} />
</div>
</article>
)
}: DiversionPanelProps & { shape: Shape }) => {
const {
addConnectionPoint,
addWaypoint,

startPoint,
endPoint,
waypoints,

detourShape,

canUndo,
undoLastWaypoint,
} = useDetour()

return (
<article className="l-diversion-page h-100 border-box">
<header className="l-diversion-page__header text-bg-light border-bottom">
<h1 className="h3 text-center">Create Detour</h1>
</header>
<div className="l-diversion-page__panel bg-light">
<DiversionPanel
directions={directions}
missedStops={missedStops}
routeName={routeName}
routeDescription={routeDescription}
routeOrigin={routeOrigin}
routeDirection={routeDirection}
/>
</div>
<div className="l-diversion-page__map">
<DetourMap
originalShape={shape.points}
detourShape={detourShape}
startPoint={startPoint ?? undefined}
endPoint={endPoint ?? undefined}
waypoints={waypoints}
onClickMap={addWaypoint}
onClickOriginalShape={addConnectionPoint}
undoDisabled={canUndo === false}
onUndoLastWaypoint={undoLastWaypoint}
/>
</div>
</article>
)
}
12 changes: 10 additions & 2 deletions assets/src/components/dummyDetourPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react"
import { fetchShapeForRoute } from "../api"
import { DetourMap } from "./detours/detourMap"
import { Shape } from "../schedule"
import { DiversionPage } from "./detours/diversionPage"

export const DummyDetourPage = () => {
const [routeShape, setRouteShape] = useState<Shape | null>(null)
Expand All @@ -14,7 +14,15 @@ export const DummyDetourPage = () => {

return (
<div className="l-page">
{routeShape && <DetourMap shape={routeShape} />}
{routeShape && (
<DiversionPage
shape={routeShape}
routeName="39"
routeDescription="Forest Hills"
routeOrigin="from Back Bay"
routeDirection="Outbound"
/>
)}
</div>
)
}
4 changes: 2 additions & 2 deletions assets/src/components/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "leaflet-defaulticon-compatibility" // see https://github.com/Leaflet/Lea
import "leaflet.fullscreen"
import React, {
MutableRefObject,
PropsWithChildren,
ReactElement,
useEffect,
useRef,
Expand Down Expand Up @@ -42,9 +43,8 @@ import {
usePickerContainerFollowerFn,
} from "./map/follower"

export interface Props {
export interface Props extends PropsWithChildren {
reactLeafletRef?: MutableRefObject<LeafletMap | null>
children?: ReactElement | ReactElement[]
tileType?: TileType
stateClasses?: string

Expand Down
Loading

0 comments on commit 305065d

Please sign in to comment.