Skip to content

Commit

Permalink
Merge pull request #1058 from opentripplanner/itin-summary-overlay
Browse files Browse the repository at this point in the history
Itinerary Summary Overlay
  • Loading branch information
miles-grant-ibigroup authored Jan 11, 2024
2 parents f289f10 + bbf4efa commit 1a70193
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 8 deletions.
10 changes: 5 additions & 5 deletions __tests__/components/viewers/__snapshots__/stop-viewer.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1437,7 +1437,7 @@ exports[`components > viewers > stop viewer should render countdown times after
title="20"
>
<span
className="sc-edoYdd cUdUnS"
className="sc-edoYdd ckpxJS"
color="333333"
style={
Object {
Expand Down Expand Up @@ -3691,7 +3691,7 @@ exports[`components > viewers > stop viewer should render countdown times for st
title="20"
>
<span
className="sc-edoYdd cUdUnS"
className="sc-edoYdd ckpxJS"
color="333333"
style={
Object {
Expand Down Expand Up @@ -5846,7 +5846,7 @@ exports[`components > viewers > stop viewer should render times after midnight w
title="20"
>
<span
className="sc-edoYdd cUdUnS"
className="sc-edoYdd ckpxJS"
color="333333"
style={
Object {
Expand Down Expand Up @@ -9169,7 +9169,7 @@ exports[`components > viewers > stop viewer should render with OTP transit index
title="36"
>
<span
className="sc-edoYdd cUdUnS"
className="sc-edoYdd ckpxJS"
color="333333"
style={
Object {
Expand Down Expand Up @@ -13547,7 +13547,7 @@ exports[`components > viewers > stop viewer should render with TriMet transit in
title="20"
>
<span
className="sc-edoYdd cUdUnS"
className="sc-edoYdd ckpxJS"
color="333333"
style={
Object {
Expand Down
2 changes: 2 additions & 0 deletions example-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@ itinerary:
showScheduleDeviation: true
# Shows the duration of a leg below the leg in the metro itinerary summary
showLegDurations: false
# Whether to show (experimental) itinerary preview overlay on itinerary results on map
previewOverlay: false
# Whether to add a OTP_RR_A11Y_ROUTING_ENABLED error to all itineraries with accessibility scores
displayA11yError: false
# The sort option to use by default
Expand Down
2 changes: 1 addition & 1 deletion lib/components/form/batch-styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const buttonTransitionCss = css`
`

export const boxShadowCss = css`
box-shadow: rgba(0, 0, 0, 0.1) 0 0 20px;
box-shadow: rgba(0, 0, 0, 0.15) 0 0 20px;
`

// TODO: this needs to be in line with the mode selector buttons, ideally importing the styles
Expand Down
2 changes: 2 additions & 0 deletions lib/components/map/default-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { updateOverlayVisibility } from '../../actions/config'
import ElevationPointMarker from './elevation-point-marker'
import EndpointsOverlay from './connected-endpoints-overlay'
import GeoJsonLayer from './connected-geojson-layer'
import ItinSummaryOverlay from './itinerary-summary-overlay'
import ParkAndRideOverlay from './connected-park-and-ride-overlay'
import PointPopup from './point-popup'
import RoutePreviewOverlay from './route-preview-overlay'
Expand Down Expand Up @@ -304,6 +305,7 @@ class DefaultMap extends Component {
zoom={zoom}
>
<PointPopup />
<ItinSummaryOverlay />
<RoutePreviewOverlay />
{/* The default overlays */}
<EndpointsOverlay />
Expand Down
241 changes: 241 additions & 0 deletions lib/components/map/itinerary-summary-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import { connect } from 'react-redux'
import { Feature, lineString, LineString, Position } from '@turf/helpers'
import { Itinerary, Location } from '@opentripplanner/types'
import { Marker } from 'react-map-gl'
import centroid from '@turf/centroid'
import distance from '@turf/distance'
import polyline from '@mapbox/polyline'
import React, { useContext, useState } from 'react'
import styled from 'styled-components'

import * as narriativeActions from '../../actions/narrative'
import { AppReduxState } from '../../util/state-types'
import { boxShadowCss } from '../form/batch-styled'
import { ComponentContext } from '../../util/contexts'
import { doMergeItineraries } from '../narrative/narrative-itineraries'
import {
getActiveItinerary,
getActiveSearch,
getVisibleItineraryIndex
} from '../../util/state'
import MetroItineraryRoutes from '../narrative/metro/metro-itinerary-routes'

type ItinWithGeometry = Itinerary & {
allLegGeometry: Feature<LineString>
allStartTimes?: Itinerary[]
index?: number
}

type Props = {
from: Location
itins: Itinerary[]
setActiveItinerary: ({ index }: { index: number | null | undefined }) => void
setVisibleItinerary: ({ index }: { index: number | null | undefined }) => void
to: Location
visible?: boolean
visibleItinerary?: number
}

const Card = styled.div`
${boxShadowCss}
background: #fffffffa;
border-radius: 5px;
padding: 6px;
align-items: center;
display: flex;
flex-wrap: wrap;
span {
span {
span {
max-height: 28px;
min-height: 20px;
}
}
}
div {
margin-top: -0px!important;
}
.route-block-wrapper span {
padding: 0px;
}
* {
height: 26px;
}
}
`

function addItinLineString(itin: Itinerary): ItinWithGeometry {
return {
...itin,
allLegGeometry: lineString(
itin.legs.flatMap((leg) => polyline.decode(leg.legGeometry.points))
)
}
}
function addTrueIndex(array: ItinWithGeometry[]): ItinWithGeometry[] {
for (let i = 0; i < array.length; i++) {
const prevIndex = array?.[i - 1]?.index
const itin = array[i]
const nextIndex = itin?.allStartTimes?.length ?? 1
array[i] = {
...itin,
index: (prevIndex ?? -1) + nextIndex
}
}
return array
}

type ItinUniquePoint = {
itin: ItinWithGeometry
uniquePoint: Position
}

function getUniquePoint(
thisItin: ItinWithGeometry,
otherPoints: ItinUniquePoint[]
): ItinUniquePoint {
const otherMidpoints = otherPoints.map((mp) => mp.uniquePoint)
let maxDistance = -Infinity
const line = thisItin.allLegGeometry
const centerOfLine = centroid(line).geometry.coordinates
let uniquePoint = centerOfLine

line.geometry.coordinates.forEach((point) => {
const totalDistance = otherMidpoints.reduce(
(prev, cur) => (prev += distance(point, cur)),
0
)

const selfDistance = distance(point, centerOfLine)
// maximize distance from all other points while minimizing distance to center of our own line
const averageDistance = totalDistance / otherMidpoints.length - selfDistance

if (averageDistance > maxDistance) {
maxDistance = averageDistance
uniquePoint = point
}
})
return { itin: thisItin, uniquePoint }
}

const ItinerarySummaryOverlay = ({
itins,
setActiveItinerary: setActive,
setVisibleItinerary: setVisible,
visible,
visibleItinerary
}: Props) => {
// @ts-expect-error React context is populated dynamically
const { LegIcon } = useContext(ComponentContext)

const [sharedTimeout, setSharedTimeout] = useState<null | NodeJS.Timeout>(
null
)

if (!itins || !visible) return <></>
const mergedItins: ItinWithGeometry[] = addTrueIndex(
doMergeItineraries(itins).mergedItineraries.map(addItinLineString)
)

const midPoints = mergedItins.reduce<ItinUniquePoint[]>((prev, curItin) => {
prev.push(getUniquePoint(curItin, prev))
return prev
}, [])
// The first point is probably not well placed, so let's run the algorithm again
if (midPoints.length > 1) {
midPoints[0] = getUniquePoint(mergedItins[0], midPoints)
}

try {
return (
<>
{midPoints.map(
(mp) =>
// If no itinerary is hovered, show all of them. If one is selected, show only that one
// TODO: clean up conditionals, move these to a more appropriate place without breaking indexing
(visibleItinerary !== null && visibleItinerary !== undefined
? visibleItinerary === mp.itin.index
: true) &&
mp.uniquePoint && (
<Marker
key={mp.itin.duration}
latitude={mp.uniquePoint[0]}
longitude={mp.uniquePoint[1]}
style={{ cursor: 'pointer' }}
>
<Card
onClick={() => {
setActive({ index: mp.itin.index })
}}
// TODO: useCallback here (getting weird errors?)
onMouseEnter={() => {
setSharedTimeout(
setTimeout(() => {
setVisible({ index: mp.itin.index })
}, 150)
)
}}
onMouseLeave={() => {
sharedTimeout && clearTimeout(sharedTimeout)
setVisible({ index: null })
}}
>
<MetroItineraryRoutes
expanded={false}
itinerary={mp.itin}
LegIcon={LegIcon}
/>
</Card>
</Marker>
)
)}
</>
)
} catch (error) {
console.warn(`Can't create geojson from route: ${error}`)
return <></>
}
}

const mapStateToProps = (state: AppReduxState) => {
const { activeSearchId, config } = state.otp
if (config.itinerary?.previewOverlay !== true) {
return {}
}
if (!activeSearchId) return {}

const visibleItinerary = getVisibleItineraryIndex(state)
const activeItinerary = getActiveItinerary(state)

const activeSearch = getActiveSearch(state)
// @ts-expect-error state is not typed
const itins = activeSearch?.response.flatMap(
(serverResponse: { plan?: { itineraries?: Itinerary[] } }) =>
serverResponse?.plan?.itineraries
)

// @ts-expect-error state is not typed
const query = activeSearch ? activeSearch?.query : state.otp.currentQuery
const { from, to } = query

return {
from,
itins,
to,
visible: activeItinerary === undefined || activeItinerary === null,
visibleItinerary
}
}

const mapDispatchToProps = {
setActiveItinerary: narriativeActions.setActiveItinerary,
setVisibleItinerary: narriativeActions.setVisibleItinerary
}

export default connect(
mapStateToProps,
mapDispatchToProps
)(ItinerarySummaryOverlay)
2 changes: 2 additions & 0 deletions lib/components/narrative/metro/default-route-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const Block = styled.span<{ color: string; isOnColoredBackground?: boolean }>`
display: inline-block;
margin-top: -2px;
padding: 3px 7px;
padding-left: 7px !important; /* TODO: this does not scale well to alternate zoom levels/text sizes */
padding-right: 7px !important;
/* Below is for route names that are too long: cut-off and show ellipsis. */
max-width: 150px;
overflow: hidden;
Expand Down
2 changes: 1 addition & 1 deletion lib/components/narrative/narrative-itineraries.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function makeStartTime(itinerary) {
}
}

const doMergeItineraries = memoize((itineraries) => {
export const doMergeItineraries = memoize((itineraries) => {
const mergedItineraries = itineraries
.reduce((prev, cur) => {
const updatedItineraries = clone(prev)
Expand Down
1 change: 1 addition & 0 deletions lib/util/config-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ export interface ItineraryConfig {
mergeItineraries?: boolean
mutedErrors?: string[]
onlyShowCountdownForRealtime?: boolean
previewOverlay?: boolean
renderRouteNamesInBlocks?: boolean
showFirstResultByDefault?: boolean
showHeaderText?: boolean
Expand Down
3 changes: 2 additions & 1 deletion lib/util/state-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import {
import { AppConfig } from './config-types'

export interface OtpState {
// TODO: Add other OTP states
activeSearchId?: string
config: AppConfig
filter: {
sort: {
type: string
}
}
// TODO: Add other OTP states
ui: any // TODO
}

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
"@opentripplanner/vehicle-rental-overlay": "^2.1.3",
"@styled-icons/fa-regular": "^10.34.0",
"@styled-icons/fa-solid": "^10.34.0",
"@turf/centroid": "^6.5.0",
"@turf/helpers": "^6.5.0",
"blob-stream": "^0.1.3",
"bootstrap": "^3.3.7",
"bowser": "^1.9.3",
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3326,6 +3326,14 @@
"@turf/helpers" "^6.5.0"
"@turf/invariant" "^6.5.0"

"@turf/centroid@^6.5.0":
version "6.5.0"
resolved "https://registry.yarnpkg.com/@turf/centroid/-/centroid-6.5.0.tgz#ecaa365412e5a4d595bb448e7dcdacfb49eb0009"
integrity sha512-MwE1oq5E3isewPprEClbfU5pXljIK/GUOMbn22UM3IFPDJX0KeoyLNwghszkdmFp/qMGL/M13MMWvU+GNLXP/A==
dependencies:
"@turf/helpers" "^6.5.0"
"@turf/meta" "^6.5.0"

"@turf/circle@^6.5.0":
version "6.5.0"
resolved "https://registry.yarnpkg.com/@turf/circle/-/circle-6.5.0.tgz#dc017d8c0131d1d212b7c06f76510c22bbeb093c"
Expand Down

0 comments on commit 1a70193

Please sign in to comment.