mirror of
https://github.com/HSLdevcom/digitransit-ui
synced 2026-02-22 16:35:47 +01:00
Part of itinerary detail view had focus on click handler. Handler never got triggered. Only the map icons do real focus actions.
340 lines
10 KiB
JavaScript
340 lines
10 KiB
JavaScript
/* eslint-disable react/no-array-index-key */
|
|
import PropTypes from 'prop-types';
|
|
import React from 'react';
|
|
import { matchShape } from 'found';
|
|
import {
|
|
configShape,
|
|
fareShape,
|
|
itineraryShape,
|
|
relayShape,
|
|
} from '../../util/shapes';
|
|
import TransitLeg from './TransitLeg';
|
|
import WalkLeg from './WalkLeg';
|
|
import WaitLeg from './WaitLeg';
|
|
import BicycleLeg from './BicycleLeg';
|
|
import EndLeg from './EndLeg';
|
|
import AirportCheckInLeg from './AirportCheckInLeg';
|
|
import AirportCollectLuggageLeg from './AirportCollectLuggageLeg';
|
|
import StopCode from '../StopCode';
|
|
import AirplaneLeg from './AirplaneLeg';
|
|
import CarLeg from './CarLeg';
|
|
import CarParkLeg from './CarParkLeg';
|
|
import ViaLeg from './ViaLeg';
|
|
import CallAgencyLeg from './CallAgencyLeg';
|
|
import TaxiLeg from './TaxiLeg';
|
|
import {
|
|
compressLegs,
|
|
isCallAgencyLeg,
|
|
isLegOnFoot,
|
|
legTime,
|
|
markViaPoints,
|
|
getBoardingLeg,
|
|
} from '../../util/legUtils';
|
|
import { getRouteMode } from '../../util/modeUtils';
|
|
import { addAnalyticsEvent } from '../../util/analyticsUtils';
|
|
import Profile from './Profile';
|
|
import BikeParkLeg from './BikeParkLeg';
|
|
import { getIntermediatePlaces } from '../../util/otpStrings';
|
|
|
|
const stopCode = stop => stop && stop.code && <StopCode code={stop.code} />;
|
|
|
|
export default class Legs extends React.Component {
|
|
static propTypes = {
|
|
itinerary: itineraryShape.isRequired,
|
|
fares: PropTypes.arrayOf(fareShape),
|
|
focusToPoint: PropTypes.func.isRequired,
|
|
focusToLeg: PropTypes.func.isRequired,
|
|
changeHash: PropTypes.func,
|
|
tabIndex: PropTypes.number,
|
|
openSettings: PropTypes.func.isRequired,
|
|
showBikeBoardingInformation: PropTypes.bool,
|
|
showCarBoardingInformation: PropTypes.bool,
|
|
usingOwnCarWholeTrip: PropTypes.bool,
|
|
relayEnvironment: relayShape,
|
|
};
|
|
|
|
static contextTypes = {
|
|
config: configShape,
|
|
match: matchShape,
|
|
};
|
|
|
|
static defaultProps = {
|
|
fares: [],
|
|
changeHash: undefined,
|
|
tabIndex: undefined,
|
|
showBikeBoardingInformation: false,
|
|
showCarBoardingInformation: false,
|
|
usingOwnCarWholeTrip: false,
|
|
relayEnvironment: undefined,
|
|
};
|
|
|
|
focus = position => e => {
|
|
e.stopPropagation();
|
|
this.props.focusToPoint(position.lat, position.lon);
|
|
addAnalyticsEvent({
|
|
category: 'Itinerary',
|
|
action: 'ZoomMapToStopLocation',
|
|
name: null,
|
|
});
|
|
};
|
|
|
|
focusToLeg = leg => e => {
|
|
e.stopPropagation();
|
|
this.props.focusToLeg(leg);
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
itinerary,
|
|
fares,
|
|
showBikeBoardingInformation,
|
|
showCarBoardingInformation,
|
|
relayEnvironment,
|
|
usingOwnCarWholeTrip,
|
|
} = this.props;
|
|
const { waitThreshold } = this.context.config.itinerary;
|
|
|
|
const { location } = this.context.match;
|
|
const intermediatePlaces = getIntermediatePlaces(location.query);
|
|
const itineraryLegs = markViaPoints(itinerary.legs, intermediatePlaces);
|
|
const compressedLegs = compressLegs(itineraryLegs, true).map(leg => ({
|
|
showBikeBoardingInformation,
|
|
showCarBoardingInformation,
|
|
...leg,
|
|
fare:
|
|
(leg.route &&
|
|
fares?.find(fare => fare.routeGtfsId === leg.route.gtfsId)) ||
|
|
undefined,
|
|
}));
|
|
const numberOfLegs = compressedLegs.length;
|
|
if (numberOfLegs === 0) {
|
|
return null;
|
|
}
|
|
const bikeParked = compressedLegs.some(
|
|
leg => leg.to.vehicleParking && leg.mode === 'BICYCLE',
|
|
);
|
|
let previousLeg;
|
|
let nextLeg;
|
|
const legs = [];
|
|
compressedLegs.forEach((leg, j) => {
|
|
nextLeg = j + 1 < numberOfLegs ? compressedLegs[j + 1] : undefined;
|
|
if (j > 0) {
|
|
previousLeg = compressedLegs[j - 1];
|
|
}
|
|
const startTime = previousLeg?.end || leg.start;
|
|
let index = j;
|
|
const interliningLegs = [];
|
|
// there can be an arbitrary amount of interlining legs, search for the last one
|
|
while (
|
|
compressedLegs[index + 1] &&
|
|
compressedLegs[index + 1].interlineWithPreviousLeg
|
|
) {
|
|
interliningLegs.push(compressedLegs[index + 1]);
|
|
index += 1;
|
|
}
|
|
const isNextLegInterlining = nextLeg?.interlineWithPreviousLeg;
|
|
const bikePark =
|
|
previousLeg?.mode === 'BICYCLE' && previousLeg.to.vehicleParking;
|
|
const carPark =
|
|
previousLeg?.mode === 'CAR' && previousLeg.to.vehicleParking;
|
|
const isSameStopTransfer =
|
|
leg.transitLeg &&
|
|
nextLeg?.transitLeg &&
|
|
leg.to.stop.gtfsId === nextLeg.from.stop.gtfsId;
|
|
const legProps = {
|
|
leg,
|
|
index: j,
|
|
focusAction: this.focus(leg.from),
|
|
focusToLeg: this.focusToLeg(leg),
|
|
};
|
|
const transitLegProps = {
|
|
leg,
|
|
index: j,
|
|
interliningLegs,
|
|
focusAction: this.focus(leg.from),
|
|
changeHash: this.props.changeHash,
|
|
tabIndex: this.props.tabIndex,
|
|
usingOwnCarWholeTrip,
|
|
};
|
|
|
|
let waitLeg;
|
|
if (nextLeg) {
|
|
const waitThresholdInMs = waitThreshold * 1000;
|
|
const waitTime = legTime(nextLeg.start) - legTime(leg.end);
|
|
if (
|
|
(waitTime > waitThresholdInMs || isSameStopTransfer) &&
|
|
nextLeg.mode !== 'AIRPLANE' &&
|
|
leg.mode !== 'AIRPLANE' &&
|
|
!nextLeg.intermediatePlace &&
|
|
!isNextLegInterlining &&
|
|
leg.to.stop
|
|
) {
|
|
const waitLegProps = { ...leg };
|
|
if (nextLeg.isViaPoint) {
|
|
waitLegProps.isViaPoint = true;
|
|
nextLeg.isViaPoint = false;
|
|
}
|
|
waitLeg = (
|
|
<WaitLeg
|
|
index={j}
|
|
leg={waitLegProps}
|
|
start={leg.end}
|
|
waitTime={waitTime}
|
|
focusAction={this.focus(leg.to)}
|
|
icon={usingOwnCarWholeTrip ? 'icon_wait-car' : undefined}
|
|
>
|
|
{stopCode(leg.to.stop)}
|
|
</WaitLeg>
|
|
);
|
|
}
|
|
}
|
|
if (leg.mode !== 'WALK' && isCallAgencyLeg(leg)) {
|
|
legs.push(<CallAgencyLeg {...transitLegProps} />);
|
|
} else if (leg.intermediatePlace) {
|
|
legs.push(<ViaLeg {...legProps} arrival={startTime} />);
|
|
} else if (bikePark) {
|
|
legs.push(<BikeParkLeg {...legProps} bikePark={bikePark} />);
|
|
} else if (carPark) {
|
|
legs.push(<CarParkLeg {...legProps} carPark={carPark} />);
|
|
} else if (leg.mode === 'TAXI') {
|
|
legs.push(<TaxiLeg {...legProps} />);
|
|
} else if (isLegOnFoot(leg)) {
|
|
legs.push(
|
|
<WalkLeg {...legProps} previousLeg={previousLeg} nextLeg={nextLeg}>
|
|
{stopCode(leg.from.stop)}
|
|
</WalkLeg>,
|
|
);
|
|
} else if (
|
|
(leg.mode === 'BUS' ||
|
|
leg.mode === 'TRAM' ||
|
|
leg.mode === 'RAIL' ||
|
|
leg.mode === 'SUBWAY' ||
|
|
leg.mode === 'FERRY' ||
|
|
leg.mode === 'FUNICULAR') &&
|
|
!leg.interlineWithPreviousLeg
|
|
) {
|
|
const mode = getRouteMode(
|
|
{
|
|
mode: leg.mode,
|
|
type: leg.route?.type,
|
|
gtfsId: leg.route?.gtfsId,
|
|
},
|
|
this.context.config,
|
|
);
|
|
legs.push(<TransitLeg mode={mode} {...transitLegProps} />);
|
|
} else if (leg.mode === 'AIRPLANE') {
|
|
legs.push(
|
|
<AirportCheckInLeg
|
|
index={j - 0.5}
|
|
leg={leg}
|
|
start={startTime}
|
|
focusAction={this.focus(leg.from)}
|
|
/>,
|
|
);
|
|
legs.push(<AirplaneLeg {...transitLegProps} />);
|
|
legs.push(
|
|
<AirportCollectLuggageLeg
|
|
index={j + 0.5}
|
|
leg={leg}
|
|
focusAction={this.focus(leg.to)}
|
|
/>,
|
|
);
|
|
} else if (
|
|
leg.rentedBike ||
|
|
leg.mode === 'BICYCLE' ||
|
|
leg.mode === 'SCOOTER'
|
|
) {
|
|
let bicycleWalkLeg;
|
|
if (nextLeg?.mode === 'BICYCLE_WALK' && !bikeParked) {
|
|
bicycleWalkLeg = nextLeg;
|
|
}
|
|
if (previousLeg?.mode === 'BICYCLE_WALK' && !bikeParked) {
|
|
bicycleWalkLeg = previousLeg;
|
|
}
|
|
// if there is a transit leg after or before a bicycle leg, render a bicycle_walk leg without distance information
|
|
// currently bike walk leg is not rendered if there is waiting at stop, because
|
|
// 'walk bike to train and wait x minutes' is too confusing instruction
|
|
// This cannot be fixed as long as bicycle leg renders also bike walking
|
|
const boardingLeg = getBoardingLeg(
|
|
nextLeg,
|
|
previousLeg,
|
|
waitLeg,
|
|
leg,
|
|
'BICYCLE_WALK',
|
|
);
|
|
if (!bikeParked && boardingLeg !== undefined) {
|
|
bicycleWalkLeg = boardingLeg;
|
|
}
|
|
legs.push(
|
|
<BicycleLeg
|
|
{...legProps}
|
|
bicycleWalkLeg={bicycleWalkLeg}
|
|
openSettings={this.props.openSettings}
|
|
nextLegMode={nextLeg?.mode}
|
|
relayEnvironment={relayEnvironment}
|
|
/>,
|
|
);
|
|
} else if (leg.mode === 'CAR') {
|
|
// If there is a transit leg after or before a car leg, render a car boarding leg without distance information.
|
|
const carBoardingLeg = getBoardingLeg(
|
|
nextLeg,
|
|
previousLeg,
|
|
waitLeg,
|
|
leg,
|
|
'CAR_BOARDING',
|
|
);
|
|
legs.push(
|
|
<CarLeg {...legProps} carBoardingLeg={carBoardingLeg}>
|
|
{stopCode(leg.from.stop)}
|
|
</CarLeg>,
|
|
);
|
|
}
|
|
|
|
if (waitLeg) {
|
|
legs.push(waitLeg);
|
|
}
|
|
});
|
|
|
|
// This solves edge case when itinerary ends at the stop without walking.
|
|
// There should be WalkLeg rendered before EndLeg.
|
|
const lastLeg = compressedLegs[numberOfLegs - 1];
|
|
if (lastLeg.transitLeg && lastLeg.to.stop) {
|
|
legs.push(
|
|
<WalkLeg
|
|
index={numberOfLegs}
|
|
leg={{ ...lastLeg, isViaPoint: false }}
|
|
previousLeg={lastLeg}
|
|
nextLeg={compressedLegs[numberOfLegs]}
|
|
focusAction={this.focus(lastLeg.to)}
|
|
focusToLeg={this.focusToLeg(lastLeg)}
|
|
>
|
|
{stopCode(lastLeg.to.stop)}
|
|
</WalkLeg>,
|
|
);
|
|
}
|
|
|
|
legs.push(
|
|
<EndLeg
|
|
index={numberOfLegs}
|
|
endTime={itinerary.end}
|
|
focusAction={this.focus(lastLeg.to)}
|
|
to={lastLeg.to}
|
|
/>,
|
|
);
|
|
|
|
legs.push(<Profile itinerary={itinerary} />);
|
|
|
|
return (
|
|
<span className="itinerary-list-container" role="list">
|
|
{legs.map((item, idx) => {
|
|
const listKey = `leg_${idx}`;
|
|
return (
|
|
<span role="listitem" key={listKey}>
|
|
{item}
|
|
</span>
|
|
);
|
|
})}
|
|
</span>
|
|
);
|
|
}
|
|
}
|