diff --git a/app/component/itinerary/ItineraryDetails.js b/app/component/itinerary/ItineraryDetails.js index 8e92712666..bce0faca56 100644 --- a/app/component/itinerary/ItineraryDetails.js +++ b/app/component/itinerary/ItineraryDetails.js @@ -615,6 +615,7 @@ const withRelay = createFragmentContainer( effectiveStartDate alertHeaderText alertDescriptionText + id entities { __typename ... on Route { diff --git a/app/component/itinerary/ItineraryPage.js b/app/component/itinerary/ItineraryPage.js index d37c787ec6..565983a523 100644 --- a/app/component/itinerary/ItineraryPage.js +++ b/app/component/itinerary/ItineraryPage.js @@ -1068,6 +1068,7 @@ export default function ItineraryPage(props, context) { showDurationBubble={planEdges?.[0]?.node.legs?.length === 1} objectsToHide={objectsToHide} itinerary={explicitItinerary} + showBackButton={!naviMode} /> ); } diff --git a/app/component/itinerary/PlanConnection.js b/app/component/itinerary/PlanConnection.js index febd5c852f..b6f69f3aa4 100644 --- a/app/component/itinerary/PlanConnection.js +++ b/app/component/itinerary/PlanConnection.js @@ -74,6 +74,14 @@ const planConnection = graphql` interlineWithPreviousLeg headsign realtimeState + alerts { + alertSeverityLevel + effectiveStartDate + effectiveEndDate + alertDescriptionText + alertHeaderText + id + } intermediatePlaces { arrival { scheduledTime diff --git a/app/component/itinerary/navigator/NaviCardContainer.js b/app/component/itinerary/navigator/NaviCardContainer.js index 2ffc89828c..517525bf5e 100644 --- a/app/component/itinerary/navigator/NaviCardContainer.js +++ b/app/component/itinerary/navigator/NaviCardContainer.js @@ -2,6 +2,7 @@ import React, { useEffect, useState, useRef } from 'react'; import PropTypes from 'prop-types'; import { intlShape } from 'react-intl'; import distance from '@digitransit-search-util/digitransit-search-util-distance'; +import { matchShape, routerShape } from 'found'; import { legShape, configShape } from '../../../util/shapes'; import { legTime, legTimeStr } from '../../../util/legUtils'; import NaviCard from './NaviCard'; @@ -10,23 +11,25 @@ import { getItineraryAlerts, getTransitLegState, getAdditionalMessages, + getFirstLastLegs, LEGTYPE, } from './NaviUtils'; const DESTINATION_RADIUS = 20; // meters const TIME_AT_DESTINATION = 3; // * 10 seconds -function getFirstLastLegs(legs) { - const first = legs[0]; - const last = legs[legs.length - 1]; - return { first, last }; -} function getNextLeg(legs, time) { return legs.find(leg => legTime(leg.start) > time); } + +function addMessages(incominMessages, newMessages) { + newMessages.forEach(m => { + incominMessages.set(m.id, m); + }); +} function NaviCardContainer( { focusToLeg, time, realTimeLegs, position }, - { intl, config }, + { intl, config, match, router }, ) { const [currentLeg, setCurrentLeg] = useState(null); const [cardExpanded, setCardExpanded] = useState(false); @@ -47,10 +50,10 @@ function NaviCardContainer( const handleClick = () => { setCardExpanded(!cardExpanded); }; - useEffect(() => { if (cardRef.current) { const contentHeight = cardRef.current.clientHeight; + // Navistack top position depending on main card height. setTopPosition(contentHeight + 86); } @@ -63,12 +66,11 @@ function NaviCardContainer( const incomingMessages = new Map(); - // TODO proper alert handling. // Alerts for NaviStack - const alerts = getItineraryAlerts(realTimeLegs, intl, messages); - alerts.forEach(alert => { - incomingMessages.set(alert.id, alert); - }); + addMessages( + incomingMessages, + getItineraryAlerts(realTimeLegs, intl, messages, match.params, router), + ); const legChanged = newLeg?.legId ? newLeg.legId !== currentLeg?.legId @@ -80,22 +82,10 @@ function NaviCardContainer( if (nextLeg?.transitLeg) { // Messages for NaviStack. - const transitLegState = getTransitLegState(nextLeg, intl, messages); - if (transitLegState) { - incomingMessages.set(transitLegState.id, transitLegState); - } - const additionalMsgs = getAdditionalMessages( - nextLeg, - time, - intl, - config, - messages, - ); - if (additionalMsgs) { - additionalMsgs.forEach(m => { - incomingMessages.set(m.id, m); - }); - } + addMessages(incomingMessages, [ + ...getTransitLegState(nextLeg, intl, messages, time), + ...getAdditionalMessages(nextLeg, time, intl, config, messages), + ]); } if (newLeg && legChanged) { focusToLeg?.(newLeg); @@ -185,8 +175,9 @@ function NaviCardContainer( type="button" className={`navitop ${cardExpanded ? 'expanded' : ''}`} onClick={handleClick} + ref={cardRef} > -
+
{ } if (legType === LEGTYPE.TRANSIT) { - const { intermediatePlaces, headsign, trip } = leg; + const { intermediatePlaces, headsign, trip, realtimeState } = leg; const hs = headsign || trip.tripHeadsign; const now = Date.now(); const idx = intermediatePlaces.findIndex(p => legTime(p.arrival) > now); - const count = idx > -1 ? intermediatePlaces.length - idx : 0; - const stopCount = {count} ; + const stopCount = ( + + {count} + + ); const translationId = count === 1 ? 'navileg-one-stop-remaining' : 'navileg-stops-remaining'; const mode = leg.mode.toLowerCase(); diff --git a/app/component/itinerary/navigator/NaviInstructions.js b/app/component/itinerary/navigator/NaviInstructions.js index 2647cd8001..e8ec5bc66a 100644 --- a/app/component/itinerary/navigator/NaviInstructions.js +++ b/app/component/itinerary/navigator/NaviInstructions.js @@ -5,7 +5,7 @@ import cx from 'classnames'; import { legShape, configShape } from '../../../util/shapes'; import { legDestination, legTimeStr, legTime } from '../../../util/legUtils'; import RouteNumber from '../../RouteNumber'; -import { LEGTYPE } from './NaviUtils'; +import { LEGTYPE, getLocalizedMode } from './NaviUtils'; import { displayDistance } from '../../../util/geo-utils'; import { durationToString } from '../../../util/timeUtils'; @@ -14,7 +14,9 @@ export default function NaviInstructions( { intl, config }, ) { const [fadeOut, setFadeOut] = useState(false); - + const withRealTime = (rt, children) => ( + {children} + ); useEffect(() => { const timer = setTimeout(() => { setFadeOut(true); @@ -44,21 +46,16 @@ export default function NaviInstructions( ); } if (legType === LEGTYPE.WAIT && nextLeg.mode !== 'WALK') { - const { mode, headsign, route, end, start } = nextLeg; + const { mode, headsign, route, start } = nextLeg; const hs = headsign || nextLeg.trip?.tripHeadsign; const color = route.color || 'currentColor'; - const localizedMode = intl.formatMessage({ - id: `to-${mode.toLowerCase()}`, - defaultMessage: `${mode}`, - }); - const t = leg ? legTime(end) : legTime(start); - const remainingDuration = Math.ceil((t - Date.now()) / 60000); // ms to minutes + const localizedMode = getLocalizedMode(mode, intl); + + const remainingDuration = Math.ceil((legTime(start) - Date.now()) / 60000); // ms to minutes const rt = nextLeg.realtimeState === 'UPDATED'; const values = { - duration: ( - {remainingDuration} - ), - legTime: {legTimeStr(end)}, + duration: withRealTime(rt, remainingDuration), + legTime: withRealTime(rt, legTimeStr(start)), }; return ( <> @@ -83,7 +80,7 @@ export default function NaviInstructions(
@@ -93,25 +90,20 @@ export default function NaviInstructions( } if (legType === LEGTYPE.TRANSIT) { + const rt = leg.realtimeState === 'UPDATED'; + const t = legTime(leg.end); const stopOrStation = leg.to.stop.parentStation ? intl.formatMessage({ id: 'navileg-from-station' }) : intl.formatMessage({ id: 'navileg-from-stop' }); - const rt = leg.realtimeState === 'UPDATED'; - const localizedMode = intl.formatMessage({ - id: `${leg.mode.toLowerCase()}`, - defaultMessage: `${leg.mode}`, - }); + const localizedMode = getLocalizedMode(leg.mode, intl); + const remainingDuration = Math.ceil((t - Date.now()) / 60000); // ms to minutes const values = { stopOrStation, stop: leg.to.stop.name, - duration: ( - {remainingDuration} - ), - legTime: ( - {legTimeStr(leg.end)} - ), + duration: withRealTime(rt, remainingDuration), + legTime: withRealTime(rt, legTimeStr(leg.end)), }; return ( diff --git a/app/component/itinerary/navigator/NaviMessage.js b/app/component/itinerary/navigator/NaviMessage.js index 5d5c4e73b6..81c4d93277 100644 --- a/app/component/itinerary/navigator/NaviMessage.js +++ b/app/component/itinerary/navigator/NaviMessage.js @@ -20,7 +20,7 @@ function NaviMessage({ severity, children, index, handleRemove }, { config }) { let color; switch (severity) { case 'INFO': - iconId = 'icon-icon_info'; + iconId = 'notification-bell'; color = '#0074BF'; break; case 'WARNING': @@ -32,7 +32,7 @@ function NaviMessage({ severity, children, index, handleRemove }, { config }) { color = '#DC0451'; break; default: - iconId = 'icon-icon_info'; + iconId = 'notification-bell'; color = '#0074BF'; } return ( @@ -43,13 +43,7 @@ function NaviMessage({ severity, children, index, handleRemove }, { config }) { `${severity.toLowerCase()}`, )} > - + {children} diff --git a/app/component/itinerary/navigator/NaviUtils.js b/app/component/itinerary/navigator/NaviUtils.js index 0cdfa580f6..3e1b59fa66 100644 --- a/app/component/itinerary/navigator/NaviUtils.js +++ b/app/component/itinerary/navigator/NaviUtils.js @@ -4,9 +4,10 @@ import { legTime } from '../../../util/legUtils'; import { timeStr } from '../../../util/timeUtils'; import { getFaresFromLegs } from '../../../util/fareUtils'; import { ExtendedRouteTypes } from '../../../constants'; +import { getItineraryPagePath } from '../../../util/path'; const TRANSFER_SLACK = 60000; - +const DISPLAY_MESSAGE_THRESHOLD = 120 * 1000; // 2 minutes function findTransferProblem(legs) { for (let i = 1; i < legs.length - 1; i++) { const prev = legs[i - 1]; @@ -33,12 +34,21 @@ function findTransferProblem(legs) { } return null; } - +export const getLocalizedMode = (mode, intl) => { + return intl.formatMessage({ + id: `${mode.toLowerCase()}`, + defaultMessage: `${mode}`, + }); +}; +export function getFirstLastLegs(legs) { + const first = legs[0]; + const last = legs[legs.length - 1]; + return { first, last }; +} export const getAdditionalMessages = (leg, time, intl, config, messages) => { const msgs = []; - const ticketDisplay = 120 * 1000; // 2 minutes const ticketMsg = messages.get('ticket'); - if (!ticketMsg && legTime(leg.start) - time < ticketDisplay) { + if (!ticketMsg && legTime(leg.start) - time < DISPLAY_MESSAGE_THRESHOLD) { // Todo: multiple fares? const fare = getFaresFromLegs([leg], config)[0]; msgs.push({ @@ -57,8 +67,8 @@ export const getAdditionalMessages = (leg, time, intl, config, messages) => { return msgs; }; -export const getTransitLegState = (leg, intl, messages) => { - const { start, realtimeState, from, mode, legId } = leg; +export const getTransitLegState = (leg, intl, messages, time) => { + const { start, realtimeState, from, mode, legId, route } = leg; const { scheduledTime, estimated } = start; if (mode === 'WALK') { return null; @@ -66,20 +76,32 @@ export const getTransitLegState = (leg, intl, messages) => { const previousMessage = messages.get(legId); const prevSeverity = previousMessage ? previousMessage.severity : null; - const late = estimated?.delay > 0; - const localizedMode = intl.formatMessage({ - id: `${mode.toLowerCase()}`, - defaultMessage: `${mode}`, - }); + const late = + estimated?.delay > DISPLAY_MESSAGE_THRESHOLD || + estimated?.delay < -DISPLAY_MESSAGE_THRESHOLD; + const localizedMode = getLocalizedMode(mode, intl); let content; let severity; const isRealTime = realtimeState === 'UPDATED'; - if (late && prevSeverity !== 'ALERT') { - // todo: Do this when design is ready. - severity = 'ALERT'; - content =
Kulkuneuvo on myöhässä
; - } else if (!isRealTime && prevSeverity !== 'WARNING') { + if (late && prevSeverity !== 'WARNING') { + const lMode = getLocalizedMode(mode, intl); + const routeName = `${lMode} ${route.shortName}`; + const { delay } = estimated; + + const id = `navigation-mode-${delay > 0 ? 'late' : 'early'}`; + + content = ( +
+ +
+ ); + severity = 'WARNING'; + } else if ( + !isRealTime && + prevSeverity !== 'WARNING' && + legTime(start) - time < DISPLAY_MESSAGE_THRESHOLD + ) { severity = 'WARNING'; content = (
@@ -117,38 +139,115 @@ export const getTransitLegState = (leg, intl, messages) => { severity = 'INFO'; } const state = severity - ? { severity, content, id: legId, expiresOn: 'legChange' } - : null; + ? [{ severity, content, id: legId, expiresOn: 'legChange' }] + : []; return state; }; -// We'll need the intl later. -// eslint-disable-next-line no-unused-vars -export const getItineraryAlerts = (realTimeLegs, intl, messages) => { - const alerts = []; +export const getItineraryAlerts = ( + realTimeLegs, + intl, + messages, + location, + router, +) => { const canceled = realTimeLegs.filter(leg => leg.realtimeState === 'CANCELED'); - const transferProblem = findTransferProblem(realTimeLegs); - const late = realTimeLegs.filter(leg => leg.start.estimate?.delay > 0); let content; - // TODO: Proper ID handling - if (canceled.length > 0 && !messages.get('canceled')) { - content =
Osa matkan lähdöistä on peruttu
; - // Todo: No current design - // todo find modes that are canceled - alerts.push({ - severity: 'ALERT', - content, - id: 'canceled', + const alerts = realTimeLegs.flatMap(leg => { + return leg.alerts + .filter(alert => { + const { first } = getFirstLastLegs(realTimeLegs); + const startTime = legTime(first.start) / 1000; + if (messages.get(alert.id)) { + return false; + } + // show only alerts that are active when + // the journey starts + if (startTime < alert.effectiveStartDate) { + return false; + } + if ( + alert.alertSeverityLevel === 'WARNING' || + alert.alertSeverityLevel === 'SEVERE' + ) { + return true; + } + return false; + }) + .map(alert => ({ + severity: 'ALERT', + content: ( +
+ {alert.alertHeaderText} +
+ ), + id: alert.id, + })); + }); + const transferProblem = findTransferProblem(realTimeLegs); + const abortTrip = ; + const withShowRoutesBtn = children => ( +
+ {children} + +
+ ); + + if (canceled) { + // show routes button only for first canceled leg. + canceled.forEach((leg, i) => { + const { legId, mode, route } = leg; + + const lMode = getLocalizedMode(mode, intl); + const routeName = `${lMode} ${route.shortName}`; + const m = ( + + ); + // we want to show the show routes button only for the first canceled leg. + if (i === 0) { + content = withShowRoutesBtn( +
+ {m} + {abortTrip} +
, + ); + } else { + content =
{m}
; + } + if (!messages.get(`canceled-${legId}`)) { + alerts.push({ + severity: 'ALERT', + content, + id: `canceled-${legId}`, + }); + } }); } + if (transferProblem !== null) { const transferId = `transfer-${transferProblem[0].legId}-${transferProblem[1].legId}}`; if (!messages.get(transferId)) { - // todo no current design - content = ( -
{`Vaihto ${transferProblem[0].route.shortName} - ${transferProblem[1].route.shortName} ei onnistu reittisuunnitelman mukaisesti`}
+ content = withShowRoutesBtn( +
+ + {abortTrip} +
, ); - alerts.push({ severity: 'ALERT', content, @@ -156,17 +255,6 @@ export const getItineraryAlerts = (realTimeLegs, intl, messages) => { }); } } - if (late.length && !messages.get('late')) { - // Todo: No current design - // Todo add mode and delay time to this message - content =
Kulkuneuvo on myöhässä
; - alerts.push({ - severity: 'ALERT', - content, - id: 'late', - }); - } - return alerts; }; diff --git a/app/component/itinerary/navigator/navigator.scss b/app/component/itinerary/navigator/navigator.scss index fe9c28cde9..9651e1be1e 100644 --- a/app/component/itinerary/navigator/navigator.scss +++ b/app/component/itinerary/navigator/navigator.scss @@ -37,6 +37,10 @@ } } +.bold { + font-weight: $font-weight-medium; +} + .navitop { margin-bottom: 5px; position: fixed; @@ -55,8 +59,6 @@ } .navitop-arrow { - margin-right: var(--space-m); - .inverted { transform: rotate(180deg); } @@ -65,12 +67,6 @@ right: 20px; } - .notifier { - margin: 15px 20px 0 20px; - width: 100%; - color: red; - } - .content { width: 100%; @@ -83,8 +79,8 @@ color: black; .mode { - width: var(--space-l); - height: var(--space-l); + width: 32px; + height: 32px; color: black; margin-right: var(--space-s); margin-top: var(--space-xxs); @@ -371,7 +367,7 @@ margin-top: 0; } - div:not(:first-child) { + .info-stack-item:not(:first-child) { margin-top: 5px; } @@ -389,7 +385,14 @@ border-radius: 15px; display: flex; align-items: center; - height: 100%; + padding: 16px; + + .icon-container { + display: flex; + align-self: flex-start; + height: 16px; + width: 16px; + } &.info { background-color: #e5f2fa; @@ -397,10 +400,53 @@ &.warning { background-color: #fff8e8; + + .navi-alert-content { + width: 100%; + } } &.alert { background-color: #fdf3f6; + + .navi-alert-content { + display: flex; + flex-direction: column; + margin-left: 8px; + + span:first-child { + font-weight: $font-weight-medium; + font-size: $font-size-normal; + } + + span:last-child { + font-size: $font-size-small; + font-weight: $font-weight-book; + } + + .header { + font-size: $font-size-small; + } + } + + .alt-btn { + display: flex; + flex-direction: column; + width: 100%; + + .show-options { + padding: var(--space-s, 16px) var(--space-xs, 8px) + var(--space-s, 16px) var(--space-s, 16px); + background: #0074bf; + color: #fff; + border-radius: 999px; // var(--radius-radius-medium, 8px); + margin-top: var(--space-xxs); + + /* box-shadow-card-s-strong */ + box-shadow: 0 2px 4px 0 + var(--color-shadow-strong, rgba(51, 51, 51, 0.2)); + } + } } &.slide-out-right { @@ -409,14 +455,25 @@ } .info-close { - position: absolute; - top: 7px; - right: 9px; + display: flex; + align-self: flex-start; + margin-left: 1px; + + .icon-container { + height: 24px; + width: 24px; + + .icon { + &.notification-close { + width: 24px; + height: 24px; + } + } + } } .info-icon { - margin-top: 10px; - margin-left: 20px; + margin-right: 8px; } &.slide-in { @@ -441,7 +498,8 @@ .navi-info-content { display: flex; flex-direction: column; - margin-left: 15px; + margin-left: 8px; + width: 100%; span:first-child { font-weight: $font-weight-medium; diff --git a/app/component/itinerary/queries/LegQuery.js b/app/component/itinerary/queries/LegQuery.js index 94dbc7f4d5..be6e8e4ceb 100644 --- a/app/component/itinerary/queries/LegQuery.js +++ b/app/component/itinerary/queries/LegQuery.js @@ -16,7 +16,21 @@ const legQuery = graphql` time } } - + alerts { + alertSeverityLevel + effectiveStartDate + alertDescriptionText + alertHeaderText + id + } + intermediatePlaces { + arrival { + scheduledTime + estimated { + time + } + } + } to { vehicleRentalStation { availableVehicles { diff --git a/app/component/map/ItineraryPageMap.js b/app/component/map/ItineraryPageMap.js index ebe377a125..af77df4404 100644 --- a/app/component/map/ItineraryPageMap.js +++ b/app/component/map/ItineraryPageMap.js @@ -31,6 +31,7 @@ function ItineraryPageMap( topics, showDurationBubble, itinerary, + showBackButton, ...rest }, { match, router, executeAction, config }, @@ -114,7 +115,7 @@ function ItineraryPageMap( zoom={POINT_FOCUS_ZOOM} {...rest} > - {breakpoint !== 'large' && ( + {showBackButton && breakpoint !== 'large' && ( + + + + + + + diff --git a/static/assets/svg-sprite.hsl.svg b/static/assets/svg-sprite.hsl.svg index 44ccd31144..cffbd407fd 100644 --- a/static/assets/svg-sprite.hsl.svg +++ b/static/assets/svg-sprite.hsl.svg @@ -2797,4 +2797,11 @@ + + + + + + +