diff --git a/app/component/IndexPage.js b/app/component/IndexPage.js index c607bebf9a..3c24d40197 100644 --- a/app/component/IndexPage.js +++ b/app/component/IndexPage.js @@ -300,20 +300,30 @@ class IndexPage extends React.Component { const destination = this.pendingDestination || this.props.destination; const sources = ['Favourite', 'History', 'Datasource']; const stopAndRouteSearchTargets = ['Stops', 'Routes']; - const locationSearchTargets = [ + let locationSearchTargets = [ 'Locations', 'CurrentPosition', 'FutureRoutes', - 'Stops', ]; - if (useCitybikes(config.vehicleRental?.networks, config)) { - stopAndRouteSearchTargets.push('VehicleRentalStations'); - locationSearchTargets.push('VehicleRentalStations'); - } - if (config.includeParkAndRideSuggestions) { - stopAndRouteSearchTargets.push('ParkingAreas'); - locationSearchTargets.push('ParkingAreas'); + if (config.locationSearchTargetsFromOTP) { + // configurable setup + locationSearchTargets = [ + ...locationSearchTargets, + ...config.locationSearchTargetsFromOTP, + ]; + } else { + // default setup + locationSearchTargets.push('Stops'); + + if (useCitybikes(config.vehicleRental?.networks, config)) { + stopAndRouteSearchTargets.push('VehicleRentalStations'); + locationSearchTargets.push('VehicleRentalStations'); + } + if (config.includeParkAndRideSuggestions) { + stopAndRouteSearchTargets.push('ParkingAreas'); + locationSearchTargets.push('ParkingAreas'); + } } const locationSearchTargetsMobile = [ ...locationSearchTargets, diff --git a/app/component/RouteNumberContainer.js b/app/component/RouteNumberContainer.js index d1f06f72a6..eac349b498 100644 --- a/app/component/RouteNumberContainer.js +++ b/app/component/RouteNumberContainer.js @@ -13,6 +13,7 @@ const RouteNumberContainer = ( isCallAgency, withBicycle, occupancyStatus, + mode, ...props }, { config }, @@ -23,7 +24,7 @@ const RouteNumberContainer = ( className={className} isCallAgency={isCallAgency || route.type === 715} color={route.color ? `#${route.color}` : null} - mode={route.mode} + mode={mode !== undefined ? mode : route.mode} text={getLegText(route, config, interliningWithRoute)} withBicycle={withBicycle} occupancyStatus={occupancyStatus} @@ -41,6 +42,7 @@ RouteNumberContainer.propTypes = { fadeLong: PropTypes.bool, withBicycle: PropTypes.bool, occupancyStatus: PropTypes.string, + mode: PropTypes.string, }; RouteNumberContainer.defaultProps = { @@ -52,6 +54,7 @@ RouteNumberContainer.defaultProps = { className: '', withBicycle: false, occupancyStatus: undefined, + mode: undefined, }; RouteNumberContainer.contextTypes = { diff --git a/app/component/StopTimetablePage.js b/app/component/StopTimetablePage.js index d76b841353..75fb1983dd 100644 --- a/app/component/StopTimetablePage.js +++ b/app/component/StopTimetablePage.js @@ -28,12 +28,12 @@ class StopTimetablePage extends React.Component { } onDateChange = value => { + this.setState({ date: value }); this.props.relay.refetch( { date: value, }, null, - () => this.setState({ date: value }), ); }; @@ -48,11 +48,8 @@ class StopTimetablePage extends React.Component { ); } diff --git a/app/component/SwipeableTabs.js b/app/component/SwipeableTabs.js index b9ad5f119a..8efb2935f8 100644 --- a/app/component/SwipeableTabs.js +++ b/app/component/SwipeableTabs.js @@ -63,13 +63,8 @@ const handleKeyPress = (e, reactSwipeEl) => { }; export default class SwipeableTabs extends React.Component { - constructor(props) { - super(); - this.state = { tabIndex: props.tabIndex }; - } - static propTypes = { - tabIndex: PropTypes.number, + tabIndex: PropTypes.number.isRequired, tabs: PropTypes.arrayOf(PropTypes.node).isRequired, onSwipe: PropTypes.func.isRequired, hideArrows: PropTypes.bool, @@ -82,7 +77,6 @@ export default class SwipeableTabs extends React.Component { static defaultProps = { hideArrows: false, navigationOnBottom: false, - tabIndex: 0, classname: undefined, }; @@ -100,7 +94,7 @@ export default class SwipeableTabs extends React.Component { } tabBalls = tabsLength => { - const tabIndex = parseInt(this.state.tabIndex, 10); + const tabIndex = parseInt(this.props.tabIndex, 10); const onLeft = tabIndex; const onRight = tabsLength - tabIndex - 1; let tabBalls = []; @@ -161,17 +155,15 @@ export default class SwipeableTabs extends React.Component { )} tabIndex={0} className={`swipe-tab-ball ${ - index === this.state.tabIndex ? 'selected' : '' + index === this.props.tabIndex ? 'selected' : '' } ${ball.smaller ? 'decreasing-small' : ''} ${ ball.small ? 'decreasing' : '' } ${ball.hidden ? 'hidden' : ''}`} onClick={() => { - this.setState({ tabIndex: index }); this.props.onSwipe(index); }} onKeyDown={e => { if (isKeyboardSelectionEvent(e)) { - this.setState({ tabIndex: index }); this.props.onSwipe(index); } }} @@ -247,7 +239,6 @@ export default class SwipeableTabs extends React.Component { callback: i => { // force transition after animation should be over because animation can randomly fail sometimes setTimeout(() => { - this.setState({ tabIndex: i }); this.props.onSwipe(i); }, 300); }, @@ -262,11 +253,7 @@ export default class SwipeableTabs extends React.Component { )} -
+
{this.props.classname === 'swipe-desktop-view' && (
)} @@ -282,7 +269,7 @@ export default class SwipeableTabs extends React.Component { {!hideArrows && (
@@ -322,7 +309,7 @@ export default class SwipeableTabs extends React.Component { {!hideArrows && (
= tabs.length - 1), + active: !(disabled || this.props.tabIndex >= tabs.length - 1), })} >
= tabs.length - 1 + disabled || this.props.tabIndex >= tabs.length - 1 ? 'disabled' : '' }`} @@ -361,7 +348,6 @@ export default class SwipeableTabs extends React.Component { callback: i => { // force transition after animation should be over because animation can randomly fail sometimes setTimeout(() => { - this.setState({ tabIndex: i }); this.props.onSwipe(i); }, 300); }, diff --git a/app/component/TerminalTimetablePage.js b/app/component/TerminalTimetablePage.js index 4abc98025d..249ce062d7 100644 --- a/app/component/TerminalTimetablePage.js +++ b/app/component/TerminalTimetablePage.js @@ -34,12 +34,12 @@ class TerminalTimetablePage extends React.Component { } onDateChange = value => { + this.setState({ date: value }); this.props.relay.refetch( { date: value, }, null, - () => this.setState({ date: value }), ); }; @@ -48,11 +48,8 @@ class TerminalTimetablePage extends React.Component { ); } diff --git a/app/component/Timetable.js b/app/component/Timetable.js index 2ed3363d04..22c212ae06 100644 --- a/app/component/Timetable.js +++ b/app/component/Timetable.js @@ -82,19 +82,12 @@ class Timetable extends React.Component { }), ).isRequired, }).isRequired, - propsForDateSelect: PropTypes.shape({ - startDate: PropTypes.string, - selectedDate: PropTypes.string, - onDateChange: PropTypes.func, - }).isRequired, - date: PropTypes.string, + startDate: PropTypes.string.isRequired, + onDateChange: PropTypes.func.isRequired, + date: PropTypes.string.isRequired, language: PropTypes.string.isRequired, }; - static defaultProps = { - date: undefined, - }; - static contextTypes = { router: routerShape.isRequired, match: matchShape.isRequired, @@ -183,7 +176,7 @@ class Timetable extends React.Component { }; dateForPrinting = () => { - const selectedDate = moment(this.props.propsForDateSelect.selectedDate); + const selectedDate = moment(this.props.date); return (
@@ -331,9 +324,7 @@ class Timetable extends React.Component { }${locationType.toLowerCase()}/${this.props.stop.gtfsId}`; const timeTableRows = this.createTimeTableRows(timetableMap); const timeDifferenceDays = moment - .duration( - moment(this.props.propsForDateSelect.selectedDate).diff(moment()), - ) + .duration(moment(this.props.date).diff(moment())) .asDays(); return ( <> @@ -349,10 +340,10 @@ class Timetable extends React.Component { ) : null}
{ - this.props.propsForDateSelect.onDateChange(e); + this.props.onDateChange(e); const showRoutes = this.state.showRoutes.length ? this.state.showRoutes.join(',') : undefined; diff --git a/app/component/itinerary/BicycleLeg.js b/app/component/itinerary/BicycleLeg.js index 33531a3a6b..4be92094c7 100644 --- a/app/component/itinerary/BicycleLeg.js +++ b/app/component/itinerary/BicycleLeg.js @@ -375,9 +375,9 @@ export default function BicycleLeg(
openSettings(true)} + onClick={() => openSettings(true, true)} onKeyPress={e => - isKeyboardSelectionEvent(e) && openSettings(true) + isKeyboardSelectionEvent(e) && openSettings(true, true) } className="itinerary-transit-leg-route-bike" > diff --git a/app/component/itinerary/Itinerary.js b/app/component/itinerary/Itinerary.js index ed32310c51..f23d646275 100644 --- a/app/component/itinerary/Itinerary.js +++ b/app/component/itinerary/Itinerary.js @@ -88,7 +88,7 @@ export function RouteLeg( ) { const isCallAgency = isCallAgencyPickupType(leg); let routeNumber; - const mode = getRouteMode(leg.route); + const mode = getRouteMode(leg.route, config); const getOccupancyStatus = () => { if (hasOneTransitLeg) { @@ -118,7 +118,7 @@ export function RouteLeg( { - const subpath = getSubPath(''); if (activeIndex === index) { onSelectImmediately(index); } else { router.replace({ ...match.location, state: { selectedItineraryIndex: index }, - pathname: `${getItineraryPagePath(params.from, params.to)}${subpath}`, }); addAnalyticsEvent({ diff --git a/app/component/itinerary/ItineraryPage.js b/app/component/itinerary/ItineraryPage.js index a6281938b0..b36a6f5cd3 100644 --- a/app/component/itinerary/ItineraryPage.js +++ b/app/component/itinerary/ItineraryPage.js @@ -503,6 +503,9 @@ export default function ItineraryPage(props, context) { laterEdges: [...state.laterEdges, ...edges], }); } + if (arriveBy) { + resetItineraryPageSelection(); + } }; const onEarlier = async () => { @@ -554,7 +557,6 @@ export default function ItineraryPage(props, context) { return; } ariaRef.current = 'itinerary-page.itineraries-loaded'; - const newState = { ...state, loadingMore: undefined, @@ -585,6 +587,9 @@ export default function ItineraryPage(props, context) { earlierEdges: [...edges, ...state.earlierEdges], }); } + if (!arriveBy) { + resetItineraryPageSelection(); + } }; // save url-defined location to old searches @@ -867,7 +872,7 @@ export default function ItineraryPage(props, context) { router.replace(newLocationState); }; - const showSettingsPanel = open => { + const showSettingsPanel = (open, changeScooterSettings) => { addAnalyticsEvent({ event: 'sendMatomoEvent', category: 'ItinerarySettings', @@ -880,6 +885,7 @@ export default function ItineraryPage(props, context) { ...settingsState, settingsOpen: true, settingsOnOpen: getSettings(config), + changeScooterSettings, }); if (breakpoint !== 'large') { router.push({ @@ -892,6 +898,17 @@ export default function ItineraryPage(props, context) { } return; } + if ( + settingsState.changeScooterSettings && + settingsState.settingsOnOpen.scooterNetworks.length < + getSettings(config).scooterNetworks.length + ) { + addAnalyticsEvent({ + category: 'ItinerarySettings', + action: 'SettingsEnableScooterNetwork', + name: 'AfterOnlyScooterRoutesFound', + }); + } const settingsChanged = !isEqual( settingsState.settingsOnOpen, @@ -903,6 +920,7 @@ export default function ItineraryPage(props, context) { ...settingsState, settingsOpen: false, settingsChanged, + changeScooterSettings: false, }); if (settingsChanged && detailView) { diff --git a/app/component/itinerary/ItineraryPageUtils.js b/app/component/itinerary/ItineraryPageUtils.js index a6342199c3..e7bb0af43d 100644 --- a/app/component/itinerary/ItineraryPageUtils.js +++ b/app/component/itinerary/ItineraryPageUtils.js @@ -30,13 +30,25 @@ import { getTotalBikingDistance, compressLegs } from '../../util/legUtils'; export function getSelectedItineraryIndex( { pathname, state } = {}, edges = [], - defaultValue = 0, ) { + // path defines the selection in detail view + const lastURLSegment = pathname?.split('/').pop(); + if (lastURLSegment !== '') { + const index = Number(pathname?.split('/').pop()); + if (!Number.isNaN(index)) { + if (index >= edges.length) { + return 0; + } + return index; + } + } + + // in summary view, look the location state if (state?.selectedItineraryIndex !== undefined) { if (state.selectedItineraryIndex < edges.length) { return state.selectedItineraryIndex; } - return defaultValue; + return 0; } /* @@ -44,13 +56,6 @@ export function getSelectedItineraryIndex( * page by an external link, we check if an itinerary selection is * supplied in URL and make that the selection. */ - const lastURLSegment = Number(pathname?.split('/').pop()); - if (!Number.isNaN(lastURLSegment)) { - if (lastURLSegment >= edges.length) { - return defaultValue; - } - return lastURLSegment; - } return 0; } diff --git a/app/component/itinerary/LegInfo.js b/app/component/itinerary/LegInfo.js index 214218f381..5bf0979d39 100644 --- a/app/component/itinerary/LegInfo.js +++ b/app/component/itinerary/LegInfo.js @@ -34,7 +34,10 @@ export default function LegInfo( !constantOperationRoutes || !constantOperationRoutes[leg.route.gtfsId]; const mode = isCallAgency ? 'call' - : getRouteMode({ mode: leg.mode, type: leg.route.type }); + : getRouteMode( + { mode: leg.mode, type: leg.route.type, gtfsId: leg.route?.gtfsId }, + config, + ); const capacity = getCapacityForLeg(config, leg); let capacityTranslation; if (capacity) { diff --git a/app/component/itinerary/Legs.js b/app/component/itinerary/Legs.js index 6a46710ac2..1da3999184 100644 --- a/app/component/itinerary/Legs.js +++ b/app/component/itinerary/Legs.js @@ -187,7 +187,14 @@ export default class Legs extends React.Component { leg.mode === 'FUNICULAR') && !leg.interlineWithPreviousLeg ) { - const mode = getRouteMode({ mode: leg.mode, type: leg.route?.type }); + const mode = getRouteMode( + { + mode: leg.mode, + type: leg.route?.type, + gtfsId: leg.route?.gtfsId, + }, + this.context.config, + ); legs.push(); } else if (leg.mode === 'AIRPLANE') { legs.push( diff --git a/app/component/itinerary/TransitLeg.js b/app/component/itinerary/TransitLeg.js index 2cfa59d98b..3a83cd6134 100644 --- a/app/component/itinerary/TransitLeg.js +++ b/app/component/itinerary/TransitLeg.js @@ -505,7 +505,6 @@ class TransitLeg extends React.Component { - { - this.halo = el; - }} - positions={filteredPoints} - className={`leg-halo ${className}`} - weight={haloWeight} - interactive={false} - /> + {displayHalo && ( + { + this.halo = el; + }} + positions={filteredPoints} + className={`leg-halo ${className}`} + weight={haloWeight} + interactive={false} + /> + )} { diff --git a/app/component/map/map.scss b/app/component/map/map.scss index 9ed66118af..045121dd6e 100644 --- a/app/component/map/map.scss +++ b/app/component/map/map.scss @@ -830,6 +830,10 @@ div.origin-popup { min-width: 50px; height: 18px; + &.only-icon { + min-width: 0; + } + .wide { min-width: 64px; } @@ -862,6 +866,10 @@ div.origin-popup { color: $white; } + .map-route-number.ferry-external { + color: $black; + } + &::before { content: ''; width: 0; @@ -878,12 +886,20 @@ div.origin-popup { } } +.map .arrow-bottomLeft.only-icon div::before { + border-right: 10px solid transparent; +} + .map .arrow-bottomRight { display: flex; align-items: center; min-width: 50px; height: 18px; + &.only-icon { + min-width: 0; + } + .wide { min-width: 64px; } @@ -916,6 +932,10 @@ div.origin-popup { color: $white; } + .map-route-number.ferry-external { + color: $black; + } + &::before { content: ''; width: 0; @@ -931,12 +951,20 @@ div.origin-popup { } } +.map .arrow-bottomRight.only-icon div::before { + border-right: 9px solid transparent; +} + .map .arrow-topRight { display: flex; align-items: center; min-width: 50px; height: 18px; + &.only-icon { + min-width: 0; + } + .wide { min-width: 64px; } @@ -969,6 +997,10 @@ div.origin-popup { color: $white; } + .map-route-number.ferry-external { + color: $black; + } + &::before { content: ''; width: 0; @@ -985,12 +1017,21 @@ div.origin-popup { } } +.map .arrow-topRight.only-icon div::before { + border-left: 15px solid transparent; + right: 11px; +} + .map .arrow-topLeft { display: flex; align-items: center; min-width: 50px; height: 18px; + &.only-icon { + min-width: 0; + } + .wide { min-width: 64px; } @@ -1023,6 +1064,10 @@ div.origin-popup { color: $white; } + .map-route-number.ferry-external { + color: $black; + } + &::before { content: ''; width: 0; @@ -1045,6 +1090,11 @@ div.origin-popup { min-width: 50px; height: 18px; + &.only-icon { + min-width: 0; + align-items: normal; + } + .wide { min-width: 64px; } @@ -1210,6 +1260,10 @@ div.origin-popup { padding-right: 3px; color: $white; } + + .map-route-number.ferry-external { + color: $black; + } } } diff --git a/app/component/map/non-tile-layer/LegMarker.js b/app/component/map/non-tile-layer/LegMarker.js index 92cf3672cd..2683d87d75 100644 --- a/app/component/map/non-tile-layer/LegMarker.js +++ b/app/component/map/non-tile-layer/LegMarker.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Icon from '../../Icon'; import { isBrowser } from '../../../util/browser'; -import { legShape } from '../../../util/shapes'; +import { legShape, configShape } from '../../../util/shapes'; /* eslint-disable global-require */ @@ -28,10 +28,26 @@ class LegMarker extends React.Component { style: undefined, }; + static contextTypes = { + config: configShape.isRequired, + }; + // An arrow marker will be displayed if the normal marker can't fit getLegMarker() { const color = this.props.color ? this.props.color : 'currentColor'; const className = this.props.wide ? 'wide' : ''; + // Do not display route number if it is an external route and the route number is empty. + const displayRouteNumber = !( + this.context.config.externalFeedIds !== undefined && + this.props.mode.includes('external') && + this.props.leg.name === '' + ); + const routeNumber = displayRouteNumber + ? ` + ${this.props.leg.name.toLowerCase()}` + : ''; return (
`, className: `${ this.props.style ? `arrow-${this.props.style}` : 'legmarker' - } ${this.props.mode}`, + } ${this.props.mode} ${displayRouteNumber ? '' : 'only-icon'}`, iconSize: null, })} zIndexOffset={this.props.zIndexOffset} diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index b65ab78bca..ee62060eba 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -90,6 +90,7 @@ export default { useRoutingFeedbackPrompt: true, feedIds: ['HSL', 'HSLlautta', 'Sipoo'], + externalFeedIds: ['HSLlautta'], showHSLTracking: false, allowLogin: true, @@ -520,15 +521,15 @@ export default { scooterInfoLink: { fi: { text: 'Potkulaudat', - url: 'https://www.hsl.fi/hsl/uutiset/teemat/potkulaudat', + url: 'https://www.hsl.fi/reittiopas_potkulaudat', }, en: { text: 'Scooters', - url: 'https://www.hsl.fi/hsl/uutiset/teemat/potkulaudat', + url: 'https://www.hsl.fi/en/journey_planner_scooters', }, sv: { text: 'Elsparkcyklar', - url: 'https://www.hsl.fi/hsl/uutiset/teemat/potkulaudat', + url: 'https://www.hsl.fi/sv/reseplaneraren_sparkcyklar', }, }, maxMinutesToRentalJourneyEnd: 240, @@ -580,7 +581,7 @@ export default { showSimilarRoutesOnRouteDropDown: true, useRealtimeTravellerCapacities: true, - navigation: true, + navigation: false, stopCard: { header: { diff --git a/app/configurations/config.jyvaskyla.js b/app/configurations/config.jyvaskyla.js index 3a777ebae2..af6d6ecd6d 100644 --- a/app/configurations/config.jyvaskyla.js +++ b/app/configurations/config.jyvaskyla.js @@ -183,7 +183,7 @@ export default configMerger(walttiConfig, { for (let i = 0; i < ticket.length; i++) { zones += `0${ticket.charCodeAt(i) - 64}`; // eslint-disable } - return `https://waltti.fi/walttiappfeat/busTicket/?operator=50209&ticketType=single&customerGroup=adult&zones=${zones}`; + return `https://waltti.fi/walttiapp/busTicket/?operator=50209&ticketType=single&customerGroup=adult&zones=${zones}`; }, fareMapping: function mapFareId(fareId) { diff --git a/app/configurations/config.matka.js b/app/configurations/config.matka.js index cb93310f92..00fc3035bf 100644 --- a/app/configurations/config.matka.js +++ b/app/configurations/config.matka.js @@ -90,6 +90,7 @@ export default { 'VARELY', 'Harma', 'PohjolanMatka', + 'Korsisaari', ], additionalFeedIds: { @@ -176,6 +177,20 @@ export default { ...KotkaConfig.vehicleRental.networks, ...KouvolaConfig.vehicleRental.networks, }, + scooterInfoLink: { + fi: { + text: 'Potkulaudat', + url: 'https://www.fintraffic.fi/fi/uutiset/sahkopotkulaudat-nyt-mukana-opasmatkafi-reittioppaassa', + }, + en: { + text: 'Scooters', + url: 'https://www.fintraffic.fi/en/news/electric-scooters-now-included-opasmatkafi-journey-planner', + }, + sv: { + text: 'Elsparkcyklar', + url: 'https://www.fintraffic.fi/sv/nyheter/elsparkcyklarna-finns-nu-med-i-reseplaneraren-opasmatkafi', + }, + }, }, getAutoSuggestIcons: { diff --git a/app/configurations/realtimeUtils.js b/app/configurations/realtimeUtils.js index e05d29778b..0c67fecc0b 100644 --- a/app/configurations/realtimeUtils.js +++ b/app/configurations/realtimeUtils.js @@ -146,6 +146,7 @@ export default { VARELY: walttiMqtt, PohjolanMatka: elyMqtt(true), Harma: elyMqtt(false), + Korsisaari: walttiMqtt, FOLI: { mqttTopicResolver: function mqttTopicResolver( route, diff --git a/app/translations.js b/app/translations.js index 0b149cf1c8..05acb751a8 100644 --- a/app/translations.js +++ b/app/translations.js @@ -1071,6 +1071,8 @@ const translations = { 'favourite-failed-text': 'Please try again in a while.', 'favourite-target': 'Favorite location', ferry: 'Ferry', + 'ferry-external': 'Ferry', + 'ferry-external-with-route-number': 'Ferry {routeNumber} {headSign}', 'ferry-with-route-number': 'Ferry {routeNumber} {headSign}', 'fetch-new-route': 'Fetch a new route', 'few-seats-available': 'Few seats available', @@ -1275,6 +1277,7 @@ const translations = { 'modes.to-bike-park': 'bike park', 'modes.to-bus': 'bus stop', 'modes.to-car-park': 'car park', + 'modes.to-ferry': 'ferry pier', 'modes.to-place': 'destination', 'modes.to-rail': 'train station', 'modes.to-scooter': 'to scooter', @@ -2305,6 +2308,8 @@ const translations = { 'favourite-failed-text': 'Yritä hetken päästä uudelleen.', 'favourite-target': 'Suosikkikohde', ferry: 'Lautta', + 'ferry-external': 'Lautta', + 'ferry-external-with-route-number': 'Lautta {routeNumber} {headSign}', 'ferry-with-route-number': 'Lautta {routeNumber} {headSign}', 'fetch-new-route': 'Hae uusi reitti', 'few-seats-available': 'Joitakin istumapaikkoja vapaana', @@ -2502,6 +2507,7 @@ const translations = { 'modes.to-bike-park': 'liityntäpyöräparkkiin', 'modes.to-bus': 'bussipysäkille', 'modes.to-car-park': 'liityntäpysäköintiin', + 'modes.to-ferry': 'lauttalaiturille', 'modes.to-place': 'kohteeseen', 'modes.to-rail': 'juna-asemalle', 'modes.to-scooter': 'potkulaudalle', @@ -5177,6 +5183,8 @@ const translations = { 'favourite-failed-text': 'Försök på nytt senare.', 'favourite-target': 'Favoritdestination', ferry: 'Färja', + 'ferry-external': 'Färja', + 'ferry-external-with-route-number': 'Färja {routeNumber} {headSign}', 'ferry-with-route-number': 'Färja {routeNumber} {headSign}', 'fetch-new-route': 'Sök en ny rutt', 'few-seats-available': 'Några sittplatser', @@ -5377,6 +5385,7 @@ const translations = { 'modes.to-bike-park': 'anslutningsparkering för cyklar', 'modes.to-bus': 'busshållplats', 'modes.to-car-park': 'infartsparkering', + 'modes.to-ferry': 'färjekajen', 'modes.to-place': 'destination', 'modes.to-rail': 'tågstation', 'modes.to-scooter': 'elsparkcykel', diff --git a/app/util/feedScopedIdUtils.js b/app/util/feedScopedIdUtils.js index 410d755561..23de971504 100644 --- a/app/util/feedScopedIdUtils.js +++ b/app/util/feedScopedIdUtils.js @@ -14,3 +14,30 @@ export const getIdWithoutFeed = feedScopedId => { } return feedScopedId.split(':')[1]; }; + +/** + * Returns feedId without the objectId. + * + * @param {String} feedScopedId should be in feedId:objectId format. + */ +export const getFeedWithoutId = feedScopedId => { + if (!feedScopedId) { + return undefined; + } + if (feedScopedId.indexOf(':') === -1) { + return feedScopedId; + } + return feedScopedId.split(':')[0]; +}; + +/** + * Returns boolean indicating whether the feed is external or not. + */ +export const isExternalFeed = (feedId, config) => { + return ( + config !== undefined && + config.externalFeedIds !== undefined && + feedId !== undefined && + config.externalFeedIds.includes(feedId) + ); +}; diff --git a/app/util/modeUtils.js b/app/util/modeUtils.js index aee5f90a6e..768aabba92 100644 --- a/app/util/modeUtils.js +++ b/app/util/modeUtils.js @@ -7,6 +7,7 @@ import { isInBoundingBox } from './geo-utils'; import { addAnalyticsEvent } from './analyticsUtils'; import { ExtendedRouteTypes, TransportMode } from '../constants'; import { isDevelopmentEnvironment } from './envUtils'; +import { getFeedWithoutId, isExternalFeed } from './feedScopedIdUtils'; function seasonMs(ddmmyyyy) { const parts = ddmmyyyy.split('.'); @@ -113,7 +114,7 @@ export function getTransportModes(config) { }; } -export function getRouteMode(route) { +export function getRouteMode(route, config) { switch (route.type) { case ExtendedRouteTypes.BusExpress: return 'bus-express'; @@ -122,7 +123,9 @@ export function getRouteMode(route) { case ExtendedRouteTypes.SpeedTram: return 'speedtram'; default: - return route.mode?.toLowerCase(); + return isExternalFeed(getFeedWithoutId(route?.gtfsId), config) + ? `${route.mode?.toLowerCase()}-external` + : route.mode?.toLowerCase(); } } diff --git a/app/util/vehicleRentalUtils.js b/app/util/vehicleRentalUtils.js index 3e6ec7cc5d..242489e5f9 100644 --- a/app/util/vehicleRentalUtils.js +++ b/app/util/vehicleRentalUtils.js @@ -131,33 +131,30 @@ const addAnalytics = (action, name) => { * Updates the list of allowed networks either by removing or adding. * Note: legacy settings had network names always in uppercase letters. * - * @param currentSettings the current settings - * @param newValue the network to be added/removed - * @param config The configuration for the software installation - * @param isUsingCitybike if citybike is enabled + * @param networks the previously selected networks + * @param networkName the network to be added/removed + * @param type the type of the network * @returns the updated citybike networks */ -export const updateVehicleNetworks = (currentSettings, newValue) => { - let chosenNetworks; +export const updateVehicleNetworks = (networks, networkName, type) => { + let updatedNetworks; + let toggleAction; - if (currentSettings) { - chosenNetworks = currentSettings.find( - o => o.toLowerCase() === newValue.toLowerCase(), - ) - ? without(currentSettings, newValue, newValue.toUpperCase()) - : currentSettings.concat([newValue]); + if (networks.find(o => o.toLowerCase() === networkName.toLowerCase())) { + updatedNetworks = without(networks, networkName, networkName.toUpperCase()); + toggleAction = 'Disable'; } else { - chosenNetworks = [newValue]; + updatedNetworks = networks.concat([networkName]); + toggleAction = 'Enable'; } - if (Array.isArray(currentSettings) && Array.isArray(chosenNetworks)) { - const action = `Settings${ - currentSettings.length > chosenNetworks.length ? 'Disable' : 'Enable' - }CityBikeNetwork`; - addAnalytics(action, newValue); - } - return chosenNetworks; + const action = `Settings${toggleAction}${ + type === 'citybike' ? 'CityBikeNetwork' : 'ScooterNetwork' + }`; + addAnalytics(action, networkName); + + return updatedNetworks; }; export const getVehicleMinZoomOnStopsNearYou = (config, override) => { @@ -184,11 +181,17 @@ export const hasVehicleRentalCode = rentalId => { }; export const mapVehicleRentalFromStore = vehicleRentalStation => { - const network = vehicleRentalStation.networks[0]; + const originalId = vehicleRentalStation.stationId; + const network = + vehicleRentalStation.networks?.[0] || originalId.split(':')[0]; + const stationId = originalId.startsWith(network) + ? originalId + : `${network}:${originalId}`; + const newStation = { ...vehicleRentalStation, network, - stationId: `${network}:${vehicleRentalStation.stationId}`, + stationId, }; delete newStation.networks; return newStation; diff --git a/package.json b/package.json index f3df387584..201a926f22 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "babel-plugin-relay": "16.2.0", "body-parser": "1.19.0", "classnames": "2.2.6", - "connect-redis": "7.1.1", + "connect-redis": "5.0.0", "cookie-parser": "1.4.5", "debug": "4.3.4", "esm": "3.2.25", @@ -204,7 +204,7 @@ "react-swipe": "6.0.4", "react-truncate-markup": "5.1.0", "recompose": "0.30.0", - "redis": "4.6.13", + "redis": "2.8.0", "relay-runtime": "16.2.0", "serialize-javascript": "4.0.0", "suncalc": "1.8.0", diff --git a/sass/base/_helper-classes.scss b/sass/base/_helper-classes.scss index 740b9e66e5..ec34c58e18 100644 --- a/sass/base/_helper-classes.scss +++ b/sass/base/_helper-classes.scss @@ -44,6 +44,10 @@ color: $ferry-color; } +.ferry-external { + color: $external-feed-color; +} + .funicular { color: $funicular-color; } diff --git a/sass/themes/default/_theme.scss b/sass/themes/default/_theme.scss index 196c8c7a0d..9fd716da7c 100644 --- a/sass/themes/default/_theme.scss +++ b/sass/themes/default/_theme.scss @@ -84,6 +84,7 @@ $bicycle-color: #666; $car-color: #333; $scooter-color: #c5cad2; $call-agency-color: #666; +$external-feed-color: #c5cad2; /* Fonts */ $font-family: 'Roboto', arial, georgia, serif; diff --git a/scripts/README.md b/scripts/README.md index 0613f70d9f..f40d37425c 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,16 +1,20 @@ # Using scripts -## Before using: +## Before using ``` source ui.sh ``` ## Usage examples -Using remote instance of OTP with subscription key. +Using remote instance of OTP with subscription key: ``` SUBSCRIPTION_KEY= ui hsl ``` -Using local instance of OTP on port `9080`. +Using local instance of OTP on port `9080`: +``` +SUBSCRIPTION_KEY= uiotp matka +``` +In case you do not need features usable with a subscription key when running a local instance of OTP on port `9080`: +``` +NO_SUBSCRIPTION_KEY=true uiotp matka ``` -uiotp matka -``` \ No newline at end of file diff --git a/scripts/ui.sh b/scripts/ui.sh index 3c1cd304ea..cfcaf235cb 100755 --- a/scripts/ui.sh +++ b/scripts/ui.sh @@ -1,6 +1,11 @@ #!/usr/bin/env bash ui () { + if [ "$SUBSCRIPTION_KEY" = "" -a "$NO_SUBSCRIPTION_KEY" != "true" ]; then + echo "In order to use the UI you need to set the SUBSCRIPTION_KEY environment variable." + echo "If you want to run the UI without a subscription key, set NO_SUBSCRIPTION_KEY=true." + return 1 2>/dev/null + fi CONFIG=$1 API_SUBSCRIPTION_QUERY_PARAMETER_NAME=digitransit-subscription-key API_SUBSCRIPTION_HEADER_NAME=digitransit-subscription-key API_SUBSCRIPTION_TOKEN=$SUBSCRIPTION_KEY yarn run dev } diff --git a/server/passport-openid-connect/openidConnect.js b/server/passport-openid-connect/openidConnect.js index 16b86dbb05..df6dcd677e 100644 --- a/server/passport-openid-connect/openidConnect.js +++ b/server/passport-openid-connect/openidConnect.js @@ -4,7 +4,7 @@ const session = require('express-session'); const redis = require('redis'); const axios = require('axios'); const moment = require('moment'); -const RedisStore = require('connect-redis').default; +const RedisStore = require('connect-redis')(session); const LoginStrategy = require('./Strategy').Strategy; const clearAllUserSessions = false; // set true if logout should erase all user's sessions @@ -28,13 +28,13 @@ export default function setUpOIDC(app, port, indexPath, hostnames) { const RedisHost = process.env.REDIS_HOST || 'localhost'; const RedisPort = process.env.REDIS_PORT || 6379; - const RedisProtocol = process.env.REDIS_HOST ? 'rediss' : 'redis'; - const RedisURL = `${RedisProtocol}://${RedisHost}:${RedisPort}`; const RedisKey = process.env.REDIS_KEY; const RedisClient = RedisKey - ? redis.createClient({ url: RedisURL, password: RedisKey }) - : redis.createClient({ url: RedisURL }); - RedisClient.connect().catch(console.error); + ? redis.createClient(RedisPort, RedisHost, { + auth_pass: RedisKey, + tls: { servername: RedisHost }, + }) + : redis.createClient(RedisPort, RedisHost); const redirectUris = hostnames.map(host => `${host}${callbackPath}`); const postLogoutRedirectUris = hostnames.map( @@ -61,7 +61,7 @@ export default function setUpOIDC(app, port, indexPath, hostnames) { console.log(`adding session for used ${userId} id ${sessionId}`); } if (clearAllUserSessions) { - RedisClient.sAdd(`sessions-${userId}`, sessionId); + RedisClient.sadd(`sessions-${userId}`, sessionId); } }, }); @@ -125,6 +125,8 @@ export default function setUpOIDC(app, port, indexPath, hostnames) { session({ secret: process.env.SESSION_SECRET || 'reittiopas_secret', store: new RedisStore({ + host: RedisHost, + port: RedisPort, client: RedisClient, ttl: 60 * 60 * 24 * 60, }), @@ -204,7 +206,7 @@ export default function setUpOIDC(app, port, indexPath, hostnames) { } const sessions = `sessions-${req.session.userId}`; req.logout({}, () => { - RedisClient.sMembers(sessions).then(sessionIds => { + RedisClient.smembers(sessions, function (err, sessionIds) { req.session.destroy(function () { res.clearCookie('connect.sid'); if (sessionIds && sessionIds.length > 0) { diff --git a/static/assets/svg-sprite.default.svg b/static/assets/svg-sprite.default.svg index b89e0b5d9a..665ff70247 100644 --- a/static/assets/svg-sprite.default.svg +++ b/static/assets/svg-sprite.default.svg @@ -1841,7 +1841,7 @@ - + diff --git a/static/assets/svg-sprite.hsl.svg b/static/assets/svg-sprite.hsl.svg index c97183ba87..00677c698d 100644 --- a/static/assets/svg-sprite.hsl.svg +++ b/static/assets/svg-sprite.hsl.svg @@ -100,6 +100,10 @@ + + + + diff --git a/test/unit/component/Timetable.test.js b/test/unit/component/Timetable.test.js index 38266b887f..45290d2128 100644 --- a/test/unit/component/Timetable.test.js +++ b/test/unit/component/Timetable.test.js @@ -12,11 +12,8 @@ import * as timetables from '../../../app/configurations/timetableConfigUtils'; const stopIdNumber = '1140199'; const props = { - propsForDateSelect: { - startDate: '20190110', - selectedDate: '20190110', - onDateChange: () => {}, - }, + startDate: '20190110', + onDateChange: () => {}, stop: { gtfsId: `HSL:${stopIdNumber}`, locationType: 'STOP', diff --git a/yarn.lock b/yarn.lock index 1b00cb003e..3d6a826414 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5453,62 +5453,6 @@ __metadata: languageName: node linkType: hard -"@redis/bloom@npm:1.2.0": - version: 1.2.0 - resolution: "@redis/bloom@npm:1.2.0" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: 8c214227287d6b278109098bca00afc601cf84f7da9c6c24f4fa7d3854b946170e5893aa86ed607ba017a4198231d570541c79931b98b6d50b262971022d1d6c - languageName: node - linkType: hard - -"@redis/client@npm:1.5.14": - version: 1.5.14 - resolution: "@redis/client@npm:1.5.14" - dependencies: - cluster-key-slot: 1.1.2 - generic-pool: 3.9.0 - yallist: 4.0.0 - checksum: f401997c6df92055c1a59385ed2fed7ee9295860f39935821107ea2570f76168dd1b25b71a3b37b9bbfaba26a9d18080d6bcd101a4bfc3852f72cc20576c6e7d - languageName: node - linkType: hard - -"@redis/graph@npm:1.1.1": - version: 1.1.1 - resolution: "@redis/graph@npm:1.1.1" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: caf9b9a3ff82a08ae543c356a3fed548399ae79aba5ed08ce6cf1b522b955eb5cee4406b0ed0c6899345f8fbc06dfd6cd51304ae8422c3ebbc468f53294dc509 - languageName: node - linkType: hard - -"@redis/json@npm:1.0.6": - version: 1.0.6 - resolution: "@redis/json@npm:1.0.6" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: 9fda29abc339c72593f34a23f8023b715c1f8f3d73f7c59889af02f25589bac2ad57073ad08d0b8da42cd8c258665a7b38d39e761e92945cc27aca651c8a93a5 - languageName: node - linkType: hard - -"@redis/search@npm:1.1.6": - version: 1.1.6 - resolution: "@redis/search@npm:1.1.6" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: 0d87e6a9e40e62e46064ccfccca9a5ba7ce608890740415008acb1e83a02690edf37ac1ee878bcc0702d30a2eeba7776e7b467b71f87d8e7b278f38637161e16 - languageName: node - linkType: hard - -"@redis/time-series@npm:1.0.5": - version: 1.0.5 - resolution: "@redis/time-series@npm:1.0.5" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: 6bbdb0b793dcbd13518aa60a09a980f953554e4c745bfacc1611baa8098f360e0378e8ee6b7faf600a67f1de83f4b68bbec6f95a0740faee6164c14be3a30752 - languageName: node - linkType: hard - "@repeaterjs/repeater@npm:3.0.4": version: 3.0.4 resolution: "@repeaterjs/repeater@npm:3.0.4" @@ -9272,13 +9216,6 @@ __metadata: languageName: node linkType: hard -"cluster-key-slot@npm:1.1.2": - version: 1.1.2 - resolution: "cluster-key-slot@npm:1.1.2" - checksum: be0ad2d262502adc998597e83f9ded1b80f827f0452127c5a37b22dfca36bab8edf393f7b25bb626006fb9fb2436106939ede6d2d6ecf4229b96a47f27edd681 - languageName: node - linkType: hard - "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -9688,12 +9625,10 @@ __metadata: languageName: node linkType: hard -"connect-redis@npm:7.1.1": - version: 7.1.1 - resolution: "connect-redis@npm:7.1.1" - peerDependencies: - express-session: ">=1" - checksum: ac91ee818d0f467866b6982f66b3423fee58de9da3562618f6d1df2ddeea426354c1efe70b3f799be1b52e3cc67f2043b9ae203678fd3ff9db5dff44b078f0ca +"connect-redis@npm:5.0.0": + version: 5.0.0 + resolution: "connect-redis@npm:5.0.0" + checksum: 0eca205a42b68842c6b461d3cb51bde2cbc99984a0af51e99701f07a55209f3f60e1e34dd6d6cc81aa4fd462cd2d9881bf9b2f44ca3bbcd4b496514822a5e585 languageName: node linkType: hard @@ -11263,7 +11198,7 @@ __metadata: chai: 4.3.7 classnames: 2.2.6 compression-webpack-plugin: 5.0.2 - connect-redis: 7.1.1 + connect-redis: 5.0.0 cookie-parser: 1.4.5 copy-webpack-plugin: 6.1.0 css-loader: ^5.2.7 @@ -11364,7 +11299,7 @@ __metadata: react-swipe: 6.0.4 react-truncate-markup: 5.1.0 recompose: 0.30.0 - redis: 4.6.13 + redis: 2.8.0 relay-compiler: 16.2.0 relay-runtime: 16.2.0 rollup: 2.35.1 @@ -11712,6 +11647,13 @@ __metadata: languageName: node linkType: hard +"double-ended-queue@npm:^2.1.0-0": + version: 2.1.0-0 + resolution: "double-ended-queue@npm:2.1.0-0" + checksum: 3030cf9dcf6f8e7d8cb6ae5b7304890445d7c32233a614e400ba7b378086ad76f5822d0e501afd5ffe0af1de4bcb842fa23d4c79174d54f6566399435fafc271 + languageName: node + linkType: hard + "dset@npm:^3.1.2": version: 3.1.3 resolution: "dset@npm:3.1.3" @@ -14070,13 +14012,6 @@ __metadata: languageName: node linkType: hard -"generic-pool@npm:3.9.0": - version: 3.9.0 - resolution: "generic-pool@npm:3.9.0" - checksum: 3d89e9b2018d2e3bbf44fec78c76b2b7d56d6a484237aa9daf6ff6eedb14b0899dadd703b5d810219baab2eb28e5128fb18b29e91e602deb2eccac14492d8ca8 - languageName: node - linkType: hard - "genfun@npm:^5.0.0": version: 5.0.0 resolution: "genfun@npm:5.0.0" @@ -24144,17 +24079,28 @@ __metadata: languageName: node linkType: hard -"redis@npm:4.6.13": - version: 4.6.13 - resolution: "redis@npm:4.6.13" +"redis-commands@npm:^1.2.0": + version: 1.7.0 + resolution: "redis-commands@npm:1.7.0" + checksum: d1ff7fbcb5e54768c77f731f1d49679d2a62c3899522c28addb4e2e5813aea8bcac3f22519d71d330224c3f2937f935dfc3d8dc65e90db0f5fe22dc2c1515aa7 + languageName: node + linkType: hard + +"redis-parser@npm:^2.6.0": + version: 2.6.0 + resolution: "redis-parser@npm:2.6.0" + checksum: 8d4936875e39d56a951e0bbb6653b4da1f7fdd727552c89561c3c78e7ffeb9c3e8820a78454e939b74d1ba20996d62ac179b4fc39d07340d10f8d52740399422 + languageName: node + linkType: hard + +"redis@npm:2.8.0": + version: 2.8.0 + resolution: "redis@npm:2.8.0" dependencies: - "@redis/bloom": 1.2.0 - "@redis/client": 1.5.14 - "@redis/graph": 1.1.1 - "@redis/json": 1.0.6 - "@redis/search": 1.1.6 - "@redis/time-series": 1.0.5 - checksum: 10150ec30f1f89e47cec41c27dc77a99c78a0b078735de4fc9b79c2a779787d52274c64c52004af161b95b9729067671a4cfca95257719968accb30057856a09 + double-ended-queue: ^2.1.0-0 + redis-commands: ^1.2.0 + redis-parser: ^2.6.0 + checksum: e44dc50a9a92ede2c95b3166482a4b04373853fdbb72ab138e834b868a3cd446798742fc0f85de2d5e442f9c86f58e1250637ea71af3ad5527478d05ef430079 languageName: node linkType: hard @@ -29554,13 +29500,6 @@ __metadata: languageName: node linkType: hard -"yallist@npm:4.0.0, yallist@npm:^4.0.0": - version: 4.0.0 - resolution: "yallist@npm:4.0.0" - checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 - languageName: node - linkType: hard - "yallist@npm:^2.1.2": version: 2.1.2 resolution: "yallist@npm:2.1.2" @@ -29575,6 +29514,13 @@ __metadata: languageName: node linkType: hard +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 + languageName: node + linkType: hard + "yaml@npm:^1.10.0, yaml@npm:^1.10.2, yaml@npm:^1.7.2": version: 1.10.2 resolution: "yaml@npm:1.10.2"