diff --git a/__tests__/components/viewers/__snapshots__/nearby-view.js.snap b/__tests__/components/viewers/__snapshots__/nearby-view.js.snap
index 65c9368a3..1a5af6b06 100644
--- a/__tests__/components/viewers/__snapshots__/nearby-view.js.snap
+++ b/__tests__/components/viewers/__snapshots__/nearby-view.js.snap
@@ -57,38 +57,47 @@ exports[`components > viewers > nearby view renders nothing on a blank page 1`]
title="components.NearbyView.header"
/>
-
-
- components.NearbyView.nearbyListIntro
-
-
+
+ components.NearbyView.nearbyListIntro
+
+
+
@@ -4091,38 +4100,47 @@ exports[`components > viewers > nearby view renders proper scooter dates 1`] = `
title="components.NearbyView.header"
/>
-
-
- components.NearbyView.nearbyListIntro
-
-
+
+ components.NearbyView.nearbyListIntro
+
+
+
diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js
index ee4c3c684..c2e11360c 100644
--- a/lib/components/app/responsive-webapp.js
+++ b/lib/components/app/responsive-webapp.js
@@ -55,6 +55,24 @@ class ResponsiveWebapp extends Component {
/** Lifecycle methods **/
+ // Check if the position has changed enough to update the currentPosition
+ // (prevent constant updates in nearby view)
+ // .001 works out to be about 94-300 ft depending on the longitude.
+ positionShouldUpdate = (position) => {
+ const { currentPosition } = this.props
+ if (!currentPosition.coords) return true
+ const latChanged =
+ Math.abs(
+ position?.coords?.latitude - currentPosition?.coords?.latitude
+ ) >= 0.001
+ const lonChanged =
+ Math.abs(
+ position?.coords?.longitude - currentPosition?.coords?.longitude
+ ) >= 0.001
+
+ return latChanged || lonChanged
+ }
+
/* eslint-disable-next-line complexity */
componentDidUpdate(prevProps) {
const {
@@ -69,6 +87,7 @@ class ResponsiveWebapp extends Component {
map,
matchContentToUrl,
query,
+ receivedPositionResponse,
setLocationToCurrent,
setMapCenter
} = this.props
@@ -119,6 +138,25 @@ class ResponsiveWebapp extends Component {
}
}
+ // Watch for position changing on mobile
+ if (isMobile()) {
+ navigator.geolocation.watchPosition(
+ // On success
+ (position) => {
+ const shouldUpdate = this.positionShouldUpdate(position)
+ if (shouldUpdate) {
+ receivedPositionResponse({ position })
+ }
+ },
+ // On error
+ (error) => {
+ console.log('error in watchPosition', error)
+ },
+ // Options
+ { enableHighAccuracy: true }
+ )
+ }
+
// If the path changes (e.g., via a back button press) check whether the
// main content needs to switch between, for example, a viewer and a search.
if (!isEqual(location.pathname, prevProps.location.pathname)) {
@@ -154,7 +192,6 @@ class ResponsiveWebapp extends Component {
map,
matchContentToUrl,
parseUrlQueryString,
- receivedPositionResponse,
setNetworkConnectionLost
} = this.props
// Add on back button press behavior.
@@ -179,19 +216,6 @@ class ResponsiveWebapp extends Component {
if (isMobile()) {
// Test location availability on load
getCurrentPosition(intl)
- // Also, watch for changes in position on mobile
- navigator.geolocation.watchPosition(
- // On success
- (position) => {
- receivedPositionResponse({ position })
- },
- // On error
- (error) => {
- console.log('error in watchPosition', error)
- },
- // Options
- { enableHighAccuracy: true }
- )
}
// Handle routing to a specific part of the app (e.g. stop viewer) on page
// load. (This happens prior to routing request in case special routerId is
@@ -426,10 +450,12 @@ class RouterWrapperWithAuth0 extends Component {
}
const mapStateToWrapperProps = (state) => {
- const { homeTimezone, map, persistence, reactRouter } = state.otp.config
+ const { homeTimezone, location, map, persistence, reactRouter } =
+ state.otp.config
return {
auth0Config: getAuth0Config(persistence),
autoFly: map.autoFlyOnTripFormUpdate,
+ currentPosition: location?.currentPosition,
defaultLocale: getDefaultLocale(state.otp.config, state.user.loggedInUser),
homeTimezone,
locale: state.otp.ui.locale,
diff --git a/lib/components/form/call-taker/date-time-picker.tsx b/lib/components/form/call-taker/date-time-picker.tsx
index 96292df86..47ae53ba7 100644
--- a/lib/components/form/call-taker/date-time-picker.tsx
+++ b/lib/components/form/call-taker/date-time-picker.tsx
@@ -5,7 +5,13 @@ import { IntlShape, useIntl } from 'react-intl'
import { isMatch, parse } from 'date-fns'
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import coreUtils from '@opentripplanner/core-utils'
-import React, { useEffect, useRef, useState } from 'react'
+import React, {
+ ChangeEvent,
+ useCallback,
+ useEffect,
+ useRef,
+ useState
+} from 'react'
import { AppReduxState, FilterType, SortType } from '../../../util/state-types'
import { DepartArriveTypeMap, DepartArriveValue } from '../date-time-modal'
@@ -69,6 +75,30 @@ const safeFormat = (date: Date | '', time: string, options?: OptionsWithTZ) => {
}
return ''
}
+/**
+ * Parse a time input expressed in the agency time zone.
+ * @returns A date if the parsing succeeded, or null.
+ */
+const parseInputAsTime = (
+ homeTimezone: string,
+ timeInput: string = getCurrentTime(homeTimezone),
+ date: string = getCurrentDate(homeTimezone)
+) => {
+ if (!timeInput) timeInput = getCurrentTime(homeTimezone)
+
+ // Match one of the supported time formats
+ const matchedTimeFormat = SUPPORTED_TIME_FORMATS.find((timeFormat) =>
+ isMatch(timeInput, timeFormat)
+ )
+ if (matchedTimeFormat) {
+ const resolvedDateTime = format(
+ parse(timeInput, matchedTimeFormat, new Date()),
+ 'HH:mm:ss'
+ )
+ return toDate(`${date}T${resolvedDateTime}`)
+ }
+ return ''
+}
type Props = {
date?: string
@@ -121,69 +151,36 @@ const DateTimeOptions = ({
)
const [date, setDate] = useState(initialDate)
const [time, setTime] = useState(initialTime)
- const [typedTime, setTypedTime] = useState(initialTime)
+ const [typedTime, setTypedTime] = useState(
+ safeFormat(parseInputAsTime(homeTimezone, time, date), timeFormat, {
+ timeZone: homeTimezone
+ })
+ )
const timeRef = useRef(null)
const intl = useIntl()
- /**
- * Parse a time input expressed in the agency time zone.
- * @returns A date if the parsing succeeded, or null.
- */
- const parseInputAsTime = (
- timeInput: string = getCurrentTime(homeTimezone),
- date: string = getCurrentDate(homeTimezone)
- ) => {
- if (!timeInput) timeInput = getCurrentTime(homeTimezone)
-
- // Match one of the supported time formats
- const matchedTimeFormat = SUPPORTED_TIME_FORMATS.find((timeFormat) =>
- isMatch(timeInput, timeFormat)
- )
- if (matchedTimeFormat) {
- const resolvedDateTime = format(
- parse(timeInput, matchedTimeFormat, new Date()),
- 'HH:mm:ss'
- )
- return toDate(`${date}T${resolvedDateTime}`)
- }
- return ''
- }
-
- const dateTime = parseInputAsTime(time, date)
+ const dateTime = parseInputAsTime(homeTimezone, time, date)
// Update state when external state is updated
useEffect(() => {
if (initialDate !== date) setDate(initialDate)
if (initialTime !== time) {
- setTime(initialTime)
+ handleTimeChange(initialTime || '')
}
+ // This effect is design to flow from state to component only
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialTime, initialDate])
- useEffect(() => {
- // Don't update if still typing
- if (timeRef.current !== document.activeElement) {
- setTypedTime(
- safeFormat(dateTime, timeFormat, {
- timeZone: homeTimezone
- }) ||
- // TODO: there doesn't seem to be an intl object present?
- 'Invalid Time'
- )
- }
- }, [time])
-
useEffect(() => {
if (initialDepartArrive && departArrive !== initialDepartArrive) {
setDepartArrive(initialDepartArrive)
}
+ // This effect is design to flow from state to component only
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialDepartArrive])
- useEffect(() => {
- if (departArrive === 'NOW') setTypedTime('')
- }, [departArrive])
-
// Handler for setting the query parameters
useEffect(() => {
if (safeFormat(dateTime, OTP_API_DATE_FORMAT, {}) !== '' && setQueryParam) {
@@ -197,42 +194,64 @@ const DateTimeOptions = ({
})
})
}
-
- if (
- syncSortWithDepartArrive &&
- DepartArriveTypeMap[departArrive] !== sort.type
- ) {
- importedUpdateItineraryFilter({
- sort: {
- ...sort,
- type: DepartArriveTypeMap[departArrive]
- }
- })
- }
}, [dateTime, departArrive, homeTimezone, setQueryParam])
- // Handler for updating the time and date fields when NOW is selected
- useEffect(() => {
- if (departArrive === 'NOW') {
- setTime(getCurrentTime(homeTimezone))
- setDate(getCurrentDate(homeTimezone))
- setTypedTime(
- safeFormat(dateTime, timeFormat, {
- timeZone: homeTimezone
+ const handleDepartArriveChange = useCallback(
+ (e: ChangeEvent) => {
+ const newValue = e.target.value as DepartArriveValue
+ setDepartArrive(newValue)
+
+ // Handler for updating the time and date fields when NOW is selected
+ if (newValue === 'NOW') {
+ handleTimeChange(getCurrentTime(homeTimezone))
+ setDate(getCurrentDate(homeTimezone))
+ setTypedTime(
+ safeFormat(dateTime, timeFormat, {
+ timeZone: homeTimezone
+ })
+ )
+ }
+
+ // Update sort type if needed
+ if (
+ syncSortWithDepartArrive &&
+ DepartArriveTypeMap[newValue] !== sort.type
+ ) {
+ importedUpdateItineraryFilter({
+ sort: {
+ ...sort,
+ type: DepartArriveTypeMap[newValue]
+ }
})
- )
- }
- }, [departArrive, setTime, setDate, homeTimezone])
+ }
+ },
+ [syncSortWithDepartArrive, sort, importedUpdateItineraryFilter]
+ )
- const unsetNow = () => {
+ const unsetNow = useCallback(() => {
if (departArrive === 'NOW') setDepartArrive('DEPART')
- }
+ }, [departArrive])
+
+ const handleTimeChange = useCallback(
+ (newTime: string) => {
+ setTime(newTime)
+ // Only update typed time if not actively typing
+ if (timeRef.current !== document.activeElement) {
+ setTypedTime(
+ safeFormat(dateTime, timeFormat, {
+ timeZone: homeTimezone
+ }) || 'Invalid Time'
+ )
+ }
+ },
+ [dateTime, timeFormat, homeTimezone]
+ )
return (
<>