From 9ed75cdfe1762d8ab016ca742104f8f554e49f21 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Wed, 16 Jun 2021 17:19:05 -0700 Subject: [PATCH 01/35] feat: display field trip subgroup size in itineraries --- lib/components/admin/call-taker-controls.js | 7 +- .../admin/field-trip-itinerary-group-size.js | 13 +++ .../narrative/default/default-itinerary.js | 5 +- lib/util/state.js | 80 ++++++++++++++++++- 4 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 lib/components/admin/field-trip-itinerary-group-size.js diff --git a/lib/components/admin/call-taker-controls.js b/lib/components/admin/call-taker-controls.js index f4391ef63..9260ed7e2 100644 --- a/lib/components/admin/call-taker-controls.js +++ b/lib/components/admin/call-taker-controls.js @@ -6,6 +6,8 @@ import * as callTakerActions from '../../actions/call-taker' import * as fieldTripActions from '../../actions/field-trip' import * as uiActions from '../../actions/ui' import Icon from '../narrative/icon' +import { isModuleEnabled } from '../../util/state' + import { CallHistoryButton, CallTimeCounter, @@ -124,10 +126,11 @@ class CallTakerControls extends Component { } const mapStateToProps = (state, ownProps) => { + const { config } = state.otp return { callTaker: state.callTaker, - callTakerEnabled: Boolean(state.otp.config.modules.find(m => m.id === 'call')), - fieldTripEnabled: Boolean(state.otp.config.modules.find(m => m.id === 'ft')), + callTakerEnabled: isModuleEnabled({ config, moduleId: 'call' }), + fieldTripEnabled: isModuleEnabled({ config, moduleId: 'ft' }), session: state.callTaker.session } } diff --git a/lib/components/admin/field-trip-itinerary-group-size.js b/lib/components/admin/field-trip-itinerary-group-size.js new file mode 100644 index 000000000..27e3015f7 --- /dev/null +++ b/lib/components/admin/field-trip-itinerary-group-size.js @@ -0,0 +1,13 @@ +import React from 'react' +import { Badge } from 'react-bootstrap' + +import Icon from '../narrative/icon' + +export default function FieldTripGroupSize ({ itinerary }) { + return itinerary.fieldTripGroupSize > 0 && ( + + + {itinerary.fieldTripGroupSize} + + ) +} diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index abd1eac24..6a9a21251 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -1,12 +1,14 @@ import coreUtils from '@opentripplanner/core-utils' import React from 'react' +import FieldTripGroupSize from '../../admin/field-trip-itinerary-group-size' import NarrativeItinerary from '../narrative-itinerary' import ItineraryBody from '../line-itin/connected-itinerary-body' -import ItinerarySummary from './itinerary-summary' import SimpleRealtimeAnnotation from '../simple-realtime-annotation' import { getTotalFareAsString } from '../../../util/state' +import ItinerarySummary from './itinerary-summary' + const { isBicycle, isTransit } = coreUtils.itinerary const { formatDuration, formatTime } = coreUtils.time @@ -176,6 +178,7 @@ export default class DefaultItinerary extends NarrativeItinerary { } + {(active && !expanded) && click to view details diff --git a/lib/util/state.js b/lib/util/state.js index 872df2620..c5c3588d8 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -205,6 +205,74 @@ const hashItinerary = memoize( ) ) +/** + * Returns true if the config has a modules list and one of the items in the + * list has an ID matching the given moduleId + * + * @param {Object} param + * @param {Object} param.config the app-wide config + * @param {string} param.moduleId the desired moduleId to check the existance of + */ +export function isModuleEnabled ({ config, moduleId }) { + return config.modules?.some(moduleConfig => moduleConfig.id === moduleId) +} + +/** + * Calculates the capacity for a field trip group of a given itinerary + * + * @param {Object} param + * @param {Object} param.config The app-wide config + * @param {Object} param.itinerary An OTP itinerary + * @return {number} The maximum size of a field trip group that could + * use this itinerary. + */ +function calculateItineraryFieldTripGroupCapacity ({ + config, + itinerary +}) { + // FIXME implement + return 10 +} + +/** + * Assigns itineraries to field trip subgroups. + */ +function assignItinerariesToFieldTripGroups ({ + config, + fieldTripGroupSize, + itineraries +}) { + if (!isModuleEnabled({ config, moduleId: 'ft' })) { + return itineraries + } + + // logic to add field trip group sizes for each itinerary + const capacityConstrainedItineraries = [] + let remainingGroupSize = fieldTripGroupSize + + for (let i = 0; i < itineraries.length; i++) { + const itinerary = {...itineraries[i]} + + // calculate itinerary capacity + const capacity = calculateItineraryFieldTripGroupCapacity({ + config, + itinerary + }) + + // assign next largest possible field trip subgroup + itinerary.fieldTripGroupSize = Math.min(remainingGroupSize, capacity) + capacityConstrainedItineraries.push(itinerary) + remainingGroupSize -= capacity + + // exit loop if all of field trip group has been assigned an itinerary + if (remainingGroupSize <= 0) { + break + } + } + + return capacityConstrainedItineraries +} + /** * Get the active itineraries for the active search, which is dependent on * whether realtime or non-realtime results should be displayed @@ -218,12 +286,14 @@ export const getActiveItineraries = createSelector( state => state.otp.filter, getActiveSearchRealtimeResponse, state => state.otp.useRealtime, + state => state.callTaker.fieldTrip.groupSize, ( config, nonRealtimeResponse, itinerarySortSettings, realtimeResponse, - useRealtime + useRealtime, + fieldTripGroupSize ) => { // set response to use depending on useRealtime const response = (!nonRealtimeResponse && !realtimeResponse) @@ -255,9 +325,15 @@ export const getActiveItineraries = createSelector( const {direction, type} = sort // If no sort type is provided (e.g., because batch routing is not enabled), // do not sort itineraries (default sort from API response is used). - return !type + const sortedItineraries = !type ? itineraries : itineraries.sort((a, b) => sortItineraries(type, direction, a, b, config)) + + return assignItinerariesToFieldTripGroups({ + config, + fieldTripGroupSize, + itineraries: sortedItineraries + }) } ) From 751c72e467a1f4863449b21cfa2780d4ac0d7471 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Fri, 18 Jun 2021 10:28:36 -0700 Subject: [PATCH 02/35] feat: allow configuration of modal capacities for field trips --- lib/util/state.js | 50 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/lib/util/state.js b/lib/util/state.js index c5c3588d8..e8868d2a9 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -217,6 +217,45 @@ export function isModuleEnabled ({ config, moduleId }) { return config.modules?.some(moduleConfig => moduleConfig.id === moduleId) } +/** + * Returns the config of the specified module if it exists. + * + * @param {Object} config the app-wide config + * @param {string} moduleId the desired id of the module + */ +function getModuleConfig (config, moduleId) { + return config.modules?.find(moduleConfig => moduleConfig.id === moduleId) +} + +const defaultFieldTripModeCapacities = { + 'TRAM': 80, + 'SUBWAY': 120, + 'RAIL': 80, + 'BUS': 40, + 'FERRY': 100, + 'CABLE_CAR': 20, + 'GONDOLA': 10, + 'FUNICULAR': 20 +} +const unknownModeCapacity = 10 + +/** + * Calculates the mode capacity based on the field trip module mode capacities + * (if it exists) or from the above default lookup of mode capacities or if + * given an unknown mode, then the unknownModeCapacity is returned. + * + * @param {Object} config the app-wide config + * @param {string} mode the OTP mode + */ +const getFieldTripGroupCapacityForMode = createSelector( + config => getModuleConfig(config, 'ft')?.modeCapacities, + (config, mode) => mode, + (configModeCapacities, mode) => (configModeCapacities && + configModeCapacities[mode]) || + defaultFieldTripModeCapacities[mode] || + unknownModeCapacity +) + /** * Calculates the capacity for a field trip group of a given itinerary * @@ -230,8 +269,15 @@ function calculateItineraryFieldTripGroupCapacity ({ config, itinerary }) { - // FIXME implement - return 10 + return itinerary.legs.reduce((constrainingLegCapacity, leg) => { + if (!leg.transitLeg) { + return constrainingLegCapacity + } + return Math.min( + constrainingLegCapacity, + getFieldTripGroupCapacityForMode(config, leg.mode) + ) + }, 10000) } /** From e903f1a1d8cdbe6c8b5bb95ad55c095eec158b57 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Fri, 18 Jun 2021 10:49:26 -0700 Subject: [PATCH 03/35] docs: fix spelling error --- lib/util/state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util/state.js b/lib/util/state.js index e8868d2a9..881716115 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -211,7 +211,7 @@ const hashItinerary = memoize( * * @param {Object} param * @param {Object} param.config the app-wide config - * @param {string} param.moduleId the desired moduleId to check the existance of + * @param {string} param.moduleId the desired moduleId to check the existence of */ export function isModuleEnabled ({ config, moduleId }) { return config.modules?.some(moduleConfig => moduleConfig.id === moduleId) From aa0f0d5858a90103d08575a3196ad0436bc68252 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 24 Jun 2021 15:16:04 -0400 Subject: [PATCH 04/35] chore(mastarm): Upgrade to mastarm 5.3.2. --- package.json | 2 +- yarn.lock | 102 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 8bbb5d158..31da91f1b 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "husky": "^6.0.0", "leaflet": "^1.6.0", "lint-staged": "^11.0.0", - "mastarm": "^5.1.3", + "mastarm": "^5.3.2", "nock": "^9.0.9", "pinst": "^2.1.6", "react": "^16.9.0", diff --git a/yarn.lock b/yarn.lock index 007094b27..56d339b5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4271,11 +4271,16 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000986.tgz#0439a4052bbd3243fa01c9998601b9226e7ea6b7" integrity sha512-8SKJ12AFwG0ReMjPwRH+keFsX/ucw2bi6LC7upeXBvxjgrMqHaTxgYhkRGm+eOwUWvVcqXDgqM7QNlRJMhvXZg== -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000941, caniuse-lite@^1.0.30000980, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001135: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000980, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001135: version "1.0.30001203" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001203.tgz" integrity sha512-/I9tvnzU/PHMH7wBPrfDMSuecDeUKerjCPX7D0xBbaJZPxoT9m+yYxt0zCTkcijCkjTdim3H56Zm0i5Adxch4w== +caniuse-lite@^1.0.30001233: + version "1.0.30001239" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001239.tgz#66e8669985bb2cb84ccb10f68c25ce6dd3e4d2b8" + integrity sha512-cyBkXJDMeI4wthy8xJ2FvDU6+0dtcZSJW3voUF8+e9f1bBeuvyZfc3PNbkOETyhbR+dGCPzn9E7MA3iwzusOhQ== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -6577,6 +6582,13 @@ eslint-plugin-react@^7.12.4: prop-types "^15.7.2" resolve "^1.10.1" +eslint-plugin-sort-destructure-keys@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-sort-destructure-keys/-/eslint-plugin-sort-destructure-keys-1.3.5.tgz#c6f45c3e58d4435564025a6ca5f4a838010800fd" + integrity sha512-JmVpidhDsLwZsmRDV7Tf/vZgOAOEQGkLtwToSvX5mD8fuWYS/xkgMRBsalW1fGlc8CgJJwnzropt4oMQ7YCHLg== + dependencies: + natural-compare-lite "^1.4.0" + eslint-plugin-standard@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.0.tgz#f845b45109c99cd90e77796940a344546c8f6b5c" @@ -6788,6 +6800,21 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-2.1.0.tgz#e5d3ecd837d2a60ec50f3da78fd39767747bbe99" + integrity sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^3.0.0" + onetime "^5.1.0" + p-finally "^2.0.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + execa@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" @@ -7323,6 +7350,15 @@ fs-extra@^7.0.0: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" @@ -7574,6 +7610,27 @@ git-log-parser@^1.2.0: through2 "~2.0.0" traverse "~0.6.6" +git-remote-origin-url@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/git-remote-origin-url/-/git-remote-origin-url-3.1.0.tgz#c90c1cb0f66658566bbc900509ab093a1522d2b3" + integrity sha512-yVSfaTMO7Bqk6Xx3696ufNfjdrajX7Ig9GuAeO2V3Ji7stkDoBNFldnWIAsy0qviUd0Z+X2P6ziJENKztW7cBQ== + dependencies: + gitconfiglocal "^2.1.0" + +git-repo-info@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/git-repo-info/-/git-repo-info-2.1.1.tgz#220ffed8cbae74ef8a80e3052f2ccb5179aed058" + integrity sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg== + +git-repo-is-up-to-date@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/git-repo-is-up-to-date/-/git-repo-is-up-to-date-1.1.0.tgz#00e9f5dab56e420e4f6baca4000facdc4bbe7d2a" + integrity sha512-1wmqIbQc9KaK03Szd655uFuZwW9iRcSFb5ail+CMtYy6I7pmbk1XGrewB34hGmJFndHzYr9BtA+mGUYdfWqw1w== + dependencies: + execa "^2.0.4" + git-remote-origin-url "^3.0.0" + git-repo-info "^2.1.0" + git-up@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/git-up/-/git-up-4.0.2.tgz#10c3d731051b366dc19d3df454bfca3f77913a7c" @@ -7589,6 +7646,13 @@ git-url-parse@^11.1.2: dependencies: git-up "^4.0.0" +gitconfiglocal@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-2.1.0.tgz#07c28685c55cc5338b27b5acbcfe34aeb92e43d1" + integrity sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg== + dependencies: + ini "^1.3.2" + github-slugger@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.2.0.tgz#8ada3286fd046d8951c3c952a8d7854cfd90fd9a" @@ -8299,6 +8363,11 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +ini@^1.3.2: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.7" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" @@ -10446,10 +10515,10 @@ marked@^1.0.0: resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.5.tgz#a44b31f2a0b8b5bfd610f00d55d1952d1ac1dfdb" integrity sha512-2AlqgYnVPOc9WDyWu7S5DJaEZsfk6dNh/neatQ3IHUW4QLutM/VPSH9lG7bif+XjFWc9K9XR3QvR+fXuECmfdA== -mastarm@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/mastarm/-/mastarm-5.1.3.tgz#954258f4db34e908dc2e3b923b7caaaa5e24655e" - integrity sha512-tPlZP/DSPMEKmB+j9Bjvu8vATOW4DuI1y0X77vttKUA/e0BsoWsb3imteEP0OAu6eQW47n/PU7EdEodqurdinA== +mastarm@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/mastarm/-/mastarm-5.3.2.tgz#47dbcdfc525f4030070542343eb071c7342063e9" + integrity sha512-8VlPY52fQIvqYb0rYJ/3zPsBgbL1r+j8MttwmJvPBncd0hZU31F3Z+FOUh0UtCbJzfagKRsaZotMeJe8Uz1d7Q== dependencies: "@babel/core" "^7.3.4" "@babel/plugin-proposal-class-properties" "^7.3.4" @@ -10471,7 +10540,7 @@ mastarm@^5.1.3: browserify "^16.2.3" browserify-markdown "2.0.1" budo "^11.6.1" - caniuse-lite "^1.0.30000941" + caniuse-lite "^1.0.30001233" chokidar "^2.1.2" commander "^2.19.0" commitizen "^3.0.7" @@ -10491,10 +10560,14 @@ mastarm@^5.1.3: eslint-plugin-node "^8.0.1" eslint-plugin-promise "^4.0.1" eslint-plugin-react "^7.12.4" + eslint-plugin-sort-destructure-keys "^1.3.5" eslint-plugin-standard "^4.0.0" + execa "^2.0.4" exorcist "^1.0.1" flow-bin "0.84.0" flow-runtime "^0.17.0" + fs-extra "^8.1.0" + git-repo-is-up-to-date "^1.1.0" glob "^7.1.3" isomorphic-fetch "^2.2.1" jest "^24.1.0" @@ -11106,6 +11179,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha1-F7CVgZiJef3a/gIB6TG6kzyWy7Q= + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -11458,6 +11536,13 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" +npm-run-path@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" + integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== + dependencies: + path-key "^3.0.0" + npm-run-path@^4.0.0, npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -11911,6 +11996,11 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= +p-finally@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" + integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== + p-is-promise@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" From 89a92bdfe96c34bed14d93ed9d61600bf2aef3f1 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 24 Jun 2021 16:31:03 -0400 Subject: [PATCH 05/35] style: Apply new eslint rules. --- __tests__/util/itinerary.js | 4 +- lib/actions/api.js | 103 +++++----- lib/actions/call-taker.js | 21 +- lib/actions/field-trip.js | 19 +- lib/actions/map.js | 22 +-- lib/actions/ui.js | 10 +- lib/actions/user.js | 11 +- lib/components/admin/call-record.js | 15 +- lib/components/admin/call-taker-controls.js | 23 +-- lib/components/admin/call-taker-windows.js | 13 +- lib/components/admin/editable-section.js | 4 +- lib/components/admin/field-trip-details.js | 17 +- lib/components/admin/field-trip-list.js | 9 +- lib/components/admin/query-record.js | 3 +- lib/components/admin/trip-status.js | 7 +- lib/components/app/app-menu.js | 7 +- lib/components/app/batch-routing-panel.js | 6 +- lib/components/app/call-taker-panel.js | 46 ++--- lib/components/app/default-main-panel.js | 22 +-- lib/components/app/desktop-nav.js | 3 +- lib/components/app/responsive-webapp.js | 9 +- lib/components/form/batch-preferences.js | 6 +- lib/components/form/batch-settings.js | 9 +- .../form/call-taker/advanced-options.js | 5 +- .../form/call-taker/date-time-options.js | 12 +- .../form/call-taker/mode-dropdown.js | 6 +- lib/components/form/connected-links.js | 2 +- .../form/connected-settings-selector-panel.js | 6 +- lib/components/form/date-time-modal.js | 13 +- lib/components/form/date-time-preview.js | 22 ++- .../form/intermediate-place-field.js | 7 +- lib/components/form/mode-buttons.js | 21 +- lib/components/form/plan-trip-button.js | 6 +- lib/components/form/settings-preview.js | 10 +- lib/components/form/styled copy.js | 184 ++++++++++++++++++ lib/components/form/switch-button.js | 2 +- lib/components/form/trimet.styled.js | 147 ++++++++++++++ lib/components/form/user-settings.js | 72 +++---- lib/components/form/user-trip-settings.js | 6 +- .../map/connected-endpoints-overlay.js | 4 +- .../map/connected-park-and-ride-overlay.js | 4 +- lib/components/map/elevation-point-marker.js | 8 +- lib/components/map/leg-diagram.js | 81 ++++---- lib/components/map/map.js | 10 +- lib/components/map/osm-base-layer.js | 2 +- lib/components/map/set-from-to.js | 5 +- lib/components/map/stylized-map.js | 20 +- lib/components/map/zipcar-overlay.js | 15 +- lib/components/mobile/date-time-screen.js | 8 +- lib/components/mobile/location-search.js | 8 +- lib/components/mobile/main.js | 14 +- lib/components/mobile/navigation-bar.js | 6 +- lib/components/mobile/options-screen.js | 8 +- lib/components/mobile/route-viewer.js | 17 +- lib/components/mobile/search-screen.js | 9 +- lib/components/mobile/stop-viewer.js | 9 +- lib/components/mobile/trip-viewer.js | 9 +- .../narrative/connected-trip-details.js | 4 +- .../narrative/default/access-leg.js | 8 +- .../narrative/default/default-itinerary.js | 25 +-- .../narrative/default/itinerary-details.js | 2 +- .../narrative/default/itinerary-summary.js | 4 +- lib/components/narrative/default/tnc-leg.js | 6 +- .../narrative/default/transit-leg.js | 6 +- lib/components/narrative/icon.js | 2 +- .../narrative/itinerary-carousel.js | 23 +-- .../line-itin/connected-itinerary-body.js | 13 +- lib/components/narrative/loading.js | 5 +- lib/components/narrative/mode-icon.js | 5 +- .../narrative/narrative-itineraries.js | 9 +- .../narrative/narrative-routing-results.js | 12 +- .../narrative/realtime-annotation.js | 12 +- .../narrative/tabbed-itineraries.js | 12 +- lib/components/narrative/trip-tools.js | 8 +- lib/components/user/after-signin-screen.js | 3 +- .../user/monitored-trip/trip-basics-pane.js | 17 +- .../base-renderer.js | 2 +- lib/components/user/new-account-wizard.js | 3 + .../user/notification-prefs-pane.js | 18 +- .../user/places/favorite-place-row.js | 4 +- lib/components/user/places/place-editor.js | 5 +- lib/components/user/user-account-screen.js | 5 +- lib/components/viewers/live-stop-times.js | 5 +- lib/components/viewers/pattern-row.js | 5 +- .../viewers/realtime-status-label.js | 2 +- lib/components/viewers/route-viewer.js | 15 +- lib/components/viewers/stop-schedule-table.js | 2 +- lib/components/viewers/stop-time-cell.js | 2 +- lib/components/viewers/stop-viewer.js | 41 ++-- lib/components/viewers/trip-viewer.js | 9 +- lib/components/viewers/view-trip-button.js | 8 +- lib/reducers/call-taker.js | 16 +- lib/reducers/create-otp-reducer.js | 126 ++++++------ lib/util/auth.js | 2 +- lib/util/call-taker.js | 75 +++---- lib/util/constants.js | 6 +- lib/util/middleware.js | 10 +- lib/util/state.js | 12 +- lib/util/viewer.js | 7 +- 99 files changed, 1039 insertions(+), 664 deletions(-) create mode 100644 lib/components/form/styled copy.js create mode 100644 lib/components/form/trimet.styled.js diff --git a/__tests__/util/itinerary.js b/__tests__/util/itinerary.js index caac6ec17..61452f0be 100644 --- a/__tests__/util/itinerary.js +++ b/__tests__/util/itinerary.js @@ -29,8 +29,8 @@ describe('util > itinerary', () => { rentedVehicle: true } const rideHailLeg = { - mode: 'CAR_HAIL', - hailedCar: true + hailedCar: true, + mode: 'CAR_HAIL' } const testCases = [{ diff --git a/lib/actions/api.js b/lib/actions/api.js index 0bf3a442c..413a01886 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -8,10 +8,11 @@ import queryParams from '@opentripplanner/core-utils/lib/query-params' import { createAction } from 'redux-actions' import qs from 'qs' -import { rememberPlace } from './map' import { getStopViewerConfig, queryIsValid } from '../util/state' import { getSecureFetchOptions } from '../util/middleware' +import { rememberPlace } from './map' + if (typeof (fetch) === 'undefined') require('isomorphic-fetch') const { hasCar } = coreUtils.itinerary @@ -32,19 +33,19 @@ export const forgetSearch = createAction('FORGET_SEARCH') function formatRecentPlace (place) { return { ...place, - type: 'recent', icon: 'clock-o', id: `recent-${randId()}`, - timestamp: new Date().getTime() + timestamp: new Date().getTime(), + type: 'recent' } } function formatRecentSearch (url, otpState) { return { - query: getTripOptionsFromQuery(otpState.currentQuery, true), - url, id: randId(), - timestamp: new Date().getTime() + query: getTripOptionsFromQuery(otpState.currentQuery, true), + timestamp: new Date().getTime(), + url } } @@ -101,7 +102,7 @@ export function routingQuery (searchId = null) { const iterations = otpState.currentQuery.combinations ? otpState.currentQuery.combinations.map(({mode, params}) => ({mode, ...params})) : [{}] - dispatch(routingRequest({ activeItinerary, routingType, searchId, pending: iterations.length })) + dispatch(routingRequest({ activeItinerary, pending: iterations.length, routingType, searchId })) iterations.forEach((injectedParams, i) => { const requestId = randId() // fetch a realtime route @@ -109,16 +110,16 @@ export function routingQuery (searchId = null) { fetch(query, getOtpFetchOptions(state)) .then(getJsonAndCheckResponse) .then(json => { - dispatch(routingResponse({ response: json, requestId, searchId })) + dispatch(routingResponse({ requestId, response: json, searchId })) // If tracking is enabled, store locations and search after successful // search is completed. if (otpState.user.trackRecent) { const { from, to } = otpState.currentQuery if (!isStoredPlace(from)) { - dispatch(rememberPlace({ type: 'recent', location: formatRecentPlace(from) })) + dispatch(rememberPlace({ location: formatRecentPlace(from), type: 'recent' })) } if (!isStoredPlace(to)) { - dispatch(rememberPlace({ type: 'recent', location: formatRecentPlace(to) })) + dispatch(rememberPlace({ location: formatRecentPlace(to), type: 'recent' })) } dispatch(rememberSearch(formatRecentSearch(query, otpState))) } @@ -372,12 +373,12 @@ export function findStop (params) { findStopResponse, findStopError, { - serviceId: 'stops', + noThrottle: true, postprocess: (payload, dispatch) => { dispatch(findRoutesAtStop(params.stopId)) dispatch(findStopTimesForStop(params)) }, - noThrottle: true + serviceId: 'stops' } ) } @@ -493,8 +494,8 @@ export function findStopsForTrip (params) { { rewritePayload: (payload) => { return { - tripId: params.tripId, - stops: payload + stops: payload, + tripId: params.tripId } } } @@ -512,13 +513,13 @@ export function findStopTimesForTrip (params) { findStopTimesForTripResponse, findStopTimesForTripError, { + noThrottle: true, rewritePayload: (payload) => { return { - tripId: params.tripId, - stopTimes: payload + stopTimes: payload, + tripId: params.tripId } - }, - noThrottle: true + } } ) } @@ -535,7 +536,7 @@ export function findGeometryForTrip (params) { findGeometryForTripResponse, findGeometryForTripError, { - rewritePayload: (payload) => ({ tripId, geometry: payload }) + rewritePayload: (payload) => ({ geometry: payload, tripId }) } ) } @@ -578,13 +579,13 @@ export function findStopTimesForStop (params) { findStopTimesForStopResponse, findStopTimesForStopError, { + noThrottle: true, rewritePayload: (stopTimes) => { return { stopId, stopTimes } - }, - noThrottle: true + } } )) } @@ -601,12 +602,12 @@ export function findRoutes (params) { findRoutesResponse, findRoutesError, { - serviceId: 'routes', rewritePayload: (payload) => { const routes = {} payload.forEach(rte => { routes[rte.id] = rte }) return routes - } + }, + serviceId: 'routes' } ) } @@ -666,11 +667,11 @@ export function findRoute (params) { findRouteResponse, findRouteError, { + noThrottle: true, postprocess: (payload, dispatch) => { // load patterns dispatch(findPatternsForRoute({ routeId: params.routeId })) - }, - noThrottle: true + } } ) } @@ -681,24 +682,24 @@ export function findPatternsForRoute (params) { findPatternsForRouteResponse, findPatternsForRouteError, { + postprocess: (payload, dispatch) => { + // load geometry for each pattern + payload.forEach(ptn => { + dispatch(findGeometryForPattern({ + patternId: ptn.id, + routeId: params.routeId + })) + }) + }, rewritePayload: (payload) => { // convert pattern array to ID-mapped object const patterns = {} payload.forEach(ptn => { patterns[ptn.id] = ptn }) return { - routeId: params.routeId, - patterns + patterns, + routeId: params.routeId } - }, - postprocess: (payload, dispatch) => { - // load geometry for each pattern - payload.forEach(ptn => { - dispatch(findGeometryForPattern({ - routeId: params.routeId, - patternId: ptn.id - })) - }) } } ) @@ -717,9 +718,9 @@ export function findGeometryForPattern (params) { { rewritePayload: (payload) => { return { - routeId: params.routeId, + geometry: payload, patternId: params.patternId, - geometry: payload + routeId: params.routeId } } } @@ -788,8 +789,8 @@ export function getTransportationNetworkCompanyEtaEstimate (params) { { rewritePayload: (payload) => { return { - from, - estimates: payload.estimates + estimates: payload.estimates, + from } } } @@ -836,7 +837,11 @@ export function findNearbyStops (params) { receivedNearbyStopsResponse, receivedNearbyStopsError, { - serviceId: 'stops', + // retrieve routes for each stop + postprocess: (stops, dispatch, getState) => { + if (params.max && stops.length > params.max) stops = stops.slice(0, params.max) + stops.forEach(stop => dispatch(findRoutesAtStop(stop.id))) + }, rewritePayload: stops => { if (stops) { // Sort the stops by proximity @@ -851,11 +856,7 @@ export function findNearbyStops (params) { } return {stops} }, - // retrieve routes for each stop - postprocess: (stops, dispatch, getState) => { - if (params.max && stops.length > params.max) stops = stops.slice(0, params.max) - stops.forEach(stop => dispatch(findRoutesAtStop(stop.id))) - } + serviceId: 'stops' } ) } @@ -871,9 +872,9 @@ export function findRoutesAtStop (stopId) { receivedRoutesAtStopResponse, receivedRoutesAtStopError, { - serviceId: 'stops/routes', - rewritePayload: routes => ({ stopId, routes }), - noThrottle: true + noThrottle: true, + rewritePayload: routes => ({ routes, stopId }), + serviceId: 'stops/routes' } ) } @@ -889,8 +890,8 @@ export function findStopsWithinBBox (params) { receivedStopsWithinBBoxResponse, receivedStopsWithinBBoxError, { - serviceId: 'stops', - rewritePayload: stops => ({stops}) + rewritePayload: stops => ({stops}), + serviceId: 'stops' } ) } diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index 61296b466..878487ddb 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -3,12 +3,13 @@ import { serialize } from 'object-to-formdata' import qs from 'qs' import { createAction } from 'redux-actions' -import {toggleFieldTrips} from './field-trip' -import {resetForm} from './form' import {searchToQuery, sessionIsInvalid} from '../util/call-taker' import {URL_ROOT} from '../util/constants' import {getTimestamp} from '../util/state' +import {resetForm} from './form' +import {toggleFieldTrips} from './field-trip' + if (typeof (fetch) === 'undefined') require('isomorphic-fetch') /// PRIVATE ACTIONS @@ -63,14 +64,14 @@ export function endCall () { if (sessionIsInvalid(session)) return // Make POST request to store new call. const callData = serialize({ - sessionId, call: { - startTime: activeCall.startTime, - endTime: getTimestamp() - } + endTime: getTimestamp(), + startTime: activeCall.startTime + }, + sessionId }) fetch(`${otp.config.datastoreUrl}/calltaker/call`, - {method: 'POST', body: callData} + {body: callData, method: 'POST'} ) .then(res => res.json()) .then(id => { @@ -120,7 +121,7 @@ function newSession (datastoreUrl, verifyLoginUrl, redirect) { .then(res => res.json()) .then(data => { const {sessionId: session} = data - const windowUrl = `${verifyLoginUrl}?${qs.stringify({session, redirect})}` + const windowUrl = `${verifyLoginUrl}?${qs.stringify({redirect, session})}` // Redirect to login url. window.location = windowUrl }) @@ -180,9 +181,9 @@ export function saveQueriesForCall (call) { const search = otp.searches[searchId] const query = searchToQuery(search, call, otp.config) const {sessionId} = callTaker.session - const queryData = serialize({ sessionId, query }) + const queryData = serialize({ query, sessionId }) return fetch(`${datastoreUrl}/calltaker/callQuery?sessionId=${sessionId}`, - {method: 'POST', body: queryData} + { body: queryData, method: 'POST' } ) .then(res => res.json()) .catch(err => { diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 50ea0c8be..887922f6b 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -5,10 +5,11 @@ import { serialize } from 'object-to-formdata' import qs from 'qs' import { createAction } from 'redux-actions' +import {getGroupSize, getTripFromRequest, sessionIsInvalid} from '../util/call-taker' + import {routingQuery} from './api' import {toggleCallHistory} from './call-taker' import {resetForm, setQueryParam} from './form' -import {getGroupSize, getTripFromRequest, sessionIsInvalid} from '../util/call-taker' if (typeof (fetch) === 'undefined') require('isomorphic-fetch') @@ -87,12 +88,12 @@ export function addFieldTripNote (request, note) { if (sessionIsInvalid(callTaker.session)) return const {sessionId, username} = callTaker.session const noteData = serialize({ - sessionId, note: {...note, userName: username}, - requestId: request.id + requestId: request.id, + sessionId }) return fetch(`${datastoreUrl}/fieldtrip/addNote`, - {method: 'POST', body: noteData} + {body: noteData, method: 'POST'} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -111,7 +112,7 @@ export function deleteFieldTripNote (request, noteId) { if (sessionIsInvalid(callTaker.session)) return const {sessionId} = callTaker.session return fetch(`${datastoreUrl}/fieldtrip/deleteNote`, - {method: 'POST', body: serialize({ noteId, sessionId })} + {body: serialize({ noteId, sessionId }), method: 'POST'} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -135,7 +136,7 @@ export function editSubmitterNotes (request, submitterNotes) { sessionId }) return fetch(`${datastoreUrl}/fieldtrip/editSubmitterNotes`, - {method: 'POST', body: noteData} + {body: noteData, method: 'POST'} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -276,7 +277,7 @@ export function setRequestGroupSize (request, groupSize) { sessionId }) return fetch(`${datastoreUrl}/fieldtrip/setRequestGroupSize`, - {method: 'POST', body: groupSizeData} + {body: groupSizeData, method: 'POST'} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -300,7 +301,7 @@ export function setRequestPaymentInfo (request, paymentInfo) { sessionId }) return fetch(`${datastoreUrl}/fieldtrip/setRequestPaymentInfo`, - {method: 'POST', body: paymentData} + {body: paymentData, method: 'POST'} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -324,7 +325,7 @@ export function setRequestStatus (request, status) { status }) return fetch(`${datastoreUrl}/fieldtrip/setRequestStatus`, - {method: 'POST', body: statusData} + {body: statusData, method: 'POST'} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { diff --git a/lib/actions/map.js b/lib/actions/map.js index 26cbcbcef..d11e8f7e0 100644 --- a/lib/actions/map.js +++ b/lib/actions/map.js @@ -39,7 +39,7 @@ export function clearLocation (payload) { /** * Handler for @opentripplanner/location-field onLocationSelected */ -export function onLocationSelected ({ locationType, location, resultType }) { +export function onLocationSelected ({ location, locationType, resultType }) { return function (dispatch, getState) { if (resultType === 'CURRENT_LOCATION') { dispatch(setLocationToCurrent({ locationType })) @@ -59,13 +59,13 @@ export function setLocation (payload) { .reverse({ point: payload.location }) .then((location) => { dispatch(settingLocation({ - locationType: payload.locationType, - location + location, + locationType: payload.locationType })) }).catch(err => { dispatch(settingLocation({ - locationType: payload.locationType, - location: payload.location + location: payload.location, + locationType: payload.locationType })) console.warn(err) }) @@ -83,10 +83,10 @@ export function setLocationToCurrent (payload) { const currentPosition = getState().otp.location.currentPosition if (currentPosition.error || !currentPosition.coords) return payload.location = { + category: 'CURRENT_LOCATION', lat: currentPosition.coords.latitude, lon: currentPosition.coords.longitude, - name: '(Current Location)', - category: 'CURRENT_LOCATION' + name: '(Current Location)' } dispatch(settingLocation(payload)) } @@ -97,12 +97,12 @@ export function switchLocations () { const { from, to } = getState().otp.currentQuery // First, reverse the locations. dispatch(settingLocation({ - locationType: 'from', - location: to + location: to, + locationType: 'from' })) dispatch(settingLocation({ - locationType: 'to', - location: from + location: from, + locationType: 'to' })) // Then kick off a routing query (if the query is invalid, search will abort). dispatch(routingQuery()) diff --git a/lib/actions/ui.js b/lib/actions/ui.js index 955515798..9755c0a73 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -3,6 +3,8 @@ import coreUtils from '@opentripplanner/core-utils' import { createAction } from 'redux-actions' import { matchPath } from 'react-router' +import { getUiUrlParams } from '../util/state' + import { findRoute, setUrlSearch } from './api' import { setMapCenter, setMapZoom, setRouterId } from './config' import { @@ -12,7 +14,6 @@ import { } from './form' import { clearLocation } from './map' import { setActiveItinerary } from './narrative' -import { getUiUrlParams } from '../util/state' /** * Wrapper function for history#push (or, if specified, replace, etc.) @@ -49,8 +50,8 @@ export function matchContentToUrl (location) { // https://github.com/ReactTraining/react-router/issues/5870#issuecomment-394194338 const root = location.pathname.split('/')[1] const match = matchPath(location.pathname, { - path: `/${root}/:id`, exact: true, + path: `/${root}/:id`, strict: false }) const id = match && match.params && match.params.id @@ -226,12 +227,17 @@ export const MainPanelContent = { export const MobileScreens = { WELCOME_SCREEN: 1, + // eslint-disable-next-line sort-keys SET_INITIAL_LOCATION: 2, + // eslint-disable-next-line sort-keys SEARCH_FORM: 3, SET_FROM_LOCATION: 4, SET_TO_LOCATION: 5, + // eslint-disable-next-line sort-keys SET_OPTIONS: 6, + // eslint-disable-next-line sort-keys SET_DATETIME: 7, + // eslint-disable-next-line sort-keys RESULTS_SUMMARY: 8 } diff --git a/lib/actions/user.js b/lib/actions/user.js index 67fe1911b..bf437917c 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -5,14 +5,15 @@ import { OTP_API_DATE_FORMAT } from '@opentripplanner/core-utils/lib/time' import qs from 'qs' import { createAction } from 'redux-actions' -import { routingQuery } from './api' -import { setQueryParam } from './form' -import { routeTo } from './ui' import { TRIPS_PATH, URL_ROOT } from '../util/constants' import { secureFetch } from '../util/middleware' import { isBlank } from '../util/ui' import { isNewUser, positionHomeAndWorkFirst } from '../util/user' +import { routeTo } from './ui' +import { setQueryParam } from './form' +import { routingQuery } from './api' + // Middleware API paths. const API_MONITORED_TRIP_PATH = '/api/secure/monitoredtrip' const API_OTPUSER_PATH = '/api/secure/user' @@ -151,7 +152,7 @@ export function createOrUpdateUser (userData, silentOnSuccess = false) { return async function (dispatch, getState) { const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(getState()) const { id } = userData // Middleware ID, NOT auth0 (or similar) id. - let requestUrl, method + let method, requestUrl // Before persisting, filter out entries from userData.savedLocations with blank addresses. userData.savedLocations = userData.savedLocations.filter( @@ -264,7 +265,7 @@ export function createOrUpdateUserMonitoredTrip ( return async function (dispatch, getState) { const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState()) const { id } = tripData - let requestUrl, method + let method, requestUrl // Determine URL and method to use. if (isNew) { diff --git a/lib/components/admin/call-record.js b/lib/components/admin/call-record.js index 7bfe29048..e4d821616 100644 --- a/lib/components/admin/call-record.js +++ b/lib/components/admin/call-record.js @@ -2,11 +2,12 @@ import humanizeDuration from 'humanize-duration' import moment from 'moment' import React, { Component } from 'react' -import CallTimeCounter from './call-time-counter' import Icon from '../narrative/icon' +import {searchToQuery} from '../../util/call-taker' + +import CallTimeCounter from './call-time-counter' import QueryRecord from './query-record' import {CallRecordButton, CallRecordIcon, QueryList} from './styled' -import {searchToQuery} from '../../util/call-taker' /** * Displays information for a particular call record in the Call Taker window. @@ -45,12 +46,12 @@ export default class CallRecord extends Component {
+ type='circle' />
- {' '} + {' '} [Active call]
@@ -60,7 +61,7 @@ export default class CallRecord extends Component { {activeQueries.length > 0 ? activeQueries.map((query, i) => ( - + )) : 'No queries recorded.' } @@ -92,7 +93,7 @@ export default class CallRecord extends Component { ? {call.queries && call.queries.length > 0 ? call.queries.map((query, i) => ( - + )) : 'No queries recorded.' } diff --git a/lib/components/admin/call-taker-controls.js b/lib/components/admin/call-taker-controls.js index f4391ef63..ccc015cc3 100644 --- a/lib/components/admin/call-taker-controls.js +++ b/lib/components/admin/call-taker-controls.js @@ -6,6 +6,7 @@ import * as callTakerActions from '../../actions/call-taker' import * as fieldTripActions from '../../actions/field-trip' import * as uiActions from '../../actions/ui' import Icon from '../narrative/icon' + import { CallHistoryButton, CallTimeCounter, @@ -46,20 +47,20 @@ class CallTakerControls extends Component { // Show stop button if call not in progress. if (this._callInProgress()) { return ( - + ) } // No call is in progress. return ( <> - + marginTop: '6px', + position: 'absolute' + }} + type='plus' /> + ) } @@ -70,9 +71,9 @@ class CallTakerControls extends Component { const { callTakerEnabled, fieldTripEnabled, - session, resetAndToggleCallHistory, - resetAndToggleFieldTrips + resetAndToggleFieldTrips, + session } = this.props // If no valid session is found, do not show calltaker controls. if (!session) return null @@ -137,10 +138,10 @@ const mapDispatchToProps = { endCall: callTakerActions.endCall, fetchCalls: callTakerActions.fetchCalls, fetchFieldTrips: fieldTripActions.fetchFieldTrips, - routingQuery: apiActions.routingQuery, - setMainPanelContent: uiActions.setMainPanelContent, resetAndToggleCallHistory: callTakerActions.resetAndToggleCallHistory, - resetAndToggleFieldTrips: fieldTripActions.resetAndToggleFieldTrips + resetAndToggleFieldTrips: fieldTripActions.resetAndToggleFieldTrips, + routingQuery: apiActions.routingQuery, + setMainPanelContent: uiActions.setMainPanelContent } export default connect(mapStateToProps, mapDispatchToProps)(CallTakerControls) diff --git a/lib/components/admin/call-taker-windows.js b/lib/components/admin/call-taker-windows.js index 56e0ed712..51715c440 100644 --- a/lib/components/admin/call-taker-windows.js +++ b/lib/components/admin/call-taker-windows.js @@ -2,9 +2,10 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import * as callTakerActions from '../../actions/call-taker' +import Icon from '../narrative/icon' + import CallRecord from './call-record' import DraggableWindow from './draggable-window' -import Icon from '../narrative/icon' import {WindowHeader} from './styled' /** @@ -25,17 +26,17 @@ class CallTakerWindows extends Component { {activeCall ? + inProgress + searches={searches} /> : null } {callHistory.calls.data.length > 0 ? callHistory.calls.data.map((call, i) => ( + fetchQueries={fetchQueries} + index={i} + key={i} /> )) :
No calls in history
} diff --git a/lib/components/admin/editable-section.js b/lib/components/admin/editable-section.js index e5b8cf27f..b8bba7975 100644 --- a/lib/components/admin/editable-section.js +++ b/lib/components/admin/editable-section.js @@ -24,7 +24,7 @@ export default class EditableSection extends Component { : '' _onClickSave = data => { - const {request, onChange} = this.props + const {onChange, request} = this.props // Convert all null values received to '', // otherwise they will appear in the backend as the 'null' string. for (const field in data) { @@ -121,7 +121,7 @@ export default class EditableSection extends Component { */ class InputToggle extends Component { render () { - const {inputProps, fieldName, isEditing, options, style, value} = this.props + const {fieldName, inputProps, isEditing, options, style, value} = this.props if (isEditing) { if (options) { return ( diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index 91d2f2af6..0df68a85d 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -6,10 +6,17 @@ import { connect } from 'react-redux' import styled from 'styled-components' import * as fieldTripActions from '../../actions/field-trip' +import Icon from '../narrative/icon' +import { + getGroupSize, + GROUP_FIELDS, + PAYMENT_FIELDS, + TICKET_TYPES +} from '../../util/call-taker' + import DraggableWindow from './draggable-window' import EditableSection from './editable-section' import FieldTripNotes from './field-trip-notes' -import Icon from '../narrative/icon' import { Bold, Button, @@ -25,12 +32,6 @@ import { } from './styled' import TripStatus from './trip-status' import Updatable from './updatable' -import { - getGroupSize, - GROUP_FIELDS, - PAYMENT_FIELDS, - TICKET_TYPES -} from '../../util/call-taker' const WindowHeader = styled(DefaultWindowHeader)` margin-bottom: 0px; @@ -157,7 +158,7 @@ class FieldTripDetails extends Component { } + label={} onUpdate={this._editSubmitterNotes} value={submitterNotes} /> diff --git a/lib/components/admin/field-trip-list.js b/lib/components/admin/field-trip-list.js index 84152aa2d..f3acf5002 100644 --- a/lib/components/admin/field-trip-list.js +++ b/lib/components/admin/field-trip-list.js @@ -3,13 +3,14 @@ import { Badge } from 'react-bootstrap' import { connect } from 'react-redux' import * as fieldTripActions from '../../actions/field-trip' -import DraggableWindow from './draggable-window' import Icon from '../narrative/icon' import Loading from '../narrative/loading' -import {FieldTripRecordButton, WindowHeader} from './styled' import {getVisibleRequests, TABS} from '../../util/call-taker' import {FETCH_STATUS} from '../../util/constants' +import {FieldTripRecordButton, WindowHeader} from './styled' +import DraggableWindow from './draggable-window' + /** * Displays a searchable list of field trip requests in a draggable window. */ @@ -134,8 +135,8 @@ class FieldTripRequestRecord extends Component { } _getStatusIcon = (status) => status - ? - : + ? + : render () { const {active, request} = this.props diff --git a/lib/components/admin/query-record.js b/lib/components/admin/query-record.js index d8fd28c36..278282dd0 100644 --- a/lib/components/admin/query-record.js +++ b/lib/components/admin/query-record.js @@ -5,6 +5,7 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import * as formActions from '../../actions/form' + import {CallRecordButton, CallRecordIcon} from './styled' /** @@ -31,7 +32,7 @@ class QueryRecordLayout extends Component { : moment(params.time, OTP_API_TIME_FORMAT).format(timeFormat) return (
  • - + {time}
    {params.from.name} to {params.to.name} diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index 741b12912..85b5a81e5 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -6,6 +6,8 @@ import { connect } from 'react-redux' import * as fieldTripActions from '../../actions/field-trip' import * as formActions from '../../actions/form' import Icon from '../narrative/icon' +import { getTripFromRequest } from '../../util/call-taker' + import { Bold, Button, @@ -13,7 +15,6 @@ import { Header, Para } from './styled' -import { getTripFromRequest } from '../../util/call-taker' class TripStatus extends Component { _getTrip = () => getTripFromRequest(this.props.request, this.props.outbound) @@ -44,8 +45,8 @@ class TripStatus extends Component { } _getStatusIcon = () => this._getStatus() - ? - : + ? + : _onPlanTrip = () => this.props.planTrip(this.props.request, this.props.outbound) diff --git a/lib/components/app/app-menu.js b/lib/components/app/app-menu.js index 703c0752d..c18504689 100644 --- a/lib/components/app/app-menu.js +++ b/lib/components/app/app-menu.js @@ -6,7 +6,6 @@ import { DropdownButton, MenuItem } from 'react-bootstrap' import { withRouter } from 'react-router' import Icon from '../narrative/icon' - import { MainPanelContent, setMainPanelContent } from '../../actions/ui' // TODO: make menu items configurable via props/config @@ -46,10 +45,10 @@ class AppMenu extends Component {
    )} - noCaret className='app-menu-button' - id='app-menu'> + id='app-menu' + noCaret + title={()}> {languageConfig.routeViewer || 'Route Viewer'} diff --git a/lib/components/app/batch-routing-panel.js b/lib/components/app/batch-routing-panel.js index 5d7125d67..2336a08dd 100644 --- a/lib/components/app/batch-routing-panel.js +++ b/lib/components/app/batch-routing-panel.js @@ -61,12 +61,12 @@ class BatchRoutingPanel extends Component { diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index b731c07da..d0046941e 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -117,8 +117,8 @@ class CallTakerPanel extends Component { const showPlanTripButton = mainPanelContent === 'EDIT_DATETIME' || mainPanelContent === 'EDIT_SETTINGS' const { - departArrive, date, + departArrive, from, intermediatePlaces, mode, @@ -128,26 +128,26 @@ class CallTakerPanel extends Component { const actionText = mobile ? 'tap' : 'click' const {expandAdvanced} = this.state const advancedSearchStyle = { - zIndex: 99999, background: 'white', - position: 'absolute', - right: '0px', + display: expandAdvanced ? undefined : 'none', left: '0px', padding: '0px 8px 5px', - display: expandAdvanced ? undefined : 'none' + position: 'absolute', + right: '0px', + zIndex: 99999 } return ( {/* FIXME: should this be a styled component */}
    { return ( this._addPlace(result, i)} - // FIXME: allow intermediate location type. locationType='to' - inputPlaceholder={`Enter intermediate destination`} + // FIXME: allow intermediate location type. + onLocationCleared={this._removePlace} + onLocationSelected={result => this._addPlace(result, i)} showClearButton={!mobile} /> ) @@ -177,8 +177,8 @@ class CallTakerPanel extends Component { showClearButton={!mobile} />
    + className='switch-button-container' + style={{top: '20px'}}> } />
    this.setState({transitModes})} />
    diff --git a/lib/components/app/default-main-panel.js b/lib/components/app/default-main-panel.js index 2f708b43a..b59c6d068 100644 --- a/lib/components/app/default-main-panel.js +++ b/lib/components/app/default-main-panel.js @@ -24,13 +24,13 @@ class DefaultMainPanel extends Component { return (
    {!activeSearch && !showPlanTripButton && showUserSettings && @@ -42,14 +42,14 @@ class DefaultMainPanel extends Component {
    {showPlanTripButton &&
    + height: 15, + left: 0, + position: 'absolute', + right: 10 + }} /> } {showPlanTripButton &&
    diff --git a/lib/components/app/desktop-nav.js b/lib/components/app/desktop-nav.js index 558f166aa..1914690b0 100644 --- a/lib/components/app/desktop-nav.js +++ b/lib/components/app/desktop-nav.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux' import NavLoginButtonAuth0 from '../user/nav-login-button-auth0.js' import { accountLinks, getAuth0Config } from '../../util/auth' import { DEFAULT_APP_TITLE } from '../../util/constants' + import AppMenu from './app-menu' /** @@ -44,7 +45,7 @@ const DesktopNav = ({ otpConfig }) => { {/* TODO: Reconcile CSS class and inline style. */} -
    +
    diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 8dfd7f934..0a96810b4 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -18,11 +18,9 @@ import * as locationActions from '../../actions/location' import * as mapActions from '../../actions/map' import * as uiActions from '../../actions/ui' import { frame } from '../app/app-frame' -import DesktopNav from './desktop-nav' import { RedirectWithQuery } from '../form/connected-links' import Map from '../map/map' import MobileMain from '../mobile/main' -import PrintLayout from './print-layout' import { getAuth0Config } from '../../util/auth' import { ACCOUNT_PATH, @@ -48,6 +46,9 @@ import SavedTripScreen from '../user/monitored-trip/saved-trip-screen' import UserAccountScreen from '../user/user-account-screen' import withLoggedInUserSupport from '../user/with-logged-in-user-support' +import PrintLayout from './print-layout' +import DesktopNav from './desktop-nav' + const { isMobile } = coreUtils.ui class ResponsiveWebapp extends Component { @@ -188,7 +189,7 @@ class ResponsiveWebapp extends Component { - + {/* Note: the main tag provides a way for users of screen readers to skip to the primary page content. @@ -199,7 +200,7 @@ class ResponsiveWebapp extends Component { {MainControls && } - + {MapWindows && } diff --git a/lib/components/form/batch-preferences.js b/lib/components/form/batch-preferences.js index 29718eac2..c01b7706c 100644 --- a/lib/components/form/batch-preferences.js +++ b/lib/components/form/batch-preferences.js @@ -28,10 +28,10 @@ class BatchPreferences extends Component { {/* FIXME: use these instead? They're currently cut off by the short @@ -98,8 +98,8 @@ class BatchPreferences extends Component { const mapStateToProps = (state, ownProps) => { const { config, currentQuery } = state.otp return { - query: currentQuery, config, + query: currentQuery, showUserSettings: getShowUserSettings(state.otp) } } diff --git a/lib/components/form/batch-settings.js b/lib/components/form/batch-settings.js index 88deaadac..ed864ada7 100644 --- a/lib/components/form/batch-settings.js +++ b/lib/components/form/batch-settings.js @@ -5,10 +5,12 @@ import styled from 'styled-components' import * as apiActions from '../../actions/api' import * as formActions from '../../actions/form' +import Icon from '../narrative/icon' +import { hasValidLocation, getActiveSearch, getShowUserSettings } from '../../util/state' + import BatchPreferences from './batch-preferences' import DateTimeModal from './date-time-modal' import ModeButtons, {MODE_OPTIONS, StyledModeButton} from './mode-buttons' -import Icon from '../narrative/icon' import { BatchPreferencesContainer, DateTimeModalContainer, @@ -18,7 +20,6 @@ import { StyledDateTimePreview } from './batch-styled' import { Dot } from './styled' -import { hasValidLocation, getActiveSearch, getShowUserSettings } from '../../util/state' /** * Simple utility to check whether a list of mode strings contains the provided @@ -144,7 +145,7 @@ class BatchSettings extends Component { {coreUtils.query.isNotDefaultQuery(currentQuery, config) && } - + - + {expanded === 'DATE_TIME' && diff --git a/lib/components/form/call-taker/advanced-options.js b/lib/components/form/call-taker/advanced-options.js index 00ae66942..2e5305570 100644 --- a/lib/components/form/call-taker/advanced-options.js +++ b/lib/components/form/call-taker/advanced-options.js @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/label-has-for */ import { hasBike } from '@opentripplanner/core-utils/lib/itinerary' import {SubmodeSelector} from '@opentripplanner/trip-form' import * as TripFormClasses from '@opentripplanner/trip-form/lib/styled' @@ -131,7 +132,7 @@ export default class AdvancedOptions extends Component { return this.state.routeOptions.filter(o => idList.indexOf(o.value) !== -1) } else { // If route list is not available, default labels to route IDs. - return idList.map(id => ({value: id, label: id})) + return idList.map(id => ({label: id, value: id})) } } @@ -145,7 +146,7 @@ export default class AdvancedOptions extends Component { const label = shortName ? `${shortName}${longName ? ` - ${longName}` : ''}` : longName - return {value, label} + return {label, value} } render () { diff --git a/lib/components/form/call-taker/date-time-options.js b/lib/components/form/call-taker/date-time-options.js index feb48b1e2..f82faf300 100644 --- a/lib/components/form/call-taker/date-time-options.js +++ b/lib/components/form/call-taker/date-time-options.js @@ -9,16 +9,16 @@ import { OverlayTrigger, Tooltip } from 'react-bootstrap' const departureOptions = [ { // Default option. - value: 'NOW', - children: 'Now' + children: 'Now', + value: 'NOW' }, { - value: 'DEPART', - children: 'Depart at' + children: 'Depart at', + value: 'DEPART' }, { - value: 'ARRIVE', - children: 'Arrive by' + children: 'Arrive by', + value: 'ARRIVE' } ] diff --git a/lib/components/form/call-taker/mode-dropdown.js b/lib/components/form/call-taker/mode-dropdown.js index 1a43063b4..f9ac748f7 100644 --- a/lib/components/form/call-taker/mode-dropdown.js +++ b/lib/components/form/call-taker/mode-dropdown.js @@ -30,12 +30,12 @@ export default class ModeDropdown extends Component { _getModeOptions = () => { const {modes} = this.props return [ - { mode: 'TRANSIT', value: 'TRANSIT', children: 'Transit' }, + { children: 'Transit', mode: 'TRANSIT', value: 'TRANSIT' }, ...modes.exclusiveModes.map(mode => - ({ mode, value: `${mode}`, children: `${toSentenceCase(mode)} only` }) + ({ children: `${toSentenceCase(mode)} only`, mode, value: `${mode}` }) ), ...modes.accessModes.map(m => - ({ ...m, value: `TRANSIT,${m.mode}`, children: m.label }) + ({ ...m, children: m.label, value: `TRANSIT,${m.mode}` }) ) ] } diff --git a/lib/components/form/connected-links.js b/lib/components/form/connected-links.js index d7fd9da77..1255b2bcb 100644 --- a/lib/components/form/connected-links.js +++ b/lib/components/form/connected-links.js @@ -33,7 +33,7 @@ const mapStateToProps = (state, ownProps) => { // Enhance routing components, connect the result to redux, // and export. export default { - LinkWithQuery: connect(mapStateToProps)(withQueryParams(Link)), LinkContainerWithQuery: connect(mapStateToProps)(withQueryParams(LinkContainer)), + LinkWithQuery: connect(mapStateToProps)(withQueryParams(Link)), RedirectWithQuery: connect(mapStateToProps)(withQueryParams(Redirect)) } diff --git a/lib/components/form/connected-settings-selector-panel.js b/lib/components/form/connected-settings-selector-panel.js index 7ca2aa5b9..21d8c6658 100644 --- a/lib/components/form/connected-settings-selector-panel.js +++ b/lib/components/form/connected-settings-selector-panel.js @@ -29,10 +29,10 @@ class ConnectedSettingsSelectorPanel extends Component {
    @@ -45,8 +45,8 @@ class ConnectedSettingsSelectorPanel extends Component { const mapStateToProps = (state, ownProps) => { const { config, currentQuery } = state.otp return { - query: currentQuery, config, + query: currentQuery, showUserSettings: getShowUserSettings(state.otp) } } diff --git a/lib/components/form/date-time-modal.js b/lib/components/form/date-time-modal.js index 5dbcaaf58..3d455e8b4 100644 --- a/lib/components/form/date-time-modal.js +++ b/lib/components/form/date-time-modal.js @@ -28,15 +28,15 @@ class DateTimeModal extends Component { `. // These props are not relevant in modern browsers, // where `` already // formats the time|date according to the OS settings. - dateFormatLegacy={dateFormatLegacy} + time={time} timeFormatLegacy={timeFormatLegacy} />
    @@ -46,16 +46,17 @@ class DateTimeModal extends Component { } const mapStateToProps = (state, ownProps) => { - const { departArrive, date, time } = state.otp.currentQuery + const { date, departArrive, time } = state.otp.currentQuery const config = state.otp.config return { config, - departArrive, date, + departArrive, time, // These props below are for legacy browsers (see render method above). - timeFormatLegacy: coreUtils.time.getTimeFormat(config), - dateFormatLegacy: coreUtils.time.getDateFormat(config) + // eslint-disable-next-line sort-keys + dateFormatLegacy: coreUtils.time.getDateFormat(config), + timeFormatLegacy: coreUtils.time.getTimeFormat(config) } } diff --git a/lib/components/form/date-time-preview.js b/lib/components/form/date-time-preview.js index 1422baa80..145e2e6bf 100644 --- a/lib/components/form/date-time-preview.js +++ b/lib/components/form/date-time-preview.js @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import moment from 'moment' import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' @@ -6,10 +8,10 @@ import { Button } from 'react-bootstrap' import { connect } from 'react-redux' const { - OTP_API_DATE_FORMAT, - OTP_API_TIME_FORMAT, + getDateFormat, getTimeFormat, - getDateFormat + OTP_API_DATE_FORMAT, + OTP_API_TIME_FORMAT } = coreUtils.time class DateTimePreview extends Component { @@ -93,18 +95,18 @@ class DateTimePreview extends Component { } const mapStateToProps = (state, ownProps) => { - const { departArrive, date, time, routingType, startTime, endTime } = state.otp.currentQuery + const { date, departArrive, endTime, routingType, startTime, time } = state.otp.currentQuery const config = state.otp.config return { config, - routingType, - departArrive, date, - time, - startTime, + dateFormat: getDateFormat(config), + departArrive, endTime, - timeFormat: getTimeFormat(config), - dateFormat: getDateFormat(config) + routingType, + startTime, + time, + timeFormat: getTimeFormat(config) } } diff --git a/lib/components/form/intermediate-place-field.js b/lib/components/form/intermediate-place-field.js index 59c1650ba..c0382de5c 100644 --- a/lib/components/form/intermediate-place-field.js +++ b/lib/components/form/intermediate-place-field.js @@ -11,6 +11,7 @@ import React, {Component} from 'react' import styled from 'styled-components' import * as mapActions from '../../actions/map' + import connectLocationField from './connect-location-field' const StyledIntermediatePlace = styled(LocationField)` @@ -61,7 +62,7 @@ const StyledIntermediatePlace = styled(LocationField)` class IntermediatePlaceField extends Component { _removeIntermediatePlace = () => { const {index, location, onLocationCleared} = this.props - onLocationCleared && onLocationCleared({location, index}) + onLocationCleared && onLocationCleared({index, location}) } render () { @@ -69,8 +70,8 @@ class IntermediatePlaceField extends Component { return ( + clearLocation={this._removeIntermediatePlace} + locationType={`intermediate-place-${index}`} /> ) } } diff --git a/lib/components/form/mode-buttons.js b/lib/components/form/mode-buttons.js index 110d4409f..eed382be2 100644 --- a/lib/components/form/mode-buttons.js +++ b/lib/components/form/mode-buttons.js @@ -4,29 +4,30 @@ import styled from 'styled-components' import Icon from '../narrative/icon' import { ComponentContext } from '../../util/contexts' + import {buttonCss} from './batch-styled' export const MODE_OPTIONS = [ { - mode: 'TRANSIT', - label: 'Transit' + label: 'Transit', + mode: 'TRANSIT' }, { - mode: 'WALK', - label: 'Walking' + label: 'Walking', + mode: 'WALK' }, { - mode: 'CAR', - label: 'Drive' + label: 'Drive', + mode: 'CAR' }, { - mode: 'BICYCLE', - label: 'Bicycle' + label: 'Bicycle', + mode: 'BICYCLE' }, { icon: 'mobile', - mode: 'RENT', // TODO: include HAIL? - label: 'Rental options' + label: 'Rental options', + mode: 'RENT' // TODO: include HAIL? } ] diff --git a/lib/components/form/plan-trip-button.js b/lib/components/form/plan-trip-button.js index e2a091d0c..22ad77469 100644 --- a/lib/components/form/plan-trip-button.js +++ b/lib/components/form/plan-trip-button.js @@ -9,11 +9,11 @@ import { setMainPanelContent } from '../../actions/ui' class PlanTripButton extends Component { static propTypes = { - routingType: PropTypes.string, - text: PropTypes.string, onClick: PropTypes.func, planTrip: PropTypes.func, - profileTrip: PropTypes.func + profileTrip: PropTypes.func, + routingType: PropTypes.string, + text: PropTypes.string } static defaultProps = { diff --git a/lib/components/form/settings-preview.js b/lib/components/form/settings-preview.js index 8e8d26ba5..5fcd312e0 100644 --- a/lib/components/form/settings-preview.js +++ b/lib/components/form/settings-preview.js @@ -1,22 +1,26 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ 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 { Dot } from './styled' import { mergeMessages } from '../../util/messages' +import { Dot } from './styled' + class SettingsPreview extends Component { static propTypes = { // component props caret: PropTypes.string, compressed: PropTypes.bool, editButtonText: PropTypes.element, - showCaret: PropTypes.bool, onClick: PropTypes.func, + showCaret: PropTypes.bool, // application state + // eslint-disable-next-line sort-keys companies: PropTypes.string, modeGroups: PropTypes.array, queryModes: PropTypes.array @@ -30,7 +34,7 @@ class SettingsPreview extends Component { } render () { - const { caret, config, query, editButtonText } = this.props + const { caret, config, editButtonText, query } = this.props const messages = mergeMessages(SettingsPreview.defaultProps, this.props) // Show dot indicator if the current query differs from the default query. const showDot = coreUtils.query.isNotDefaultQuery(query, config) diff --git a/lib/components/form/styled copy.js b/lib/components/form/styled copy.js new file mode 100644 index 000000000..0ff6705a9 --- /dev/null +++ b/lib/components/form/styled copy.js @@ -0,0 +1,184 @@ +import styled, { css } from 'styled-components' +import { DateTimeSelector, SettingsSelectorPanel } from '@opentripplanner/trip-form' +import * as TripFormClasses from '@opentripplanner/trip-form/lib/styled' + +const commonButtonCss = css` + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + background: none; + font-family: inherit; + user-select: none; + text-align: center; + touch-action: manipulation; +` + +const commonInputCss = css` + background-color: #fff; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + color: #555; + font-family: inherit; + padding: 6px 12px; + transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; + + &:focus { + border-color: #66afe9; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); + outline: 0; + } +` + +const modeButtonButtonCss = css` + ${TripFormClasses.ModeButton.Button} { + ${commonButtonCss} + background-color: #fff; + border: 1px solid #ccc; + border-radius: 4px; + color: #333; + font-weight: 400; + font-size: 14px; + line-height: 1.42857143; + outline-offset:-2px; + padding: 6px 12px; + &.active { + background-color: #e6e6e6; + border-color: #adadad; + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + font-weight: 400; + } + &:hover { + background-color: #e6e6e6; + border-color: #adadad; + } + &.active { + background-color: #e6e6e6; + border-color: #adadad; + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + font-weight: 400; + &:hover { + background-color: #d4d4d4; + border-color: #8c8c8c; + } + } + } +` + +export const StyledSettingsSelectorPanel = styled(SettingsSelectorPanel)` + ${TripFormClasses.SettingLabel} { + font-weight: 400; + margin-bottom: 0; + } + ${TripFormClasses.SettingsHeader} { + font-size: 18px; + margin: 16px 0px; + } + ${TripFormClasses.SettingsSection} { + margin-bottom: 16px; + } + ${TripFormClasses.DropdownSelector} { + margin-bottom:20px; + select { + ${commonInputCss} + font-size: 14px; + height: 34px; + line-height: 1.42857143; + } + } + + ${TripFormClasses.ModeSelector} { + ${TripFormClasses.ModeButton.Button} { + ${commonButtonCss} + border: 1px solid rgb(187, 187, 187); + border-radius: 3px; + box-shadow: none; + outline: 0; + padding: 3px; + &.active { + background-color: rgb(173, 216, 230); + border: 2px solid rgb(0, 0, 0); + } + } + ${TripFormClasses.ModeButton.Title} { + font-size: 10px; + font-weight: 300; + line-height: 12px; + padding: 4px 0px 0px; + &.active { + font-weight: 600; + } + } + } + ${TripFormClasses.ModeSelector.MainRow} { + margin: 0 -10px 18px; + padding: 0 5px; + + ${TripFormClasses.ModeButton.Button} { + font-size: 200%; + font-weight: 300; + height: 54px; + &.active { + font-weight: 600; + } + } + } + ${TripFormClasses.ModeSelector.SecondaryRow} { + margin: 0 -10px 10px; + ${TripFormClasses.ModeButton.Button} { + font-size: 150%; + font-weight: 600; + height: 46px; + } + } + ${TripFormClasses.ModeSelector.TertiaryRow} { + margin: 0 -10px 10px; + ${TripFormClasses.ModeButton.Button} { + font-size: 90%; + height: 36px; + } + } + + ${TripFormClasses.SubmodeSelector.Row} { + > * { + padding: 3px 5px 3px 0px; + } + > :last-child { + padding-right: 0px; + } + ${TripFormClasses.ModeButton.Button} { + padding: 6px 12px; + } + svg, + img { + margin-left: 0px; + } + } + ${TripFormClasses.SubmodeSelector.InlineRow} { + margin: -3px 0px; + } + + ${TripFormClasses.SubmodeSelector} { + ${modeButtonButtonCss} + } +` + +export const StyledDateTimeSelector = styled(DateTimeSelector)` + margin: 0 -15px 20px; + ${TripFormClasses.DateTimeSelector.DateTimeRow} { + margin-top: 20px; + } + + input { + ${commonInputCss} + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; + box-shadow: none; + font-size: 16px; + height: 34px; + text-align: center; /* For legacy browsers. */ + } + + ${modeButtonButtonCss} +` diff --git a/lib/components/form/switch-button.js b/lib/components/form/switch-button.js index 0508a0303..afa9ef342 100644 --- a/lib/components/form/switch-button.js +++ b/lib/components/form/switch-button.js @@ -23,8 +23,8 @@ class SwitchButton extends Component { const { content } = this.props return ( ) } diff --git a/lib/components/form/trimet.styled.js b/lib/components/form/trimet.styled.js new file mode 100644 index 000000000..a3b40c96e --- /dev/null +++ b/lib/components/form/trimet.styled.js @@ -0,0 +1,147 @@ +import React from 'react' +import styled from 'styled-components' + +import * as TripFormClasses from '../styled' + +/** + * This file is provided as an illustrative example for custom styling. + */ + +import './trimet-mock.css' // Downloads the font. + +const TriMetStyled = styled.div` + font-family: Hind, sans-serif; + font-size: 14px; + background-color: #f0f0f0; + padding: 15px; + + ${TripFormClasses.SettingsHeader} { + color: #333333; + font-size: 18px; + margin: 16px 0px; + } + ${TripFormClasses.SettingsSection} { + margin-bottom: 16px; + } + ${TripFormClasses.SettingLabel} { + padding-top: 8px; + color: #808080; + font-weight: 100; + text-transform: uppercase; + letter-spacing: 1px; + } + ${TripFormClasses.ModeButton.Button} { + border: 1px solid rgb(187, 187, 187); + padding: 3px; + border-radius: 3px; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + background: none; + outline: none; + + &.active { + border: 2px solid rgb(0, 0, 0); + background-color: rgb(173, 216, 230); + font-weight: 600; + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + } + } + ${TripFormClasses.ModeButton.Title} { + padding: 4px 0px 0px; + font-size: 10px; + line-height: 12px; + + &.active { + text-decoration: underline; + } + } + ${TripFormClasses.DateTimeSelector.DateTimeRow} { + margin: 15px 0px; + input { + padding: 6px 12px; + text-align: center; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + border: 0; + border-bottom: 1px solid #000; + } + } + ${TripFormClasses.DropdownSelector} { + select { + -webkit-appearance: none; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + margin-bottom: 15px; + background: none; + border-radius: 3px; + padding: 6px 12px; + border: 1px solid #ccc; + height: 34px; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + line-height: 1.42857; + color: #555; + } + > div:last-child::after { + content: "▼"; + font-size: 75%; + color: #000; + right: 8px; + top: 10px; + position: absolute; + pointer-events: none; + box-sizing: border-box; + } + } + ${TripFormClasses.SubmodeSelector.Row} { + font-size: 85%; + > * { + padding: 3px 5px 3px 0px; + } + > :last-child { + padding-right: 0px; + } + button { + padding: 6px 12px; + } + svg, + img { + margin-left: 0px; + } + } + ${TripFormClasses.SubmodeSelector.InlineRow} { + margin: -3px 0px; + } + ${TripFormClasses.ModeSelector.MainRow} { + padding: 0px 5px; + font-size: 200%; + margin-bottom: 18px; + box-sizing: border-box; + > * { + width: 100%; + height: 55px; + } + } + ${TripFormClasses.ModeSelector.SecondaryRow} { + margin-bottom: 10px; + > * { + font-size: 150%; + height: 46px; + } + } + ${TripFormClasses.ModeSelector.TertiaryRow} { + font-size: 90%; + margin-bottom: 10px; + text-align: center; + > * { + height: 36px; + } + } +` + +const trimet = contents => {contents} + +export default trimet diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 71b80e41a..d68b22e8c 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -10,14 +10,14 @@ import { setQueryParam } from '../../actions/form' import { forgetPlace, forgetStop, setLocation } from '../../actions/map' import { setViewedStop } from '../../actions/ui' -const { getDetailText, formatStoredPlaceName, matchLatLon } = coreUtils.map +const { formatStoredPlaceName, getDetailText, matchLatLon } = coreUtils.map const { summarizeQuery } = coreUtils.query const BUTTON_WIDTH = 40 class UserSettings extends Component { _disableTracking = () => { - const { user, toggleTracking } = this.props + const { toggleTracking, user } = this.props if (!user.trackRecent) return const hasRecents = user.recentPlaces.length > 0 || user.recentSearches.length > 0 // If user has recents and does not confirm deletion, return without doing @@ -35,20 +35,20 @@ class UserSettings extends Component { const locations = [...user.locations] if (!locations.find(l => l.type === 'work')) { locations.push({ - id: 'work', - type: 'work', + blank: true, icon: 'briefcase', + id: 'work', name: 'click to add', - blank: true + type: 'work' }) } if (!locations.find(l => l.type === 'home')) { locations.push({ - id: 'home', - type: 'home', + blank: true, icon: 'home', + id: 'home', name: 'click to add', - blank: true + type: 'home' }) } return locations @@ -56,7 +56,7 @@ class UserSettings extends Component { render () { const { storageDisclaimer, user } = this.props - const { favoriteStops, trackRecent, recentPlaces, recentSearches } = user + const { favoriteStops, recentPlaces, recentSearches, trackRecent } = user // Clone locations in order to prevent blank locations from seeping into the // app state/store. const locations = this._getLocations(user) @@ -110,15 +110,15 @@ class UserSettings extends Component {
    My preferences
    Remember recent searches/places? + bsStyle='link' + className={trackRecent ? 'active' : ''} + onClick={this._enableTracking}>Yes + bsStyle='link' + className={!trackRecent ? 'active' : ''} + onClick={this._disableTracking}>No
    {storageDisclaimer &&
    @@ -145,13 +145,13 @@ class Place extends Component { !query.from || !matchLatLon(location, query.from) ) ) { - setLocation({ locationType: 'to', location }) + setLocation({ location, locationType: 'to' }) } else if ( // Vice versa for setting as 'from'. !query.from && !matchLatLon(location, query.to) ) { - setLocation({ locationType: 'from', location }) + setLocation({ location, locationType: 'from' }) } } } @@ -186,10 +186,10 @@ class Place extends Component {
  • {showView && + title='View stop'> } {showForget && + bsStyle='link' + className='place-clear' + onClick={this._onForget} + style={{ width: `${BUTTON_WIDTH}px` }}>Clear }
  • ) @@ -238,24 +238,24 @@ class RecentSearch extends Component {
  • + bsStyle='link' + onClick={this._onForget} + style={{ paddingTop: '6px', width: `${BUTTON_WIDTH}px` }}>Clear
  • ) } @@ -277,9 +277,9 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - forgetStop, forgetPlace, forgetSearch, + forgetStop, setLocation, setQueryParam, setViewedStop, diff --git a/lib/components/form/user-trip-settings.js b/lib/components/form/user-trip-settings.js index 130992ccc..0844203ba 100644 --- a/lib/components/form/user-trip-settings.js +++ b/lib/components/form/user-trip-settings.js @@ -38,10 +38,10 @@ class UserTripSettings extends Component { const rememberIsDisabled = queryIsDefault && !defaults return ( -
    +
    diff --git a/lib/components/map/osm-base-layer.js b/lib/components/map/osm-base-layer.js index 0bea38444..a7a28a410 100644 --- a/lib/components/map/osm-base-layer.js +++ b/lib/components/map/osm-base-layer.js @@ -5,9 +5,9 @@ export default class OsmBaseLayer extends Component { render () { return ( ) } diff --git a/lib/components/map/set-from-to.js b/lib/components/map/set-from-to.js index 80d383bb6..163d82a9c 100644 --- a/lib/components/map/set-from-to.js +++ b/lib/components/map/set-from-to.js @@ -1,13 +1,12 @@ import React, { Component } from 'react' - import FromToLocationPicker from '@opentripplanner/from-to-location-picker' export default class SetFromToButtons extends Component { _setLocation = (type) => { this.props.setLocation({ - type, location: this.props.location, - reverseGeocode: false + reverseGeocode: false, + type }) this.props.map.closePopup() } diff --git a/lib/components/map/stylized-map.js b/lib/components/map/stylized-map.js index 48a8ee794..e7fc3b7de 100644 --- a/lib/components/map/stylized-map.js +++ b/lib/components/map/stylized-map.js @@ -21,9 +21,9 @@ STYLES.places = { } }, fill: '#fff', + r: 8, stroke: '#000', - 'stroke-width': 2, - r: 8 + 'stroke-width': 2 } STYLES.stops_merged = { @@ -47,17 +47,17 @@ class StylizedMap extends Component { componentDidMount () { const el = document.getElementById('trn-canvas') this._transitive = new Transitive({ - el, display: 'svg', - styles: STYLES, + el, gridCellSize: 200, + styles: STYLES, zoomFactors: [ { - minScale: 0, + angleConstraint: 45, gridCellSize: 300, internalVertexFactor: 1000000, - angleConstraint: 45, - mergeVertexThreshold: 200 + mergeVertexThreshold: 200, + minScale: 0 } ] }) @@ -99,7 +99,7 @@ class StylizedMap extends Component { return (
    ) } @@ -127,9 +127,9 @@ const mapStateToProps = (state, ownProps) => { } return { - transitiveData, activeItinerary: activeSearch && activeSearch.activeItinerary, - routingType: activeSearch && activeSearch.query && activeSearch.query.routingType + routingType: activeSearch && activeSearch.query && activeSearch.query.routingType, + transitiveData } } diff --git a/lib/components/map/zipcar-overlay.js b/lib/components/map/zipcar-overlay.js index 399831bfb..c8dbdcac9 100644 --- a/lib/components/map/zipcar-overlay.js +++ b/lib/components/map/zipcar-overlay.js @@ -4,18 +4,19 @@ import { connect } from 'react-redux' import { FeatureGroup, MapLayer, Marker, Popup, withLeaflet } from 'react-leaflet' import { divIcon } from 'leaflet' -import SetFromToButtons from './set-from-to' import { setLocation } from '../../actions/map' import { zipcarLocationsQuery } from '../../actions/zipcar' +import SetFromToButtons from './set-from-to' + const zipcarIcon = 'zipcar-icon' class ZipcarOverlay extends MapLayer { static propTypes = { api: PropTypes.string, locations: PropTypes.array, - zipcarLocationsQuery: PropTypes.func, - setLocation: PropTypes.func + setLocation: PropTypes.func, + zipcarLocationsQuery: PropTypes.func } _startRefreshing () { @@ -65,10 +66,10 @@ class ZipcarOverlay extends MapLayer { if (!locations || locations.length === 0) return const markerIcon = divIcon({ - iconSize: [24, 24], - popupAnchor: [0, -12], + className: '', html: zipcarIcon, - className: '' + iconSize: [24, 24], + popupAnchor: [0, -12] }) const bulletIconStyle = { @@ -106,12 +107,12 @@ class ZipcarOverlay extends MapLayer { {/* Set as from/to toolbar */}
    diff --git a/lib/components/mobile/date-time-screen.js b/lib/components/mobile/date-time-screen.js index b80a96a99..969235c41 100644 --- a/lib/components/mobile/date-time-screen.js +++ b/lib/components/mobile/date-time-screen.js @@ -2,13 +2,13 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import MobileContainer from './container' -import MobileNavigationBar from './navigation-bar' import DateTimeModal from '../form/date-time-modal' import PlanTripButton from '../form/plan-trip-button' - import { MobileScreens, setMobileScreen } from '../../actions/ui' +import MobileNavigationBar from './navigation-bar' +import MobileContainer from './container' + class MobileDateTimeScreen extends Component { static propTypes = { setMobileScreen: PropTypes.func @@ -22,9 +22,9 @@ class MobileDateTimeScreen extends Component { return (
    diff --git a/lib/components/mobile/location-search.js b/lib/components/mobile/location-search.js index 11a7e02d6..0e0df8d07 100644 --- a/lib/components/mobile/location-search.js +++ b/lib/components/mobile/location-search.js @@ -2,12 +2,12 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import MobileContainer from './container' -import MobileNavigationBar from './navigation-bar' import LocationField from '../form/connected-location-field' - import { MobileScreens, setMobileScreen } from '../../actions/ui' +import MobileContainer from './container' +import MobileNavigationBar from './navigation-bar' + class MobileLocationSearch extends Component { static propTypes = { backScreen: PropTypes.number, @@ -30,9 +30,9 @@ class MobileLocationSearch extends Component { return (
    ) @@ -91,16 +91,16 @@ class MobileMain extends Component { case MobileScreens.SET_FROM_LOCATION: return ( ) case MobileScreens.SET_TO_LOCATION: return ( ) diff --git a/lib/components/mobile/navigation-bar.js b/lib/components/mobile/navigation-bar.js index 8b611d6a0..cdd845530 100644 --- a/lib/components/mobile/navigation-bar.js +++ b/lib/components/mobile/navigation-bar.js @@ -15,8 +15,8 @@ class MobileNavigationBar extends Component { backScreen: PropTypes.number, headerAction: PropTypes.element, headerText: PropTypes.string, - showBackButton: PropTypes.bool, - setMobileScreen: PropTypes.func + setMobileScreen: PropTypes.func, + showBackButton: PropTypes.bool } static contextType = ComponentContext @@ -37,7 +37,7 @@ class MobileNavigationBar extends Component { } = this.props return ( - + diff --git a/lib/components/mobile/options-screen.js b/lib/components/mobile/options-screen.js index 106a695be..85444cc3c 100644 --- a/lib/components/mobile/options-screen.js +++ b/lib/components/mobile/options-screen.js @@ -1,13 +1,13 @@ import React, { Component } from 'react' import { connect } from 'react-redux' -import MobileContainer from './container' -import MobileNavigationBar from './navigation-bar' import ConnectedSettingsSelectorPanel from '../form/connected-settings-selector-panel' import PlanTripButton from '../form/plan-trip-button' - import { MobileScreens, setMobileScreen } from '../../actions/ui' +import MobileNavigationBar from './navigation-bar' +import MobileContainer from './container' + class MobileOptionsScreen extends Component { _planTripClicked = () => { this.props.setMobileScreen(MobileScreens.RESULTS_SUMMARY) @@ -17,9 +17,9 @@ class MobileOptionsScreen extends Component { return (
    diff --git a/lib/components/mobile/route-viewer.js b/lib/components/mobile/route-viewer.js index f93270d58..97b25fe5b 100644 --- a/lib/components/mobile/route-viewer.js +++ b/lib/components/mobile/route-viewer.js @@ -2,19 +2,18 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import MobileContainer from './container' -import MobileNavigationBar from './navigation-bar' - import RouteViewer from '../viewers/route-viewer' import DefaultMap from '../map/default-map' - import { setViewedRoute, setMainPanelContent } from '../../actions/ui' import { ComponentContext } from '../../util/contexts' +import MobileNavigationBar from './navigation-bar' +import MobileContainer from './container' + class MobileRouteViewer extends Component { static propTypes = { - setViewedRoute: PropTypes.func, - setMainPanelContent: PropTypes.func + setMainPanelContent: PropTypes.func, + setViewedRoute: PropTypes.func } static contextType = ComponentContext @@ -30,8 +29,8 @@ class MobileRouteViewer extends Component {
    @@ -54,8 +53,8 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - setViewedRoute, - setMainPanelContent + setMainPanelContent, + setViewedRoute } export default connect(mapStateToProps, mapDispatchToProps)(MobileRouteViewer) diff --git a/lib/components/mobile/search-screen.js b/lib/components/mobile/search-screen.js index b21a56693..01ce77a55 100644 --- a/lib/components/mobile/search-screen.js +++ b/lib/components/mobile/search-screen.js @@ -9,12 +9,11 @@ import LocationField from '../form/connected-location-field' import PlanTripButton from '../form/plan-trip-button' import SettingsPreview from '../form/settings-preview' import SwitchButton from '../form/switch-button' +import { MobileScreens, setMobileScreen } from '../../actions/ui' import MobileContainer from './container' import MobileNavigationBar from './navigation-bar' -import { MobileScreens, setMobileScreen } from '../../actions/ui' - class MobileSearchScreen extends Component { static propTypes = { map: PropTypes.element, @@ -62,16 +61,16 @@ class MobileSearchScreen extends Component {
    - + diff --git a/lib/components/mobile/stop-viewer.js b/lib/components/mobile/stop-viewer.js index 557c3e318..ef44b0dee 100644 --- a/lib/components/mobile/stop-viewer.js +++ b/lib/components/mobile/stop-viewer.js @@ -2,14 +2,13 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import MobileContainer from './container' -import MobileNavigationBar from './navigation-bar' - import StopViewer from '../viewers/stop-viewer' import DefaultMap from '../map/default-map' - import { setViewedStop } from '../../actions/ui' +import MobileNavigationBar from './navigation-bar' +import MobileContainer from './container' + class MobileStopViewer extends Component { static propTypes = { setViewedStop: PropTypes.func @@ -20,8 +19,8 @@ class MobileStopViewer extends Component { { this.props.setViewedStop(null) }} + showBackButton /> {/* include map as fixed component */} diff --git a/lib/components/mobile/trip-viewer.js b/lib/components/mobile/trip-viewer.js index 34d1b1bc1..d8c74e1b4 100644 --- a/lib/components/mobile/trip-viewer.js +++ b/lib/components/mobile/trip-viewer.js @@ -2,14 +2,13 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import MobileContainer from './container' -import MobileNavigationBar from './navigation-bar' - import TripViewer from '../viewers/trip-viewer' import DefaultMap from '../map/default-map' - import { setViewedTrip } from '../../actions/ui' +import MobileNavigationBar from './navigation-bar' +import MobileContainer from './container' + class MobileTripViewer extends Component { static propTypes = { setViewedTrip: PropTypes.func @@ -22,8 +21,8 @@ class MobileTripViewer extends Component { {/* include map as fixed component */} diff --git a/lib/components/narrative/connected-trip-details.js b/lib/components/narrative/connected-trip-details.js index bbab9c706..8904c3702 100644 --- a/lib/components/narrative/connected-trip-details.js +++ b/lib/components/narrative/connected-trip-details.js @@ -13,13 +13,13 @@ const TripDetails = styled(TripDetailsBase)` const mapStateToProps = (state, ownProps) => { return { + longDateFormat: coreUtils.time.getLongDateFormat(state.otp.config), messages: state.otp.config.language.tripDetails, routingType: state.otp.currentQuery.routingType, - tnc: state.otp.tnc, timeOptions: { format: coreUtils.time.getTimeFormat(state.otp.config) }, - longDateFormat: coreUtils.time.getLongDateFormat(state.otp.config) + tnc: state.otp.tnc } } diff --git a/lib/components/narrative/default/access-leg.js b/lib/components/narrative/default/access-leg.js index 1b541635a..e6687def5 100644 --- a/lib/components/narrative/default/access-leg.js +++ b/lib/components/narrative/default/access-leg.js @@ -18,7 +18,7 @@ export default class AccessLeg extends Component { } _onLegClick = (e) => { - const {active, leg, index, setActiveLeg} = this.props + const {active, index, leg, setActiveLeg} = this.props if (active) { setActiveLeg(null) } else { @@ -38,8 +38,8 @@ export default class AccessLeg extends Component { const { active, activeStep, index, leg } = this.props return (
    + className={`leg${active ? ' active' : ''} access-leg`} + key={index}>
    diff --git a/lib/components/narrative/default/itinerary-details.js b/lib/components/narrative/default/itinerary-details.js index 337943dcc..45bb65d19 100644 --- a/lib/components/narrative/default/itinerary-details.js +++ b/lib/components/narrative/default/itinerary-details.js @@ -12,7 +12,7 @@ export default class ItineraryDetails extends Component { } render () { - const { itinerary, activeLeg, activeStep, LegIcon, setActiveLeg, setActiveStep } = this.props + const { activeLeg, activeStep, itinerary, LegIcon, setActiveLeg, setActiveStep } = this.props return (
    {itinerary.legs.map((leg, index) => { diff --git a/lib/components/narrative/default/itinerary-summary.js b/lib/components/narrative/default/itinerary-summary.js index 4a87862c5..f8da7f666 100644 --- a/lib/components/narrative/default/itinerary-summary.js +++ b/lib/components/narrative/default/itinerary-summary.js @@ -75,10 +75,10 @@ export default class ItinerarySummary extends Component { } blocks.push( diff --git a/lib/components/narrative/default/tnc-leg.js b/lib/components/narrative/default/tnc-leg.js index 8b2a301ef..721d25b9d 100644 --- a/lib/components/narrative/default/tnc-leg.js +++ b/lib/components/narrative/default/tnc-leg.js @@ -24,8 +24,8 @@ class TransportationNetworkCompanyLeg extends Component { render () { const { leg, legMode, LYFT_CLIENT_ID, UBER_CLIENT_ID } = this.props const universalLinks = { - 'UBER': `https://m.uber.com/${isMobile() ? 'ul/' : ''}?client_id=${UBER_CLIENT_ID}&action=setPickup&pickup[latitude]=${leg.from.lat}&pickup[longitude]=${leg.from.lon}&pickup[nickname]=${encodeURI(leg.from.name)}&dropoff[latitude]=${leg.to.lat}&dropoff[longitude]=${leg.to.lon}&dropoff[nickname]=${encodeURI(leg.to.name)}`, - 'LYFT': `https://lyft.com/ride?id=${defaultTncRideTypes['LYFT']}&partner=${LYFT_CLIENT_ID}&pickup[latitude]=${leg.from.lat}&pickup[longitude]=${leg.from.lon}&destination[latitude]=${leg.to.lat}&destination[longitude]=${leg.to.lon}` + 'LYFT': `https://lyft.com/ride?id=${defaultTncRideTypes['LYFT']}&partner=${LYFT_CLIENT_ID}&pickup[latitude]=${leg.from.lat}&pickup[longitude]=${leg.from.lon}&destination[latitude]=${leg.to.lat}&destination[longitude]=${leg.to.lon}`, + 'UBER': `https://m.uber.com/${isMobile() ? 'ul/' : ''}?client_id=${UBER_CLIENT_ID}&action=setPickup&pickup[latitude]=${leg.from.lat}&pickup[longitude]=${leg.from.lon}&pickup[nickname]=${encodeURI(leg.from.name)}&dropoff[latitude]=${leg.to.lat}&dropoff[longitude]=${leg.to.lon}&dropoff[nickname]=${encodeURI(leg.to.name)}` } const { tncData } = leg return ( @@ -64,8 +64,8 @@ const mapStateToProps = (state, ownProps) => { const { LYFT_CLIENT_ID, UBER_CLIENT_ID } = state.otp.config return { companies: state.otp.currentQuery.companies, - tncData: state.otp.tnc, LYFT_CLIENT_ID, + tncData: state.otp.tnc, UBER_CLIENT_ID } } diff --git a/lib/components/narrative/default/transit-leg.js b/lib/components/narrative/default/transit-leg.js index 44b826ecd..2e2f3023d 100644 --- a/lib/components/narrative/default/transit-leg.js +++ b/lib/components/narrative/default/transit-leg.js @@ -89,9 +89,9 @@ export default class TransitLeg extends Component { {/* Trip Viewer Button */}
    @@ -110,7 +110,7 @@ export default class TransitLeg extends Component {
    {leg.intermediateStops.map((s, i) => ( -
    +
    {formatLocation(s.name)}
    @@ -130,7 +130,7 @@ export default class TransitLeg extends Component {
    {alert.alertDescriptionText} {' '} - {alert.alertUrl ? more info : null} + {alert.alertUrl ? more info : null}
    ))}
    diff --git a/lib/components/narrative/icon.js b/lib/components/narrative/icon.js index 4e40cec04..cca4799b3 100644 --- a/lib/components/narrative/icon.js +++ b/lib/components/narrative/icon.js @@ -8,8 +8,8 @@ export default class Icon extends Component { render () { return ( ) } diff --git a/lib/components/narrative/itinerary-carousel.js b/lib/components/narrative/itinerary-carousel.js index 912126d88..11c9ffe11 100644 --- a/lib/components/narrative/itinerary-carousel.js +++ b/lib/components/narrative/itinerary-carousel.js @@ -6,24 +6,25 @@ import { connect } from 'react-redux' import SwipeableViews from 'react-swipeable-views' import { setActiveItinerary, setActiveLeg, setActiveStep } from '../../actions/narrative' -import Icon from './icon' -import Loading from './loading' import { ComponentContext } from '../../util/contexts' import { getActiveItineraries, getActiveSearch } from '../../util/state' +import Icon from './icon' +import Loading from './loading' + class ItineraryCarousel extends Component { state = {} static propTypes = { - itineraries: PropTypes.array, - pending: PropTypes.number, activeItinerary: PropTypes.number, + companies: PropTypes.string, + expanded: PropTypes.bool, hideHeader: PropTypes.bool, + itineraries: PropTypes.array, onClick: PropTypes.func, + pending: PropTypes.number, setActiveItinerary: PropTypes.func, setActiveLeg: PropTypes.func, - setActiveStep: PropTypes.func, - expanded: PropTypes.bool, - companies: PropTypes.string + setActiveStep: PropTypes.func } static contextType = ComponentContext @@ -50,8 +51,8 @@ class ItineraryCarousel extends Component { const { activeItinerary, expanded, - itineraries, hideHeader, + itineraries, pending } = this.props const { ItineraryBody, LegIcon } = this.context @@ -75,8 +76,8 @@ class ItineraryCarousel extends Component { {activeItinerary + 1} of {itineraries.length}
    @@ -114,12 +115,12 @@ const mapStateToProps = (state, ownProps) => { const itineraries = getActiveItineraries(state.otp) return { - itineraries, - pending: activeSearch && activeSearch.pending, activeItinerary: activeSearch && activeSearch.activeItinerary, activeLeg: activeSearch && activeSearch.activeLeg, activeStep: activeSearch && activeSearch.activeStep, companies: state.otp.currentQuery.companies, + itineraries, + pending: activeSearch && activeSearch.pending, timeFormat: coreUtils.time.getTimeFormat(state.otp.config) } } diff --git a/lib/components/narrative/line-itin/connected-itinerary-body.js b/lib/components/narrative/line-itin/connected-itinerary-body.js index 14441f51e..4e41498f4 100644 --- a/lib/components/narrative/line-itin/connected-itinerary-body.js +++ b/lib/components/narrative/line-itin/connected-itinerary-body.js @@ -11,12 +11,13 @@ import styled from 'styled-components' import { setLegDiagram } from '../../../actions/map' import { setViewedTrip } from '../../../actions/ui' -import TransitLegSubheader from './connected-transit-leg-subheader' -import RealtimeTimeColumn from './realtime-time-column' import TripDetails from '../connected-trip-details' import TripTools from '../trip-tools' import { ComponentContext } from '../../../util/contexts' +import RealtimeTimeColumn from './realtime-time-column' +import TransitLegSubheader from './connected-transit-leg-subheader' + const noop = () => {} const ItineraryBodyContainer = styled.div` @@ -46,8 +47,8 @@ class ConnectedItineraryBody extends Component { diagramVisible, itinerary, setActiveLeg, - setViewedTrip, setLegDiagram, + setViewedTrip, timeOptions } = this.props const { LegIcon } = this.context @@ -71,11 +72,11 @@ class ConnectedItineraryBody extends Component { showMapButtonColumn={false} showRouteFares={config.itinerary && config.itinerary.showRouteFares} showViewTripButton + TimeColumnContent={RealtimeTimeColumn} timeOptions={timeOptions} toRouteAbbreviation={noop} TransitLegSubheader={TransitLegSubheader} TransitLegSummary={TransitLegSummary} - TimeColumnContent={RealtimeTimeColumn} /> @@ -92,8 +93,8 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - setViewedTrip, - setLegDiagram + setLegDiagram, + setViewedTrip } export default connect(mapStateToProps, mapDispatchToProps)( diff --git a/lib/components/narrative/loading.js b/lib/components/narrative/loading.js index d80507df5..adc668564 100644 --- a/lib/components/narrative/loading.js +++ b/lib/components/narrative/loading.js @@ -1,4 +1,5 @@ import React, { Component } from 'react' + import Icon from './icon' export default class Loading extends Component { @@ -6,8 +7,8 @@ export default class Loading extends Component { const { small } = this.props return (

    + className='text-center' + style={{ marginTop: '15px' }}> diff --git a/lib/components/narrative/mode-icon.js b/lib/components/narrative/mode-icon.js index 453c1188e..eacb1436e 100644 --- a/lib/components/narrative/mode-icon.js +++ b/lib/components/narrative/mode-icon.js @@ -1,13 +1,14 @@ -import Icon from './icon' import React, { Component } from 'react' import PropTypes from 'prop-types' +import Icon from './icon' + export default class ModeIcon extends Component { static propTypes = { mode: PropTypes.string } render () { - const { mode, defaultToText } = this.props + const { defaultToText, mode } = this.props switch (mode) { case 'BICYCLE': return diff --git a/lib/components/narrative/narrative-itineraries.js b/lib/components/narrative/narrative-itineraries.js index 8343d7a8a..3d6459c02 100644 --- a/lib/components/narrative/narrative-itineraries.js +++ b/lib/components/narrative/narrative-itineraries.js @@ -13,8 +13,6 @@ import { updateItineraryFilter } from '../../actions/narrative' import * as uiActions from '../../actions/ui' -import NarrativeItinerariesErrors from './narrative-itineraries-errors' -import NarrativeItinerariesHeader from './narrative-itineraries-header' import { ComponentContext } from '../../util/contexts' import { getActiveItineraries, @@ -24,6 +22,9 @@ import { getVisibleItineraryIndex } from '../../util/state' +import NarrativeItinerariesErrors from './narrative-itineraries-errors' +import NarrativeItinerariesHeader from './narrative-itineraries-header' + const { ItineraryView } = uiActions class NarrativeItineraries extends Component { @@ -102,7 +103,7 @@ class NarrativeItineraries extends Component { return Array.from( {length: count}, (v, i) => -

    +
    @@ -235,8 +236,8 @@ const mapStateToProps = (state, ownProps) => { activeLeg: activeSearch && activeSearch.activeLeg, activeSearch, activeStep: activeSearch && activeSearch.activeStep, - errors: getResponsesWithErrors(state.otp), errorMessages, + errors: getResponsesWithErrors(state.otp), itineraries, itineraryIsExpanded, itineraryView, diff --git a/lib/components/narrative/narrative-routing-results.js b/lib/components/narrative/narrative-routing-results.js index 5660c7e29..00b7712cf 100644 --- a/lib/components/narrative/narrative-routing-results.js +++ b/lib/components/narrative/narrative-routing-results.js @@ -2,10 +2,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import Loading from './loading' -import TabbedItineraries from './tabbed-itineraries' import ErrorMessage from '../form/error-message' - import { getActiveError, getActiveItineraries, @@ -13,6 +10,9 @@ import { } from '../../util/state' import { setMainPanelContent } from '../../actions/ui' +import TabbedItineraries from './tabbed-itineraries' +import Loading from './loading' + class NarrativeRoutingResults extends Component { static propTypes = { routingType: PropTypes.string @@ -29,9 +29,9 @@ class NarrativeRoutingResults extends Component { render () { const { error, - pending, itineraries, - mainPanelContent + mainPanelContent, + pending } = this.props if (pending) return @@ -49,9 +49,9 @@ const mapStateToProps = (state, ownProps) => { const activeSearch = getActiveSearch(state.otp) const pending = activeSearch ? Boolean(activeSearch.pending) : false return { - mainPanelContent: state.otp.ui.mainPanelContent, error: getActiveError(state.otp), itineraries: getActiveItineraries(state.otp), + mainPanelContent: state.otp.ui.mainPanelContent, pending, routingType: activeSearch && activeSearch.query.routingType } diff --git a/lib/components/narrative/realtime-annotation.js b/lib/components/narrative/realtime-annotation.js index 31ed7cd0e..6b2a4b61b 100644 --- a/lib/components/narrative/realtime-annotation.js +++ b/lib/components/narrative/realtime-annotation.js @@ -63,15 +63,15 @@ export default class RealtimeAnnotation extends Component { if (componentClass === 'popover') { return + {innerContent} - }> + } + placement='bottom' + // container={this} + // containerPadding={40} + trigger='click'> } else { diff --git a/lib/components/narrative/tabbed-itineraries.js b/lib/components/narrative/tabbed-itineraries.js index cfa2af827..4e7c1c64d 100644 --- a/lib/components/narrative/tabbed-itineraries.js +++ b/lib/components/narrative/tabbed-itineraries.js @@ -13,9 +13,9 @@ const { formatDuration, formatTime, getTimeFormat } = coreUtils.time class TabbedItineraries extends Component { static propTypes = { + activeItinerary: PropTypes.number, itineraries: PropTypes.array, pending: PropTypes.bool, - activeItinerary: PropTypes.number, setActiveItinerary: PropTypes.func, setActiveLeg: PropTypes.func, setActiveStep: PropTypes.func, @@ -124,8 +124,8 @@ class TabButton extends Component { if (isActive) classNames.push('selected') return ( : null @@ -192,8 +193,8 @@ class StopViewer extends Component { Stop ID: {stopId} @@ -204,16 +205,16 @@ class StopViewer extends Component { onToClick={this._onClickPlanTo} /> {scheduleView && } {timezoneWarning} @@ -296,13 +297,13 @@ const mapStateToProps = (state, ownProps) => { autoRefreshStopTimes: state.otp.user.autoRefreshStopTimes, favoriteStops: state.otp.user.favoriteStops, homeTimezone: state.otp.config.homeTimezone, - viewedStop: state.otp.ui.viewedStop, showUserSettings, stopData: state.otp.transitIndex.stops[state.otp.ui.viewedStop.stopId], stopViewerArriving: state.otp.config.language.stopViewerArriving, stopViewerConfig, timeFormat: getTimeFormat(state.otp.config), - transitOperators: state.otp.config.transitOperators + transitOperators: state.otp.config.transitOperators, + viewedStop: state.otp.ui.viewedStop } } diff --git a/lib/components/viewers/trip-viewer.js b/lib/components/viewers/trip-viewer.js index 9ce414d29..75fe15b99 100644 --- a/lib/components/viewers/trip-viewer.js +++ b/lib/components/viewers/trip-viewer.js @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/label-has-for */ import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' import React, { Component } from 'react' @@ -5,12 +6,12 @@ import { Button, Label } from 'react-bootstrap' import { connect } from 'react-redux' import Icon from '../narrative/icon' -import ViewStopButton from './view-stop-button' - import { setViewedTrip } from '../../actions/ui' import { findTrip } from '../../actions/api' import { setLocation } from '../../actions/map' +import ViewStopButton from './view-stop-button' + class TripViewer extends Component { static propTypes = { hideBackButton: PropTypes.bool, @@ -143,9 +144,9 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - setViewedTrip, findTrip, - setLocation + setLocation, + setViewedTrip } export default connect(mapStateToProps, mapDispatchToProps)(TripViewer) diff --git a/lib/components/viewers/view-trip-button.js b/lib/components/viewers/view-trip-button.js index 06267d8d2..95a57837f 100644 --- a/lib/components/viewers/view-trip-button.js +++ b/lib/components/viewers/view-trip-button.js @@ -8,17 +8,17 @@ import { setMainPanelContent, setViewedTrip } from '../../actions/ui' class ViewTripButton extends Component { static propTypes = { fromIndex: PropTypes.number, - tripId: PropTypes.string, text: PropTypes.string, - toIndex: PropTypes.number + toIndex: PropTypes.number, + tripId: PropTypes.string } _onClick = () => { this.props.setMainPanelContent(null) this.props.setViewedTrip({ - tripId: this.props.tripId, fromIndex: this.props.fromIndex, - toIndex: this.props.toIndex + toIndex: this.props.toIndex, + tripId: this.props.tripId }) } diff --git a/lib/reducers/call-taker.js b/lib/reducers/call-taker.js index 4fa3849b7..9dc4cf46b 100644 --- a/lib/reducers/call-taker.js +++ b/lib/reducers/call-taker.js @@ -10,8 +10,8 @@ function createCallTakerReducer (config) { activeCall: null, callHistory: { calls: { - status: FETCH_STATUS.UNFETCHED, - data: [] + data: [], + status: FETCH_STATUS.UNFETCHED }, visible: calltakerConfig?.options?.showCallHistoryOnLoad }, @@ -22,8 +22,8 @@ function createCallTakerReducer (config) { }, groupSize: null, requests: { - status: FETCH_STATUS.UNFETCHED, - data: [] + data: [], + status: FETCH_STATUS.UNFETCHED }, visible: false }, @@ -47,8 +47,8 @@ function createCallTakerReducer (config) { case 'RECEIVED_CALLS': { const data = action.payload.calls const calls = { - status: FETCH_STATUS.FETCHED, - data: data.sort((a, b) => moment(b.endTime) - moment(a.endTime)) + data: data.sort((a, b) => moment(b.endTime) - moment(a.endTime)), + status: FETCH_STATUS.FETCHED } return update(state, { callHistory: { calls: { $set: calls } } @@ -62,8 +62,8 @@ function createCallTakerReducer (config) { case 'RECEIVED_FIELD_TRIPS': { const data = action.payload.fieldTrips const requests = { - status: FETCH_STATUS.FETCHED, - data: data.sort((a, b) => moment(b.endTime) - moment(a.endTime)) + data: data.sort((a, b) => moment(b.endTime) - moment(a.endTime)), + status: FETCH_STATUS.FETCHED } return update(state, { fieldTrip: { requests: { $set: requests } } diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 14d835197..810f312bf 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -9,7 +9,7 @@ import {FETCH_STATUS} from '../util/constants' import {getTimestamp} from '../util/state' import {isBatchRoutingEnabled} from '../util/itinerary' -const { isTransit, getTransitModes } = coreUtils.itinerary +const { getTransitModes, isTransit } = coreUtils.itinerary const { matchLatLon } = coreUtils.map const { filterProfileOptions } = coreUtils.profile const { @@ -67,15 +67,14 @@ function validateInitialState (initialState) { export function getInitialState (userDefinedConfig) { const defaultConfig = { autoPlan: { - mobile: 'BOTH_LOCATIONS_CHANGED', - default: 'ONE_LOCATION_CHANGED' + default: 'ONE_LOCATION_CHANGED', + mobile: 'BOTH_LOCATIONS_CHANGED' }, debouncePlanTimeMs: 0, language: {}, - transitOperators: [], + onTimeThresholdSeconds: 60, realtimeEffectsDisplayThreshold: 120, routingTypes: [], - onTimeThresholdSeconds: 60, stopViewer: { numberOfDepartures: 3, // per pattern // Hide block ids unless explicitly enabled in config. @@ -84,7 +83,8 @@ export function getInitialState (userDefinedConfig) { // a route does not begin service again until Monday, we are showing its next // departure and it is not entirely excluded from display. timeRange: 345600 // four days in seconds - } + }, + transitOperators: [] } const config = Object.assign(defaultConfig, userDefinedConfig) @@ -175,6 +175,7 @@ export function getInitialState (userDefinedConfig) { } return { + activeSearchId: 0, config, currentQuery, filter: { @@ -186,31 +187,13 @@ export function getInitialState (userDefinedConfig) { }, location: { currentPosition: { - error: null, coords: null, + error: null, fetching: false }, - sessionSearches: [], - nearbyStops: [] - }, - user: { - autoRefreshStopTimes, - // Do not store from/to or date/time in defaults - defaults: getTripOptionsFromQuery(defaults), - expandAdvanced, - favoriteStops, - trackRecent, - locations, - recentPlaces, - recentSearches - }, - searches: {}, - transitIndex: { - stops: {}, - trips: {} + nearbyStops: [], + sessionSearches: [] }, - useRealtime: true, - activeSearchId: 0, overlay: { bikeRental: { stations: [] @@ -233,15 +216,32 @@ export function getInitialState (userDefinedConfig) { locations: [] } }, + searches: {}, tnc: { etaEstimates: {}, rideEstimates: {} }, + transitIndex: { + stops: {}, + trips: {} + }, ui: { + diagramLeg: null, mobileScreen: MobileScreens.WELCOME_SCREEN, - printView: window.location.href.indexOf('/print/') !== -1, - diagramLeg: null - } + printView: window.location.href.indexOf('/print/') !== -1 + }, + user: { + autoRefreshStopTimes, + // Do not store from/to or date/time in defaults + defaults: getTripOptionsFromQuery(defaults), + expandAdvanced, + favoriteStops, + locations, + recentPlaces, + recentSearches, + trackRecent + }, + useRealtime: true } } @@ -258,6 +258,7 @@ function createOtpReducer (config) { case 'ROUTING_REQUEST': const { activeItinerary, pending } = action.payload return update(state, { + activeSearchId: { $set: searchId }, searches: { [searchId]: { $set: { @@ -271,20 +272,19 @@ function createOtpReducer (config) { timestamp: getTimestamp() } } - }, - activeSearchId: { $set: searchId } + } }) case 'ROUTING_ERROR': return update(state, { searches: { [searchId]: { + pending: { $set: activeSearch.pending - 1 }, response: { $push: [{ error: action.payload.error, requestId }] - }, - pending: { $set: activeSearch.pending - 1 } + } } } }) @@ -296,8 +296,8 @@ function createOtpReducer (config) { return update(state, { searches: { [searchId]: { - response: { $push: [response] }, - pending: { $set: activeSearch.pending - 1 } + pending: { $set: activeSearch.pending - 1 }, + response: { $push: [response] } } }, ui: { @@ -316,8 +316,8 @@ function createOtpReducer (config) { return update(state, { overlay: { bikeRental: { - pending: { $set: true }, - error: { $set: null } + error: { $set: null }, + pending: { $set: true } } } }) @@ -325,8 +325,8 @@ function createOtpReducer (config) { return update(state, { overlay: { bikeRental: { - pending: { $set: false }, - error: { $set: action.payload } + error: { $set: action.payload }, + pending: { $set: false } } } }) @@ -334,8 +334,8 @@ function createOtpReducer (config) { return update(state, { overlay: { bikeRental: { - stations: { $set: action.payload.stations }, - pending: { $set: false } + pending: { $set: false }, + stations: { $set: action.payload.stations } } } }) @@ -343,8 +343,8 @@ function createOtpReducer (config) { return update(state, { overlay: { carRental: { - pending: { $set: false }, - error: { $set: action.payload } + error: { $set: action.payload }, + pending: { $set: false } } } }) @@ -352,8 +352,8 @@ function createOtpReducer (config) { return update(state, { overlay: { carRental: { - stations: { $set: action.payload.stations }, - pending: { $set: false } + pending: { $set: false }, + stations: { $set: action.payload.stations } } } }) @@ -361,8 +361,8 @@ function createOtpReducer (config) { return update(state, { overlay: { vehicleRental: { - pending: { $set: false }, - error: { $set: action.payload } + error: { $set: action.payload }, + pending: { $set: false } } } }) @@ -370,8 +370,8 @@ function createOtpReducer (config) { return update(state, { overlay: { vehicleRental: { - stations: { $set: action.payload.stations }, - pending: { $set: false } + pending: { $set: false }, + stations: { $set: action.payload.stations } } } }) @@ -514,14 +514,14 @@ function createOtpReducer (config) { case 'REMEMBER_STOP': { // Payload is stop data. We want to avoid saving other attributes that // might be contained there (like lists of patterns). - const { id, name, lat, lon } = action.payload + const { id, lat, lon, name } = action.payload const stop = { - type: 'stop', icon: 'bus', id, - name, lat, - lon + lon, + name, + type: 'stop' } const favoriteStops = clone(state.user.favoriteStops) if (favoriteStops.length >= MAX_RECENT_STORAGE) { @@ -558,9 +558,9 @@ function createOtpReducer (config) { removeItem('recentSearches') } return update(state, { user: { - trackRecent: { $set: action.payload }, recentPlaces: { $set: recentPlaces }, - recentSearches: { $set: recentSearches } + recentSearches: { $set: recentSearches }, + trackRecent: { $set: action.payload } } }) } case 'REMEMBER_SEARCH': @@ -623,8 +623,8 @@ function createOtpReducer (config) { return update(state, { config: { api: { - path: { $set: path }, - originalPath: { $set: originalPath } + originalPath: { $set: originalPath }, + path: { $set: path } } } }) @@ -680,8 +680,8 @@ function createOtpReducer (config) { return update(state, { overlay: { transit: { - stops: { $set: action.payload.stops }, - pending: { $set: false } + pending: { $set: false }, + stops: { $set: action.payload.stops } } } }) @@ -689,8 +689,8 @@ function createOtpReducer (config) { return update(state, { overlay: { transit: { - stops: { $set: [] }, - pending: { $set: false } + pending: { $set: false }, + stops: { $set: [] } } } }) @@ -937,7 +937,7 @@ function createOtpReducer (config) { }) case 'UPDATE_OVERLAY_VISIBILITY': const mapOverlays = clone(state.config.map.overlays) - for (let key in action.payload) { + for (const key in action.payload) { const overlay = mapOverlays.find(o => o.name === key) overlay.visible = action.payload[key] } diff --git a/lib/util/auth.js b/lib/util/auth.js index 1da8e5e08..e96aae9d4 100644 --- a/lib/util/auth.js +++ b/lib/util/auth.js @@ -29,7 +29,7 @@ export const accountLinks = [ */ export function getAuth0Config (persistence) { if (persistence) { - const { enabled = false, strategy = null, auth0 = null } = persistence + const { auth0 = null, enabled = false, strategy = null } = persistence return (enabled && strategy === PERSISTENCE_STRATEGY_OTP_MIDDLEWARE) ? auth0 : null } return null diff --git a/lib/util/call-taker.js b/lib/util/call-taker.js index f3b378f99..12880598e 100644 --- a/lib/util/call-taker.js +++ b/lib/util/call-taker.js @@ -3,19 +3,20 @@ import {randId} from '@opentripplanner/core-utils/lib/storage' import moment from 'moment' import {getRoutingParams} from '../actions/api' + import {getTimestamp} from './state' export const TICKET_TYPES = { - own_tickets: 'Will use own tickets', hop_new: 'Will purchase new Hop Card', - hop_reload: 'Will reload existing Hop Card' + hop_reload: 'Will reload existing Hop Card', + own_tickets: 'Will use own tickets' } const PAYMENT_PREFS = { - request_call: 'Call requested at provided phone number', - phone_cc: 'Will call in credit card info to TriMet', fax_cc: 'Will fax credit card info to TriMet', - mail_check: 'Will mail check to TriMet' + mail_check: 'Will mail check to TriMet', + phone_cc: 'Will call in credit card info to TriMet', + request_call: 'Call requested at provided phone number' } const positiveIntInputProps = { @@ -25,49 +26,49 @@ const positiveIntInputProps = { } export const GROUP_FIELDS = [ - {inputProps: positiveIntInputProps, fieldName: 'numStudents', label: 'students 7 or older'}, - {inputProps: positiveIntInputProps, fieldName: 'numFreeStudents', label: 'students under 7'}, - {inputProps: positiveIntInputProps, fieldName: 'numChaperones', label: 'chaperones'} + {fieldName: 'numStudents', inputProps: positiveIntInputProps, label: 'students 7 or older'}, + {fieldName: 'numFreeStudents', inputProps: positiveIntInputProps, label: 'students under 7'}, + {fieldName: 'numChaperones', inputProps: positiveIntInputProps, label: 'chaperones'} ] export const PAYMENT_FIELDS = [ - {label: 'Payment preference', fieldName: 'paymentPreference', options: PAYMENT_PREFS}, - {label: 'Class Pass Hop Card #', fieldName: 'classpassId'}, - {label: 'Credit card type', fieldName: 'ccType'}, - {label: 'Name on credit card', fieldName: 'ccName'}, - {label: 'Credit card last 4 digits', fieldName: 'ccLastFour'}, - {label: 'Check/Money order number', fieldName: 'checkNumber'} + {fieldName: 'paymentPreference', label: 'Payment preference', options: PAYMENT_PREFS}, + {fieldName: 'classpassId', label: 'Class Pass Hop Card #'}, + {fieldName: 'ccType', label: 'Credit card type'}, + {fieldName: 'ccName', label: 'Name on credit card'}, + {fieldName: 'ccLastFour', label: 'Credit card last 4 digits'}, + {fieldName: 'checkNumber', label: 'Check/Money order number'} ] // List of tabs used for filtering field trips. export const TABS = [ { - id: 'new', - label: 'New', filter: (req) => req.status !== 'cancelled' && - (!req.inboundTripStatus || !req.outboundTripStatus) + (!req.inboundTripStatus || !req.outboundTripStatus), + id: 'new', + label: 'New' }, { - id: 'planned', - label: 'Planned', filter: (req) => req.status !== 'cancelled' && - req.inboundTripStatus && req.outboundTripStatus + req.inboundTripStatus && req.outboundTripStatus, + id: 'planned', + label: 'Planned' }, { + filter: (req) => req.status === 'cancelled', id: 'cancelled', - label: 'Cancelled', - filter: (req) => req.status === 'cancelled' + label: 'Cancelled' }, { - id: 'past', - label: 'Past', filter: (req) => req.travelDate && - moment(req.travelDate).diff(moment(), 'days') < 0 + moment(req.travelDate).diff(moment(), 'days') < 0, + id: 'past', + label: 'Past' }, { + filter: (req) => true, id: 'all', - label: 'All', - filter: (req) => true + label: 'All' } ] @@ -92,9 +93,9 @@ const SEARCH_FIELDS = [ export function constructNewCall () { return { - startTime: getTimestamp(), id: randId(), - searches: [] + searches: [], + startTime: getTimestamp() } } @@ -170,10 +171,10 @@ export function searchToQuery (search, call, otpConfig) { const queryParams = getRoutingParams(search.query, otpConfig, true) const {from, to} = search.query return { - queryParams: JSON.stringify(queryParams), + call, fromPlace: from.name || placeToLatLonStr(from), - toPlace: to.name || placeToLatLonStr(to), - call + queryParams: JSON.stringify(queryParams), + toPlace: to.name || placeToLatLonStr(to) } } @@ -215,10 +216,10 @@ export function getTripFromRequest (request, outbound = false) { export function createTripPlan (planData, queryParams) { const tripPlan = { earliestStartTime: null, + itineraries: [], latestEndTime: null, planData, - queryParams, - itineraries: [] + queryParams } if (!planData) return tripPlan.itineraries = tripPlan.planData.map(itinData => @@ -254,11 +255,11 @@ function calculateTimeBounds (itineraries) { */ function createItinerary (itinData, tripPlan) { const itin = { - itinData, - tripPlan, firstStopIds: [], hasTransit: false, - totalWalk: 0 + itinData, + totalWalk: 0, + tripPlan } itinData.legs.forEach(leg => { if (isTransit(leg.mode)) { diff --git a/lib/util/constants.js b/lib/util/constants.js index b37da6ead..1a0335955 100644 --- a/lib/util/constants.js +++ b/lib/util/constants.js @@ -5,10 +5,10 @@ export const DEFAULT_APP_TITLE = 'OpenTripPlanner' export const PERSISTENCE_STRATEGY_OTP_MIDDLEWARE = 'otp_middleware' export const FETCH_STATUS = { - UNFETCHED: 0, - FETCHING: 1, + ERROR: -1, FETCHED: 2, - ERROR: -1 + FETCHING: 1, + UNFETCHED: 0 } export const ACCOUNT_PATH = '/account' export const ACCOUNT_SETTINGS_PATH = `${ACCOUNT_PATH}/settings` diff --git a/lib/util/middleware.js b/lib/util/middleware.js index 5db426133..ffe7f1621 100644 --- a/lib/util/middleware.js +++ b/lib/util/middleware.js @@ -20,9 +20,9 @@ export function getSecureFetchOptions (accessToken, apiKey, method = 'get', opti } return { + headers, method, mode: 'cors', // Middleware is at a different URL. - headers, ...options } } @@ -46,12 +46,12 @@ export async function secureFetch (url, accessToken, apiKey, method = 'get', opt if (result.detail) message += ` (${result.detail})` return { - status: 'error', - message + message, + status: 'error' } } return { - status: 'success', - data: await res.json() + data: await res.json(), + status: 'success' } } diff --git a/lib/util/state.js b/lib/util/state.js index 00b24d847..da633a580 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -300,12 +300,12 @@ function sortItineraries (type, direction, a, b, config) { * time, etc. */ const DEFAULT_WEIGHTS = { - walkReluctance: 0.1, driveReluctance: 2, durationFactor: 0.25, fareFactor: 0.5, + transferReluctance: 0.9, waitReluctance: 0.1, - transferReluctance: 0.9 + walkReluctance: 0.1 } /** @@ -550,13 +550,13 @@ export function getRealtimeEffects (otpState) { const realtimeDuration = isAffectedByRealtimeData && realtimeItineraries ? realtimeItineraries[0].duration : 0 return { + exceedsThreshold: Math.abs(normalDuration - realtimeDuration) >= otpState.config.realtimeEffectsDisplayThreshold, isAffectedByRealtimeData, - normalRoutes, - realtimeRoutes, - routesDiffer: !isEqual(normalRoutes, realtimeRoutes), normalDuration, + normalRoutes, realtimeDuration, - exceedsThreshold: Math.abs(normalDuration - realtimeDuration) >= otpState.config.realtimeEffectsDisplayThreshold + realtimeRoutes, + routesDiffer: !isEqual(normalRoutes, realtimeRoutes) } // // TESTING: Return this instead to simulate a realtime-affected itinerary. // return { diff --git a/lib/util/viewer.js b/lib/util/viewer.js index d7e02b233..1f52ab41d 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -77,8 +77,8 @@ export function getStopTimesByPattern (stopData) { } stopTimesByPattern[id] = { id, - route, pattern, + route, times: [] } } @@ -108,6 +108,7 @@ export function getModeFromRoute (route) { 7: '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. + // eslint-disable-next-line sort-keys 11: 'BUS', // - Trolleybus. 12: 'RAIL' // - Monorail. } @@ -163,8 +164,8 @@ export function mergeAndSortStopTimes (stopData) { const headsign = isBlank(stopTime.headsign) ? pattern.headsign : stopTime.headsign return { ...stopTime, - route, - headsign + headsign, + route } }) mergedStopTimes = mergedStopTimes.concat(filteredTimes) From 913d40ac342cdc412e6e625fa8d65ceb544793e9 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Fri, 25 Jun 2021 13:59:26 -0700 Subject: [PATCH 06/35] refactor(field-trip): replace utils/state#isModuleEnabled with fn from utils/config --- lib/util/state.js | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/lib/util/state.js b/lib/util/state.js index 881716115..580407145 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -7,6 +7,8 @@ import { createSelector } from 'reselect' import { MainPanelContent } from '../actions/ui' +import { isModuleEnabled, Modules } from './config' + const { calculateFares } = coreUtils.itinerary /** @@ -205,18 +207,6 @@ const hashItinerary = memoize( ) ) -/** - * Returns true if the config has a modules list and one of the items in the - * list has an ID matching the given moduleId - * - * @param {Object} param - * @param {Object} param.config the app-wide config - * @param {string} param.moduleId the desired moduleId to check the existence of - */ -export function isModuleEnabled ({ config, moduleId }) { - return config.modules?.some(moduleConfig => moduleConfig.id === moduleId) -} - /** * Returns the config of the specified module if it exists. * @@ -288,10 +278,6 @@ function assignItinerariesToFieldTripGroups ({ fieldTripGroupSize, itineraries }) { - if (!isModuleEnabled({ config, moduleId: 'ft' })) { - return itineraries - } - // logic to add field trip group sizes for each itinerary const capacityConstrainedItineraries = [] let remainingGroupSize = fieldTripGroupSize @@ -332,6 +318,7 @@ export const getActiveItineraries = createSelector( state => state.otp.filter, getActiveSearchRealtimeResponse, state => state.otp.useRealtime, + state => isModuleEnabled(state, Modules.FIELD_TRIP), state => state.callTaker.fieldTrip.groupSize, ( config, @@ -339,6 +326,7 @@ export const getActiveItineraries = createSelector( itinerarySortSettings, realtimeResponse, useRealtime, + fieldTripModuleEnabled, fieldTripGroupSize ) => { // set response to use depending on useRealtime @@ -375,11 +363,13 @@ export const getActiveItineraries = createSelector( ? itineraries : itineraries.sort((a, b) => sortItineraries(type, direction, a, b, config)) - return assignItinerariesToFieldTripGroups({ - config, - fieldTripGroupSize, - itineraries: sortedItineraries - }) + return fieldTripModuleEnabled + ? assignItinerariesToFieldTripGroups({ + config, + fieldTripGroupSize, + itineraries: sortedItineraries + }) + : sortedItineraries } ) From 2248c55535e6a5a943e4c6dd2aec4cdbd5b97e4f Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Tue, 29 Jun 2021 11:59:12 -0700 Subject: [PATCH 07/35] refactor: remove duplicate getModuleConfig method --- lib/util/state.js | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/lib/util/state.js b/lib/util/state.js index 580407145..2df1f0555 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -7,7 +7,7 @@ import { createSelector } from 'reselect' import { MainPanelContent } from '../actions/ui' -import { isModuleEnabled, Modules } from './config' +import { getModuleConfig, Modules } from './config' const { calculateFares } = coreUtils.itinerary @@ -207,16 +207,6 @@ const hashItinerary = memoize( ) ) -/** - * Returns the config of the specified module if it exists. - * - * @param {Object} config the app-wide config - * @param {string} moduleId the desired id of the module - */ -function getModuleConfig (config, moduleId) { - return config.modules?.find(moduleConfig => moduleConfig.id === moduleId) -} - const defaultFieldTripModeCapacities = { 'TRAM': 80, 'SUBWAY': 120, @@ -238,8 +228,8 @@ const unknownModeCapacity = 10 * @param {string} mode the OTP mode */ const getFieldTripGroupCapacityForMode = createSelector( - config => getModuleConfig(config, 'ft')?.modeCapacities, - (config, mode) => mode, + fieldTripModuleConfig => fieldTripModuleConfig?.modeCapacities, + (fieldTripModuleConfig, mode) => mode, (configModeCapacities, mode) => (configModeCapacities && configModeCapacities[mode]) || defaultFieldTripModeCapacities[mode] || @@ -250,13 +240,13 @@ const getFieldTripGroupCapacityForMode = createSelector( * Calculates the capacity for a field trip group of a given itinerary * * @param {Object} param - * @param {Object} param.config The app-wide config + * @param {Object} param.fieldTripModuleConfig The field trip module config * @param {Object} param.itinerary An OTP itinerary * @return {number} The maximum size of a field trip group that could * use this itinerary. */ function calculateItineraryFieldTripGroupCapacity ({ - config, + fieldTripModuleConfig, itinerary }) { return itinerary.legs.reduce((constrainingLegCapacity, leg) => { @@ -265,7 +255,7 @@ function calculateItineraryFieldTripGroupCapacity ({ } return Math.min( constrainingLegCapacity, - getFieldTripGroupCapacityForMode(config, leg.mode) + getFieldTripGroupCapacityForMode(fieldTripModuleConfig, leg.mode) ) }, 10000) } @@ -274,7 +264,7 @@ function calculateItineraryFieldTripGroupCapacity ({ * Assigns itineraries to field trip subgroups. */ function assignItinerariesToFieldTripGroups ({ - config, + fieldTripModuleConfig, fieldTripGroupSize, itineraries }) { @@ -287,7 +277,7 @@ function assignItinerariesToFieldTripGroups ({ // calculate itinerary capacity const capacity = calculateItineraryFieldTripGroupCapacity({ - config, + fieldTripModuleConfig, itinerary }) @@ -318,7 +308,7 @@ export const getActiveItineraries = createSelector( state => state.otp.filter, getActiveSearchRealtimeResponse, state => state.otp.useRealtime, - state => isModuleEnabled(state, Modules.FIELD_TRIP), + state => getModuleConfig(state, Modules.FIELD_TRIP), state => state.callTaker.fieldTrip.groupSize, ( config, @@ -326,7 +316,7 @@ export const getActiveItineraries = createSelector( itinerarySortSettings, realtimeResponse, useRealtime, - fieldTripModuleEnabled, + fieldTripModuleConfig, fieldTripGroupSize ) => { // set response to use depending on useRealtime @@ -363,9 +353,9 @@ export const getActiveItineraries = createSelector( ? itineraries : itineraries.sort((a, b) => sortItineraries(type, direction, a, b, config)) - return fieldTripModuleEnabled + return fieldTripModuleConfig ? assignItinerariesToFieldTripGroups({ - config, + fieldTripModuleConfig, fieldTripGroupSize, itineraries: sortedItineraries }) From 5dc4c89999c3c29e89475113068681c05ffeaa0e Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Tue, 29 Jun 2021 19:34:15 -0700 Subject: [PATCH 08/35] refactor(field-trip): WIP to plan field trips with capacity constraints --- lib/actions/api.js | 19 +- lib/actions/field-trip.js | 243 +++++++++++++++++---- lib/components/admin/field-trip-details.js | 26 +-- lib/util/call-taker.js | 118 +++++----- lib/util/state.js | 83 ++++--- 5 files changed, 323 insertions(+), 166 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index 88acc858e..8ba0b362c 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -112,14 +112,18 @@ export function routingQuery (searchId = null) { ) : [{}] dispatch(routingRequest({ activeItinerary, routingType, searchId, pending: iterations.length })) - iterations.forEach((injectedParams, i) => { + return Promise.all(iterations.map((injectedParams, i) => { const requestId = randId() // fetch a realtime route const query = constructRoutingQuery(state, false, injectedParams) - fetch(query, getOtpFetchOptions(state)) + const realTimeFetch = fetch(query, getOtpFetchOptions(state)) .then(getJsonAndCheckResponse) .then(json => { - dispatch(routingResponse({ response: json, requestId, searchId })) + const dispatchedRoutingResponse = dispatch(routingResponse({ + response: json, + requestId, + searchId + })) // If tracking is enabled, store locations and search after successful // search is completed. if (state.otp.user.trackRecent) { @@ -132,6 +136,7 @@ export function routingQuery (searchId = null) { } dispatch(rememberSearch(formatRecentSearch(query, state))) } + return dispatchedRoutingResponse }) .catch(error => { dispatch(routingError({ error, requestId, searchId })) @@ -169,7 +174,7 @@ export function routingQuery (searchId = null) { user.loggedInUser && user.loggedInUser.storeTripHistory - fetch( + const nonRealtimeFetch = fetch( constructRoutingQuery(state, true), getOtpFetchOptions(state, storeTripHistory) ) @@ -180,13 +185,15 @@ export function routingQuery (searchId = null) { // FIXME: We should check that the mode combination actually has // realtime (or maybe this is set in the config file) to determine // whether this extra query to OTP is needed. - dispatch(nonRealtimeRoutingResponse({ response: json, searchId })) + return dispatch(nonRealtimeRoutingResponse({ response: json, searchId })) }) .catch(error => { console.error(error) // do nothing }) - }) + + return Promise.all([realTimeFetch, nonRealtimeFetch]) + })) } } diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 50ea0c8be..8cf946402 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -1,14 +1,23 @@ import { planParamsToQueryAsync } from '@opentripplanner/core-utils/lib/query' import { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } from '@opentripplanner/core-utils/lib/time' +import { randId } from '@opentripplanner/core-utils/lib/storage' import moment from 'moment' import { serialize } from 'object-to-formdata' import qs from 'qs' import { createAction } from 'redux-actions' +import { + getGroupSize, + getTripFromRequest, + // lzwEncode, + sessionIsInvalid +} from '../util/call-taker' +import { getModuleConfig, Modules } from '../util/config' +import { getActiveItineraries } from '../util/state' + import {routingQuery} from './api' import {toggleCallHistory} from './call-taker' -import {resetForm, setQueryParam} from './form' -import {getGroupSize, getTripFromRequest, sessionIsInvalid} from '../util/call-taker' +import {clearActiveSearch, resetForm, setQueryParam} from './form' if (typeof (fetch) === 'undefined') require('isomorphic-fetch') @@ -74,6 +83,8 @@ export function fetchFieldTripDetails (requestId) { .catch(err => { alert(`Error fetching field trips: ${JSON.stringify(err)}`) }) + + // TODO: fetch trip IDs for day } } @@ -144,48 +155,70 @@ export function editSubmitterNotes (request, submitterNotes) { } } -export function saveRequestTrip (request, outbound, groupPlan) { - return function (dispatch, getState) { +export function saveRequestTrip (request, outbound) { + return async function (dispatch, getState) { + const state = getState() + const { session } = state.callTaker + if (sessionIsInvalid(session)) return + + const itineraries = getActiveItineraries(state) + // If plan is not valid, return before persisting trip. - const check = checkPlanValidity(request, groupPlan) - if (!check.isValid) return alert(check.message) - const requestOrder = outbound ? 0 : 1 - const type = outbound ? 'outbound' : 'inbound' - const preExistingTrip = getTripFromRequest(request, outbound) - if (preExistingTrip) { - const msg = `This action will overwrite a previously planned ${type} itinerary for this request. Do you wish to continue?` - if (!confirm(msg)) return + if (fieldTripPlanIsInvalid(request, itineraries)) return + + // Show a confirmation dialog before overwriting existing plan + if (!overwriteExistingRequestTripsConfirmed(request, outbound)) return + + // construct data for request to save + const data = { + // itins: itineraries.map(createFieldTripItinerarySaveData), + requestId: request.id, + sessionId: session.sessionId, + trip: { + createdBy: session.username, + departure: moment(getEarliestStartTime(itineraries)).format('YYYY-MM-DDTHH:mm:ss'), + // destination: getEndOTPString(), + // origin: getStartOTPString(), + passengers: getGroupSize(request), + queryParams: JSON.stringify(state.otp.currentQuery), + requestOrder: outbound ? 0 : 1 + } } - alert(`TODO: Save trip in request order ${requestOrder}!`) - // TODO: Enable saveTrip - // dispatch(saveTrip(request, requestOrder)) + console.log(data) + + // do actual saving of trip + // const res = await fetch(`${state.otp.config.datastoreUrl}/calltaker/call`, + // {method: 'POST', body: makeFieldTripData(request)} + // ) + // + // this.serverRequest('/fieldtrip/newTrip', 'POST', data, _.bind(function(data) { + // if(data === -1) { + // otp.widgets.Dialogs.showOkDialog("This plan could not be saved due to a lack of capacity on one or more vehicles. Please re-plan your trip.", "Cannot Save Plan"); + // } + // else successCallback.call(this, data); + // }, this)); } } -/** - * @typedef {Object} ValidationCheck - * @property {boolean} isValid - Whether the check is valid - * @property {string} message - The message explaining why the check returned - * invalid. - */ - /** * Checks that a group plan is valid for a given request, i.e., that it occurs * on the requested travel date. * @param request field trip request - * @param groupPlan the group plan to check - * @return {ValidationCheck} + * @param itineraries the currently active itineraries + * @return true if invalid */ -function checkPlanValidity (request, groupPlan) { - if (groupPlan == null) { +function fieldTripPlanIsInvalid (request, itineraries) { + if (!itineraries || itineraries.length === 0) { return { isValid: false, message: 'No active plan to save' } } + const earliestStartTime = getEarliestStartTime(itineraries) + // FIXME: add back in offset? - const planDeparture = moment(groupPlan.earliestStartTime) // .add('hours', otp.config.timeOffset) + const planDeparture = moment(earliestStartTime) // .add('hours', otp.config.timeOffset) const requestDate = moment(request.travelDate) if ( @@ -193,30 +226,70 @@ function checkPlanValidity (request, groupPlan) { planDeparture.month() !== requestDate.month() || planDeparture.year() !== requestDate.year() ) { - return { - isValid: false, - message: `Planned trip date (${planDeparture.format('MM/DD/YYYY')}) is not the requested day of travel (${requestDate.format('MM/DD/YYYY')})` - } + alert( + `Planned trip date (${planDeparture.format('MM/DD/YYYY')}) is not the requested day of travel (${requestDate.format('MM/DD/YYYY')})` + ) + return true } // FIXME More checks? E.g., origin/destination - return { isValid: true, message: null } + return false +} + +function getEarliestStartTime (itineraries) { + return itineraries.reduce( + (earliestStartTime, itinerary) => + Math.min(earliestStartTime, itinerary.startTime), + Number.POSITIVE_INFINITY + ) +} + +function overwriteExistingRequestTripsConfirmed (request, outbound) { + const type = outbound ? 'outbound' : 'inbound' + const preExistingTrip = getTripFromRequest(request, outbound) + if (preExistingTrip) { + const msg = `This action will overwrite a previously planned ${type} itinerary for this request. Do you wish to continue?` + return confirm(msg) + } + return true } +// function createFieldTripItinerarySaveData (itinerary) { +// const result = {} +// result.passengers = itinerary.fieldTripGroupSize +// data['itins['+i+'].itinData'] = lzwEncode(JSON.stringify(itin.itinData)); +// data['itins['+i+'].timeOffset'] = otp.config.timeOffset || 0; +// +// var legs = itin.getTransitLegs(); +// +// for(var l = 0; l < legs.length; l++) { +// var leg = legs[l]; +// var routeName = (leg.routeShortName !== null ? ('(' + leg.routeShortName + ') ') : '') + (leg.routeLongName || ""); +// var tripHash = this.tripHashLookup[leg.tripId]; +// +// data['gtfsTrips['+i+']['+l+'].depart'] = moment(leg.startTime).format("HH:mm:ss"); +// data['gtfsTrips['+i+']['+l+'].arrive'] = moment(leg.endTime).format("HH:mm:ss"); +// data['gtfsTrips['+i+']['+l+'].agencyAndId'] = leg.tripId; +// data['gtfsTrips['+i+']['+l+'].tripHash'] = tripHash; +// data['gtfsTrips['+i+']['+l+'].routeName'] = routeName; +// data['gtfsTrips['+i+']['+l+'].fromStopIndex'] = leg.from.stopIndex; +// data['gtfsTrips['+i+']['+l+'].toStopIndex'] = leg.to.stopIndex; +// data['gtfsTrips['+i+']['+l+'].fromStopName'] = leg.from.name; +// data['gtfsTrips['+i+']['+l+'].toStopName'] = leg.to.name; +// data['gtfsTrips['+i+']['+l+'].headsign'] = leg.headsign; +// data['gtfsTrips['+i+']['+l+'].capacity'] = itin.getModeCapacity(leg.mode); +// if(leg.tripBlockId) data['gtfsTrips['+i+']['+l+'].blockId'] = leg.tripBlockId; +// } +// return result +// } + export function planTrip (request, outbound) { - return async function (dispatch, getState) { + return function (dispatch, getState) { dispatch(setGroupSize(getGroupSize(request))) - const trip = getTripFromRequest(request, outbound) - if (!trip) { - // Construct params from request details - if (outbound) dispatch(planOutbound(request)) - else dispatch(planInbound(request)) - } else { - // Populate params from saved query params - const params = await planParamsToQueryAsync(JSON.parse(trip.queryParams)) - dispatch(setQueryParam(params, trip.id)) - } + // Construct params from request details + if (outbound) dispatch(planOutbound(request)) + else dispatch(planInbound(request)) } } @@ -231,12 +304,11 @@ function planOutbound (request) { const queryParams = { date: moment(request.travelDate).format(OTP_API_DATE_FORMAT), departArrive: 'ARRIVE', - groupSize: getGroupSize(request), time: moment(request.arriveDestinationTime).format(OTP_API_TIME_FORMAT), ...locations } dispatch(setQueryParam(queryParams)) - dispatch(routingQuery()) + dispatch(makeFieldTripPlanRequests(request)) } } @@ -251,12 +323,77 @@ function planInbound (request) { const queryParams = { date: moment(request.travelDate).format(OTP_API_DATE_FORMAT), departArrive: 'DEPART', - groupSize: getGroupSize(request), time: moment(request.leaveDestinationTime).format(OTP_API_TIME_FORMAT), ...locations } dispatch(setQueryParam(queryParams)) - dispatch(routingQuery()) + dispatch(makeFieldTripPlanRequests(request)) + } +} + +/** + * Makes appropriate OTP requests until enough itineraries have been found to + * accomodate the field trip group. + */ +function makeFieldTripPlanRequests (request) { + return async function (dispatch, getState) { + const fieldTripModuleConfig = getModuleConfig( + getState(), + Modules.FIELD_TRIP + ) + // set numItineraries param for field trip requests + dispatch(setQueryParam({ numItineraries: 1 })) + + // initialize the remaining group size to be the total group size + let remainingGroupSize = getGroupSize(request) + + // create a new searchId to use for making all requests + const searchId = randId() + + // track number of requests made such that endless requesting doesn't occur + const maxRequests = fieldTripModuleConfig?.maxRequests || 10 + let numRequests = 0 + + // make requests until + while (remainingGroupSize > 0) { + numRequests++ + if (numRequests > maxRequests) { + // max number of requests exceeded. Show error. + alert('Number of trip requests exceeded without valid results') + return dispatch(clearActiveSearch()) + } + + // make next query + await dispatch(routingQuery(searchId)) + + // obtain trip hashes from OTP Index API + const state = getState() + await getTripHashesFromActiveItineraries(state) + + // check trip validity and calculate itinerary capacity + + // calculate remaining group size + // FIXME: actually implement and remove lint-passing placeholder + remainingGroupSize -= 12345 + + // set parameters for next iteration + } + } +} + +function getTripHashesFromActiveItineraries (state) { + return async function (dispatch, getState) { + const activeItineraries = getActiveItineraries(state) + const tripHashesToRequest = [] + activeItineraries.forEach(itinerary => { + itinerary.legs.forEach(leg => { + + }) + }) + + return Promise.all(tripHashesToRequest.map(tripId => { + return fetch() + })) } } @@ -332,3 +469,15 @@ export function setRequestStatus (request, status) { }) } } + +/** + * Clears and resets all relevant data after a field trip loses focus (upon + * closing the field trip details window) + */ +export function clearActiveFieldTrip () { + return function (dispatch, getState) { + dispatch(setActiveFieldTrip(null)) + dispatch(clearActiveSearch()) + dispatch(setQueryParam({ numItineraries: undefined })) + } +} diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index 91d2f2af6..cdcd2e242 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -6,10 +6,18 @@ import { connect } from 'react-redux' import styled from 'styled-components' import * as fieldTripActions from '../../actions/field-trip' +import Icon from '../narrative/icon' +import { + getActiveFieldTripRequest, + getGroupSize, + GROUP_FIELDS, + PAYMENT_FIELDS, + TICKET_TYPES +} from '../../util/call-taker' + import DraggableWindow from './draggable-window' import EditableSection from './editable-section' import FieldTripNotes from './field-trip-notes' -import Icon from '../narrative/icon' import { Bold, Button, @@ -25,12 +33,6 @@ import { } from './styled' import TripStatus from './trip-status' import Updatable from './updatable' -import { - getGroupSize, - GROUP_FIELDS, - PAYMENT_FIELDS, - TICKET_TYPES -} from '../../util/call-taker' const WindowHeader = styled(DefaultWindowHeader)` margin-bottom: 0px; @@ -45,8 +47,6 @@ class FieldTripDetails extends Component { _getRequestLink = (path, isPublic = false) => `${this.props.datastoreUrl}/${isPublic ? 'public/' : ''}fieldtrip/${path}?requestId=${this.props.request.id}` - _onCloseActiveFieldTrip = () => this.props.setActiveFieldTrip(null) - _onToggleStatus = () => { const {request, setRequestStatus} = this.props if (request.status !== 'cancelled') { @@ -118,6 +118,7 @@ class FieldTripDetails extends Component { render () { const { addFieldTripNote, + clearActiveFieldTrip, deleteFieldTripNote, request, setRequestGroupSize, @@ -144,7 +145,7 @@ class FieldTripDetails extends Component { footer={this._renderFooter()} header={this._renderHeader()} height='375px' - onClickClose={this._onCloseActiveFieldTrip} + onClickClose={clearActiveFieldTrip} style={style} > @@ -208,18 +209,17 @@ class FieldTripDetails extends Component { } const mapStateToProps = (state, ownProps) => { - const {activeId, requests} = state.callTaker.fieldTrip - const request = requests.data.find(req => req.id === activeId) return { currentQuery: state.otp.currentQuery, datastoreUrl: state.otp.config.datastoreUrl, dateFormat: getDateFormat(state.otp.config), - request + request: getActiveFieldTripRequest(state) } } const mapDispatchToProps = { addFieldTripNote: fieldTripActions.addFieldTripNote, + clearActiveFieldTrip: fieldTripActions.clearActiveFieldTrip, deleteFieldTripNote: fieldTripActions.deleteFieldTripNote, editSubmitterNotes: fieldTripActions.editSubmitterNotes, fetchQueries: fieldTripActions.fetchQueries, diff --git a/lib/util/call-taker.js b/lib/util/call-taker.js index f3b378f99..15aedaf7a 100644 --- a/lib/util/call-taker.js +++ b/lib/util/call-taker.js @@ -1,8 +1,9 @@ -import {isTransit} from '@opentripplanner/core-utils/lib/itinerary' import {randId} from '@opentripplanner/core-utils/lib/storage' import moment from 'moment' +import { createSelector } from 'reselect' import {getRoutingParams} from '../actions/api' + import {getTimestamp} from './state' export const TICKET_TYPES = { @@ -161,6 +162,18 @@ export function getVisibleRequests (state) { }) } +/** + * Selector to get the active field trip request + */ +export const getActiveFieldTripRequest = createSelector( + state => state.callTaker?.fieldTrip.activeId, + state => state.callTaker?.fieldTrip.requests, + (activeId, requests) => { + if (!activeId || !requests) return + return requests.data.find(req => req.id === activeId) + } +) + /** * Utility to map an OTP MOD UI search object to a Call Taker datastore query * object. @@ -206,66 +219,63 @@ export function getTripFromRequest (request, outbound = false) { } /** - * Create trip plan from plan data with itineraries. Note: this is based on - * original code in OpenTripPlanner: - * https://github.com/ibi-group/OpenTripPlanner/blob/fdf972e590b809014e3f80160aeb6dde209dd1d4/src/client/js/otp/modules/planner/TripPlan.js#L27-L38 + * LZW-compress a string * - * FIXME: This still needs to be implemented for the field trip module. + * LZW functions adaped from jsolait library (LGPL) + * via http://stackoverflow.com/questions/294297/javascript-implementation-of-gzip */ -export function createTripPlan (planData, queryParams) { - const tripPlan = { - earliestStartTime: null, - latestEndTime: null, - planData, - queryParams, - itineraries: [] +export function lzwEncode (s) { + var dict = {} + var data = (s + '').split('') + var out = [] + var currChar + var phrase = data[0] + var code = 256 + let i + for (i = 1; i < data.length; i++) { + currChar = data[i] + if (dict[phrase + currChar] != null) { + phrase += currChar + } else { + out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0)) + dict[phrase + currChar] = code + code++ + phrase = currChar + } } - if (!planData) return - tripPlan.itineraries = tripPlan.planData.map(itinData => - createItinerary(itinData, tripPlan)) - const timeBounds = calculateTimeBounds(tripPlan.itineraries) - return {...tripPlan, ...timeBounds} + out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0)) + for (i = 0; i < out.length; i++) { + out[i] = String.fromCharCode(out[i]) + } + return out.join('') } /** - * Calculate time bounds for all of the itineraries. Note: this is based on - * original code in OpenTripPlanner: - * https://github.com/ibi-group/OpenTripPlanner/blob/fdf972e590b809014e3f80160aeb6dde209dd1d4/src/client/js/otp/modules/planner/TripPlan.js#L53-L66 + * Decompress an LZW-encoded string * - * FIXME: This still needs to be implemented for the field trip module. + * LZW functions adaped from jsolait library (LGPL) + * via http://stackoverflow.com/questions/294297/javascript-implementation-of-gzip */ -function calculateTimeBounds (itineraries) { - let earliestStartTime = null - let latestEndTime = null - itineraries.forEach(itin => { - earliestStartTime = (earliestStartTime == null || itin.getStartTime() < earliestStartTime) - ? itin.getStartTime() - : earliestStartTime - latestEndTime = (latestEndTime == null || itin.getEndTime() > latestEndTime) - ? itin.getEndTime() - : latestEndTime - }) - return {earliestStartTime, latestEndTime} -} - -/** - * Create itinerary. Note this is based on original code in OpenTripPlanner: - * https://github.com/ibi-group/OpenTripPlanner/blob/46e1f9ffd9a55f0c5409d25a34769cdaff2d8cbb/src/client/js/otp/modules/planner/Itinerary.js#L27-L40 - */ -function createItinerary (itinData, tripPlan) { - const itin = { - itinData, - tripPlan, - firstStopIds: [], - hasTransit: false, - totalWalk: 0 - } - itinData.legs.forEach(leg => { - if (isTransit(leg.mode)) { - itin.hasTransit = true - itin.firstStopIDs.push(leg.from.stopId) +export function lzwDecode (s) { + var dict = {} + var data = (s + '').split('') + var currChar = data[0] + var oldPhrase = currChar + var out = [currChar] + var code = 256 + var phrase + for (var i = 1; i < data.length; i++) { + var currCode = data[i].charCodeAt(0) + if (currCode < 256) { + phrase = data[i] + } else { + phrase = dict[currCode] ? dict[currCode] : (oldPhrase + currChar) } - if (leg.mode === 'WALK') itin.totalWalk += leg.distance - }) - return itin + out.push(phrase) + currChar = phrase.charAt(0) + dict[code] = oldPhrase + currChar + code++ + oldPhrase = phrase + } + return out.join('') } diff --git a/lib/util/state.js b/lib/util/state.js index 2df1f0555..0b2779360 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -7,7 +7,7 @@ import { createSelector } from 'reselect' import { MainPanelContent } from '../actions/ui' -import { getModuleConfig, Modules } from './config' +import { getActiveFieldTripRequest } from './call-taker' const { calculateFares } = coreUtils.itinerary @@ -245,7 +245,7 @@ const getFieldTripGroupCapacityForMode = createSelector( * @return {number} The maximum size of a field trip group that could * use this itinerary. */ -function calculateItineraryFieldTripGroupCapacity ({ +export function calculateItineraryFieldTripGroupCapacity ({ fieldTripModuleConfig, itinerary }) { @@ -263,37 +263,37 @@ function calculateItineraryFieldTripGroupCapacity ({ /** * Assigns itineraries to field trip subgroups. */ -function assignItinerariesToFieldTripGroups ({ - fieldTripModuleConfig, - fieldTripGroupSize, - itineraries -}) { - // logic to add field trip group sizes for each itinerary - const capacityConstrainedItineraries = [] - let remainingGroupSize = fieldTripGroupSize - - for (let i = 0; i < itineraries.length; i++) { - const itinerary = {...itineraries[i]} - - // calculate itinerary capacity - const capacity = calculateItineraryFieldTripGroupCapacity({ - fieldTripModuleConfig, - itinerary - }) - - // assign next largest possible field trip subgroup - itinerary.fieldTripGroupSize = Math.min(remainingGroupSize, capacity) - capacityConstrainedItineraries.push(itinerary) - remainingGroupSize -= capacity - - // exit loop if all of field trip group has been assigned an itinerary - if (remainingGroupSize <= 0) { - break - } - } - - return capacityConstrainedItineraries -} +// function assignItinerariesToFieldTripGroups ({ +// fieldTripModuleConfig, +// fieldTripGroupSize, +// itineraries +// }) { +// // logic to add field trip group sizes for each itinerary +// const capacityConstrainedItineraries = [] +// let remainingGroupSize = fieldTripGroupSize +// +// for (let i = 0; i < itineraries.length; i++) { +// const itinerary = {...itineraries[i]} +// +// // calculate itinerary capacity +// const capacity = calculateItineraryFieldTripGroupCapacity({ +// fieldTripModuleConfig, +// itinerary +// }) +// +// // assign next largest possible field trip subgroup +// itinerary.fieldTripGroupSize = Math.min(remainingGroupSize, capacity) +// capacityConstrainedItineraries.push(itinerary) +// remainingGroupSize -= capacity +// +// // exit loop if all of field trip group has been assigned an itinerary +// if (remainingGroupSize <= 0) { +// break +// } +// } +// +// return capacityConstrainedItineraries +// } /** * Get the active itineraries for the active search, which is dependent on @@ -308,16 +308,14 @@ export const getActiveItineraries = createSelector( state => state.otp.filter, getActiveSearchRealtimeResponse, state => state.otp.useRealtime, - state => getModuleConfig(state, Modules.FIELD_TRIP), - state => state.callTaker.fieldTrip.groupSize, + getActiveFieldTripRequest, ( config, nonRealtimeResponse, itinerarySortSettings, realtimeResponse, useRealtime, - fieldTripModuleConfig, - fieldTripGroupSize + activeFieldTripRequest ) => { // set response to use depending on useRealtime const response = (!nonRealtimeResponse && !realtimeResponse) @@ -349,17 +347,10 @@ export const getActiveItineraries = createSelector( const {direction, type} = sort // If no sort type is provided (e.g., because batch routing is not enabled), // do not sort itineraries (default sort from API response is used). - const sortedItineraries = !type + // Also, do not sort itineraries if the a field trip request is active + return (!type || Boolean(activeFieldTripRequest)) ? itineraries : itineraries.sort((a, b) => sortItineraries(type, direction, a, b, config)) - - return fieldTripModuleConfig - ? assignItinerariesToFieldTripGroups({ - fieldTripModuleConfig, - fieldTripGroupSize, - itineraries: sortedItineraries - }) - : sortedItineraries } ) From 810659db7b29a5cd8b2416a7889d1f7493ff421a Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Wed, 30 Jun 2021 18:02:41 -0700 Subject: [PATCH 09/35] refactor(field-trip): fully implement trip constraining --- lib/actions/api.js | 17 +- lib/actions/field-trip.js | 264 +++++++++++++++++++++++++---- lib/reducers/call-taker.js | 13 ++ lib/reducers/create-otp-reducer.js | 76 +++++++-- lib/util/call-taker.js | 37 ++++ lib/util/state.js | 88 ---------- package.json | 2 +- yarn.lock | 23 +-- 8 files changed, 364 insertions(+), 156 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index 8ba0b362c..a75fe0d6b 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -27,6 +27,10 @@ export const nonRealtimeRoutingResponse = createAction('NON_REALTIME_ROUTING_RES export const routingRequest = createAction('ROUTING_REQUEST') export const routingResponse = createAction('ROUTING_RESPONSE') export const routingError = createAction('ROUTING_ERROR') +export const setPendingRequests = createAction('SET_PENDING_REQUESTS') +// This action is used to replace a search's itineraries in case they need to be +// modified by some postprocess analysis such as in the field trip module +export const setActiveItineraries = createAction('SET_ACTIVE_ITINERARIES') export const toggleTracking = createAction('TOGGLE_TRACKING') export const rememberSearch = createAction('REMEMBER_SEARCH') export const forgetSearch = createAction('FORGET_SEARCH') @@ -81,8 +85,11 @@ function getActiveItinerary (state) { * that are no longer contained in the state we don't confuse the search IDs * with search IDs from the new session. If we were to use sequential numbers * as IDs, we would run into this problem. + * + * The updateSearchInReducer instructs the reducer to update an existing search + * if it exists. This is used by the field trip module. */ -export function routingQuery (searchId = null) { +export function routingQuery (searchId = null, updateSearchInReducer = false) { return async function (dispatch, getState) { // FIXME: batchId is searchId for now. const state = getState() @@ -111,7 +118,13 @@ export function routingQuery (searchId = null) { ({mode, params}) => ({mode, ...params}) ) : [{}] - dispatch(routingRequest({ activeItinerary, routingType, searchId, pending: iterations.length })) + dispatch(routingRequest({ + activeItinerary, + updateSearchInReducer, + routingType, + searchId, + pending: iterations.length + })) return Promise.all(iterations.map((injectedParams, i) => { const requestId = randId() // fetch a realtime route diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 8cf946402..510464aa0 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -1,5 +1,6 @@ +import { isTransit } from '@opentripplanner/core-utils/lib/itinerary' import { planParamsToQueryAsync } from '@opentripplanner/core-utils/lib/query' -import { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } from '@opentripplanner/core-utils/lib/time' +import { OTP_API_TIME_FORMAT } from '@opentripplanner/core-utils/lib/time' import { randId } from '@opentripplanner/core-utils/lib/storage' import moment from 'moment' import { serialize } from 'object-to-formdata' @@ -7,15 +8,17 @@ import qs from 'qs' import { createAction } from 'redux-actions' import { + getFieldTripGroupCapacityForMode, + getFormattedRequestTravelDate, getGroupSize, getTripFromRequest, // lzwEncode, sessionIsInvalid } from '../util/call-taker' import { getModuleConfig, Modules } from '../util/config' -import { getActiveItineraries } from '../util/state' +import { getActiveItineraries, getActiveSearch } from '../util/state' -import {routingQuery} from './api' +import {routingQuery, setActiveItineraries, setPendingRequests} from './api' import {toggleCallHistory} from './call-taker' import {clearActiveSearch, resetForm, setQueryParam} from './form' @@ -24,6 +27,8 @@ if (typeof (fetch) === 'undefined') require('isomorphic-fetch') /// PRIVATE ACTIONS const receivedFieldTrips = createAction('RECEIVED_FIELD_TRIPS') +const receivedTravelDateTrips = createAction('RECEIVED_TRAVEL_DATE_TRIPS') +const receiveTripHash = createAction('RECEIVE_TRIP_HASH') const requestingFieldTrips = createAction('REQUESTING_FIELD_TRIPS') const receivedFieldTripDetails = createAction('RECEIVED_FIELD_TRIP_DETAILS') const requestingFieldTripDetails = createAction('REQUESTING_FIELD_TRIP_DETAILS') @@ -83,8 +88,6 @@ export function fetchFieldTripDetails (requestId) { .catch(err => { alert(`Error fetching field trips: ${JSON.stringify(err)}`) }) - - // TODO: fetch trip IDs for day } } @@ -302,7 +305,7 @@ function planOutbound (request) { toPlace: request.endLocation }, config) const queryParams = { - date: moment(request.travelDate).format(OTP_API_DATE_FORMAT), + date: getFormattedRequestTravelDate(request), departArrive: 'ARRIVE', time: moment(request.arriveDestinationTime).format(OTP_API_TIME_FORMAT), ...locations @@ -321,7 +324,7 @@ function planInbound (request) { }, config) // clearTrip() const queryParams = { - date: moment(request.travelDate).format(OTP_API_DATE_FORMAT), + date: getFormattedRequestTravelDate(request), departArrive: 'DEPART', time: moment(request.leaveDestinationTime).format(OTP_API_TIME_FORMAT), ...locations @@ -337,66 +340,269 @@ function planInbound (request) { */ function makeFieldTripPlanRequests (request) { return async function (dispatch, getState) { + const prePlanState = getState() const fieldTripModuleConfig = getModuleConfig( - getState(), + prePlanState, Modules.FIELD_TRIP ) - // set numItineraries param for field trip requests - dispatch(setQueryParam({ numItineraries: 1 })) - // initialize the remaining group size to be the total group size - let remainingGroupSize = getGroupSize(request) + // request other known trip IDs that other field trips are using on the + // field trip request date + try { + await getTripIdsForTravelDate(dispatch, prePlanState, request) + } catch (e) { + alert( + `Error fetching trips for field trip travel date: ${JSON.stringify(e)}` + ) + return + } // create a new searchId to use for making all requests const searchId = randId() + // set numItineraries param for field trip requests + dispatch(setQueryParam({ numItineraries: 1 })) + + // create a new set to keep track of trips that should be banned + const bannedTrips = new Set() + // track number of requests made such that endless requesting doesn't occur const maxRequests = fieldTripModuleConfig?.maxRequests || 10 let numRequests = 0 - // make requests until - while (remainingGroupSize > 0) { + let shouldContinueSearching = true + + // make requests until enough itineraries have been found to accomodate + // group + while (shouldContinueSearching) { numRequests++ if (numRequests > maxRequests) { // max number of requests exceeded. Show error. alert('Number of trip requests exceeded without valid results') - return dispatch(clearActiveSearch()) + return doFieldTripPlanRequestCleanup(dispatch, searchId) } - // make next query - await dispatch(routingQuery(searchId)) + // make next query. The second param instructs the action/reducer whether + // or not to override previous search results in the state. + await dispatch(routingQuery(searchId, numRequests > 1)) + + // set the pending amount of requests to be 2 so that there will always + // seem to be potentially additional requests that have to be made. If + // there aren't after making this next request, the pending amount will + // be set to 0. This needs to happen after the routingQuery so the search + // is defined. + dispatch(setPendingRequests({ searchId, pending: 2 })) // obtain trip hashes from OTP Index API - const state = getState() - await getTripHashesFromActiveItineraries(state) + await getTripHashesFromActiveItineraries() // check trip validity and calculate itinerary capacity - - // calculate remaining group size - // FIXME: actually implement and remove lint-passing placeholder - remainingGroupSize -= 12345 - - // set parameters for next iteration + const { + assignedItinerariesByResponse, + remainingGroupSize, + tripsToBanInSubsequentSearches + } = checkValidityAndCapacity( + getState(), + request + ) + + // always update itineraries on each itineration + dispatch(setActiveItineraries({ + assignedItinerariesByResponse, + searchId + })) + + if (remainingGroupSize <= 0) { + // All members of the field trip group have been assigned! + shouldContinueSearching = false + doFieldTripPlanRequestCleanup(dispatch, searchId) + } else { + // Not enough acceptable itineraries have been generated. Request more. + + // Update banned trips + tripsToBanInSubsequentSearches.forEach(tripId => { + bannedTrips.add(tripId) + }) + dispatch(setQueryParam({ bannedTrips: [...bannedTrips].join(',') })) + } } } } -function getTripHashesFromActiveItineraries (state) { - return async function (dispatch, getState) { +async function getTripIdsForTravelDate (dispatch, state, request) { + const {datastoreUrl} = state.otp.config + const {sessionId} = state.callTaker.session + const formattedTravelDate = getFormattedRequestTravelDate(request, 'MM/DD/YYYY') + const params = { + date: formattedTravelDate, + sessionId + } + + const res = await fetch( + `${datastoreUrl}/fieldtrip/getTrips?${qs.stringify(params)}` + ) + const fieldTrips = await res.json() + + // add passengers and converted tripId to trips + const trips = [] + fieldTrips.forEach(fieldTrip => { + fieldTrip.groupItineraries.forEach(groupItinerary => { + groupItinerary.trips.forEach(gtfsTrip => { + // tripIds still stored as 'agencyAndId' in DB, so convert them to + // be compatible with OTP responses + gtfsTrip.tripId = gtfsTrip.agencyAndId.replace('_', ':') + // Add passengers to each trip from group itinerary + gtfsTrip.passengers = groupItinerary.passengers + trips.push(gtfsTrip) + }) + }) + }) + await dispatch(receivedTravelDateTrips(trips)) +} + +function getTripHashesFromActiveItineraries () { + return function (dispatch, getState) { + const state = getState() const activeItineraries = getActiveItineraries(state) + const { tripHashLookup } = state.otp.callTaker.fieldTrip const tripHashesToRequest = [] activeItineraries.forEach(itinerary => { itinerary.legs.forEach(leg => { - + if (leg.tripId && !tripHashLookup[leg.tripId]) { + tripHashesToRequest.push(leg.tripId) + } }) }) + const api = state.config.api + const baseUrl = `${api.host}${api.port ? ':' + api.port : ''}${api.path}` + return Promise.all(tripHashesToRequest.map(tripId => { - return fetch() + return fetch(`${baseUrl}/index/trips/${tripId}/semanticHash`) + .then(res => res.text()) + .then(hash => dispatch(receiveTripHash({ hash, tripId }))) })) } } +function checkValidityAndCapacity (state, request) { + const fieldTripModuleConfig = getModuleConfig(state, Modules.FIELD_TRIP) + const minimumAllowableRemainingCapacity = + fieldTripModuleConfig?.minimumAllowableRemainingCapacity || 10 + const { travelDateTripsInUse, tripHashLookup } = state.callTaker.fieldTrip + const activeSearch = getActiveSearch(state) + + // initialize the remaining group size to be the total group size + let remainingGroupSize = getGroupSize(request) + const assignedItinerariesByResponse = {} + const tripsToBanInSubsequentSearches = [] + + // iterate through responses as we need to keep track of the response that + // each itinerary came from so that they can be updated + activeSearch.response.forEach((response, responseIdx) => { + if (!response.plan) { + // An error might have occurred! + return + } + + // iterate through itineraries to check validity and assign field trip + // groups + response.plan.itineraries.forEach((itinerary, idx) => { + let itineraryCapacity = Number.POSITIVE_INFINITY + + // check each individual trip to see if there aren't any conflicts + itinerary.legs.forEach(leg => { + // non-transit legs have infinite capacity + if (!isTransit(leg.mode)) { + return + } + const { tripId } = leg + + // this variable is used to track how many other field trips are using a + // particular trip + let capacityInUse = 0 + + // iterate over trips that are already being used by other field trips + travelDateTripsInUse.forEach(tripInUse => { + // check if the trip is being used by another field trip + let sameVehicleTrip = false + if (tripId in tripHashLookup && tripInUse.tripHash) { + // use the trip hashes if available + sameVehicleTrip = (tripHashLookup[tripId] === tripInUse.tripHash) + } else { + // as fallback, compare the tripId strings + sameVehicleTrip = (tripId === tripInUse.tripId) + } + // not used by another vehicle, so not used by this other field trip + if (!sameVehicleTrip) return + + // check if the stop ranges overlap. It is OK if one field trip begins + // where the other ends. + if ( + leg.from.stopIndex >= tripInUse.toStopIndex || + leg.to.stopIndex <= tripInUse.fromStopIndex + ) { + // legs do not overlap, so not used by this other field trip + return + } + + // ranges overlap! Add number of passengers on this other field trip + // to total capacity in use + capacityInUse += tripInUse.passengers + }) + + // check if the remaining capacity on this trip is enough to allow more + // field trip passengers on board + const legModeCapacity = getFieldTripGroupCapacityForMode( + fieldTripModuleConfig, + leg.mode + ) + let remainingTripCapacity = legModeCapacity - capacityInUse + if (remainingTripCapacity < minimumAllowableRemainingCapacity) { + // This trip is already too "full" to allow any addition field trips + // on board. Ban this trip in future searches and don't use this + // itinerary in final results (set trip and itinerary capacity to 0). + remainingTripCapacity = 0 + } + + // always ban trips found in itineraries so that subsequent searches + // don't encounter them. + // TODO: a more advanced way of doing things might be to ban trip + // sequences to not find the same exact sequence, but also + // individual trips that are too full. + tripsToBanInSubsequentSearches.push(tripId) + + itineraryCapacity = Math.min(itineraryCapacity, remainingTripCapacity) + }) + + if (itineraryCapacity > 0) { + // itinerary is possible, add to list and update remaining group size. + // A field trip response is gauranteed to have only one itinerary, so it + // ok to set the itinerary by response as an array with a single + // itinerary. + assignedItinerariesByResponse[responseIdx] = [{ + ...itinerary, + fieldTripGroupSize: Math.min(itineraryCapacity, remainingGroupSize) + }] + remainingGroupSize -= itineraryCapacity + } + }) + }) + + return { + assignedItinerariesByResponse, + remainingGroupSize, + tripsToBanInSubsequentSearches + } +} + +function doFieldTripPlanRequestCleanup (dispatch, searchId) { + // set pending searches to 0 to indicate searching is finished + dispatch(setPendingRequests({ searchId, pending: 0 })) + // clear banned trips query param + dispatch(setQueryParam({ bannedTrips: undefined })) +} + /** * Set group size for a field trip request. Group size consists of numStudents, * numFreeStudents, and numChaperones. diff --git a/lib/reducers/call-taker.js b/lib/reducers/call-taker.js index a8bf02574..78dc6f2c2 100644 --- a/lib/reducers/call-taker.js +++ b/lib/reducers/call-taker.js @@ -26,6 +26,8 @@ function createCallTakerReducer (config) { status: FETCH_STATUS.UNFETCHED, data: [] }, + travelDateTripsInUse: [], + tripHashLookup: {}, visible: false }, mailables: { @@ -98,6 +100,17 @@ function createCallTakerReducer (config) { fieldTrip: { requests: { data: { [index]: { $set: fieldTrip } } } } }) } + case 'RECEIVED_TRAVEL_DATE_TRIPS': { + return update(state, { + fieldTrip: { travelDateTripsInUse: { $set: action.payload } } + }) + } + case 'RECEIVE_TRIP_HASH': { + const { hash, tripId } = action.payload + return update(state, { + fieldTrip: { tripHashLookup: { [tripId]: { $set: hash } } } + }) + } case 'RECEIVED_QUERIES': { const {callId, queries} = action.payload const {data} = state.callHistory.calls diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 14d835197..896f9e8ed 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -256,22 +256,37 @@ function createOtpReducer (config) { const activeSearch = state.searches[searchId] switch (action.type) { case 'ROUTING_REQUEST': - const { activeItinerary, pending } = action.payload - return update(state, { - searches: { - [searchId]: { - $set: { - activeItinerary, - activeLeg: null, - activeStep: null, - pending, - // FIXME: get query from action payload? - query: clone(state.currentQuery), - response: [], - timestamp: getTimestamp() - } - } - }, + const { + activeItinerary, + pending, + updateSearchInReducer + } = action.payload + const searchUpdate = updateSearchInReducer + ? { + activeItinerary: { $set: activeItinerary }, + activeLeg: { $set: null }, + activeStep: { $set: null }, + pending: { $set: pending }, + // FIXME: get query from action payload? + query: { $set: clone(state.currentQuery) }, + // omit requests reset to make sure requests can be added to this + // search + timestamp: { $set: getTimestamp() } + } + : { + $set: { + activeItinerary, + activeLeg: null, + activeStep: null, + pending, + // FIXME: get query from action payload? + query: clone(state.currentQuery), + response: [], + timestamp: getTimestamp() + } + } + return update(state, { + searches: { [searchId]: searchUpdate }, activeSearchId: { $set: searchId } }) case 'ROUTING_ERROR': @@ -312,6 +327,33 @@ function createOtpReducer (config) { } } }) + case 'SET_PENDING_REQUESTS': + return update(state, { + searches: { + [searchId]: { + pending: { $set: action.payload.pending } + } + } + }) + case 'SET_ACTIVE_ITINERARIES': + const responseUpdate = {} + Object.entries(action.payload.assignedItinerariesByResponse) + .forEach(([ responseIdx, responsePlanItineraries ]) => { + responseUpdate[responseIdx] = { + plan: { + itineraries: { + $set: responsePlanItineraries + } + } + } + }) + return update(state, { + searches: { + [searchId]: { + response: responseUpdate + } + } + }) case 'BIKE_RENTAL_REQUEST': return update(state, { overlay: { @@ -937,7 +979,7 @@ function createOtpReducer (config) { }) case 'UPDATE_OVERLAY_VISIBILITY': const mapOverlays = clone(state.config.map.overlays) - for (let key in action.payload) { + for (const key in action.payload) { const overlay = mapOverlays.find(o => o.name === key) overlay.visible = action.payload[key] } diff --git a/lib/util/call-taker.js b/lib/util/call-taker.js index 15aedaf7a..c71295ff5 100644 --- a/lib/util/call-taker.js +++ b/lib/util/call-taker.js @@ -1,4 +1,5 @@ import {randId} from '@opentripplanner/core-utils/lib/storage' +import { OTP_API_DATE_FORMAT } from '@opentripplanner/core-utils/lib/time' import moment from 'moment' import { createSelector } from 'reselect' @@ -218,6 +219,42 @@ export function getTripFromRequest (request, outbound = false) { return trip } +export function getFormattedRequestTravelDate ( + request, + dateFormat = OTP_API_DATE_FORMAT +) { + return moment(request.travelDate).format(dateFormat) +} + +const defaultFieldTripModeCapacities = { + 'TRAM': 80, + 'SUBWAY': 120, + 'RAIL': 80, + 'BUS': 40, + 'FERRY': 100, + 'CABLE_CAR': 20, + 'GONDOLA': 10, + 'FUNICULAR': 20 +} +const unknownModeCapacity = 10 + +/** + * Calculates the mode capacity based on the field trip module mode capacities + * (if it exists) or from the above default lookup of mode capacities or if + * given an unknown mode, then the unknownModeCapacity is returned. + * + * @param {Object} config the app-wide config + * @param {string} mode the OTP mode + */ +export const getFieldTripGroupCapacityForMode = createSelector( + fieldTripModuleConfig => fieldTripModuleConfig?.modeCapacities, + (fieldTripModuleConfig, mode) => mode, + (configModeCapacities, mode) => (configModeCapacities && + configModeCapacities[mode]) || + defaultFieldTripModeCapacities[mode] || + unknownModeCapacity +) + /** * LZW-compress a string * diff --git a/lib/util/state.js b/lib/util/state.js index 0b2779360..af4527ba0 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -207,94 +207,6 @@ const hashItinerary = memoize( ) ) -const defaultFieldTripModeCapacities = { - 'TRAM': 80, - 'SUBWAY': 120, - 'RAIL': 80, - 'BUS': 40, - 'FERRY': 100, - 'CABLE_CAR': 20, - 'GONDOLA': 10, - 'FUNICULAR': 20 -} -const unknownModeCapacity = 10 - -/** - * Calculates the mode capacity based on the field trip module mode capacities - * (if it exists) or from the above default lookup of mode capacities or if - * given an unknown mode, then the unknownModeCapacity is returned. - * - * @param {Object} config the app-wide config - * @param {string} mode the OTP mode - */ -const getFieldTripGroupCapacityForMode = createSelector( - fieldTripModuleConfig => fieldTripModuleConfig?.modeCapacities, - (fieldTripModuleConfig, mode) => mode, - (configModeCapacities, mode) => (configModeCapacities && - configModeCapacities[mode]) || - defaultFieldTripModeCapacities[mode] || - unknownModeCapacity -) - -/** - * Calculates the capacity for a field trip group of a given itinerary - * - * @param {Object} param - * @param {Object} param.fieldTripModuleConfig The field trip module config - * @param {Object} param.itinerary An OTP itinerary - * @return {number} The maximum size of a field trip group that could - * use this itinerary. - */ -export function calculateItineraryFieldTripGroupCapacity ({ - fieldTripModuleConfig, - itinerary -}) { - return itinerary.legs.reduce((constrainingLegCapacity, leg) => { - if (!leg.transitLeg) { - return constrainingLegCapacity - } - return Math.min( - constrainingLegCapacity, - getFieldTripGroupCapacityForMode(fieldTripModuleConfig, leg.mode) - ) - }, 10000) -} - -/** - * Assigns itineraries to field trip subgroups. - */ -// function assignItinerariesToFieldTripGroups ({ -// fieldTripModuleConfig, -// fieldTripGroupSize, -// itineraries -// }) { -// // logic to add field trip group sizes for each itinerary -// const capacityConstrainedItineraries = [] -// let remainingGroupSize = fieldTripGroupSize -// -// for (let i = 0; i < itineraries.length; i++) { -// const itinerary = {...itineraries[i]} -// -// // calculate itinerary capacity -// const capacity = calculateItineraryFieldTripGroupCapacity({ -// fieldTripModuleConfig, -// itinerary -// }) -// -// // assign next largest possible field trip subgroup -// itinerary.fieldTripGroupSize = Math.min(remainingGroupSize, capacity) -// capacityConstrainedItineraries.push(itinerary) -// remainingGroupSize -= capacity -// -// // exit loop if all of field trip group has been assigned an itinerary -// if (remainingGroupSize <= 0) { -// break -// } -// } -// -// return capacityConstrainedItineraries -// } - /** * Get the active itineraries for the active search, which is dependent on * whether realtime or non-realtime results should be displayed diff --git a/package.json b/package.json index c464afc33..220bb2a3c 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "dependencies": { "@auth0/auth0-react": "^1.1.0", "@opentripplanner/base-map": "^1.0.5", - "@opentripplanner/core-utils": "^3.1.1", + "@opentripplanner/core-utils": "^3.2.1", "@opentripplanner/endpoints-overlay": "^1.0.6", "@opentripplanner/from-to-location-picker": "^1.0.4", "@opentripplanner/geocoder": "^1.0.2", diff --git a/yarn.lock b/yarn.lock index a90a1d5c7..8329c2f76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1495,25 +1495,10 @@ "@opentripplanner/core-utils" "^3.0.4" prop-types "^15.7.2" -"@opentripplanner/core-utils@^3.0.0", "@opentripplanner/core-utils@^3.0.4": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-3.1.0.tgz#4626807893874503c5d365b05e5db4a1b3bf163c" - integrity sha512-EYhIv6nQdmadmkQumuXGrEwvRrhUl8xDPckSv24y3o9FX7uk8h5Gqe4FPdjnBT1eOj/XUj0/fZzNcETHUvZvUg== - dependencies: - "@mapbox/polyline" "^1.1.0" - "@opentripplanner/geocoder" "^1.0.2" - "@turf/along" "^6.0.1" - bowser "^2.7.0" - lodash.isequal "^4.5.0" - moment "^2.24.0" - moment-timezone "^0.5.27" - prop-types "^15.7.2" - qs "^6.9.1" - -"@opentripplanner/core-utils@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-3.1.1.tgz#d3582cbfe7a84cd5370d63a857dd26dff17591e5" - integrity sha512-20uY3uh2TawPG9PWLLWi5TVJ8F1/bws6cRaRfRNwk11qtdeFQNnVqxZGEd8q1OQSW7UI15cM45SCrK7V7Kxn6A== +"@opentripplanner/core-utils@^3.0.0", "@opentripplanner/core-utils@^3.0.4", "@opentripplanner/core-utils@^3.1.1", "@opentripplanner/core-utils@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-3.2.1.tgz#eee2db0195f7fdefbce62154b99ae719f54af699" + integrity sha512-PgFUKGlOBI+McEqkCPaNYWLfdZvoX56CPa4LLyXmTX7Wy9EiHMarEI5IbSdIH5OWjhBDq5QqOOQnZvvcItpf8Q== dependencies: "@mapbox/polyline" "^1.1.0" "@opentripplanner/geocoder" "^1.0.2" From 58d8b65439554d117fb3aa903dd8e3072514bd94 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Wed, 30 Jun 2021 18:36:53 -0700 Subject: [PATCH 10/35] test: update snapshots and make them deterministic --- __tests__/actions/__snapshots__/api.js.snap | 59 ++++++++++++++++++- __tests__/actions/api.js | 13 ++++ .../__snapshots__/create-otp-reducer.js.snap | 6 -- 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/__tests__/actions/__snapshots__/api.js.snap b/__tests__/actions/__snapshots__/api.js.snap index 6fe83a8fd..f5cf71d3b 100644 --- a/__tests__/actions/__snapshots__/api.js.snap +++ b/__tests__/actions/__snapshots__/api.js.snap @@ -12,6 +12,7 @@ Array [ "pending": 1, "routingType": "ITINERARY", "searchId": "abcd1234", + "updateSearchInReducer": false, }, "type": "ROUTING_REQUEST", }, @@ -19,6 +20,28 @@ Array [ Array [ [Function], ], + Array [ + Object { + "payload": Object { + "requestId": "abcd1238", + "response": Object { + "fake": "response", + }, + "searchId": "abcd1234", + }, + "type": "ROUTING_RESPONSE", + }, + ], + Array [ + Object { + "payload": Object { + "error": [TypeError: Cannot read property 'trackRecent' of undefined], + "requestId": "abcd1239", + "searchId": "abcd1234", + }, + "type": "ROUTING_ERROR", + }, + ], Array [ [Function], ], @@ -28,7 +51,8 @@ Array [ "activeItinerary": 0, "pending": 1, "routingType": "ITINERARY", - "searchId": "abcd1235", + "searchId": "abcd1237", + "updateSearchInReducer": false, }, "type": "ROUTING_REQUEST", }, @@ -36,6 +60,16 @@ Array [ Array [ [Function], ], + Array [ + Object { + "payload": Object { + "error": [Error: Received error from server], + "requestId": "abcd1240", + "searchId": "abcd1237", + }, + "type": "ROUTING_ERROR", + }, + ], ] `; @@ -51,6 +85,7 @@ Array [ "pending": 1, "routingType": "ITINERARY", "searchId": "abcd1234", + "updateSearchInReducer": false, }, "type": "ROUTING_REQUEST", }, @@ -58,6 +93,28 @@ Array [ Array [ [Function], ], + Array [ + Object { + "payload": Object { + "requestId": "abcd1235", + "response": Object { + "fake": "response", + }, + "searchId": "abcd1234", + }, + "type": "ROUTING_RESPONSE", + }, + ], + Array [ + Object { + "payload": Object { + "error": [TypeError: Cannot read property 'trackRecent' of undefined], + "requestId": "abcd1236", + "searchId": "abcd1234", + }, + "type": "ROUTING_ERROR", + }, + ], ] `; diff --git a/__tests__/actions/api.js b/__tests__/actions/api.js index 5238a8c6a..3aedcef0a 100644 --- a/__tests__/actions/api.js +++ b/__tests__/actions/api.js @@ -44,6 +44,8 @@ describe('actions > api', () => { }) await routingQueryAction(mockDispatch, mockGetState) + + setMockRequestIds(mockDispatch.mock.calls) expect(mockDispatch.mock.calls).toMatchSnapshot() }) @@ -57,7 +59,18 @@ describe('actions > api', () => { }) await routingQueryAction(mockDispatch, mockGetState) + setMockRequestIds(mockDispatch.mock.calls) expect(mockDispatch.mock.calls).toMatchSnapshot() }) }) }) + +function setMockRequestIds (calls) { + calls.forEach(call => { + call.forEach(action => { + if (action.payload && action.payload.requestId) { + action.payload.requestId = randId() + } + }) + }) +} diff --git a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap index 35365d63f..ba62a9748 100644 --- a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap +++ b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap @@ -25,9 +25,7 @@ Object { "transitOperators": Array [], }, "currentQuery": Object { - "bannedRoutes": "", "bikeSpeed": 3.58, - "companies": null, "date": "2019-08-04", "departArrive": "NOW", "endTime": "09:00", @@ -44,7 +42,6 @@ Object { "optimize": "QUICK", "optimizeBike": "SAFE", "otherThanPreferredRoutesPenalty": 900, - "preferredRoutes": "", "routingType": "ITINERARY", "showIntermediateStops": true, "startTime": "07:00", @@ -108,9 +105,7 @@ Object { "user": Object { "autoRefreshStopTimes": true, "defaults": Object { - "bannedRoutes": "", "bikeSpeed": 3.58, - "companies": null, "endTime": "09:00", "ignoreRealtimeUpdates": false, "intermediatePlaces": Array [], @@ -124,7 +119,6 @@ Object { "optimize": "QUICK", "optimizeBike": "SAFE", "otherThanPreferredRoutesPenalty": 900, - "preferredRoutes": "", "routingType": "ITINERARY", "showIntermediateStops": true, "startTime": "07:00", From 709f5351b9068eccc18596ccf07d12fdb40f4183 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Thu, 1 Jul 2021 14:49:16 -0700 Subject: [PATCH 11/35] feat(field-trip): implement ability to save field trip itineraries --- lib/actions/field-trip.js | 162 ++++++++++++++++++---------- lib/components/admin/trip-status.js | 15 ++- lib/util/state.js | 6 +- 3 files changed, 117 insertions(+), 66 deletions(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 3949e3515..c219d9250 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -1,5 +1,8 @@ import { isTransit } from '@opentripplanner/core-utils/lib/itinerary' -import { planParamsToQueryAsync } from '@opentripplanner/core-utils/lib/query' +import { + getRoutingParams, + planParamsToQueryAsync +} from '@opentripplanner/core-utils/lib/query' import { OTP_API_TIME_FORMAT } from '@opentripplanner/core-utils/lib/time' import { randId } from '@opentripplanner/core-utils/lib/storage' import moment from 'moment' @@ -12,7 +15,7 @@ import { getFormattedRequestTravelDate, getGroupSize, getTripFromRequest, - // lzwEncode, + lzwEncode, sessionIsInvalid } from '../util/call-taker' import { getModuleConfig, Modules } from '../util/config' @@ -39,6 +42,10 @@ export const setActiveFieldTrip = createAction('SET_ACTIVE_FIELD_TRIP') export const setGroupSize = createAction('SET_GROUP_SIZE') export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS') +const FIELD_TRIP_DATE_FORMAT = 'MM/DD/YYYY' +const FIELD_TRIP_DATE_TIME_FORMAT = 'YYYY-MM-DDTHH:mm:ss' +const FIELD_TRIP_TIME_FORMAT = 'HH:mm:ss' + /** * Fully reset form and toggle field trips (and close call history if open). */ @@ -157,7 +164,7 @@ export function editSubmitterNotes (request, submitterNotes) { } } -export function saveRequestTrip (request, outbound) { +export function saveRequestTripItineraries (request, outbound) { return async function (dispatch, getState) { const state = getState() const { session } = state.callTaker @@ -171,34 +178,26 @@ export function saveRequestTrip (request, outbound) { // Show a confirmation dialog before overwriting existing plan if (!overwriteExistingRequestTripsConfirmed(request, outbound)) return - // construct data for request to save - const data = { - // itins: itineraries.map(createFieldTripItinerarySaveData), - requestId: request.id, - sessionId: session.sessionId, - trip: { - createdBy: session.username, - departure: moment(getEarliestStartTime(itineraries)).format('YYYY-MM-DDTHH:mm:ss'), - // destination: getEndOTPString(), - // origin: getStartOTPString(), - passengers: getGroupSize(request), - queryParams: JSON.stringify(state.otp.currentQuery), - requestOrder: outbound ? 0 : 1 - } + // Send data to server for saving. + let text + try { + const res = await fetch( + `${state.otp.config.datastoreUrl}/fieldtrip/newTrip`, + { + method: 'POST', + body: makeSaveFieldTripItinerariesData(request, outbound, state) + } + ) + text = await res.text() + } catch (e) { + return alert(`Failed to save itineraries: ${JSON.stringify(e)}`) + } + + if (text === '-1') { + return alert('Cannot Save Plan: This plan could not be saved due to a lack of capacity on one or more vehicles. Please re-plan your trip.') } - console.log(data) - - // do actual saving of trip - // const res = await fetch(`${state.otp.config.datastoreUrl}/calltaker/call`, - // {method: 'POST', body: makeFieldTripData(request)} - // ) - // - // this.serverRequest('/fieldtrip/newTrip', 'POST', data, _.bind(function(data) { - // if(data === -1) { - // otp.widgets.Dialogs.showOkDialog("This plan could not be saved due to a lack of capacity on one or more vehicles. Please re-plan your trip.", "Cannot Save Plan"); - // } - // else successCallback.call(this, data); - // }, this)); + + dispatch(fetchFieldTripDetails(request.id)) } } @@ -229,7 +228,7 @@ function fieldTripPlanIsInvalid (request, itineraries) { planDeparture.year() !== requestDate.year() ) { alert( - `Planned trip date (${planDeparture.format('MM/DD/YYYY')}) is not the requested day of travel (${requestDate.format('MM/DD/YYYY')})` + `Planned trip date (${planDeparture.format(FIELD_TRIP_DATE_FORMAT)}) is not the requested day of travel (${requestDate.format(FIELD_TRIP_DATE_FORMAT)})` ) return true } @@ -257,34 +256,79 @@ function overwriteExistingRequestTripsConfirmed (request, outbound) { return true } -// function createFieldTripItinerarySaveData (itinerary) { -// const result = {} -// result.passengers = itinerary.fieldTripGroupSize -// data['itins['+i+'].itinData'] = lzwEncode(JSON.stringify(itin.itinData)); -// data['itins['+i+'].timeOffset'] = otp.config.timeOffset || 0; -// -// var legs = itin.getTransitLegs(); -// -// for(var l = 0; l < legs.length; l++) { -// var leg = legs[l]; -// var routeName = (leg.routeShortName !== null ? ('(' + leg.routeShortName + ') ') : '') + (leg.routeLongName || ""); -// var tripHash = this.tripHashLookup[leg.tripId]; -// -// data['gtfsTrips['+i+']['+l+'].depart'] = moment(leg.startTime).format("HH:mm:ss"); -// data['gtfsTrips['+i+']['+l+'].arrive'] = moment(leg.endTime).format("HH:mm:ss"); -// data['gtfsTrips['+i+']['+l+'].agencyAndId'] = leg.tripId; -// data['gtfsTrips['+i+']['+l+'].tripHash'] = tripHash; -// data['gtfsTrips['+i+']['+l+'].routeName'] = routeName; -// data['gtfsTrips['+i+']['+l+'].fromStopIndex'] = leg.from.stopIndex; -// data['gtfsTrips['+i+']['+l+'].toStopIndex'] = leg.to.stopIndex; -// data['gtfsTrips['+i+']['+l+'].fromStopName'] = leg.from.name; -// data['gtfsTrips['+i+']['+l+'].toStopName'] = leg.to.name; -// data['gtfsTrips['+i+']['+l+'].headsign'] = leg.headsign; -// data['gtfsTrips['+i+']['+l+'].capacity'] = itin.getModeCapacity(leg.mode); -// if(leg.tripBlockId) data['gtfsTrips['+i+']['+l+'].blockId'] = leg.tripBlockId; -// } -// return result -// } +/** + * Construct data for request to save + */ +function makeSaveFieldTripItinerariesData (request, outbound, state) { + const { fieldTrip, session } = state.callTaker + const { tripHashLookup } = fieldTrip + const { config, currentQuery } = state.otp + const fieldTripModuleConfig = getModuleConfig(state, Modules.FIELD_TRIP) + const itineraries = getActiveItineraries(state) + + const data = { + gtfsTrips: [], + itins: [], + requestId: request.id, + sessionId: session.sessionId, + trip: { + createdBy: session.username, + departure: moment(getEarliestStartTime(itineraries)) + .format(FIELD_TRIP_DATE_TIME_FORMAT), + destination: getOTPLocationString(currentQuery.from), + origin: getOTPLocationString(currentQuery.to), + passengers: getGroupSize(request), + queryParams: JSON.stringify(getRoutingParams(config, currentQuery)), + requestOrder: outbound ? 0 : 1 + } + } + + itineraries.forEach((itinerary, itinIdx) => { + const itineraryDataToSave = { + itinData: lzwEncode(JSON.stringify(itinerary)), + passengers: itinerary.fieldTripGroupSize, + timeOffset: 0 + } + + const gtfsTripsForItinerary = [] + itinerary.legs.filter(leg => isTransit(leg.mode)) + .forEach(leg => { + let routeName = leg.routeShortName + ? `(${leg.routeShortName}) ` + : '' + routeName = `${routeName}${leg.routeLongName}` + const gtfsTrip = { + depart: moment(leg.startTime).format(FIELD_TRIP_TIME_FORMAT), + arrive: moment(leg.endTime).format(FIELD_TRIP_TIME_FORMAT), + agencyAndId: leg.tripId, + tripHash: tripHashLookup[leg.tripId], + routeName, + fromStopIndex: leg.from.stopIndex, + toStopIndex: leg.to.stopIndex, + fromStopName: leg.from.name, + toStopName: leg.to.name, + headsign: leg.headsign, + capacity: getFieldTripGroupCapacityForMode( + fieldTripModuleConfig, + leg.mode + ) + } + if (leg.tripBlockId) gtfsTrip.blockId = leg.tripBlockId + gtfsTripsForItinerary.push(gtfsTrip) + }) + + data.itins.push(itineraryDataToSave) + data.gtfsTrips.push(gtfsTripsForItinerary) + }) + return serialize(data, { indices: true }) +} + +function getOTPLocationString (location) { + const llString = `${location.lat},${location.lon}` + return location.name + ? `${location.name}::${llString}` + : llString +} export function planTrip (request, outbound) { return function (dispatch, getState) { diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index 741b12912..2ae185fcc 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -6,6 +6,8 @@ import { connect } from 'react-redux' import * as fieldTripActions from '../../actions/field-trip' import * as formActions from '../../actions/form' import Icon from '../narrative/icon' +import { getTripFromRequest } from '../../util/call-taker' + import { Bold, Button, @@ -13,7 +15,6 @@ import { Header, Para } from './styled' -import { getTripFromRequest } from '../../util/call-taker' class TripStatus extends Component { _getTrip = () => getTripFromRequest(this.props.request, this.props.outbound) @@ -47,9 +48,15 @@ class TripStatus extends Component { ? : - _onPlanTrip = () => this.props.planTrip(this.props.request, this.props.outbound) + _onPlanTrip = () => { + const { outbound, planTrip, request } = this.props + planTrip(request, outbound) + } - _onSaveTrip = () => this.props.saveRequestTrip(this.props.request, this.props.outbound) + _onSaveTrip = () => { + const { outbound, request, saveRequestTripItineraries } = this.props + saveRequestTripItineraries(request, outbound) + } render () { const {outbound, request} = this.props @@ -102,7 +109,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { planTrip: fieldTripActions.planTrip, - saveRequestTrip: fieldTripActions.saveRequestTrip, + saveRequestTripItineraries: fieldTripActions.saveRequestTripItineraries, setQueryParam: formActions.setQueryParam } diff --git a/lib/util/state.js b/lib/util/state.js index af4527ba0..c70add7ed 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -93,15 +93,15 @@ export function getResponsesWithErrors (state) { response.forEach(res => { if (res) { if (res.error) { - let msg = res.error.msg + let msg = res.error.msg || 'An error occured while planning a trip' // include the modes if doing batch routing - if (showModes) { + if (showModes && res.requestParameters?.mode) { const mode = humanReadableMode(res.requestParameters.mode) msg = `No trip found for ${mode}. ${msg.replace(/^No trip found. /, '')}` } tripPlanningErrors.push({ id: res.error.id, - modes: res.requestParameters.mode.split(','), + modes: res.requestParameters?.mode?.split(','), msg }) } From 8c2afa10f9de7cbd7dfb41d1feae5b55247d80e3 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Thu, 1 Jul 2021 15:58:49 -0700 Subject: [PATCH 12/35] docs: improve code comments, fix typos, move a few functions --- __tests__/actions/api.js | 3 + lib/actions/field-trip.js | 129 +++++++++++++++++++++++++++++++++++--- lib/util/call-taker.js | 37 ----------- lib/util/state.js | 2 +- 4 files changed, 123 insertions(+), 48 deletions(-) diff --git a/__tests__/actions/api.js b/__tests__/actions/api.js index 3aedcef0a..8b5dee1c2 100644 --- a/__tests__/actions/api.js +++ b/__tests__/actions/api.js @@ -65,6 +65,9 @@ describe('actions > api', () => { }) }) +/** + * Sets the requestId values as needed to deterministic IDs. + */ function setMockRequestIds (calls) { calls.forEach(call => { call.forEach(action => { diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index c219d9250..c5c840a8c 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -3,7 +3,10 @@ import { getRoutingParams, planParamsToQueryAsync } from '@opentripplanner/core-utils/lib/query' -import { OTP_API_TIME_FORMAT } from '@opentripplanner/core-utils/lib/time' +import { + OTP_API_DATE_FORMAT, + OTP_API_TIME_FORMAT +} from '@opentripplanner/core-utils/lib/time' import { randId } from '@opentripplanner/core-utils/lib/storage' import moment from 'moment' import { serialize } from 'object-to-formdata' @@ -11,8 +14,6 @@ import qs from 'qs' import { createAction } from 'redux-actions' import { - getFieldTripGroupCapacityForMode, - getFormattedRequestTravelDate, getGroupSize, getTripFromRequest, lzwEncode, @@ -164,6 +165,14 @@ export function editSubmitterNotes (request, submitterNotes) { } } +/** + * Validates and saves the currently active field trip itineraries to the + * appropriate inbound or outbound trip of a field trip request. + * + * @param {Object} request The field trip request + * @param {boolean} outbound If true, save the current itineraries to the + * outbound field trip journey. + */ export function saveRequestTripItineraries (request, outbound) { return async function (dispatch, getState) { const state = getState() @@ -204,6 +213,7 @@ export function saveRequestTripItineraries (request, outbound) { /** * Checks that a group plan is valid for a given request, i.e., that it occurs * on the requested travel date. + * * @param request field trip request * @param itineraries the currently active itineraries * @return true if invalid @@ -233,11 +243,13 @@ function fieldTripPlanIsInvalid (request, itineraries) { return true } - // FIXME More checks? E.g., origin/destination - return false } +/** + * Returns the earliest start time (in unix epoch milliseconds) in the given + * itineraries. + */ function getEarliestStartTime (itineraries) { return itineraries.reduce( (earliestStartTime, itinerary) => @@ -246,6 +258,14 @@ function getEarliestStartTime (itineraries) { ) } +/** + * Returns true if the user confirms the overwrite of an existing set of + * itineraries planned for the inbound or outbound part of the field trip. + * + * @param {Object} request The field trip request + * @param {boolean} outbound If true, save the current itineraries to the + * outbound field trip journey. + */ function overwriteExistingRequestTripsConfirmed (request, outbound) { const type = outbound ? 'outbound' : 'inbound' const preExistingTrip = getTripFromRequest(request, outbound) @@ -257,7 +277,8 @@ function overwriteExistingRequestTripsConfirmed (request, outbound) { } /** - * Construct data for request to save + * Constructs data used to save a set of itineraries for the inbound or outbound + * part of the field trip. */ function makeSaveFieldTripItinerariesData (request, outbound, state) { const { fieldTrip, session } = state.callTaker @@ -266,6 +287,7 @@ function makeSaveFieldTripItinerariesData (request, outbound, state) { const fieldTripModuleConfig = getModuleConfig(state, Modules.FIELD_TRIP) const itineraries = getActiveItineraries(state) + // initialize base object const data = { gtfsTrips: [], itins: [], @@ -283,6 +305,8 @@ function makeSaveFieldTripItinerariesData (request, outbound, state) { } } + // itierate through itineraries to construct itinerary and gtfsTrip data to + // save the itineraries to otp-datastore. itineraries.forEach((itinerary, itinIdx) => { const itineraryDataToSave = { itinData: lzwEncode(JSON.stringify(itinerary)), @@ -323,6 +347,9 @@ function makeSaveFieldTripItinerariesData (request, outbound, state) { return serialize(data, { indices: true }) } +/** + * Creates an OTP-style location string based on the location name, lat and lon. + */ function getOTPLocationString (location) { const llString = `${location.lat},${location.lon}` return location.name @@ -330,6 +357,10 @@ function getOTPLocationString (location) { : llString } +/** + * Begins the process of making trip requests to find suitable itineraries for + * either an inbound or outbound journey of a field trip. + */ export function planTrip (request, outbound) { return function (dispatch, getState) { dispatch(setGroupSize(getGroupSize(request))) @@ -339,6 +370,10 @@ export function planTrip (request, outbound) { } } +/** + * Sets the appropriate request parameters for an outbound journey of a field + * trip. + */ function planOutbound (request) { return async function (dispatch, getState) { const {config} = getState().otp @@ -358,6 +393,10 @@ function planOutbound (request) { } } +/** + * Sets the appropriate request parameters for an inbound journey of a field + * trip. + */ function planInbound (request) { return async function (dispatch, getState) { const {config} = getState().otp @@ -379,7 +418,7 @@ function planInbound (request) { /** * Makes appropriate OTP requests until enough itineraries have been found to - * accomodate the field trip group. + * accommodate the field trip group. */ function makeFieldTripPlanRequests (request) { return async function (dispatch, getState) { @@ -415,7 +454,7 @@ function makeFieldTripPlanRequests (request) { let shouldContinueSearching = true - // make requests until enough itineraries have been found to accomodate + // make requests until enough itineraries have been found to accommodate // group while (shouldContinueSearching) { numRequests++ @@ -472,10 +511,17 @@ function makeFieldTripPlanRequests (request) { } } +/** + * Makes a request to get data about other planned field trips happening on a + * particular date. + */ async function getTripIdsForTravelDate (dispatch, state, request) { const {datastoreUrl} = state.otp.config const {sessionId} = state.callTaker.session - const formattedTravelDate = getFormattedRequestTravelDate(request, 'MM/DD/YYYY') + const formattedTravelDate = getFormattedRequestTravelDate( + request, + FIELD_TRIP_DATE_FORMAT + ) const params = { date: formattedTravelDate, sessionId @@ -503,6 +549,11 @@ async function getTripIdsForTravelDate (dispatch, state, request) { await dispatch(receivedTravelDateTrips(trips)) } +/** + * Makes the needed requests to the OTP index API to obtain semantic trip hashes + * for any tripIds in the active itineraries that haven't already been obtained + * from the OTP index API. + */ function getTripHashesFromActiveItineraries () { return function (dispatch, getState) { const state = getState() @@ -528,6 +579,22 @@ function getTripHashesFromActiveItineraries () { } } +/** + * Analyzes the current itineraries from each response in the active search to + * appropriately assign the field trip group to subgroups that use each + * itinerary according to the capacity avaiable in each itinerary. + * + * @return {Object} result + * @return {Object} result.assignedItinerariesByResponse An Object organized by + * response index and list of itineraries that can be used to update the state + * with modified itinerary objects that can help display the field trip group + * size assigned to each itinerary. + * @return {Object} result.remainingGroupSize The remaining about of people in + * the field trip group that have yet to be assigned to an itinerary. + * @return {Object} result.tripsToBanInSubsequentSearches An array of strings + * representing tripIds that should be added to the list of tripIds to ban when + * making requests for additional itineraries from OTP. + */ function checkValidityAndCapacity (state, request) { const fieldTripModuleConfig = getModuleConfig(state, Modules.FIELD_TRIP) const minimumAllowableRemainingCapacity = @@ -620,7 +687,7 @@ function checkValidityAndCapacity (state, request) { if (itineraryCapacity > 0) { // itinerary is possible, add to list and update remaining group size. - // A field trip response is gauranteed to have only one itinerary, so it + // A field trip response is guaranteed to have only one itinerary, so it // ok to set the itinerary by response as an array with a single // itinerary. assignedItinerariesByResponse[responseIdx] = [{ @@ -639,6 +706,48 @@ function checkValidityAndCapacity (state, request) { } } +/** + * Formats a field trip's travel date into the given format. + */ +function getFormattedRequestTravelDate ( + request, + dateFormat = OTP_API_DATE_FORMAT +) { + return moment(request.travelDate).format(dateFormat) +} + +// These can be overridden in the field trip module config. +const defaultFieldTripModeCapacities = { + 'TRAM': 80, + 'SUBWAY': 120, + 'RAIL': 80, + 'BUS': 40, + 'FERRY': 100, + 'CABLE_CAR': 20, + 'GONDOLA': 15, + 'FUNICULAR': 20 +} +const unknownModeCapacity = 15 + +/** + * Calculates the mode capacity based on the field trip module mode capacities + * (if it exists) or from the above default lookup of mode capacities or if + * given an unknown mode, then the unknownModeCapacity is returned. + * + * @param {Object} config the app-wide config + * @param {string} mode the OTP mode + */ +function getFieldTripGroupCapacityForMode (fieldTripModuleConfig, mode) { + const configModeCapacities = fieldTripModuleConfig?.modeCapacities + return (configModeCapacities && configModeCapacities[mode]) || + defaultFieldTripModeCapacities[mode] || + unknownModeCapacity +} + +/** + * Dispatches the appropriate cleanup actions after making requests to find the + * itineraries for an inbound or outbound field trip journey. + */ function doFieldTripPlanRequestCleanup (dispatch, searchId) { // set pending searches to 0 to indicate searching is finished dispatch(setPendingRequests({ searchId, pending: 0 })) diff --git a/lib/util/call-taker.js b/lib/util/call-taker.js index 3be286507..16671520f 100644 --- a/lib/util/call-taker.js +++ b/lib/util/call-taker.js @@ -1,6 +1,5 @@ import {getRoutingParams} from '@opentripplanner/core-utils/lib/query' import {randId} from '@opentripplanner/core-utils/lib/storage' -import { OTP_API_DATE_FORMAT } from '@opentripplanner/core-utils/lib/time' import moment from 'moment' import { createSelector } from 'reselect' @@ -218,42 +217,6 @@ export function getTripFromRequest (request, outbound = false) { return trip } -export function getFormattedRequestTravelDate ( - request, - dateFormat = OTP_API_DATE_FORMAT -) { - return moment(request.travelDate).format(dateFormat) -} - -const defaultFieldTripModeCapacities = { - 'TRAM': 80, - 'SUBWAY': 120, - 'RAIL': 80, - 'BUS': 40, - 'FERRY': 100, - 'CABLE_CAR': 20, - 'GONDOLA': 10, - 'FUNICULAR': 20 -} -const unknownModeCapacity = 10 - -/** - * Calculates the mode capacity based on the field trip module mode capacities - * (if it exists) or from the above default lookup of mode capacities or if - * given an unknown mode, then the unknownModeCapacity is returned. - * - * @param {Object} config the app-wide config - * @param {string} mode the OTP mode - */ -export const getFieldTripGroupCapacityForMode = createSelector( - fieldTripModuleConfig => fieldTripModuleConfig?.modeCapacities, - (fieldTripModuleConfig, mode) => mode, - (configModeCapacities, mode) => (configModeCapacities && - configModeCapacities[mode]) || - defaultFieldTripModeCapacities[mode] || - unknownModeCapacity -) - /** * LZW-compress a string * diff --git a/lib/util/state.js b/lib/util/state.js index c70add7ed..196b3a98a 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -93,7 +93,7 @@ export function getResponsesWithErrors (state) { response.forEach(res => { if (res) { if (res.error) { - let msg = res.error.msg || 'An error occured while planning a trip' + let msg = res.error.msg || 'An error occurred while planning a trip' // include the modes if doing batch routing if (showModes && res.requestParameters?.mode) { const mode = humanReadableMode(res.requestParameters.mode) From 6495d980f1e4edb3c0f29b31b2d4fd0be48e9f74 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Thu, 1 Jul 2021 16:06:20 -0700 Subject: [PATCH 13/35] docs: add example field trip module config --- example-config.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/example-config.yml b/example-config.yml index 1a33faf03..d17cda366 100644 --- a/example-config.yml +++ b/example-config.yml @@ -170,6 +170,24 @@ modes: # - id: call # # Provides UI elements for planning field trips on transit vehicles. # - id: ft +# # An optional maximum number of requests to make to OTP when trying to +# # find itineraries. Defaults to 10 if not provided. +# maxRequests: 10 +# # An optional minimum remaining capacity that a tripId must retain in the +# # event that multiple field trips use the same trip. Defaults to 10 if not +# # provided. +# minimumAllowableRemainingCapacity: 10 +# # An optional lookup of the field trip capacity for each mode. Defaults +# # are shown below if any one of these are not provided. +# modeCapacities: +# TRAM: 80 +# SUBWAY: 120 +# RAIL: 80 +# BUS: 40 +# FERRY: 100 +# CABLE_CAR: 20 +# GONDOLA: 15 +# FUNICULAR: 20 # # Provides a form for constructing PDF documents for mailing to customers. # - id: mailables # items: From a79da5d7b8fbd415ed39b89c7cbc01b06dff42ba Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Thu, 1 Jul 2021 16:11:28 -0700 Subject: [PATCH 14/35] docs: fix some JSDoc --- lib/actions/field-trip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index c5c840a8c..30512c056 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -734,7 +734,7 @@ const unknownModeCapacity = 15 * (if it exists) or from the above default lookup of mode capacities or if * given an unknown mode, then the unknownModeCapacity is returned. * - * @param {Object} config the app-wide config + * @param {Object} fieldTripModuleConfig the field trip module config * @param {string} mode the OTP mode */ function getFieldTripGroupCapacityForMode (fieldTripModuleConfig, mode) { From 7934eed43ddf32d50c82b4e1548560da56f14b22 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Thu, 1 Jul 2021 16:12:21 -0700 Subject: [PATCH 15/35] docs: fix typo --- lib/actions/field-trip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 30512c056..6f768cdc4 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -582,7 +582,7 @@ function getTripHashesFromActiveItineraries () { /** * Analyzes the current itineraries from each response in the active search to * appropriately assign the field trip group to subgroups that use each - * itinerary according to the capacity avaiable in each itinerary. + * itinerary according to the capacity available in each itinerary. * * @return {Object} result * @return {Object} result.assignedItinerariesByResponse An Object organized by From 2e9be4e7d92184433cde8a861ea165b8a17d9a21 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 9 Jul 2021 14:51:51 -0400 Subject: [PATCH 16/35] fix(util): use proper stop_id format --- lib/util/itinerary.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/util/itinerary.js b/lib/util/itinerary.js index 4433b62f4..15a651f79 100644 --- a/lib/util/itinerary.js +++ b/lib/util/itinerary.js @@ -46,8 +46,12 @@ export function itineraryCanBeMonitored (itinerary) { return hasTransit && !hasRentalOrRideHail } +/** + * Get the first stop ID from the itinerary in the underscore format required by + * the startTransitStopId query param (e.g., TRIMET_12345 instead of TRIMET:12345). + */ export function getFirstStopId (itinerary) { - return getFirstTransitLeg(itinerary)?.from.stopId + return getFirstTransitLeg(itinerary)?.from.stopId.replace(':', '_') } export function getMinutesUntilItineraryStart (itinerary) { From ae52f030c4a22075c50cb7f77fd41e77c216539c Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Fri, 9 Jul 2021 16:45:32 -0700 Subject: [PATCH 17/35] Update example-config.yml Co-authored-by: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> --- example-config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example-config.yml b/example-config.yml index d17cda366..f35b9b9f3 100644 --- a/example-config.yml +++ b/example-config.yml @@ -171,7 +171,7 @@ modes: # # Provides UI elements for planning field trips on transit vehicles. # - id: ft # # An optional maximum number of requests to make to OTP when trying to -# # find itineraries. Defaults to 10 if not provided. +# # find itineraries. Defaults to 10 if not provided. # maxRequests: 10 # # An optional minimum remaining capacity that a tripId must retain in the # # event that multiple field trips use the same trip. Defaults to 10 if not From e4e328a89c95c6ff5054dd6700b2fac88e2f523e Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Fri, 9 Jul 2021 16:46:09 -0700 Subject: [PATCH 18/35] Update lib/util/state.js Co-authored-by: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> --- lib/util/state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util/state.js b/lib/util/state.js index 196b3a98a..d9bf43484 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -259,7 +259,7 @@ export const getActiveItineraries = createSelector( const {direction, type} = sort // If no sort type is provided (e.g., because batch routing is not enabled), // do not sort itineraries (default sort from API response is used). - // Also, do not sort itineraries if the a field trip request is active + // Also, do not sort itineraries if a field trip request is active return (!type || Boolean(activeFieldTripRequest)) ? itineraries : itineraries.sort((a, b) => sortItineraries(type, direction, a, b, config)) From a67f9a60762527ae3472c4128c3a2c82f1c78e35 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Fri, 9 Jul 2021 16:46:39 -0700 Subject: [PATCH 19/35] Update lib/actions/field-trip.js Co-authored-by: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> --- lib/actions/field-trip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 6f768cdc4..b4bb20bc5 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -305,7 +305,7 @@ function makeSaveFieldTripItinerariesData (request, outbound, state) { } } - // itierate through itineraries to construct itinerary and gtfsTrip data to + // iterate through itineraries to construct itinerary and gtfsTrip data to // save the itineraries to otp-datastore. itineraries.forEach((itinerary, itinIdx) => { const itineraryDataToSave = { From bf5dd2f268a07a42d8cb613238ee6b433d6ac688 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Fri, 9 Jul 2021 16:47:12 -0700 Subject: [PATCH 20/35] Update lib/actions/field-trip.js Co-authored-by: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> --- lib/actions/field-trip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index b4bb20bc5..8ee6575a8 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -589,7 +589,7 @@ function getTripHashesFromActiveItineraries () { * response index and list of itineraries that can be used to update the state * with modified itinerary objects that can help display the field trip group * size assigned to each itinerary. - * @return {Object} result.remainingGroupSize The remaining about of people in + * @return {Object} result.remainingGroupSize The remaining number of people in * the field trip group that have yet to be assigned to an itinerary. * @return {Object} result.tripsToBanInSubsequentSearches An array of strings * representing tripIds that should be added to the list of tripIds to ban when From a4480936984a2a1cb2d9dd6f97adc4ecd3a9a12c Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Fri, 9 Jul 2021 22:31:06 -0700 Subject: [PATCH 21/35] refactor(field-trip): address PR review comments See https://github.com/opentripplanner/otp-react-redux/pull/388 --- lib/actions/api.js | 6 +- lib/actions/field-trip.js | 186 +++++++++++++++++++++----------------- 2 files changed, 106 insertions(+), 86 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index e80176225..0806017ce 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -86,7 +86,7 @@ function getActiveItinerary (state) { * if it exists. This is used by the field trip module. */ export function routingQuery (searchId = null, updateSearchInReducer = false) { - return async function (dispatch, getState) { + return function (dispatch, getState) { // FIXME: batchId is searchId for now. const state = getState() @@ -116,10 +116,10 @@ export function routingQuery (searchId = null, updateSearchInReducer = false) { : [{}] dispatch(routingRequest({ activeItinerary, - updateSearchInReducer, + pending: iterations.length, routingType, searchId, - pending: iterations.length + updateSearchInReducer })) return Promise.all(iterations.map((injectedParams, i) => { const requestId = randId() diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 8ee6575a8..5c92dacdb 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -43,6 +43,7 @@ export const setActiveFieldTrip = createAction('SET_ACTIVE_FIELD_TRIP') export const setGroupSize = createAction('SET_GROUP_SIZE') export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS') +// these are date/time formats specifically used by otp-datastore const FIELD_TRIP_DATE_FORMAT = 'MM/DD/YYYY' const FIELD_TRIP_DATE_TIME_FORMAT = 'YYYY-MM-DDTHH:mm:ss' const FIELD_TRIP_TIME_FORMAT = 'HH:mm:ss' @@ -297,8 +298,8 @@ function makeSaveFieldTripItinerariesData (request, outbound, state) { createdBy: session.username, departure: moment(getEarliestStartTime(itineraries)) .format(FIELD_TRIP_DATE_TIME_FORMAT), - destination: getOTPLocationString(currentQuery.from), - origin: getOTPLocationString(currentQuery.to), + destination: getOtpLocationString(currentQuery.from), + origin: getOtpLocationString(currentQuery.to), passengers: getGroupSize(request), queryParams: JSON.stringify(getRoutingParams(config, currentQuery)), requestOrder: outbound ? 0 : 1 @@ -350,11 +351,11 @@ function makeSaveFieldTripItinerariesData (request, outbound, state) { /** * Creates an OTP-style location string based on the location name, lat and lon. */ -function getOTPLocationString (location) { - const llString = `${location.lat},${location.lon}` +function getOtpLocationString (location) { + const latLonString = `${location.lat},${location.lon}` return location.name - ? `${location.name}::${llString}` - : llString + ? `${location.name}::${latLonString}` + : latLonString } /** @@ -383,7 +384,7 @@ function planOutbound (request) { toPlace: request.endLocation }, config) const queryParams = { - date: getFormattedRequestTravelDate(request), + date: moment(request.travelDate).format(OTP_API_DATE_FORMAT), departArrive: 'ARRIVE', time: moment(request.arriveDestinationTime).format(OTP_API_TIME_FORMAT), ...locations @@ -406,7 +407,7 @@ function planInbound (request) { }, config) // clearTrip() const queryParams = { - date: getFormattedRequestTravelDate(request), + date: moment(request.travelDate).format(OTP_API_DATE_FORMAT), departArrive: 'DEPART', time: moment(request.leaveDestinationTime).format(OTP_API_TIME_FORMAT), ...locations @@ -418,20 +419,32 @@ function planInbound (request) { /** * Makes appropriate OTP requests until enough itineraries have been found to - * accommodate the field trip group. + * accommodate the field trip group. This is done as follows: + * + * 1. Fetch a list of all the existing transit trips that already have a field + * trip assigned to the trip. + * 2. In a loop of up to 10 times: + * i. Make a trip plan query to OTP for one additional itinerary + * ii. Calculate the trip hashes in the resulting itinerary by making requests + * to the OTP index API + * iii. Assign as many field trip travelers to the resulting itinerary as + * possible. + * iv. Check if there are still unassigned field trip travelers + * a. If there are still more to assign, ban each trip used in this + * itinerary in subsequent OTP plan requests. + * b. If all travelers have been assigned, exit the loop and cleanup */ function makeFieldTripPlanRequests (request) { return async function (dispatch, getState) { - const prePlanState = getState() const fieldTripModuleConfig = getModuleConfig( - prePlanState, + getState(), Modules.FIELD_TRIP ) // request other known trip IDs that other field trips are using on the // field trip request date try { - await getTripIdsForTravelDate(dispatch, prePlanState, request) + await dispatch(getTripIdsForTravelDate(request)) } catch (e) { alert( `Error fetching trips for field trip travel date: ${JSON.stringify(e)}` @@ -461,7 +474,7 @@ function makeFieldTripPlanRequests (request) { if (numRequests > maxRequests) { // max number of requests exceeded. Show error. alert('Number of trip requests exceeded without valid results') - return doFieldTripPlanRequestCleanup(dispatch, searchId) + return dispatch(doFieldTripPlanRequestCleanup(searchId)) } // make next query. The second param instructs the action/reducer whether @@ -476,7 +489,7 @@ function makeFieldTripPlanRequests (request) { dispatch(setPendingRequests({ searchId, pending: 2 })) // obtain trip hashes from OTP Index API - await getTripHashesFromActiveItineraries() + await getMissingTripHashesForActiveItineraries() // check trip validity and calculate itinerary capacity const { @@ -497,7 +510,7 @@ function makeFieldTripPlanRequests (request) { if (remainingGroupSize <= 0) { // All members of the field trip group have been assigned! shouldContinueSearching = false - doFieldTripPlanRequestCleanup(dispatch, searchId) + dispatch(doFieldTripPlanRequestCleanup(searchId)) } else { // Not enough acceptable itineraries have been generated. Request more. @@ -515,38 +528,39 @@ function makeFieldTripPlanRequests (request) { * Makes a request to get data about other planned field trips happening on a * particular date. */ -async function getTripIdsForTravelDate (dispatch, state, request) { - const {datastoreUrl} = state.otp.config - const {sessionId} = state.callTaker.session - const formattedTravelDate = getFormattedRequestTravelDate( - request, - FIELD_TRIP_DATE_FORMAT - ) - const params = { - date: formattedTravelDate, - sessionId - } +function getTripIdsForTravelDate (request) { + return async function (dispatch, getState) { + const state = getState() + const {datastoreUrl} = state.otp.config + const {sessionId} = state.callTaker.session + const formattedTravelDate = moment(request.travelDate) + .format(FIELD_TRIP_DATE_FORMAT) + const params = { + date: formattedTravelDate, + sessionId + } - const res = await fetch( - `${datastoreUrl}/fieldtrip/getTrips?${qs.stringify(params)}` - ) - const fieldTrips = await res.json() - - // add passengers and converted tripId to trips - const trips = [] - fieldTrips.forEach(fieldTrip => { - fieldTrip.groupItineraries.forEach(groupItinerary => { - groupItinerary.trips.forEach(gtfsTrip => { - // tripIds still stored as 'agencyAndId' in DB, so convert them to - // be compatible with OTP responses - gtfsTrip.tripId = gtfsTrip.agencyAndId.replace('_', ':') - // Add passengers to each trip from group itinerary - gtfsTrip.passengers = groupItinerary.passengers - trips.push(gtfsTrip) + const res = await fetch( + `${datastoreUrl}/fieldtrip/getTrips?${qs.stringify(params)}` + ) + const fieldTrips = await res.json() + + // add passengers and converted tripId to trips + const trips = [] + fieldTrips.forEach(fieldTrip => { + fieldTrip.groupItineraries.forEach(groupItinerary => { + groupItinerary.trips.forEach(gtfsTrip => { + // tripIds still stored as 'agencyAndId' in DB, so convert them to + // be compatible with OTP responses + gtfsTrip.tripId = gtfsTrip.agencyAndId.replace('_', ':') + // Add passengers to each trip from group itinerary + gtfsTrip.passengers = groupItinerary.passengers + trips.push(gtfsTrip) + }) }) }) - }) - await dispatch(receivedTravelDateTrips(trips)) + return dispatch(receivedTravelDateTrips(trips)) + } } /** @@ -554,7 +568,7 @@ async function getTripIdsForTravelDate (dispatch, state, request) { * for any tripIds in the active itineraries that haven't already been obtained * from the OTP index API. */ -function getTripHashesFromActiveItineraries () { +function getMissingTripHashesForActiveItineraries () { return function (dispatch, getState) { const state = getState() const activeItineraries = getActiveItineraries(state) @@ -620,12 +634,9 @@ function checkValidityAndCapacity (state, request) { response.plan.itineraries.forEach((itinerary, idx) => { let itineraryCapacity = Number.POSITIVE_INFINITY - // check each individual trip to see if there aren't any conflicts - itinerary.legs.forEach(leg => { - // non-transit legs have infinite capacity - if (!isTransit(leg.mode)) { - return - } + // check each individual trip to see if there aren't any trips in this + // itinerary that are already in use by another field trip + itinerary.legs.filter(leg => isTransit(leg.mode)).forEach(leg => { const { tripId } = leg // this variable is used to track how many other field trips are using a @@ -634,27 +645,7 @@ function checkValidityAndCapacity (state, request) { // iterate over trips that are already being used by other field trips travelDateTripsInUse.forEach(tripInUse => { - // check if the trip is being used by another field trip - let sameVehicleTrip = false - if (tripId in tripHashLookup && tripInUse.tripHash) { - // use the trip hashes if available - sameVehicleTrip = (tripHashLookup[tripId] === tripInUse.tripHash) - } else { - // as fallback, compare the tripId strings - sameVehicleTrip = (tripId === tripInUse.tripId) - } - // not used by another vehicle, so not used by this other field trip - if (!sameVehicleTrip) return - - // check if the stop ranges overlap. It is OK if one field trip begins - // where the other ends. - if ( - leg.from.stopIndex >= tripInUse.toStopIndex || - leg.to.stopIndex <= tripInUse.fromStopIndex - ) { - // legs do not overlap, so not used by this other field trip - return - } + if (!tripsOverlap(leg, tripHashLookup, tripId, tripInUse)) return // ranges overlap! Add number of passengers on this other field trip // to total capacity in use @@ -707,13 +698,40 @@ function checkValidityAndCapacity (state, request) { } /** - * Formats a field trip's travel date into the given format. + * Checks whether an existing trip in use by another field trip overlaps with a + * a trip identified by tripId. + * + * @param leg The leg information of the trip in the associated + * itinerary + * @param tripHashLookup The lookup of trip hashes + * @param tripId The tripId to analyze with respect to a trip in use + * @param tripInUse The trip in use by an existing field trip. This is an + * otp-datastore object. + * @return true if the trips overlap */ -function getFormattedRequestTravelDate ( - request, - dateFormat = OTP_API_DATE_FORMAT -) { - return moment(request.travelDate).format(dateFormat) +function tripsOverlap (leg, tripHashLookup, tripId, tripInUse) { + // check if the trip is being used by another field trip + let sameVehicleTrip = false + if (tripId in tripHashLookup && tripInUse.tripHash) { + // use the trip hashes if available + sameVehicleTrip = (tripHashLookup[tripId] === tripInUse.tripHash) + } else { + // as fallback, compare the tripId strings + sameVehicleTrip = (tripId === tripInUse.tripId) + } + // not used by another vehicle, so this trip/vehicle is free to use + if (!sameVehicleTrip) return false + + // check if the stop ranges overlap. It is OK if one field trip begins + // where the other ends. + if ( + leg.from.stopIndex >= tripInUse.toStopIndex || + leg.to.stopIndex <= tripInUse.fromStopIndex + ) { + // legs do not overlap, so this trip/vehicle is free to use + return false + } + return true } // These can be overridden in the field trip module config. @@ -748,11 +766,13 @@ function getFieldTripGroupCapacityForMode (fieldTripModuleConfig, mode) { * Dispatches the appropriate cleanup actions after making requests to find the * itineraries for an inbound or outbound field trip journey. */ -function doFieldTripPlanRequestCleanup (dispatch, searchId) { - // set pending searches to 0 to indicate searching is finished - dispatch(setPendingRequests({ searchId, pending: 0 })) - // clear banned trips query param - dispatch(setQueryParam({ bannedTrips: undefined })) +function doFieldTripPlanRequestCleanup (searchId) { + return function (dispatch, getState) { + // set pending searches to 0 to indicate searching is finished + dispatch(setPendingRequests({ searchId, pending: 0 })) + // clear banned trips query param + dispatch(setQueryParam({ bannedTrips: undefined })) + } } /** From 59587d539315f25894a8d4382569f5a4782721c4 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Fri, 9 Jul 2021 22:33:51 -0700 Subject: [PATCH 22/35] Update lib/actions/field-trip.js Co-authored-by: Landon Reed --- lib/actions/field-trip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 5c92dacdb..c1aa5abf2 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -677,7 +677,7 @@ function checkValidityAndCapacity (state, request) { }) if (itineraryCapacity > 0) { - // itinerary is possible, add to list and update remaining group size. + // itinerary has capacity, add to list and update remaining group size. // A field trip response is guaranteed to have only one itinerary, so it // ok to set the itinerary by response as an array with a single // itinerary. From b54708ece3b4908f2f2ab5e3a56fddde0f639611 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Mon, 12 Jul 2021 17:50:39 -0700 Subject: [PATCH 23/35] feat(field-trip): en/dis-able save trip button, show view, delete buttons --- lib/actions/api.js | 2 +- lib/actions/field-trip.js | 127 +++++++++++++++++++--------- lib/components/admin/trip-status.js | 100 ++++++++++++++-------- lib/reducers/call-taker.js | 13 +++ lib/reducers/create-otp-reducer.js | 21 +++++ 5 files changed, 185 insertions(+), 78 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index 0806017ce..b1ac3bb3b 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -973,7 +973,7 @@ export function setUrlSearch (params, replaceCurrent = false) { * Update the OTP Query parameters in the URL and ensure that the active search * is set correctly. Leaves any other existing URL parameters (e.g., UI) unchanged. */ -function updateOtpUrlParams (state, searchId) { +export function updateOtpUrlParams (state, searchId) { const {config, currentQuery} = state.otp // Get updated OTP params from current query. const otpParams = getRoutingParams(config, currentQuery, true) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 5c92dacdb..56898054e 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -16,13 +16,19 @@ import { createAction } from 'redux-actions' import { getGroupSize, getTripFromRequest, + lzwDecode, lzwEncode, sessionIsInvalid } from '../util/call-taker' import { getModuleConfig, Modules } from '../util/config' import { getActiveItineraries, getActiveSearch } from '../util/state' -import {routingQuery, setActiveItineraries, setPendingRequests} from './api' +import { + routingQuery, + setActiveItineraries, + setPendingRequests, + updateOtpUrlParams +} from './api' import {toggleCallHistory} from './call-taker' import {clearActiveSearch, resetForm, setQueryParam} from './form' @@ -35,6 +41,8 @@ const receiveTripHash = createAction('RECEIVE_TRIP_HASH') const requestingFieldTrips = createAction('REQUESTING_FIELD_TRIPS') const receivedFieldTripDetails = createAction('RECEIVED_FIELD_TRIP_DETAILS') const requestingFieldTripDetails = createAction('REQUESTING_FIELD_TRIP_DETAILS') +const setActiveItinerariesFromFieldTrip = + createAction('SET_ACTIVE_ITINERARIES_FROM_FIELD_TRIP') // PUBLIC ACTIONS export const receivedFieldTrips = createAction('RECEIVED_FIELD_TRIPS') @@ -42,6 +50,8 @@ export const setFieldTripFilter = createAction('SET_FIELD_TRIP_FILTER') export const setActiveFieldTrip = createAction('SET_ACTIVE_FIELD_TRIP') export const setGroupSize = createAction('SET_GROUP_SIZE') export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS') +export const clearSavable = createAction('CLEAR_SAVABLE') +export const setSavable = createAction('SET_SAVABLE') // these are date/time formats specifically used by otp-datastore const FIELD_TRIP_DATE_FORMAT = 'MM/DD/YYYY' @@ -363,57 +373,44 @@ function getOtpLocationString (location) { * either an inbound or outbound journey of a field trip. */ export function planTrip (request, outbound) { - return function (dispatch, getState) { + return async function (dispatch, getState) { + dispatch(clearSavable()) dispatch(setGroupSize(getGroupSize(request))) - // Construct params from request details - if (outbound) dispatch(planOutbound(request)) - else dispatch(planInbound(request)) + await dispatch(prepareQueryParams(request, outbound)) + dispatch(makeFieldTripPlanRequests(request, outbound)) } } /** - * Sets the appropriate request parameters for an outbound journey of a field - * trip. + * Sets the appropriate request parameters for either the outbound or inbound + * journey of a field trip. */ -function planOutbound (request) { +function prepareQueryParams (request, outbound) { return async function (dispatch, getState) { const {config} = getState().otp - // clearTrip() - const locations = await planParamsToQueryAsync({ - fromPlace: request.startLocation, - toPlace: request.endLocation - }, config) const queryParams = { - date: moment(request.travelDate).format(OTP_API_DATE_FORMAT), - departArrive: 'ARRIVE', - time: moment(request.arriveDestinationTime).format(OTP_API_TIME_FORMAT), - ...locations + date: moment(request.travelDate).format(OTP_API_DATE_FORMAT) } - dispatch(setQueryParam(queryParams)) - dispatch(makeFieldTripPlanRequests(request)) - } -} - -/** - * Sets the appropriate request parameters for an inbound journey of a field - * trip. - */ -function planInbound (request) { - return async function (dispatch, getState) { - const {config} = getState().otp - const locations = await planParamsToQueryAsync({ - fromPlace: request.endLocation, - toPlace: request.startLocation - }, config) - // clearTrip() - const queryParams = { - date: moment(request.travelDate).format(OTP_API_DATE_FORMAT), - departArrive: 'DEPART', - time: moment(request.leaveDestinationTime).format(OTP_API_TIME_FORMAT), - ...locations + let locationsToGeocode + if (outbound) { + locationsToGeocode = { + fromPlace: request.startLocation, + toPlace: request.endLocation + } + queryParams.departArrive = 'ARRIVE' + queryParams.time = moment(request.arriveDestinationTime) + .format(OTP_API_TIME_FORMAT) + } else { + locationsToGeocode = { + fromPlace: request.endLocation, + toPlace: request.startLocation + } + queryParams.departArrive = 'DEPART' + queryParams.time = moment(request.leaveDestinationTime) + .format(OTP_API_TIME_FORMAT) } - dispatch(setQueryParam(queryParams)) - dispatch(makeFieldTripPlanRequests(request)) + const locations = await planParamsToQueryAsync(locationsToGeocode, config) + return dispatch(setQueryParam({ queryParams, ...locations })) } } @@ -434,7 +431,7 @@ function planInbound (request) { * itinerary in subsequent OTP plan requests. * b. If all travelers have been assigned, exit the loop and cleanup */ -function makeFieldTripPlanRequests (request) { +function makeFieldTripPlanRequests (request, outbound) { return async function (dispatch, getState) { const fieldTripModuleConfig = getModuleConfig( getState(), @@ -510,6 +507,7 @@ function makeFieldTripPlanRequests (request) { if (remainingGroupSize <= 0) { // All members of the field trip group have been assigned! shouldContinueSearching = false + dispatch(setSavable(outbound)) dispatch(doFieldTripPlanRequestCleanup(searchId)) } else { // Not enough acceptable itineraries have been generated. Request more. @@ -644,6 +642,11 @@ function checkValidityAndCapacity (state, request) { let capacityInUse = 0 // iterate over trips that are already being used by other field trips + // NOTE: In the use case of re-planning trips, there is currently no way + // to discern whether a tripInUse belongs to the current direction of + // the field trip being planned. Therefore, this will result in the + // re-planning of trips avoiding it's own previously planned trips + // that it currently has saved travelDateTripsInUse.forEach(tripInUse => { if (!tripsOverlap(leg, tripHashLookup, tripId, tripInUse)) return @@ -775,6 +778,45 @@ function doFieldTripPlanRequestCleanup (searchId) { } } +export function deleteRequestTripItineraries (request, outbound) { + return function (dispatch, getState) { + // TODO + } +} + +/** + * Sets the appropriate query parameters for the saved field trip and loads the + * saved itineraries from the field trip request and sets these loaded + * itineraries as if they appeared from a new OTP trip plan request. + */ +export function viewRequestTripItineraries (request, outbound) { + return async function (dispatch, getState) { + // set the appropriate query parameters as if the trip were being planned + await dispatch(prepareQueryParams(request, outbound)) + + // get the trip from the request + const trip = getTripFromRequest(request, outbound) + + // decode the saved itineraries + const itineraries = trip.groupItineraries?.map(groupItin => + JSON.parse(lzwDecode(groupItin.itinData)) + ) || [] + + const searchId = randId() + + // set the itineraries in a new OTP response + dispatch(setActiveItinerariesFromFieldTrip({ + response: [{ plan: { itineraries } }], + searchId + })) + + // appropriately initialize the Url params. If this doesn't happen, it won't + // be possible to set an active itinerary properly due to funkiness with the + // change in URL + dispatch(updateOtpUrlParams(getState(), searchId)) + } +} + /** * Set group size for a field trip request. Group size consists of numStudents, * numFreeStudents, and numChaperones. @@ -857,5 +899,6 @@ export function clearActiveFieldTrip () { dispatch(setActiveFieldTrip(null)) dispatch(clearActiveSearch()) dispatch(setQueryParam({ numItineraries: undefined })) + dispatch(clearSavable()) } } diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index 2ae185fcc..febc5df6f 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -17,39 +17,20 @@ import { } from './styled' class TripStatus extends Component { - _getTrip = () => getTripFromRequest(this.props.request, this.props.outbound) - _formatTime = (time) => moment(time).format(this.props.timeFormat) - _formatTripStatus = () => { - if (!this._getStatus()) { - return ( - - No itineraries planned! Click Plan to plan trip. - - ) - } - const trip = this._getTrip() - if (!trip) return Error finding trip! - return ( - - {trip.groupItineraries.length} group itineraries, planned by{' '} - {trip.createdBy} at {trip.timeStamp} - - ) - } - - _getStatus = () => { - const {outbound, request} = this.props - return outbound ? request.outboundTripStatus : request.inboundTripStatus + _onDeleteTrip = () => { + const { outbound, request, deleteRequestTripItineraries } = this.props + deleteRequestTripItineraries(request, outbound) } - _getStatusIcon = () => this._getStatus() - ? - : - _onPlanTrip = () => { - const { outbound, planTrip, request } = this.props + const { outbound, planTrip, request, status, trip } = this.props + if (status && trip) { + if (!confirm('Re-planning this trip will cause the trip planner to avoid the currently saved trip. Are you sure you want to continue?')) { + return + } + } planTrip(request, outbound) } @@ -58,8 +39,45 @@ class TripStatus extends Component { saveRequestTripItineraries(request, outbound) } + _onViewTrip = () => { + const { outbound, request, viewRequestTripItineraries } = this.props + viewRequestTripItineraries(request, outbound) + } + + _renderStatusIcon = () => this.props.status + ? + : + + _renderTripStatus = () => { + const { status, trip } = this.props + if (!status) { + return ( + + + No itineraries planned! Click Plan to plan trip. + + + ) + } + if (!trip) return Error finding trip! + return ( + <> + + + {trip.groupItineraries.length} group itineraries, planned by{' '} + {trip.createdBy} at {trip.timeStamp} + + + + + + + + ) + } + render () { - const {outbound, request} = this.props + const {outbound, request, savable} = this.props const { arriveDestinationTime, arriveSchoolTime, @@ -76,10 +94,16 @@ class TripStatus extends Component { return (
    - {this._getStatusIcon()} + {this._renderStatusIcon()} {outbound ? 'Outbound' : 'Inbound'} trip - +
    From {start} to {end} {outbound @@ -93,24 +117,30 @@ class TripStatus extends Component { } - {this._formatTripStatus()} + {this._renderTripStatus()}
    ) } } const mapStateToProps = (state, ownProps) => { + const { request, outbound } = ownProps + const { savable } = state.callTaker.fieldTrip return { - callTaker: state.callTaker, currentQuery: state.otp.currentQuery, - timeFormat: getTimeFormat(state.otp.config) + savable: outbound ? savable?.outbound : savable?.inbound, + status: outbound ? request.outboundTripStatus : request.inboundTripStatus, + timeFormat: getTimeFormat(state.otp.config), + trip: getTripFromRequest(request, outbound) } } const mapDispatchToProps = { + deleteRequestTripItineraries: fieldTripActions.deleteRequestTripItineraries, planTrip: fieldTripActions.planTrip, saveRequestTripItineraries: fieldTripActions.saveRequestTripItineraries, - setQueryParam: formActions.setQueryParam + setQueryParam: formActions.setQueryParam, + viewRequestTripItineraries: fieldTripActions.viewRequestTripItineraries } export default connect(mapStateToProps, mapDispatchToProps)(TripStatus) diff --git a/lib/reducers/call-taker.js b/lib/reducers/call-taker.js index ffa5655fc..d706b1c1e 100644 --- a/lib/reducers/call-taker.js +++ b/lib/reducers/call-taker.js @@ -111,6 +111,19 @@ function createCallTakerReducer (config) { fieldTrip: { tripHashLookup: { [tripId]: { $set: hash } } } }) } + case 'CLEAR_SAVABLE': { + return update(state, { + fieldTrip: { savable: { $set: {} } } + }) + } + case 'SET_SAVABLE': { + // The payload represents whether the savable set of itineraries are for + // the inbound or outbound journey. + const savableUpdate = { [action.payload ? 'outbound' : 'inbound']: true } + return update(state, { + fieldTrip: { savable: { $set: savableUpdate } } + }) + } case 'RECEIVED_QUERIES': { const {callId, queries} = action.payload const {data} = state.callHistory.calls diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 896f9e8ed..ec53563cd 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -354,6 +354,27 @@ function createOtpReducer (config) { } } }) + case 'SET_ACTIVE_ITINERARIES_FROM_FIELD_TRIP': + return update(state, { + activeSearchId: { $set: searchId }, + searches: { + [searchId]: { + $set: { + activeItinerary: 0, + activeLeg: null, + activeStep: null, + pending: 0, + // FIXME: get query from action payload? + query: clone(state.currentQuery), + response: action.payload.response, + timestamp: getTimestamp() + } + } + }, + ui: { + diagramLeg: { $set: null } + } + }) case 'BIKE_RENTAL_REQUEST': return update(state, { overlay: { From 3d25ee37345ecce66dcdb5209e5bfd1bf28460be Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Mon, 12 Jul 2021 23:53:40 -0700 Subject: [PATCH 24/35] feat(field-trip): implement delete planned "trip" (itineraries) functionality --- lib/actions/field-trip.js | 19 ++++++++++++++++--- lib/components/admin/trip-status.js | 7 +++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 6f196b957..3ae50f972 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -410,7 +410,7 @@ function prepareQueryParams (request, outbound) { .format(OTP_API_TIME_FORMAT) } const locations = await planParamsToQueryAsync(locationsToGeocode, config) - return dispatch(setQueryParam({ queryParams, ...locations })) + return dispatch(setQueryParam({ ...locations, ...queryParams })) } } @@ -778,9 +778,22 @@ function doFieldTripPlanRequestCleanup (searchId) { } } -export function deleteRequestTripItineraries (request, outbound) { +/** + * Removes the planned journey associated with the given id. + */ +export function deleteRequestTripItineraries (request, tripId) { return function (dispatch, getState) { - // TODO + const {callTaker, otp} = getState() + const {datastoreUrl} = otp.config + if (sessionIsInvalid(callTaker.session)) return + const {sessionId} = callTaker.session + return fetch(`${datastoreUrl}/fieldtrip/deleteTrip`, + {method: 'POST', body: serialize({ id: tripId, sessionId })} + ) + .then(() => dispatch(fetchFieldTripDetails(request.id))) + .catch(err => { + alert(`Error deleting field trip plan: ${JSON.stringify(err)}`) + }) } } diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index febc5df6f..39ec75fbb 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -20,8 +20,8 @@ class TripStatus extends Component { _formatTime = (time) => moment(time).format(this.props.timeFormat) _onDeleteTrip = () => { - const { outbound, request, deleteRequestTripItineraries } = this.props - deleteRequestTripItineraries(request, outbound) + const { deleteRequestTripItineraries, request, trip } = this.props + deleteRequestTripItineraries(request, trip.id) } _onPlanTrip = () => { @@ -50,7 +50,7 @@ class TripStatus extends Component { _renderTripStatus = () => { const { status, trip } = this.props - if (!status) { + if (!status || !trip) { return ( @@ -59,7 +59,6 @@ class TripStatus extends Component { ) } - if (!trip) return Error finding trip! return ( <> From ae0fe186c7cec5031d69fe73c8968110528e29aa Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Tue, 13 Jul 2021 00:04:38 -0700 Subject: [PATCH 25/35] refactor(field-trip): fix spelling --- lib/actions/field-trip.js | 10 +++++----- lib/components/admin/trip-status.js | 8 ++++---- lib/reducers/call-taker.js | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 3ae50f972..715eeed8c 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -50,8 +50,8 @@ export const setFieldTripFilter = createAction('SET_FIELD_TRIP_FILTER') export const setActiveFieldTrip = createAction('SET_ACTIVE_FIELD_TRIP') export const setGroupSize = createAction('SET_GROUP_SIZE') export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS') -export const clearSavable = createAction('CLEAR_SAVABLE') -export const setSavable = createAction('SET_SAVABLE') +export const clearSaveable = createAction('CLEAR_SAVEABLE') +export const setSaveable = createAction('SET_SAVEABLE') // these are date/time formats specifically used by otp-datastore const FIELD_TRIP_DATE_FORMAT = 'MM/DD/YYYY' @@ -374,7 +374,7 @@ function getOtpLocationString (location) { */ export function planTrip (request, outbound) { return async function (dispatch, getState) { - dispatch(clearSavable()) + dispatch(clearSaveable()) dispatch(setGroupSize(getGroupSize(request))) await dispatch(prepareQueryParams(request, outbound)) dispatch(makeFieldTripPlanRequests(request, outbound)) @@ -507,7 +507,7 @@ function makeFieldTripPlanRequests (request, outbound) { if (remainingGroupSize <= 0) { // All members of the field trip group have been assigned! shouldContinueSearching = false - dispatch(setSavable(outbound)) + dispatch(setSaveable(outbound)) dispatch(doFieldTripPlanRequestCleanup(searchId)) } else { // Not enough acceptable itineraries have been generated. Request more. @@ -912,6 +912,6 @@ export function clearActiveFieldTrip () { dispatch(setActiveFieldTrip(null)) dispatch(clearActiveSearch()) dispatch(setQueryParam({ numItineraries: undefined })) - dispatch(clearSavable()) + dispatch(clearSaveable()) } } diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index 39ec75fbb..65d8a0336 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -76,7 +76,7 @@ class TripStatus extends Component { } render () { - const {outbound, request, savable} = this.props + const {outbound, request, saveable} = this.props const { arriveDestinationTime, arriveSchoolTime, @@ -98,7 +98,7 @@ class TripStatus extends Component {
    diff --git a/lib/components/form/date-time-preview.js b/lib/components/form/date-time-preview.js index 145e2e6bf..d1d620951 100644 --- a/lib/components/form/date-time-preview.js +++ b/lib/components/form/date-time-preview.js @@ -1,3 +1,4 @@ +// FIXME: Remove the following eslint rule exceptions. /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import moment from 'moment' diff --git a/lib/components/form/settings-preview.js b/lib/components/form/settings-preview.js index 5fcd312e0..8388e16f2 100644 --- a/lib/components/form/settings-preview.js +++ b/lib/components/form/settings-preview.js @@ -1,3 +1,4 @@ +// FIXME: Remove the following eslint rule exceptions. /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import coreUtils from '@opentripplanner/core-utils' diff --git a/lib/components/form/styled copy.js b/lib/components/form/styled copy.js deleted file mode 100644 index 0ff6705a9..000000000 --- a/lib/components/form/styled copy.js +++ /dev/null @@ -1,184 +0,0 @@ -import styled, { css } from 'styled-components' -import { DateTimeSelector, SettingsSelectorPanel } from '@opentripplanner/trip-form' -import * as TripFormClasses from '@opentripplanner/trip-form/lib/styled' - -const commonButtonCss = css` - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - background: none; - font-family: inherit; - user-select: none; - text-align: center; - touch-action: manipulation; -` - -const commonInputCss = css` - background-color: #fff; - border: 1px solid #ccc; - border-radius: 4px; - box-shadow: inset 0 1px 1px rgba(0,0,0,.075); - color: #555; - font-family: inherit; - padding: 6px 12px; - transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; - - &:focus { - border-color: #66afe9; - box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); - outline: 0; - } -` - -const modeButtonButtonCss = css` - ${TripFormClasses.ModeButton.Button} { - ${commonButtonCss} - background-color: #fff; - border: 1px solid #ccc; - border-radius: 4px; - color: #333; - font-weight: 400; - font-size: 14px; - line-height: 1.42857143; - outline-offset:-2px; - padding: 6px 12px; - &.active { - background-color: #e6e6e6; - border-color: #adadad; - box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); - font-weight: 400; - } - &:hover { - background-color: #e6e6e6; - border-color: #adadad; - } - &.active { - background-color: #e6e6e6; - border-color: #adadad; - box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); - font-weight: 400; - &:hover { - background-color: #d4d4d4; - border-color: #8c8c8c; - } - } - } -` - -export const StyledSettingsSelectorPanel = styled(SettingsSelectorPanel)` - ${TripFormClasses.SettingLabel} { - font-weight: 400; - margin-bottom: 0; - } - ${TripFormClasses.SettingsHeader} { - font-size: 18px; - margin: 16px 0px; - } - ${TripFormClasses.SettingsSection} { - margin-bottom: 16px; - } - ${TripFormClasses.DropdownSelector} { - margin-bottom:20px; - select { - ${commonInputCss} - font-size: 14px; - height: 34px; - line-height: 1.42857143; - } - } - - ${TripFormClasses.ModeSelector} { - ${TripFormClasses.ModeButton.Button} { - ${commonButtonCss} - border: 1px solid rgb(187, 187, 187); - border-radius: 3px; - box-shadow: none; - outline: 0; - padding: 3px; - &.active { - background-color: rgb(173, 216, 230); - border: 2px solid rgb(0, 0, 0); - } - } - ${TripFormClasses.ModeButton.Title} { - font-size: 10px; - font-weight: 300; - line-height: 12px; - padding: 4px 0px 0px; - &.active { - font-weight: 600; - } - } - } - ${TripFormClasses.ModeSelector.MainRow} { - margin: 0 -10px 18px; - padding: 0 5px; - - ${TripFormClasses.ModeButton.Button} { - font-size: 200%; - font-weight: 300; - height: 54px; - &.active { - font-weight: 600; - } - } - } - ${TripFormClasses.ModeSelector.SecondaryRow} { - margin: 0 -10px 10px; - ${TripFormClasses.ModeButton.Button} { - font-size: 150%; - font-weight: 600; - height: 46px; - } - } - ${TripFormClasses.ModeSelector.TertiaryRow} { - margin: 0 -10px 10px; - ${TripFormClasses.ModeButton.Button} { - font-size: 90%; - height: 36px; - } - } - - ${TripFormClasses.SubmodeSelector.Row} { - > * { - padding: 3px 5px 3px 0px; - } - > :last-child { - padding-right: 0px; - } - ${TripFormClasses.ModeButton.Button} { - padding: 6px 12px; - } - svg, - img { - margin-left: 0px; - } - } - ${TripFormClasses.SubmodeSelector.InlineRow} { - margin: -3px 0px; - } - - ${TripFormClasses.SubmodeSelector} { - ${modeButtonButtonCss} - } -` - -export const StyledDateTimeSelector = styled(DateTimeSelector)` - margin: 0 -15px 20px; - ${TripFormClasses.DateTimeSelector.DateTimeRow} { - margin-top: 20px; - } - - input { - ${commonInputCss} - -webkit-appearance: textfield; - -moz-appearance: textfield; - appearance: textfield; - box-shadow: none; - font-size: 16px; - height: 34px; - text-align: center; /* For legacy browsers. */ - } - - ${modeButtonButtonCss} -` diff --git a/lib/components/form/trimet.styled.js b/lib/components/form/trimet.styled.js deleted file mode 100644 index a3b40c96e..000000000 --- a/lib/components/form/trimet.styled.js +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react' -import styled from 'styled-components' - -import * as TripFormClasses from '../styled' - -/** - * This file is provided as an illustrative example for custom styling. - */ - -import './trimet-mock.css' // Downloads the font. - -const TriMetStyled = styled.div` - font-family: Hind, sans-serif; - font-size: 14px; - background-color: #f0f0f0; - padding: 15px; - - ${TripFormClasses.SettingsHeader} { - color: #333333; - font-size: 18px; - margin: 16px 0px; - } - ${TripFormClasses.SettingsSection} { - margin-bottom: 16px; - } - ${TripFormClasses.SettingLabel} { - padding-top: 8px; - color: #808080; - font-weight: 100; - text-transform: uppercase; - letter-spacing: 1px; - } - ${TripFormClasses.ModeButton.Button} { - border: 1px solid rgb(187, 187, 187); - padding: 3px; - border-radius: 3px; - font-size: inherit; - font-family: inherit; - font-weight: inherit; - background: none; - outline: none; - - &.active { - border: 2px solid rgb(0, 0, 0); - background-color: rgb(173, 216, 230); - font-weight: 600; - box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - } - } - ${TripFormClasses.ModeButton.Title} { - padding: 4px 0px 0px; - font-size: 10px; - line-height: 12px; - - &.active { - text-decoration: underline; - } - } - ${TripFormClasses.DateTimeSelector.DateTimeRow} { - margin: 15px 0px; - input { - padding: 6px 12px; - text-align: center; - font-size: inherit; - font-family: inherit; - font-weight: inherit; - border: 0; - border-bottom: 1px solid #000; - } - } - ${TripFormClasses.DropdownSelector} { - select { - -webkit-appearance: none; - font-size: inherit; - font-family: inherit; - font-weight: inherit; - margin-bottom: 15px; - background: none; - border-radius: 3px; - padding: 6px 12px; - border: 1px solid #ccc; - height: 34px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - line-height: 1.42857; - color: #555; - } - > div:last-child::after { - content: "▼"; - font-size: 75%; - color: #000; - right: 8px; - top: 10px; - position: absolute; - pointer-events: none; - box-sizing: border-box; - } - } - ${TripFormClasses.SubmodeSelector.Row} { - font-size: 85%; - > * { - padding: 3px 5px 3px 0px; - } - > :last-child { - padding-right: 0px; - } - button { - padding: 6px 12px; - } - svg, - img { - margin-left: 0px; - } - } - ${TripFormClasses.SubmodeSelector.InlineRow} { - margin: -3px 0px; - } - ${TripFormClasses.ModeSelector.MainRow} { - padding: 0px 5px; - font-size: 200%; - margin-bottom: 18px; - box-sizing: border-box; - > * { - width: 100%; - height: 55px; - } - } - ${TripFormClasses.ModeSelector.SecondaryRow} { - margin-bottom: 10px; - > * { - font-size: 150%; - height: 46px; - } - } - ${TripFormClasses.ModeSelector.TertiaryRow} { - font-size: 90%; - margin-bottom: 10px; - text-align: center; - > * { - height: 36px; - } - } -` - -const trimet = contents => {contents} - -export default trimet diff --git a/lib/components/map/leg-diagram.js b/lib/components/map/leg-diagram.js index f9f98c7e1..1ad10ad5c 100644 --- a/lib/components/map/leg-diagram.js +++ b/lib/components/map/leg-diagram.js @@ -1,3 +1,4 @@ +// FIXME: Remove this eslint rule exception. /* eslint-disable jsx-a11y/no-static-element-interactions */ import memoize from 'lodash.memoize' import coreUtils from '@opentripplanner/core-utils' From e381aa802e86af0e6889e57e83cec419af1d5589 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Wed, 14 Jul 2021 22:37:31 -0700 Subject: [PATCH 28/35] refactor: address PR review comments See https://github.com/opentripplanner/otp-react-redux/pull/388 --- lib/actions/field-trip.js | 2 +- lib/components/admin/trip-status.js | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 715eeed8c..2e43e5925 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -823,7 +823,7 @@ export function viewRequestTripItineraries (request, outbound) { searchId })) - // appropriately initialize the Url params. If this doesn't happen, it won't + // appropriately initialize the URL params. If this doesn't happen, it won't // be possible to set an active itinerary properly due to funkiness with the // change in URL dispatch(updateOtpUrlParams(getState(), searchId)) diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index 2ebdb4633..05eb4c3f7 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -56,19 +56,15 @@ class TripStatus extends Component { if (!status || !trip) { return ( - - No itineraries planned! Click Plan to plan trip. - + No itineraries planned! Click Plan to plan trip. ) } return ( <> - - {trip.groupItineraries.length} group itineraries, planned by{' '} - {trip.createdBy} at {trip.timeStamp} - + {trip.groupItineraries.length} group itineraries, planned by{' '} + {trip.createdBy} at {trip.timeStamp} From 11773ca2755c074d3ca83db0e40a92031c737b97 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 15 Jul 2021 16:57:33 -0400 Subject: [PATCH 29/35] refactor: Address PR comments --- .../narrative/default/default-itinerary.js | 2 +- lib/components/narrative/realtime-annotation.js | 2 -- lib/components/viewers/trip-viewer.js | 1 + lib/util/api.js | 15 --------------- 4 files changed, 2 insertions(+), 18 deletions(-) delete mode 100644 lib/util/api.js diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 892b15c54..7826bcf81 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -211,9 +211,9 @@ class DefaultItinerary extends NarrativeItinerary {
    } placement='bottom' - // container={this} - // containerPadding={40} trigger='click'> diff --git a/lib/components/viewers/trip-viewer.js b/lib/components/viewers/trip-viewer.js index 75fe15b99..3835b99da 100644 --- a/lib/components/viewers/trip-viewer.js +++ b/lib/components/viewers/trip-viewer.js @@ -1,3 +1,4 @@ +// FIXME: Remove this eslint rule exception. /* eslint-disable jsx-a11y/label-has-for */ import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' diff --git a/lib/util/api.js b/lib/util/api.js deleted file mode 100644 index 4b158620a..000000000 --- a/lib/util/api.js +++ /dev/null @@ -1,15 +0,0 @@ -if (typeof (fetch) === 'undefined') require('isomorphic-fetch') - -/** - * Gets JSON from a response object. - * If the response contains an error code, logs the error in the console and throw exception. - * @param {*} res The HTTP response object. - */ -export function getJsonAndCheckResponse (res) { - if (res.status >= 400) { - const error = new Error('Received error from server') - error.response = res - throw error - } - return res.json() -} From 1a84f60fbfc6d79e3b1f4ab6f17010bb31a80181 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Thu, 15 Jul 2021 22:51:19 -0700 Subject: [PATCH 30/35] refactor: fix some linting errors --- lib/actions/field-trip.js | 40 ++++++++++++++--------------- lib/components/admin/trip-status.js | 2 +- lib/reducers/create-otp-reducer.js | 4 +-- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 43e5ddf75..0e4297ab8 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -204,8 +204,8 @@ export function saveRequestTripItineraries (request, outbound) { const res = await fetch( `${state.otp.config.datastoreUrl}/fieldtrip/newTrip`, { - method: 'POST', - body: makeSaveFieldTripItinerariesData(request, outbound, state) + body: makeSaveFieldTripItinerariesData(request, outbound, state), + method: 'POST' } ) text = await res.text() @@ -333,20 +333,20 @@ function makeSaveFieldTripItinerariesData (request, outbound, state) { : '' routeName = `${routeName}${leg.routeLongName}` const gtfsTrip = { - depart: moment(leg.startTime).format(FIELD_TRIP_TIME_FORMAT), - arrive: moment(leg.endTime).format(FIELD_TRIP_TIME_FORMAT), agencyAndId: leg.tripId, - tripHash: tripHashLookup[leg.tripId], - routeName, - fromStopIndex: leg.from.stopIndex, - toStopIndex: leg.to.stopIndex, - fromStopName: leg.from.name, - toStopName: leg.to.name, - headsign: leg.headsign, + arrive: moment(leg.endTime).format(FIELD_TRIP_TIME_FORMAT), capacity: getFieldTripGroupCapacityForMode( fieldTripModuleConfig, leg.mode - ) + ), + depart: moment(leg.startTime).format(FIELD_TRIP_TIME_FORMAT), + fromStopIndex: leg.from.stopIndex, + fromStopName: leg.from.name, + headsign: leg.headsign, + routeName, + toStopIndex: leg.to.stopIndex, + toStopName: leg.to.name, + tripHash: tripHashLookup[leg.tripId] } if (leg.tripBlockId) gtfsTrip.blockId = leg.tripBlockId gtfsTripsForItinerary.push(gtfsTrip) @@ -483,7 +483,7 @@ function makeFieldTripPlanRequests (request, outbound) { // there aren't after making this next request, the pending amount will // be set to 0. This needs to happen after the routingQuery so the search // is defined. - dispatch(setPendingRequests({ searchId, pending: 2 })) + dispatch(setPendingRequests({ pending: 2, searchId })) // obtain trip hashes from OTP Index API await getMissingTripHashesForActiveItineraries() @@ -739,14 +739,14 @@ function tripsOverlap (leg, tripHashLookup, tripId, tripInUse) { // These can be overridden in the field trip module config. const defaultFieldTripModeCapacities = { - 'TRAM': 80, - 'SUBWAY': 120, - 'RAIL': 80, 'BUS': 40, - 'FERRY': 100, 'CABLE_CAR': 20, + 'FERRY': 100, + 'FUNICULAR': 20, 'GONDOLA': 15, - 'FUNICULAR': 20 + 'RAIL': 80, + 'SUBWAY': 120, + 'TRAM': 80 } const unknownModeCapacity = 15 @@ -772,7 +772,7 @@ function getFieldTripGroupCapacityForMode (fieldTripModuleConfig, mode) { function doFieldTripPlanRequestCleanup (searchId) { return function (dispatch, getState) { // set pending searches to 0 to indicate searching is finished - dispatch(setPendingRequests({ searchId, pending: 0 })) + dispatch(setPendingRequests({ pending: 0, searchId })) // clear banned trips query param dispatch(setQueryParam({ bannedTrips: undefined })) } @@ -788,7 +788,7 @@ export function deleteRequestTripItineraries (request, tripId) { if (sessionIsInvalid(callTaker.session)) return const {sessionId} = callTaker.session return fetch(`${datastoreUrl}/fieldtrip/deleteTrip`, - {method: 'POST', body: serialize({ id: tripId, sessionId })} + {body: serialize({ id: tripId, sessionId }), method: 'POST'} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index feb40f0c4..629390e02 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -122,7 +122,7 @@ class TripStatus extends Component { } const mapStateToProps = (state, ownProps) => { - const { request, outbound } = ownProps + const { outbound, request } = ownProps const { saveable } = state.callTaker.fieldTrip return { currentQuery: state.otp.currentQuery, diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 31598daa0..f019c56eb 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -288,8 +288,8 @@ function createOtpReducer (config) { } } return update(state, { - activeSearchId: { $set: searchId } - searches: { [searchId]: searchUpdate }, + activeSearchId: { $set: searchId }, + searches: { [searchId]: searchUpdate } }) case 'ROUTING_ERROR': return update(state, { From 84ccbb77815f819b5ae2429081ae7cf9ef0d1141 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 16 Jul 2021 10:39:47 -0400 Subject: [PATCH 31/35] fix(error-msg): use url query params if response lacks OTP data fix #417 --- lib/actions/api.js | 170 +---------------------------- lib/reducers/create-otp-reducer.js | 3 +- lib/util/state.js | 12 +- 3 files changed, 14 insertions(+), 171 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index 435854913..4be9c5bf9 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -124,8 +124,8 @@ export function routingQuery (searchId = null, updateSearchInReducer = false) { return Promise.all(iterations.map((injectedParams, i) => { const requestId = randId() // fetch a realtime route - const query = constructRoutingQuery(state, false, injectedParams) - const realTimeFetch = fetch(query, getOtpFetchOptions(state)) + const url = constructRoutingQuery(state, false, injectedParams) + const realTimeFetch = fetch(url, getOtpFetchOptions(state)) .then(getJsonAndCheckResponse) .then(json => { const dispatchedRoutingResponse = dispatch(routingResponse({ @@ -143,12 +143,12 @@ export function routingQuery (searchId = null, updateSearchInReducer = false) { if (!isStoredPlace(to)) { dispatch(rememberPlace({ location: formatRecentPlace(to), type: 'recent' })) } - dispatch(rememberSearch(formatRecentSearch(query, state))) + dispatch(rememberSearch(formatRecentSearch(url, state))) } return dispatchedRoutingResponse }) .catch(error => { - dispatch(routingError({ error, requestId, searchId })) + dispatch(routingError({ error, requestId, searchId, url })) }) // Update OTP URL params if a new search. In other words, if we're // performing a search based on query params taken from the URL after a back @@ -328,84 +328,6 @@ export function findStop (params) { ) } -// TODO: Optionally substitute GraphQL queries? Note: this is not currently -// possible because gtfsdb (the alternative transit index used by TriMet) does not -// support GraphQL queries. -// export function findStop (params) { -// const query = ` -// query stopQuery($stopId: [String]) { -// stops (ids: $stopId) { -// id: gtfsId -// code -// name -// url -// lat -// lon -// stoptimesForPatterns { -// pattern { -// id: semanticHash -// route { -// id: gtfsId -// longName -// shortName -// sortOrder -// } -// } -// stoptimes { -// scheduledArrival -// realtimeArrival -// arrivalDelay -// scheduledDeparture -// realtimeDeparture -// departureDelay -// timepoint -// realtime -// realtimeState -// serviceDay -// headsign -// } -// } -// } -// } -// ` -// return createGraphQLQueryAction( -// query, -// { stopId: params.stopId }, -// findStopResponse, -// findStopError, -// { -// // find stop should not be throttled since it can make quite frequent -// // updates when fetching stop times for a stop -// noThrottle: true, -// serviceId: 'stops', -// rewritePayload: (payload) => { -// // convert pattern array to ID-mapped object -// const patterns = [] -// const { stoptimesForPatterns, ...stop } = payload.data.stops[0] -// stoptimesForPatterns.forEach(obj => { -// const { pattern, stoptimes: stopTimes } = obj -// // It's possible that not all stop times for a pattern will share the -// // same headsign, but this is probably a minor edge case. -// const headsign = stopTimes[0] -// ? stopTimes[0].headsign -// : pattern.route.longName -// const patternIndex = patterns.findIndex(p => -// p.headsign === headsign && pattern.route.id === p.route.id) -// if (patternIndex === -1) { -// patterns.push({ ...pattern, headsign, stopTimes }) -// } else { -// patterns[patternIndex].stopTimes.push(...stopTimes) -// } -// }) -// return { -// ...stop, -// patterns -// } -// } -// } -// ) -// } - // Single trip lookup query export const findTripResponse = createAction('FIND_TRIP_RESPONSE') @@ -558,45 +480,6 @@ export function findRoutes (params) { ) } -// export function findRoutes (params) { -// const query = ` -// { -// routes { -// id: gtfsId -// color -// longName -// shortName -// mode -// type -// desc -// bikesAllowed -// sortOrder -// textColor -// url -// agency { -// id: gtfsId -// name -// url -// } -// } -// } -// ` -// return createGraphQLQueryAction( -// query, -// {}, -// findRoutesResponse, -// findRoutesError, -// { -// serviceId: 'routes', -// rewritePayload: (payload) => { -// const routes = {} -// payload.data.routes.forEach(rte => { routes[rte.id] = rte }) -// return routes -// } -// } -// ) -// } - // Patterns for Route lookup query // TODO: replace with GraphQL query for route => patterns => geometry const findPatternsForRouteResponse = createAction('FIND_PATTERNS_FOR_ROUTE_RESPONSE') @@ -673,51 +556,6 @@ export function findGeometryForPattern (params) { ) } -// export function findRoute (params) { -// const query = ` -// query routeQuery($routeId: [String]) { -// routes (ids: $routeId) { -// id: gtfsId -// patterns { -// id: semanticHash -// directionId -// headsign -// name -// semanticHash -// geometry { -// lat -// lon -// } -// } -// } -// } -// ` -// return createGraphQLQueryAction( -// query, -// { routeId: params.routeId }, -// findPatternsForRouteResponse, -// findPatternsForRouteError, -// { -// rewritePayload: (payload) => { -// // convert pattern array to ID-mapped object -// const patterns = {} -// payload.data.routes[0].patterns.forEach(ptn => { -// patterns[ptn.id] = { -// routeId: params.routeId, -// patternId: ptn.id, -// geometry: ptn.geometry -// } -// }) -// -// return { -// routeId: params.routeId, -// patterns -// } -// } -// } -// ) -// } - // TNC ETA estimate lookup query export const transportationNetworkCompanyEtaResponse = createAction('TNC_ETA_RESPONSE') diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index f019c56eb..488f99ce5 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -299,7 +299,8 @@ function createOtpReducer (config) { response: { $push: [{ error: action.payload.error, - requestId + requestId, + url: action.payload.url }] } } diff --git a/lib/util/state.js b/lib/util/state.js index a1bc3e2b2..eae3c4bb7 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -3,6 +3,7 @@ import isEqual from 'lodash.isequal' import memoize from 'lodash.memoize' import moment from 'moment' import hash from 'object-hash' +import qs from 'qs' import { createSelector } from 'reselect' import { MainPanelContent } from '../actions/ui' @@ -93,16 +94,19 @@ export function getResponsesWithErrors (state) { response.forEach(res => { if (res) { if (res.error) { + const params = qs.parse(res.url) + const modeStr = res.requestParameters?.mode || params.mode || '' let msg = res.error.msg || 'An error occurred while planning a trip' // include the modes if doing batch routing - if (showModes && res.requestParameters?.mode) { - const mode = humanReadableMode(res.requestParameters.mode) + if (showModes && modeStr) { + const mode = humanReadableMode(modeStr) msg = `No trip found for ${mode}. ${msg.replace(/^No trip found. /, '')}` } tripPlanningErrors.push({ id: res.error.id, - modes: res.requestParameters?.mode?.split(','), - msg + modes: modeStr.split(','), + msg, + url: res.url }) } const feedWideRentalErrors = getFeedWideRentalErrors(res) From a46c56c3d300f7e0730cbfd2067ab5530f65d445 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 16 Jul 2021 14:04:56 -0400 Subject: [PATCH 32/35] test(api.js): update snapshots --- __tests__/actions/__snapshots__/api.js.snap | 3 +++ 1 file changed, 3 insertions(+) diff --git a/__tests__/actions/__snapshots__/api.js.snap b/__tests__/actions/__snapshots__/api.js.snap index f5cf71d3b..f00d42d1d 100644 --- a/__tests__/actions/__snapshots__/api.js.snap +++ b/__tests__/actions/__snapshots__/api.js.snap @@ -38,6 +38,7 @@ Array [ "error": [TypeError: Cannot read property 'trackRecent' of undefined], "requestId": "abcd1239", "searchId": "abcd1234", + "url": "http://mock-host.com:80/api/plan?fromPlace=Origin%20%2812%2C34%29%3A%3A12%2C34&toPlace=Destination%20%2834%2C12%29%3A%3A34%2C12&mode=WALK%2CTRANSIT&ignoreRealtimeUpdates=false", }, "type": "ROUTING_ERROR", }, @@ -66,6 +67,7 @@ Array [ "error": [Error: Received error from server], "requestId": "abcd1240", "searchId": "abcd1237", + "url": "http://mock-host.com:80/api/plan?fromPlace=Origin%20%2812%2C34%29%3A%3A12%2C34&toPlace=Destination%20%2834%2C12%29%3A%3A34%2C12&mode=WALK%2CTRANSIT&ignoreRealtimeUpdates=false", }, "type": "ROUTING_ERROR", }, @@ -111,6 +113,7 @@ Array [ "error": [TypeError: Cannot read property 'trackRecent' of undefined], "requestId": "abcd1236", "searchId": "abcd1234", + "url": "http://mock-host.com:80/api/plan?fromPlace=Origin%20%2812%2C34%29%3A%3A12%2C34&toPlace=Destination%20%2834%2C12%29%3A%3A34%2C12&mode=WALK%2CTRANSIT&ignoreRealtimeUpdates=false", }, "type": "ROUTING_ERROR", }, From 948069ba4c0a5b6cdeff7d51b05b12344f0fc43e Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 19 Jul 2021 18:14:39 -0400 Subject: [PATCH 33/35] fix(deps): bump vehicle overlay/base map --- package.json | 4 ++-- yarn.lock | 33 +++++++++------------------------ 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 92c1e503e..5de73db58 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "homepage": "https://github.com/opentripplanner/otp-react-redux#readme", "dependencies": { "@auth0/auth0-react": "^1.1.0", - "@opentripplanner/base-map": "^1.0.5", + "@opentripplanner/base-map": "^1.1.0", "@opentripplanner/core-utils": "^3.2.2", "@opentripplanner/endpoints-overlay": "^1.0.6", "@opentripplanner/from-to-location-picker": "^1.0.4", @@ -52,7 +52,7 @@ "@opentripplanner/trip-details": "^1.1.4", "@opentripplanner/trip-form": "^1.0.5", "@opentripplanner/trip-viewer-overlay": "^1.0.4", - "@opentripplanner/vehicle-rental-overlay": "^1.0.6", + "@opentripplanner/vehicle-rental-overlay": "^1.1.1", "blob-stream": "^0.1.3", "bootstrap": "^3.3.7", "bowser": "^1.9.3", diff --git a/yarn.lock b/yarn.lock index 058839498..74b606965 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1545,30 +1545,15 @@ dependencies: "@types/node" ">= 8" -"@opentripplanner/base-map@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@opentripplanner/base-map/-/base-map-1.0.5.tgz#b4f92e4e848f340e30b1ed5c6d3a2338d6b8e017" - integrity sha512-4Ra/BPWV0laCiYLuqEPZaI5xQtfNbYbPl8bcysoD82m1ED3uqS/b8CW7i14ZsVVdfoXOZoI3b6QVAXfPzfxM1A== +"@opentripplanner/base-map@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/base-map/-/base-map-1.1.0.tgz#7426c1f68db4e11e6e36f27a4ca34a88964e0392" + integrity sha512-tSPSfzv92IaS3LLWUQfsDSSOE6PyCrccMmungfZwbfbeZFgflNTT9Z5WS/Jju6KEbPJKI8NRX6Qi8/DI5ZWXOg== dependencies: "@opentripplanner/core-utils" "^3.0.4" prop-types "^15.7.2" -"@opentripplanner/core-utils@^3.0.0", "@opentripplanner/core-utils@^3.0.4", "@opentripplanner/core-utils@^3.1.1", "@opentripplanner/core-utils@^3.2.2": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-3.2.2.tgz#5d39b6a16670e7818cbe1a8784d26a6f9699bb4e" - integrity sha512-+mmIWbvAtVEfU5lJnxs+3/qxJqhZqJne1kpeobK3Do1pqMXIHs0MVY1cZjd2/SpjXoQy+BYBZUnJmbjgDOdY/w== - dependencies: - "@mapbox/polyline" "^1.1.0" - "@opentripplanner/geocoder" "^1.0.2" - "@turf/along" "^6.0.1" - bowser "^2.7.0" - lodash.isequal "^4.5.0" - moment "^2.24.0" - moment-timezone "^0.5.27" - prop-types "^15.7.2" - qs "^6.9.1" - -"@opentripplanner/core-utils@^3.2.1", "@opentripplanner/core-utils@^3.2.2": +"@opentripplanner/core-utils@^3.0.0", "@opentripplanner/core-utils@^3.0.4", "@opentripplanner/core-utils@^3.1.1", "@opentripplanner/core-utils@^3.2.1", "@opentripplanner/core-utils@^3.2.2": version "3.2.2" resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-3.2.2.tgz#5d39b6a16670e7818cbe1a8784d26a6f9699bb4e" integrity sha512-+mmIWbvAtVEfU5lJnxs+3/qxJqhZqJne1kpeobK3Do1pqMXIHs0MVY1cZjd2/SpjXoQy+BYBZUnJmbjgDOdY/w== @@ -1757,10 +1742,10 @@ "@opentripplanner/core-utils" "^3.0.4" prop-types "^15.7.2" -"@opentripplanner/vehicle-rental-overlay@^1.0.6": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@opentripplanner/vehicle-rental-overlay/-/vehicle-rental-overlay-1.0.6.tgz#95f894181070c64fa176918cd6a6dd6b122e3d19" - integrity sha512-Ued2Q1S0CT5ZHDAYtO+B1rqX0p63KdFN6noDshaDtHc3y37XYQuI6mzFTUrkkRDRlWWOxu8qDB1ZdSYj5tJCKw== +"@opentripplanner/vehicle-rental-overlay@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@opentripplanner/vehicle-rental-overlay/-/vehicle-rental-overlay-1.1.1.tgz#f236459cc9ab589cbdc72f64d43f2a62d3569eb0" + integrity sha512-RGT7euClDx+7lZaqBmz5ck9y98UUpLgsxh2UmB+xe08ygBIia1rPRyf/ocWGLHRa6NBNZF+YMoqSQw+bcOTJnQ== dependencies: "@opentripplanner/core-utils" "^3.0.4" "@opentripplanner/from-to-location-picker" "^1.0.3" From 6cdbdb6ccdc554972dad6a66a316e49a43b4b774 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Wed, 21 Jul 2021 15:53:34 -0700 Subject: [PATCH 34/35] fix(vehicle-rental): bump to latest vehicle-rental-overlay fixes https://github.com/opentripplanner/otp-react-redux/issues/414 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 5de73db58..da7762a19 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@opentripplanner/trip-details": "^1.1.4", "@opentripplanner/trip-form": "^1.0.5", "@opentripplanner/trip-viewer-overlay": "^1.0.4", - "@opentripplanner/vehicle-rental-overlay": "^1.1.1", + "@opentripplanner/vehicle-rental-overlay": "^1.1.2", "blob-stream": "^0.1.3", "bootstrap": "^3.3.7", "bowser": "^1.9.3", diff --git a/yarn.lock b/yarn.lock index 74b606965..ccdd578e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1742,10 +1742,10 @@ "@opentripplanner/core-utils" "^3.0.4" prop-types "^15.7.2" -"@opentripplanner/vehicle-rental-overlay@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@opentripplanner/vehicle-rental-overlay/-/vehicle-rental-overlay-1.1.1.tgz#f236459cc9ab589cbdc72f64d43f2a62d3569eb0" - integrity sha512-RGT7euClDx+7lZaqBmz5ck9y98UUpLgsxh2UmB+xe08ygBIia1rPRyf/ocWGLHRa6NBNZF+YMoqSQw+bcOTJnQ== +"@opentripplanner/vehicle-rental-overlay@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@opentripplanner/vehicle-rental-overlay/-/vehicle-rental-overlay-1.1.2.tgz#04c670da35205c94786118cd5a8610190c26d71c" + integrity sha512-d36c79zj37Y9M+gDW5fE7RU3nVlGh68mIaKxAU3zlqY/jDsPHMjMltp+KRJwAhFVV19vnfMzriqjU6epRqBhwQ== dependencies: "@opentripplanner/core-utils" "^3.0.4" "@opentripplanner/from-to-location-picker" "^1.0.3" From c698a8ac592ebeb3c89e10dbbd4ed804dc3dabcd Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Wed, 21 Jul 2021 15:58:51 -0700 Subject: [PATCH 35/35] fix(vehicle-rental): fix two itinerary-related vehicle rental bugs --- lib/components/narrative/narrative-itineraries-errors.js | 9 +++++---- lib/util/state.js | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/components/narrative/narrative-itineraries-errors.js b/lib/components/narrative/narrative-itineraries-errors.js index 7b927a936..37ca1a098 100644 --- a/lib/components/narrative/narrative-itineraries-errors.js +++ b/lib/components/narrative/narrative-itineraries-errors.js @@ -24,12 +24,13 @@ const IssueContents = styled.div` export default function NarrativeItinerariesErrors ({ errorMessages, errors }) { return errors.map((error, idx) => { - let icon + let icon = if (error.network) { const CompanyIcon = getCompanyIcon(error.network) - icon = - } else { - icon = + // check if company icon exists to avoid rendering undefined + if (CompanyIcon) { + icon = + } } return ( diff --git a/lib/util/state.js b/lib/util/state.js index eae3c4bb7..0e1f1fa3b 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -471,12 +471,14 @@ export const getTransitiveData = createSelector( getOtpResponse, itineraryResponseExists, getItineraryToRender, + state => state.otp.config.companies, (state, props) => props.getTransitiveRouteLabel, ( hasResponse, otpResponse, hasItineraryResponse, itineraryToRender, + companies, getTransitiveRouteLabel ) => { if (hasResponse) { @@ -484,7 +486,7 @@ export const getTransitiveData = createSelector( return itineraryToRender ? coreUtils.map.itineraryToTransitive( itineraryToRender, - null, + companies, getTransitiveRouteLabel ) : null