diff --git a/app/component/RouteNumber.js b/app/component/RouteNumber.js index 0bb9a7cddd..0f799fc9e5 100644 --- a/app/component/RouteNumber.js +++ b/app/component/RouteNumber.js @@ -12,15 +12,43 @@ const LONG_ROUTE_NUMBER_LENGTH = 6; function RouteNumber(props, context) { const mode = props.mode.toLowerCase(); - const { alertSeverityLevel, color, withBicycle, text } = props; + const { alertSeverityLevel, color, withBicycle, withCar } = props; const isScooter = mode === TransportMode.Scooter.toLowerCase(); - const textIsText = typeof text === 'string'; // can be also react node + + // Perform text-related processing + let filteredText = props.text; + if ( + props.shortenLongText && + context.config.disabledLegTextModes?.includes(mode) && + props.className.includes('line') + ) { + filteredText = ''; + } + const textFieldIsText = typeof filteredText === 'string'; // can be also react node + if ( + props.shortenLongText && + context.config.shortenLongTextThreshold && + filteredText && + textFieldIsText && + filteredText.length > context.config.shortenLongTextThreshold + ) { + filteredText = `${filteredText.substring( + 0, + context.config.shortenLongTextThreshold - 3, + )}...`; + } const longText = - text && textIsText && text.length >= LONG_ROUTE_NUMBER_LENGTH; + filteredText && + textFieldIsText && + filteredText.length >= LONG_ROUTE_NUMBER_LENGTH; // Checks if route only has letters without identifying numbers and // length doesn't fit in the tab view const hasNoShortName = - text && textIsText && /^([^0-9]*)$/.test(text) && text.length > 3; + filteredText && + textFieldIsText && + /^([^0-9]*)$/.test(filteredText) && + filteredText.length > 3; + const getColor = () => color || (props.isTransitLeg ? 'currentColor' : null); const getIcon = ( @@ -57,6 +85,12 @@ function RouteNumber(props, context) { className="itinerary-icon_with-bicycle" /> )} + {withCar && ( + + )} ); } @@ -83,6 +117,12 @@ function RouteNumber(props, context) { className="itinerary-icon_with-bicycle" /> )} + {withCar && ( + + )} ); }; @@ -128,7 +168,7 @@ function RouteNumber(props, context) { )} )} - {text && ( + {filteredText && (
- {props.text} + {filteredText} - {textIsText && ( - {text?.toLowerCase()} + {textFieldIsText && ( + {filteredText?.toLowerCase()} )}
)} @@ -206,9 +246,11 @@ RouteNumber.propTypes = { duration: PropTypes.number, isTransitLeg: PropTypes.bool, withBicycle: PropTypes.bool, + withCar: PropTypes.bool, card: PropTypes.bool, appendClass: PropTypes.string, occupancyStatus: PropTypes.string, + shortenLongText: PropTypes.bool, }; RouteNumber.defaultProps = { @@ -228,9 +270,11 @@ RouteNumber.defaultProps = { isTransitLeg: false, renderModeIcons: false, withBicycle: false, + withCar: false, color: undefined, duration: undefined, occupancyStatus: undefined, + shortenLongText: false, }; RouteNumber.contextTypes = { @@ -238,5 +282,4 @@ RouteNumber.contextTypes = { config: configShape.isRequired, }; -RouteNumber.displayName = 'RouteNumber'; export default RouteNumber; diff --git a/app/component/RouteNumberContainer.js b/app/component/RouteNumberContainer.js index c17b06c808..eee7a62fae 100644 --- a/app/component/RouteNumberContainer.js +++ b/app/component/RouteNumberContainer.js @@ -1,59 +1,30 @@ import PropTypes from 'prop-types'; import React from 'react'; import { routeShape, configShape } from '../util/shapes'; -import { getLegText } from '../util/legUtils'; +import { getRouteText } from '../util/legUtils'; import RouteNumber from './RouteNumber'; const RouteNumberContainer = ( - { - alertSeverityLevel, - interliningWithRoute, - className, - route, - isCallAgency, - withBicycle, - occupancyStatus, - mode, - ...props - }, + { interliningWithRoute, route, mode, ...props }, { config }, ) => route && ( ); RouteNumberContainer.propTypes = { - alertSeverityLevel: PropTypes.string, route: routeShape.isRequired, interliningWithRoute: PropTypes.string, - isCallAgency: PropTypes.bool, - vertical: PropTypes.bool, - className: PropTypes.string, - fadeLong: PropTypes.bool, - withBicycle: PropTypes.bool, - occupancyStatus: PropTypes.string, mode: PropTypes.string, }; RouteNumberContainer.defaultProps = { interliningWithRoute: undefined, - alertSeverityLevel: undefined, - isCallAgency: false, - vertical: false, - fadeLong: false, - className: '', - withBicycle: false, - occupancyStatus: undefined, mode: undefined, }; @@ -61,5 +32,4 @@ RouteNumberContainer.contextTypes = { config: configShape.isRequired, }; -RouteNumberContainer.displayName = 'RouteNumberContainer'; export default RouteNumberContainer; diff --git a/app/component/itinerary/AlternativeItineraryBar.js b/app/component/itinerary/AlternativeItineraryBar.js index cc7401f344..ebeda81df6 100644 --- a/app/component/itinerary/AlternativeItineraryBar.js +++ b/app/component/itinerary/AlternativeItineraryBar.js @@ -15,6 +15,7 @@ export default function AlternativeItineraryBar( bikePlan, bikePublicPlan, carPlan, + carPublicPlan, parkRidePlan, loading, }, @@ -69,6 +70,14 @@ export default function AlternativeItineraryBar( onClick={selectStreetMode} /> )} + {carPublicPlan?.edges?.length > 0 && ( + + )} {config.emphasizeOneWayJourney && (
); } else if (bicycleWalkLeg) { - const modeClassNames = bicycleWalkLeg.to?.stop - ? [modeClassName, bicycleWalkLeg.mode.toLowerCase()] - : [bicycleWalkLeg.mode.toLowerCase(), modeClassName]; circleLine = ( - + ); } else if (mode === 'BICYCLE') { circleLine = ( @@ -315,42 +316,46 @@ export default function BicycleLeg(
)} {bicycleWalkLeg?.from.stop && ( -
-
- {bicycleWalkLeg.distance === -1 ? ( - - ), - }} - /> - ) : ( - - ), - duration: durationToString(bicycleWalkLeg.duration * 1000), - distance: displayDistance( - parseInt(bicycleWalkLeg.distance, 10), - config, - intl.formatNumber, - ), - }} - /> - )} - + {bicycleWalkLeg.distance === -1 ? ( + + ), + }} /> -
+ ) : ( + + ), + duration: durationToString(bicycleWalkLeg.duration * 1000), + distance: displayDistance( + parseInt(bicycleWalkLeg.distance, 10), + config, + intl.formatNumber, + ), + }} + /> + )} +
)} {isScooter && !scooterSettingsOn && ( @@ -401,53 +406,61 @@ export default function BicycleLeg( )} -
-
- {stopsDescription} - -
+
+ {stopsDescription} +
{bicycleWalkLeg && bicycleWalkLeg?.to.stop && ( -
-
- {bicycleWalkLeg.distance === -1 ? ( - - ), - }} - /> - ) : ( - - ), - duration: durationToString(bicycleWalkLeg.duration * 1000), - distance: displayDistance( - parseInt(bicycleWalkLeg.distance, 10), - config, - intl.formatNumber, - ), - }} - /> - )} - + {bicycleWalkLeg.distance === -1 ? ( + + ), + }} /> -
+ ) : ( + + ), + duration: durationToString(bicycleWalkLeg.duration * 1000), + distance: displayDistance( + parseInt(bicycleWalkLeg.distance, 10), + config, + intl.formatNumber, + ), + }} + /> + )} +
)} {isScooter && ( diff --git a/app/component/itinerary/BikeParkLeg.js b/app/component/itinerary/BikeParkLeg.js index 30c81c124b..783e392ac1 100644 --- a/app/component/itinerary/BikeParkLeg.js +++ b/app/component/itinerary/BikeParkLeg.js @@ -72,18 +72,22 @@ const BikeParkLeg = ( focusAction={focusAction} />
-
-
- - -
+
+ +
diff --git a/app/component/itinerary/CarLeg.js b/app/component/itinerary/CarLeg.js index c019576969..40f1f1f059 100644 --- a/app/component/itinerary/CarLeg.js +++ b/app/component/itinerary/CarLeg.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { FormattedMessage, intlShape } from 'react-intl'; +import cx from 'classnames'; import { legShape, configShape } from '../../util/shapes'; import Icon from '../Icon'; import ItineraryMapAction from './ItineraryMapAction'; @@ -8,6 +9,7 @@ import { displayDistance } from '../../util/geo-utils'; import { durationToString } from '../../util/timeUtils'; import ItineraryCircleLineWithIcon from './ItineraryCircleLineWithIcon'; import { legTimeStr, legDestination } from '../../util/legUtils'; +import ItineraryCircleLineLong from './ItineraryCircleLineLong'; export default function CarLeg(props, { config, intl }) { const distance = displayDistance( @@ -19,6 +21,20 @@ export default function CarLeg(props, { config, intl }) { const firstLegClassName = props.index === 0 ? 'first' : ''; const modeClassName = 'car'; + const circleLine = props.carBoardingLeg ? ( + + ) : ( + + ); + const [address, place] = props.leg.from.name.split(/, (.+)/); // Splits the name-string to two parts from the first occurance of ', ' /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ @@ -42,11 +58,7 @@ export default function CarLeg(props, { config, intl }) { {legTimeStr(props.leg.start)} - + {circleLine}
@@ -70,23 +82,69 @@ export default function CarLeg(props, { config, intl }) { focusAction={props.focusAction} />
-
-
+ {props.carBoardingLeg?.from.stop && ( +
+ ), + }} />
+ )} +
+ +
+ {props.carBoardingLeg?.to.stop && ( +
+ + ), + }} + /> + +
+ )}
); @@ -98,9 +156,10 @@ CarLeg.propTypes = { focusAction: PropTypes.func.isRequired, focusToLeg: PropTypes.func.isRequired, children: PropTypes.node, + carBoardingLeg: legShape, }; -CarLeg.defaultProps = { children: undefined }; +CarLeg.defaultProps = { children: undefined, carBoardingLeg: undefined }; CarLeg.contextTypes = { config: configShape.isRequired, diff --git a/app/component/itinerary/CarParkLeg.js b/app/component/itinerary/CarParkLeg.js index 1e99e67836..9065b6c62e 100644 --- a/app/component/itinerary/CarParkLeg.js +++ b/app/component/itinerary/CarParkLeg.js @@ -90,18 +90,22 @@ function CarParkLeg(props, { config, intl }) { /> {!props.noWalk && ( -
-
- - -
+
+ +
)}
diff --git a/app/component/itinerary/InterlineInfo.js b/app/component/itinerary/InterlineInfo.js index f000524f77..fd0ad98652 100644 --- a/app/component/itinerary/InterlineInfo.js +++ b/app/component/itinerary/InterlineInfo.js @@ -6,7 +6,7 @@ import { getHeadsignFromRouteLongName, legTime } from '../../util/legUtils'; import Icon from '../Icon'; import { legShape } from '../../util/shapes'; -const InterlineInfo = ({ legs, leg }) => { +const InterlineInfo = ({ legs, leg, usingOwnCarWholeTrip }) => { let totalWait = 0; const allLegs = [leg, ...legs]; const routes = []; @@ -18,11 +18,12 @@ const InterlineInfo = ({ legs, leg }) => { } }); } + const icon = usingOwnCarWholeTrip ? 'icon-icon_wait-car' : 'icon-icon_wait'; return (
{legs.length === 1 && ( <> - + { )} {legs.length > 1 && ( <> - + { InterlineInfo.propTypes = { leg: legShape.isRequired, legs: PropTypes.arrayOf(legShape).isRequired, + usingOwnCarWholeTrip: PropTypes.bool.isRequired, }; export default InterlineInfo; diff --git a/app/component/itinerary/Itinerary.js b/app/component/itinerary/Itinerary.js index acf5d4de2a..22eb769587 100644 --- a/app/component/itinerary/Itinerary.js +++ b/app/component/itinerary/Itinerary.js @@ -1,6 +1,6 @@ import cx from 'classnames'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, { createRef, useLayoutEffect, useState } from 'react'; import { graphql, createFragmentContainer } from 'react-relay'; import { FormattedMessage, intlShape } from 'react-intl'; import { @@ -22,8 +22,10 @@ import { isCallAgencyLeg, getInterliningLegs, getTotalDistance, + getRouteText, legTime, legTimeStr, + LegMode, } from '../../util/legUtils'; import { dateOrEmpty, isTomorrow, timeStr } from '../../util/timeUtils'; import withBreakpoint from '../../util/withBreakpoint'; @@ -38,6 +40,8 @@ import { getRouteMode } from '../../util/modeUtils'; import { getCapacityForLeg } from '../../util/occupancyUtil'; import getCo2Value from '../../util/emissions'; +const NAME_LENGTH_THRESHOLD = 65; // for truncating long short names + const Leg = ({ mode, routeNumber, @@ -83,7 +87,9 @@ export function RouteLeg( interliningWithRoute, fitRouteNumber, withBicycle, + withCar, hasOneTransitLeg, + shortenLabels, }, { config }, ) { @@ -126,7 +132,9 @@ export function RouteLeg( withBar isTransitLeg={isTransitLeg} withBicycle={withBicycle} + withCar={withCar} occupancyStatus={getOccupancyStatus()} + shortenLongText={shortenLabels} /> ); } @@ -150,7 +158,9 @@ RouteLeg.propTypes = { interliningWithRoute: PropTypes.string, isTransitLeg: PropTypes.bool, withBicycle: PropTypes.bool.isRequired, + withCar: PropTypes.bool.isRequired, hasOneTransitLeg: PropTypes.bool, + shortenLabels: PropTypes.bool, }; RouteLeg.contextTypes = { @@ -161,6 +171,7 @@ RouteLeg.defaultProps = { isTransitLeg: true, interliningWithRoute: undefined, hasOneTransitLeg: false, + shortenLabels: false, }; export const ModeLeg = ( @@ -277,7 +288,10 @@ const Itinerary = ( leg => getLegMode(leg) === 'BICYCLE' && leg.rentedBike === false, ); const usingOwnBicycleWholeTrip = - usingOwnBicycle && itinerary.legs.every(leg => !leg.to?.vehicleParking); + usingOwnBicycle && itinerary.legs.every(leg => !leg.to.vehicleParking); + const usingOwnCar = itinerary.legs.some(leg => getLegMode(leg) === 'CAR'); + const usingOwnCarWholeTrip = + usingOwnCar && itinerary.legs.every(leg => !leg.to.vehicleParking); const { refTime } = props; const startTime = Date.parse(itinerary.start); const endTime = Date.parse(itinerary.end); @@ -295,11 +309,14 @@ const Itinerary = ( let intermediateSlack = 0; let transitLegCount = 0; let containsScooterLeg = false; + let nameLengthSum = 0; // approximate space required for route labels compressedLegs.forEach((leg, i) => { if (isTransitLeg(leg)) { noTransitLegs = false; transitLegCount += 1; + nameLengthSum += getRouteText(leg.route, config).length; } + nameLengthSum += 10; // every leg requires some minimum space if ( leg.intermediatePlace || connectsFromViaPoint(leg, intermediatePlaces) @@ -309,6 +326,7 @@ const Itinerary = ( } containsScooterLeg = leg.mode === 'SCOOTER' || containsScooterLeg; }); + const shortenLabels = nameLengthSum > NAME_LENGTH_THRESHOLD; const durationWithoutSlack = duration - intermediateSlack; // don't include time spent at intermediate places in calculations for bar lengths const relativeLength = durationMs => (100 * durationMs) / durationWithoutSlack; // as % @@ -559,6 +577,9 @@ const Itinerary = ( const withBicycle = usingOwnBicycleWholeTrip && config.bikeBoardingModes[leg.route.mode] !== undefined; + const withCar = + usingOwnCarWholeTrip && + config.carBoardingModes[leg.route.mode] !== undefined; if ( previousLeg && !previousLeg.intermediatePlace && @@ -584,7 +605,9 @@ const Itinerary = ( legLength={legLength} large={breakpoint === 'large'} withBicycle={withBicycle} + withCar={withCar} hasOneTransitLeg={hasOneTransitLeg(itinerary)} + shortenLabels={shortenLabels} />, ); } @@ -612,8 +635,9 @@ const Itinerary = ( renderModeIcons={renderModeIcons} duration={waitingTimeinMin} isTransitLeg={false} - mode="WAIT" + mode={LegMode.Wait} large={breakpoint === 'large'} + icon={usingOwnCarWholeTrip ? 'icon-icon_wait-car' : undefined} />, ); } @@ -825,6 +849,20 @@ const Itinerary = ( co2value !== null && co2value >= 0 && !containsScooterLeg; + + const itineraryContainerOverflowRef = createRef(); + const [showOverflowIcon, setShowOverflowIcon] = useState(false); + useLayoutEffect(() => { + // If the itinerary length exceeds its boundaries an icon with dots is displayed. + if ( + itineraryContainerOverflowRef.current.clientWidth < + itineraryContainerOverflowRef.current.scrollWidth + ) { + setShowOverflowIcon(true); + } else { + setShowOverflowIcon(false); + } + }, [itineraryContainerOverflowRef]); return (

@@ -899,11 +937,20 @@ const Itinerary = ( aria-hidden="true" >
{legs}
+
+ {showOverflowIcon && ( + + )} +

{ const [imgUrl, setImgUrl] = useState(''); @@ -32,14 +33,39 @@ const ItineraryCircleLineLong = props => { } return null; }; + + let firstModeClassName; + let secondModeClassName; + let positionRelativeToTransit; + if ( + props.boardingLeg.to.stop !== null && + props.boardingLeg.from.stop !== null + ) { + positionRelativeToTransit = 'between-transit'; + firstModeClassName = props.boardingLeg.mode.toLowerCase(); + secondModeClassName = props.modeClassName.toLowerCase(); + } else if (props.boardingLeg.to.stop !== null) { + positionRelativeToTransit = 'before-transit'; + firstModeClassName = props.modeClassName.toLowerCase(); + secondModeClassName = props.boardingLeg.mode.toLowerCase(); + } else { + // props.boardingLeg.from.stop !== undefined + positionRelativeToTransit = 'after-transit'; + firstModeClassName = props.boardingLeg.mode.toLowerCase(); + secondModeClassName = props.modeClassName.toLowerCase(); + } + const topMarker = getMarker(true); const bottomMarker = getMarker(false); const legBeforeLineStyle = { color: props.color }; + const carBoardingRouteNumber = ( + + ); // eslint-disable-next-line global-require legBeforeLineStyle.backgroundImage = imgUrl; return (