mirror of
https://github.com/HSLdevcom/digitransit-ui
synced 2025-07-06 01:00:37 +02:00
346 lines
10 KiB
JavaScript
346 lines
10 KiB
JavaScript
import PropTypes from 'prop-types';
|
|
import React from 'react';
|
|
import Link from 'found/Link';
|
|
import { FormattedMessage, intlShape } from 'react-intl';
|
|
import cx from 'classnames';
|
|
import { alertShape, configShape, vehicleShape } from '../../util/shapes';
|
|
import AddressRow from '../AddressRow';
|
|
import TripLink from './TripLink';
|
|
import FuzzyTripLink from './FuzzyTripLink';
|
|
import ServiceAlertIcon from '../ServiceAlertIcon';
|
|
import { fromStopTime } from './DepartureTime';
|
|
import ZoneIcon from '../ZoneIcon';
|
|
import { getActiveAlertSeverityLevel } from '../../util/alertUtils';
|
|
import { PREFIX_STOPS } from '../../util/path';
|
|
import { addAnalyticsEvent } from '../../util/analyticsUtils';
|
|
import { getZoneLabel } from '../../util/legUtils';
|
|
import { estimateItineraryDistance } from '../../util/geo-utils';
|
|
import getVehicleState from '../../util/vehicleStateUtils';
|
|
import Icon from '../Icon';
|
|
|
|
const RouteStop = (
|
|
{
|
|
className,
|
|
color,
|
|
currentTime,
|
|
first,
|
|
last,
|
|
mode,
|
|
stop,
|
|
nextStop,
|
|
vehicle,
|
|
displayNextDeparture,
|
|
shortName,
|
|
prevStop,
|
|
hideDepartures,
|
|
},
|
|
{ config, intl },
|
|
) => {
|
|
const patternExists =
|
|
stop.stopTimesForPattern && stop.stopTimesForPattern.length > 0;
|
|
|
|
const firstDeparture = patternExists && stop.stopTimesForPattern[0];
|
|
const nextDeparture = patternExists && stop.stopTimesForPattern[1];
|
|
|
|
const getDepartureTime = stoptime => {
|
|
let departureText = '';
|
|
if (stoptime) {
|
|
const departureTime =
|
|
stoptime.serviceDay +
|
|
(stoptime.realtimeState === 'CANCELED' ||
|
|
stoptime.realtimeDeparture === -1
|
|
? stoptime.scheduledDeparture
|
|
: stoptime.realtimeDeparture);
|
|
const timeDiffInMinutes = Math.floor((departureTime - currentTime) / 60);
|
|
if (
|
|
timeDiffInMinutes < 0 ||
|
|
timeDiffInMinutes > config.minutesToDepartureLimit
|
|
) {
|
|
const date = new Date(departureTime * 1000);
|
|
departureText = `${
|
|
(date.getHours() < 10 ? '0' : '') + date.getHours()
|
|
}:${date.getMinutes()}`;
|
|
} else if (timeDiffInMinutes === 0) {
|
|
departureText = intl.formatMessage({
|
|
id: 'arriving-soon',
|
|
defaultMessage: 'Now',
|
|
});
|
|
} else {
|
|
departureText = intl.formatMessage(
|
|
{ id: 'departure-time-in-minutes', defaultMessage: '{minutes} min' },
|
|
{ minutes: timeDiffInMinutes },
|
|
);
|
|
}
|
|
}
|
|
return departureText;
|
|
};
|
|
|
|
const getText = () => {
|
|
let text = intl.formatMessage({ id: 'stop' });
|
|
text += ` ${stop.name},`;
|
|
text += `${stop.code},`;
|
|
text += `${stop.desc},`;
|
|
|
|
if (getActiveAlertSeverityLevel(stop.alerts, currentTime)) {
|
|
text += `${intl.formatMessage({
|
|
id: 'disruptions-tab.sr-disruptions',
|
|
})},`;
|
|
}
|
|
|
|
if (patternExists) {
|
|
text += `${intl.formatMessage({ id: 'leaves' })},`;
|
|
text += `${getDepartureTime(stop.stopTimesForPattern[0])},`;
|
|
if (firstDeparture.realtime) {
|
|
text += `${intl.formatMessage({ id: 'realtime' })},`;
|
|
}
|
|
if (stop.stopTimesForPattern[0].stop.platformCode) {
|
|
text += `${intl.formatMessage({ id: 'platform' })},`;
|
|
text += `${stop.stopTimesForPattern[0].stop.platformCode},`;
|
|
}
|
|
if (displayNextDeparture) {
|
|
text += `${intl.formatMessage({ id: 'next' })},`;
|
|
text += `${getDepartureTime(
|
|
stop.stopTimesForPattern[1],
|
|
currentTime,
|
|
)},`;
|
|
if (nextDeparture?.realtime) {
|
|
text += `${intl.formatMessage({ id: 'realtime' })},`;
|
|
}
|
|
if (
|
|
stop.stopTimesForPattern[1] &&
|
|
stop.stopTimesForPattern[1].stop.platformCode
|
|
) {
|
|
text += `${intl.formatMessage({ id: 'platform' })},`;
|
|
text += `${stop.stopTimesForPattern[1].stop.platformCode}`;
|
|
}
|
|
}
|
|
}
|
|
return text;
|
|
};
|
|
|
|
const getVehicleTripLink = () => {
|
|
let vehicleTripLink;
|
|
let vehicleState;
|
|
if (vehicle) {
|
|
const maxDistance = vehicle.mode === 'rail' ? 100 : 50;
|
|
const { realtimeDeparture, realtimeArrival, serviceDay } = firstDeparture;
|
|
const arrivalTimeToStop = (serviceDay + realtimeArrival) * 1000;
|
|
const departureTimeFromStop = (serviceDay + realtimeDeparture) * 1000;
|
|
const vehicleTime = vehicle.timestamp * 1000;
|
|
const distanceToStop = estimateItineraryDistance(stop, {
|
|
lat: vehicle.lat,
|
|
lon: vehicle.long,
|
|
});
|
|
vehicleState = getVehicleState(
|
|
distanceToStop,
|
|
maxDistance,
|
|
vehicleTime,
|
|
arrivalTimeToStop,
|
|
departureTimeFromStop,
|
|
first,
|
|
last,
|
|
);
|
|
const vehicleWithParsedShortname = {
|
|
...vehicle,
|
|
shortName:
|
|
vehicle.shortName &&
|
|
config.realTime[vehicle.route?.split(':')[0]].vehicleNumberParser(
|
|
vehicle.shortName,
|
|
),
|
|
};
|
|
vehicleTripLink = vehicle.tripId ? (
|
|
<TripLink
|
|
key={vehicle.id}
|
|
vehicle={vehicleWithParsedShortname}
|
|
shortName={shortName}
|
|
mode={mode}
|
|
/>
|
|
) : (
|
|
<FuzzyTripLink
|
|
stopName={vehicleState === 'arriving' ? prevStop?.name : stop?.name}
|
|
nextStopName={
|
|
vehicleState === 'arriving' ? stop?.name : nextStop?.name
|
|
}
|
|
mode={mode}
|
|
key={vehicle.id}
|
|
vehicle={vehicleWithParsedShortname}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<div className={cx('route-stop-now', vehicleState)}>
|
|
{vehicleTripLink}
|
|
</div>
|
|
);
|
|
};
|
|
return (
|
|
<li className={cx('route-stop location-details_container ', className)}>
|
|
{getVehicleTripLink()}
|
|
<div className={cx('route-stop-now_circleline', mode)} aria-hidden="true">
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<circle
|
|
cx="8"
|
|
cy="8"
|
|
r="6"
|
|
fill="white"
|
|
stroke={color || 'currentColor'}
|
|
strokeWidth="4"
|
|
/>
|
|
</svg>
|
|
<div
|
|
className={cx('route-stop-now_line', mode)}
|
|
style={{ backgroundColor: color }}
|
|
/>
|
|
</div>
|
|
<div className="route-stop-row_content-container">
|
|
<Link
|
|
as="button"
|
|
type="button"
|
|
to={`/${PREFIX_STOPS}/${encodeURIComponent(stop.gtfsId)}`}
|
|
onClick={() => {
|
|
addAnalyticsEvent({
|
|
category: 'Routes',
|
|
action: 'OpenStopViewFromRoute',
|
|
name: null,
|
|
});
|
|
}}
|
|
aria-label={getText()}
|
|
>
|
|
<div className="route-stop-container">
|
|
<div className="route-details-upper-row">
|
|
<div className={` route-details_container ${mode}`}>
|
|
<div className="route-stop-name">
|
|
<span>{stop.name}</span>
|
|
<ServiceAlertIcon
|
|
className="inline-icon"
|
|
severityLevel={getActiveAlertSeverityLevel(
|
|
stop.alerts,
|
|
currentTime,
|
|
)}
|
|
/>
|
|
</div>
|
|
<div className="platform-number-container">
|
|
<div
|
|
key={`${stop.scheduledDeparture}-platform-number`}
|
|
className={`platform-code ${
|
|
!stop.platformCode ? 'empty' : ''
|
|
}`}
|
|
>
|
|
{stop.platformCode}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{patternExists && (
|
|
<div
|
|
key={firstDeparture.scheduledDeparture}
|
|
className="route-stop-time"
|
|
>
|
|
{!hideDepartures && fromStopTime(firstDeparture, currentTime)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="route-details-bottom-row">
|
|
<AddressRow desc={stop.desc} code={stop.code} />
|
|
{config.zones.stops && stop.zoneId ? (
|
|
<ZoneIcon
|
|
className="itinerary-zone-icon"
|
|
zoneId={getZoneLabel(stop.zoneId, config)}
|
|
showUnknown={false}
|
|
/>
|
|
) : (
|
|
<div className="itinerary-zone-icon" />
|
|
)}
|
|
{nextDeparture && displayNextDeparture && (
|
|
<div
|
|
key={nextDeparture.scheduledDeparture}
|
|
className="route-stop-time"
|
|
>
|
|
{!hideDepartures &&
|
|
fromStopTime(nextDeparture, currentTime, true, true)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{patternExists &&
|
|
stop.stopTimesForPattern[0].pickupType === 'NONE' &&
|
|
!last && (
|
|
<div className="drop-off-container">
|
|
<Icon img="icon-icon_info" color={config.colors.primary} />
|
|
<FormattedMessage
|
|
id="route-destination-arrives"
|
|
defaultMessage="Drop-off only"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
</li>
|
|
);
|
|
};
|
|
|
|
RouteStop.propTypes = {
|
|
color: PropTypes.string,
|
|
vehicle: vehicleShape,
|
|
stop: PropTypes.shape({
|
|
code: PropTypes.string,
|
|
name: PropTypes.string,
|
|
desc: PropTypes.string,
|
|
gtfsId: PropTypes.string,
|
|
zoneId: PropTypes.string,
|
|
scheduledDeparture: PropTypes.number,
|
|
platformCode: PropTypes.string,
|
|
alerts: PropTypes.arrayOf(alertShape),
|
|
stopTimesForPattern: PropTypes.arrayOf(
|
|
PropTypes.shape({
|
|
realtimeDeparture: PropTypes.number,
|
|
realtimeArrival: PropTypes.number,
|
|
serviceDay: PropTypes.number,
|
|
pickupType: PropTypes.string,
|
|
stop: PropTypes.shape({
|
|
platformCode: PropTypes.string,
|
|
}),
|
|
}),
|
|
),
|
|
}).isRequired,
|
|
nextStop: PropTypes.shape({
|
|
name: PropTypes.string,
|
|
}),
|
|
prevStop: PropTypes.shape({
|
|
name: PropTypes.string,
|
|
}),
|
|
mode: PropTypes.string,
|
|
className: PropTypes.string,
|
|
currentTime: PropTypes.number.isRequired,
|
|
first: PropTypes.bool,
|
|
last: PropTypes.bool,
|
|
displayNextDeparture: PropTypes.bool,
|
|
shortName: PropTypes.string,
|
|
hideDepartures: PropTypes.bool,
|
|
};
|
|
|
|
RouteStop.defaultProps = {
|
|
className: undefined,
|
|
color: null,
|
|
displayNextDeparture: true,
|
|
first: false,
|
|
last: false,
|
|
mode: undefined,
|
|
nextStop: null,
|
|
prevStop: null,
|
|
shortName: undefined,
|
|
vehicle: undefined,
|
|
hideDepartures: false,
|
|
};
|
|
|
|
RouteStop.contextTypes = {
|
|
intl: intlShape.isRequired,
|
|
config: configShape.isRequired,
|
|
};
|
|
|
|
export default RouteStop;
|