diff --git a/__tests__/util/__mocks__/itinerary.json b/__tests__/util/__mocks__/itinerary.json deleted file mode 100644 index c00a605a4..000000000 --- a/__tests__/util/__mocks__/itinerary.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "route1": { - "longName": "Across town", - "mode": "BUS", - "shortName": "10", - "sortOrder": 10 - }, - "route2": { - "longName": "Around town", - "mode": "BUS", - "shortName": "20", - "sortOrder": 2 - }, - "route3": { - "longName": "Around another town", - "shortName": "3", - "sortOrder": -999, - "type": 3 - }, - "route4": { - "longName": "Loop route", - "mode": "BUS", - "shortName": "2", - "sortOrder": -999 - }, - "route5": { - "longName": "A-line", - "mode": "BUS", - "shortName": "A", - "sortOrder": -999 - }, - "route6": { - "longName": "B-line", - "mode": "BUS", - "shortName": "B", - "sortOrder": -999 - }, - "route7": { - "longName": "A meandering route", - "mode": "BUS", - "sortOrder": -999 - }, - "route8": { - "longName": "Zig-zagging route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2 - }, - "route9": { - "longName": "Express route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2 - }, - "route10": { - "longName": "Variation of express route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2 - }, - "route11": { - "longName": "Local route", - "mode": "BUS", - "shortName": "6", - "sortOrder": 2 - }, - "route12": { - "longName": "Intercity Train", - "mode": "RAIL", - "shortName": "IC", - "sortOrder": 2 - }, - "route13": { - "longName": "Yellow line Subway", - "mode": "SUBWAY", - "shortName": "Yellow", - "sortOrder": 2 - }, - "route14": { - "longName": "Xpress route C", - "mode": "BUS", - "shortName": "30C", - "sortOrder": 2 - }, - "route15": { - "longName": "Express route X", - "mode": "BUS", - "shortName": "30X", - "sortOrder": 2 - } -} diff --git a/__tests__/util/__snapshots__/itinerary.js.snap b/__tests__/util/__snapshots__/itinerary.js.snap deleted file mode 100644 index 6887d0868..000000000 --- a/__tests__/util/__snapshots__/itinerary.js.snap +++ /dev/null @@ -1,247 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`util > itinerary routeComparator should prioritize routes with integer shortNames over alphabetic shortNames 1`] = ` -Array [ - Object { - "longName": "A-line", - "mode": "BUS", - "shortName": "A", - "sortOrder": -999, - }, - Object { - "longName": "Loop route", - "mode": "BUS", - "shortName": "2", - "sortOrder": -999, - }, -] -`; - -exports[`util > itinerary routeComparator should prioritize routes with shortNames over those with just longNames 1`] = ` -Array [ - Object { - "longName": "B-line", - "mode": "BUS", - "shortName": "B", - "sortOrder": -999, - }, - Object { - "longName": "A meandering route", - "mode": "BUS", - "sortOrder": -999, - }, -] -`; - -exports[`util > itinerary routeComparator should prioritize routes with valid sortOrder 1`] = ` -Array [ - Object { - "longName": "Around town", - "mode": "BUS", - "shortName": "20", - "sortOrder": 2, - }, - Object { - "longName": "Around another town", - "shortName": "3", - "sortOrder": -999, - "type": 3, - }, -] -`; - -exports[`util > itinerary routeComparator should sort based off of route type 1`] = ` -Array [ - Object { - "longName": "Yellow line Subway", - "mode": "SUBWAY", - "shortName": "Yellow", - "sortOrder": 2, - }, - Object { - "longName": "Intercity Train", - "mode": "RAIL", - "shortName": "IC", - "sortOrder": 2, - }, -] -`; - -exports[`util > itinerary routeComparator should sort routes based off of integer shortName 1`] = ` -Array [ - Object { - "longName": "Loop route", - "mode": "BUS", - "shortName": "2", - "sortOrder": -999, - }, - Object { - "longName": "Around another town", - "shortName": "3", - "sortOrder": -999, - "type": 3, - }, -] -`; - -exports[`util > itinerary routeComparator should sort routes based off of longNames 1`] = ` -Array [ - Object { - "longName": "Express route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2, - }, - Object { - "longName": "Variation of express route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2, - }, -] -`; - -exports[`util > itinerary routeComparator should sort routes based off of shortNames 1`] = ` -Array [ - Object { - "longName": "A-line", - "mode": "BUS", - "shortName": "A", - "sortOrder": -999, - }, - Object { - "longName": "B-line", - "mode": "BUS", - "shortName": "B", - "sortOrder": -999, - }, -] -`; - -exports[`util > itinerary routeComparator should sort routes based off of sortOrder 1`] = ` -Array [ - Object { - "longName": "Around town", - "mode": "BUS", - "shortName": "20", - "sortOrder": 2, - }, - Object { - "longName": "Across town", - "mode": "BUS", - "shortName": "10", - "sortOrder": 10, - }, -] -`; - -exports[`util > itinerary routeComparator should sort routes on all of the criteria at once 1`] = ` -Array [ - Object { - "longName": "Yellow line Subway", - "mode": "SUBWAY", - "shortName": "Yellow", - "sortOrder": 2, - }, - Object { - "longName": "Intercity Train", - "mode": "RAIL", - "shortName": "IC", - "sortOrder": 2, - }, - Object { - "longName": "Local route", - "mode": "BUS", - "shortName": "6", - "sortOrder": 2, - }, - Object { - "longName": "Around town", - "mode": "BUS", - "shortName": "20", - "sortOrder": 2, - }, - Object { - "longName": "Express route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2, - }, - Object { - "longName": "Variation of express route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2, - }, - Object { - "longName": "Zig-zagging route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2, - }, - Object { - "longName": "Xpress route C", - "mode": "BUS", - "shortName": "30C", - "sortOrder": 2, - }, - Object { - "longName": "Express route X", - "mode": "BUS", - "shortName": "30X", - "sortOrder": 2, - }, - Object { - "longName": "Across town", - "mode": "BUS", - "shortName": "10", - "sortOrder": 10, - }, - Object { - "longName": "A-line", - "mode": "BUS", - "shortName": "A", - "sortOrder": -999, - }, - Object { - "longName": "B-line", - "mode": "BUS", - "shortName": "B", - "sortOrder": -999, - }, - Object { - "longName": "Loop route", - "mode": "BUS", - "shortName": "2", - "sortOrder": -999, - }, - Object { - "longName": "Around another town", - "shortName": "3", - "sortOrder": -999, - "type": 3, - }, - Object { - "longName": "A meandering route", - "mode": "BUS", - "sortOrder": -999, - }, -] -`; - -exports[`util > itinerary routeComparator should sort routes with alphanumeric shortNames 1`] = ` -Array [ - Object { - "longName": "Xpress route C", - "mode": "BUS", - "shortName": "30C", - "sortOrder": 2, - }, - Object { - "longName": "Express route X", - "mode": "BUS", - "shortName": "30X", - "sortOrder": 2, - }, -] -`; diff --git a/__tests__/util/itinerary.js b/__tests__/util/itinerary.js deleted file mode 100644 index 74e5d5a56..000000000 --- a/__tests__/util/itinerary.js +++ /dev/null @@ -1,88 +0,0 @@ -import {isTransit, routeComparator} from '../../lib/util/itinerary' - -const { - route1, - route2, - route3, - route4, - route5, - route6, - route7, - route8, - route9, - route10, - route11, - route12, - route13, - route14, - route15 -} = require('./__mocks__/itinerary.json') - -function sortRoutes (...routes) { - routes.sort(routeComparator) - return routes -} - -describe('util > itinerary', () => { - it('isTransit should work', () => { - expect(isTransit('CAR')).toBeFalsy() - }) - - describe('routeComparator', () => { - it('should sort routes based off of sortOrder', () => { - expect(sortRoutes(route1, route2)).toMatchSnapshot() - }) - - it('should prioritize routes with valid sortOrder', () => { - expect(sortRoutes(route2, route3)).toMatchSnapshot() - }) - - it('should sort routes based off of integer shortName', () => { - expect(sortRoutes(route3, route4)).toMatchSnapshot() - }) - - it('should prioritize routes with integer shortNames over alphabetic shortNames', () => { - expect(sortRoutes(route4, route5)).toMatchSnapshot() - }) - - it('should sort routes based off of shortNames', () => { - expect(sortRoutes(route5, route6)).toMatchSnapshot() - }) - - it('should sort routes with alphanumeric shortNames', () => { - expect(sortRoutes(route14, route15)).toMatchSnapshot() - }) - - it('should prioritize routes with shortNames over those with just longNames', () => { - expect(sortRoutes(route6, route7)).toMatchSnapshot() - }) - - it('should sort routes based off of longNames', () => { - expect(sortRoutes(route9, route10)).toMatchSnapshot() - }) - - it('should sort routes on all of the criteria at once', () => { - expect(sortRoutes( - route1, - route2, - route3, - route4, - route5, - route6, - route7, - route8, - route9, - route10, - route11, - route12, - route13, - route14, - route15 - )).toMatchSnapshot() - }) - - it('should sort based off of route type', () => { - expect(sortRoutes(route12, route13)).toMatchSnapshot() - }) - }) -}) diff --git a/lib/actions/api.js b/lib/actions/api.js index 705822ecb..988e9900e 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -1,20 +1,22 @@ /* globals fetch */ import { push, replace } from 'connected-react-router' +import haversine from 'haversine' +import moment from 'moment' import hash from 'object-hash' +import coreUtils from '@opentripplanner/core-utils' +import queryParams from '@opentripplanner/core-utils/lib/query-params' import { createAction } from 'redux-actions' import qs from 'qs' -import moment from 'moment' -import haversine from 'haversine' import { rememberPlace } from './map' -import { hasCar } from '../util/itinerary' -import { getTripOptionsFromQuery, getUrlParams } from '../util/query' -import queryParams from '../util/query-params' import { getStopViewerConfig, queryIsValid } from '../util/state' -import { randId } from '../util/storage' -import { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } from '../util/time' if (typeof (fetch) === 'undefined') require('isomorphic-fetch') +const { hasCar } = coreUtils.itinerary +const { getTripOptionsFromQuery, getUrlParams } = coreUtils.query +const { randId } = coreUtils.storage +const { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } = coreUtils.time + // Generic API actions export const nonRealtimeRoutingResponse = createAction('NON_REALTIME_ROUTING_RESPONSE') diff --git a/lib/actions/form.js b/lib/actions/form.js index 66860f1a4..abaa4ff00 100644 --- a/lib/actions/form.js +++ b/lib/actions/form.js @@ -1,18 +1,10 @@ import debounce from 'lodash.debounce' +import isEqual from 'lodash.isequal' import moment from 'moment' +import coreUtils from '@opentripplanner/core-utils' import { createAction } from 'redux-actions' -import isEqual from 'lodash.isequal' -import { - getDefaultQuery, - getTripOptionsFromQuery, - getUrlParams, - planParamsToQuery -} from '../util/query' -import { getItem, randId } from '../util/storage' import { queryIsValid } from '../util/state' -import { OTP_API_TIME_FORMAT } from '../util/time' -import { isMobile } from '../util/ui' import { MobileScreens, setMainPanelContent, @@ -21,6 +13,13 @@ import { import { routingQuery } from './api' +const { + getDefaultQuery, + getTripOptionsFromQuery, + getUrlParams, + planParamsToQuery +} = coreUtils.query + export const settingQueryParam = createAction('SET_QUERY_PARAM') export const clearActiveSearch = createAction('CLEAR_ACTIVE_SEARCH') export const setActiveSearch = createAction('SET_ACTIVE_SEARCH') @@ -35,7 +34,7 @@ export function resetForm () { dispatch(settingQueryParam(otpState.user.defaults)) } else { // Get user overrides and apply to default query - const userOverrides = getItem('defaultQuery', {}) + const userOverrides = coreUtils.storage.getItem('defaultQuery', {}) const defaultQuery = Object.assign( getDefaultQuery(otpState.config), userOverrides @@ -69,7 +68,7 @@ export function parseUrlQueryString (params = getUrlParams()) { Object.keys(params).forEach(key => { if (!key.startsWith('ui_')) planParams[key] = params[key] }) - const searchId = params.ui_activeSearch || randId() + const searchId = params.ui_activeSearch || coreUtils.storage.randId() // Convert strings to numbers/objects and dispatch dispatch( setQueryParam( @@ -89,10 +88,11 @@ let lastDebouncePlanTimeMs export function formChanged (oldQuery, newQuery) { return function (dispatch, getState) { const otpState = getState().otp + const isMobile = coreUtils.ui.isMobile() // If departArrive is set to 'NOW', update the query time to current if (otpState.currentQuery && otpState.currentQuery.departArrive === 'NOW') { - dispatch(settingQueryParam({ time: moment().format(OTP_API_TIME_FORMAT) })) + dispatch(settingQueryParam({ time: moment().format(coreUtils.time.OTP_API_TIME_FORMAT) })) } // Determine if either from/to location has changed @@ -111,7 +111,7 @@ export function formChanged (oldQuery, newQuery) { // either location changes only if not currently on welcome screen (otherwise // when the current position is auto-set the screen will change unexpectedly). if ( - isMobile() && + isMobile && (fromChanged || toChanged) && otpState.ui.mobileScreen !== MobileScreens.WELCOME_SCREEN ) { @@ -123,8 +123,8 @@ export function formChanged (oldQuery, newQuery) { const { autoPlan, debouncePlanTimeMs } = otpState.config const updatePlan = autoPlan || - (!isMobile() && oneLocationChanged) || // TODO: make autoplan configurable at the parameter level? - (isMobile() && fromChanged && toChanged) + (!isMobile && oneLocationChanged) || // TODO: make autoplan configurable at the parameter level? + (isMobile && fromChanged && toChanged) if (updatePlan && queryIsValid(otpState)) { // trip plan should be made // check if debouncing function needs to be (re)created if (!debouncedPlanTrip || lastDebouncePlanTimeMs !== debouncePlanTimeMs) { diff --git a/lib/actions/map.js b/lib/actions/map.js index fce9deb91..f3ec18447 100644 --- a/lib/actions/map.js +++ b/lib/actions/map.js @@ -1,9 +1,9 @@ +import coreUtils from '@opentripplanner/core-utils' import getGeocoder from '@opentripplanner/geocoder' import { createAction } from 'redux-actions' import { routingQuery } from './api' import { clearActiveSearch } from './form' -import { constructLocation } from '../util/map' /* SET_LOCATION action creator. Updates a from or to location in the store * @@ -116,7 +116,7 @@ export const setElevationPoint = createAction('SET_ELEVATION_POINT') export const setMapPopupLocation = createAction('SET_MAP_POPUP_LOCATION') export function setMapPopupLocationAndGeocode (mapEvent) { - const location = constructLocation(mapEvent.latlng) + const location = coreUtils.map.constructLocation(mapEvent.latlng) return function (dispatch, getState) { dispatch(setMapPopupLocation({ location })) getGeocoder(getState().otp.config.geocoder) diff --git a/lib/actions/narrative.js b/lib/actions/narrative.js index 8f016a400..56cc5279c 100644 --- a/lib/actions/narrative.js +++ b/lib/actions/narrative.js @@ -1,14 +1,14 @@ +import coreUtils from '@opentripplanner/core-utils' import { createAction } from 'redux-actions' import { setUrlSearch } from './api' -import { getUrlParams } from '../util/query' export function setActiveItinerary (payload) { return function (dispatch, getState) { // Trigger change in store. dispatch(settingActiveitinerary(payload)) // Update URL params. - const urlParams = getUrlParams() + const urlParams = coreUtils.query.getUrlParams() urlParams.ui_activeItinerary = payload.index dispatch(setUrlSearch(urlParams)) } diff --git a/lib/actions/ui.js b/lib/actions/ui.js index ad04d2805..9dc8decfe 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -1,13 +1,14 @@ +import { push } from 'connected-react-router' +import coreUtils from '@opentripplanner/core-utils' import { createAction } from 'redux-actions' import { matchPath } from 'react-router' -import { push } from 'connected-react-router' import { findRoute } from './api' import { setMapCenter, setMapZoom, setRouterId } from './config' import { clearActiveSearch, parseUrlQueryString, setActiveSearch } from './form' import { clearLocation } from './map' import { setActiveItinerary } from './narrative' -import { getUiUrlParams, getUrlParams } from '../util/query' +import { getUiUrlParams } from '../util/state' /** * Wrapper function for history#push that preserves the current search or, if @@ -103,7 +104,7 @@ export function handleBackButtonPress (e) { const uiUrlParams = getUiUrlParams(otpState) // Get new search ID from URL after back button pressed. // console.log('back button pressed', e) - const urlParams = getUrlParams() + const urlParams = coreUtils.query.getUrlParams() const previousSearchId = urlParams.ui_activeSearch const previousItinIndex = +urlParams.ui_activeItinerary || 0 const previousSearch = otpState.searches[previousSearchId] diff --git a/lib/components/app/print-layout.js b/lib/components/app/print-layout.js index 2fda1815f..724db3ee3 100644 --- a/lib/components/app/print-layout.js +++ b/lib/components/app/print-layout.js @@ -1,4 +1,4 @@ -import TriMetLegIcon from '@opentripplanner/icons/lib/trimet-leg-icon' +import { TriMetLegIcon } from '@opentripplanner/icons' import PrintableItinerary from '@opentripplanner/printable-itinerary' import PropTypes from 'prop-types' import React, { Component } from 'react' diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 284200975..eea5f49a7 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -1,20 +1,21 @@ -import React, { Component } from 'react' +import { ConnectedRouter } from 'connected-react-router' +import { createHashHistory } from 'history' +import isEqual from 'lodash.isequal' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { connect } from 'react-redux' -import isEqual from 'lodash.isequal' import { Route, Switch, withRouter } from 'react-router' -import { createHashHistory } from 'history' -import { ConnectedRouter } from 'connected-react-router' import PrintLayout from './print-layout' import { setMapCenter, setMapZoom } from '../../actions/config' -import { setLocationToCurrent } from '../../actions/map' -import { getCurrentPosition, receivedPositionResponse } from '../../actions/location' import { formChanged, parseUrlQueryString } from '../../actions/form' +import { getCurrentPosition, receivedPositionResponse } from '../../actions/location' +import { setLocationToCurrent } from '../../actions/map' import { handleBackButtonPress, matchContentToUrl } from '../../actions/ui' -import { getUrlParams } from '../../util/query' -import { getTitle, isMobile } from '../../util/ui' -import { getActiveItinerary } from '../../util/state' +import { getActiveItinerary, getTitle } from '../../util/state' + +const { isMobile } = coreUtils.ui class ResponsiveWebapp extends Component { static propTypes = { @@ -29,7 +30,7 @@ class ResponsiveWebapp extends Component { componentDidUpdate (prevProps) { const { currentPosition, location, query, title } = this.props document.title = title - const urlParams = getUrlParams() + const urlParams = coreUtils.query.getUrlParams() const newSearchId = urlParams.ui_activeSearch // Determine if trip is being replanned by checking the active search ID // against the ID found in the URL params. If they are different, a new one diff --git a/lib/components/form/date-time-modal.js b/lib/components/form/date-time-modal.js index 6b54176c4..0d934e8e5 100644 --- a/lib/components/form/date-time-modal.js +++ b/lib/components/form/date-time-modal.js @@ -1,8 +1,7 @@ -// import necessary React/Redux libraries -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { connect } from 'react-redux' -import { getTimeFormat, getDateFormat } from '@opentripplanner/core-utils/lib/time' import { setQueryParam } from '../../actions/form' @@ -41,14 +40,15 @@ class DateTimeModal extends Component { const mapStateToProps = (state, ownProps) => { const { departArrive, date, time } = state.otp.currentQuery + const config = state.otp.config return { - config: state.otp.config, + config, departArrive, date, time, // These props below are for legacy browsers (see render method above). - timeFormatLegacy: getTimeFormat(state.otp.config), - dateFormatLegacy: getDateFormat(state.otp.config) + timeFormatLegacy: coreUtils.time.getTimeFormat(config), + dateFormatLegacy: coreUtils.time.getDateFormat(config) } } diff --git a/lib/components/form/date-time-preview.js b/lib/components/form/date-time-preview.js index 08ce1d25b..846c8b974 100644 --- a/lib/components/form/date-time-preview.js +++ b/lib/components/form/date-time-preview.js @@ -1,15 +1,16 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' import moment from 'moment' -import { connect } from 'react-redux' +import coreUtils from '@opentripplanner/core-utils' +import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' +import { connect } from 'react-redux' -import { +const { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT, getTimeFormat, getDateFormat -} from '../../util/time' +} = coreUtils.time class DateTimePreview extends Component { static propTypes = { @@ -82,16 +83,17 @@ class DateTimePreview extends Component { const mapStateToProps = (state, ownProps) => { const { departArrive, date, time, routingType, startTime, endTime } = state.otp.currentQuery + const config = state.otp.config return { - config: state.otp.config, + config, routingType, departArrive, date, time, startTime, endTime, - timeFormat: getTimeFormat(state.otp.config), - dateFormat: getDateFormat(state.otp.config) + timeFormat: getTimeFormat(config), + dateFormat: getDateFormat(config) } } diff --git a/lib/components/form/default-search-form.js b/lib/components/form/default-search-form.js index 1f2578231..98be08f8a 100644 --- a/lib/components/form/default-search-form.js +++ b/lib/components/form/default-search-form.js @@ -1,9 +1,9 @@ -import React, { Component } from 'react' import PropTypes from 'prop-types' +import React, { Component } from 'react' import LocationField from './connected-location-field' -import SwitchButton from './switch-button' import TabbedFormPanel from './tabbed-form-panel' +import SwitchButton from './switch-button' import defaultIcons from '../icons' export default class DefaultSearchForm extends Component { diff --git a/lib/components/form/plan-trip-button.js b/lib/components/form/plan-trip-button.js index b619e647b..e2a091d0c 100644 --- a/lib/components/form/plan-trip-button.js +++ b/lib/components/form/plan-trip-button.js @@ -1,11 +1,11 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' import { routingQuery } from '../../actions/api' import { setMainPanelContent } from '../../actions/ui' -import { isMobile } from '../../util/ui' class PlanTripButton extends Component { static propTypes = { @@ -23,7 +23,7 @@ class PlanTripButton extends Component { _onClick = () => { this.props.routingQuery() if (typeof this.props.onClick === 'function') this.props.onClick() - if (!isMobile()) this.props.setMainPanelContent(null) + if (!coreUtils.ui.isMobile()) this.props.setMainPanelContent(null) } render () { diff --git a/lib/components/form/settings-preview.js b/lib/components/form/settings-preview.js index 1cefa4d98..337c5d2b6 100644 --- a/lib/components/form/settings-preview.js +++ b/lib/components/form/settings-preview.js @@ -1,10 +1,9 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import { isNotDefaultQuery } from '../../util/query' - class SettingsPreview extends Component { static propTypes = { // component props @@ -28,7 +27,7 @@ class SettingsPreview extends Component { render () { const { config, query, caret, editButtonText } = this.props // Show dot indicator if the current query differs from the default query. - let showDot = isNotDefaultQuery(query, config) + let showDot = coreUtils.query.isNotDefaultQuery(query, config) const button = (
@@ -62,7 +61,7 @@ export default class AccessLeg extends Component { className={`step ${stepIsActive ? 'active' : ''}`} onClick={(e) => this._onStepClick(e, step, stepIndex)}> {humanizeDistanceString(step.distance)} - {getStepInstructions(step)} + {coreUtils.itinerary.getStepInstructions(step)} ) })} diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 57f2717de..efc740c3a 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -1,3 +1,4 @@ +import coreUtils from '@opentripplanner/core-utils' import React from 'react' import NarrativeItinerary from '../narrative-itinerary' @@ -5,7 +6,8 @@ import ItinerarySummary from './itinerary-summary' import ItineraryDetails from './itinerary-details' import TripDetails from '../connected-trip-details' import TripTools from '../trip-tools' -import { formatDuration, formatTime } from '../../../util/time' + +const { formatDuration, formatTime } = coreUtils.time export default class DefaultItinerary extends NarrativeItinerary { render () { diff --git a/lib/components/narrative/default/itinerary-details.js b/lib/components/narrative/default/itinerary-details.js index 2eec48805..91537b8b6 100644 --- a/lib/components/narrative/default/itinerary-details.js +++ b/lib/components/narrative/default/itinerary-details.js @@ -1,9 +1,9 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import AccessLeg from './access-leg' import TransitLeg from './transit-leg' -import { isTransit } from '../../../util/itinerary' export default class ItineraryDetails extends Component { static propTypes = { @@ -16,7 +16,7 @@ export default class ItineraryDetails extends Component {
{itinerary.legs.map((leg, index) => { const legIsActive = activeLeg === index - return isTransit(leg.mode) + return coreUtils.itinerary.isTransit(leg.mode) ? if (!itineraries) return null - let views = [] - if (showProfileSummary) { - views.push(
-
Your Best Options (Swipe to View All)
- -
) - } - views = views.concat(itineraries.map((itinerary, index) => { + const views = itineraries.map((itinerary, index) => { return React.createElement(itineraryClass, { itinerary, index, @@ -74,7 +63,7 @@ class ItineraryCarousel extends Component { onClick: this._onItineraryClick, ...this.props }) - })) + }) return (
@@ -112,28 +101,18 @@ class ItineraryCarousel extends Component { const mapStateToProps = (state, ownProps) => { const activeSearch = getActiveSearch(state.otp) - let itineraries = null - let profileOptions = null - let showProfileSummary = false - if (activeSearch && activeSearch.response && activeSearch.response.plan) { - itineraries = getActiveItineraries(state.otp) - } else if (activeSearch && activeSearch.response && activeSearch.response.otp) { - profileOptions = activeSearch.response.otp.profile - itineraries = profileOptionsToItineraries(profileOptions) - showProfileSummary = true - } + const itineraries = activeSearch && activeSearch.response && activeSearch.response.plan + ? getActiveItineraries(state.otp) + : null - const pending = activeSearch && activeSearch.pending return { itineraries, - profileOptions, - pending, - showProfileSummary, + pending: activeSearch && activeSearch.pending, activeItinerary: activeSearch && activeSearch.activeItinerary, activeLeg: activeSearch && activeSearch.activeLeg, activeStep: activeSearch && activeSearch.activeStep, companies: state.otp.currentQuery.companies, - timeFormat: getTimeFormat(state.otp.config) + timeFormat: coreUtils.time.getTimeFormat(state.otp.config) } } diff --git a/lib/components/narrative/leg-diagram-preview.js b/lib/components/narrative/leg-diagram-preview.js index 5a736e5b9..fd624d807 100644 --- a/lib/components/narrative/leg-diagram-preview.js +++ b/lib/components/narrative/leg-diagram-preview.js @@ -1,10 +1,10 @@ -import React, {Component} from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, {Component} from 'react' import { connect } from 'react-redux' import ReactResizeDetector from 'react-resize-detector' import { showLegDiagram } from '../../actions/map' -import { getElevationProfile } from '../../util/itinerary' const METERS_TO_FEET = 3.28084 @@ -45,7 +45,7 @@ class LegDiagramPreview extends Component { render () { const { leg, showElevationProfile } = this.props if (!showElevationProfile) return null - const profile = getElevationProfile(leg.steps) + const profile = coreUtils.itinerary.getElevationProfile(leg.steps) // Don't show for very short legs if (leg.distance < 500 || leg.mode === 'CAR') return null diff --git a/lib/components/narrative/line-itin/connected-itinerary-body.js b/lib/components/narrative/line-itin/connected-itinerary-body.js index 0c8774da3..e4c92a4b6 100644 --- a/lib/components/narrative/line-itin/connected-itinerary-body.js +++ b/lib/components/narrative/line-itin/connected-itinerary-body.js @@ -1,5 +1,5 @@ import isEqual from 'lodash.isequal' -import TriMetLegIcon from '@opentripplanner/icons/lib/trimet-leg-icon' +import { TriMetLegIcon } from '@opentripplanner/icons' import TransitLegSummary from '@opentripplanner/itinerary-body/lib/defaults/transit-leg-summary' import ItineraryBody from '@opentripplanner/itinerary-body/lib/otp-react-redux/itinerary-body' import LineColumnContent from '@opentripplanner/itinerary-body/lib/otp-react-redux/line-column-content' diff --git a/lib/components/narrative/line-itin/itin-summary.js b/lib/components/narrative/line-itin/itin-summary.js index 2e74575a7..955491975 100644 --- a/lib/components/narrative/line-itin/itin-summary.js +++ b/lib/components/narrative/line-itin/itin-summary.js @@ -4,12 +4,8 @@ import React, { Component } from 'react' import styled from 'styled-components' import { - calculateFares, - calculatePhysicalActivity, - getLegIcon, - isTransit + getLegIcon } from '../../../util/itinerary' -import { formatDuration, formatTime } from '../../../util/time' // TODO: make this a prop const defaultRouteColor = '#008' @@ -48,7 +44,7 @@ const NonTransitSpacer = styled.div` overflow: hidden ` -const RoutePreivew = styled.div` +const RoutePreview = styled.div` display: inline-block; margin-left: 8px; vertical-align: top; @@ -93,22 +89,22 @@ export default class ItinerarySummary extends Component { maxTNCFare, minTNCFare, transitFare - } = calculateFares(itinerary) + } = coreUtils.itinerary.calculateFares(itinerary) // TODO: support non-USD const minTotalFare = minTNCFare * 100 + transitFare const maxTotalFare = maxTNCFare * 100 + transitFare - const { caloriesBurned } = calculatePhysicalActivity(itinerary) + const { caloriesBurned } = coreUtils.itinerary.calculatePhysicalActivity(itinerary) return (
{/* Travel time in hrs/mins */} -
{formatDuration(itinerary.duration)}
+
{coreUtils.time.formatDuration(itinerary.duration)}
{/* Duration as time range */} - {formatTime(itinerary.startTime, timeOptions)} - {formatTime(itinerary.endTime, timeOptions)} + {coreUtils.time.formatTime(itinerary.startTime, timeOptions)} - {coreUtils.time.formatTime(itinerary.endTime, timeOptions)} {/* Fare / Calories */} @@ -134,9 +130,9 @@ export default class ItinerarySummary extends Component { return !(leg.mode === 'WALK' && itinerary.transitTime > 0) }).map((leg, k) => { return ( - + {getLegIcon(leg, customIcons)} - {isTransit(leg.mode) + {coreUtils.itinerary.isTransit(leg.mode) ? ( {getRouteNameForBadge(leg)} @@ -144,7 +140,7 @@ export default class ItinerarySummary extends Component { ) : () } - + ) })} diff --git a/lib/components/narrative/line-itin/line-itinerary.js b/lib/components/narrative/line-itin/line-itinerary.js index 989d726c5..baf6034bd 100644 --- a/lib/components/narrative/line-itin/line-itinerary.js +++ b/lib/components/narrative/line-itin/line-itinerary.js @@ -1,3 +1,4 @@ +import coreUtils from '@opentripplanner/core-utils' import React from 'react' import styled from 'styled-components' @@ -5,7 +6,8 @@ import ItineraryBody from './connected-itinerary-body' import ItinerarySummary from './itin-summary' import NarrativeItinerary from '../narrative-itinerary' import SimpleRealtimeAnnotation from '../simple-realtime-annotation' -import { getLegModeLabel, getTimeZoneOffset, isTransit } from '../../../util/itinerary' + +const { getLegModeLabel, getTimeZoneOffset, isTransit } = coreUtils.itinerary export const LineItineraryContainer = styled.div` margin-bottom: 20px; diff --git a/lib/components/narrative/narrative-profile-options.js b/lib/components/narrative/narrative-profile-options.js deleted file mode 100644 index 405d6609a..000000000 --- a/lib/components/narrative/narrative-profile-options.js +++ /dev/null @@ -1,86 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' - -import { setActiveItinerary, setActiveLeg, setActiveStep } from '../../actions/narrative' -import DefaultItinerary from './default/default-itinerary' -import NarrativeProfileSummary from './narrative-profile-summary' -import Loading from './loading' -import { getActiveSearch } from '../../util/state' -import { profileOptionsToItineraries } from '../../util/profile' - -class NarrativeProfileOptions extends Component { - static propTypes = { - options: PropTypes.array, - query: PropTypes.object, - itineraryClass: PropTypes.func, - pending: PropTypes.bool, - activeOption: PropTypes.number, - setActiveItinerary: PropTypes.func, - setActiveLeg: PropTypes.func, - setActiveStep: PropTypes.func, - customIcons: PropTypes.object - } - - static defaultProps = { - itineraryClass: DefaultItinerary - } - - render () { - const { pending, itineraryClass, query, activeItinerary } = this.props - if (pending) return - - const options = this.props.options - if (!options) return null - - const itineraries = profileOptionsToItineraries(options, query) - - return ( -
-
Your best options:
- -
We found {options.length} total options:
- {itineraries.map((itinerary, index) => { - return React.createElement(itineraryClass, { - itinerary, - index, - key: index, - active: index === activeItinerary, - routingType: 'PROFILE', - ...this.props - }) - })} -
- ) - } -} - -// connect to the redux store -const mapStateToProps = (state, ownProps) => { - const activeSearch = getActiveSearch(state.otp) - // const { activeItinerary, activeLeg, activeStep } = activeSearch ? activeSearch.activeItinerary : {} - const pending = activeSearch && activeSearch.pending - return { - options: - activeSearch && - activeSearch.response && - activeSearch.response.otp - ? activeSearch.response.otp.profile - : null, - pending, - activeItinerary: activeSearch && activeSearch.activeItinerary, - activeLeg: activeSearch && activeSearch.activeLeg, - activeStep: activeSearch && activeSearch.activeStep, - query: activeSearch && activeSearch.query - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - return { - setActiveItinerary: (index) => { dispatch(setActiveItinerary({ index })) }, - setActiveLeg: (index, leg) => { dispatch(setActiveLeg({ index, leg })) }, - setActiveStep: (index, step) => { dispatch(setActiveStep({ index, step })) } - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(NarrativeProfileOptions) diff --git a/lib/components/narrative/narrative-profile-summary.js b/lib/components/narrative/narrative-profile-summary.js deleted file mode 100644 index cf8db6c6e..000000000 --- a/lib/components/narrative/narrative-profile-summary.js +++ /dev/null @@ -1,80 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' - -import { getIcon } from '../../util/itinerary' - -export default class NarrativeProfileSummary extends Component { - static propTypes = { - options: PropTypes.array, - customIcons: PropTypes.object - } - - render () { - const { options } = this.props - - let bestTransit = 0 - let walk = 0 - let bicycle = 0 - let bicycleRent = 0 - - options.forEach((option, i) => { - if (option.transit) { - if (option.time < bestTransit || bestTransit === 0) { - bestTransit = option.time - } - } else { - if (option.modes.length === 1 && option.modes[0] === 'bicycle') bicycle = option.time - else if (option.modes.length === 1 && option.modes[0] === 'walk') walk = option.time - else if (option.modes.indexOf('bicycle_rent') !== -1) bicycleRent = option.time - } - }) - - const summary = [ - { - icon: 'BUS', - title: 'Transit', - time: bestTransit - }, { - icon: 'BICYCLE', - title: 'Bicycle', - time: bicycle - }, { - icon: 'BICYCLE_RENT', - title: 'Bikeshare', - time: bicycleRent - }, { - icon: 'WALK', - title: 'Walk', - time: walk - } - ] - - return ( -
- {summary.map((option, k) => { - return ( -
0 ? '#084C8D' : '#bbb', - width: '22%', - display: 'inline-block', - verticalAlign: 'top', - marginRight: (k < 3 ? '4%' : 0), - padding: '3px', - textAlign: 'center', - color: 'white' }} - > -
{getIcon(option.icon, this.props.customIcons)}
-
{option.title}
-
- {option.time > 0 - ? {Math.round(option.time / 60)} min - : (Not Found) - } -
-
- ) - })} -
- ) - } -} diff --git a/lib/components/narrative/narrative-routing-results.js b/lib/components/narrative/narrative-routing-results.js index f7f407420..204cc245b 100644 --- a/lib/components/narrative/narrative-routing-results.js +++ b/lib/components/narrative/narrative-routing-results.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types' import { connect } from 'react-redux' import Loading from './loading' -import NarrativeProfileOptions from './narrative-profile-options' import TabbedItineraries from './tabbed-itineraries' import ErrorMessage from '../form/error-message' @@ -26,18 +25,14 @@ class NarrativeRoutingResults extends Component { } render () { - const { customIcons, error, itineraryClass, itineraryFooter, pending, routingType, itineraries, mainPanelContent } = this.props + const { customIcons, error, itineraryClass, itineraryFooter, pending, itineraries, mainPanelContent } = this.props if (pending) return if (error) return if (mainPanelContent) return null return ( - routingType === 'ITINERARY' - ? - : + // TODO: If multiple routing types exist, do the check here. + ) } } diff --git a/lib/components/narrative/realtime-annotation.js b/lib/components/narrative/realtime-annotation.js index 834046d50..31ed7cd0e 100644 --- a/lib/components/narrative/realtime-annotation.js +++ b/lib/components/narrative/realtime-annotation.js @@ -1,9 +1,8 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button, OverlayTrigger, Popover } from 'react-bootstrap' -import { formatDuration } from '../../util/time' - export default class RealtimeAnnotation extends Component { static propTypes = { realtimeEffects: PropTypes.object, @@ -33,7 +32,7 @@ export default class RealtimeAnnotation extends Component { ? Your trip results have been adjusted based on real-time information. Under normal conditions, this trip would take{' '} - {formatDuration(realtimeEffects.normalDuration)} + {coreUtils.time.formatDuration(realtimeEffects.normalDuration)} using the following routes:{' '} {filteredRoutes .map((route, idx) => ( diff --git a/lib/components/narrative/tabbed-itineraries.js b/lib/components/narrative/tabbed-itineraries.js index f3c4e2d76..94f38eeb8 100644 --- a/lib/components/narrative/tabbed-itineraries.js +++ b/lib/components/narrative/tabbed-itineraries.js @@ -1,13 +1,15 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' -import { connect } from 'react-redux' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' +import { connect } from 'react-redux' import { setActiveItinerary, setActiveLeg, setActiveStep, setUseRealtimeResponse } from '../../actions/narrative' import DefaultItinerary from './default/default-itinerary' import { getActiveSearch, getRealtimeEffects } from '../../util/state' -import { calculateFares, calculatePhysicalActivity, getTimeZoneOffset } from '../../util/itinerary' -import { formatDuration, formatTime, getTimeFormat } from '../../util/time' + +const { calculateFares, calculatePhysicalActivity, getTimeZoneOffset } = coreUtils.itinerary +const { formatDuration, formatTime, getTimeFormat } = coreUtils.time class TabbedItineraries extends Component { static propTypes = { diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index c298d5a50..3cb3e6df5 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -1,3 +1,4 @@ +import coreUtils from '@opentripplanner/core-utils' import React, { Component, PureComponent } from 'react' import PropTypes from 'prop-types' import { Label, Button } from 'react-bootstrap' @@ -8,7 +9,6 @@ import Icon from '../narrative/icon' import { setMainPanelContent, setViewedRoute } from '../../actions/ui' import { findRoutes, findRoute } from '../../actions/api' -import { routeComparator } from '../../util/itinerary' function operatorIndexForRoute (transitOperators, route) { if (!route.agency) return 0 @@ -58,7 +58,7 @@ class RouteViewer extends Component { viewedRoute } = this.props const sortedRoutes = routes - ? Object.values(routes).sort(routeComparator) + ? Object.values(routes).sort(coreUtils.itinerary.routeComparator) : [] const agencySortedRoutes = transitOperators.length > 0 ? sortedRoutes.sort((a, b) => { diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 424d7278b..ac668f353 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -1,20 +1,25 @@ -import React, { Component } from 'react' +import moment from 'moment' +import 'moment-timezone' +import coreUtils from '@opentripplanner/core-utils' +import FromToLocationPicker from '@opentripplanner/from-to-location-picker' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import moment from 'moment' -import 'moment-timezone' import { VelocityTransitionGroup } from 'velocity-react' -import FromToLocationPicker from '@opentripplanner/from-to-location-picker' - import Icon from '../narrative/icon' import { setMainPanelContent, toggleAutoRefresh } from '../../actions/ui' import { findStop, findStopTimesForStop } from '../../actions/api' import { forgetStop, rememberStop, setLocation } from '../../actions/map' -import { routeComparator } from '../../util/itinerary' import { getShowUserSettings, getStopViewerConfig } from '../../util/state' -import { formatDuration, formatSecondsAfterMidnight, getTimeFormat, getUserTimezone } from '../../util/time' + +const { + formatDuration, + formatSecondsAfterMidnight, + getTimeFormat, + getUserTimezone +} = coreUtils.time class StopViewer extends Component { state = {} @@ -225,7 +230,7 @@ class StopViewer extends Component { {stopData.stopTimes && stopData.routes && (
{Object.values(stopTimesByPattern) - .sort((a, b) => routeComparator(a.route, b.route)) + .sort((a, b) => coreUtils.itinerary.routeComparator(a.route, b.route)) .map(patternTimes => { // Only add pattern row if route is found. // FIXME: there is currently a bug with the alernative transit index diff --git a/lib/components/viewers/trip-viewer.js b/lib/components/viewers/trip-viewer.js index f1d3fe928..9ce414d29 100644 --- a/lib/components/viewers/trip-viewer.js +++ b/lib/components/viewers/trip-viewer.js @@ -1,5 +1,6 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button, Label } from 'react-bootstrap' import { connect } from 'react-redux' @@ -10,8 +11,6 @@ import { setViewedTrip } from '../../actions/ui' import { findTrip } from '../../actions/api' import { setLocation } from '../../actions/map' -import { formatSecondsAfterMidnight, getTimeFormat } from '../../util/time' - class TripViewer extends Component { static propTypes = { hideBackButton: PropTypes.bool, @@ -101,7 +100,7 @@ class TripViewer extends Component {
{/* the departure time */}
- {formatSecondsAfterMidnight(tripData.stopTimes[i].scheduledDeparture, timeFormat)} + {coreUtils.time.formatSecondsAfterMidnight(tripData.stopTimes[i].scheduledDeparture, timeFormat)}
{/* the vertical strip map */} @@ -137,7 +136,7 @@ const mapStateToProps = (state, ownProps) => { const viewedTrip = state.otp.ui.viewedTrip return { languageConfig: state.otp.config.language, - timeFormat: getTimeFormat(state.otp.config), + timeFormat: coreUtils.time.getTimeFormat(state.otp.config), tripData: state.otp.transitIndex.trips[viewedTrip.tripId], viewedTrip } diff --git a/lib/index.js b/lib/index.js index ad1bb1d41..4ed9f8b55 100644 --- a/lib/index.js +++ b/lib/index.js @@ -16,7 +16,6 @@ import TileOverlay from './components/map/tile-overlay' import ItineraryCarousel from './components/narrative/itinerary-carousel' import LegDiagramPreview from './components/narrative/leg-diagram-preview' import NarrativeItineraries from './components/narrative/narrative-itineraries' -import NarrativeProfileOptions from './components/narrative/narrative-profile-options' import NarrativeItinerary from './components/narrative/narrative-itinerary' import NarrativeRoutingResults from './components/narrative/narrative-routing-results' import RealtimeAnnotation from './components/narrative/realtime-annotation' @@ -71,7 +70,6 @@ export { LineItinerary, NarrativeItineraries, NarrativeItinerary, - NarrativeProfileOptions, NarrativeRoutingResults, RealtimeAnnotation, SimpleRealtimeAnnotation, diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 2ea229618..d18fbdb20 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -2,18 +2,20 @@ import clone from 'clone' import update from 'immutability-helper' import isEqual from 'lodash.isequal' import objectPath from 'object-path' +import coreUtils from '@opentripplanner/core-utils' -import { matchLatLon } from '../util/map' -import { +import { MainPanelContent, MobileScreens } from '../actions/ui' + +const { isTransit, getTransitModes } = coreUtils.itinerary +const { matchLatLon } = coreUtils.map +const { filterProfileOptions } = coreUtils.profile +const { ensureSingleAccessMode, getDefaultQuery, getTripOptionsFromQuery -} from '../util/query' -import { isTransit, getTransitModes } from '../util/itinerary' -import { filterProfileOptions } from '../util/profile' -import { getItem, removeItem, storeItem } from '../util/storage' -import { getUserTimezone } from '../util/time' -import { MainPanelContent, MobileScreens } from '../actions/ui' +} = coreUtils.query +const { getItem, removeItem, storeItem } = coreUtils.storage +const { getUserTimezone } = coreUtils.time const MAX_RECENT_STORAGE = 5 diff --git a/lib/util/index.js b/lib/util/index.js index da1a44a7c..02d4e6327 100644 --- a/lib/util/index.js +++ b/lib/util/index.js @@ -1,21 +1,9 @@ import * as itinerary from './itinerary' -import * as map from './map' -import * as profile from './profile' -import * as query from './query' -import * as reverse from './reverse' import * as state from './state' -import * as time from './time' -import * as ui from './ui' const OtpUtils = { itinerary, - map, - profile, - query, - reverse, - state, - time, - ui + state } export default OtpUtils diff --git a/lib/util/itinerary.js b/lib/util/itinerary.js index 748b88068..6a65f5f13 100644 --- a/lib/util/itinerary.js +++ b/lib/util/itinerary.js @@ -1,197 +1,9 @@ -import React from 'react' import { latLngBounds } from 'leaflet' -import polyline from '@mapbox/polyline' -import turfAlong from '@turf/along' +import coreUtils from '@opentripplanner/core-utils' +import React from 'react' import ModeIcon from '../components/icons/mode-icon' -// All OTP transit modes -export const transitModes = ['TRAM', 'BUS', 'SUBWAY', 'FERRY', 'RAIL', 'GONDOLA'] - -/** - * @param {config} config OTP-RR configuration object - * @return {Array} List of all transit modes defined in config; otherwise default mode list - */ - -export function getTransitModes (config) { - if (!config || !config.modes || !config.modes.transitModes) return transitModes - return config.modes.transitModes.map(tm => tm.mode) -} - -export function isTransit (mode) { - return transitModes.includes(mode) || mode === 'TRANSIT' -} - -/** - * @param {string} modesStr a comma-separated list of OTP modes - * @return {boolean} whether any of the modes are transit modes - */ -export function hasTransit (modesStr) { - for (const mode of modesStr.split(',')) { - if (isTransit(mode)) return true - } - return false -} - -/** - * @param {string} modesStr a comma-separated list of OTP modes - * @return {boolean} whether any of the modes are car-based modes - */ -export function hasCar (modesStr) { - if (modesStr) { - for (const mode of modesStr.split(',')) { - if (isCar(mode)) return true - } - } - return false -} - -/** - * @param {string} modesStr a comma-separated list of OTP modes - * @return {boolean} whether any of the modes are bicycle-based modes - */ -export function hasBike (modesStr) { - if (modesStr) { - for (const mode of modesStr.split(',')) { - if (isBicycle(mode) || isBicycleRent(mode)) return true - } - } - return false -} - -/** - * @param {string} modesStr a comma-separated list of OTP modes - * @return {boolean} whether any of the modes are micromobility-based modes - */ -export function hasMicromobility (modesStr) { - if (modesStr) { - for (const mode of modesStr.split(',')) { - if (isMicromobility(mode)) return true - } - } - return false -} - -/** - * @param {string} modesStr a comma-separated list of OTP modes - * @return {boolean} whether any of the modes is a hailing mode - */ -export function hasHail (modesStr) { - if (modesStr) { - for (const mode of modesStr.split(',')) { - if (mode.indexOf('_HAIL') > -1) return true - } - } - return false -} - -/** - * @param {string} modesStr a comma-separated list of OTP modes - * @return {boolean} whether any of the modes is a rental mode - */ -export function hasRental (modesStr) { - if (modesStr) { - for (const mode of modesStr.split(',')) { - if (mode.indexOf('_RENT') > -1) return true - } - } - return false -} - -export function isWalk (mode) { - if (!mode) return false - - return mode === 'WALK' -} - -export function isBicycle (mode) { - if (!mode) return false - - return mode === 'BICYCLE' -} - -export function isBicycleRent (mode) { - if (!mode) return false - - return mode === 'BICYCLE_RENT' -} - -export function isCar (mode) { - if (!mode) return false - return mode.startsWith('CAR') -} - -export function isMicromobility (mode) { - if (!mode) return false - return mode.startsWith('MICROMOBILITY') -} - -export function isAccessMode (mode) { - return isWalk(mode) || - isBicycle(mode) || - isBicycleRent(mode) || - isCar(mode) || - isMicromobility(mode) -} - -export function getMapColor (mode) { - mode = mode || this.get('mode') - if (mode === 'WALK') return '#444' - if (mode === 'BICYCLE') return '#0073e5' - if (mode === 'SUBWAY') return '#f00' - if (mode === 'RAIL') return '#b00' - if (mode === 'BUS') return '#080' - if (mode === 'TRAM') return '#800' - if (mode === 'FERRY') return '#008' - if (mode === 'CAR') return '#444' - if (mode === 'MICROMOBILITY') return '#f5a729' - return '#aaa' -} - -// TODO: temporary code; handle via migrated OTP i18n language table -export function getStepDirection (step) { - switch (step.relativeDirection) { - case 'DEPART': return 'Head ' + step.absoluteDirection.toLowerCase() - case 'LEFT': return 'Left' - case 'HARD_LEFT': return 'Hard left' - case 'SLIGHTLY_LEFT': return 'Slight left' - case 'CONTINUE': return 'Continue' - case 'SLIGHTLY_RIGHT': return 'Slight right' - case 'RIGHT': return 'Right' - case 'HARD_RIGHT': return 'Hard right' - case 'CIRCLE_CLOCKWISE': return 'Follow circle clockwise' - case 'CIRCLE_COUNTERCLOCKWISE': return 'Follow circle counterclockwise' - case 'ELEVATOR': return 'Take elevator' - case 'UTURN_LEFT': return 'Left U-turn' - case 'UTURN_RIGHT': return 'Right U-turn' - } - return step.relativeDirection -} - -export function getStepInstructions (step) { - const conjunction = step.relativeDirection === 'ELEVATOR' ? 'to' : 'on' - return `${getStepDirection(step)} ${conjunction} ${step.streetName}` -} - -export function getStepStreetName (step) { - if (step.streetName === 'road') return 'Unnamed Road' - if (step.streetName === 'path') return 'Unnamed Path' - return step.streetName -} - -export function getLegModeLabel (leg) { - switch (leg.mode) { - case 'BICYCLE_RENT': return 'Biketown' - case 'CAR': return leg.hailedCar ? 'Ride' : 'Drive' - case 'GONDOLA': return 'Aerial Tram' - case 'TRAM': - if (leg.routeLongName.toLowerCase().indexOf('streetcar') !== -1) return 'Streetcar' - return 'Light Rail' - case 'MICROMOBILITY': return 'Ride' - } - return toSentenceCase(leg.mode) -} - /** * Returns a react element of the desired icon. If customIcons are defined, then * the icon will be attempted to be used from that lookup of icons. Otherwise, @@ -217,386 +29,15 @@ export function getIcon (iconId, customIcons) { return } -export function getItineraryBounds (itinerary) { - let coords = [] - itinerary.legs.forEach(leg => { - const legCoords = polyline - .toGeoJSON(leg.legGeometry.points) - .coordinates.map(c => [c[1], c[0]]) - coords = [...coords, ...legCoords] - }) - return latLngBounds(coords) +export function getLeafletItineraryBounds (itinerary) { + return latLngBounds(coreUtils.itinerary.getItineraryBounds(itinerary)) } /** * Return a leaflet LatLngBounds object that encloses the given leg's geometry. */ -export function getLegBounds (leg) { - const coords = polyline - .toGeoJSON(leg.legGeometry.points) - .coordinates.map(c => [c[1], c[0]]) - - // in certain cases, there might be zero-length coordinates in the leg - // geometry. In these cases, build us an array of coordinates using the from - // and to data of the leg. - if (coords.length === 0) { - coords.push([leg.from.lat, leg.from.lon], [leg.to.lat, leg.to.lon]) - } - return latLngBounds(coords) -} - -/** - * Gets the desired sort values according to an optional getter function. If the - * getter function is not defined, the original sort values are returned. - */ -function getSortValues (getterFn, a, b) { - let aVal - let bVal - if (typeof getterFn === 'function') { - aVal = getterFn(a) - bVal = getterFn(b) - } else { - aVal = a - bVal = b - } - return { aVal, bVal } -} - -// Lookup for the sort values associated with various OTP modes. -// Note: JSDoc format not used to avoid bug in documentationjs. -// https://github.com/documentationjs/documentation/issues/372 -const modeComparatorValue = { - SUBWAY: 1, - TRAM: 2, - RAIL: 3, - GONDOLA: 4, - FERRY: 5, - CABLE_CAR: 6, - FUNICULAR: 7, - BUS: 8 -} - -// Lookup that maps route types to the OTP mode sort values. -// Note: JSDoc format not used to avoid bug in documentationjs. -// https://github.com/documentationjs/documentation/issues/372 -const routeTypeComparatorValue = { - 0: modeComparatorValue.TRAM, // - Tram, Streetcar, Light rail. - 1: modeComparatorValue.SUBWAY, // - Subway, Metro. - 2: modeComparatorValue.RAIL, // - Rail. Used for intercity or long-distance travel. - 3: modeComparatorValue.BUS, // - Bus. - 4: modeComparatorValue.FERRY, // - Ferry. - 5: modeComparatorValue.CABLE_CAR, // - Cable tram. - 6: modeComparatorValue.GONDOLA, // - Gondola, etc. - 7: modeComparatorValue.FUNICULAR, // - Funicular. - // TODO: 11 and 12 are not a part of OTP as of 2019-02-14, but for now just - // associate them with bus/rail. - 11: modeComparatorValue.BUS, // - Trolleybus. - 12: modeComparatorValue.RAIL // - Monorail. -} - -// Gets a comparator value for a given route's type (OTP mode). -// Note: JSDoc format not used to avoid bug in documentationjs. -// ttps://github.com/documentationjs/documentation/issues/372 -function getRouteTypeComparatorValue (route) { - // For some strange reason, the short route response in OTP returns the - // string-based modes, but the long route response returns the - // integer route type. This attempts to account for both of those cases. - if (!route) throw new Error('Route is undefined.', route) - if (typeof modeComparatorValue[route.mode] !== 'undefined') { - return modeComparatorValue[route.mode] - } else if (typeof routeTypeComparatorValue[route.type] !== 'undefined') { - return routeTypeComparatorValue[route.type] - } else { - // Default the comparator value to a large number (placing the route at the - // end of the list). - console.warn('no mode/route type found for route', route) - return 9999 - } -} - -/** - * Calculates the sort comparator value given two routes based off of route type - * (OTP mode). - */ -function routeTypeComparator (a, b) { - return getRouteTypeComparatorValue(a) - getRouteTypeComparatorValue(b) -} - -/** - * Determines whether a value is a string that starts with an alphabetic - * ascii character. - */ -function startsWithAlphabeticCharacter (val) { - if (typeof val === 'string' && val.length > 0) { - const firstCharCode = val.charCodeAt(0) - return (firstCharCode >= 65 && firstCharCode <= 90) || - (firstCharCode >= 97 && firstCharCode <= 122) - } - return false -} - -/** - * Sorts routes based off of whether the shortName begins with an alphabetic - * character. Routes with shortn that do start with an alphabetic character will - * be prioritized over those that don't. - */ -function alphabeticShortNameComparator (a, b) { - const aStartsWithAlphabeticCharacter = startsWithAlphabeticCharacter( - a.shortName - ) - const bStartsWithAlphabeticCharacter = startsWithAlphabeticCharacter( - b.shortName - ) - - if (aStartsWithAlphabeticCharacter && bStartsWithAlphabeticCharacter) { - // both start with an alphabetic character, return equivalence - return 0 - } - // a does start with an alphabetic character, but b does not. Prioritize a - if (aStartsWithAlphabeticCharacter) return -1 - // b does start with an alphabetic character, but a does not. Prioritize b - if (bStartsWithAlphabeticCharacter) return 1 - // neither route has a shortName that starts with an alphabetic character. - // Return equivalence - return 0 -} - -/** - * Checks whether an appropriate comparison of numeric values can be made for - * sorting purposes. If both values are not valid numbers according to the - * isNaN check, then this function returns undefined which indicates that a - * secondary sorting criteria should be used instead. If one value is valid and - * the other is not, then the valid value will be given sorting priority. If - * both values are valid numbers, the difference is obtained as the sort value. - * - * An optional argument can be provided which will be used to obtain the - * comparison value from the comparison function arguments. - * - * IMPORTANT: the comparison values must be numeric values or at least be - * attempted to be converted to numeric values! If one of the arguments is - * something crazy like an empty string, unexpected behavior will occur because - * JavaScript. - * - * @param {function} [objGetterFn] An optional function to obtain the - * comparison value from the comparator function arguments - */ -function makeNumericValueComparator (objGetterFn) { - return (a, b) => { - const { aVal, bVal } = getSortValues(objGetterFn, a, b) - // if both values aren't valid numbers, use the next sort criteria - if (isNaN(aVal) && isNaN(bVal)) return 0 - // b is a valid number, b gets priority - if (isNaN(aVal)) return 1 - // a is a valid number, a gets priority - if (isNaN(bVal)) return -1 - // a and b are valid numbers, return the sort value - return aVal - bVal - } -} - -/** - * Create a comparator function that compares string values. The comparison - * values feed to the sort comparator function are assumed to be objects that - * will have either undefined, null or string values at the given key. If one - * object has undefined, null or an empty string, but the other does have a - * string with length > 0, then that string will get priority. - * - * @param {function} [objGetterFn] An optional function to obtain the - * comparison value from the comparator function arguments - */ -function makeStringValueComparator (objGetterFn) { - return (a, b) => { - const { aVal, bVal } = getSortValues(objGetterFn, a, b) - // both a and b are uncomparable strings, return equivalent value - if (!aVal && !bVal) return 0 - // a is not a comparable string, b gets priority - if (!aVal) return 1 - // b is not a comparable string, a gets priority - if (!bVal) return -1 - // a and b are comparable strings, return the sort value - if (aVal < bVal) return -1 - if (aVal > bVal) return 1 - return 0 - } -} - -/** - * OpenTripPlanner sets the routeSortOrder to -999 by default. So, if that value - * is encountered, assume that it actually means that the routeSortOrder is not - * set in the GTFS. - * - * See https://github.com/opentripplanner/OpenTripPlanner/issues/2938 - * Also see https://github.com/opentripplanner/otp-react-redux/issues/122 - */ -function getRouteSortOrderValue (val) { - return val === -999 ? undefined : val -} - -/** - * Create a multi-criteria sort comparator function composed of other sort - * comparator functions. Each comparator function will be ran in the order given - * until a non-zero comparison value is obtained which is then immediately - * returned. If all comparison functions return equivalance, then the values - * are assumed to be equivalent. - */ -function makeMultiCriteriaSort (...criteria) { - return (a, b) => { - for (let i = 0; i < criteria.length; i++) { - const curCriteriaComparatorValue = criteria[i](a, b) - // if the comparison objects are not equivalent, return the value obtained - // in this current criteria comparison - if (curCriteriaComparatorValue !== 0) { - return curCriteriaComparatorValue - } - } - return 0 - } -} - -/** - * Compares routes for the purposes of sorting and displaying in a user - * interface. Due to GTFS feeds having varying levels of data quality, a multi- - * criteria sort is needed to account for various differences. The criteria - * included here are each applied to the routes in the order listed. If a given - * sort criterion yields equivalence (e.g., two routes have the short name - * "20"), the comparator falls back onto the next sort criterion (e.g., long - * name). If desired, the criteria of sorting based off of integer shortName can - * be disabled. The sort operates on the following values (in order): - * - * 1. sortOrder. Routes that do not have a valid sortOrder will be placed - * beneath those that do. - * 2. route type (OTP mode). See routeTypeComparator code for prioritization of - * route types. - * 3. shortNames that begin with alphabetic characters. shortNames that do not - * start with alphabetic characters will be place beneath those that do. - * 4. shortName as integer. shortNames that cannot be parsed as integers will - * be placed beneath those that are valid. - * 5. shortName as string. Routes without shortNames will be placed beneath - * those with shortNames. - * 6. longName as string. - */ -export const routeComparator = makeMultiCriteriaSort( - makeNumericValueComparator(obj => getRouteSortOrderValue(obj.sortOrder)), - routeTypeComparator, - alphabeticShortNameComparator, - makeNumericValueComparator(obj => parseInt(obj.shortName)), - makeStringValueComparator(obj => obj.shortName), - makeStringValueComparator(obj => obj.longName) -) - -/* Returns an interpolated lat-lon at a specified distance along a leg */ - -export function legLocationAtDistance (leg, distance) { - if (!leg.legGeometry) return null - - try { - const line = polyline.toGeoJSON(leg.legGeometry.points) - const pt = turfAlong(line, distance, { units: 'meters' }) - if (pt && pt.geometry && pt.geometry.coordinates) { - return [ - pt.geometry.coordinates[1], - pt.geometry.coordinates[0] - ] - } - } catch (e) { } - - return null -} - -/* Returns an interpolated elevation at a specified distance along a leg */ - -export function legElevationAtDistance (points, distance) { - // Iterate through the combined elevation profile - let traversed = 0 - // If first point distance is not zero, insert starting point at zero with - // null elevation. Encountering this value should trigger the warning below. - if (points[0][0] > 0) { - points.unshift([0, null]) - } - for (let i = 1; i < points.length; i++) { - const start = points[i - 1] - const elevDistanceSpan = points[i][0] - start[0] - if (distance >= traversed && distance <= traversed + elevDistanceSpan) { - // Distance falls within this point and the previous one; - // compute & return iterpolated elevation value - if (start[1] === null) { - console.warn('Elevation value does not exist for distance.', distance, traversed) - return null - } - const pct = (distance - traversed) / elevDistanceSpan - const elevSpan = points[i][1] - start[1] - return start[1] + elevSpan * pct - } - traversed += elevDistanceSpan - } - console.warn('Elevation value does not exist for distance.', distance, traversed) - return null -} - -// Iterate through the steps, building the array of elevation points and -// keeping track of the minimum and maximum elevations reached -export function getElevationProfile (steps, unitConversion = 1) { - let minElev = 100000 - let maxElev = -100000 - let traversed = 0 - let gain = 0 - let loss = 0 - let previous = null - const points = [] - steps.forEach((step, stepIndex) => { - if (!step.elevation || step.elevation.length === 0) { - traversed += step.distance - return - } - for (let i = 0; i < step.elevation.length; i++) { - const elev = step.elevation[i] - if (previous) { - const diff = (elev.second - previous.second) * unitConversion - if (diff > 0) gain += diff - else loss += diff - } - if (i === 0 && elev.first !== 0) { - // console.warn(`No elevation data available for step ${stepIndex}-${i} at beginning of segment`, elev) - } - const convertedElevation = elev.second * unitConversion - if (convertedElevation < minElev) minElev = convertedElevation - if (convertedElevation > maxElev) maxElev = convertedElevation - points.push([traversed + elev.first, elev.second]) - // Insert "filler" point if the last point in elevation profile does not - // reach the full distance of the step. - if (i === step.elevation.length - 1 && elev.first !== step.distance) { - // points.push([traversed + step.distance, elev.second]) - } - previous = elev - } - traversed += step.distance - }) - return { maxElev, minElev, points, traversed, gain, loss } -} - -/** - * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. - * - * @param {string} text The text to be rendered. - * @param {string} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). - * - * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 - */ -export function getTextWidth (text, font = '22px Arial') { - // re-use canvas object for better performance - var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement('canvas')) - var context = canvas.getContext('2d') - context.font = font - var metrics = context.measureText(text) - return metrics.width -} - -export function toSentenceCase (str) { - if (str == null) { - return '' - } - str = String(str) - return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase() +export function getLeafletLegBounds (leg) { + return latLngBounds(coreUtils.itinerary.getLegBounds(leg)) } /** @@ -633,134 +74,3 @@ export function getLegIcon (leg, customIcons) { return getIcon(iconStr, customIcons) } - -/** - * Get the configured company object for the given network string if the company - * has been defined in the provided companies array config. - */ -function getCompanyForNetwork (networkString, companies = []) { - const company = companies.find(co => co.id === networkString) - if (!company) { - console.warn(`No company found in config.yml that matches rented vehicle network: ${networkString}`, companies) - } - return company -} - -/** - * Get a string label to display from a list of vehicle rental networks. - * - * @param {Array} networks A list of network ids. - * @param {Array} [companies=[]] An optional list of the companies config. - * @return {string} A label for use in presentation on a website. - */ -export function getCompaniesLabelFromNetworks (networks, companies = []) { - return networks.map(network => getCompanyForNetwork(network, companies)) - .filter(co => !!co) - .map(co => co.label) - .join('/') -} - -/** - * Returns mode name by checking the vertex type (VertexType class in OTP) for - * the provided place. NOTE: this is currently only intended for vehicles at - * the moment (not transit or walking). - * - * TODO: I18N - * @param {string} place place from itinerary leg - */ -export function getModeForPlace (place) { - switch (place.vertexType) { - case 'CARSHARE': - return 'car' - case 'VEHICLERENTAL': - return 'E-scooter' - // TODO: Should the type change depending on bike vertex type? - case 'BIKESHARE': - case 'BIKEPARK': - return 'bike' - // If company offers more than one mode, default to `vehicle` string. - default: - return 'vehicle' - } -} - -export function getPlaceName (place, companies) { - // If address is provided (i.e. for carshare station, use it) - if (place.address) return place.address.split(',')[0] - if (place.networks && place.vertexType === 'VEHICLERENTAL') { - // For vehicle rental pick up, do not use the place name. Rather, use - // company name + vehicle type (e.g., SPIN E-scooter). Place name is often just - // a UUID that has no relevance to the actual vehicle. For bikeshare, however, - // there are often hubs or bikes that have relevant names to the user. - const company = getCompanyForNetwork(place.networks[0], companies) - if (company) { - return `${company.label} ${getModeForPlace(place)}` - } - } - // Default to place name - return place.name -} - -export function getTNCLocation (leg, type) { - const location = leg[type] - return `${location.lat.toFixed(5)},${location.lon.toFixed(5)}` -} - -export function calculatePhysicalActivity (itinerary) { - let walkDuration = 0 - let bikeDuration = 0 - for (const leg of itinerary.legs) { - if (leg.mode.startsWith('WALK')) walkDuration += leg.duration - if (leg.mode.startsWith('BICYCLE')) bikeDuration += leg.duration - } - const caloriesBurned = - walkDuration / 3600 * 280 + - bikeDuration / 3600 * 290 - return { - bikeDuration, - caloriesBurned, - walkDuration - } -} - -export function calculateFares (itinerary) { - let transitFare = 0 - let symbol = '$' // default to USD - let dollarsToString = dollars => `${symbol}${dollars.toFixed(2)}` - let centsToString = cents => `${symbol}${(cents / Math.pow(10, 2)).toFixed(2)}` - if (itinerary.fare && itinerary.fare.fare && itinerary.fare.fare.regular) { - const reg = itinerary.fare.fare.regular - symbol = reg.currency.symbol - transitFare = reg.cents - centsToString = cents => `${symbol}${(cents / Math.pow(10, reg.currency.defaultFractionDigits)).toFixed(reg.currency.defaultFractionDigits)}` - dollarsToString = dollars => `${symbol}${dollars.toFixed(2)}` - } - - // Process any TNC fares - let minTNCFare = 0 - let maxTNCFare = 0 - for (const leg of itinerary.legs) { - if (leg.mode === 'CAR' && leg.hailedCar && leg.tncData) { - const { maxCost, minCost } = leg.tncData - // TODO: Support non-USD - minTNCFare += minCost - maxTNCFare += maxCost - } - } - return { - centsToString, - dollarsToString, - maxTNCFare, - minTNCFare, - transitFare - } -} - -export function getTimeZoneOffset (itinerary) { - if (!itinerary.legs || !itinerary.legs.length) return 0 - - // Determine if there is a DST offset between now and the itinerary start date - const dstOffset = new Date(itinerary.startTime).getTimezoneOffset() - new Date().getTimezoneOffset() - - return itinerary.legs[0].agencyTimeZoneOffset + (new Date().getTimezoneOffset() + dstOffset) * 60000 -} diff --git a/lib/util/map.js b/lib/util/map.js deleted file mode 100644 index 8c5d44db7..000000000 --- a/lib/util/map.js +++ /dev/null @@ -1,223 +0,0 @@ -import moment from 'moment' - -import { isTransit, toSentenceCase } from './itinerary' - -export function latlngToString (latlng) { - return latlng && `${latlng.lat.toFixed(5)}, ${(latlng.lng || latlng.lon).toFixed(5)}` -} - -export function coordsToString (coords) { - return coords.length && coords.map(c => (+c).toFixed(5)).join(', ') -} - -export function stringToCoords (str) { - return (str && str.split(',').map(c => +c)) || [] -} - -export function constructLocation (latlng) { - return { - name: latlngToString(latlng), - lat: latlng.lat, - lon: latlng.lng - } -} - -export function formatStoredPlaceName (location, withDetails = true) { - let displayName = location.type === 'home' || location.type === 'work' - ? toSentenceCase(location.type) - : location.name - if (withDetails) { - let detailText = getDetailText(location) - if (detailText) displayName += ` (${detailText})` - } - return displayName -} - -export function getDetailText (location) { - let detailText - if (location.type === 'home' || location.type === 'work') { - detailText = location.name - } - if (location.type === 'stop') { - detailText = location.id - } else if (location.type === 'recent' && location.timestamp) { - detailText = moment(location.timestamp).fromNow() - } - return detailText -} - -export function matchLatLon (location1, location2) { - if (!location1 || !location2) return location1 === location2 - return location1.lat === location2.lat && location1.lon === location2.lon -} - -export function itineraryToTransitive (itin, includeGeometry) { - // console.log('itineraryToTransitive', itin); - const tdata = { - journeys: [], - streetEdges: [], - places: [], - patterns: [], - routes: [], - stops: [] - } - const routes = {} - const stops = {} - let streetEdgeId = 0 - let patternId = 0 - - const journey = { - journey_id: 'itin', - journey_name: 'Iterarary-derived Journey', - segments: [] - } - - // add 'from' and 'to' places to the tdata places array - tdata.places.push({ - place_id: 'from', - place_lat: itin.legs[0].from.lat, - place_lon: itin.legs[0].from.lon - }) - tdata.places.push({ - place_id: 'to', - place_lat: itin.legs[itin.legs.length - 1].to.lat, - place_lon: itin.legs[itin.legs.length - 1].to.lon - }) - - itin.legs.forEach(leg => { - if ( - leg.mode === 'WALK' || - leg.mode === 'BICYCLE' || - leg.mode === 'CAR' || - leg.mode === 'MICROMOBILITY' - ) { - const fromPlaceId = leg.from.bikeShareId - ? `bicycle_rent_station_${leg.from.bikeShareId}` - : `itin_street_${streetEdgeId}_from` - const toPlaceId = leg.to.bikeShareId - ? `bicycle_rent_station_${leg.to.bikeShareId}` - : `itin_street_${streetEdgeId}_to` - - const segment = { - type: leg.mode, - streetEdges: [streetEdgeId], - from: { type: 'PLACE', place_id: fromPlaceId }, - to: { type: 'PLACE', place_id: toPlaceId } - } - // For TNC segments, draw using an arc - if (leg.mode === 'CAR' && leg.hailedCar) segment.arc = true - journey.segments.push(segment) - - tdata.streetEdges.push({ - edge_id: streetEdgeId, - geometry: leg.legGeometry - }) - tdata.places.push({ - place_id: fromPlaceId, - // Do not label the from place in addition to the to place. Otherwise, - // in some cases (bike rental station) the label for a single place will - // appear twice on the rendered transitive view. - // See https://github.com/conveyal/trimet-mod-otp/issues/152 - // place_name: leg.from.name, - place_lat: leg.from.lat, - place_lon: leg.from.lon - }) - tdata.places.push({ - place_id: toPlaceId, - place_name: leg.to.name, - place_lat: leg.to.lat, - place_lon: leg.to.lon - }) - streetEdgeId++ - } - if (isTransit(leg.mode)) { - // determine if we have valid inter-stop geometry - const hasInterStopGeometry = - leg.interStopGeometry && - leg.interStopGeometry.length === leg.intermediateStops.length + 1 - - // create leg-specific pattern - const ptnId = 'ptn_' + patternId - const pattern = { - pattern_id: ptnId, - pattern_name: 'Pattern ' + patternId, - route_id: leg.routeId, - stops: [] - } - - // add 'from' stop to stops dictionary and pattern object - stops[leg.from.stopId] = { - stop_id: leg.from.stopId, - stop_name: leg.from.name, - stop_lat: leg.from.lat, - stop_lon: leg.from.lon - } - pattern.stops.push({ stop_id: leg.from.stopId }) - - // add intermediate stops to stops dictionary and pattern object - for (const [i, stop] of leg.intermediateStops.entries()) { - stops[stop.stopId] = { - stop_id: stop.stopId, - stop_name: stop.name, - stop_lat: stop.lat, - stop_lon: stop.lon - } - pattern.stops.push({ - stop_id: stop.stopId, - geometry: hasInterStopGeometry && leg.interStopGeometry[i].points - }) - } - - // add 'to' stop to stops dictionary and pattern object - stops[leg.to.stopId] = { - stop_id: leg.to.stopId, - stop_name: leg.to.name, - stop_lat: leg.to.lat, - stop_lon: leg.to.lon - } - pattern.stops.push({ - stop_id: leg.to.stopId, - geometry: hasInterStopGeometry && leg.interStopGeometry[leg.interStopGeometry.length - 1].points - }) - - // add route to the route dictionary - routes[leg.routeId] = { - agency_id: leg.agencyId, - route_id: leg.routeId, - route_short_name: leg.routeShortName || '', - route_long_name: leg.routeLongName || '', - route_type: leg.routeType, - route_color: leg.routeColor - } - - // add the pattern to the tdata patterns array - tdata.patterns.push(pattern) - - // add the pattern refrerence to the journey object - journey.segments.push({ - type: 'TRANSIT', - patterns: [{ - pattern_id: ptnId, - from_stop_index: 0, - to_stop_index: (leg.intermediateStops.length + 2) - 1 - }] - }) - - patternId++ - } - }) - - // add the routes and stops to the tdata arrays - for (const k in routes) tdata.routes.push(routes[k]) - for (const k in stops) tdata.stops.push(stops[k]) - - // add the journey to the tdata journeys array - tdata.journeys.push(journey) - - // console.log('derived tdata', tdata); - return tdata -} - -export function isBikeshareStation (place) { - return place.place_id.lastIndexOf('bicycle_rent_station') !== -1 -} diff --git a/lib/util/profile.js b/lib/util/profile.js deleted file mode 100644 index ed79ed806..000000000 --- a/lib/util/profile.js +++ /dev/null @@ -1,180 +0,0 @@ -export function filterProfileOptions (response) { - // Filter out similar options. TODO: handle on server? - const optStrs = [] - const filteredIndices = [] - - const filteredProfile = response.otp.profile.filter((option, i) => { - let optStr = option.access.map(a => a.mode).join('/') - if (option.transit) { - optStr += ' to ' + option.transit.map(transit => { - return transit.routes.map(route => route.id).join('/') - }).join(',') - } - if (optStrs.indexOf(optStr) !== -1) return false - optStrs.push(optStr) - filteredIndices.push(i) - return true - }) - - const filteredJourneys = response.otp.journeys.filter((journey, i) => filteredIndices.indexOf(i) !== -1) - - response.otp.profile = filteredProfile - response.otp.journeys = filteredJourneys - return response -} - -/** profileOptionsToItineraries **/ - -export function profileOptionsToItineraries (options, query) { - return options.map(option => optionToItinerary(option, query)) -} - -// helper functions for profileOptionsToItineraries: - -function optionToItinerary (option, query) { - const itin = { - duration: option.time, - legs: [], - walkTime: 0, - waitingTime: 0 - } - - // access leg - if (option.access && option.access.length > 0) { - if (option.access[0].mode === 'BICYCLE_RENT') { - let status = 'WALK_ON' - const walkOnEdges = [] - const bikeEdges = [] - const walkOffEdges = [] - let onStationName - let walkOnTime = 0 - let offStationName - let walkOffTime = 0 - option.access[0].streetEdges.forEach(edge => { - // check if we're returning the bike - if (edge.bikeRentalOffStation) { - status = 'WALK_OFF' - offStationName = edge.bikeRentalOffStation.name - } - - if (status === 'WALK_ON') { - walkOnEdges.push(edge) - walkOnTime += edge.distance - } else if (status === 'BIKE') { - bikeEdges.push(edge) - } else if (status === 'WALK_OFF') { - walkOffEdges.push(edge) - walkOffTime += edge.distance - } - - // check if we're picking up the bike - if (edge.bikeRentalOnStation) { - status = 'BIKE' - onStationName = edge.bikeRentalOnStation.name - } - }) - - itin.walkTime += (walkOnTime + walkOffTime) - - // create the 'on' walk leg - itin.legs.push({ - mode: 'WALK', - duration: walkOnTime, - transitLeg: false, - from: { - name: locationString(query && query.from.name, 'Destination') - }, - to: { - name: onStationName - } - }) - - // create the bike leg - itin.legs.push({ - mode: 'BICYCLE_RENT', - duration: option.time - walkOnTime - walkOffTime, - transitLeg: false, - from: { - name: onStationName - }, - to: { - name: offStationName - } - }) - - // create the 'off' walk leg - itin.legs.push({ - mode: 'WALK', - duration: walkOffTime, - transitLeg: false, - from: { - name: offStationName - }, - to: { - name: locationString(query && query.to.name, 'Destination') - } - }) - } else { - itin.legs.push(accessToLeg(option.access[0], query && query.from.name, option.transit ? null : query && query.to.name)) - if (option.access[0].mode === 'WALK') itin.walkTime += option.access[0].time - } - } - - // transit legs - if (option.transit) { - option.transit.forEach(transit => { - itin.legs.push({ - transitLeg: true, - mode: transit.mode, - from: { - name: transit.fromName - }, - to: { - name: transit.toName - }, - routes: transit.routes, - duration: transit.rideStats.avg, - averageWait: transit.waitStats.avg - }) - itin.waitingTime += transit.waitStats.avg - }) - } - - // egress leg - if (option.egress && option.egress.length > 0) { - // find the origin name, for transit trips - const origin = option.transit ? option.transit[option.transit.length - 1].toName : null - - itin.legs.push(accessToLeg(option.egress[0], origin, query && query.to.name)) - if (option.egress[0].mode === 'WALK') itin.walkTime += option.egress[0].time - } - - // construct summary - if (option.transit) { - itin.summary = 'Transit' - } else { - if (option.modes.length === 1 && option.modes[0] === 'bicycle') itin.summary = 'Bicycle' - else if (option.modes.length === 1 && option.modes[0] === 'walk') itin.summary = 'Walk' - else if (option.modes.indexOf('bicycle_rent') !== -1) itin.summary = 'Bikeshare' - } - - return itin -} - -function accessToLeg (access, origin, destination) { - return { - mode: access.mode, - duration: access.time, - transitLeg: false, - from: { - name: locationString(origin, 'Origin') - }, - to: { - name: locationString(destination, 'Destination') - } - } -} - -function locationString (str, defaultStr) { - return str ? str.split(',')[0] : defaultStr -} diff --git a/lib/util/query-params.js b/lib/util/query-params.js deleted file mode 100644 index 94ebf17e2..000000000 --- a/lib/util/query-params.js +++ /dev/null @@ -1,546 +0,0 @@ -import { - isTransit, - isAccessMode, - isCar, - hasTransit, - hasBike, - hasMicromobility -} from './itinerary' -import { getItem } from './storage' -import { getCurrentDate, getCurrentTime } from './time' - -/** - * name: the default name of the parameter used for internal reference and API calls - * - * routingTypes: array of routing type(s) (ITINERARY, PROFILE, or both) this param applies to - * - * applicable: an optional function (accepting the current full query as a - * parameter) indicating whether this query parameter is applicable to the query. - * (Applicability is assumed if this function is not provided.) - * - * default: the default value for this parameter. The default can be also be a - * function that gets executed when accessing the default value. When the value - * is a funciton, it will take an argument of the current config of the otp-rr - * store. This is needed when a brand new time-dependent value is desired to be - * calculated. It's also helpful for producing tests that have consistent data - * output. - * - * itineraryRewrite: an optional function for translating the key and/or value - * for ITINERARY mode only (e.g. 'to' is rewritten as 'toPlace'). Accepts the - * intial internal value as a function parameter. - * - * profileRewrite: an optional function for translating the value for PROFILE mode - * - * label: a text label for for onscreen display. May either be a text string or a - * function (accepting the current full query as a parameter) returning a string - * - * selector: the default type of UI selector to use in the form. Can be one of: - * - DROPDOWN: a standard drop-down menu selector - * - * options: an array of text/value pairs used with a dropdown selector - * - * TODO: validation system for rewrite functions and/or better user documentation - * TODO: alphabetize below list - */ - -// FIXME: Use for parsing URL values? -// const stringToLocation = string => { -// const split = string.split(',') -// return split.length === 2 -// ? {lat: split[0], lon: split[1]} -// : {lat: null, lon: null} -// } - -const formatPlace = (location, alternateName) => { - if (!location) return null - const name = location.name || `${alternateName || 'Place'} (${location.lat},${location.lon})` - return `${name}::${location.lat},${location.lon}` -} - -// Load stored default query settings from local storage -let storedSettings = getItem('defaultQuery', {}) - -const queryParams = [ - { /* from - the trip origin. stored internally as a location (lat/lon/name) object */ - name: 'from', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: null, - itineraryRewrite: value => ({ fromPlace: formatPlace(value, 'Origin') }), - profileRewrite: value => ({ from: { lat: value.lat, lon: value.lon } }) - // FIXME: Use for parsing URL values? - // fromURL: stringToLocation - }, - - { /* to - the trip destination. stored internally as a location (lat/lon/name) object */ - name: 'to', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: null, - itineraryRewrite: value => ({ toPlace: formatPlace(value, 'Destination') }), - profileRewrite: value => ({ to: { lat: value.lat, lon: value.lon } }) - // FIXME: Use for parsing URL values? - // fromURL: stringToLocation - }, - - { /* date - the date of travel, in MM-DD-YYYY format */ - name: 'date', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: getCurrentDate - }, - - { /* time - the arrival/departure time for an itinerary trip, in HH:mm format */ - name: 'time', - routingTypes: [ 'ITINERARY' ], - default: getCurrentTime - }, - - { /* departArrive - whether this is a depart-at, arrive-by, or leave-now trip */ - name: 'departArrive', - routingTypes: [ 'ITINERARY' ], - default: 'NOW', - itineraryRewrite: value => ({ arriveBy: (value === 'ARRIVE') }) - }, - - { /* startTime - the start time for a profile trip, in HH:mm format */ - name: 'startTime', - routingTypes: [ 'PROFILE' ], - default: '07:00' - }, - - { /* endTime - the end time for a profile trip, in HH:mm format */ - name: 'endTime', - routingTypes: [ 'PROFILE' ], - default: '09:00' - }, - - { /* mode - the allowed modes for a trip, as a comma-separated list */ - name: 'mode', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: 'WALK,TRANSIT', // TODO: make this dependent on routingType? - profileRewrite: value => { - const accessModes = [] - const directModes = [] - const transitModes = [] - - if (value && value.length > 0) { - value.split(',').forEach(m => { - if (isTransit(m)) transitModes.push(m) - if (isAccessMode(m)) { - accessModes.push(m) - // TODO: make configurable whether direct-driving is considered - if (!isCar(m)) directModes.push(m) - } - }) - } - - return { accessModes, directModes, transitModes } - } - }, - - { /* showIntermediateStops - whether response should include intermediate stops for transit legs */ - name: 'showIntermediateStops', - routingTypes: [ 'ITINERARY' ], - default: true - }, - - { /* maxWalkDistance - the maximum distance in meters the user will walk to transit. */ - name: 'maxWalkDistance', - routingTypes: [ 'ITINERARY' ], - applicable: query => query.mode && hasTransit(query.mode) && query.mode.indexOf('WALK') !== -1, - default: 1207, // 3/4 mi. - selector: 'DROPDOWN', - label: 'Maximum Walk', - options: [ - { - text: '1/10 mile', - value: 160.9 - }, { - text: '1/4 mile', - value: 402.3 - }, { - text: '1/2 mile', - value: 804.7 - }, { - text: '3/4 mile', - value: 1207 - }, { - text: '1 mile', - value: 1609 - }, { - text: '2 miles', - value: 3219 - }, { - text: '5 miles', - value: 8047 - } - ] - }, - - { /* maxBikeDistance - the maximum distance in meters the user will bike. Not - * actually an OTP parameter (maxWalkDistance doubles for biking) but we - * store it separately internally in order to allow different default values, - * options, etc. Translated to 'maxWalkDistance' via the rewrite function. - */ - name: 'maxBikeDistance', - routingTypes: [ 'ITINERARY' ], - applicable: query => query.mode && hasTransit(query.mode) && query.mode.indexOf('BICYCLE') !== -1, - default: 4828, // 3 mi. - selector: 'DROPDOWN', - label: 'Maximum Bike', - options: [ - { - text: '1/4 mile', - value: 402.3 - }, { - text: '1/2 mile', - value: 804.7 - }, { - text: '3/4 mile', - value: 1207 - }, { - text: '1 mile', - value: 1609 - }, { - text: '2 miles', - value: 3219 - }, { - text: '3 miles', - value: 4828 - }, { - text: '5 miles', - value: 8047 - }, { - text: '10 miles', - value: 16093 - }, { - text: '20 miles', - value: 32187 - }, { - text: '30 miles', - value: 48280 - } - ], - itineraryRewrite: value => ({ - maxWalkDistance: value, - // ensures that the value is repopulated when loaded from URL params - maxBikeDistance: value - }) - }, - - { /* optimize -- how to optimize a trip (non-bike, non-micromobility trips) */ - name: 'optimize', - applicable: query => hasTransit(query.mode) && !hasBike(query.mode), - routingTypes: [ 'ITINERARY' ], - default: 'QUICK', - selector: 'DROPDOWN', - label: 'Optimize for', - options: [ - { - text: 'Speed', - value: 'QUICK' - }, { - text: 'Fewest Transfers', - value: 'TRANSFERS' - } - ] - }, - - { /* optimizeBike -- how to optimize an bike-based trip */ - name: 'optimizeBike', - applicable: query => hasBike(query.mode), - routingTypes: [ 'ITINERARY' ], - default: 'SAFE', - selector: 'DROPDOWN', - label: 'Optimize for', - options: query => { - const opts = [{ - text: 'Speed', - value: 'QUICK' - }, { - text: 'Bike-Friendly Trip', - value: 'SAFE' - }, { - text: 'Flat Trip', - value: 'FLAT' - }] - - // Include transit-specific option, if applicable - if (hasTransit(query.mode)) { - opts.splice(1, 0, { - text: 'Fewest Transfers', - value: 'TRANSFERS' - }) - } - - return opts - }, - itineraryRewrite: value => ({ optimize: value }) - }, - - { /* maxWalkTime -- the maximum time the user will spend walking in minutes */ - name: 'maxWalkTime', - routingTypes: [ 'PROFILE' ], - default: 15, - selector: 'DROPDOWN', - label: 'Max Walk Time', - applicable: query => query.mode && hasTransit(query.mode) && query.mode.indexOf('WALK') !== -1, - options: [ - { - text: '5 minutes', - value: 5 - }, { - text: '10 minutes', - value: 10 - }, { - text: '15 minutes', - value: 15 - }, { - text: '20 minutes', - value: 20 - }, { - text: '30 minutes', - value: 30 - }, { - text: '45 minutes', - value: 45 - }, { - text: '1 hour', - value: 60 - } - ] - }, - - { /* walkSpeed -- the user's walking speed in m/s */ - name: 'walkSpeed', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: 1.34, - selector: 'DROPDOWN', - label: 'Walk Speed', - applicable: query => query.mode && query.mode.indexOf('WALK') !== -1, - options: [ - { - text: '2 MPH', - value: 0.89 - }, { - text: '3 MPH', - value: 1.34 - }, { - text: '4 MPH', - value: 1.79 - } - ] - }, - - { /* maxBikeTime -- the maximum time the user will spend biking in minutes */ - name: 'maxBikeTime', - routingTypes: [ 'PROFILE' ], - default: 20, - selector: 'DROPDOWN', - label: 'Max Bike Time', - applicable: query => query.mode && hasTransit(query.mode) && query.mode.indexOf('BICYCLE') !== -1, - options: [ - { - text: '5 minutes', - value: 5 - }, { - text: '10 minutes', - value: 10 - }, { - text: '15 minutes', - value: 15 - }, { - text: '20 minutes', - value: 20 - }, { - text: '30 minutes', - value: 30 - }, { - text: '45 minutes', - value: 45 - }, { - text: '1 hour', - value: 60 - } - ] - }, - - { /* bikeSpeed -- the user's bikeSpeed speed in m/s */ - name: 'bikeSpeed', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: 3.58, - selector: 'DROPDOWN', - label: 'Bicycle Speed', - applicable: query => query.mode && query.mode.indexOf('BICYCLE') !== -1, - options: [ - { - text: '6 MPH', - value: 2.68 - }, { - text: '8 MPH', - value: 3.58 - }, { - text: '10 MPH', - value: 4.47 - }, { - text: '12 MPH', - value: 5.36 - } - ] - }, - - { /* maxEScooterDistance - the maximum distance in meters the user will ride - * an E-scooter. Not actually an OTP parameter (maxWalkDistance doubles for - * any non-transit mode except for car) but we store it separately - * internally in order to allow different default values, options, etc. - * Translated to 'maxWalkDistance' via the rewrite function. - */ - name: 'maxEScooterDistance', - routingTypes: [ 'ITINERARY' ], - applicable: query => query.mode && hasTransit(query.mode) && hasMicromobility(query.mode), - default: 4828, // 3 mi. - selector: 'DROPDOWN', - label: 'Maximum E-scooter Distance', - options: [ - { - text: '1/4 mile', - value: 402.3 - }, { - text: '1/2 mile', - value: 804.7 - }, { - text: '3/4 mile', - value: 1207 - }, { - text: '1 mile', - value: 1609 - }, { - text: '2 miles', - value: 3219 - }, { - text: '3 miles', - value: 4828 - }, { - text: '5 miles', - value: 8047 - }, { - text: '10 miles', - value: 16093 - }, { - text: '20 miles', - value: 32187 - }, { - text: '30 miles', - value: 48280 - } - ], - itineraryRewrite: value => ({ - maxWalkDistance: value, - // ensures that the value is repopulated when loaded from URL params - maxEScooterDistance: value - }) - }, - - { /* bikeSpeed -- the user's bikeSpeed speed in m/s */ - name: 'watts', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: 250, - selector: 'DROPDOWN', - label: 'E-scooter Power', - // this configuration should only be allowed for personal E-scooters as these - // settings will be defined by the vehicle type of an E-scooter being rented - applicable: query => ( - query.mode && - query.mode.indexOf('MICROMOBILITY') !== -1 && - query.mode.indexOf('MICROMOBILITY_RENT') === -1 - ), - options: [ - { - text: 'Kid\'s hoverboard (6mph)', - value: 125 - }, { - text: 'Entry-level scooter (11mph)', - value: 250 - }, { - text: 'Robust E-scooter (18mph)', - value: 500 - }, { - text: 'Powerful E-scooter (24mph)', - value: 1500 - } - ], - // rewrite a few other values to add some baseline assumptions about the - // vehicle - itineraryRewrite: value => { - const watts = value - // the maximum cruising and downhill speed. Units in m/s - let maximumMicromobilitySpeed - let weight - // see https://en.wikipedia.org/wiki/Human_body_weight#Average_weight_around_the_world - // estimate is for an average North American human with clothes and stuff - // units are in kg - const TYPICAL_RIDER_WEIGHT = 90 - switch (watts) { - case 125: - // exemplar: Swagtron Turbo 5 hoverboard (https://swagtron.com/product/recertified-swagtron-turbo-five-hoverboard-classic/) - maximumMicromobilitySpeed = 2.8 // ~= 6mph - weight = TYPICAL_RIDER_WEIGHT + 9 - break - case 250: - // exemplar: Xiaomi M365 (https://www.gearbest.com/skateboard/pp_596618.html) - maximumMicromobilitySpeed = 5 // ~= 11.5mph - weight = TYPICAL_RIDER_WEIGHT + 12.5 - break - case 500: - // exemplar: Razor EcoSmart Metro (https://www.amazon.com/Razor-EcoSmart-Metro-Electric-Scooter/dp/B002ZDAEIS?SubscriptionId=AKIAJMXJ2YFJTEDLQMUQ&tag=digitren08-20&linkCode=xm2&camp=2025&creative=165953&creativeASIN=B002ZDAEIS&ascsubtag=15599460143449ocb) - maximumMicromobilitySpeed = 8 // ~= 18mph - weight = TYPICAL_RIDER_WEIGHT + 30 - break - case 1000: - // exemplar: Boosted Rev (https://boostedboards.com/vehicles/scooters/boosted-rev) - maximumMicromobilitySpeed = 11 // ~= 24mph - weight = TYPICAL_RIDER_WEIGHT + 21 - break - } - return {maximumMicromobilitySpeed, watts, weight} - } - }, - - { /* ignoreRealtimeUpdates -- if true, do not use realtime updates in routing */ - name: 'ignoreRealtimeUpdates', - routingTypes: [ 'ITINERARY' ], - default: false - }, - - { /* companies -- tnc companies to query */ - name: 'companies', - routingTypes: [ 'ITINERARY' ], - default: null - }, - - { /* wheelchair -- whether the user requires a wheelchair-accessible trip */ - name: 'wheelchair', - routingTypes: [ 'ITINERARY' ], - default: false, - selector: 'CHECKBOX', - label: 'Wheelchair Accessible', - applicable: (query, config) => { - if (!query.mode || !config.modes) return false - const configModes = (config.modes.accessModes || []).concat(config.modes.transitModes || []) - for (const mode of query.mode.split(',')) { - const configMode = configModes.find(m => m.mode === mode) - if (!configMode || !configMode.showWheelchairSetting) continue - if (configMode.company && (!query.companies || !query.companies.split(',').includes(configMode.company))) continue - return true - } - } - } -] -// Iterate over stored settings and update query param defaults. -// FIXME: this does not get updated if the user defaults are cleared -queryParams.forEach(param => { - if (param.name in storedSettings) { - param.default = storedSettings[param.name] - param.userDefaultOverride = true - } -}) - -export default queryParams diff --git a/lib/util/query.js b/lib/util/query.js deleted file mode 100644 index 5779108c7..000000000 --- a/lib/util/query.js +++ /dev/null @@ -1,206 +0,0 @@ -import qs from 'qs' - -import { getTransitModes, hasTransit, isAccessMode, toSentenceCase } from './itinerary' -import { coordsToString, matchLatLon, stringToCoords } from './map' -import queryParams from './query-params' -import { getActiveSearch } from './state' -import { getCurrentTime, getCurrentDate } from './time' - -/* The list of default parameters considered in the settings panel */ - -export const defaultParams = [ - 'wheelchair', - 'maxWalkDistance', - 'maxWalkTime', - 'walkSpeed', - 'maxBikeDistance', - 'maxBikeTime', - 'bikeSpeed', - 'optimize', - 'optimizeBike', - 'maxEScooterDistance', - 'watts' -] - -/* A function to retrieve a property value from an entry in the query-params - * table, checking for either a static value or a function */ - -export function getQueryParamProperty (paramInfo, property, query) { - return typeof paramInfo[property] === 'function' - ? paramInfo[property](query) - : paramInfo[property] -} - -export function ensureSingleAccessMode (queryModes) { - // Count the number of access modes - const accessCount = queryModes.filter(m => isAccessMode(m)).length - - // If multiple access modes are specified, keep only the first one - if (accessCount > 1) { - const firstAccess = queryModes.find(m => isAccessMode(m)) - queryModes = queryModes.filter(m => !isAccessMode(m) || m === firstAccess) - - // If no access modes are specified, add 'WALK' as the default - } else if (accessCount === 0) { - queryModes.push('WALK') - } - - return queryModes -} - -export function getUrlParams () { - return qs.parse(window.location.href.split('?')[1]) -} - -export function getOtpUrlParams () { - return Object.keys(getUrlParams()).filter(key => !key.startsWith('ui_')) -} - -function findLocationType (location, locations = [], types = ['home', 'work', 'suggested']) { - const match = locations.find(l => matchLatLon(l, location)) - return match && types.indexOf(match.type) !== -1 ? match.type : null -} - -export function summarizeQuery (query, locations = []) { - const from = findLocationType(query.from, locations) || query.from.name.split(',')[0] - const to = findLocationType(query.to, locations) || query.to.name.split(',')[0] - const mode = hasTransit(query.mode) - ? 'Transit' - : toSentenceCase(query.mode) - return `${mode} from ${from} to ${to}` -} - -/** - * Assemble any UI-state properties to be tracked via URL into a single object - * TODO: Expand to include additional UI properties - */ - -export function getUiUrlParams (otpState) { - const activeSearch = getActiveSearch(otpState) - const uiParams = { - ui_activeItinerary: activeSearch ? activeSearch.activeItinerary : 0, - ui_activeSearch: otpState.activeSearchId - } - return uiParams -} - -export function getTripOptionsFromQuery (query, keepPlace = false) { - const options = Object.assign({}, query) - // Delete time/date options and from/to - delete options.time - delete options.departArrive - delete options.date - if (!keepPlace) { - delete options.from - delete options.to - } - return options -} - -/** - * Gets the default query param by executing the default value function with the - * provided otp config if the default value is a function. - */ -function getDefaultQueryParamValue (param, config) { - return typeof param.default === 'function' ? param.default(config) : param.default -} - -/** - * Determines whether the specified query differs from the default query, i.e., - * whether the user has modified any trip options (including mode) from their - * default values. - */ -export function isNotDefaultQuery (query, config) { - const activeModes = query.mode.split(',') - const defaultModes = getTransitModes(config).concat(['WALK']) - let queryIsDifferent = false - const modesEqual = (activeModes.length === defaultModes.length) && - activeModes.sort().every((value, index) => { return value === defaultModes.sort()[index] }) - - if (!modesEqual) { - queryIsDifferent = true - } else { - defaultParams.forEach(param => { - const paramInfo = queryParams.find(qp => qp.name === param) - // Check that the parameter applies to the specified routingType - if (!paramInfo.routingTypes.includes(query.routingType)) return - // Check that the applicability test (if provided) is satisfied - if (typeof paramInfo.applicable === 'function' && - !paramInfo.applicable(query, config)) return - if (query[param] !== getDefaultQueryParamValue(paramInfo, config)) { - queryIsDifferent = true - } - }) - } - return queryIsDifferent -} - -/** - * Get the default query to OTP based on the given config. - * - * @param config the config in the otp-rr store. - */ -export function getDefaultQuery (config) { - const defaultQuery = { routingType: 'ITINERARY' } - queryParams.filter(qp => 'default' in qp).forEach(qp => { - defaultQuery[qp.name] = getDefaultQueryParamValue(qp, config) - }) - return defaultQuery -} - -/** - * Create a otp query based on a the url params. - * - * @param {Object} params An object representing the parsed querystring of url - * params. - * @param config the config in the otp-rr store. - */ -export function planParamsToQuery (params, config) { - const query = {} - for (var key in params) { - switch (key) { - case 'fromPlace': - query.from = parseLocationString(params.fromPlace) - break - case 'toPlace': - query.to = parseLocationString(params.toPlace) - break - case 'arriveBy': - query.departArrive = params.arriveBy === 'true' - ? 'ARRIVE' - : params.arriveBy === 'false' - ? 'DEPART' - : 'NOW' - break - case 'date': - query.date = params.date || getCurrentDate(config) - break - case 'time': - query.time = params.time || getCurrentTime(config) - break - default: - if (!isNaN(params[key])) query[key] = parseFloat(params[key]) - else query[key] = params[key] - } - } - return query -} - -/** - * OTP allows passing a location in the form '123 Main St::lat,lon', so we check - * for the double colon and parse the coordinates accordingly. - */ -function parseLocationString (value) { - const parts = value.split('::') - const coordinates = parts[1] - ? stringToCoords(parts[1]) - : stringToCoords(parts[0]) - const name = parts[1] - ? parts[0] - : coordsToString(coordinates) - return coordinates.length === 2 ? { - name: name || null, - lat: coordinates[0] || null, - lon: coordinates[1] || null - } : null -} diff --git a/lib/util/reverse.js b/lib/util/reverse.js deleted file mode 100644 index 8f8f93409..000000000 --- a/lib/util/reverse.js +++ /dev/null @@ -1,12 +0,0 @@ -// TODO: add reverse geocode for map click -// export async function reversePelias (point) { -// const location = {lon: point.lng, lat: point.lat} -// const apiKey = getConfigProperty('MAPZEN_TURN_BY_TURN_KEY') -// const params = { -// api_key: apiKey, -// ...location -// } -// const url = `https://search.mapzen.com/v1/reverse?${qs.stringify(params)}` -// const response = await fetch(url) -// return await response.json() -// } diff --git a/lib/util/state.js b/lib/util/state.js index 8019564e8..cc22fc24c 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -1,5 +1,8 @@ +import coreUtils from '@opentripplanner/core-utils' import isEqual from 'lodash.isequal' +import { MainPanelContent } from '../actions/ui' + /** * Get the active search object * @param {Object} otpState the OTP state object @@ -135,3 +138,44 @@ export function getShowUserSettings (otpState) { export function getStopViewerConfig (otpState) { return otpState.config.stopViewer } + +/** + * Assemble any UI-state properties to be tracked via URL into a single object + * TODO: Expand to include additional UI properties + */ +export function getUiUrlParams (otpState) { + const activeSearch = getActiveSearch(otpState) + const uiParams = { + ui_activeItinerary: activeSearch ? activeSearch.activeItinerary : 0, + ui_activeSearch: otpState.activeSearchId + } + return uiParams +} + +// Set default title to the original document title (on load) set in index.html +const DEFAULT_TITLE = document.title + +export function getTitle (state) { + // Override title can optionally be provided in config.yml + const { config, ui, user } = state.otp + let title = config.title || DEFAULT_TITLE + const { mainPanelContent, viewedRoute, viewedStop } = ui + switch (mainPanelContent) { + case MainPanelContent.ROUTE_VIEWER: + title += ' | Route' + if (viewedRoute && viewedRoute.routeId) title += ` ${viewedRoute.routeId}` + break + case MainPanelContent.STOP_VIEWER: + title += ' | Stop' + if (viewedStop && viewedStop.stopId) title += ` ${viewedStop.stopId}` + break + default: + const activeSearch = getActiveSearch(state.otp) + if (activeSearch) { + title += ` | ${coreUtils.query.summarizeQuery(activeSearch.query, user.locations)}` + } + break + } + // if (printView) title += ' | Print' + return title +} diff --git a/lib/util/storage.js b/lib/util/storage.js deleted file mode 100644 index 4d9b7d88c..000000000 --- a/lib/util/storage.js +++ /dev/null @@ -1,42 +0,0 @@ -// Prefix to use with local storage keys. -const STORAGE_PREFIX = 'otp' - -/** - * Store a javascript object at the specified key. - */ -export function storeItem (key, object) { - window.localStorage.setItem(`${STORAGE_PREFIX}.${key}`, JSON.stringify(object)) -} - -/** - * Retrieve a javascript object at the specified key. If not found, defaults to - * null or, the optionally provided notFoundValue. - */ -export function getItem (key, notFoundValue = null) { - let itemAsString - try { - itemAsString = window.localStorage.getItem(`${STORAGE_PREFIX}.${key}`) - const json = JSON.parse(itemAsString) - if (json) return json - else return notFoundValue - } catch (e) { - // Catch any errors associated with parsing bad JSON. - console.warn(e, itemAsString) - return notFoundValue - } -} - -/** - * Remove item at specified key. - */ -export function removeItem (key) { - window.localStorage.removeItem(`${STORAGE_PREFIX}.${key}`) -} - -/** - * Generate a random ID. This might not quite be a UUID, but it serves our - * purposes for now. - */ -export function randId () { - return Math.random().toString(36).substr(2, 9) -} diff --git a/lib/util/time.js b/lib/util/time.js deleted file mode 100644 index be7e5964c..000000000 --- a/lib/util/time.js +++ /dev/null @@ -1,89 +0,0 @@ -import moment from 'moment' -import 'moment-timezone' - -// special constants for making sure the following date format is always sent to -// OTP regardless of whatever the user has configured as the display format -export const OTP_API_DATE_FORMAT = 'YYYY-MM-DD' -export const OTP_API_TIME_FORMAT = 'HH:mm' - -/** - * @param {[type]} config the OTP config object found in store - * @return {string} the config-defined time formatter or HH:mm (24-hr time) - */ -export function getTimeFormat (config) { - return (config.dateTime && config.dateTime.timeFormat) - ? config.dateTime.timeFormat - : OTP_API_TIME_FORMAT -} - -export function getDateFormat (config) { - return (config.dateTime && config.dateTime.dateFormat) - ? config.dateTime.dateFormat - : OTP_API_DATE_FORMAT -} - -export function getLongDateFormat (config) { - return (config.dateTime && config.dateTime.longDateFormat) - ? config.dateTime.longDateFormat - : 'D MMMM YYYY' -} - -/** - * Formats an elapsed time duration for display in narrative - * TODO: internationalization - * @param {number} seconds duration in seconds - * @returns {string} formatted text representation - */ -export function formatDuration (seconds) { - const dur = moment.duration(seconds, 'seconds') - let text = '' - if (dur.hours() > 0) text += dur.hours() + ' hr, ' - text += dur.minutes() + ' min' - return text -} - -/** - * Formats a time value for display in narrative - * TODO: internationalization/timezone - * @param {number} ms epoch time value in milliseconds - * @returns {string} formatted text representation - */ -export function formatTime (ms, options) { - return moment(ms + (options && options.offset ? options.offset : 0)) - .format(options && options.format ? options.format : OTP_API_TIME_FORMAT) -} - -/** - * Formats a seconds after midnight value for display in narrative - * @param {number} seconds time since midnight in seconds - * @param {string} timeFormat A valid moment.js time format - * @return {string} formatted text representation - */ -export function formatSecondsAfterMidnight (seconds, timeFormat) { - return moment().startOf('day').seconds(seconds).format(timeFormat) -} - -/** - * Formats current time for use in OTP query - * The conversion to the user's timezone is needed for testing purposes. - */ -export function getCurrentTime () { - return moment().tz(getUserTimezone()).format(OTP_API_TIME_FORMAT) -} - -/** - * Formats current date for use in OTP query - * The conversion to the user's timezone is needed for testing purposes. - */ -export function getCurrentDate (config) { - return moment().tz(getUserTimezone()).format(OTP_API_DATE_FORMAT) -} - -/** - * Get the timezone name that is set for the user that is currently looking at - * this website. Use a bit of hackery to force a specific timezone if in a - * test environment. - */ -export function getUserTimezone () { - return process.env.NODE_ENV === 'test' ? process.env.TZ : moment.tz.guess() -} diff --git a/lib/util/ui.js b/lib/util/ui.js deleted file mode 100644 index bf5be4753..000000000 --- a/lib/util/ui.js +++ /dev/null @@ -1,80 +0,0 @@ -import { MainPanelContent } from '../actions/ui' -import { summarizeQuery } from './query' -import { getActiveSearch } from './state' - -// Set default title to the original document title (on load) set in index.html -const DEFAULT_TITLE = document.title - -export function isMobile () { - // TODO: consider using 3rd-party library? - return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) -} - -/** - * Enables scrolling for a specified selector, while disabling scrolling for all - * other targets. This is adapted from https://stackoverflow.com/a/41601290/915811 - * and intended to fix issues with iOS elastic scrolling, e.g., - * https://github.com/conveyal/trimet-mod-otp/issues/92. - */ -export function enableScrollForSelector (selector) { - const _overlay = document.querySelector(selector) - let _clientY = null // remember Y position on touch start - - _overlay.addEventListener('touchstart', function (event) { - if (event.targetTouches.length === 1) { - // detect single touch - _clientY = event.targetTouches[0].clientY - } - }, false) - - _overlay.addEventListener('touchmove', function (event) { - if (event.targetTouches.length === 1) { - // detect single touch - disableRubberBand(event) - } - }, false) - - function disableRubberBand (event) { - const clientY = event.targetTouches[0].clientY - _clientY - - if (_overlay.scrollTop === 0 && clientY > 0) { - // element is at the top of its scroll - event.preventDefault() - } - - if (isOverlayTotallyScrolled() && clientY < 0) { - // element is at the top of its scroll - event.preventDefault() - } - } - - function isOverlayTotallyScrolled () { - // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions - return _overlay.scrollHeight - _overlay.scrollTop <= _overlay.clientHeight - } -} - -export function getTitle (state) { - // Override title can optionally be provided in config.yml - const { config, ui, user } = state.otp - let title = config.title || DEFAULT_TITLE - const { mainPanelContent, viewedRoute, viewedStop } = ui - switch (mainPanelContent) { - case MainPanelContent.ROUTE_VIEWER: - title += ' | Route' - if (viewedRoute && viewedRoute.routeId) title += ` ${viewedRoute.routeId}` - break - case MainPanelContent.STOP_VIEWER: - title += ' | Stop' - if (viewedStop && viewedStop.stopId) title += ` ${viewedStop.stopId}` - break - default: - const activeSearch = getActiveSearch(state.otp) - if (activeSearch) { - title += ` | ${summarizeQuery(activeSearch.query, user.locations)}` - } - break - } - // if (printView) title += ' | Print' - return title -} diff --git a/package.json b/package.json index 080c59b60..f2ac610dc 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@opentripplanner/vehicle-rental-overlay": "^0.0.20", "@turf/along": "^6.0.1", "bootstrap": "^3.3.7", + "bowser": "^1.9.3", "clone": "^2.1.0", "connected-react-router": "^6.5.2", "copy-to-clipboard": "^3.0.8", diff --git a/yarn.lock b/yarn.lock index 2db3165eb..e2ad479a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3517,6 +3517,11 @@ bottleneck@^2.18.1: resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.4.tgz#63c505687a0ddaf89a6f515225c75e05833bb079" integrity sha512-2poBdvpAGG+dkMVKZqtDhyuMN6JviD81h89W4bfmt3UO7O60F+qf/84V0alNqL8PM1RByl4SZ1fVMu/ZvxkmcA== +bowser@^1.9.3: + version "1.9.4" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" + integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ== + bowser@^2.7.0: version "2.9.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.9.0.tgz#3bed854233b419b9a7422d9ee3e85504373821c9"