Skip to content

Commit

Permalink
Detour pilot: basic detour route drawing (#2334)
Browse files Browse the repository at this point in the history
* refactor: remove redundant fragment

* feat: dummy detour page with ability to select starting point

* feat: allow adding points to the detour and finishing the detour

* fixup! feat: allow adding points to the detour and finishing the detour

* refactor: move some state up into DetourMap component

* fix: lock in endpoint once selected

* feat: add ability to remove most recent waypoint

* feat: style shapes and start / end markers

* feat: marker for intermediate points on detour

* fix: titles for start and end markers

* test: basic unit tests for route shape drawing

* fix: fix CSS class names

* fixup! fix: fix CSS class names

* test: more unit tests for detour drawing

* refactor: change props to take shape, remove dummy page

* fixup! refactor: change props to take shape, remove dummy page

* feat: place route drawing component in diversions page

* fix: remove last remnants of dummy detours page

* fix: UI details better match route shape in story

Co-authored-by: Josh Larson <[email protected]>

* refactor: rename `shape` prop to `originalShape`

* fix: prevent duplicate positions on detour shape

---------

Co-authored-by: Josh Larson <[email protected]>
  • Loading branch information
lemald and joshlarson authored Jan 3, 2024
1 parent 3372f5e commit 33fc966
Show file tree
Hide file tree
Showing 9 changed files with 1,358 additions and 36 deletions.
1 change: 1 addition & 0 deletions assets/css/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ $z-properties-panel-context: (
@import "crowding";
@import "cutout_overlay";
@import "data_status_banner";
@import "detours/detour_map";
@import "directions_button";
@import "disconnected_modal";
@import "diversion_page";
Expand Down
2 changes: 1 addition & 1 deletion assets/css/bootstrap.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
// @import "../node_modules/bootstrap/scss/badge";
// @import "../node_modules/bootstrap/scss/breadcrumb";
// @import "../node_modules/bootstrap/scss/button-group";
// @import "../node_modules/bootstrap/scss/buttons";
@import "../node_modules/bootstrap/scss/buttons";
// @import "../node_modules/bootstrap/scss/card";
// @import "../node_modules/bootstrap/scss/carousel";
// @import "../node_modules/bootstrap/scss/close";
Expand Down
17 changes: 17 additions & 0 deletions assets/css/detours/_detour_map.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.c-detour_map--original-route-shape {
stroke: $color-kiwi-500;
}

.c-detour_map--detour-route-shape {
stroke: $color-lemon-500;
}

.c-detour_map-circle-marker--start {
stroke: $color-kiwi-400;
fill: $color-kiwi-400;
}

.c-detour_map-circle-marker--end {
stroke: $color-strawberry-400;
fill: $color-strawberry-400;
}
204 changes: 204 additions & 0 deletions assets/src/components/detours/detourMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import React, { useState } from "react"
import { Shape } from "../../schedule"
import { LatLngExpression } from "leaflet"
import { Polyline, useMap, useMapEvent } from "react-leaflet"
import Leaflet, { Map as LeafletMap } from "leaflet"
import Map from "../map"
import { CustomControl } from "../map/controls/customControl"
import { Button } from "react-bootstrap"
import { ReactMarker } from "../map/utilities/reactMarker"

export const DetourMap = ({ shape }: { shape: Shape }) => {
const [startPoint, setStartPoint] = useState<LatLngExpression | null>(null)
const [endPoint, setEndPoint] = useState<LatLngExpression | null>(null)
const [detourPositions, setDetourPositions] = useState<LatLngExpression[]>([])

const onAddDetourPosition = (p: LatLngExpression) =>
setDetourPositions((positions) => [...positions, p])

return (
<Map vehicles={[]}>
<CustomControl position="topleft" className="leaflet-bar">
<Button
variant="primary"
disabled={
startPoint === null ||
endPoint !== null ||
detourPositions.length === 1
}
onClick={() =>
setDetourPositions((positions) =>
positions.slice(0, positions.length - 1)
)
}
>
Clear Last Waypoint
</Button>
</CustomControl>
<RouteShapeWithDetour
originalShape={shape}
startPoint={startPoint}
onSetStartPoint={setStartPoint}
endPoint={endPoint}
onSetEndPoint={setEndPoint}
detourPositions={detourPositions}
onAddDetourPosition={onAddDetourPosition}
/>
</Map>
)
}

const RouteShapeWithDetour = ({
originalShape,
startPoint,
onSetStartPoint,
endPoint,
onSetEndPoint,
detourPositions,
onAddDetourPosition,
}: {
originalShape: Shape
startPoint: LatLngExpression | null
onSetStartPoint: (p: LatLngExpression | null) => void
endPoint: LatLngExpression | null
onSetEndPoint: (p: LatLngExpression | null) => void
detourPositions: LatLngExpression[]
onAddDetourPosition: (p: LatLngExpression) => void
}) => {
const routeShapePositions: LatLngExpression[] = originalShape.points.map(
(point) => [point.lat, point.lon]
)

const map = useMap()

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

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

return (
<>
<Polyline
positions={routeShapePositions}
className="c-detour_map--original-route-shape"
eventHandlers={{
click: (e) => {
if (startPoint === null) {
const position = closestPosition(
routeShapePositions,
e.latlng,
map
)
onSetStartPoint(position)
position && onAddDetourPosition(position)
} else if (endPoint === null) {
const position = closestPosition(
routeShapePositions,
e.latlng,
map
)
onSetEndPoint(position)
position && onAddDetourPosition(position)
}
},
}}
bubblingMouseEvents={false}
/>
{startPoint && <StartMarker position={startPoint} />}
{endPoint && <EndMarker position={endPoint} />}
<Polyline
positions={detourPositions}
className="c-detour_map--detour-route-shape"
/>
{uniqueDetourPositions.map((position) => (
<DetourPointMarker key={position.toString()} position={position} />
))}
</>
)
}

const StartMarker = ({ position }: { position: LatLngExpression }) => (
<StartOrEndMarker
classSuffix="start"
title="Detour Start"
position={position}
/>
)

const EndMarker = ({ position }: { position: LatLngExpression }) => (
<StartOrEndMarker classSuffix="end" title="Detour End" position={position} />
)

const StartOrEndMarker = ({
classSuffix,
title,
position,
}: {
classSuffix: string
title: string
position: LatLngExpression
}) => (
<ReactMarker
interactive={false}
position={position}
divIconSettings={{
iconSize: [20, 20],
iconAnchor: new Leaflet.Point(10, 10),
className: "c-detour_map-circle-marker--" + classSuffix,
}}
title={title}
icon={
<svg height="20" width="20">
<circle cx={10} cy={10} r={10} />
</svg>
}
/>
)

const DetourPointMarker = ({ position }: { position: LatLngExpression }) => (
<ReactMarker
interactive={false}
position={position}
divIconSettings={{
iconSize: [10, 10],
iconAnchor: new Leaflet.Point(5, 5),
className: "c-detour_map-circle-marker--detour-point",
}}
icon={
<svg height="10" width="10">
<circle cx={5} cy={5} r={5} />
</svg>
}
/>
)

const closestPosition = (
positions: LatLngExpression[],
position: LatLngExpression,
map: LeafletMap
): LatLngExpression | null => {
const [closestPosition] = positions.reduce<
[LatLngExpression | null, number | null]
>(
([closestPosition, closestDistance], currentPosition) => {
const distance = map.distance(position, currentPosition)
if (closestDistance === null || distance < closestDistance) {
return [position, distance]
} else {
return [closestPosition, closestDistance]
}
},
[null, null]
)

return closestPosition
}
12 changes: 5 additions & 7 deletions assets/src/components/detours/diversionPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react"
import { DiversionPanel, DiversionPanelProps } from "./diversionPanel"
import MapDisplay from "../mapPage/mapDisplay"
import { DetourMap } from "./detourMap"
import { Shape } from "../../schedule"

export const DiversionPage = ({
directions,
Expand All @@ -9,7 +10,8 @@ export const DiversionPage = ({
routeDescription,
routeDirection,
routeOrigin,
}: DiversionPanelProps) => (
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>
Expand All @@ -25,11 +27,7 @@ export const DiversionPage = ({
/>
</div>
<div className="l-diversion-page__map">
<MapDisplay
selectedEntity={null}
setSelection={() => {}}
fetchedSelectedLocation={null}
/>
<DetourMap shape={shape} />
</div>
</article>
)
54 changes: 26 additions & 28 deletions assets/src/components/mapPage/mapDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,35 +200,33 @@ const RoutePatternLayers = ({
isSelected: boolean
}): JSX.Element => {
return routePattern.shape ? (
<>
<ZoomLevelWrapper>
{(zoomLevel) => {
return (
<>
{routePattern.shape && (
<>
<RouteShape
shape={routePattern.shape}
isSelected={isSelected}
<ZoomLevelWrapper>
{(zoomLevel) => {
return (
<>
{routePattern.shape && (
<>
<RouteShape
shape={routePattern.shape}
isSelected={isSelected}
/>
<Pane
name="selectedRoutePatternStops"
pane="markerPane"
style={{ zIndex: 450 }} // should be above other non-interactive elements
>
<RouteStopMarkers
stops={routePattern.shape.stops || []}
includeStopCard={true}
zoomLevel={zoomLevel}
/>
<Pane
name="selectedRoutePatternStops"
pane="markerPane"
style={{ zIndex: 450 }} // should be above other non-interactive elements
>
<RouteStopMarkers
stops={routePattern.shape.stops || []}
includeStopCard={true}
zoomLevel={zoomLevel}
/>
</Pane>
</>
)}
</>
)
}}
</ZoomLevelWrapper>
</>
</Pane>
</>
)}
</>
)
}}
</ZoomLevelWrapper>
) : (
<></>
)
Expand Down
Loading

0 comments on commit 33fc966

Please sign in to comment.