From 0d932e87de98736cb8b1ca682bf785c205095478 Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 28 Feb 2024 11:38:09 +0200 Subject: [PATCH 01/79] feat: dt-6182 initial scooters --- app/component/BicycleLeg.js | 87 ++++++- app/component/CustomizeSearch.js | 98 +++++++- app/component/Itinerary.js | 18 ++ app/component/ItineraryDetails.js | 20 +- app/component/ItineraryList/ItineraryList.js | 36 +++ .../ItineraryList/errorCardProperties.js | 7 + app/component/ItineraryListContainer.js | 11 + app/component/ItineraryPage.js | 97 +++++++- app/component/ItineraryPageContainer.js | 2 + app/component/ItineraryQueries.js | 165 +++++++++++++ app/component/RentalVehicle.js | 35 +++ app/component/RentalVehicleContent.js | 113 +++++++++ .../RentalVehiclePageMapContainer.js | 45 ++++ app/component/RouteNumber.js | 22 +- app/component/StopsNearYouPage.js | 6 +- app/component/VehicleRentalLeg.js | 161 ++++++++----- app/component/WalkLeg.js | 15 +- app/component/customize-search.scss | 70 ++++++ .../ScooterRentalNetworkSelector.js | 95 ++++++++ .../customizesearch/TransportModesSection.js | 2 +- .../VehicleRentalStationNetworkSelector.js | 107 +++++---- app/component/itinerary-summary.scss | 25 +- app/component/itinerary.scss | 63 +++++ app/component/map/ItineraryLine.js | 28 ++- app/component/map/StopsNearYouMap.js | 2 +- .../map/non-tile-layer/VehicleMarker.js | 51 ++-- app/component/rental-vehicle-content.scss | 226 ++++++++++++++++++ app/constants.js | 2 + app/routes.js | 48 ++++ app/store/localStorage.js | 4 + app/translations.js | 74 ++++++ app/util/legUtils.js | 3 +- app/util/modeUtils.js | 19 +- app/util/path.js | 2 +- app/util/planParamUtil.js | 37 ++- app/util/vehicleRentalUtils.js | 23 +- sass/_main.scss | 1 + sass/base/_helper-classes.scss | 4 + sass/themes/default/_theme.scss | 3 + sass/themes/kuopio/_theme.scss | 2 +- static/assets/svg-sprite.default.svg | 24 ++ static/assets/svg-sprite.hsl.svg | 24 ++ test/unit/component/BicycleLeg.test.js | 12 + .../component/MapLayersDialogContent.test.js | 1 + 44 files changed, 1708 insertions(+), 182 deletions(-) create mode 100644 app/component/RentalVehicle.js create mode 100644 app/component/RentalVehicleContent.js create mode 100644 app/component/RentalVehiclePageMapContainer.js create mode 100644 app/component/customizesearch/ScooterRentalNetworkSelector.js create mode 100644 app/component/rental-vehicle-content.scss diff --git a/app/component/BicycleLeg.js b/app/component/BicycleLeg.js index ed1c3d5187..f78e4a6a43 100644 --- a/app/component/BicycleLeg.js +++ b/app/component/BicycleLeg.js @@ -20,6 +20,8 @@ import { splitStringToAddressAndPlace } from '../util/otpStrings'; import VehicleRentalLeg from './VehicleRentalLeg'; import StopCode from './StopCode'; import PlatformNumber from './PlatformNumber'; +import { getSettings } from '../util/planParamUtil'; +import { TransportMode } from '../constants'; function BicycleLeg( { focusAction, index, leg, focusToLeg, bicycleWalkLeg }, @@ -38,16 +40,18 @@ function BicycleLeg( const firstLegClassName = index === 0 ? 'start' : ''; let modeClassName = 'bicycle'; const [address, place] = splitStringToAddressAndPlace(leg.from.name); + const rentalVehicleNetwork = + leg.from.vehicleRentalStation?.network || leg.from.rentalVehicle?.network; const networkConfig = leg.rentedBike && - leg.from.vehicleRentalStation && - getVehicleRentalStationNetworkConfig( - leg.from.vehicleRentalStation.network, - config, - ); + rentalVehicleNetwork && + getVehicleRentalStationNetworkConfig(rentalVehicleNetwork, config); const isFirstLeg = i => i === 0; const isScooter = networkConfig && networkConfig.type === CityBikeNetworkType.Scooter; + const settings = getSettings(config); + const scooterSettingsOn = + settings.modes.indexOf(TransportMode.Scooter) !== -1; if (leg.mode === 'WALK' || leg.mode === 'BICYCLE_WALK') { modeClassName = leg.mode.toLowerCase(); @@ -77,6 +81,9 @@ function BicycleLeg( legDescription = ( ); @@ -88,8 +95,21 @@ function BicycleLeg( if (leg.mode === 'WALK') { mode = 'CITYBIKE_WALK'; } + + if (leg.mode === 'SCOOTER') { + mode = 'SCOOTER'; + } } - if (bicycleWalkLeg) { + + if (isScooter) { + circleLine = ( + + ); + } else if (bicycleWalkLeg) { const modeClassNames = bicycleWalkLeg.to?.stop ? [modeClassName, bicycleWalkLeg.mode.toLowerCase()] : [bicycleWalkLeg.mode.toLowerCase(), modeClassName]; @@ -203,6 +223,7 @@ function BicycleLeg( stationName={leg.from.name} isScooter={isScooter} vehicleRentalStation={leg.from.vehicleRentalStation} + rentalVehicle={leg.from.rentalVehicle} /> )} {bicycleWalkLeg?.from.stop && ( @@ -246,6 +267,48 @@ function BicycleLeg( )} + {isScooter && !scooterSettingsOn && ( +
+
+
+ + +
+
+ +
+
+
+ + + +
+
+
+ + + +
+
+
+ )}
{stopsDescription} @@ -297,6 +360,15 @@ function BicycleLeg(
)} + {isScooter && ( + + )} ); @@ -314,6 +386,9 @@ BicycleLeg.propTypes = { vehiclesAvailable: PropTypes.number.isRequired, network: PropTypes.string.isRequired, }), + rentalVehicle: PropTypes.shape({ + network: PropTypes.string.isRequired, + }), stop: PropTypes.object, }).isRequired, to: PropTypes.shape({ diff --git a/app/component/CustomizeSearch.js b/app/component/CustomizeSearch.js index 6d73b0539c..8800a2ee82 100644 --- a/app/component/CustomizeSearch.js +++ b/app/component/CustomizeSearch.js @@ -11,10 +11,17 @@ import WalkingOptionsSection from './customizesearch/WalkingOptionsSection'; import AccessibilityOptionSection from './customizesearch/AccessibilityOptionSection'; import TransferOptionsSection from './customizesearch/TransferOptionsSection'; import VehicleRentalStationNetworkSelector from './customizesearch/VehicleRentalStationNetworkSelector'; -import { showModeSettings, useCitybikes } from '../util/modeUtils'; +import ScooterRentalNetworkSelector from './customizesearch/ScooterRentalNetworkSelector'; +import { showModeSettings, useRentalVehiclesOfType } from '../util/modeUtils'; import ScrollableWrapper from './ScrollableWrapper'; import { getDefaultSettings } from '../util/planParamUtil'; -import { getVehicleRentalStationNetworks } from '../util/vehicleRentalUtils'; +import { + getCitybikeRentalStationNetworks, + getScooterRentalNetworks, + CityBikeNetworkType, +} from '../util/vehicleRentalUtils'; +import { getReadMessageIds, setReadMessageIds } from '../store/localStorage'; +import { isKeyboardSelectionEvent } from '../util/browser'; class CustomizeSearch extends React.Component { static contextTypes = { @@ -34,6 +41,19 @@ class CustomizeSearch extends React.Component { defaultSettings = getDefaultSettings(this.context.config); + state = { + showEScooterDisclaimer: !getReadMessageIds().includes( + 'e_scooter_settings_disclaimer', + ), + }; + + handleEScooterDisclaimerClose = () => { + const readMessageIds = getReadMessageIds() || []; + readMessageIds.push('e_scooter_settings_disclaimer'); + setReadMessageIds(readMessageIds); + this.setState({ showEScooterDisclaimer: false }); // ???? + }; + render() { const { config, intl } = this.context; const { onToggleClick, customizedSettings, mobile } = this.props; @@ -112,7 +132,11 @@ class CustomizeSearch extends React.Component { /> - {useCitybikes(config?.cityBike?.networks, config) && ( + {useRentalVehiclesOfType( + config?.cityBike?.networks, + config, + CityBikeNetworkType.CityBike, + ) && (
@@ -127,7 +151,73 @@ class CustomizeSearch extends React.Component {
+
+
+
+
+ )} + {useRentalVehiclesOfType( + config?.cityBike?.networks, + config, + CityBikeNetworkType.Scooter, + ) && ( +
+
+
+ + + +
+ {this.state.showEScooterDisclaimer && ( +
+
+
+ +
{ + if ( + isKeyboardSelectionEvent(e) && + (e.keyCode === 13 || e.keyCode === 32) + ) { + this.handleEScooterDisclaimerClose(); + } + }} + onClick={this.handleEScooterDisclaimerClose} + role="button" + > + +
+
+
+ + ), + }} + /> +
+
{' '} +
+ )} +
diff --git a/app/component/Itinerary.js b/app/component/Itinerary.js index 2a040881fd..9bb42c3cfb 100644 --- a/app/component/Itinerary.js +++ b/app/component/Itinerary.js @@ -166,6 +166,8 @@ export const ModeLeg = ( config, ), ); + } else if (mode === 'SCOOTER') { + networkIcon = 'icon-icon_scooter_rider_white'; } const routeNumber = ( , ); + } else if (leg.mode === 'SCOOTER' && leg.rentedBike) { + const scooterDuration = Math.floor( + (leg.endTime - leg.startTime) / 1000 / 60, + ); + legs.push( + , + ); } else if (leg.mode === 'CAR') { const drivingTime = Math.floor((leg.endTime - leg.startTime) / 1000 / 60); legs.push( diff --git a/app/component/ItineraryDetails.js b/app/component/ItineraryDetails.js index 398bcfd982..ae3b14aa65 100644 --- a/app/component/ItineraryDetails.js +++ b/app/component/ItineraryDetails.js @@ -87,7 +87,7 @@ class ItineraryDetails extends React.Component { carItinerary: ItineraryShape, currentLanguage: PropTypes.string, changeHash: PropTypes.func, - }; + }; static defaultProps = { hideTitle: false, @@ -340,14 +340,14 @@ class ItineraryDetails extends React.Component { > {disclaimers} + /> {config.showRouteInformation && }
{config.showCO2InItinerarySummary && ( @@ -473,6 +473,13 @@ const withRelay = createFragmentContainer( lon stationId } + rentalVehicle { + vehicleId + name + lat + lon + network + } stop { gtfsId code @@ -506,6 +513,13 @@ const withRelay = createFragmentContainer( network vehiclesAvailable } + rentalVehicle { + vehicleId + name + lat + lon + network + } stop { gtfsId code diff --git a/app/component/ItineraryList/ItineraryList.js b/app/component/ItineraryList/ItineraryList.js index ea9a3ef018..9c93ee6f31 100644 --- a/app/component/ItineraryList/ItineraryList.js +++ b/app/component/ItineraryList/ItineraryList.js @@ -42,6 +42,7 @@ function ItineraryList( biking, driving, showRelaxedPlanNotifier, + showRentalVehicleNotifier, separatorPosition, loadingMore, routingErrors, @@ -164,6 +165,32 @@ function ItineraryList(
)} + {showRentalVehicleNotifier && ( +
+ +
+
+ +
+
+ + ), + }} + /> +
+
+
+ )} {loadingMore === spinnerPosition.top && (
@@ -267,6 +294,7 @@ ItineraryList.propTypes = { biking: PropTypes.bool, driving: PropTypes.bool, showRelaxedPlanNotifier: PropTypes.bool, + showRentalVehicleNotifier: PropTypes.bool, separatorPosition: PropTypes.number, loadingMore: PropTypes.string, routingFeedbackPosition: PropTypes.number, @@ -280,6 +308,7 @@ ItineraryList.defaultProps = { biking: false, driving: false, showRelaxedPlanNotifier: false, + showRentalVehicleNotifier: false, separatorPosition: undefined, loadingMore: undefined, routingErrors: [], @@ -364,6 +393,13 @@ const containerComponent = createFragmentContainer(ItineraryList, { vehiclesAvailable network } + rentalVehicle { + vehicleId + name + lat + lon + network + } } to { stop { diff --git a/app/component/ItineraryList/errorCardProperties.js b/app/component/ItineraryList/errorCardProperties.js index c28ad6facf..5f1c82c26d 100644 --- a/app/component/ItineraryList/errorCardProperties.js +++ b/app/component/ItineraryList/errorCardProperties.js @@ -193,6 +193,13 @@ const errorCardProps = [ ...caution, }, }, + { + id: 'no-route-alternative-suggestion', + props: { + bodyId: 'e-scooter-or-taxi-alternative', + ...info, + }, + }, { id: 'no-route-msg', props: { diff --git a/app/component/ItineraryListContainer.js b/app/component/ItineraryListContainer.js index 89eae2ed9e..1b72b8947e 100644 --- a/app/component/ItineraryListContainer.js +++ b/app/component/ItineraryListContainer.js @@ -51,6 +51,7 @@ class ItineraryListContainer extends React.Component { walking: PropTypes.bool, biking: PropTypes.bool, showRelaxedPlanNotifier: PropTypes.bool, + showRentalVehicleNotifier: PropTypes.bool, separatorPosition: PropTypes.number, onLater: PropTypes.func.isRequired, onEarlier: PropTypes.func.isRequired, @@ -71,6 +72,7 @@ class ItineraryListContainer extends React.Component { biking: false, bikeAndParkItineraryCount: 0, showRelaxedPlanNotifier: false, + showRentalVehicleNotifier: false, loadingMore: undefined, driving: false, routingErrors: [], @@ -224,6 +226,7 @@ class ItineraryListContainer extends React.Component { biking, driving, showRelaxedPlanNotifier, + showRentalVehicleNotifier, separatorPosition, loadingMore, routingFeedbackPosition, @@ -269,6 +272,7 @@ class ItineraryListContainer extends React.Component { biking={biking} driving={driving} showRelaxedPlanNotifier={showRelaxedPlanNotifier} + showRentalVehicleNotifier={showRentalVehicleNotifier} separatorPosition={separatorPosition} loadingMore={loadingMore} routingFeedbackPosition={routingFeedbackPosition} @@ -354,6 +358,13 @@ const connectedContainer = createFragmentContainer( vehiclesAvailable network } + rentalVehicle { + vehicleId + name + lat + lon + network + } } to { stop { diff --git a/app/component/ItineraryPage.js b/app/component/ItineraryPage.js index 1a604f8c5f..552963aa38 100644 --- a/app/component/ItineraryPage.js +++ b/app/component/ItineraryPage.js @@ -31,6 +31,7 @@ import { moreQuery, alternativeQuery, viewerQuery, + scooterViewerQuery, } from './ItineraryQueries'; import { getSelectedItineraryIndex, @@ -73,6 +74,8 @@ import { getMapLayerOptions } from '../util/mapLayerUtils'; import ItineraryShape from '../prop-types/ItineraryShape'; import ErrorShape from '../prop-types/ErrorShape'; import RoutingErrorShape from '../prop-types/RoutingErrorShape'; +import { getAllScooterNetworks } from '../util/vehicleRentalUtils'; +import { TransportMode } from '../constants'; const streetHashes = [ streetHash.walk, @@ -125,6 +128,7 @@ function ItineraryPage(props, context) { loading: ALT_LOADING_STATES.UNSET, }); const [relaxState, setRelaxState] = useState({ loading: false }); + const [relaxRentalState, setRelaxRentalState] = useState({ loading: false }); const [settingsState, setSettingsState] = useState({ settingsOpen: false }); const [weatherState, setWeatherState] = useState({ loading: false }); const [topicsState, setTopicsState] = useState(null); @@ -182,6 +186,34 @@ function ItineraryPage(props, context) { ); } + function mergedPlans() { + const scooterItinerary = transitItineraries( + filterItineraries( + props.scooterRentPlan?.itineraries, + TransportMode.Scooter, + ), + )?.[0]; + if (scooterItinerary) { + const mergedRoutingErrors = []; + mergedRoutingErrors.concat( + props.scooterRentPlan?.routingErrors, + props.viewer.plan.routingErrors, + ); + const mergedItineraries = [ + scooterItinerary, + ...props.viewer.plan.itineraries.slice(0, -1), + ].sort((a, b) => { + return a.endTime > b.endTime; + }); + return { + itineraries: mergedItineraries, + routingErrors: mergedRoutingErrors, + }; + } + + return props.viewer.plan; + } + function mapHashToPlan(hash) { switch (hash) { case streetHash.walk: @@ -197,12 +229,16 @@ function ItineraryPage(props, context) { default: if ( !transitItineraries(props.viewer?.plan?.itineraries).length && - !state.settingsChanged && - relaxState.relaxedPlan?.itineraries?.length > 0 + !state.settingsChanged ) { - return relaxState.relaxedPlan; + if (relaxState.relaxedPlan?.itineraries?.length > 0) { + return relaxState.relaxedPlan; + } + if (relaxRentalState.relaxedScooterPlan?.itineraries?.length > 0) { + return relaxRentalState.relaxedScooterPlan; + } } - return props.viewer.plan; + return mergedPlans(); } } @@ -361,6 +397,42 @@ function ItineraryPage(props, context) { }); } + function makeRelaxedScooterQuery() { + if (!hasValidFromTo()) { + return; + } + setRelaxRentalState({ loading: true }); + const allScooterNetworks = getAllScooterNetworks(context.config); + const planParams = getPlanParams( + context.config, + props.match, + true, // relax settings + true, // force scooter query + ); + + const tunedParams = { + ...planParams, + allowedBikeRentalNetworks: allScooterNetworks, + }; + fetchQuery(props.relayEnvironment, moreQuery, tunedParams, { + force: true, + }) + .toPromise() + .then(result => { + const relaxedScooterPlan = { + ...result.plan, + itineraries: transitItineraries(result.plan.itineraries), + }; + setRelaxRentalState({ + relaxedScooterPlan, + earlierItineraries: [], + laterItineraries: [], + separatorPosition: undefined, + loadingRelaxedScooter: false, + }); + }); + } + const onLater = (itineraries, reversed) => { addAnalyticsEvent({ event: 'sendMatomoEvent', @@ -672,11 +744,18 @@ function ItineraryPage(props, context) { useEffect(() => { // eslint-disable-next-line react/no-did-update-set-state setState({ ...state, ...emptyState }); + const settings = getSettings(context.config); if (!props.loading) { ariaRef.current = 'itinerary-page.itineraries-loaded'; if (settingsLimitRouting(context.config) && !state.settingsChanged) { makeRelaxedQuery(); } + if ( + !settings.allowedScooterRentalNetworks.length && + !relaxState.relaxedPlan?.itineraries?.length + ) { + makeRelaxedScooterQuery(); + } makeAlternativeQuery(); } else { ariaRef.current = 'itinerary-page.loading-itineraries'; @@ -739,6 +818,7 @@ function ItineraryPage(props, context) { altState.bikeTransitPlan, altState.parkRidePlan, relaxState.relaxedPlan, + relaxRentalState.scooterRentPlan, props.match.location.state?.selectedItineraryIndex, ]); @@ -984,12 +1064,15 @@ function ItineraryPage(props, context) { (props.loading || state.loading || (relaxState.loading && hasNoTransitItineraries) || + (relaxRentalState.loadingRelaxedScooter && hasNoTransitItineraries) || waitAlternatives || (streetHashes.includes(hash) && altState.loading === ALT_LOADING_STATES.LOADING)) && // viewing unfinished alt plan !error; const showRelaxedPlanNotifier = selectedPlan === relaxState.relaxedPlan; + const showRentalVehicleNotifier = + selectedPlan === relaxRentalState.relaxedScooterPlan; const settingsNotification = !showRelaxedPlanNotifier && // show only on notifier about limitations settingsLimitRouting(context.config) && @@ -1032,6 +1115,7 @@ function ItineraryPage(props, context) { routingFeedbackPosition={state.routingFeedbackPosition} topNote={state.topNote} bottomNote={state.bottomNote} + showRentalVehicleNotifier={showRentalVehicleNotifier} /> ); @@ -1162,6 +1246,10 @@ ItineraryPage.propTypes = { start: PropTypes.number.isRequired, end: PropTypes.number.isRequired, }).isRequired, + scooterRentPlan: PropTypes.shape({ + routingErrors: PropTypes.arrayOf(RoutingErrorShape), + itineraries: PropTypes.arrayOf(ItineraryShape), + }).isRequired, // not really content: PropTypes.node, map: PropTypes.shape({ type: PropTypes.func.isRequired, @@ -1215,6 +1303,7 @@ const containerComponent = createRefetchContainer( end } `, + scooterRentPlan: scooterViewerQuery, }, planQuery, ); diff --git a/app/component/ItineraryPageContainer.js b/app/component/ItineraryPageContainer.js index fde0b81caa..30c5d9ea34 100644 --- a/app/component/ItineraryPageContainer.js +++ b/app/component/ItineraryPageContainer.js @@ -35,6 +35,7 @@ export default function ItineraryPageContainer({ content, match }, { config }) { viewer={{ plan: {} }} serviceTimeRange={validateServiceTimeRange()} loading={false} + scooterRentPlan={{ plan: {} }} /> ) : ( ); }} diff --git a/app/component/ItineraryQueries.js b/app/component/ItineraryQueries.js index 8e4f8bcd5a..a297863d54 100644 --- a/app/component/ItineraryQueries.js +++ b/app/component/ItineraryQueries.js @@ -24,6 +24,9 @@ export const planQuery = graphql` $unpreferred: InputUnpreferred $allowedBikeRentalNetworks: [String] $modeWeight: InputModeWeight + $scooterRentModes: [TransportMode!] + $allowedScooterRentalNetworks: [String] + $shouldMakeScooterRentQuery: Boolean! ) { viewer { ...ItineraryQueries_Plan_Viewer @@ -53,6 +56,100 @@ export const planQuery = graphql` serviceTimeRange { ...ItineraryPage_serviceTimeRange } + scooterRentPlan: plan( + fromPlace: $fromPlace + toPlace: $toPlace + numItineraries: $numItineraries + transportModes: $scooterRentModes + date: $date + time: $time + walkReluctance: $walkReluctance + walkBoardCost: $walkBoardCost + minTransferTime: $minTransferTime + walkSpeed: $walkSpeed + wheelchair: $wheelchair + arriveBy: $arriveBy + transferPenalty: $transferPenalty + bikeSpeed: $bikeSpeed + optimize: $optimize + unpreferred: $unpreferred + allowedBikeRentalNetworks: $allowedScooterRentalNetworks + modeWeight: $modeWeight + ) @include(if: $shouldMakeScooterRentQuery) { + ...ItineraryListContainer_plan + ...ItineraryDetails_plan + routingErrors { + code + inputField + } + itineraries { + startTime + endTime + ...ItineraryDetails_itinerary + ...ItineraryListContainer_itineraries + emissionsPerPerson { + co2 + } + legs { + mode + ...ItineraryLine_legs + transitLeg + legGeometry { + points + } + route { + gtfsId + type + shortName + } + trip { + gtfsId + directionId + occupancy { + occupancyStatus + } + stoptimesForDate { + scheduledDeparture + pickupType + } + pattern { + ...RouteLine_pattern + } + } + from { + name + lat + lon + stop { + gtfsId + zoneId + } + vehicleRentalStation { + stationId + vehiclesAvailable + network + } + rentalVehicle { + vehicleId + name + lat + lon + network + } + } + to { + stop { + gtfsId + zoneId + } + bikePark { + bikeParkId + name + } + } + } + } + } } `; @@ -472,6 +569,13 @@ export const moreQuery = graphql` vehiclesAvailable network } + rentalVehicle { + vehicleId + name + lat + lon + network + } } to { stop { @@ -586,6 +690,13 @@ export const viewerQuery = graphql` vehiclesAvailable network } + rentalVehicle { + vehicleId + name + lat + lon + network + } } to { stop { @@ -602,3 +713,57 @@ export const viewerQuery = graphql` } } `; + +export const scooterViewerQuery = graphql` + fragment ItineraryQueries_Scooter_Query on QueryType + @argumentDefinitions( + fromPlace: { type: "String!" } + toPlace: { type: "String!" } + numItineraries: { type: "Int!" } + modes: { type: "[TransportMode!]" } + date: { type: "String!" } + time: { type: "String!" } + walkReluctance: { type: "Float" } + walkBoardCost: { type: "Int" } + minTransferTime: { type: "Int" } + walkSpeed: { type: "Float" } + wheelchair: { type: "Boolean" } + ticketTypes: { type: "[String]" } + arriveBy: { type: "Boolean" } + transferPenalty: { type: "Int" } + bikeSpeed: { type: "Float" } + optimize: { type: "OptimizeType" } + unpreferred: { type: "InputUnpreferred" } + modeWeight: { type: "InputModeWeight" } + allowedScooterRentalNetworks: { type: "[String]" } + ) { + viewer { + ...ItineraryQueries_Plan_Viewer + @arguments( + fromPlace: $fromPlace + toPlace: $toPlace + numItineraries: $numItineraries + modes: $modes + date: $date + time: $time + walkReluctance: $walkReluctance + walkBoardCost: $walkBoardCost + minTransferTime: $minTransferTime + walkSpeed: $walkSpeed + wheelchair: $wheelchair + ticketTypes: $ticketTypes + arriveBy: $arriveBy + transferPenalty: $transferPenalty + bikeSpeed: $bikeSpeed + optimize: $optimize + unpreferred: $unpreferred + allowedBikeRentalNetworks: $allowedScooterRentalNetworks + modeWeight: $modeWeight + ) + } + + serviceTimeRange { + ...ItineraryPage_serviceTimeRange + } + } +`; diff --git a/app/component/RentalVehicle.js b/app/component/RentalVehicle.js new file mode 100644 index 0000000000..e20f6a0aaa --- /dev/null +++ b/app/component/RentalVehicle.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Icon from './Icon'; +import { + getVehicleRentalStationNetworkIcon, + getVehicleRentalStationNetworkConfig, +} from '../util/vehicleRentalUtils'; + +const RentalVehicle = ({ rentalVehicle }, { config }) => { + const disabled = !rentalVehicle.operative; + + const vehicleIcon = getVehicleRentalStationNetworkIcon( + getVehicleRentalStationNetworkConfig(rentalVehicle.network, config), + disabled, + ); + return ( +
+ +
+ ); +}; + +RentalVehicle.contextTypes = { + config: PropTypes.object.isRequired, +}; +RentalVehicle.propTypes = { + rentalVehicle: PropTypes.shape({ + vehiclesAvailable: PropTypes.number.isRequired, + spacesAvailable: PropTypes.number.isRequired, + capacity: PropTypes.number.isRequired, + network: PropTypes.string, + operative: PropTypes.bool.isRequired, + }).isRequired, +}; +export default RentalVehicle; diff --git a/app/component/RentalVehicleContent.js b/app/component/RentalVehicleContent.js new file mode 100644 index 0000000000..4c2dace899 --- /dev/null +++ b/app/component/RentalVehicleContent.js @@ -0,0 +1,113 @@ +import PropTypes from 'prop-types'; +import React, { useState, useEffect } from 'react'; +import { createFragmentContainer, graphql } from 'react-relay'; +import connectToStores from 'fluxible-addons-react/connectToStores'; +import { FormattedMessage } from 'react-intl'; +import { routerShape, RedirectException } from 'found'; +import Icon from './Icon'; +import withBreakpoint from '../util/withBreakpoint'; +import { + getVehicleRentalStationNetworkIcon, + getVehicleRentalStationNetworkConfig, +} from '../util/vehicleRentalUtils'; +import { isBrowser } from '../util/browser'; +import { PREFIX_RENTALVEHICLES } from '../util/path'; +import VehicleRentalLeg from './VehicleRentalLeg'; +import BackButton from './BackButton'; + +const RentalVehicleContent = ( + { rentalVehicle, breakpoint, router, error }, + { config }, +) => { + const [isClient, setClient] = useState(false); + + useEffect(() => { + setClient(true); + }); + + // throw error in client side relay query fails + if (isClient && error && !rentalVehicle) { + throw error.message; + } + + if (!rentalVehicle && !error) { + if (isBrowser) { + router.replace(`/${PREFIX_RENTALVEHICLES}`); + } else { + throw new RedirectException(`/${PREFIX_RENTALVEHICLES}`); + } + return null; + } + + const vehicleIcon = getVehicleRentalStationNetworkIcon( + getVehicleRentalStationNetworkConfig(rentalVehicle.network, config), + !rentalVehicle.operative, + ); + + return ( +
+
+
+ +
+ {breakpoint === 'large' && ( + + )} +
+

{rentalVehicle.network}

+
+ +
+
+
+
+ +
+ +
+
+
+ ); +}; + +RentalVehicleContent.propTypes = { + rentalVehicle: PropTypes.any.isRequired, + breakpoint: PropTypes.string.isRequired, + router: routerShape.isRequired, + error: PropTypes.object, +}; + +RentalVehicleContent.contextTypes = { + config: PropTypes.object.isRequired, +}; + +const RentalVehicleContentWithBreakpoint = withBreakpoint(RentalVehicleContent); + +const connectedComponent = connectToStores( + RentalVehicleContentWithBreakpoint, + ['PreferencesStore'], + context => ({ + language: context.getStore('PreferencesStore').getLanguage(), + }), +); + +const containerComponent = createFragmentContainer(connectedComponent, { + /* mitä muita tietoja rentalvehicleltä */ + rentalVehicle: graphql` + fragment RentalVehicleContent_rentalVehicle on RentalVehicle { + lat + lon + name + network + vehicleId + } + `, +}); + +export { + containerComponent as default, + RentalVehicleContentWithBreakpoint as Component, +}; diff --git a/app/component/RentalVehiclePageMapContainer.js b/app/component/RentalVehiclePageMapContainer.js new file mode 100644 index 0000000000..bb27a4b7e7 --- /dev/null +++ b/app/component/RentalVehiclePageMapContainer.js @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { createFragmentContainer, graphql } from 'react-relay'; +import StopPageMap from './map/StopPageMap'; + +const RentalVehiclePageMapContainer = ({ rentalVehicle }) => { + if (!rentalVehicle) { + return false; + } + return ; +}; + +RentalVehiclePageMapContainer.contextTypes = { + config: PropTypes.object.isRequired, +}; + +RentalVehiclePageMapContainer.propTypes = { + rentalVehicle: PropTypes.shape({ + lat: PropTypes.number.isRequired, + lon: PropTypes.number.isRequired, + name: PropTypes.string, + }), +}; + +RentalVehiclePageMapContainer.defaultProps = { + rentalVehicle: undefined, +}; + +const containerComponent = createFragmentContainer( + RentalVehiclePageMapContainer, + { + rentalVehicle: graphql` + fragment RentalVehiclePageMapContainer_rentalVehicle on RentalVehicle { + lat + lon + name + } + `, + }, +); + +export { + containerComponent as default, + RentalVehiclePageMapContainer as Component, +}; diff --git a/app/component/RouteNumber.js b/app/component/RouteNumber.js index d05aad1707..702b1e54bb 100644 --- a/app/component/RouteNumber.js +++ b/app/component/RouteNumber.js @@ -5,12 +5,14 @@ import { intlShape } from 'react-intl'; import IconWithBigCaution from './IconWithBigCaution'; import IconWithIcon from './IconWithIcon'; import Icon from './Icon'; +import { TransportMode } from '../constants'; const LONG_ROUTE_NUMBER_LENGTH = 6; function RouteNumber(props, context) { const mode = props.mode.toLowerCase(); const { alertSeverityLevel, color, withBicycle, text } = props; + const isScooter = mode === TransportMode.Scooter.toLowerCase(); // 'scooter'; const textIsText = typeof text === 'string'; // can be also react node const longText = text && textIsText && text.length >= LONG_ROUTE_NUMBER_LENGTH; @@ -149,13 +151,17 @@ function RouteNumber(props, context) { )}
)} - {!context.config?.hideWalkLegDurationSummary && + {((!context.config?.hideWalkLegDurationSummary && props.isTransitLeg === false && - props.duration > 0 && ( -
- {props.duration} -
- )} + props.duration > 0) || + isScooter) && ( +
+ {props.duration} +
+ )} + {isScooter && ( + + )} {props.occupancyStatus && ( @@ -220,6 +226,10 @@ RouteNumber.defaultProps = { isTransitLeg: false, renderModeIcons: false, withBicycle: false, + duration: undefined, + appendClass: undefined, + occupancyStatus: undefined, + color: undefined, }; RouteNumber.contextTypes = { diff --git a/app/component/StopsNearYouPage.js b/app/component/StopsNearYouPage.js index 08fdcaa7f4..2674f368ff 100644 --- a/app/component/StopsNearYouPage.js +++ b/app/component/StopsNearYouPage.js @@ -217,9 +217,13 @@ class StopsNearYouPage extends React.Component { let placeTypes = ['STOP', 'STATION']; let modes = [mode]; if (mode === 'CITYBIKE') { - placeTypes = 'BICYCLE_RENT'; + placeTypes = 'VEHICLE_RENT'; modes = ['BICYCLE']; } + if (mode === 'SCOOTER') { + placeTypes = 'VEHICLE_RENT'; + modes = ['SCOOTER']; + } const prioritizedStops = this.context.config.prioritizedStopsNearYou[mode.toLowerCase()] || []; return { diff --git a/app/component/VehicleRentalLeg.js b/app/component/VehicleRentalLeg.js index 5150a49fde..253049b360 100644 --- a/app/component/VehicleRentalLeg.js +++ b/app/component/VehicleRentalLeg.js @@ -13,7 +13,7 @@ import { import withBreakpoint from '../util/withBreakpoint'; import Icon from './Icon'; -import { PREFIX_BIKESTATIONS } from '../util/path'; +import { PREFIX_BIKESTATIONS, PREFIX_RENTALVEHICLES } from '../util/path'; import { getVehicleAvailabilityTextColor, getVehicleAvailabilityIndicatorColor, @@ -27,88 +27,126 @@ function VehicleRentalLeg( vehicleRentalStation, returnBike = false, breakpoint, + rentalVehicle, }, { config, intl }, ) { - if (!vehicleRentalStation) { + if (!vehicleRentalStation && !isScooter) { return null; } + const network = vehicleRentalStation?.network || rentalVehicle?.network; // eslint-disable-next-line no-nested-ternary - const id = returnBike - ? isScooter - ? 'return-scooter-to' - : 'return-cycle-to' - : isScooter - ? 'rent-scooter-at' - : 'rent-cycle-at'; + const rentMessageId = isScooter ? 'rent-e-scooter-at' : 'rent-cycle-at'; + const returnMessageId = isScooter ? 'return-e-scooter-to' : 'return-cycle-to'; + const id = returnBike ? returnMessageId : rentMessageId; const legDescription = ( ); const vehicleIcon = getVehicleRentalStationNetworkIcon( - getVehicleRentalStationNetworkConfig(vehicleRentalStation.network, config), - ); - const availabilityIndicatorColor = getVehicleAvailabilityIndicatorColor( - vehicleRentalStation.vehiclesAvailable, - config, - ); - const availabilityTextColor = getVehicleAvailabilityTextColor( - vehicleRentalStation.vehiclesAvailable, - config, + getVehicleRentalStationNetworkConfig(network, config), ); + const availabilityIndicatorColor = vehicleRentalStation + ? getVehicleAvailabilityIndicatorColor( + vehicleRentalStation.vehiclesAvailable, + config, + ) + : null; + const availabilityTextColor = vehicleRentalStation + ? getVehicleAvailabilityTextColor( + vehicleRentalStation.vehiclesAvailable, + config, + ) + : null; const mobileReturn = breakpoint === 'small' && returnBike; - const vehicleCapacity = getVehicleCapacity( - config, - vehicleRentalStation?.network, + const vehicleCapacity = vehicleRentalStation + ? getVehicleCapacity(config, vehicleRentalStation?.network) + : null; + const scooterHeadsign = ( + ); + const link = isScooter + ? `/${PREFIX_RENTALVEHICLES}/${rentalVehicle?.vehicleId}` // TO DO: link from data + : `/${PREFIX_BIKESTATIONS}/${vehicleRentalStation?.stationId}`; return ( <>
{legDescription}
-
-
-
- -
-
- {stationName} - - {intl.formatMessage({ - id: 'citybike-station-no-id', - defaultMessage: 'Bike station', - })} - {hasStationCode(vehicleRentalStation) && ( - - {getIdWithoutFeed(vehicleRentalStation.stationId)} + {(!isScooter || (isScooter && !returnBike)) && ( +
+
+
+ {isScooter ? ( + + ) : ( + + )} +
+
+ + + {isScooter ? scooterHeadsign : stationName} + + + + {!isScooter && ( + + {intl.formatMessage({ + id: 'citybike-station-no-id', + defaultMessage: 'Bike station', + })} + {vehicleRentalStation && + hasStationCode(vehicleRentalStation) && ( + + {getIdWithoutFeed(vehicleRentalStation?.stationId)} + + )} )} - +
+ {isScooter ? ( +
+ + + +
+ ) : ( +
+ + + +
+ )}
-
- - - -
-
+ )} ); } @@ -119,6 +157,7 @@ VehicleRentalLeg.propTypes = { isScooter: PropTypes.bool, returnBike: PropTypes.bool, breakpoint: PropTypes.string, + rentalVehicle: PropTypes.object, }; VehicleRentalLeg.contextTypes = { config: PropTypes.object.isRequired, diff --git a/app/component/WalkLeg.js b/app/component/WalkLeg.js index 1c751c41bc..97b0af6974 100644 --- a/app/component/WalkLeg.js +++ b/app/component/WalkLeg.js @@ -40,12 +40,12 @@ function WalkLeg( const fromMode = (leg[toOrFrom].stop && leg[toOrFrom].stop.vehicleMode) || ''; const isFirstLeg = i => i === 0; const [address, place] = splitStringToAddressAndPlace(leg[toOrFrom].name); + const network = + previousLeg?.[toOrFrom]?.vehicleRentalStation?.network || + previousLeg?.[toOrFrom]?.rentalVehicle?.network; const networkType = getVehicleRentalStationNetworkConfig( - previousLeg && - previousLeg.rentedBike && - previousLeg[toOrFrom].vehicleRentalStation && - previousLeg[toOrFrom].vehicleRentalStation.network, + previousLeg?.rentedBike && network, config, ).type; @@ -171,6 +171,7 @@ function WalkLeg( stationName={leg[toOrFrom].name} vehicleRentalStation={leg[toOrFrom].vehicleRentalStation} returnBike + rentalVehicle={leg.from.rentalVehicle} /> ) : ( leg[toOrFrom].name @@ -254,6 +255,9 @@ const walkLegShape = PropTypes.shape({ vehicleRentalStation: PropTypes.shape({ network: PropTypes.string, }), + rentalVehicle: PropTypes.shape({ + network: PropTypes.string.isRequired, + }), }).isRequired, to: PropTypes.shape({ name: PropTypes.string.isRequired, @@ -267,6 +271,9 @@ const walkLegShape = PropTypes.shape({ vehicleRentalStation: PropTypes.shape({ network: PropTypes.string, }), + rentalVehicle: PropTypes.shape({ + network: PropTypes.string.isRequired, + }), }).isRequired, mode: PropTypes.string.isRequired, rentedBike: PropTypes.bool, diff --git a/app/component/customize-search.scss b/app/component/customize-search.scss index 4efa13ce90..c95a71f9fc 100644 --- a/app/component/customize-search.scss +++ b/app/component/customize-search.scss @@ -480,3 +480,73 @@ } } } + +.e-scooter-disclaimer-container { + border-bottom: 1px solid #e3e3e3; + + .e-scooter-disclaimer { + min-height: 120px; + border-radius: 5px; + background-color: #ffefe8; + padding: 16px 15px 16px 18px; + width: 354px; + margin-left: 14.72px; + margin-top: 12px; + margin-bottom: 17px; + font-family: $font-family; + font-size: $font-size-small; + font-weight: normal; + line-height: 18px; + letter-spacing: -0.03em; + + .disclaimer-header { + display: flex; + font-size: $font-size-normal; + font-weight: $font-weight-medium; + line-height: 22px; + margin: 0 0 4px; + color: #333; + } + + .disclaimer-content { + display: flex; + font-size: $font-size-normal; + margin-bottom: 8px; + color: #666; + line-height: 18px; + letter-spacing: -0.03em; + } + + .external-link { + color: #007ac9; + text-decoration: none; + font-weight: $font-weight-medium; + + .icon-container { + padding-left: 7px; + vertical-align: text-top; + } + } + + .disclaimer-close-button-container { + margin-left: 10px; + background-color: $primary-color; + width: fit-content; + height: fit-content; + color: white; + text-decoration: none; + border-radius: 25px; + padding: 3px 25px 3px 25px; + } + + .disclaimer-close-button { + font-weight: $font-weight-medium; + } + + .disclaimer-close { + margin-left: auto; + cursor: pointer; + font-weight: $font-weight-medium; + } + } +} diff --git a/app/component/customizesearch/ScooterRentalNetworkSelector.js b/app/component/customizesearch/ScooterRentalNetworkSelector.js new file mode 100644 index 0000000000..c4c0b7d47d --- /dev/null +++ b/app/component/customizesearch/ScooterRentalNetworkSelector.js @@ -0,0 +1,95 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import xor from 'lodash/xor'; +import Toggle from './Toggle'; +import { saveRoutingSettings } from '../../action/SearchSettingsActions'; +import Icon from '../Icon'; +import { + mapDefaultNetworkProperties, + getVehicleRentalStationNetworkName, + getVehicleRentalStationNetworkConfig, + updateVehicleNetworks, + getScooterRentalNetworks, +} from '../../util/vehicleRentalUtils'; +import { getModes } from '../../util/modeUtils'; +import { TransportMode } from '../../constants'; + +const ScooterRentalNetworkSelector = ( + { currentOptions }, + { config, getStore, executeAction }, +) => ( + + {mapDefaultNetworkProperties(config) + .filter(network => network.type === TransportMode.Scooter.toLowerCase()) + .map(network => ( +
+ +
+ ))} +
+); + +ScooterRentalNetworkSelector.propTypes = { + currentOptions: PropTypes.array.isRequired, +}; + +ScooterRentalNetworkSelector.contextTypes = { + config: PropTypes.object.isRequired, + getStore: PropTypes.func.isRequired, + executeAction: PropTypes.func.isRequired, +}; + +export default ScooterRentalNetworkSelector; diff --git a/app/component/customizesearch/TransportModesSection.js b/app/component/customizesearch/TransportModesSection.js index 281df83407..7c7c9142b7 100644 --- a/app/component/customizesearch/TransportModesSection.js +++ b/app/component/customizesearch/TransportModesSection.js @@ -32,7 +32,7 @@ const TransportModesSection = (
{transportModes - .filter(mode => mode !== 'CITYBIKE') + .filter(mode => mode !== 'CITYBIKE' && mode !== 'SCOOTER') .map(mode => (
( - {mapDefaultNetworkProperties(config).map(network => ( -
- +
+ ))}
); diff --git a/app/component/itinerary-summary.scss b/app/component/itinerary-summary.scss index e009a85bf8..7b141f8220 100644 --- a/app/component/itinerary-summary.scss +++ b/app/component/itinerary-summary.scss @@ -236,7 +236,7 @@ } .leg { - min-width: 24px; + min-width: max-content; // forces the leg block to fit all the icons width: calc(var(--width) - (var(--minus)) - 2px + var(--plus)); margin: 0 2px 0 0; justify-content: center; @@ -369,7 +369,8 @@ &.bicycle, &.wait, &.bicycle_walk, - &.car { + &.car, + &.scooter { display: block; overflow: hidden; min-width: unset; @@ -382,7 +383,8 @@ &.bicycle, &.wait, &.bicycle_walk, - &.car { + &.car, + &.scooter { float: left; display: flex; justify-content: center; @@ -430,6 +432,20 @@ } } + &.scooter { + .icon { + width: 20px; + margin-left: 1px; + } + } + + .phone-icon { + .icon { + color: white; + height: 16px; + } + } + &.citybike { .icon { width: 24px; @@ -497,7 +513,8 @@ } } - &.car { + &.car, + &.scooter { .leg-duration { color: $white; } diff --git a/app/component/itinerary.scss b/app/component/itinerary.scss index 2d3649afa6..9e1f6b54ee 100644 --- a/app/component/itinerary.scss +++ b/app/component/itinerary.scss @@ -75,6 +75,10 @@ $itinerary-tab-switch-height: 48px; &.call { @include getCircleSvg($bus-color, $fill); } + + &.scooter { + @include getCircleSvg($scooter-color, $fill); + } } .itinerary-list-container { @@ -889,6 +893,10 @@ $itinerary-tab-switch-height: 48px; &.call { @include setModeCircles($bus-color); } + + &.scooter { + @include setModeCircles($scooter-color); + } } } @@ -965,6 +973,7 @@ $itinerary-tab-switch-height: 48px; } } + &.scooter, &.bicycle, &.bicycle_walk { .route-number { @@ -974,6 +983,7 @@ $itinerary-tab-switch-height: 48px; } } + &.scooter, &.walk, &.bicycle { :last-child { @@ -1147,6 +1157,19 @@ $itinerary-tab-switch-height: 48px; } } + &.scooter { + border-left: 6px solid; + border-radius: 3px; + height: 50%; + left: 8px; + + &.bottom { + height: 15%; + top: 85%; + overflow: hidden; + } + } + &.walk { height: 48%; @@ -1645,6 +1668,10 @@ $itinerary-tab-switch-height: 48px; top: 34%; } + .link-to-e-scooter-operator { + margin: auto 10px auto auto; + } + .citybike-itinerary { padding: 7px; @@ -1659,6 +1686,10 @@ $itinerary-tab-switch-height: 48px; padding: 2px 0 3px 6px; } + .scooter-headsign { + padding: 6px 0 3px 6px; + } + span.itinerary-stop-code { margin-left: 5px; padding: 0 6px; @@ -2771,6 +2802,7 @@ $font-print-decrease: 0.7; .itinerary-center-right { display: none; + &.scooter, &.walk, &.bicycle_walk, &.bicycle { @@ -2945,3 +2977,34 @@ $font-print-decrease: 0.7; text-decoration: none; margin-bottom: 30px; } + +.e-scooter-or-taxi-info { + background-color: $infobox-color-generic-blue; + margin-top: 30px; + + .info-icon { + margin-right: 1em; + margin-top: 0.5em; + color: $info-icon-blue; + width: 2em; + height: 2em; + } + + .e-scooter-or-taxi-info-header { + display: flex; + font-size: $font-size-normal; + font-weight: $font-weight-medium; + line-height: 22px; + margin: 0 0 4px; + color: #333; + } + + .e-scooter-or-taxi-info-content { + display: flex; + font-size: $font-size-normal; + margin-bottom: 8px; + color: #666; + line-height: 18px; + letter-spacing: -0.03em; + } +} diff --git a/app/component/map/ItineraryLine.js b/app/component/map/ItineraryLine.js index d4a086dc2f..c45999706f 100644 --- a/app/component/map/ItineraryLine.js +++ b/app/component/map/ItineraryLine.js @@ -71,7 +71,7 @@ class ItineraryLine extends React.Component { const interliningWithRoute = interliningLines.join(' / '); - if (leg.rentedBike && leg.mode !== 'WALK') { + if (leg.rentedBike && leg.mode !== 'WALK' && leg.mode !== 'SCOOTER') { mode = 'CITYBIKE'; } @@ -82,6 +82,13 @@ class ItineraryLine extends React.Component { let middle = getMiddleOf(geometry); let { to, endTime } = leg; + const rentalId = + leg.from.vehicleRentalStation?.stationId || + leg.from.rentalVehicle?.vehicleId; + const rentalNetwork = + leg.from.vehicleRentalStation?.network || + leg.from.rentalVehicle?.network; + if (interliningLegs.length > 0) { // merge the geometries of legs where user can wait in the vehicle and find the middle point // of the new geometry @@ -142,9 +149,17 @@ class ItineraryLine extends React.Component { if (leg.from.vertexType === 'BIKESHARE') { objs.push( , ); @@ -264,6 +279,13 @@ export default createFragmentContainer(ItineraryLine, { network vehiclesAvailable } + rentalVehicle { + vehicleId + name + lat + lon + network + } stop { gtfsId code diff --git a/app/component/map/StopsNearYouMap.js b/app/component/map/StopsNearYouMap.js index 7eb35acb04..b0aa062819 100644 --- a/app/component/map/StopsNearYouMap.js +++ b/app/component/map/StopsNearYouMap.js @@ -170,7 +170,7 @@ function StopsNearYouMap( const prevPlace = useRef(); const prevMode = useRef(); const { mode } = match.params; - const isTransitMode = mode !== 'CITYBIKE'; + const isTransitMode = mode !== 'CITYBIKE' && mode !== 'SCOOTER'; const walkRoutingThreshold = mode === 'RAIL' || mode === 'SUBWAY' || mode === 'FERRY' ? 3000 : 1500; const { environment } = relay; diff --git a/app/component/map/non-tile-layer/VehicleMarker.js b/app/component/map/non-tile-layer/VehicleMarker.js index 98304a8cdf..135d0cceaf 100644 --- a/app/component/map/non-tile-layer/VehicleMarker.js +++ b/app/component/map/non-tile-layer/VehicleMarker.js @@ -15,7 +15,7 @@ import { getVehicleAvailabilityTextColor, } from '../../../util/legUtils'; -import { PREFIX_BIKESTATIONS } from '../../../util/path'; +import { PREFIX_BIKESTATIONS, PREFIX_RENTALVEHICLES } from '../../../util/path'; let L; @@ -40,8 +40,17 @@ export default class VehicleMarker extends React.Component { static propTypes = { showBikeAvailability: PropTypes.bool, - station: PropTypes.object.isRequired, + rental: PropTypes.objectOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + lat: PropTypes.string.isRequired, + lon: PropTypes.string.isRequired, + network: PropTypes.string.isRequired, + vehiclesAvailable: PropTypes.string, + }), + ).isRequired, transit: PropTypes.bool, + mode: PropTypes.string, }; static contextTypes = { @@ -51,21 +60,20 @@ export default class VehicleMarker extends React.Component { static defaultProps = { showBikeAvailability: false, + transit: false, + mode: 'BICYCLE', }; - handleClick() { - const { stationId } = this.props.station; - this.context.router.push( - `/${PREFIX_BIKESTATIONS}/${encodeURIComponent(stationId)}`, - ); - } + handleClick = (id, prefix) => { + this.context.router.push(`/${prefix}/${encodeURIComponent(id)}`); + }; getIcon = zoom => { - const { showBikeAvailability, station, transit } = this.props; + const { showBikeAvailability, rental, transit } = this.props; const { config } = this.context; - const vehicleCapacity = getVehicleCapacity(config, station.network); + const vehicleCapacity = getVehicleCapacity(config, rental?.network); const iconName = `${getVehicleRentalStationNetworkIcon( - getVehicleRentalStationNetworkConfig(station.network, config), + getVehicleRentalStationNetworkConfig(rental.network, config), )}-lollipop`; return !transit && zoom <= config.stopsSmallMaxZoom @@ -81,16 +89,16 @@ export default class VehicleMarker extends React.Component { img: iconName, className: 'city-bike-medium-size', badgeFill: getVehicleAvailabilityIndicatorColor( - station.vehiclesAvailable, + rental?.vehiclesAvailable, config, ), badgeTextFill: getVehicleAvailabilityTextColor( - station.vehiclesAvailable, + rental?.vehiclesAvailable, config, ), badgeText: vehicleCapacity !== BIKEAVL_UNKNOWN - ? station.vehiclesAvailable + ? rental?.vehiclesAvailable : null, }) : Icon.asString({ @@ -109,12 +117,19 @@ export default class VehicleMarker extends React.Component { return ( + this.handleClick( + this.props.rental.id, + this.props.mode === 'SCOOTER' + ? PREFIX_RENTALVEHICLES + : PREFIX_BIKESTATIONS, + ) + } getIcon={this.getIcon} - id={this.props.station.stationId} + id={this.props.rental?.id} /> ); } diff --git a/app/component/rental-vehicle-content.scss b/app/component/rental-vehicle-content.scss new file mode 100644 index 0000000000..83ccfba93e --- /dev/null +++ b/app/component/rental-vehicle-content.scss @@ -0,0 +1,226 @@ +.mobile { + .scooter-page-container { + height: calc(100% - 20rem); + padding: 18px 1.563em 2em; + background-color: white; + border-radius: 15px 15px 0 0; + box-shadow: 0 -5px 5px 0 rgba(0, 0, 0, 0.2); + position: relative; + top: -15px; + margin: 0; + + .scooter-box { + margin-top: 19px; + } + + .scooter-header { + padding-left: 15px; + border: none; + padding-top: 0; + + .header { + h1 { + font-size: 1.25rem; + line-height: 1.2; + margin: 0; + } + + .scooter-sub-header { + font-size: 0.88rem; + padding: 0; + } + } + } + + .scooter-content-container { + padding: 0 0 0 0; + margin-bottom: 13px; + + .icon-container { + .icon { + width: 40px; + height: 40px; + } + } + } + } +} + +.scooter-page-container { + margin: 0 3.75em; + + .scooter-box { + min-height: 120px; + border-radius: 10px; + border: solid 1px #ddd; + padding: 16px 21px 16px 18px; + width: 100%; + margin-top: 24px; + font-family: $font-family; + font-size: $font-size-normal; + font-weight: normal; + line-height: 18px; + letter-spacing: -0.03em; + + .disclaimer-header { + display: flex; + font-size: $font-size-large; + font-weight: $font-weight-medium; + line-height: 22px; + margin: 0 0 4px; + color: #333; + } + + .disclaimer-content { + display: block; + font-size: $font-size-normal; + margin-bottom: 8px; + color: #666; + line-height: 18px; + letter-spacing: -0.03em; + + .itinerary-transit-leg-route-bike { + display: flex; + background-color: #f4f4f5; + border-radius: 5px; + border: 1px solid #ddd; + margin-top: 16px; + color: $black; + + .link-to-stop { + right: 10px; + position: absolute; + top: 34%; + } + + .link-to-e-scooter-operator { + margin: auto 10px auto auto; + } + + .citybike-itinerary { + padding: 7px; + + .citybike-itinerary-text-container { + display: flow-root; + } + + .headsign { + font-weight: $font-weight-medium; + display: block; + font-size: 15px; + padding: 2px 0 3px 6px; + } + + .scooter-headsign { + padding: 6px 0 3px 6px; + } + + span.itinerary-stop-code { + margin-left: 5px; + padding: 0 6px; + border-radius: 10%; + } + + .citybike-station-text { + padding-left: 6px; + font-size: 13px; + font-weight: $font-weight-book; + color: #666; + } + + .citybike-icon { + float: left; + + &.small { + .icon-badge { + top: 2.33em; + left: 1.66em; + } + } + + padding-right: 2px; + + .icon-badge { + height: 1.34em; + width: 1.34em; + top: 2.33em; + left: 1.66em; + border-radius: 50%; + + .badge-text { + font-family: $font-family-narrow; + font-size: 22px; + } + + .badge-circle { + stroke-width: 6%; + } + + @media print { + left: 155px; + } + } + } + + .citybike-info { + position: absolute; + bottom: 74px; + } + } // test end here.... + } + } + + .external-link { + color: #007ac9; + text-decoration: none; + font-weight: $font-weight-medium; + + .icon-container { + padding-left: 7px; + vertical-align: text-top; + } + } + } + + .scooter-header { + border-bottom: none; + padding-bottom: 15px; + display: flex; + margin-left: 12px; + + .header { + flex: 1; + + h1 { + color: #333; + font-size: 1.5rem; + line-height: 1.2; + margin: 0; + } + } + } + + .scooter-sub-header { + font-size: 0.813rem; + color: #666; + font-weight: normal; + padding-top: 2px; + } + + .scooter-content-container { + padding: 15px 14px 22px; + display: flex; + height: 55px; + box-sizing: content-box; + + .icon-container { + display: flex; + align-self: center; + + .icon { + width: 40px; + height: 40px; + } + } + } +} diff --git a/app/constants.js b/app/constants.js index d02fc1ca46..57a6f324c0 100644 --- a/app/constants.js +++ b/app/constants.js @@ -32,6 +32,8 @@ export const TransportMode = Object.freeze({ Tram: 'TRAM', /** Taking the funicular */ Funicular: 'FUNICULAR', + /** Riding a scooter */ + Scooter: 'SCOOTER', }); /** diff --git a/app/routes.js b/app/routes.js index c5064ba8af..076fe07258 100644 --- a/app/routes.js +++ b/app/routes.js @@ -15,6 +15,7 @@ import { PREFIX_BIKESTATIONS, PREFIX_BIKEPARK, PREFIX_CARPARK, + PREFIX_RENTALVEHICLES, createReturnPath, TAB_NEARBY, TAB_FAVOURITES, @@ -294,6 +295,53 @@ export default config => { ), }} + + {{ + content: ( + + import('./component/RentalVehicleContent').then(getDefault) + } + query={graphql` + query routes_RentalVehicle_Query($id: String!) { + rentalVehicle(id: $id) { + ...RentalVehicleContent_rentalVehicle + } + } + `} + render={({ Component, props, error, retry }) => { + if (Component && (props || error)) { + return ; + } + return getComponentOrLoadingRenderer({ + Component, + props, + error, + retry, + }); + }} + /> + ), + map: ( + + import('./component/RentalVehiclePageMapContainer').then( + getDefault, + ) + } + query={graphql` + query routes_RentalVehicleMap_Query($id: String!) { + rentalVehicle(id: $id) { + ...RentalVehiclePageMapContainer_rentalVehicle + } + } + `} + render={getComponentOrNullRenderer} + /> + ), + }} + { + if (!networks) { + return false; + } + return Object.values(networks).some( + network => + network.type === type && citybikeRoutingIsActive(network, config), + ); +}; + export function useCitybikes(networks, config) { if (!networks) { return false; @@ -73,8 +83,9 @@ export function showCityBikes(networks, config) { if (!networks) { return false; } - return Object.values(networks).some(network => - showCitybikeNetwork(network, config), + return Object.values(networks).some( + network => + network.type === 'citybike' && showCitybikeNetwork(network, config), ); } @@ -288,8 +299,10 @@ export function getModes(config) { if ( activeAndAllowedBikeRentalNetworks && activeAndAllowedBikeRentalNetworks.length > 0 && - modesWithWalk.indexOf(TransportMode.Citybike) === -1 + modesWithWalk.indexOf(TransportMode.Citybike) === -1 && + modesWithWalk.indexOf(TransportMode.Scooter) === -1 ) { + // Assume citybike if no rental network mode found modesWithWalk.push(TransportMode.Citybike); } return modesWithWalk; diff --git a/app/util/path.js b/app/util/path.js index 9110f016d0..a6feca5900 100644 --- a/app/util/path.js +++ b/app/util/path.js @@ -19,7 +19,7 @@ export const PREFIX_TIMETABLE = 'aikataulu'; export const stopUrl = id => id; export const LOCAL_STORAGE_EMITTER_PATH = '/local-storage-emitter'; export const EMBEDDED_SEARCH_PATH = '/haku'; - +export const PREFIX_RENTALVEHICLES = 'skuutit'; /** * Join argument with slash separator. * diff --git a/app/util/planParamUtil.js b/app/util/planParamUtil.js index 3ab02fc3f8..99e9374193 100644 --- a/app/util/planParamUtil.js +++ b/app/util/planParamUtil.js @@ -49,12 +49,14 @@ export function getDefaultSettings(config) { if (!config) { return {}; } + return { ...config.defaultSettings, modes: getDefaultModes(config).sort(), - allowedBikeRentalNetworks: config.transportModes.citybike.defaultValue + allowedBikeRentalNetworks: config.transportModes?.citybike?.defaultValue ? getDefaultNetworks(config) : [], + allowedScooterRentalNetworks: [], }; } @@ -67,6 +69,8 @@ export function getSettings(config) { const defaultSettings = getDefaultSettings(config); const userSettings = getCustomizedSettings(); const allNetworks = getDefaultNetworks(config); + + // const allScooterNetworks = getAllScooterNetworks(config); const settings = { ...defaultSettings, ...userSettings, @@ -85,6 +89,12 @@ export function getSettings(config) { allNetworks.includes(network), ) : defaultSettings.allowedBikeRentalNetworks, + allowedScooterRentalNetworks: + userSettings.allowedScooterRentalNetworks?.length > 0 + ? userSettings.allowedScooterRentalNetworks.filter(network => + allNetworks.includes(network), + ) + : defaultSettings.allowedScooterRentalNetworks, }; const { defaultOptions } = config; return { @@ -121,6 +131,7 @@ export function getPlanParams( }, }, relaxSettings, + forceScooters = false, ) { const defaultSettings = getDefaultSettings(config); const settings = getSettings(config); @@ -129,10 +140,23 @@ export function getPlanParams( const intermediateLocations = getIntermediatePlaces({ intermediatePlaces, }); + const shouldMakeScooterRentQuery = + settings.allowedScooterRentalNetworks?.length > 0; let modesOrDefault = relaxSettings ? defaultSettings.modes : settings.modes; - modesOrDefault = modesOrDefault.map(mode => - mode === 'CITYBIKE' ? 'BICYCLE_RENT' : mode, - ); + modesOrDefault = modesOrDefault.map(mode => { + if (mode === 'CITYBIKE') { + return 'BICYCLE_RENT'; + } + return mode; + }); + + if (forceScooters) { + modesOrDefault.push('SCOOTER_RENT'); + modesOrDefault = modesOrDefault.filter(mode => mode !== 'BICYCLE'); // cannot search citybikes with scooters, causes an error: "Multiple non-walk modes provided" + } else { + modesOrDefault = modesOrDefault.filter(mode => mode !== 'SCOOTER'); + } + if (!settings.allowedBikeRentalNetworks?.length) { // do not ask citybike routes without networks modesOrDefault = modesOrDefault.filter(mode => mode !== 'BICYCLE_RENT'); @@ -192,6 +216,7 @@ export function getPlanParams( shouldMakeParkRideQuery: modesOrDefault.length > 1 && shouldMakeParkRideQuery(linearDistance, config, settings), + shouldMakeScooterRentQuery, showBikeAndPublicItineraries: modesOrDefault.length > 1 && !wheelchair && @@ -213,5 +238,9 @@ export function getPlanParams( ...modesWithoutRent, // BICYCLE_RENT can't be used together with BICYCLE_PARK ], parkRideModes: [{ mode: 'CAR', qualifier: 'PARK' }, ...modesWithoutRent], + scooterRentModes: [ + { mode: 'SCOOTER', qualifier: 'RENT' }, + ...modesWithoutRent, // Cannot use multiple rental modes + ], }; } diff --git a/app/util/vehicleRentalUtils.js b/app/util/vehicleRentalUtils.js index f10af9583f..04ea0e5ca1 100644 --- a/app/util/vehicleRentalUtils.js +++ b/app/util/vehicleRentalUtils.js @@ -4,6 +4,7 @@ import { getCustomizedSettings } from '../store/localStorage'; import { addAnalyticsEvent } from './analyticsUtils'; import { citybikeRoutingIsActive } from './modeUtils'; import { getIdWithoutFeed } from './feedScopedIdUtils'; +import { TransportMode } from '../constants'; export const BIKEAVL_UNKNOWN = 'No availability'; export const BIKEAVL_BIKES = 'Bikes on station'; @@ -51,10 +52,7 @@ export const getVehicleRentalStationNetworkConfig = (networkId, config) => { } const id = networkId.toLowerCase(); if ( - config && - config.cityBike && - config.cityBike.networks && - config.cityBike.networks[id] && + config?.cityBike?.networks?.[id] && Object.keys(config.cityBike.networks[id]).length > 0 ) { return config.cityBike.networks[id]; @@ -72,6 +70,16 @@ export const getDefaultNetworks = config => { return mappedNetworks; }; +export const getAllScooterNetworks = config => { + const mappedNetworks = []; + Object.entries(config.cityBike.networks).forEach(n => { + if (n[1].type === TransportMode.Scooter.toLowerCase()) { + mappedNetworks.push(n[0]); + } + }); + return mappedNetworks; +}; + export const mapDefaultNetworkProperties = config => { const mappedNetworks = []; Object.keys(config.cityBike.networks).forEach(key => { @@ -97,11 +105,16 @@ export const getVehicleCapacity = (config, network = undefined) => { * @param {*} config The configuration for the software installation */ -export const getVehicleRentalStationNetworks = () => { +export const getCitybikeRentalStationNetworks = () => { const { allowedBikeRentalNetworks } = getCustomizedSettings(); return allowedBikeRentalNetworks || []; }; +export const getScooterRentalNetworks = () => { + const { allowedScooterRentalNetworks } = getCustomizedSettings(); + return allowedScooterRentalNetworks || []; +}; + const addAnalytics = (action, name) => { addAnalyticsEvent({ category: 'ItinerarySettings', diff --git a/sass/_main.scss b/sass/_main.scss index 227632b6e4..d15d8b1a2f 100644 --- a/sass/_main.scss +++ b/sass/_main.scss @@ -23,6 +23,7 @@ $body-font-weight: $font-weight-medium; @import '../app/component/navigation'; @import '../app/component/itinerary'; @import '../app/component/bike-park-rental-station'; +@import '../app/component/rental-vehicle-content'; @import '../app/component/route'; @import '../app/component/customize-search'; @import '../app/component/checkbox'; diff --git a/sass/base/_helper-classes.scss b/sass/base/_helper-classes.scss index 2d89d0b005..01da0bf138 100644 --- a/sass/base/_helper-classes.scss +++ b/sass/base/_helper-classes.scss @@ -64,6 +64,10 @@ color: $citybike-color; } +.scooter { + color: $scooter-color; +} + .walk { color: $walk-color; } diff --git a/sass/themes/default/_theme.scss b/sass/themes/default/_theme.scss index d8138f31fa..7a293885a0 100644 --- a/sass/themes/default/_theme.scss +++ b/sass/themes/default/_theme.scss @@ -41,6 +41,8 @@ $visited-link-color: #8c4799; $current-location-color: $primary-color; $desktop-title-color: $primary-color; $desktop-title-arrow-icon-color: $secondary-color; +$infobox-color-generic-blue: #e5f2fa; +$info-icon-blue: #0074be; /* Component palette */ $title-color: $white; @@ -76,6 +78,7 @@ $walk-color: #666; $wait-color: #979797; $bicycle-color: #666; $car-color: #333; +$scooter-color: #666; /* Fonts */ $font-family: 'Roboto', arial, georgia, serif; diff --git a/sass/themes/kuopio/_theme.scss b/sass/themes/kuopio/_theme.scss index 8d9729ee42..6bfb78b94f 100644 --- a/sass/themes/kuopio/_theme.scss +++ b/sass/themes/kuopio/_theme.scss @@ -10,7 +10,7 @@ $viewpoint-marker-color: $primary-color; $current-location-color: $secondary-color; $standalone-btn-color: $primary-color; $link-color: $primary-color; -$rail-color: #0E7F3C; +$rail-color: #0e7f3c; /* Component palette */ $desktop-title-color: $primary-color; diff --git a/static/assets/svg-sprite.default.svg b/static/assets/svg-sprite.default.svg index 6989b19c19..3e47c95b12 100644 --- a/static/assets/svg-sprite.default.svg +++ b/static/assets/svg-sprite.default.svg @@ -2792,5 +2792,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/assets/svg-sprite.hsl.svg b/static/assets/svg-sprite.hsl.svg index 4237874b61..87041ac141 100644 --- a/static/assets/svg-sprite.hsl.svg +++ b/static/assets/svg-sprite.hsl.svg @@ -2714,5 +2714,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/unit/component/BicycleLeg.test.js b/test/unit/component/BicycleLeg.test.js index f518c8b38d..d1bc120d3f 100644 --- a/test/unit/component/BicycleLeg.test.js +++ b/test/unit/component/BicycleLeg.test.js @@ -36,6 +36,8 @@ describe('', () => { cityBike: { networks: { foobar: { type: CityBikeNetworkType.CityBike } }, }, + defaultSettings: { walkSpeed: 1, bikeSpeed: 1 }, + defaultOptions: { walkSpeed: 1, bikeSpeed: 1 }, }, }, }); @@ -74,6 +76,8 @@ describe('', () => { cityBike: { networks: { foobar: { type: CityBikeNetworkType.Scooter } }, }, + defaultSettings: { walkSpeed: 1, bikeSpeed: 1 }, + defaultOptions: { walkSpeed: 1, bikeSpeed: 1 }, }, }, }); @@ -112,6 +116,8 @@ describe('', () => { cityBike: { networks: { foobar: { type: CityBikeNetworkType.CityBike } }, }, + defaultSettings: { walkSpeed: 1, bikeSpeed: 1 }, + defaultOptions: { walkSpeed: 1, bikeSpeed: 1 }, }, }, }); @@ -153,6 +159,8 @@ describe('', () => { cityBike: { networks: { foobar: { type: CityBikeNetworkType.Scooter } }, }, + defaultSettings: { walkSpeed: 1, bikeSpeed: 1 }, + defaultOptions: { walkSpeed: 1, bikeSpeed: 1 }, }, }, }); @@ -194,6 +202,8 @@ describe('', () => { cityBike: { networks: { foobar: { type: CityBikeNetworkType.CityBike } }, }, + defaultSettings: { walkSpeed: 1, bikeSpeed: 1 }, + defaultOptions: { walkSpeed: 1, bikeSpeed: 1 }, }, }, }); @@ -232,6 +242,8 @@ describe('', () => { cityBike: { networks: { foobar: { type: CityBikeNetworkType.Scooter } }, }, + defaultSettings: { walkSpeed: 1, bikeSpeed: 1 }, + defaultOptions: { walkSpeed: 1, bikeSpeed: 1 }, }, }, }); diff --git a/test/unit/component/MapLayersDialogContent.test.js b/test/unit/component/MapLayersDialogContent.test.js index f81c3ae382..d1c42dd219 100644 --- a/test/unit/component/MapLayersDialogContent.test.js +++ b/test/unit/component/MapLayersDialogContent.test.js @@ -216,6 +216,7 @@ describe('', () => { cityBike: { networks: { foo: { + type: 'citybike', enabled: true, season: { start: today, From c668df00efc78721c1e5a3e0af270fd13e8da614 Mon Sep 17 00:00:00 2001 From: sharhio Date: Thu, 18 Apr 2024 16:15:47 +0300 Subject: [PATCH 02/79] DT-6182 scooter features --- app/component/BicycleLeg.js | 33 +-- app/component/CustomizeSearch.js | 14 +- app/component/ItineraryDetails.js | 15 +- app/component/ItineraryLegs.js | 2 + app/component/ItineraryListContainer.js | 6 + app/component/ItineraryPage.js | 80 ++++--- app/component/ItineraryPageContainer.js | 2 +- app/component/ItineraryQueries.js | 3 + app/component/ItineraryTabs.js | 4 +- app/component/MapLayersDialogContent.js | 26 ++- app/component/ParkOrStationHeader.js | 20 +- app/component/RentalVehicleContent.js | 25 +- app/component/StopsNearYouPage.js | 6 +- app/component/VehicleRentalLeg.js | 82 +++++-- app/component/VehicleRentalStation.js | 8 +- app/component/VehicleRentalStationContent.js | 48 ++-- app/component/VehicleRentalStationNearYou.js | 2 +- app/component/customize-search.scss | 4 +- app/component/itinerary.scss | 30 ++- app/component/map/StopPageMap.js | 5 + app/component/map/StopsNearYouMap.js | 2 +- .../map/tile-layer/MarkerSelectPopup.js | 23 +- .../map/tile-layer/RentalVehicles.js | 219 ++++++++++++++++++ .../SelectVehicleRentalStationRow.js | 11 +- app/component/map/tile-layer/TileContainer.js | 5 +- .../map/tile-layer/TileLayerContainer.js | 12 + .../tile-layer/VectorTileLayerContainer.js | 5 +- .../map/tile-layer/VehicleRentalStations.js | 19 +- app/component/rental-vehicle-content.scss | 32 ++- app/configurations/config.default.js | 8 +- app/store/MapLayerStore.js | 17 +- app/translations.js | 8 + app/util/mapIconUtils.js | 93 +++++++- app/util/mapLayerUtils.js | 4 + app/util/modeUtils.js | 42 ++-- app/util/path.js | 2 + app/util/planParamUtil.js | 11 +- app/util/vehicleRentalUtils.js | 11 +- 38 files changed, 738 insertions(+), 201 deletions(-) create mode 100644 app/component/map/tile-layer/RentalVehicles.js diff --git a/app/component/BicycleLeg.js b/app/component/BicycleLeg.js index 6647fa4686..7ae2862684 100644 --- a/app/component/BicycleLeg.js +++ b/app/component/BicycleLeg.js @@ -22,9 +22,10 @@ import VehicleRentalLeg from './VehicleRentalLeg'; import StopCode from './StopCode'; import PlatformNumber from './PlatformNumber'; import { getSettings } from '../util/planParamUtil'; +import { isKeyboardSelectionEvent } from '../util/browser'; export default function BicycleLeg( - { focusAction, index, leg, focusToLeg, bicycleWalkLeg }, + { focusAction, index, leg, focusToLeg, bicycleWalkLeg, toggleSettings }, { config, intl }, ) { let stopsDescription; @@ -280,13 +281,18 @@ export default function BicycleLeg( />
- -
+
+ isKeyboardSelectionEvent(e) && toggleSettings(true) + } + className="itinerary-transit-leg-route-bike" + >
- +
- - - +
@@ -378,6 +382,7 @@ BicycleLeg.propTypes = { index: PropTypes.number.isRequired, focusAction: PropTypes.func.isRequired, focusToLeg: PropTypes.func.isRequired, + toggleSettings: PropTypes.func.isRequired, }; BicycleLeg.defaultProps = { diff --git a/app/component/CustomizeSearch.js b/app/component/CustomizeSearch.js index dd06fc092b..2a12fc3053 100644 --- a/app/component/CustomizeSearch.js +++ b/app/component/CustomizeSearch.js @@ -13,7 +13,7 @@ import AccessibilityOptionSection from './customizesearch/AccessibilityOptionSec import TransferOptionsSection from './customizesearch/TransferOptionsSection'; import VehicleRentalStationNetworkSelector from './customizesearch/VehicleRentalStationNetworkSelector'; import ScooterRentalNetworkSelector from './customizesearch/ScooterRentalNetworkSelector'; -import { showModeSettings, useRentalVehiclesOfType } from '../util/modeUtils'; +import { showModeSettings, useCitybikes, useScooters } from '../util/modeUtils'; import ScrollableWrapper from './ScrollableWrapper'; import { getDefaultSettings } from '../util/planParamUtil'; import { @@ -133,11 +133,7 @@ class CustomizeSearch extends React.Component { />
- {useRentalVehiclesOfType( - config?.cityBike?.networks, - config, - CityBikeNetworkType.CityBike, - ) && ( + {useCitybikes(config?.cityBike?.networks, config) && (
@@ -162,11 +158,7 @@ class CustomizeSearch extends React.Component {
)} - {useRentalVehiclesOfType( - config?.cityBike?.networks, - config, - CityBikeNetworkType.Scooter, - ) && ( + {useScooters(config?.cityBike?.networks) && (
diff --git a/app/component/ItineraryDetails.js b/app/component/ItineraryDetails.js index 02f13587f0..b69518de27 100644 --- a/app/component/ItineraryDetails.js +++ b/app/component/ItineraryDetails.js @@ -55,6 +55,7 @@ class ItineraryDetails extends React.Component { carItinerary: itineraryShape, currentLanguage: PropTypes.string, changeHash: PropTypes.func, + toggleSettings: PropTypes.func.isRequired, }; static defaultProps = { @@ -315,6 +316,7 @@ class ItineraryDetails extends React.Component { focusToLeg={this.props.focusToLeg} changeHash={this.props.changeHash} tabIndex={itineraryIndex - 1} + toggleSettings={this.props.toggleSettings} /> {config.showRouteInformation && }
@@ -442,6 +444,12 @@ const withRelay = createFragmentContainer( lat lon network + rentalUris { + android + ios + web + } + systemUrl } stop { gtfsId @@ -476,13 +484,6 @@ const withRelay = createFragmentContainer( network vehiclesAvailable } - rentalVehicle { - vehicleId - name - lat - lon - network - } stop { gtfsId code diff --git a/app/component/ItineraryLegs.js b/app/component/ItineraryLegs.js index a5e3c0f30d..73529f4301 100644 --- a/app/component/ItineraryLegs.js +++ b/app/component/ItineraryLegs.js @@ -43,6 +43,7 @@ export default class ItineraryLegs extends React.Component { focusToLeg: PropTypes.func.isRequired, changeHash: PropTypes.func, tabIndex: PropTypes.number, + toggleSettings: PropTypes.func.isRequired, }; static contextTypes = { config: configShape }; @@ -324,6 +325,7 @@ export default class ItineraryLegs extends React.Component { focusAction={this.focus(leg.from)} focusToLeg={this.focusToLeg(leg)} bicycleWalkLeg={bicycleWalkLeg} + toggleSettings={this.props.toggleSettings} />, ); } else if (leg.mode === 'CAR') { diff --git a/app/component/ItineraryListContainer.js b/app/component/ItineraryListContainer.js index c5313bd917..8c4ac17cda 100644 --- a/app/component/ItineraryListContainer.js +++ b/app/component/ItineraryListContainer.js @@ -339,6 +339,12 @@ const connectedContainer = createFragmentContainer( lat lon network + rentalUris { + android + ios + web + } + systemUrl } } to { diff --git a/app/component/ItineraryPage.js b/app/component/ItineraryPage.js index 2962323961..9d2d4d9c5c 100644 --- a/app/component/ItineraryPage.js +++ b/app/component/ItineraryPage.js @@ -59,7 +59,7 @@ import { saveFutureRoute } from '../action/FutureRoutesActions'; import { saveSearch } from '../action/SearchActions'; import CustomizeSearch from './CustomizeSearch'; import { mapLayerShape } from '../store/MapLayerStore'; -import { getAllScooterNetworks } from '../util/vehicleRentalUtils'; +import { getAllNetworksOfType } from '../util/vehicleRentalUtils'; import { TransportMode } from '../constants'; const streetHashes = [ @@ -114,9 +114,15 @@ export default function ItineraryPage(props, context) { ...emptyPlans, loading: ALT_LOADING_STATES.UNSET, }); - const [scooterState, setScooterState] = useState({ loading: false }); + const [scooterState, setScooterState] = useState({ + plan: {}, + loading: false, + }); const [relaxState, setRelaxState] = useState({ loading: false }); - const [relaxRentalState, setRelaxRentalState] = useState({ loading: false }); + const [relaxRentalState, setRelaxRentalState] = useState({ + plan: {}, + loading: false, + }); const [settingsState, setSettingsState] = useState({ settingsOpen: false, settingsChanged: 0, @@ -190,10 +196,15 @@ export default function ItineraryPage(props, context) { } function mergedPlans() { - const scooterItinerary = transitItineraries( + const scooterItineraries = transitItineraries( filterItineraries(scooterState.plan?.itineraries, TransportMode.Scooter), - )?.[0]; + ); + const scooterItinerary = scooterItineraries[0]; if (scooterItinerary) { + if (state.plan?.itineraries?.length <= 0) { + // when the only option is a scooter itinerary + return scooterItinerary; + } const mergedRoutingErrors = []; mergedRoutingErrors.concat( scooterState.plan?.routingErrors, @@ -238,7 +249,8 @@ export default function ItineraryPage(props, context) { } } if ( - state.plan?.itineraries?.length > 0 && + state.loading === false && + scooterState.loading === false && scooterState.plan?.itineraries?.length > 0 ) { return mergedPlans(); @@ -447,12 +459,17 @@ export default function ItineraryPage(props, context) { fetchQuery(props.relayEnvironment, planQuery, tunedParams) .toPromise() .then(result => { - setScooterState({ ...scooterState, plan: result.plan }); + setScooterState({ ...scooterState, plan: result.plan, loading: false }); resetItineraryPageSelection(); ariaRef.current = 'itinerary-page.itineraries-loaded'; }) .catch(err => { - setScooterState({ ...scooterState, plan: {}, error: err }); + setScooterState({ + ...scooterState, + plan: {}, + loading: false, + error: err, + }); reportError(err); }); } @@ -463,7 +480,10 @@ export default function ItineraryPage(props, context) { return; } setRelaxRentalState({ loading: true }); - const allScooterNetworks = getAllScooterNetworks(context.config); + const allScooterNetworks = getAllNetworksOfType( + context.config, + TransportMode.Scooter, + ); const planParams = getPlanParams( context.config, props.match, @@ -486,7 +506,7 @@ export default function ItineraryPage(props, context) { }; setRelaxRentalState({ relaxedScooterPlan, - loadingRelaxedScooter: false, + loading: false, }); }) .catch(() => { @@ -788,6 +808,8 @@ export default function ItineraryPage(props, context) { const settings = getSettings(context.config); if (settings.allowedScooterRentalNetworks?.length > 0) { makeForcedScooterQuery(settings); + } else { + setScooterState({ plan: {} }); } makeMainQuery(); makeAlternativeQuery(); @@ -796,12 +818,9 @@ export default function ItineraryPage(props, context) { !settingsState.settingsChanged ) { makeRelaxedQuery(); - } - if ( - !settings.allowedScooterRentalNetworks.length && - !relaxState.relaxedPlan?.itineraries?.length - ) { - makeRelaxedScooterQuery(); + if (!settings.allowedScooterRentalNetworks.length) { + makeRelaxedScooterQuery(); + } } }, [ settingsState.settingsChanged, @@ -926,7 +945,7 @@ export default function ItineraryPage(props, context) { context.router.replace(newLocationState); }; - function showSettingsPanel(open) { + function showSettingsPanel(open, detailView) { addAnalyticsEvent({ event: 'sendMatomoEvent', category: 'ItinerarySettings', @@ -964,13 +983,18 @@ export default function ItineraryPage(props, context) { settingsChanged, }); + if (settingsChanged && detailView) { + // Ensures returning to the list view after changing the settings in detail view. + selectStreetMode(); + } + if (props.breakpoint !== 'large') { context.router.go(-1); } } - const toggleSettings = () => { - showSettingsPanel(!settingsState.settingsOpen); + const toggleSettings = detailView => { + showSettingsPanel(!settingsState.settingsOpen, detailView); }; const focusToHeader = () => { @@ -1104,18 +1128,21 @@ export default function ItineraryPage(props, context) { hasNoTransitItineraries && altState.loading === ALT_LOADING_STATES.LOADING; const loading = state.loading || + scooterState.loading || (relaxState.loading && hasNoTransitItineraries) || - (relaxRentalState.loadingRelaxedScooter && hasNoTransitItineraries) || + (relaxRentalState.loading && hasNoTransitItineraries) || waitAlternatives || (streetHashes.includes(hash) && altState.loading === ALT_LOADING_STATES.LOADING); // viewing unfinished alt plan - const settingsDrawer = - !detailView && settingsState.settingsOpen ? ( -
- -
- ) : null; + const settingsDrawer = settingsState.settingsOpen ? ( +
+ toggleSettings(detailView)} + mobile={!desktop} + /> +
+ ) : null; // in mobile, settings drawer hides other content const panelHidden = !desktop && settingsDrawer !== null; @@ -1140,6 +1167,7 @@ export default function ItineraryPage(props, context) { focusToPoint={focusToPoint} focusToLeg={focusToLeg} carItinerary={carPlan?.itineraries[0]} + toggleSettings={toggleSettings} /> ); } else if (plan?.itineraries?.length) { diff --git a/app/component/ItineraryPageContainer.js b/app/component/ItineraryPageContainer.js index b82c43d142..f0fe09b642 100644 --- a/app/component/ItineraryPageContainer.js +++ b/app/component/ItineraryPageContainer.js @@ -20,7 +20,7 @@ const ItineraryPageWithStores = connectToStores( ['MapLayerStore'], ({ getStore }) => ({ mapLayers: getStore('MapLayerStore').getMapLayers({ - notThese: ['stop', 'citybike', 'vehicles'], + notThese: ['stop', 'citybike', 'vehicles', 'scooter'], }), mapLayerOptions: getMapLayerOptions({ lockedMapLayers: ['vehicles', 'citybike', 'stop'], diff --git a/app/component/ItineraryQueries.js b/app/component/ItineraryQueries.js index d8ca22aecb..07317ce82a 100644 --- a/app/component/ItineraryQueries.js +++ b/app/component/ItineraryQueries.js @@ -95,6 +95,9 @@ export const planQuery = graphql` vehicleId name network + vehicleType { + formFactor + } } } to { diff --git a/app/component/ItineraryTabs.js b/app/component/ItineraryTabs.js index 4e712ceb77..4c97adcd29 100644 --- a/app/component/ItineraryTabs.js +++ b/app/component/ItineraryTabs.js @@ -6,7 +6,7 @@ import { itineraryShape } from '../util/shapes'; /* eslint-disable react/no-array-index-key */ function ItineraryTabs(props) { - const { itineraries } = props; + const { itineraries, toggleSettings } = props; const itineraryTabs = itineraries.map((itinerary, i) => { return ( @@ -20,6 +20,7 @@ function ItineraryTabs(props) { itinerary={itinerary} hideTitle={!props.isMobile} changeHash={props.isMobile ? props.changeHash : undefined} + toggleSettings={toggleSettings} />
); @@ -45,6 +46,7 @@ ItineraryTabs.propTypes = { itineraries: PropTypes.arrayOf(itineraryShape).isRequired, carItinerary: itineraryShape, changeHash: PropTypes.func, + toggleSettings: PropTypes.func.isRequired, }; ItineraryTabs.defaultProps = { diff --git a/app/component/MapLayersDialogContent.js b/app/component/MapLayersDialogContent.js index 1fae304a58..0ca75497c5 100644 --- a/app/component/MapLayersDialogContent.js +++ b/app/component/MapLayersDialogContent.js @@ -13,7 +13,8 @@ import MapLayerStore, { mapLayerShape } from '../store/MapLayerStore'; import { updateMapLayers } from '../action/MapLayerActions'; import { addAnalyticsEvent } from '../util/analyticsUtils'; import withGeojsonObjects from './map/withGeojsonObjects'; -import { getTransportModes, showCityBikes } from '../util/modeUtils'; +import { getTransportModes, showRentalVehiclesOfType } from '../util/modeUtils'; +import { TransportMode } from '../constants'; const transportModeconfigShape = PropTypes.shape({ availableForSelection: PropTypes.bool, @@ -50,6 +51,7 @@ const mapLayersconfigShape = PropTypes.shape({ rail: transportModeconfigShape, subway: transportModeconfigShape, tram: transportModeconfigShape, + scooter: transportModeconfigShape, }), mapLayers: PropTypes.shape({ tooltip: PropTypes.shape({ @@ -115,7 +117,7 @@ class MapLayersDialogContent extends React.Component { }; render() { - const { citybike, parkAndRide, stop, geoJson, vehicles } = + const { citybike, parkAndRide, stop, geoJson, vehicles, scooter } = this.props.mapLayers; let arr; if (this.props.geoJson) { @@ -205,9 +207,10 @@ class MapLayersDialogContent extends React.Component { }} /> )} - {showCityBikes( + {showRentalVehiclesOfType( this.context.config?.cityBike?.networks, this.context.config, + TransportMode.Citybike, ) && ( )} + {showRentalVehiclesOfType( + this.context.config?.cityBike?.networks, + this.context.config, + TransportMode.Scooter, + ) && ( + { + this.updateSetting({ scooter: e.target.checked }); + sendLayerChangeAnalytic('Scooter', e.target.checked); + }} + /> + )} {isTransportModeEnabled(transportModes.funicular) && ( @@ -43,8 +47,13 @@ const ParkOrBikeStationHeader = ({ parkOrStation, breakpoint }, { config }) => { }); }, []); - const { name, bikeParkId, stationId } = parkOrStation; + const { name, bikeParkId, stationId, network } = parkOrStation; + const networkConfig = getVehicleRentalStationNetworkConfig(network, config); const parkHeaderId = bikeParkId ? 'bike-park' : 'car_park'; + const noIdHeaderName = + networkConfig.type === TransportMode.Citybike.toLowerCase() + ? 'citybike-station-no-id' + : 'e-scooter-station'; return (
{breakpoint === 'large' && ( @@ -56,10 +65,8 @@ const ParkOrBikeStationHeader = ({ parkOrStation, breakpoint }, { config }) => {

{name}

- - {stationId && hasStationCode(parkOrStation) && ( + + {stationId && hasStationCode(parkOrStation.stationId) && ( )} {zoneId && ( @@ -91,6 +98,7 @@ ParkOrBikeStationHeader.propTypes = { stationId: PropTypes.string, lat: PropTypes.number.isRequired, lon: PropTypes.number.isRequired, + network: PropTypes.string.isRequired, }).isRequired, }; diff --git a/app/component/RentalVehicleContent.js b/app/component/RentalVehicleContent.js index 4c2dace899..15cc26fed4 100644 --- a/app/component/RentalVehicleContent.js +++ b/app/component/RentalVehicleContent.js @@ -16,7 +16,7 @@ import VehicleRentalLeg from './VehicleRentalLeg'; import BackButton from './BackButton'; const RentalVehicleContent = ( - { rentalVehicle, breakpoint, router, error }, + { rentalVehicle, breakpoint, router, error, language }, { config }, ) => { const [isClient, setClient] = useState(false); @@ -38,9 +38,12 @@ const RentalVehicleContent = ( } return null; } - + const networkConfig = getVehicleRentalStationNetworkConfig( + rentalVehicle.network, + config, + ); const vehicleIcon = getVehicleRentalStationNetworkIcon( - getVehicleRentalStationNetworkConfig(rentalVehicle.network, config), + networkConfig, !rentalVehicle.operative, ); @@ -57,7 +60,7 @@ const RentalVehicleContent = ( /> )}
-

{rentalVehicle.network}

+

{networkConfig.name[language] || rentalVehicle.network}

@@ -78,6 +81,7 @@ RentalVehicleContent.propTypes = { breakpoint: PropTypes.string.isRequired, router: routerShape.isRequired, error: PropTypes.object, + language: PropTypes.string.isRequired, }; RentalVehicleContent.contextTypes = { @@ -89,9 +93,10 @@ const RentalVehicleContentWithBreakpoint = withBreakpoint(RentalVehicleContent); const connectedComponent = connectToStores( RentalVehicleContentWithBreakpoint, ['PreferencesStore'], - context => ({ - language: context.getStore('PreferencesStore').getLanguage(), - }), + ({ getStore }) => { + const language = getStore('PreferencesStore').getLanguage(); + return { language }; + }, ); const containerComponent = createFragmentContainer(connectedComponent, { @@ -103,6 +108,12 @@ const containerComponent = createFragmentContainer(connectedComponent, { name network vehicleId + rentalUris { + android + ios + web + } + systemUrl } `, }); diff --git a/app/component/StopsNearYouPage.js b/app/component/StopsNearYouPage.js index 76bc6ed559..19cebb6891 100644 --- a/app/component/StopsNearYouPage.js +++ b/app/component/StopsNearYouPage.js @@ -218,10 +218,6 @@ class StopsNearYouPage extends React.Component { placeTypes = 'VEHICLE_RENT'; modes = ['BICYCLE']; } - if (mode === 'SCOOTER') { - placeTypes = 'VEHICLE_RENT'; - modes = ['SCOOTER']; - } const prioritizedStops = this.context.config.prioritizedStopsNearYou[mode.toLowerCase()] || []; return { @@ -1005,7 +1001,7 @@ const PositioningWrapper = connectToStores( lang: context.getStore('PreferencesStore').getLanguage(), mapLayers: context .getStore('MapLayerStore') - .getMapLayers({ notThese: ['vehicles'] }), + .getMapLayers({ notThese: ['vehicles', 'scooter'] }), favouriteStopIds, favouriteVehicleStationIds, favouriteStationIds, diff --git a/app/component/VehicleRentalLeg.js b/app/component/VehicleRentalLeg.js index 9ef377d76a..a32de323e6 100644 --- a/app/component/VehicleRentalLeg.js +++ b/app/component/VehicleRentalLeg.js @@ -3,7 +3,12 @@ import Link from 'found/Link'; import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; -import { configShape, vehicleRentalStationShape } from '../util/shapes'; +import connectToStores from 'fluxible-addons-react/connectToStores'; +import { + configShape, + vehicleRentalStationShape, + rentalVehicleShape, +} from '../util/shapes'; import { BIKEAVL_UNKNOWN, getVehicleCapacity, @@ -14,12 +19,14 @@ import { import withBreakpoint from '../util/withBreakpoint'; import Icon from './Icon'; -import { PREFIX_BIKESTATIONS, PREFIX_RENTALVEHICLES } from '../util/path'; +import { PREFIX_BIKESTATIONS } from '../util/path'; import { getVehicleAvailabilityTextColor, getVehicleAvailabilityIndicatorColor, } from '../util/legUtils'; import { getIdWithoutFeed } from '../util/feedScopedIdUtils'; +import ExternalLink from './ExternalLink'; +import { isAndroid, isIOS } from '../util/browser'; function VehicleRentalLeg( { @@ -29,6 +36,7 @@ function VehicleRentalLeg( returnBike = false, breakpoint, rentalVehicle, + language, }, { config, intl }, ) { @@ -45,9 +53,8 @@ function VehicleRentalLeg( ); - const vehicleIcon = getVehicleRentalStationNetworkIcon( - getVehicleRentalStationNetworkConfig(network, config), - ); + const networkConfig = getVehicleRentalStationNetworkConfig(network, config); + const vehicleIcon = getVehicleRentalStationNetworkIcon(networkConfig); const availabilityIndicatorColor = vehicleRentalStation ? getVehicleAvailabilityIndicatorColor( vehicleRentalStation.vehiclesAvailable, @@ -67,13 +74,21 @@ function VehicleRentalLeg( const scooterHeadsign = ( ); - const link = isScooter - ? `/${PREFIX_RENTALVEHICLES}/${rentalVehicle?.vehicleId}` // TO DO: link from data - : `/${PREFIX_BIKESTATIONS}/${vehicleRentalStation?.stationId}`; + const rentalStationLink = `/${PREFIX_BIKESTATIONS}/${vehicleRentalStation?.stationId}`; + let rentalVehicleLink = + rentalVehicle?.rentalUris.web || rentalVehicle?.systemUrl; + + if (isIOS && rentalVehicle?.rentalUris.ios) { + rentalVehicleLink = rentalVehicle?.rentalUris.ios; + } else if (isAndroid && rentalVehicle?.rentalUris.android) { + rentalVehicleLink = rentalVehicle?.rentalUris.android; + } return ( <>
{legDescription}
@@ -102,9 +117,22 @@ function VehicleRentalLeg(
- - {isScooter ? scooterHeadsign : stationName} - + {!isScooter && ( + + {isScooter ? scooterHeadsign : stationName} + + )} + {isScooter && ( + + {scooterHeadsign} + + )} {!isScooter && ( @@ -114,7 +142,7 @@ function VehicleRentalLeg( defaultMessage: 'Bike station', })} {vehicleRentalStation && - hasStationCode(vehicleRentalStation) && ( + hasStationCode(vehicleRentalStation.stationId) && ( {getIdWithoutFeed(vehicleRentalStation?.stationId)} @@ -125,18 +153,21 @@ function VehicleRentalLeg(
{isScooter ? (
- + - +
) : (
- + { + const language = getStore('PreferencesStore').getLanguage(); + return { language }; + }, +); + export { connectedComponent as default, VehicleRentalLeg as Component }; diff --git a/app/component/VehicleRentalStation.js b/app/component/VehicleRentalStation.js index d2805ea13a..7cacfd1302 100644 --- a/app/component/VehicleRentalStation.js +++ b/app/component/VehicleRentalStation.js @@ -32,9 +32,12 @@ const VehicleRentalStation = ({ vehicleRentalStation }, { config }) => { fewerAvailableCount = Math.floor(totalSpaces / 6); } const disabled = !vehicleRentalStation.operative; - + const networkConfig = getVehicleRentalStationNetworkConfig( + vehicleRentalStation.network, + config, + ); const vehicleIcon = getVehicleRentalStationNetworkIcon( - getVehicleRentalStationNetworkConfig(vehicleRentalStation.network, config), + networkConfig, disabled, ); return ( @@ -47,6 +50,7 @@ const VehicleRentalStation = ({ vehicleRentalStation }, { config }) => { fewAvailableCount={fewAvailableCount} fewerAvailableCount={fewerAvailableCount} useSpacesAvailable={vehicleCapacity === BIKEAVL_WITHMAX} + type={networkConfig.type} />
); diff --git a/app/component/VehicleRentalStationContent.js b/app/component/VehicleRentalStationContent.js index 3f02af3f81..e03aa1d184 100644 --- a/app/component/VehicleRentalStationContent.js +++ b/app/component/VehicleRentalStationContent.js @@ -16,6 +16,7 @@ import withBreakpoint from '../util/withBreakpoint'; import { getVehicleRentalStationNetworkConfig } from '../util/vehicleRentalUtils'; import { isBrowser } from '../util/browser'; import { PREFIX_BIKESTATIONS } from '../util/path'; +import { TransportMode } from '../constants'; const VehicleRentalStationContent = ( { vehicleRentalStation, breakpoint, language, router, error }, @@ -81,32 +82,33 @@ const VehicleRentalStationContent = (
)} - {(cityBikeBuyUrl || cityBikeNetworkUrl) && ( - ); }; diff --git a/app/component/VehicleRentalStationNearYou.js b/app/component/VehicleRentalStationNearYou.js index 7ce244fd0c..7478349766 100644 --- a/app/component/VehicleRentalStationNearYou.js +++ b/app/component/VehicleRentalStationNearYou.js @@ -52,7 +52,7 @@ const VehicleRentalStationNearYou = ({ { if (props.stop.vehicleMode) { return props.stop.vehicleMode.toLowerCase(); } + if (props.scooter) { + return 'scooter'; + } return 'stop'; }; @@ -237,6 +240,8 @@ const StopPageMapWithStores = connectToStores( const ml = config.showVehiclesOnStopPage ? { notThese: ['vehicles'] } : {}; if (props.citybike) { ml.force = ['citybike']; // show always + } else if (props.scooter) { + ml.force = ['scooter']; // show always } else { ml.force = ['terminal']; } diff --git a/app/component/map/StopsNearYouMap.js b/app/component/map/StopsNearYouMap.js index 393666c1c7..95647499ce 100644 --- a/app/component/map/StopsNearYouMap.js +++ b/app/component/map/StopsNearYouMap.js @@ -177,7 +177,7 @@ function StopsNearYouMap( const prevPlace = useRef(); const prevMode = useRef(); const { mode } = match.params; - const isTransitMode = mode !== 'CITYBIKE' && mode !== 'SCOOTER'; + const isTransitMode = mode !== 'CITYBIKE'; const walkRoutingThreshold = mode === 'RAIL' || mode === 'SUBWAY' || mode === 'FERRY' ? 3000 : 1500; const { environment } = relay; diff --git a/app/component/map/tile-layer/MarkerSelectPopup.js b/app/component/map/tile-layer/MarkerSelectPopup.js index 5c082b4bc8..3171ad6a85 100644 --- a/app/component/map/tile-layer/MarkerSelectPopup.js +++ b/app/component/map/tile-layer/MarkerSelectPopup.js @@ -1,14 +1,15 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, intlShape } from 'react-intl'; import SelectStopRow from './SelectStopRow'; import SelectVehicleRentalStationRow from './SelectVehicleRentalStationRow'; import SelectParkAndRideRow from './SelectParkAndRideRow'; import SelectVehicleContainer from './SelectVehicleContainer'; import { getIdWithoutFeed } from '../../../util/feedScopedIdUtils'; import { popupColorShape } from '../../../util/shapes'; +import { PREFIX_BIKESTATIONS, PREFIX_RENTALVEHICLES } from '../../../util/path'; -function MarkerSelectPopup(props) { +function MarkerSelectPopup(props, { intl }) { const hasStop = () => props.options.find(option => option.layer !== 'realTimeVehicle'); @@ -51,6 +52,20 @@ function MarkerSelectPopup(props) { + ); + } + if (option.layer === 'scooter') { + return ( + ); } @@ -120,4 +135,8 @@ MarkerSelectPopup.propTypes = { colors: popupColorShape.isRequired, }; +MarkerSelectPopup.contextTypes = { + intl: intlShape.isRequired, +}; + export default MarkerSelectPopup; diff --git a/app/component/map/tile-layer/RentalVehicles.js b/app/component/map/tile-layer/RentalVehicles.js new file mode 100644 index 0000000000..8fd19573c6 --- /dev/null +++ b/app/component/map/tile-layer/RentalVehicles.js @@ -0,0 +1,219 @@ +import { VectorTile } from '@mapbox/vector-tile'; +import Protobuf from 'pbf'; +import { graphql, fetchQuery } from 'react-relay'; +import pick from 'lodash/pick'; + +import { isBrowser } from '../../../util/browser'; +import { + getMapIconScale, + drawScooterIcon, + drawSmallVehicleRentalMarker, +} from '../../../util/mapIconUtils'; + +import { + getVehicleRentalStationNetworkConfig, + getVehicleRentalStationNetworkIcon, +} from '../../../util/vehicleRentalUtils'; +import { fetchWithLanguageAndSubscription } from '../../../util/fetchUtils'; +import { getLayerBaseUrl } from '../../../util/mapLayerUtils'; +import { TransportMode } from '../../../constants'; + +const query = graphql` + query RentalVehiclesQuery($id: String!) { + station: rentalVehicle(id: $id) { + vehicleId + name + } + } +`; + +const REALTIME_REFETCH_FREQUENCY = 60000; // 60 seconds + +class RentalVehicles { + constructor(tile, config, mapLayers, relayEnvironment) { + this.tile = tile; + this.config = config; + this.relayEnvironment = relayEnvironment; + this.scaleratio = (isBrowser && window.devicePixelRatio) || 1; + this.citybikeImageSize = + 20 * this.scaleratio * getMapIconScale(this.tile.coords.z); + this.availabilityImageSize = + 14 * this.scaleratio * getMapIconScale(this.tile.coords.z); + this.timeOfLastFetch = undefined; + this.canHaveStationUpdates = true; + } + + getPromise = lang => this.fetchAndDraw(lang); + + fetchAndDraw = lang => { + const zoomedIn = + this.tile.coords.z > this.config.cityBike.cityBikeSmallIconZoom; + let baseUrl = zoomedIn + ? getLayerBaseUrl(this.config.URL.REALTIME_RENTAL_VEHICLE_MAP, lang) + : getLayerBaseUrl(this.config.URL.RENTAL_VEHICLE_MAP, lang); + + if (this.tile.coords.z >= 14) { + baseUrl = getLayerBaseUrl( + this.config.URL.RENTAL_VEHICLE_CLUSTER_MEDIUM_MAP, // 100m + lang, + ); + } + if (this.tile.coords.z >= 16) { + baseUrl = getLayerBaseUrl( + this.config.URL.RENTAL_VEHICLE_CLUSTER_CLOSE_MAP, // 50m + lang, + ); + } + if (this.tile.coords.z >= 18) { + baseUrl = getLayerBaseUrl( + this.config.URL.RENTAL_VEHICLE_MAP, // No clustering + lang, + ); + } + + const tileUrl = `${baseUrl}${ + this.tile.coords.z + (this.tile.props.zoomOffset || 0) + }/${this.tile.coords.x}/${this.tile.coords.y}.pbf`; + return fetchWithLanguageAndSubscription(tileUrl, this.config, lang) + .then(res => { + this.timeOfLastFetch = new Date().getTime(); + if (res.status !== 200) { + return undefined; + } + + return res.arrayBuffer().then( + buf => { + const vt = new VectorTile(new Protobuf(buf)); + + this.features = []; + const layer = + vt.layers.rentalVehicles || + vt.layers.realtimeRentalVehicles || + vt.layers.rentalVehicleClusterClose || + vt.layers.rentalVehicleClusterMedium || + vt.layers.rentalVehicleClusterFar; + + if (layer) { + for (let i = 0, ref = layer.length - 1; i <= ref; i++) { + const feature = layer.feature(i); + [[feature.geom]] = feature.loadGeometry(); + this.features.push(pick(feature, ['geom', 'properties'])); + } + } + if ( + this.features.length === 0 || + !this.features.some(feature => + this.shouldShowRentalVehicle( + feature.properties.id, + feature.properties.network, + feature.properties.isDisabled, + ), + ) + ) { + this.canHaveStationUpdates = false; + } else { + // if zoomed out and there is a highlighted station, + // this value will be later reset to true + this.canHaveStationUpdates = zoomedIn; + this.features.forEach(feature => this.draw(feature, zoomedIn)); + } + }, + err => console.log(err), // eslint-disable-line no-console + ); + }) + .catch(err => { + this.timeOfLastFetch = new Date().getTime(); + console.log(err); // eslint-disable-line no-console + }); + }; + + draw = (feature, zoomedIn) => { + const { id, network } = feature.properties; + if (!this.shouldShowRentalVehicle(id, network)) { + return; + } + const iconName = getVehicleRentalStationNetworkIcon( + getVehicleRentalStationNetworkConfig(network, this.config), + ); + + const isHilighted = this.tile.hilightedStops?.includes(id); + + if (zoomedIn) { + this.drawLargeIcon(feature, iconName, isHilighted); + } else if (isHilighted) { + this.canHaveStationUpdates = true; + this.drawHighlighted(feature, iconName); + } else { + this.drawSmallScooterMarker(feature.geom, iconName); + } + }; + + drawLargeIcon = ( + { geom, properties: { operative, vehiclesAvailable } }, + iconName, + isHilighted, + ) => { + drawScooterIcon( + this.tile, + geom, + operative, + vehiclesAvailable, + iconName, + isHilighted, + ); + }; + + drawHighlighted = ({ geom, properties: { id } }, iconName) => { + const callback = ({ station: result }) => { + if (result) { + drawScooterIcon( + this.tile, + geom, + result.operative, + result.vehiclesAvailable, + iconName, + true, + ); + } + return this; + }; + + fetchQuery(this.relayEnvironment, query, { id }, { force: true }) + .toPromise() + .then(callback); + }; + + drawSmallScooterMarker = (geom, iconName) => { + const iconColor = + iconName.includes('secondary') && + this.config.colors.iconColors['mode-scooter-secondary'] + ? this.config.colors.iconColors['mode-scooter-secondary'] + : this.config.colors.iconColors['mode-scooter']; + drawSmallVehicleRentalMarker( + this.tile, + geom, + iconColor, + TransportMode.Scooter, + ); + }; + + onTimeChange = lang => { + const currentTime = new Date().getTime(); + if ( + this.canHaveStationUpdates && + (!this.timeOfLastFetch || + currentTime - this.timeOfLastFetch > REALTIME_REFETCH_FREQUENCY) + ) { + this.fetchAndDraw(lang); + } + }; + + shouldShowRentalVehicle = (id, network, isDisabled) => + (!this.tile.stopsToShow || this.tile.stopsToShow.includes(id)) && + this.config.cityBike.networks[network].enabled && + !isDisabled; + + static getName = () => 'scooter'; +} + +export default RentalVehicles; diff --git a/app/component/map/tile-layer/SelectVehicleRentalStationRow.js b/app/component/map/tile-layer/SelectVehicleRentalStationRow.js index f211acd13c..3105459170 100644 --- a/app/component/map/tile-layer/SelectVehicleRentalStationRow.js +++ b/app/component/map/tile-layer/SelectVehicleRentalStationRow.js @@ -9,12 +9,11 @@ import { getVehicleRentalStationNetworkIcon, hasStationCode, } from '../../../util/vehicleRentalUtils'; -import { PREFIX_BIKESTATIONS } from '../../../util/path'; import { getIdWithoutFeed } from '../../../util/feedScopedIdUtils'; /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ function SelectVehicleRentalStationRow( - { name, network, id, desc }, + { name, network, id, desc, prefix }, { config }, ) { const img = `${getVehicleRentalStationNetworkIcon( @@ -24,7 +23,7 @@ function SelectVehicleRentalStationRow( return (
)} - {((!context.config?.hideWalkLegDurationSummary && + {!context.config?.hideWalkLegDurationSummary && props.isTransitLeg === false && - props.duration > 0) || - isScooter) && ( -
- {props.duration} -
- )} + props.duration > 0 && ( +
+ {props.duration} +
+ )} {isScooter && ( )} diff --git a/app/component/itinerary/Itinerary.js b/app/component/itinerary/Itinerary.js index c2755dd88a..59082b3593 100644 --- a/app/component/itinerary/Itinerary.js +++ b/app/component/itinerary/Itinerary.js @@ -461,9 +461,7 @@ const Itinerary = ( />, ); } else if (leg.mode === 'SCOOTER' && leg.rentedBike) { - const scooterDuration = Math.floor( - (leg.endTime - leg.startTime) / 1000 / 60, - ); + const scooterDuration = Math.floor(leg.duration / 60); legs.push( ) : null; let appendClass; - const isScooter = networkType === CityBikeNetworkType.Scooter; + if (returnNotice) { appendClass = 'return-citybike'; } From 9d637d797dc2819b91d40bbb1c44fd7671ed8529 Mon Sep 17 00:00:00 2001 From: sharhio Date: Mon, 29 Apr 2024 16:24:43 +0300 Subject: [PATCH 07/79] DT-6182 scooter return removed from walkleg, always shows --- test/unit/WalkLeg.test.js | 58 --------------------------------------- 1 file changed, 58 deletions(-) diff --git a/test/unit/WalkLeg.test.js b/test/unit/WalkLeg.test.js index ed8edcb032..fc634ab982 100644 --- a/test/unit/WalkLeg.test.js +++ b/test/unit/WalkLeg.test.js @@ -3,7 +3,6 @@ import { FormattedMessage } from 'react-intl'; import { shallowWithIntl } from './helpers/mock-intl-enzyme'; import WalkLeg from '../../app/component/itinerary/WalkLeg'; -import { CityBikeNetworkType } from '../../app/util/vehicleRentalUtils'; import ServiceAlertIcon from '../../app/component/ServiceAlertIcon'; import { AlertSeverityLevelType } from '../../app/constants'; @@ -90,63 +89,6 @@ describe('', () => { ); }); - it('should tell the user to return a rented kick scooter to the starting point station', () => { - const props = { - focusAction: () => {}, - focusToLeg: () => {}, - index: 2, - leg: { - distance: 284.787, - duration: 289, - from: { - name: 'Veturitori', - stop: null, - }, - to: { - name: 'Testipaikka', - stop: null, - }, - mode: 'WALK', - rentedBike: false, - start: { scheduledTime: new Date(1529589709000).toISOString() }, - end: { scheduledTime: new Date(1529589701000).toISOString() }, - }, - previousLeg: { - distance: 3297.017000000001, - duration: 904, - from: { - vehicleRentalStation: { - network: 'foobar', - }, - name: 'Kaisaniemenpuisto', - stop: null, - }, - to: { - name: 'Testipaikka', - stop: null, - }, - mode: 'BICYCLE', - rentedBike: true, - start: { scheduledTime: new Date(1529588805000).toISOString() }, - end: { scheduledTime: new Date(1529589701000).toISOString() }, - }, - }; - - const wrapper = shallowWithIntl(, { - context: { - config: { - cityBike: { - networks: { foobar: { type: CityBikeNetworkType.Scooter } }, - }, - }, - }, - }); - - expect(wrapper.find(FormattedMessage).at(0).prop('id')).to.equal( - 'return-scooter-to', - ); - }); - it('should show a service alert icon if there is one at the "from" stop', () => { const startTime = 1529589709000; const props = { From 26886ec0478f1bc997cd313008a3393a1f8f974b Mon Sep 17 00:00:00 2001 From: sharhio Date: Thu, 2 May 2024 16:47:48 +0300 Subject: [PATCH 08/79] DT-6182 scooter cluster details view, show only bikestations --- app/component/RentalVehicleContent.js | 40 +++++++++++- .../map/tile-layer/RentalVehicles.js | 7 +++ .../map/tile-layer/TileLayerContainer.js | 10 ++- .../map/tile-layer/VehicleRentalStations.js | 1 + app/component/rental-vehicle-content.scss | 61 ++++++++++++------- app/routes.js | 2 +- app/translations.js | 6 ++ 7 files changed, 103 insertions(+), 24 deletions(-) diff --git a/app/component/RentalVehicleContent.js b/app/component/RentalVehicleContent.js index 16a5c2b2f6..ad91985972 100644 --- a/app/component/RentalVehicleContent.js +++ b/app/component/RentalVehicleContent.js @@ -16,7 +16,7 @@ import VehicleRentalLeg from './itinerary/VehicleRentalLeg'; import BackButton from './BackButton'; const RentalVehicleContent = ( - { rentalVehicle, breakpoint, router, error, language }, + { rentalVehicle, breakpoint, router, error, language, match }, { config }, ) => { const [isClient, setClient] = useState(false); @@ -25,6 +25,8 @@ const RentalVehicleContent = ( setClient(true); }); + const networks = match.params.networks?.split(','); + // throw error in client side relay query fails if (isClient && error && !rentalVehicle) { throw error.message; @@ -47,6 +49,41 @@ const RentalVehicleContent = ( !rentalVehicle.operative, ); + if (networks) { + return ( +
+
+ {breakpoint === 'large' && ( + + )} +
+
+ +
+ {networks.map(network => ( +
+
+ +
+
+

+ {networkConfig.name[language] || rentalVehicle.network} +

+
+ +
+
+
+
+
+ ))} +
+ ); + } + return (
@@ -82,6 +119,7 @@ RentalVehicleContent.propTypes = { router: routerShape.isRequired, error: PropTypes.object, language: PropTypes.string.isRequired, + match: PropTypes.object.isRequired, }; RentalVehicleContent.contextTypes = { diff --git a/app/component/map/tile-layer/RentalVehicles.js b/app/component/map/tile-layer/RentalVehicles.js index 8fd19573c6..696c6431e0 100644 --- a/app/component/map/tile-layer/RentalVehicles.js +++ b/app/component/map/tile-layer/RentalVehicles.js @@ -53,6 +53,13 @@ class RentalVehicles { : getLayerBaseUrl(this.config.URL.RENTAL_VEHICLE_MAP, lang); if (this.tile.coords.z >= 14) { + baseUrl = getLayerBaseUrl( + this.config.URL.RENTAL_VEHICLE_CLUSTER_FAR_MAP, // 120m + lang, + ); + } + + if (this.tile.coords.z >= 15) { baseUrl = getLayerBaseUrl( this.config.URL.RENTAL_VEHICLE_CLUSTER_MEDIUM_MAP, // 100m lang, diff --git a/app/component/map/tile-layer/TileLayerContainer.js b/app/component/map/tile-layer/TileLayerContainer.js index 006be32e06..c4f1a8e9ad 100644 --- a/app/component/map/tile-layer/TileLayerContainer.js +++ b/app/component/map/tile-layer/TileLayerContainer.js @@ -213,10 +213,18 @@ class TileLayerContainer extends GridLayer { selectableTargets.length === 1 && selectableTargets[0].layer === 'scooter' ) { + const networks = new Set(); + if (selectableTargets[0].feature.properties.vehicles) { + JSON.parse(selectableTargets[0].feature.properties.vehicles).forEach( + vehicle => { + networks.add(vehicle.properties.network); + }, + ); + } this.context.router.push( `/${PREFIX_RENTALVEHICLES}/${encodeURIComponent( selectableTargets[0].feature.properties.id, - )}`, + )}/${[...networks]}`, ); return; } diff --git a/app/component/map/tile-layer/VehicleRentalStations.js b/app/component/map/tile-layer/VehicleRentalStations.js index 15054ee98f..58b4a5e2d7 100644 --- a/app/component/map/tile-layer/VehicleRentalStations.js +++ b/app/component/map/tile-layer/VehicleRentalStations.js @@ -197,6 +197,7 @@ class VehicleRentalStations { shouldShowStation = (id, network) => (!this.tile.stopsToShow || this.tile.stopsToShow.includes(id)) && !this.tile.objectsToHide.vehicleRentalStations.includes(id) && + this.config.cityBike.networks[network].type === 'citybike' && showCitybikeNetwork(this.config.cityBike.networks[network], this.config); static getName = () => 'citybike'; diff --git a/app/component/rental-vehicle-content.scss b/app/component/rental-vehicle-content.scss index 3b95565e78..8b562a406c 100644 --- a/app/component/rental-vehicle-content.scss +++ b/app/component/rental-vehicle-content.scss @@ -49,6 +49,12 @@ .scooter-page-container { margin: 0 3.75em; + .scooter-cluster-back-button-container .icon-container .icon { + width: 48px; + height: 48px; + margin-top: 90px; + } + .scooter-box { min-height: 120px; border-radius: 10px; @@ -62,6 +68,12 @@ line-height: 18px; letter-spacing: -0.03em; + &.cluster { + margin-top: 10px; + min-height: 75px; + padding: 0; + } + .disclaimer-header { display: flex; font-size: $font-size-large; @@ -100,31 +112,30 @@ .citybike-itinerary { display: flex; padding: 7px; - + .citybike-itinerary-text-container { display: flex; - } + } .headsign { font-weight: $font-weight-medium; display: block; font-size: 15px; padding: 2px 0 3px 6px; - } + } .scooter-headsign { - padding: 6px 0 3px 6px; - - .rental-vehicle-link - .external-link-container { - border: none !important; - - .external-link { - font-weight: $font-weight-medium; - font-size: 15px; - color: $link-color; - text-decoration: none - } + padding: 6px 0 3px 6px; + + .rental-vehicle-link .external-link-container { + border: none !important; + + .external-link { + font-weight: $font-weight-medium; + font-size: 15px; + color: $link-color; + text-decoration: none; + } } } @@ -145,7 +156,7 @@ float: left; padding-right: 2px; margin: auto; - + &.small { .icon-badge { top: 2.33em; @@ -218,18 +229,26 @@ color: #666; font-weight: normal; padding-top: 2px; + + &.scooters-available { + margin-top: 22px; + padding-left: 5px; + } } .scooter-content-container { padding: 15px 14px 22px; - display: flex; + display: flex; height: 48px; box-sizing: content-box; - .icon-container - .icon { - width: 48px; - height: 48px; + &.cluster { + padding: 20px 20px; + } + + .icon-container .icon { + width: 48px; + height: 48px; } } } diff --git a/app/routes.js b/app/routes.js index bd19de1003..59aac38ca8 100644 --- a/app/routes.js +++ b/app/routes.js @@ -254,7 +254,7 @@ export default config => { ), }} - + {{ content: ( Date: Mon, 6 May 2024 10:08:26 +0300 Subject: [PATCH 09/79] DT-6182 scooter lighter grey color scheme --- app/component/itinerary/Itinerary.js | 2 +- app/component/itinerary/itinerary-summary.scss | 5 ++--- app/component/itinerary/itinerary.scss | 6 +++--- app/configurations/config.default.js | 1 + app/configurations/config.hsl.js | 2 +- sass/themes/default/_theme.scss | 2 +- static/assets/svg-sprite.default.svg | 11 ++++++++--- static/assets/svg-sprite.hsl.svg | 16 +++++----------- 8 files changed, 22 insertions(+), 23 deletions(-) diff --git a/app/component/itinerary/Itinerary.js b/app/component/itinerary/Itinerary.js index 59082b3593..10acc19270 100644 --- a/app/component/itinerary/Itinerary.js +++ b/app/component/itinerary/Itinerary.js @@ -180,7 +180,7 @@ export const ModeLeg = ( ), ); } else if (mode === 'SCOOTER') { - networkIcon = 'icon-icon_scooter_rider_white'; + networkIcon = 'icon-icon_scooter_rider'; } const routeNumber = ( - + @@ -2812,8 +2812,13 @@ - - + + + + + + + diff --git a/static/assets/svg-sprite.hsl.svg b/static/assets/svg-sprite.hsl.svg index 3574e8b1c3..ea42b96ae9 100644 --- a/static/assets/svg-sprite.hsl.svg +++ b/static/assets/svg-sprite.hsl.svg @@ -2725,7 +2725,7 @@ - + @@ -2734,19 +2734,13 @@ - - + + - - - - - - - - + + From 713e89506fb0d4df02a7cc9edc5f399339962c59 Mon Sep 17 00:00:00 2001 From: sharhio Date: Tue, 7 May 2024 17:03:47 +0300 Subject: [PATCH 10/79] DT-6182 search nearest by network, scooters are never default --- app/component/RouteNumber.js | 2 +- app/component/StopsNearYouContainer.js | 4 ++++ app/component/StopsNearYouMapContainer.js | 4 ++++ app/component/StopsNearYouPage.js | 8 ++++++++ app/component/itinerary/Itinerary.js | 2 +- app/component/map/tile-layer/VehicleRentalStations.js | 1 - app/util/modeUtils.js | 9 +++++---- app/util/planParamUtil.js | 9 +++++++-- app/util/vehicleRentalUtils.js | 5 ++++- build/schema.graphql | 3 +++ .../schema/schema.graphql | 3 +++ 11 files changed, 40 insertions(+), 10 deletions(-) diff --git a/app/component/RouteNumber.js b/app/component/RouteNumber.js index 908283b478..1134f81fb6 100644 --- a/app/component/RouteNumber.js +++ b/app/component/RouteNumber.js @@ -159,7 +159,7 @@ function RouteNumber(props, context) { {props.duration}
)} - {isScooter && ( + {isScooter && !props.vertical && ( )} diff --git a/app/component/StopsNearYouContainer.js b/app/component/StopsNearYouContainer.js index 54138f3578..ec3f4c2714 100644 --- a/app/component/StopsNearYouContainer.js +++ b/app/component/StopsNearYouContainer.js @@ -346,6 +346,7 @@ const refetchContainer = createPaginationContainer( after: { type: "String" } maxResults: { type: "Int" } maxDistance: { type: "Int" } + filterByNetworkNames: { type: "[String]", defaultValue: null } ) { nearest( lat: $lat @@ -356,6 +357,7 @@ const refetchContainer = createPaginationContainer( after: $after maxResults: $maxResults maxDistance: $maxDistance + filterByNetworkNames: $filterByNetworkNames ) @connection(key: "StopsNearYouContainer_nearest") { edges { node { @@ -420,6 +422,7 @@ const refetchContainer = createPaginationContainer( $maxDistance: Int! $startTime: Long! $omitNonPickups: Boolean! + $filterByNetworkNames: [String!] ) { viewer { ...StopsNearYouContainer_stopPatterns @@ -434,6 +437,7 @@ const refetchContainer = createPaginationContainer( after: $after maxResults: $maxResults maxDistance: $maxDistance + filterByNetworkNames: $filterByNetworkNames ) } } diff --git a/app/component/StopsNearYouMapContainer.js b/app/component/StopsNearYouMapContainer.js index 705f855f9e..ce25be206c 100644 --- a/app/component/StopsNearYouMapContainer.js +++ b/app/component/StopsNearYouMapContainer.js @@ -48,6 +48,7 @@ const containerComponent = createPaginationContainer( after: { type: "String" } maxResults: { type: "Int" } maxDistance: { type: "Int" } + filterByNetworkNames: { type: "[String]", defaultValue: null } ) { nearest( lat: $lat @@ -58,6 +59,7 @@ const containerComponent = createPaginationContainer( after: $after maxResults: $maxResults maxDistance: $maxDistance + filterByNetworkNames: $filterByNetworkNames ) @connection(key: "StopsNearYouMapContainer_nearest") { edges { node { @@ -187,6 +189,7 @@ const containerComponent = createPaginationContainer( $maxDistance: Int! $startTime: Long! $omitNonPickups: Boolean! + $filterByNetworkNames: [String] ) { viewer { ...StopsNearYouMapContainer_stopsNearYou @@ -201,6 +204,7 @@ const containerComponent = createPaginationContainer( after: $after maxResults: $maxResults maxDistance: $maxDistance + filterByNetworkNames: $filterByNetworkNames ) } } diff --git a/app/component/StopsNearYouPage.js b/app/component/StopsNearYouPage.js index 19cebb6891..453169f566 100644 --- a/app/component/StopsNearYouPage.js +++ b/app/component/StopsNearYouPage.js @@ -40,6 +40,7 @@ import { mapLayerShape } from '../store/MapLayerStore'; import { getVehicleRentalStationNetworkConfig, getVehicleRentalStationNetworkId, + getDefaultNetworks, } from '../util/vehicleRentalUtils'; import { getMapLayerOptions } from '../util/mapLayerUtils'; import { @@ -214,9 +215,11 @@ class StopsNearYouPage extends React.Component { const { searchPosition } = this.state; let placeTypes = ['STOP', 'STATION']; let modes = [mode]; + let allowedNetworks = []; if (mode === 'CITYBIKE') { placeTypes = 'VEHICLE_RENT'; modes = ['BICYCLE']; + allowedNetworks = getDefaultNetworks(this.context.config); } const prioritizedStops = this.context.config.prioritizedStopsNearYou[mode.toLowerCase()] || []; @@ -232,6 +235,7 @@ class StopsNearYouPage extends React.Component { omitNonPickups: this.context.config.omitNonPickups, feedIds: this.context.config.feedIds, prioritizedStopIds: prioritizedStops, + filterByNetworkNames: allowedNetworks, }; }; @@ -446,6 +450,7 @@ class StopsNearYouPage extends React.Component { $maxDistance: Int! $omitNonPickups: Boolean! $feedIds: [String!] + $filterByNetworkNames: [String!] ) { stopPatterns: viewer { ...StopsNearYouContainer_stopPatterns @@ -458,6 +463,7 @@ class StopsNearYouPage extends React.Component { maxResults: $maxResults maxDistance: $maxDistance omitNonPickups: $omitNonPickups + filterByNetworkNames: $filterByNetworkNames ) } alerts: alerts(feeds: $feedIds, severityLevel: [SEVERE]) { @@ -739,6 +745,7 @@ class StopsNearYouPage extends React.Component { $maxDistance: Int! $omitNonPickups: Boolean! $prioritizedStopIds: [String!]! + $filterByNetworkNames: [String!] ) { stops: viewer { ...StopsNearYouMapContainer_stopsNearYou @@ -751,6 +758,7 @@ class StopsNearYouPage extends React.Component { maxResults: $maxResults maxDistance: $maxDistance omitNonPickups: $omitNonPickups + filterByNetworkNames: $filterByNetworkNames ) } prioritizedStops: stops(ids: $prioritizedStopIds) { diff --git a/app/component/itinerary/Itinerary.js b/app/component/itinerary/Itinerary.js index 10acc19270..04f1304218 100644 --- a/app/component/itinerary/Itinerary.js +++ b/app/component/itinerary/Itinerary.js @@ -464,7 +464,7 @@ const Itinerary = ( const scooterDuration = Math.floor(leg.duration / 60); legs.push( (!this.tile.stopsToShow || this.tile.stopsToShow.includes(id)) && !this.tile.objectsToHide.vehicleRentalStations.includes(id) && - this.config.cityBike.networks[network].type === 'citybike' && showCitybikeNetwork(this.config.cityBike.networks[network], this.config); static getName = () => 'citybike'; diff --git a/app/util/modeUtils.js b/app/util/modeUtils.js index 7b3a0a6315..9454ba91c9 100644 --- a/app/util/modeUtils.js +++ b/app/util/modeUtils.js @@ -32,11 +32,12 @@ export function isCitybikePreSeasonActive(season) { return false; } -export function showCitybikeNetwork(network, config) { +export function showCitybikeNetwork(networkConfig, config) { return ( - network?.enabled && - (isCitybikeSeasonActive(network?.season) || - isCitybikePreSeasonActive(network?.season) || + networkConfig?.enabled && + networkConfig.type === 'citybike' && + (isCitybikeSeasonActive(networkConfig?.season) || + isCitybikePreSeasonActive(networkConfig?.season) || isDevelopmentEnvironment(config)) ); } diff --git a/app/util/planParamUtil.js b/app/util/planParamUtil.js index 641a1afe40..ea15281186 100644 --- a/app/util/planParamUtil.js +++ b/app/util/planParamUtil.js @@ -6,9 +6,10 @@ import { isTransportModeAvailable, } from './modeUtils'; import { otpToLocation, getIntermediatePlaces } from './otpStrings'; -import { getDefaultNetworks } from './vehicleRentalUtils'; +import { getAllNetworksOfType, getDefaultNetworks } from './vehicleRentalUtils'; import { getCustomizedSettings } from '../store/localStorage'; import { estimateItineraryDistance } from './geo-utils'; +import { TransportMode } from '../constants'; export const PLANTYPE = { WALK: 'WALK', @@ -73,6 +74,10 @@ export function getSettings(config) { const defaultSettings = getDefaultSettings(config); const userSettings = getCustomizedSettings(); const allNetworks = getDefaultNetworks(config); + const allScooterNetworks = getAllNetworksOfType( + config, + TransportMode.Scooter, + ); // const allScooterNetworks = getAllScooterNetworks(config); const settings = { @@ -96,7 +101,7 @@ export function getSettings(config) { allowedScooterRentalNetworks: userSettings.allowedScooterRentalNetworks?.length > 0 ? userSettings.allowedScooterRentalNetworks.filter(network => - allNetworks.includes(network), + allScooterNetworks.includes(network), ) : defaultSettings.allowedScooterRentalNetworks, }; diff --git a/app/util/vehicleRentalUtils.js b/app/util/vehicleRentalUtils.js index 1072948c46..90a9fdc2cd 100644 --- a/app/util/vehicleRentalUtils.js +++ b/app/util/vehicleRentalUtils.js @@ -62,7 +62,10 @@ export const getVehicleRentalStationNetworkConfig = (networkId, config) => { export const getDefaultNetworks = config => { const mappedNetworks = []; Object.entries(config.cityBike.networks).forEach(n => { - if (citybikeRoutingIsActive(n[1], config)) { + if ( + citybikeRoutingIsActive(n[1], config) && + n[1]?.type !== CityBikeNetworkType.Scooter // scooter networks are never on by default + ) { mappedNetworks.push(n[0]); } }); diff --git a/build/schema.graphql b/build/schema.graphql index dcce7c0d38..26c59a1683 100644 --- a/build/schema.graphql +++ b/build/schema.graphql @@ -3531,6 +3531,9 @@ type QueryType { """ filterByModes: [Mode] + """Only include places that match one of the given network names.""" + filterByNetworkNames: [String] + """Only include places that match one of the given GTFS ids.""" filterByIds: InputFilters @deprecated(reason: "Not actively maintained") before: String diff --git a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql index dcce7c0d38..26c59a1683 100644 --- a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql +++ b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql @@ -3531,6 +3531,9 @@ type QueryType { """ filterByModes: [Mode] + """Only include places that match one of the given network names.""" + filterByNetworkNames: [String] + """Only include places that match one of the given GTFS ids.""" filterByIds: InputFilters @deprecated(reason: "Not actively maintained") before: String From ab3281716f48e4268e75f28a6582cc7310c33d4f Mon Sep 17 00:00:00 2001 From: sharhio Date: Tue, 7 May 2024 17:04:38 +0300 Subject: [PATCH 11/79] DT-6182 remove scooters from unnecessary maplayers --- app/component/map/RoutePageMap.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/component/map/RoutePageMap.js b/app/component/map/RoutePageMap.js index b98f1ed07c..0bc13a98c9 100644 --- a/app/component/map/RoutePageMap.js +++ b/app/component/map/RoutePageMap.js @@ -186,10 +186,10 @@ const RoutePageMapWithVehicles = connectToStores( ['RealTimeInformationStore', 'MapLayerStore'], ({ getStore }, { trip }) => { const mapLayers = getStore('MapLayerStore').getMapLayers({ - notThese: ['stop', 'citybike', 'vehicles'], + notThese: ['stop', 'citybike', 'vehicles', 'scooter'], }); const mapLayerOptions = getMapLayerOptions({ - lockedMapLayers: ['vehicles', 'stop', 'citybike'], + lockedMapLayers: ['vehicles', 'stop', 'citybike', 'scooter'], selectedMapLayers: ['vehicles'], }); if (trip) { From 7d579db7a48b87657976e4c0777c1114f450aacf Mon Sep 17 00:00:00 2001 From: sharhio Date: Mon, 27 May 2024 11:25:40 +0300 Subject: [PATCH 12/79] DT-6182 show only configured stations or vehicles on map + fixes --- app/component/map/non-tile-layer/VehicleMarker.js | 6 +++--- app/component/map/tile-layer/TileContainer.js | 4 ++-- app/component/map/tile-layer/VehicleRentalStations.js | 3 ++- app/configurations/config.hsl.js | 6 ++++++ app/util/modeUtils.js | 5 ++++- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/component/map/non-tile-layer/VehicleMarker.js b/app/component/map/non-tile-layer/VehicleMarker.js index 694305985c..0890f22abc 100644 --- a/app/component/map/non-tile-layer/VehicleMarker.js +++ b/app/component/map/non-tile-layer/VehicleMarker.js @@ -86,16 +86,16 @@ export default class VehicleMarker extends React.Component { img: iconName, className: 'city-bike-medium-size', badgeFill: getVehicleAvailabilityIndicatorColor( - rental?.availableVehicles.total, + rental?.availableVehicles?.total, config, ), badgeTextFill: getVehicleAvailabilityTextColor( - rental?.availableVehicles.total, + rental?.availableVehicles?.total, config, ), badgeText: vehicleCapacity !== BIKEAVL_UNKNOWN - ? rental?.availableVehicles.total + ? rental?.availableVehicles?.total : null, }) : Icon.asString({ diff --git a/app/component/map/tile-layer/TileContainer.js b/app/component/map/tile-layer/TileContainer.js index e5e125111b..e9db0f5d1e 100644 --- a/app/component/map/tile-layer/TileContainer.js +++ b/app/component/map/tile-layer/TileContainer.js @@ -238,14 +238,14 @@ class TileContainer { // features array is sorted by y coord so combo stops should be next to each other if ( index > 0 && - features[index - 1].feature.properties.code === + features[index - 1]?.feature.properties.code === feature.feature.properties.code ) { isCombo = true; } if ( index < features.length - 1 && - features[index + 1].feature.properties.code === + features[index + 1]?.feature.properties.code === feature.feature.properties.code ) { isCombo = true; diff --git a/app/component/map/tile-layer/VehicleRentalStations.js b/app/component/map/tile-layer/VehicleRentalStations.js index 85d83325ed..660c490b2e 100644 --- a/app/component/map/tile-layer/VehicleRentalStations.js +++ b/app/component/map/tile-layer/VehicleRentalStations.js @@ -197,7 +197,8 @@ class VehicleRentalStations { shouldShowStation = (id, network) => (!this.tile.stopsToShow || this.tile.stopsToShow.includes(id)) && !this.tile.objectsToHide.vehicleRentalStations.includes(id) && - showCitybikeNetwork(this.config.cityBike.networks[network]); + showCitybikeNetwork(this.config.cityBike.networks[network]) && + this.config.cityBike.networks[network].showRentalStations; static getName = () => 'citybike'; } diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index 33bac7ff0d..45e751e110 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -471,6 +471,7 @@ export default { en: 'https://www.hsl.fi/en/citybikes/helsinki/instructions#ride', }, timeBeforeSurcharge: 60 * 60, + showRentalStations: true, }, vantaa: { enabled: true, @@ -498,9 +499,13 @@ export default { en: 'https://www.hsl.fi/en/citybikes/vantaa/instructions#ride', }, timeBeforeSurcharge: 120 * 60, + showRentalStations: true, }, bolt: { enabled: true, + season: { + alwaysOn: true, + }, icon: 'scooter', name: { fi: 'Bolt', @@ -508,6 +513,7 @@ export default { en: 'Bolt', }, type: 'scooter', + showRentalVehicles: true, }, }, buyUrl: { diff --git a/app/util/modeUtils.js b/app/util/modeUtils.js index 69c7f06979..41313b4b0b 100644 --- a/app/util/modeUtils.js +++ b/app/util/modeUtils.js @@ -20,6 +20,9 @@ export function isCitybikeSeasonActive(season) { if (!season) { return false; } + if (season.alwaysOn) { + return true; + } const now = Date.now(); return now <= seasonMs(season.end) + dayMs && now >= seasonMs(season.start); } @@ -82,7 +85,7 @@ export function showRentalVehiclesOfType(networks, config, type) { return Object.values(networks).some( network => network.type === type.toLowerCase() && - (network.type !== 'citybike' || showCitybikeNetwork(network, config)), + (network.showRentalVehicles || showCitybikeNetwork(network, config)), ); } From 1f12e46af5ac77c8ac018d88a1d52f5d09a005c2 Mon Sep 17 00:00:00 2001 From: sharhio Date: Mon, 27 May 2024 11:28:23 +0300 Subject: [PATCH 13/79] DT-6182 schema update rentalvehicle systemurl --- build/schema.graphql | 4 ++-- .../digitransit-search-util-query-utils/schema/schema.graphql | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/schema.graphql b/build/schema.graphql index d029124451..243fa7c6e9 100644 --- a/build/schema.graphql +++ b/build/schema.graphql @@ -743,8 +743,8 @@ type RentalVehicle implements Node & PlaceInterface { """The type of the rental vehicle (scooter, bicycle, car...)""" vehicleType: RentalVehicleType - """System URL""" - systemUrl: String + """The rental vehicle operator's system URL.""" + systemUrl: String! } type BikeRentalStationUris { diff --git a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql index d029124451..243fa7c6e9 100644 --- a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql +++ b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql @@ -743,8 +743,8 @@ type RentalVehicle implements Node & PlaceInterface { """The type of the rental vehicle (scooter, bicycle, car...)""" vehicleType: RentalVehicleType - """System URL""" - systemUrl: String + """The rental vehicle operator's system URL.""" + systemUrl: String! } type BikeRentalStationUris { From b7e22cf6272be2226ef0b8e1d2bb1c49b3fd2230 Mon Sep 17 00:00:00 2001 From: sharhio Date: Mon, 10 Jun 2024 16:44:27 +0300 Subject: [PATCH 14/79] DT-6182 filter out invisible stations --- .../map/tile-layer/VehicleRentalStations.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/component/map/tile-layer/VehicleRentalStations.js b/app/component/map/tile-layer/VehicleRentalStations.js index 660c490b2e..b1ac1283e0 100644 --- a/app/component/map/tile-layer/VehicleRentalStations.js +++ b/app/component/map/tile-layer/VehicleRentalStations.js @@ -82,16 +82,16 @@ class VehicleRentalStations { } } - if ( - this.features.length === 0 || - !this.features.some(feature => - this.shouldShowStation( - feature.properties.id, - feature.properties.network, - feature.properties.formFactors, - ), - ) - ) { + // Must filter out stations that are not shown as there can be a large amount of invisible rental stations, + // which are often accidentally clicked + this.features = this.features.filter(feature => + this.shouldShowStation( + feature.properties.id, + feature.properties.network, + feature.properties.formFactors, + ), + ); + if (this.features.length === 0) { this.canHaveStationUpdates = false; } else { // if zoomed out and there is a highlighted station, @@ -195,10 +195,10 @@ class VehicleRentalStations { }; shouldShowStation = (id, network) => + this.config.cityBike.networks[network].showRentalStations && (!this.tile.stopsToShow || this.tile.stopsToShow.includes(id)) && !this.tile.objectsToHide.vehicleRentalStations.includes(id) && - showCitybikeNetwork(this.config.cityBike.networks[network]) && - this.config.cityBike.networks[network].showRentalStations; + showCitybikeNetwork(this.config.cityBike.networks[network]); static getName = () => 'citybike'; } From 2d9d0053116b496227d9accf20b8fea29c94237a Mon Sep 17 00:00:00 2001 From: sharhio Date: Tue, 11 Jun 2024 17:21:58 +0300 Subject: [PATCH 15/79] DT-6182 ui clustering --- .../map/tile-layer/MarkerSelectPopup.js | 11 +- .../map/tile-layer/RentalVehicles.js | 237 +++++++++--------- ...tationRow.js => SelectVehicleRentalRow.js} | 36 +-- .../map/tile-layer/TileLayerContainer.js | 28 ++- app/configurations/config.hsl.js | 1 + app/util/mapIconUtils.js | 17 +- static/assets/svg-sprite.default.svg | 2 +- static/assets/svg-sprite.hsl.svg | 2 +- 8 files changed, 167 insertions(+), 167 deletions(-) rename app/component/map/tile-layer/{SelectVehicleRentalStationRow.js => SelectVehicleRentalRow.js} (64%) diff --git a/app/component/map/tile-layer/MarkerSelectPopup.js b/app/component/map/tile-layer/MarkerSelectPopup.js index 0602285389..f85e2b9f68 100644 --- a/app/component/map/tile-layer/MarkerSelectPopup.js +++ b/app/component/map/tile-layer/MarkerSelectPopup.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { FormattedMessage, intlShape } from 'react-intl'; import SelectStopRow from './SelectStopRow'; -import SelectVehicleRentalStationRow from './SelectVehicleRentalStationRow'; +import SelectVehicleRentalRow from './SelectVehicleRentalRow'; import SelectParkAndRideRow from './SelectParkAndRideRow'; import SelectVehicleContainer from './SelectVehicleContainer'; import { popupColorShape } from '../../../util/shapes'; @@ -41,19 +41,22 @@ function MarkerSelectPopup(props, { intl }) { } if (option.layer === 'citybike') { return ( - ); } - if (option.layer === 'scooter') { + // show only scooters that are clusters (avoids massive lists) + if (option.layer === 'scooter' && option.feature.properties.cluster) { return ( - { const zoomedIn = this.tile.coords.z > this.config.cityBike.cityBikeSmallIconZoom; - let baseUrl = zoomedIn - ? getLayerBaseUrl(this.config.URL.REALTIME_RENTAL_VEHICLE_MAP, lang) - : getLayerBaseUrl(this.config.URL.RENTAL_VEHICLE_MAP, lang); - - if (this.tile.coords.z >= 14) { - baseUrl = getLayerBaseUrl( - this.config.URL.RENTAL_VEHICLE_CLUSTER_FAR_MAP, // 120m - lang, - ); - } - - if (this.tile.coords.z >= 15) { - baseUrl = getLayerBaseUrl( - this.config.URL.RENTAL_VEHICLE_CLUSTER_MEDIUM_MAP, // 100m - lang, - ); - } - if (this.tile.coords.z >= 16) { - baseUrl = getLayerBaseUrl( - this.config.URL.RENTAL_VEHICLE_CLUSTER_CLOSE_MAP, // 50m - lang, - ); - } - if (this.tile.coords.z >= 18) { - baseUrl = getLayerBaseUrl( - this.config.URL.RENTAL_VEHICLE_MAP, // No clustering - lang, - ); - } - + const baseUrl = getLayerBaseUrl(this.config.URL.RENTAL_VEHICLE_MAP, lang); const tileUrl = `${baseUrl}${ this.tile.coords.z + (this.tile.props.zoomOffset || 0) }/${this.tile.coords.x}/${this.tile.coords.y}.pbf`; @@ -93,36 +54,44 @@ class RentalVehicles { const vt = new VectorTile(new Protobuf(buf)); this.features = []; - const layer = - vt.layers.rentalVehicles || - vt.layers.realtimeRentalVehicles || - vt.layers.rentalVehicleClusterClose || - vt.layers.rentalVehicleClusterMedium || - vt.layers.rentalVehicleClusterFar; + const layer = vt.layers.rentalVehicles; + const settings = getSettings(this.config); + const allowedScooterNetworks = + settings.allowedScooterRentalNetworks; + const scooterIconPrefix = `icon-icon_scooter`; if (layer) { for (let i = 0, ref = layer.length - 1; i <= ref; i++) { const feature = layer.feature(i); [[feature.geom]] = feature.loadGeometry(); - this.features.push(pick(feature, ['geom', 'properties'])); + // Filter out vehicles that are not in the allowedScooterNetworks (selected by a user) to avoid including unwanted vehicles in clusters + // Also Filter out vehicles that should not be shown to avoid user accidentally clicking on invisible objects on the map + if ( + allowedScooterNetworks.includes(feature.properties.network) && + this.shouldShowRentalVehicle( + feature.properties.id, + feature.properties.network, + feature.properties.isDisabled, + feature.properties.formFactor, + ) + ) { + this.features.push(pick(feature, ['geom', 'properties'])); + } } } - if ( - this.features.length === 0 || - !this.features.some(feature => - this.shouldShowRentalVehicle( - feature.properties.id, - feature.properties.network, - feature.properties.isDisabled, - ), - ) - ) { + + if (this.features.length === 0) { this.canHaveStationUpdates = false; } else { // if zoomed out and there is a highlighted station, // this value will be later reset to true this.canHaveStationUpdates = zoomedIn; - this.features.forEach(feature => this.draw(feature, zoomedIn)); + + if (this.tile.coords.z >= 13 && this.tile.coords.z < 18) { + this.clusterAndDraw(zoomedIn, scooterIconPrefix); + } else { + this.features.forEach(feature => this.draw(feature, zoomedIn)); + } } }, err => console.log(err), // eslint-disable-line no-console @@ -134,62 +103,57 @@ class RentalVehicles { }); }; - draw = (feature, zoomedIn) => { - const { id, network } = feature.properties; - if (!this.shouldShowRentalVehicle(id, network)) { - return; - } - const iconName = getVehicleRentalStationNetworkIcon( - getVehicleRentalStationNetworkConfig(network, this.config), - ); + clusterAndDraw = (zoomedIn, iconPrefix) => { + const index = new Supercluster({ + radius: 40, // in pixels + maxZoom: 17, + minPoints: 2, + extent: 512, // tile size (512) + minZoom: 13, + map: featureProps => ({ + networks: [featureProps.network], + scooterId: featureProps.id, // an id of a vehicle to zoom into when a cluster is clicked + }), + reduce: (accumulated, featureProps) => { + if ( + featureProps.network && + !accumulated.networks.includes(featureProps.network) + ) { + accumulated.networks.push(featureProps.network); + } + return accumulated; + }, + }); + + index.load(this.pointsInSuperclusterFormat()); + const bbox = [-180, -85, 180, 85]; // Bounding box covers the entire world + const clusters = index.getClusters(bbox, this.tile.coords.z); + const clusteredFeatures = []; + + clusters.forEach(clusterFeature => { + const newFeature = this.featureWithGeom(clusterFeature); + clusteredFeatures.push(newFeature); + this.draw(newFeature, zoomedIn, iconPrefix); + }); + this.features = clusteredFeatures; + }; + draw = (feature, zoomedIn, iconPrefix) => { + const { id, network } = feature.properties; + const { geom } = feature; + const iconName = + iconPrefix || + getVehicleRentalStationNetworkIcon( + getVehicleRentalStationNetworkConfig(network, this.config), + ); const isHilighted = this.tile.hilightedStops?.includes(id); - - if (zoomedIn) { - this.drawLargeIcon(feature, iconName, isHilighted); - } else if (isHilighted) { - this.canHaveStationUpdates = true; - this.drawHighlighted(feature, iconName); + if (zoomedIn || isHilighted) { + drawScooterIcon(this.tile, geom, iconName, isHilighted); } else { - this.drawSmallScooterMarker(feature.geom, iconName); + this.drawSmallScooterMarker(geom, iconName); } }; - drawLargeIcon = ( - { geom, properties: { operative, vehiclesAvailable } }, - iconName, - isHilighted, - ) => { - drawScooterIcon( - this.tile, - geom, - operative, - vehiclesAvailable, - iconName, - isHilighted, - ); - }; - - drawHighlighted = ({ geom, properties: { id } }, iconName) => { - const callback = ({ station: result }) => { - if (result) { - drawScooterIcon( - this.tile, - geom, - result.operative, - result.vehiclesAvailable, - iconName, - true, - ); - } - return this; - }; - - fetchQuery(this.relayEnvironment, query, { id }, { force: true }) - .toPromise() - .then(callback); - }; - drawSmallScooterMarker = (geom, iconName) => { const iconColor = iconName.includes('secondary') && @@ -204,23 +168,50 @@ class RentalVehicles { ); }; - onTimeChange = lang => { - const currentTime = new Date().getTime(); - if ( - this.canHaveStationUpdates && - (!this.timeOfLastFetch || - currentTime - this.timeOfLastFetch > REALTIME_REFETCH_FREQUENCY) - ) { - this.fetchAndDraw(lang); - } - }; - - shouldShowRentalVehicle = (id, network, isDisabled) => + shouldShowRentalVehicle = (id, network, isDisabled, formFactor) => (!this.tile.stopsToShow || this.tile.stopsToShow.includes(id)) && - this.config.cityBike.networks[network].enabled && + (!network || + (this.config.cityBike.networks[network].enabled && + this.config.cityBike.networks[network].showRentalVehicles && + this.config.cityBike.networks[network].type === + formFactor.toLowerCase())) && !isDisabled; static getName = () => 'scooter'; + + pointsInSuperclusterFormat = () => { + return this.features.map(feature => { + // Convert the feature's x/y to lat/lon for clustering + const latLon = this.tile.project({ + x: feature.geom.x, + y: feature.geom.y, + }); + return { + type: 'Feature', + properties: { ...feature.properties }, + geom: { ...feature.geom }, + geometry: { + type: 'Point', + coordinates: [latLon.lat, latLon.lon], + }, + }; + }); + }; + + featureWithGeom = clusterFeature => { + // Convert the cluster's lat/lon to x/y + const point = this.tile.latLngToPoint( + clusterFeature.geometry.coordinates[0], + clusterFeature.geometry.coordinates[1], + ); + return { + ...clusterFeature, + geom: { + x: point.x, + y: point.y, + }, + }; + }; } export default RentalVehicles; diff --git a/app/component/map/tile-layer/SelectVehicleRentalStationRow.js b/app/component/map/tile-layer/SelectVehicleRentalRow.js similarity index 64% rename from app/component/map/tile-layer/SelectVehicleRentalStationRow.js rename to app/component/map/tile-layer/SelectVehicleRentalRow.js index 3105459170..55adecd282 100644 --- a/app/component/map/tile-layer/SelectVehicleRentalStationRow.js +++ b/app/component/map/tile-layer/SelectVehicleRentalRow.js @@ -12,19 +12,23 @@ import { import { getIdWithoutFeed } from '../../../util/feedScopedIdUtils'; /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ -function SelectVehicleRentalStationRow( - { name, network, id, desc, prefix }, +function SelectVehicleRentalRow( + { name, network, id, desc, prefix, cluster, networks: networksInCluster }, { config }, ) { - const img = `${getVehicleRentalStationNetworkIcon( - getVehicleRentalStationNetworkConfig(network, config), - )}-stop-lollipop`; + const img = cluster + ? 'icon-icon_scooter-lollipop' + : `${getVehicleRentalStationNetworkIcon( + getVehicleRentalStationNetworkConfig(network, config), + )}-stop-lollipop`; + + const linkAddress = cluster + ? `/${prefix}/${encodeURIComponent(id)}/${[...networksInCluster]}` + : `/${prefix}/${encodeURIComponent(id)}`; + const address = desc || ; return ( - + @@ -44,23 +48,27 @@ function SelectVehicleRentalStationRow( ); } -SelectVehicleRentalStationRow.displayName = 'SelectVehicleRentalStationRow'; +SelectVehicleRentalRow.displayName = 'SelectVehicleRentalRow'; -SelectVehicleRentalStationRow.propTypes = { +SelectVehicleRentalRow.propTypes = { name: PropTypes.string, network: PropTypes.string.isRequired, id: PropTypes.string.isRequired, desc: PropTypes.string, prefix: PropTypes.string.isRequired, + cluster: PropTypes.bool, + networks: PropTypes.arrayOf(PropTypes.string), }; -SelectVehicleRentalStationRow.defaultProps = { +SelectVehicleRentalRow.defaultProps = { desc: undefined, name: undefined, + cluster: false, + networks: [], }; -SelectVehicleRentalStationRow.contextTypes = { +SelectVehicleRentalRow.contextTypes = { config: configShape.isRequired, }; -export default SelectVehicleRentalStationRow; +export default SelectVehicleRentalRow; diff --git a/app/component/map/tile-layer/TileLayerContainer.js b/app/component/map/tile-layer/TileLayerContainer.js index c4f1a8e9ad..7205270c4a 100644 --- a/app/component/map/tile-layer/TileLayerContainer.js +++ b/app/component/map/tile-layer/TileLayerContainer.js @@ -210,21 +210,23 @@ class TileLayerContainer extends GridLayer { return; } if ( - selectableTargets.length === 1 && - selectableTargets[0].layer === 'scooter' + (selectableTargets.length === 1 && + selectableTargets[0].layer === 'scooter') || + (selectableTargets.length > 1 && + selectableTargets.every(target => target.layer === 'scooter')) ) { - const networks = new Set(); - if (selectableTargets[0].feature.properties.vehicles) { - JSON.parse(selectableTargets[0].feature.properties.vehicles).forEach( - vehicle => { - networks.add(vehicle.properties.network); - }, - ); - } + const cluster = selectableTargets.find( + target => target.feature.properties.cluster, + ); + const networks = cluster ? cluster.feature.properties.networks : ''; + const id = cluster + ? cluster.feature.properties.scooterId + : selectableTargets[0].feature.properties.id; + // adding networks directs to scooter cluster view this.context.router.push( - `/${PREFIX_RENTALVEHICLES}/${encodeURIComponent( - selectableTargets[0].feature.properties.id, - )}/${[...networks]}`, + `/${PREFIX_RENTALVEHICLES}/${encodeURIComponent(id)}/${[ + ...networks, + ]}`, ); return; } diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index 45e751e110..4d4daffdf1 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -514,6 +514,7 @@ export default { }, type: 'scooter', showRentalVehicles: true, + showRentalStations: false, }, }, buyUrl: { diff --git a/app/util/mapIconUtils.js b/app/util/mapIconUtils.js index 3a6184cbcb..64629336df 100644 --- a/app/util/mapIconUtils.js +++ b/app/util/mapIconUtils.js @@ -101,8 +101,8 @@ export function getStopIconStyles(type, zoom, isHilighted) { }, 16: { style: 'large', - width: 24, - height: 33, + width: 35, + height: 43, }, }, }; @@ -734,7 +734,7 @@ export function drawCitybikeIcon( * Draw an icon for rental vehicles. * Determine icon size based on zoom level. */ -export function drawScooterIcon(tile, geom, isHilighted) { +export function drawScooterIcon(tile, geom, iconName, isHilighted) { const zoom = tile.coords.z - 1; const styles = getStopIconStyles('scooter', zoom, isHilighted); const { style } = styles; @@ -747,12 +747,10 @@ export function drawScooterIcon(tile, geom, isHilighted) { const radius = width / 2; let x; let y; - if (style === 'medium') { x = geom.x / tile.ratio - width / 2; y = geom.y / tile.ratio - height; - const icon = 'icon-icon_scooter-lollipop'; - + const icon = `${iconName}-lollipop`; getImageFromSpriteCache(icon, width, height).then(image => { tile.ctx.drawImage(image, x, y); if (isHilighted) { @@ -761,20 +759,17 @@ export function drawScooterIcon(tile, geom, isHilighted) { }); } if (style === 'large') { + const icon = `${iconName}-lollipop-large`; const smallCircleRadius = 11 * tile.scaleratio; x = geom.x / tile.ratio - width + smallCircleRadius * 2; y = geom.y / tile.ratio - height; const iconX = x; const iconY = y; - const icon = 'icon-icon_scooter-lollipop'; getImageFromSpriteCache(icon, width, height).then(image => { tile.ctx.drawImage(image, x, y); - x = x + width - smallCircleRadius; - y += smallCircleRadius; - if (isHilighted) { - drawSelectionCircle(tile, iconX, iconY, radius, true, true); + drawSelectionCircle(tile, iconX, iconY, radius, true, false); } }); } diff --git a/static/assets/svg-sprite.default.svg b/static/assets/svg-sprite.default.svg index 640b5d9410..54c6ddca66 100644 --- a/static/assets/svg-sprite.default.svg +++ b/static/assets/svg-sprite.default.svg @@ -2816,7 +2816,7 @@ - + diff --git a/static/assets/svg-sprite.hsl.svg b/static/assets/svg-sprite.hsl.svg index ea42b96ae9..dd801ad4cf 100644 --- a/static/assets/svg-sprite.hsl.svg +++ b/static/assets/svg-sprite.hsl.svg @@ -2738,7 +2738,7 @@ - + From 914af2f3760aa07437a8385f1c2ff7a23afd1002 Mon Sep 17 00:00:00 2001 From: sharhio Date: Tue, 11 Jun 2024 17:25:44 +0300 Subject: [PATCH 16/79] DT-6182 do not add invisible rental stations to map data --- .../map/tile-layer/VehicleRentalStations.js | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/app/component/map/tile-layer/VehicleRentalStations.js b/app/component/map/tile-layer/VehicleRentalStations.js index b1ac1283e0..32cbb3fbeb 100644 --- a/app/component/map/tile-layer/VehicleRentalStations.js +++ b/app/component/map/tile-layer/VehicleRentalStations.js @@ -78,19 +78,20 @@ class VehicleRentalStations { for (let i = 0, ref = layer.length - 1; i <= ref; i++) { const feature = layer.feature(i); [[feature.geom]] = feature.loadGeometry(); - this.features.push(pick(feature, ['geom', 'properties'])); + // Must filter out stations that are not shown as there can be a large amount + // of invisible rental stations, which are often accidentally clicked + if ( + this.shouldShowStation( + feature.properties.id, + feature.properties.network, + feature.properties.formFactors, + ) + ) { + this.features.push(pick(feature, ['geom', 'properties'])); + } } } - // Must filter out stations that are not shown as there can be a large amount of invisible rental stations, - // which are often accidentally clicked - this.features = this.features.filter(feature => - this.shouldShowStation( - feature.properties.id, - feature.properties.network, - feature.properties.formFactors, - ), - ); if (this.features.length === 0) { this.canHaveStationUpdates = false; } else { @@ -111,9 +112,6 @@ class VehicleRentalStations { draw = (feature, zoomedIn) => { const { id, network, formFactors } = feature.properties; - if (!this.shouldShowStation(id, network, formFactors)) { - return; - } const iconName = getVehicleRentalStationNetworkIcon( getVehicleRentalStationNetworkConfig(network, this.config), From 6effb335582a1b89edf4de3d152248fbb9b38853 Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 12 Jun 2024 11:06:21 +0300 Subject: [PATCH 17/79] DT-6182 sorting & settings fixed --- app/component/itinerary/ItineraryPageUtils.js | 2 +- app/component/itinerary/PlanConnection.js | 1 + app/configurations/config.hsl.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/component/itinerary/ItineraryPageUtils.js b/app/component/itinerary/ItineraryPageUtils.js index 1a47e30e0b..9aed9d68d5 100644 --- a/app/component/itinerary/ItineraryPageUtils.js +++ b/app/component/itinerary/ItineraryPageUtils.js @@ -448,7 +448,7 @@ export function mergeScooterTransitPlan(scooterPlan, transitPlan) { ...publicTransitEdges.slice(0, maxTransitEdges), ] .sort((a, b) => { - return a.endTime > b.endTime; + return a.node.end > b.node.end; }) .map(edge => { return { diff --git a/app/component/itinerary/PlanConnection.js b/app/component/itinerary/PlanConnection.js index 09ccd3b2c1..eb11b8860e 100644 --- a/app/component/itinerary/PlanConnection.js +++ b/app/component/itinerary/PlanConnection.js @@ -104,6 +104,7 @@ const planConnection = graphql` } } } + end } } } diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index 4d4daffdf1..df79a30670 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -208,7 +208,7 @@ export default { }, scooter: { availableForSelection: true, - selectedByDefault: true, + defaultValue: false, }, airplane: { availableForSelection: false, From 0dd3714a7f26e5bbbfc5346af111d66c7378df71 Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 12 Jun 2024 13:09:00 +0300 Subject: [PATCH 18/79] DT-6182 cluster row selection, renaming --- app/component/ParkOrStationHeader.js | 4 +- app/component/VehicleRentalStationNearYou.js | 4 +- .../map/tile-layer/MarkerSelectPopup.js | 6 +- .../SelectRentalVehicleClusterRow.js | 70 +++++++++++++++++++ .../map/tile-layer/SelectVehicleRentalRow.js | 22 ++---- app/util/vehicleRentalUtils.js | 6 +- 6 files changed, 87 insertions(+), 25 deletions(-) create mode 100644 app/component/map/tile-layer/SelectRentalVehicleClusterRow.js diff --git a/app/component/ParkOrStationHeader.js b/app/component/ParkOrStationHeader.js index bc3674bc3d..1138701bb7 100644 --- a/app/component/ParkOrStationHeader.js +++ b/app/component/ParkOrStationHeader.js @@ -10,7 +10,7 @@ import getZoneId from '../util/zoneIconUtils'; import ZoneIcon from './ZoneIcon'; import withBreakpoint from '../util/withBreakpoint'; import { - hasStationCode, + hasVehicleRentalCode, getVehicleRentalStationNetworkConfig, } from '../util/vehicleRentalUtils'; import { getIdWithoutFeed } from '../util/feedScopedIdUtils'; @@ -69,7 +69,7 @@ const ParkOrBikeStationHeader = (

{name}

- {stationId && hasStationCode(parkOrStation.stationId) && ( + {stationId && hasVehicleRentalCode(parkOrStation.stationId) && ( )} {zoneId && ( diff --git a/app/component/VehicleRentalStationNearYou.js b/app/component/VehicleRentalStationNearYou.js index 8542c0c5ff..ca9c1f7fea 100644 --- a/app/component/VehicleRentalStationNearYou.js +++ b/app/component/VehicleRentalStationNearYou.js @@ -7,7 +7,7 @@ import VehicleRentalStation from './VehicleRentalStation'; import FavouriteVehicleRentalStationContainer from './FavouriteVehicleRentalStationContainer'; import { PREFIX_BIKESTATIONS } from '../util/path'; import { isKeyboardSelectionEvent } from '../util/browser'; -import { hasStationCode } from '../util/vehicleRentalUtils'; +import { hasVehicleRentalCode } from '../util/vehicleRentalUtils'; import { getIdWithoutFeed } from '../util/feedScopedIdUtils'; import { relayShape } from '../util/shapes'; @@ -52,7 +52,7 @@ const VehicleRentalStationNearYou = ({ ); } - // show only scooters that are clusters (avoids massive lists) if (option.layer === 'scooter' && option.feature.properties.cluster) { return ( - ); } diff --git a/app/component/map/tile-layer/SelectRentalVehicleClusterRow.js b/app/component/map/tile-layer/SelectRentalVehicleClusterRow.js new file mode 100644 index 0000000000..2dfce4c347 --- /dev/null +++ b/app/component/map/tile-layer/SelectRentalVehicleClusterRow.js @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Link from 'found/Link'; +import { FormattedMessage } from 'react-intl'; +import { configShape } from '../../../util/shapes'; +import Icon from '../../Icon'; +import { hasVehicleRentalCode } from '../../../util/vehicleRentalUtils'; +import { getIdWithoutFeed } from '../../../util/feedScopedIdUtils'; + +/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ +function SelectVehicleRentalClusterRow({ + name, + id, + desc, + prefix, + networks: networksInCluster, + isScooter, +}) { + const img = isScooter + ? 'icon-icon_scooter-lollipop' + : 'icon-icon_citybike-stop-lollipop'; + + const linkAddress = `/${prefix}/${encodeURIComponent(id)}/${[ + ...networksInCluster, + ]}`; + + const address = desc || ; + return ( + + + +
{name}
+ + {address} + {hasVehicleRentalCode(id) && ( + {getIdWithoutFeed(id)} + )} + +
+ + + + + ); +} + +SelectVehicleRentalClusterRow.displayName = 'SelectVehicleRentalRow'; + +SelectVehicleRentalClusterRow.propTypes = { + name: PropTypes.string, + id: PropTypes.string.isRequired, + desc: PropTypes.string, + prefix: PropTypes.string.isRequired, + networks: PropTypes.arrayOf(PropTypes.string).isRequired, + isScooter: PropTypes.bool, +}; + +SelectVehicleRentalClusterRow.defaultProps = { + desc: undefined, + name: undefined, + isScooter: false, +}; + +SelectVehicleRentalClusterRow.contextTypes = { + config: configShape.isRequired, +}; + +export default SelectVehicleRentalClusterRow; diff --git a/app/component/map/tile-layer/SelectVehicleRentalRow.js b/app/component/map/tile-layer/SelectVehicleRentalRow.js index 55adecd282..2ba99408ef 100644 --- a/app/component/map/tile-layer/SelectVehicleRentalRow.js +++ b/app/component/map/tile-layer/SelectVehicleRentalRow.js @@ -7,24 +7,20 @@ import Icon from '../../Icon'; import { getVehicleRentalStationNetworkConfig, getVehicleRentalStationNetworkIcon, - hasStationCode, + hasVehicleRentalCode, } from '../../../util/vehicleRentalUtils'; import { getIdWithoutFeed } from '../../../util/feedScopedIdUtils'; /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ function SelectVehicleRentalRow( - { name, network, id, desc, prefix, cluster, networks: networksInCluster }, + { name, network, id, desc, prefix }, { config }, ) { - const img = cluster - ? 'icon-icon_scooter-lollipop' - : `${getVehicleRentalStationNetworkIcon( - getVehicleRentalStationNetworkConfig(network, config), - )}-stop-lollipop`; + const img = `${getVehicleRentalStationNetworkIcon( + getVehicleRentalStationNetworkConfig(network, config), + )}-stop-lollipop`; - const linkAddress = cluster - ? `/${prefix}/${encodeURIComponent(id)}/${[...networksInCluster]}` - : `/${prefix}/${encodeURIComponent(id)}`; + const linkAddress = `/${prefix}/${encodeURIComponent(id)}`; const address = desc || ; return ( @@ -36,7 +32,7 @@ function SelectVehicleRentalRow(
{name}
{address} - {hasStationCode(id) && ( + {hasVehicleRentalCode(id) && ( {getIdWithoutFeed(id)} )} @@ -56,15 +52,11 @@ SelectVehicleRentalRow.propTypes = { id: PropTypes.string.isRequired, desc: PropTypes.string, prefix: PropTypes.string.isRequired, - cluster: PropTypes.bool, - networks: PropTypes.arrayOf(PropTypes.string), }; SelectVehicleRentalRow.defaultProps = { desc: undefined, name: undefined, - cluster: false, - networks: [], }; SelectVehicleRentalRow.contextTypes = { diff --git a/app/util/vehicleRentalUtils.js b/app/util/vehicleRentalUtils.js index c6e8783b27..75a22e6d79 100644 --- a/app/util/vehicleRentalUtils.js +++ b/app/util/vehicleRentalUtils.js @@ -166,11 +166,11 @@ export const getVehicleMinZoomOnStopsNearYou = (config, override) => { }; /** * - * Checks if stationId is a number. We don't want to display random hashes or names. + * Checks if rentalId (station or vehicle) is a number. We don't want to display random hashes or names. * - * @param rentalId bike rental station from OTP + * @param rentalId id of a rental station or rental vehicle from OTP */ -export const hasStationCode = rentalId => { +export const hasVehicleRentalCode = rentalId => { const id = rentalId?.split(':')[1]; return ( id && From cba1851106c0fdbcf44070d98532bdc1e9a75cbd Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 12 Jun 2024 13:10:48 +0300 Subject: [PATCH 19/79] DT-6182 vehicle rental row tests --- .../map/tile-layer/MarkerSelectPopup.test.js | 51 ++++++++++++++++++- .../SelectVehicleRentalStationRow.test.js | 21 +++----- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/test/unit/component/map/tile-layer/MarkerSelectPopup.test.js b/test/unit/component/map/tile-layer/MarkerSelectPopup.test.js index 5384301c37..012bcc998c 100644 --- a/test/unit/component/map/tile-layer/MarkerSelectPopup.test.js +++ b/test/unit/component/map/tile-layer/MarkerSelectPopup.test.js @@ -3,7 +3,8 @@ import React from 'react'; import { shallowWithIntl } from '../../../helpers/mock-intl-enzyme'; import MarkerSelectPopup from '../../../../../app/component/map/tile-layer/MarkerSelectPopup'; import SelectStopRow from '../../../../../app/component/map/tile-layer/SelectStopRow'; -import SelectVehicleRentalStationRow from '../../../../../app/component/map/tile-layer/SelectVehicleRentalStationRow'; +import SelectVehicleRentalRow from '../../../../../app/component/map/tile-layer/SelectVehicleRentalRow'; +import SelectRentalVehicleClusterRow from '../../../../../app/component/map/tile-layer/SelectRentalVehicleClusterRow'; import SelectParkAndRideRow from '../../../../../app/component/map/tile-layer/SelectParkAndRideRow'; import SelectVehicleContainer from '../../../../../app/component/map/tile-layer/SelectVehicleContainer'; import { mockMatch } from '../../../helpers/mock-router'; @@ -106,8 +107,54 @@ describe('', () => { }, }); expect(wrapper.find(SelectStopRow)).to.have.lengthOf(1); - expect(wrapper.find(SelectVehicleRentalStationRow)).to.have.lengthOf(1); + expect(wrapper.find(SelectVehicleRentalRow)).to.have.lengthOf(1); expect(wrapper.find(SelectParkAndRideRow)).to.have.lengthOf(2); expect(wrapper.find(SelectVehicleContainer)).to.have.lengthOf(1); }); + + it('should render a scooter cluster row with valid data but not a scooter row', () => { + const props = { + options: [ + { + layer: 'scooter', + feature: { + geom: { x: 2948, y: 3452 }, + properties: { + scooterId: '116', + name: 'Clustered vehicles should be shown', + network: 'foobar', + networks: ['foo', 'bar'], + cluster: true, + }, + }, + }, + { + layer: 'scooter', + feature: { + geom: { x: 2948, y: 3452 }, + properties: { + id: '115', + name: 'Single vehicles should not be shown', + network: 'foobar', + }, + }, + }, + ], + selectRow: () => {}, + location: { + lat: 60.169525626502484, + lng: 24.933235645294193, + }, + colors: { + primary: '#007ac9', + hover: '#0062a1', + }, + }; + const wrapper = shallowWithIntl(, { + context: { + match: mockMatch, + }, + }); + expect(wrapper.find(SelectRentalVehicleClusterRow)).to.have.lengthOf(1); + }); }); diff --git a/test/unit/component/map/tile-layer/SelectVehicleRentalStationRow.test.js b/test/unit/component/map/tile-layer/SelectVehicleRentalStationRow.test.js index c580fad89f..f865001116 100644 --- a/test/unit/component/map/tile-layer/SelectVehicleRentalStationRow.test.js +++ b/test/unit/component/map/tile-layer/SelectVehicleRentalStationRow.test.js @@ -1,10 +1,10 @@ import React from 'react'; -import SelectVehicleRentalStationRow from '../../../../../app/component/map/tile-layer/SelectVehicleRentalStationRow'; +import SelectVehicleRentalRow from '../../../../../app/component/map/tile-layer/SelectVehicleRentalRow'; import { shallowWithIntl } from '../../../helpers/mock-intl-enzyme'; import Icon from '../../../../../app/component/Icon'; -describe('', () => { +describe('', () => { it('should use the citybike icon by default', () => { const props = { name: 'foobar', @@ -12,9 +12,7 @@ describe('', () => { id: '001', prefix: 'citybike', }; - const wrapper = shallowWithIntl( - , - ); + const wrapper = shallowWithIntl(); expect(wrapper.find(Icon).first().prop('img')).to.contain('citybike'); }); @@ -25,16 +23,13 @@ describe('', () => { id: '001', prefix: 'citybike', }; - const wrapper = shallowWithIntl( - , - { - context: { - config: { - cityBike: { networks: { scooter_network: { icon: 'scooter' } } }, - }, + const wrapper = shallowWithIntl(, { + context: { + config: { + cityBike: { networks: { scooter_network: { icon: 'scooter' } } }, }, }, - ); + }); expect(wrapper.find(Icon).first().prop('img')).to.contain('scooter'); }); }); From 0a0097b675f57f1568bb247b8e30e2259501518c Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 12 Jun 2024 13:16:34 +0300 Subject: [PATCH 20/79] DT-6182 renaming --- app/component/itinerary/VehicleRentalLeg.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/component/itinerary/VehicleRentalLeg.js b/app/component/itinerary/VehicleRentalLeg.js index 58e3041c97..86113ba735 100644 --- a/app/component/itinerary/VehicleRentalLeg.js +++ b/app/component/itinerary/VehicleRentalLeg.js @@ -14,7 +14,7 @@ import { getVehicleCapacity, getVehicleRentalStationNetworkConfig, getVehicleRentalStationNetworkIcon, - hasStationCode, + hasVehicleRentalCode, } from '../../util/vehicleRentalUtils'; import withBreakpoint from '../../util/withBreakpoint'; @@ -152,7 +152,7 @@ function VehicleRentalLeg( defaultMessage: 'Bike station', })} {vehicleRentalStation && - hasStationCode(vehicleRentalStation.stationId) && ( + hasVehicleRentalCode(vehicleRentalStation.stationId) && ( {getIdWithoutFeed(vehicleRentalStation?.stationId)} From da33efb3c3d400dd3d9d529b755033d622eeb233 Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 12 Jun 2024 14:33:19 +0300 Subject: [PATCH 21/79] DT-6182 renaming, translations fix --- app/component/RouteNumber.js | 2 +- app/component/itinerary/ItineraryList.js | 8 ++++---- app/component/itinerary/errorCardProperties.js | 2 +- app/component/itinerary/itinerary.scss | 6 +++--- app/component/map/non-tile-layer/VehicleMarker.js | 3 ++- app/translations.js | 12 +++++++++--- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/component/RouteNumber.js b/app/component/RouteNumber.js index 1134f81fb6..2d6d08fe72 100644 --- a/app/component/RouteNumber.js +++ b/app/component/RouteNumber.js @@ -13,7 +13,7 @@ const LONG_ROUTE_NUMBER_LENGTH = 6; function RouteNumber(props, context) { const mode = props.mode.toLowerCase(); const { alertSeverityLevel, color, withBicycle, text } = props; - const isScooter = mode === TransportMode.Scooter.toLowerCase(); // 'scooter'; + const isScooter = mode === TransportMode.Scooter.toLowerCase(); const textIsText = typeof text === 'string'; // can be also react node const longText = text && textIsText && text.length >= LONG_ROUTE_NUMBER_LENGTH; diff --git a/app/component/itinerary/ItineraryList.js b/app/component/itinerary/ItineraryList.js index 20da4067f0..54eb75d163 100644 --- a/app/component/itinerary/ItineraryList.js +++ b/app/component/itinerary/ItineraryList.js @@ -150,18 +150,18 @@ function ItineraryList(
-
+
-
+
diff --git a/app/component/itinerary/errorCardProperties.js b/app/component/itinerary/errorCardProperties.js index a159aa0a0f..3d310e46e4 100644 --- a/app/component/itinerary/errorCardProperties.js +++ b/app/component/itinerary/errorCardProperties.js @@ -196,7 +196,7 @@ const errorCardProps = [ { id: 'no-route-alternative-suggestion', props: { - bodyId: 'e-scooter-or-taxi-alternative', + bodyId: 'e-scooter-alternative', ...info, }, }, diff --git a/app/component/itinerary/itinerary.scss b/app/component/itinerary/itinerary.scss index 1c45b2cd52..35668e2a8f 100644 --- a/app/component/itinerary/itinerary.scss +++ b/app/component/itinerary/itinerary.scss @@ -3010,7 +3010,7 @@ $font-print-decrease: 0.7; margin-bottom: 30px; } -.e-scooter-or-taxi-info { +.alternative-vehicle-info { background-color: $infobox-color-generic-blue; margin-top: 30px; @@ -3022,7 +3022,7 @@ $font-print-decrease: 0.7; height: 2em; } - .e-scooter-or-taxi-info-header { + .alternative-vehicle-info-header { display: flex; font-size: $font-size-normal; font-weight: $font-weight-medium; @@ -3031,7 +3031,7 @@ $font-print-decrease: 0.7; color: #333; } - .e-scooter-or-taxi-info-content { + .alternative-vehicle-info-content { display: flex; font-size: $font-size-normal; margin-bottom: 8px; diff --git a/app/component/map/non-tile-layer/VehicleMarker.js b/app/component/map/non-tile-layer/VehicleMarker.js index 0890f22abc..34fd9651a0 100644 --- a/app/component/map/non-tile-layer/VehicleMarker.js +++ b/app/component/map/non-tile-layer/VehicleMarker.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { routerShape } from 'found'; +import { TransportMode } from '../../../constants'; import { vehicleRentalStationShape, rentalVehicleShape, @@ -120,7 +121,7 @@ export default class VehicleMarker extends React.Component { onClick={() => this.handleClick( this.props.rental.id, - this.props.mode === 'SCOOTER' + this.props.mode === TransportMode.Scooter ? PREFIX_RENTALVEHICLES : PREFIX_BIKESTATIONS, ) diff --git a/app/translations.js b/app/translations.js index 99b0c57179..94fcc50a0b 100644 --- a/app/translations.js +++ b/app/translations.js @@ -1034,6 +1034,8 @@ const translations = { 'distance-total': 'Total distance', 'distance-under': 'Distance less than {distance} m', 'e-scooter': 'Sähköpotkulauta', + 'e-scooter-alternative': + 'Entä jos kulkisit osan matkasta sähköpotkulaudalla? {paymentInfo}', 'e-scooter-or-taxi-alternative': 'Entä jos kulkisit osan matkasta taksilla tai sähköpotkulaudalla? {paymentInfo}', 'e-scooter-quantity': 'Sähköpotkulautoja vapaana juuri nyt: ', @@ -1327,7 +1329,7 @@ const translations = { 'Log in to the service to save your favorites and utilize them on other devices', 'one-way-journey': 'The length of a one-way journey', 'open-app': 'Open the app', - 'open-operator-app': 'Avaa {operator}-sovellus', + 'open-operator-app': 'Open {operator} app', 'open-settings': 'Avaa asetukset', 'option-default': 'Average', 'option-least': 'Slow', @@ -2243,6 +2245,8 @@ const translations = { 'distance-total': 'Matkan pituus', 'distance-under': 'Etäisyys alle {distance} m', 'e-scooter': 'Sähköpotkulauta', + 'e-scooter-alternative': + 'Entä jos kulkisit osan matkasta sähköpotkulaudalla? {paymentInfo}', 'e-scooter-or-taxi-alternative': 'Entä jos kulkisit osan matkasta taksilla tai sähköpotkulaudalla? {paymentInfo}', 'e-scooter-quantity': 'Sähköpotkulautoja vapaana juuri nyt: ', @@ -4204,6 +4208,8 @@ const translations = { 'distance-total': 'Resans längd', 'distance-under': 'Avstånd mindre än {distance} m', 'e-scooter': 'Sähköpotkulauta', + 'e-scooter-alternative': + 'Entä jos kulkisit osan matkasta sähköpotkulaudalla? {paymentInfo}', 'e-scooter-or-taxi-alternative': 'Entä jos kulkisit osan matkasta taksilla tai sähköpotkulaudalla? {paymentInfo}', 'e-scooter-quantity': 'Sähköpotkulautoja vapaana juuri nyt: ', @@ -4491,8 +4497,8 @@ const translations = { 'Genom att logga in kan du spara dina favoriter och använda dem med dina andra enheter.', 'one-way-journey': ' Längden på en enkel resa', 'open-app': ' Öppna appen', - 'open-operator-app': 'Avaa {operator}-sovellus', - 'open-settings': 'Avaa asetukset', + 'open-operator-app': 'Öppna {operator} appen', + 'open-settings': 'Öppna inställningar', 'option-default': 'Standard', 'option-least': 'Minst', 'option-less': 'Mindre', From b2db6fb2f64579721a3ba0b1a4e5bcab7cf6e9e7 Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 12 Jun 2024 15:15:19 +0300 Subject: [PATCH 22/79] DT-6182 clarification --- app/component/itinerary/Legs.js | 6 +++++- app/component/map/tile-layer/TileLayerContainer.js | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/component/itinerary/Legs.js b/app/component/itinerary/Legs.js index 804cf3d917..529f65699c 100644 --- a/app/component/itinerary/Legs.js +++ b/app/component/itinerary/Legs.js @@ -209,7 +209,11 @@ export default class Legs extends React.Component { focusAction={this.focus(leg.to)} />, ); - } else if (leg.rentedBike || leg.mode === 'BICYCLE') { + } else if ( + leg.rentedBike || + leg.mode === 'BICYCLE' || + leg.mode === 'SCOOTER' + ) { let bicycleWalkLeg; if (nextLeg?.mode === 'BICYCLE_WALK' && !bikeParked) { bicycleWalkLeg = nextLeg; diff --git a/app/component/map/tile-layer/TileLayerContainer.js b/app/component/map/tile-layer/TileLayerContainer.js index 7205270c4a..ce1c48ec33 100644 --- a/app/component/map/tile-layer/TileLayerContainer.js +++ b/app/component/map/tile-layer/TileLayerContainer.js @@ -214,6 +214,8 @@ class TileLayerContainer extends GridLayer { selectableTargets[0].layer === 'scooter') || (selectableTargets.length > 1 && selectableTargets.every(target => target.layer === 'scooter')) + // scooters are not shown in the selection popup as there can be too many. + // Instead, the user is directed to the scooter cluster view or the first one in a group of singles. ) { const cluster = selectableTargets.find( target => target.feature.properties.cluster, From 2ddff7b788dcfe0bc402ff044e0ec6c71c5acd74 Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 12 Jun 2024 17:48:09 +0300 Subject: [PATCH 23/79] DT-6182 style fix in relaxed scooter result --- app/component/itinerary/BicycleLeg.js | 1 + app/component/itinerary/itinerary.scss | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/component/itinerary/BicycleLeg.js b/app/component/itinerary/BicycleLeg.js index 3119298165..638e42cf47 100644 --- a/app/component/itinerary/BicycleLeg.js +++ b/app/component/itinerary/BicycleLeg.js @@ -115,6 +115,7 @@ export default function BicycleLeg( index={index} modeClassName={mode.toLowerCase()} icon="icon-icon_scooter_rider" + appendClass={!scooterSettingsOn ? 'settings' : ''} /> ); } else if (bicycleWalkLeg) { diff --git a/app/component/itinerary/itinerary.scss b/app/component/itinerary/itinerary.scss index 35668e2a8f..c015fe1abc 100644 --- a/app/component/itinerary/itinerary.scss +++ b/app/component/itinerary/itinerary.scss @@ -1171,13 +1171,22 @@ $itinerary-tab-switch-height: 48px; &.scooter { border-left: 6px solid; border-radius: 3px; - height: 56%; + height: 54%; left: 8px; + &.settings { + height: 58%; + } + &.bottom { height: 25%; top: 82%; overflow: hidden; + + &.settings { + height: 30%; + top: 75%; + } } } From c3351a0b6fb00bf7e4a0eceb8bf358026f0df340 Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 12 Jun 2024 17:49:41 +0300 Subject: [PATCH 24/79] DT-6182 rentalvehicle link validation --- app/component/itinerary/VehicleRentalLeg.js | 15 +++++------- app/util/vehicleRentalUtils.js | 27 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/app/component/itinerary/VehicleRentalLeg.js b/app/component/itinerary/VehicleRentalLeg.js index 86113ba735..0a88321209 100644 --- a/app/component/itinerary/VehicleRentalLeg.js +++ b/app/component/itinerary/VehicleRentalLeg.js @@ -15,6 +15,7 @@ import { getVehicleRentalStationNetworkConfig, getVehicleRentalStationNetworkIcon, hasVehicleRentalCode, + getRentalVehicleLink, } from '../../util/vehicleRentalUtils'; import withBreakpoint from '../../util/withBreakpoint'; @@ -25,7 +26,6 @@ import { getVehicleAvailabilityIndicatorColor, } from '../../util/legUtils'; import ExternalLink from '../ExternalLink'; -import { isAndroid, isIOS } from '../../util/browser'; import { getIdWithoutFeed } from '../../util/feedScopedIdUtils'; function VehicleRentalLeg( @@ -82,14 +82,11 @@ function VehicleRentalLeg( /> ); const rentalStationLink = `/${PREFIX_BIKESTATIONS}/${vehicleRentalStation?.stationId}`; - let rentalVehicleLink = - rentalVehicle?.rentalUris.web || rentalVehicle?.systemUrl; - - if (isIOS && rentalVehicle?.rentalUris.ios) { - rentalVehicleLink = rentalVehicle?.rentalUris.ios; - } else if (isAndroid && rentalVehicle?.rentalUris.android) { - rentalVehicleLink = rentalVehicle?.rentalUris.android; - } + const rentalVehicleLink = getRentalVehicleLink( + rentalVehicle, + network, + networkConfig, + ); return ( <> {(!isScooter || (nextLegMode !== 'WALK' && isScooter)) && ( diff --git a/app/util/vehicleRentalUtils.js b/app/util/vehicleRentalUtils.js index 75a22e6d79..fb9b4cf07f 100644 --- a/app/util/vehicleRentalUtils.js +++ b/app/util/vehicleRentalUtils.js @@ -4,6 +4,7 @@ import { getCustomizedSettings } from '../store/localStorage'; import { addAnalyticsEvent } from './analyticsUtils'; import { citybikeRoutingIsActive } from './modeUtils'; import { getIdWithoutFeed } from './feedScopedIdUtils'; +import { isAndroid, isIOS } from './browser'; export const BIKEAVL_UNKNOWN = 'No availability'; export const BIKEAVL_BIKES = 'Bikes on station'; @@ -202,3 +203,29 @@ export const mapVehicleRentalToStore = vehicleRentalStation => { delete newStation.network; return newStation; }; + +export const getRentalVehicleLink = (rentalVehicle, network, networkConfig) => { + if (!networkConfig || !rentalVehicle) { + return null; + } + + const { ios, android, web } = rentalVehicle?.rentalUris || {}; + + if (isIOS && ios?.startsWith(`${network}://`)) { + return ios; + } + + if (isAndroid && android?.startsWith(`${network}://`)) { + return android; + } + + if (web?.includes(network)) { + return web; + } + + if (rentalVehicle?.systemUrl?.includes(network)) { + return rentalVehicle.systemUrl; + } + + return null; +}; From 7d1233ee9ce44379612a08ac6061062f4c4a6d48 Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 12 Jun 2024 17:50:14 +0300 Subject: [PATCH 25/79] DT-6182 removed test config --- app/configurations/config.hsl.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index df79a30670..c955b7ac54 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -40,15 +40,6 @@ export default { REALTIME_RENTAL_STATION_MAP: { default: `${OTP_URL}vectorTiles/realtimeRentalStations/`, }, - RENTAL_VEHICLE_CLUSTER_MEDIUM_MAP: { - default: `${OTP_URL}vectorTiles/rentalVehicleClusterClose/`, - }, - RENTAL_VEHICLE_CLUSTER_CLOSE_MAP: { - default: `${OTP_URL}vectorTiles/rentalVehicleClusterMedium/`, - }, - RENTAL_VEHICLE_CLUSTER_FAR_MAP: { - default: `${OTP_URL}vectorTiles/rentalVehicleClusterFar/`, - }, PARK_AND_RIDE_MAP: { default: `${POI_MAP_PREFIX}/en/vehicleParking/`, sv: `${POI_MAP_PREFIX}/sv/vehicleParking/`, From e671c23afb7b841e14a8a702b5797b42960d1f10 Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 12 Jun 2024 17:51:53 +0300 Subject: [PATCH 26/79] DT-6182 scooter logo color fixed --- static/assets/svg-sprite.default.svg | 2 +- static/assets/svg-sprite.hsl.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/static/assets/svg-sprite.default.svg b/static/assets/svg-sprite.default.svg index 54c6ddca66..cee10aa5a7 100644 --- a/static/assets/svg-sprite.default.svg +++ b/static/assets/svg-sprite.default.svg @@ -1008,7 +1008,7 @@ - + diff --git a/static/assets/svg-sprite.hsl.svg b/static/assets/svg-sprite.hsl.svg index dd801ad4cf..37d7bff024 100644 --- a/static/assets/svg-sprite.hsl.svg +++ b/static/assets/svg-sprite.hsl.svg @@ -1017,7 +1017,7 @@ - + From 186fd821d4e8e37c0fea00ff1ef49fb69a31ae35 Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 12 Jun 2024 18:11:46 +0300 Subject: [PATCH 27/79] schema fix --- build/schema.graphql | 24 +++++++++---------- .../schema/schema.graphql | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/build/schema.graphql b/build/schema.graphql index 243fa7c6e9..485a0727ff 100644 --- a/build/schema.graphql +++ b/build/schema.graphql @@ -980,23 +980,23 @@ type Cluster implements Node { """ id: ID! - """ID of the vehicleRentalPlaceCluster""" + """ID of the cluster""" gtfsId: String! - """Name of the vehicleRentalPlaceCluster""" + """Name of the cluster""" name: String! """ - Latitude of the center of this vehicleRentalPlaceCluster (i.e. average latitude of stops in this vehicleRentalPlaceCluster) + Latitude of the center of this cluster (i.e. average latitude of stops in this cluster) """ lat: Float! """ - Longitude of the center of this vehicleRentalPlaceCluster (i.e. average longitude of stops in this vehicleRentalPlaceCluster) + Longitude of the center of this cluster (i.e. average longitude of stops in this cluster) """ lon: Float! - """List of stops in the vehicleRentalPlaceCluster""" + """List of stops in the cluster""" stops: [Stop!] } @@ -3686,11 +3686,11 @@ type QueryType { """ pattern(id: String!): Pattern - """Get all vehicleRentalPlaceClusters""" - vehicleRentalPlaceClusters: [Cluster] + """Get all clusters""" + clusters: [Cluster] - """Get a single vehicleRentalPlaceCluster based on its ID, i.e. value of field `gtfsId`""" - vehicleRentalPlaceCluster(id: String!): Cluster + """Get a single cluster based on its ID, i.e. value of field `gtfsId`""" + cluster(id: String!): Cluster """Get all active alerts""" alerts( @@ -4859,8 +4859,8 @@ type Stop implements Node & PlaceInterface { """ platformCode: String - """The vehicleRentalPlaceCluster which this stop is part of""" - vehicleRentalPlaceCluster: Cluster + """The cluster which this stop is part of""" + cluster: Cluster """ Returns all stops that are children of this station (Only applicable for stations) @@ -5585,4 +5585,4 @@ input WheelchairPreferencesInput { that the itineraries are wheelchair accessible as there can be data issues. """ enabled: Boolean -} +} \ No newline at end of file diff --git a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql index 243fa7c6e9..485a0727ff 100644 --- a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql +++ b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql @@ -980,23 +980,23 @@ type Cluster implements Node { """ id: ID! - """ID of the vehicleRentalPlaceCluster""" + """ID of the cluster""" gtfsId: String! - """Name of the vehicleRentalPlaceCluster""" + """Name of the cluster""" name: String! """ - Latitude of the center of this vehicleRentalPlaceCluster (i.e. average latitude of stops in this vehicleRentalPlaceCluster) + Latitude of the center of this cluster (i.e. average latitude of stops in this cluster) """ lat: Float! """ - Longitude of the center of this vehicleRentalPlaceCluster (i.e. average longitude of stops in this vehicleRentalPlaceCluster) + Longitude of the center of this cluster (i.e. average longitude of stops in this cluster) """ lon: Float! - """List of stops in the vehicleRentalPlaceCluster""" + """List of stops in the cluster""" stops: [Stop!] } @@ -3686,11 +3686,11 @@ type QueryType { """ pattern(id: String!): Pattern - """Get all vehicleRentalPlaceClusters""" - vehicleRentalPlaceClusters: [Cluster] + """Get all clusters""" + clusters: [Cluster] - """Get a single vehicleRentalPlaceCluster based on its ID, i.e. value of field `gtfsId`""" - vehicleRentalPlaceCluster(id: String!): Cluster + """Get a single cluster based on its ID, i.e. value of field `gtfsId`""" + cluster(id: String!): Cluster """Get all active alerts""" alerts( @@ -4859,8 +4859,8 @@ type Stop implements Node & PlaceInterface { """ platformCode: String - """The vehicleRentalPlaceCluster which this stop is part of""" - vehicleRentalPlaceCluster: Cluster + """The cluster which this stop is part of""" + cluster: Cluster """ Returns all stops that are children of this station (Only applicable for stations) @@ -5585,4 +5585,4 @@ input WheelchairPreferencesInput { that the itineraries are wheelchair accessible as there can be data issues. """ enabled: Boolean -} +} \ No newline at end of file From 0d3ce30e75f28034083c588e590874532b035f7a Mon Sep 17 00:00:00 2001 From: sharhio Date: Thu, 13 Jun 2024 12:34:59 +0300 Subject: [PATCH 28/79] DT-6182 clustering dependency --- package.json | 1 + yarn.lock | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/package.json b/package.json index 32fbd7581d..010cb0fbc4 100644 --- a/package.json +++ b/package.json @@ -208,6 +208,7 @@ "relay-runtime": "16.2.0", "serialize-javascript": "4.0.0", "suncalc": "1.8.0", + "supercluster": "^8.0.1", "swipe-js-iso": "2.1.5", "universal-cookie": "4.0.4", "uuid": "8.3.0", diff --git a/yarn.lock b/yarn.lock index 9c2e35a70c..664b7295e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11382,6 +11382,7 @@ __metadata: stylelint: 15.11.0 stylelint-config-standard-scss: 11.1.0 suncalc: 1.8.0 + supercluster: ^8.0.1 swipe-js-iso: 2.1.5 terser-webpack-plugin: 4.1.0 universal-cookie: 4.0.4 @@ -17833,6 +17834,13 @@ __metadata: languageName: node linkType: hard +"kdbush@npm:^4.0.2": + version: 4.0.2 + resolution: "kdbush@npm:4.0.2" + checksum: 6782ef2cdaec9322376b9955a16b0163beda0cefa2f87da76e8970ade2572d8b63bec915347aaeac609484b0c6e84d7b591f229ef353b68b460238095bacde2d + languageName: node + linkType: hard + "keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -26925,6 +26933,15 @@ __metadata: languageName: node linkType: hard +"supercluster@npm:^8.0.1": + version: 8.0.1 + resolution: "supercluster@npm:8.0.1" + dependencies: + kdbush: ^4.0.2 + checksum: 39d141f768a511efa53260252f9dab9a2ce0228b334e55482c8d3019e151932f05e1a9a0252d681737651b13c741c665542a6ddb40ec27de96159ea7ad41f7f4 + languageName: node + linkType: hard + "supports-color@npm:8.1.1, supports-color@npm:^8.0.0": version: 8.1.1 resolution: "supports-color@npm:8.1.1" From dccc91fa940d731be31e336352ef05cfe023b8ee Mon Sep 17 00:00:00 2001 From: sharhio Date: Thu, 13 Jun 2024 12:37:43 +0300 Subject: [PATCH 29/79] DT-6182 scooter configs --- app/configurations/config.default.js | 8 +++++++- app/configurations/config.matka.js | 7 +++++++ app/configurations/config.waltti.js | 6 ++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/configurations/config.default.js b/app/configurations/config.default.js index dc7c77144e..bc3760adc3 100644 --- a/app/configurations/config.default.js +++ b/app/configurations/config.default.js @@ -58,7 +58,13 @@ export default { default: `${POI_MAP_PREFIX}/fi/rentalStations/`, }, REALTIME_RENTAL_STATION_MAP: { - default: `${POI_MAP_PREFIX}/fi/realtimeRentalStations/`, + default: `${OTP_URL}vectorTiles/realtimeRentalStations/`, + }, + RENTAL_VEHICLE_MAP: { + default: `${OTP_URL}vectorTiles/rentalVehicles/`, + }, + REALTIME_RENTAL_VEHICLE_MAP: { + default: `${OTP_URL}vectorTiles/realtimeRentalVehicles/`, }, PARK_AND_RIDE_MAP: { default: `${POI_MAP_PREFIX}/en/vehicleParking/`, diff --git a/app/configurations/config.matka.js b/app/configurations/config.matka.js index 49c8684960..fca6377613 100644 --- a/app/configurations/config.matka.js +++ b/app/configurations/config.matka.js @@ -62,6 +62,8 @@ export default { 'mode-ferry-pier': '#666666', 'mode-citybike': '#FCBC19', 'mode-citybike-secondary': '#333333', + 'mode-scooter': '#BABABA', + 'mode-scooter-secondary': '#333333', }, }, feedIds: [ @@ -177,6 +179,7 @@ export default { LappeenrantaConfig.cityBike.networks.donkey_lappeenranta, donkey_kotka: KotkaConfig.cityBike.networks.donkey_kotka, donkey_kouvola: KouvolaConfig.cityBike.networks.donkey_kouvola, + bolt: HSLConfig.cityBike.networks.bolt, }, }, @@ -196,6 +199,10 @@ export default { citybike: { availableForSelection: true, }, + scooter: { + availableForSelection: true, + defaultValue: false, + }, }, useRealtimeTravellerCapacities: true, diff --git a/app/configurations/config.waltti.js b/app/configurations/config.waltti.js index 6a9cf151b2..ef09702125 100644 --- a/app/configurations/config.waltti.js +++ b/app/configurations/config.waltti.js @@ -31,6 +31,12 @@ export default { sv: `${POI_MAP_PREFIX}/sv/vehicleParkingGroups/`, fi: `${POI_MAP_PREFIX}/fi/vehicleParkingGroups/`, }, + RENTAL_VEHICLE_MAP: { + default: `${POI_MAP_PREFIX}/fi/rentalVehicles/`, + }, + REALTIME_RENTAL_VEHICLE_MAP: { + default: `${POI_MAP_PREFIX}/fi/realtimeRentalVehicles/`, + }, }, stopsMinZoom: 14, From 1ebdcb7ce1986c48107102b430b9c27536fbe30c Mon Sep 17 00:00:00 2001 From: sharhio Date: Fri, 14 Jun 2024 12:08:28 +0300 Subject: [PATCH 30/79] DT-6182 scooters off in layeroptions, default icon fix, taxi text removed --- app/component/itinerary/ItineraryList.js | 4 +--- app/component/map/tile-layer/RentalVehicles.js | 8 ++++++-- app/configurations/config.hsl.js | 1 + app/store/MapLayerStore.js | 14 +++++++------- static/assets/svg-sprite.default.svg | 2 +- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/component/itinerary/ItineraryList.js b/app/component/itinerary/ItineraryList.js index 54eb75d163..d8408a149d 100644 --- a/app/component/itinerary/ItineraryList.js +++ b/app/component/itinerary/ItineraryList.js @@ -163,9 +163,7 @@ function ItineraryList( - ), + paymentInfo: , }} />
diff --git a/app/component/map/tile-layer/RentalVehicles.js b/app/component/map/tile-layer/RentalVehicles.js index ce4b3f6fcc..13507459e6 100644 --- a/app/component/map/tile-layer/RentalVehicles.js +++ b/app/component/map/tile-layer/RentalVehicles.js @@ -59,7 +59,8 @@ class RentalVehicles { const allowedScooterNetworks = settings.allowedScooterRentalNetworks; const scooterIconPrefix = `icon-icon_scooter`; - + const showAllNetworks = + !this.config.transportModes.scooter.hideUnlessSelectedForRouting; if (layer) { for (let i = 0, ref = layer.length - 1; i <= ref; i++) { const feature = layer.feature(i); @@ -67,7 +68,10 @@ class RentalVehicles { // Filter out vehicles that are not in the allowedScooterNetworks (selected by a user) to avoid including unwanted vehicles in clusters // Also Filter out vehicles that should not be shown to avoid user accidentally clicking on invisible objects on the map if ( - allowedScooterNetworks.includes(feature.properties.network) && + (showAllNetworks || + allowedScooterNetworks.includes( + feature.properties.network, + )) && this.shouldShowRentalVehicle( feature.properties.id, feature.properties.network, diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index c955b7ac54..16f4f4d2ac 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -200,6 +200,7 @@ export default { scooter: { availableForSelection: true, defaultValue: false, + hideUnlessSelectedForRouting: true, }, airplane: { availableForSelection: false, diff --git a/app/store/MapLayerStore.js b/app/store/MapLayerStore.js index 65a132b516..e8702c309d 100644 --- a/app/store/MapLayerStore.js +++ b/app/store/MapLayerStore.js @@ -42,13 +42,13 @@ class MapLayerStore extends Store { config, TransportMode.Citybike, ); - - this.mapLayers.scooter = showRentalVehiclesOfType( - config.cityBike?.networks, - config, - TransportMode.Scooter, - ); - + this.mapLayers.scooter = + config.transportModes.scooter.hideUnlessSelectedForRouting && + showRentalVehiclesOfType( + config.cityBike?.networks, + config, + TransportMode.Scooter, + ); if (config.hideMapLayersByDefault) { this.mapLayers.stop = Object.keys(this.mapLayers.stop).map(() => false); diff --git a/static/assets/svg-sprite.default.svg b/static/assets/svg-sprite.default.svg index cee10aa5a7..23aea73290 100644 --- a/static/assets/svg-sprite.default.svg +++ b/static/assets/svg-sprite.default.svg @@ -2816,7 +2816,7 @@ - + From 0d741c8727ae0a9824fed55702ae69eac2970ef7 Mon Sep 17 00:00:00 2001 From: sharhio Date: Fri, 14 Jun 2024 12:09:14 +0300 Subject: [PATCH 31/79] old test config removed --- app/configurations/config.tampere.js | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/app/configurations/config.tampere.js b/app/configurations/config.tampere.js index 7fbf39be73..e876327026 100644 --- a/app/configurations/config.tampere.js +++ b/app/configurations/config.tampere.js @@ -256,29 +256,6 @@ export default configMerger(walttiConfig, { }, timeBeforeSurcharge: 60 * 60, }, - scooter_tampere: { - capacity: BIKEAVL_WITHMAX, - enabled: true, - season: { - // 15.4. - 31.10. - start: new Date(new Date().getFullYear(), 3, 15), - end: new Date(new Date().getFullYear(), 10, 1), - }, - icon: 'scooter', - name: { - fi: 'Tampere', - sv: 'Tammerfors', - en: 'Tampere', - }, - type: 'scooter', - // Shown if citybike leg duration exceeds timeBeforeSurcharge - durationInstructions: { - fi: 'https://www.nysse.fi/kaupunkipyorat', - sv: 'https://www.nysse.fi/en/city-bikes.html', - en: 'https://www.nysse.fi/en/city-bikes.html', - }, - timeBeforeSurcharge: 60 * 60, - }, }, buyUrl: { fi: 'https://www.nysse.fi/kaupunkipyorat', From cc6d345350c7e733bdcdaf67158ece7aab0c4ca1 Mon Sep 17 00:00:00 2001 From: sharhio Date: Fri, 14 Jun 2024 12:11:17 +0300 Subject: [PATCH 32/79] local test maplayer config removed --- app/configurations/config.default.js | 6 +++--- app/configurations/config.hsl.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/configurations/config.default.js b/app/configurations/config.default.js index bc3760adc3..35665069d0 100644 --- a/app/configurations/config.default.js +++ b/app/configurations/config.default.js @@ -58,13 +58,13 @@ export default { default: `${POI_MAP_PREFIX}/fi/rentalStations/`, }, REALTIME_RENTAL_STATION_MAP: { - default: `${OTP_URL}vectorTiles/realtimeRentalStations/`, + default: `${POI_MAP_PREFIX}/fi/realtimeRentalStations/`, }, RENTAL_VEHICLE_MAP: { - default: `${OTP_URL}vectorTiles/rentalVehicles/`, + default: `${POI_MAP_PREFIX}/fi/rentalVehicles/`, }, REALTIME_RENTAL_VEHICLE_MAP: { - default: `${OTP_URL}vectorTiles/realtimeRentalVehicles/`, + default: `${POI_MAP_PREFIX}/fi/realtimeRentalVehicles/`, }, PARK_AND_RIDE_MAP: { default: `${POI_MAP_PREFIX}/en/vehicleParking/`, diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index 16f4f4d2ac..6ae62be793 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -32,13 +32,13 @@ export default { default: `${POI_MAP_PREFIX}/fi/rentalStations/`, }, RENTAL_VEHICLE_MAP: { - default: `${OTP_URL}vectorTiles/rentalVehicles/`, + default: `${POI_MAP_PREFIX}/fi/rentalVehicles/`, }, REALTIME_RENTAL_VEHICLE_MAP: { - default: `${OTP_URL}vectorTiles/realtimeRentalVehicles/`, + default: `${POI_MAP_PREFIX}/fi/realtimeRentalVehicles/`, }, REALTIME_RENTAL_STATION_MAP: { - default: `${OTP_URL}vectorTiles/realtimeRentalStations/`, + default: `${POI_MAP_PREFIX}/fi/realtimeRentalStations/`, }, PARK_AND_RIDE_MAP: { default: `${POI_MAP_PREFIX}/en/vehicleParking/`, From b96e3ab9dc4571bcd2b900ea5267ff364cfe911d Mon Sep 17 00:00:00 2001 From: sharhio Date: Fri, 14 Jun 2024 13:28:00 +0300 Subject: [PATCH 33/79] fix error in merge --- sass/_main.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/sass/_main.scss b/sass/_main.scss index 1b93d316b0..d65d8a8e96 100644 --- a/sass/_main.scss +++ b/sass/_main.scss @@ -23,7 +23,6 @@ $body-font-weight: $font-weight-medium; @import '../app/component/navigation'; @import '../app/component/bike-park-rental-station'; @import '../app/component/rental-vehicle-content'; -@import '../app/component/route'; @import '../app/component/routepage/route'; @import '../app/component/checkbox'; @import '../app/component/toggle'; From 09965df569ce27f6677cb70450e6cbb05ec467fb Mon Sep 17 00:00:00 2001 From: sharhio Date: Mon, 17 Jun 2024 12:36:44 +0300 Subject: [PATCH 34/79] DT-6182 use vehicleRentalSystemType for system url --- app/component/RentalVehicleContent.js | 4 +++- app/component/itinerary/Itinerary.js | 4 +++- app/component/itinerary/ItineraryDetails.js | 8 ++++++-- app/util/vehicleRentalUtils.js | 4 ++-- build/schema.graphql | 15 +++++++++++---- .../schema/schema.graphql | 15 +++++++++++---- 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/app/component/RentalVehicleContent.js b/app/component/RentalVehicleContent.js index ad91985972..36fa727bad 100644 --- a/app/component/RentalVehicleContent.js +++ b/app/component/RentalVehicleContent.js @@ -151,7 +151,9 @@ const containerComponent = createFragmentContainer(connectedComponent, { ios web } - systemUrl + vehicleRentalSystem { + url + } } `, }); diff --git a/app/component/itinerary/Itinerary.js b/app/component/itinerary/Itinerary.js index 80f1431dba..99b588e18d 100644 --- a/app/component/itinerary/Itinerary.js +++ b/app/component/itinerary/Itinerary.js @@ -1046,7 +1046,9 @@ const containerComponent = createFragmentContainer(ItineraryWithBreakpoint, { ios web } - systemUrl + vehicleRentalSystem { + url + } } } to { diff --git a/app/component/itinerary/ItineraryDetails.js b/app/component/itinerary/ItineraryDetails.js index 28a8391a2b..9b1c286c5a 100644 --- a/app/component/itinerary/ItineraryDetails.js +++ b/app/component/itinerary/ItineraryDetails.js @@ -455,7 +455,9 @@ const withRelay = createFragmentContainer( ios web } - systemUrl + vehicleRentalSystem { + url + } } stop { gtfsId @@ -502,7 +504,9 @@ const withRelay = createFragmentContainer( ios web } - systemUrl + vehicleRentalSystem { + url + } } stop { gtfsId diff --git a/app/util/vehicleRentalUtils.js b/app/util/vehicleRentalUtils.js index fb9b4cf07f..545a18b4df 100644 --- a/app/util/vehicleRentalUtils.js +++ b/app/util/vehicleRentalUtils.js @@ -223,8 +223,8 @@ export const getRentalVehicleLink = (rentalVehicle, network, networkConfig) => { return web; } - if (rentalVehicle?.systemUrl?.includes(network)) { - return rentalVehicle.systemUrl; + if (rentalVehicle?.vehicleRentalSystem?.url?.includes(network)) { + return rentalVehicle.vehicleRentalSystem.url; } return null; diff --git a/build/schema.graphql b/build/schema.graphql index 485a0727ff..24ac9980f9 100644 --- a/build/schema.graphql +++ b/build/schema.graphql @@ -743,8 +743,8 @@ type RentalVehicle implements Node & PlaceInterface { """The type of the rental vehicle (scooter, bicycle, car...)""" vehicleType: RentalVehicleType - """The rental vehicle operator's system URL.""" - systemUrl: String! + """The vehicle rental system information.""" + vehicleRentalSystem: VehicleRentalSystemType } type BikeRentalStationUris { @@ -798,6 +798,11 @@ type RentalVehicleType { propulsionType: PropulsionType } +type VehicleRentalSystemType { + """The rental vehicle operator's system URL.""" + url: String +} + enum FormFactor { """A bicycle""" BICYCLE @@ -3537,7 +3542,9 @@ type QueryType { """ filterByModes: [Mode] - """Only include places that match one of the given network names.""" + """ + Only include places that match one of the given network names. + """ filterByNetworkNames: [String] """Only include places that match one of the given GTFS ids.""" @@ -5585,4 +5592,4 @@ input WheelchairPreferencesInput { that the itineraries are wheelchair accessible as there can be data issues. """ enabled: Boolean -} \ No newline at end of file +} diff --git a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql index 485a0727ff..24ac9980f9 100644 --- a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql +++ b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql @@ -743,8 +743,8 @@ type RentalVehicle implements Node & PlaceInterface { """The type of the rental vehicle (scooter, bicycle, car...)""" vehicleType: RentalVehicleType - """The rental vehicle operator's system URL.""" - systemUrl: String! + """The vehicle rental system information.""" + vehicleRentalSystem: VehicleRentalSystemType } type BikeRentalStationUris { @@ -798,6 +798,11 @@ type RentalVehicleType { propulsionType: PropulsionType } +type VehicleRentalSystemType { + """The rental vehicle operator's system URL.""" + url: String +} + enum FormFactor { """A bicycle""" BICYCLE @@ -3537,7 +3542,9 @@ type QueryType { """ filterByModes: [Mode] - """Only include places that match one of the given network names.""" + """ + Only include places that match one of the given network names. + """ filterByNetworkNames: [String] """Only include places that match one of the given GTFS ids.""" @@ -5585,4 +5592,4 @@ input WheelchairPreferencesInput { that the itineraries are wheelchair accessible as there can be data issues. """ enabled: Boolean -} \ No newline at end of file +} From cf0d76996e1b55ca82d80da3bf0f9faf3932c696 Mon Sep 17 00:00:00 2001 From: sharhio Date: Mon, 17 Jun 2024 15:16:44 +0300 Subject: [PATCH 35/79] DT-6182 filterByNetwork naming --- app/component/StopsNearYouContainer.js | 8 ++++---- app/component/StopsNearYouMapContainer.js | 8 ++++---- app/component/StopsNearYouPage.js | 10 +++++----- build/schema.graphql | 2 +- .../schema/schema.graphql | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/component/StopsNearYouContainer.js b/app/component/StopsNearYouContainer.js index 8e8bca573f..0bb3b07648 100644 --- a/app/component/StopsNearYouContainer.js +++ b/app/component/StopsNearYouContainer.js @@ -346,7 +346,7 @@ const refetchContainer = createPaginationContainer( after: { type: "String" } maxResults: { type: "Int" } maxDistance: { type: "Int" } - filterByNetworkNames: { type: "[String]", defaultValue: null } + filterByNetwork: { type: "[String]", defaultValue: null } ) { nearest( lat: $lat @@ -357,7 +357,7 @@ const refetchContainer = createPaginationContainer( after: $after maxResults: $maxResults maxDistance: $maxDistance - filterByNetworkNames: $filterByNetworkNames + filterByNetwork: $filterByNetwork ) @connection(key: "StopsNearYouContainer_nearest") { edges { node { @@ -422,7 +422,7 @@ const refetchContainer = createPaginationContainer( $maxDistance: Int! $startTime: Long! $omitNonPickups: Boolean! - $filterByNetworkNames: [String!] + $filterByNetwork: [String!] ) { viewer { ...StopsNearYouContainer_stopPatterns @@ -437,7 +437,7 @@ const refetchContainer = createPaginationContainer( after: $after maxResults: $maxResults maxDistance: $maxDistance - filterByNetworkNames: $filterByNetworkNames + filterByNetwork: $filterByNetwork ) } } diff --git a/app/component/StopsNearYouMapContainer.js b/app/component/StopsNearYouMapContainer.js index 6527f9db9b..b9757d11b1 100644 --- a/app/component/StopsNearYouMapContainer.js +++ b/app/component/StopsNearYouMapContainer.js @@ -48,7 +48,7 @@ const containerComponent = createPaginationContainer( after: { type: "String" } maxResults: { type: "Int" } maxDistance: { type: "Int" } - filterByNetworkNames: { type: "[String]", defaultValue: null } + filterByNetwork: { type: "[String]", defaultValue: null } ) { nearest( lat: $lat @@ -59,7 +59,7 @@ const containerComponent = createPaginationContainer( after: $after maxResults: $maxResults maxDistance: $maxDistance - filterByNetworkNames: $filterByNetworkNames + filterByNetwork: $filterByNetwork ) @connection(key: "StopsNearYouMapContainer_nearest") { edges { node { @@ -189,7 +189,7 @@ const containerComponent = createPaginationContainer( $maxDistance: Int! $startTime: Long! $omitNonPickups: Boolean! - $filterByNetworkNames: [String] + $filterByNetwork: [String] ) { viewer { ...StopsNearYouMapContainer_stopsNearYou @@ -204,7 +204,7 @@ const containerComponent = createPaginationContainer( after: $after maxResults: $maxResults maxDistance: $maxDistance - filterByNetworkNames: $filterByNetworkNames + filterByNetwork: $filterByNetwork ) } } diff --git a/app/component/StopsNearYouPage.js b/app/component/StopsNearYouPage.js index 1be41d72c9..6d7164cc66 100644 --- a/app/component/StopsNearYouPage.js +++ b/app/component/StopsNearYouPage.js @@ -234,7 +234,7 @@ class StopsNearYouPage extends React.Component { omitNonPickups: this.context.config.omitNonPickups, feedIds: this.context.config.feedIds, prioritizedStopIds: prioritizedStops, - filterByNetworkNames: allowedNetworks, + filterByNetwork: allowedNetworks, }; }; @@ -430,7 +430,7 @@ class StopsNearYouPage extends React.Component { $maxDistance: Int! $omitNonPickups: Boolean! $feedIds: [String!] - $filterByNetworkNames: [String!] + $filterByNetwork: [String!] ) { stopPatterns: viewer { ...StopsNearYouContainer_stopPatterns @@ -443,7 +443,7 @@ class StopsNearYouPage extends React.Component { maxResults: $maxResults maxDistance: $maxDistance omitNonPickups: $omitNonPickups - filterByNetworkNames: $filterByNetworkNames + filterByNetwork: $filterByNetwork ) } alerts: alerts(feeds: $feedIds, severityLevel: [SEVERE]) { @@ -725,7 +725,7 @@ class StopsNearYouPage extends React.Component { $maxDistance: Int! $omitNonPickups: Boolean! $prioritizedStopIds: [String!]! - $filterByNetworkNames: [String!] + $filterByNetwork: [String!] ) { stops: viewer { ...StopsNearYouMapContainer_stopsNearYou @@ -738,7 +738,7 @@ class StopsNearYouPage extends React.Component { maxResults: $maxResults maxDistance: $maxDistance omitNonPickups: $omitNonPickups - filterByNetworkNames: $filterByNetworkNames + filterByNetwork: $filterByNetwork ) } prioritizedStops: stops(ids: $prioritizedStopIds) { diff --git a/build/schema.graphql b/build/schema.graphql index 24ac9980f9..4ba9bf97d1 100644 --- a/build/schema.graphql +++ b/build/schema.graphql @@ -3545,7 +3545,7 @@ type QueryType { """ Only include places that match one of the given network names. """ - filterByNetworkNames: [String] + filterByNetwork: [String] """Only include places that match one of the given GTFS ids.""" filterByIds: InputFilters @deprecated(reason: "Not actively maintained") diff --git a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql index 24ac9980f9..4ba9bf97d1 100644 --- a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql +++ b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql @@ -3545,7 +3545,7 @@ type QueryType { """ Only include places that match one of the given network names. """ - filterByNetworkNames: [String] + filterByNetwork: [String] """Only include places that match one of the given GTFS ids.""" filterByIds: InputFilters @deprecated(reason: "Not actively maintained") From 1c0eb8556b0e807376acf9972547fdf51c70c28a Mon Sep 17 00:00:00 2001 From: sharhio Date: Mon, 17 Jun 2024 18:44:23 +0300 Subject: [PATCH 36/79] filterByNetwork description --- build/schema.graphql | 2 +- .../digitransit-search-util-query-utils/schema/schema.graphql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/schema.graphql b/build/schema.graphql index 4ba9bf97d1..fc6fbd6f27 100644 --- a/build/schema.graphql +++ b/build/schema.graphql @@ -3543,7 +3543,7 @@ type QueryType { filterByModes: [Mode] """ - Only include places that match one of the given network names. + Only include vehicle rental networks that match one of the given network names. """ filterByNetwork: [String] diff --git a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql index 4ba9bf97d1..fc6fbd6f27 100644 --- a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql +++ b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql @@ -3543,7 +3543,7 @@ type QueryType { filterByModes: [Mode] """ - Only include places that match one of the given network names. + Only include vehicle rental networks that match one of the given network names. """ filterByNetwork: [String] From 13ba69e0c41a70c021f0f619ce4656775d2da72a Mon Sep 17 00:00:00 2001 From: sharhio Date: Tue, 18 Jun 2024 11:51:03 +0300 Subject: [PATCH 37/79] DT-6182 schema fix --- build/schema.graphql | 7 +++++-- .../schema/schema.graphql | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/build/schema.graphql b/build/schema.graphql index fc6fbd6f27..a19ad34537 100644 --- a/build/schema.graphql +++ b/build/schema.graphql @@ -687,6 +687,9 @@ type VehicleRentalStation implements Node & PlaceInterface { If true, station is on and in service. """ operative: Boolean + + """The vehicle rental system information.""" + vehicleRentalSystem: VehicleRentalSystem } type RentalVehicleEntityCounts { @@ -744,7 +747,7 @@ type RentalVehicle implements Node & PlaceInterface { vehicleType: RentalVehicleType """The vehicle rental system information.""" - vehicleRentalSystem: VehicleRentalSystemType + vehicleRentalSystem: VehicleRentalSystem } type BikeRentalStationUris { @@ -798,7 +801,7 @@ type RentalVehicleType { propulsionType: PropulsionType } -type VehicleRentalSystemType { +type VehicleRentalSystem { """The rental vehicle operator's system URL.""" url: String } diff --git a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql index fc6fbd6f27..a19ad34537 100644 --- a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql +++ b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql @@ -687,6 +687,9 @@ type VehicleRentalStation implements Node & PlaceInterface { If true, station is on and in service. """ operative: Boolean + + """The vehicle rental system information.""" + vehicleRentalSystem: VehicleRentalSystem } type RentalVehicleEntityCounts { @@ -744,7 +747,7 @@ type RentalVehicle implements Node & PlaceInterface { vehicleType: RentalVehicleType """The vehicle rental system information.""" - vehicleRentalSystem: VehicleRentalSystemType + vehicleRentalSystem: VehicleRentalSystem } type BikeRentalStationUris { @@ -798,7 +801,7 @@ type RentalVehicleType { propulsionType: PropulsionType } -type VehicleRentalSystemType { +type VehicleRentalSystem { """The rental vehicle operator's system URL.""" url: String } From 5cd420a0d591b4dd5004a2ad8dda258539ef75af Mon Sep 17 00:00:00 2001 From: sharhio Date: Tue, 18 Jun 2024 12:58:03 +0300 Subject: [PATCH 38/79] DT-6182 missing icon added --- static/assets/svg-sprite.hsl.svg | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/static/assets/svg-sprite.hsl.svg b/static/assets/svg-sprite.hsl.svg index 37d7bff024..3bc295ea46 100644 --- a/static/assets/svg-sprite.hsl.svg +++ b/static/assets/svg-sprite.hsl.svg @@ -1174,6 +1174,25 @@ + + + + + + + + + + + + + + + + + + + From f37243e1ce2de098a6d6f21eee4ebe0c7df02270 Mon Sep 17 00:00:00 2001 From: sharhio Date: Tue, 18 Jun 2024 14:49:58 +0300 Subject: [PATCH 39/79] DT-6182 linter warning fixes --- app/component/RentalVehicle.js | 13 +++++------ app/component/RentalVehicleContent.js | 22 +++++++++++++++---- .../RentalVehiclePageMapContainer.js | 14 +++++++----- .../ScooterRentalNetworkSelector.js | 6 +++-- .../map/non-tile-layer/VehicleMarker.js | 2 +- app/component/routepage/TripLink.js | 2 +- app/util/shapes.js | 15 ++++++++++++- 7 files changed, 51 insertions(+), 23 deletions(-) diff --git a/app/component/RentalVehicle.js b/app/component/RentalVehicle.js index 4ff763bad4..ad9159176f 100644 --- a/app/component/RentalVehicle.js +++ b/app/component/RentalVehicle.js @@ -5,6 +5,7 @@ import { getVehicleRentalStationNetworkIcon, getVehicleRentalStationNetworkConfig, } from '../util/vehicleRentalUtils'; +import { rentalVehicleShape } from '../util/shapes'; const RentalVehicle = ({ rentalVehicle }, { config }) => { const disabled = !rentalVehicle.operative; @@ -21,15 +22,11 @@ const RentalVehicle = ({ rentalVehicle }, { config }) => { }; RentalVehicle.contextTypes = { - config: PropTypes.object.isRequired, + config: PropTypes.shape({ + cityBike: { networks: PropTypes.arrayOf(PropTypes.string.isRequired) }, + }).isRequired, }; RentalVehicle.propTypes = { - rentalVehicle: PropTypes.shape({ - availableVehicles: PropTypes.number.isRequired, - spacesAvailable: PropTypes.number.isRequired, - capacity: PropTypes.number.isRequired, - network: PropTypes.string, - operative: PropTypes.bool.isRequired, - }).isRequired, + rentalVehicle: rentalVehicleShape.isRequired, }; export default RentalVehicle; diff --git a/app/component/RentalVehicleContent.js b/app/component/RentalVehicleContent.js index 36fa727bad..6834b1479a 100644 --- a/app/component/RentalVehicleContent.js +++ b/app/component/RentalVehicleContent.js @@ -14,6 +14,7 @@ import { isBrowser } from '../util/browser'; import { PREFIX_RENTALVEHICLES } from '../util/path'; import VehicleRentalLeg from './itinerary/VehicleRentalLeg'; import BackButton from './BackButton'; +import { rentalVehicleShape } from '../util/shapes'; const RentalVehicleContent = ( { rentalVehicle, breakpoint, router, error, language, match }, @@ -114,16 +115,29 @@ const RentalVehicleContent = ( }; RentalVehicleContent.propTypes = { - rentalVehicle: PropTypes.any.isRequired, + rentalVehicle: rentalVehicleShape.isRequired, breakpoint: PropTypes.string.isRequired, router: routerShape.isRequired, - error: PropTypes.object, + error: PropTypes.shape({ + message: PropTypes.string, + }), language: PropTypes.string.isRequired, - match: PropTypes.object.isRequired, + match: PropTypes.shape({ + params: PropTypes.shape({ + networks: PropTypes.string, + }), + }), +}; + +RentalVehicleContent.defaultProps = { + error: undefined, + match: undefined, }; RentalVehicleContent.contextTypes = { - config: PropTypes.object.isRequired, + config: PropTypes.shape({ + text: PropTypes.string, + }).isRequired, }; const RentalVehicleContentWithBreakpoint = withBreakpoint(RentalVehicleContent); diff --git a/app/component/RentalVehiclePageMapContainer.js b/app/component/RentalVehiclePageMapContainer.js index bb27a4b7e7..8474bccb84 100644 --- a/app/component/RentalVehiclePageMapContainer.js +++ b/app/component/RentalVehiclePageMapContainer.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { createFragmentContainer, graphql } from 'react-relay'; import StopPageMap from './map/StopPageMap'; +import { rentalVehicleShape } from '../util/shapes'; const RentalVehiclePageMapContainer = ({ rentalVehicle }) => { if (!rentalVehicle) { @@ -11,15 +12,16 @@ const RentalVehiclePageMapContainer = ({ rentalVehicle }) => { }; RentalVehiclePageMapContainer.contextTypes = { - config: PropTypes.object.isRequired, + config: PropTypes.shape({ + map: PropTypes.shape({ + tileSize: PropTypes.number, + zoom: PropTypes.number, + }), + }), }; RentalVehiclePageMapContainer.propTypes = { - rentalVehicle: PropTypes.shape({ - lat: PropTypes.number.isRequired, - lon: PropTypes.number.isRequired, - name: PropTypes.string, - }), + rentalVehicle: rentalVehicleShape, }; RentalVehiclePageMapContainer.defaultProps = { diff --git a/app/component/itinerary/customizesearch/ScooterRentalNetworkSelector.js b/app/component/itinerary/customizesearch/ScooterRentalNetworkSelector.js index 3310d6b50b..b634d992ea 100644 --- a/app/component/itinerary/customizesearch/ScooterRentalNetworkSelector.js +++ b/app/component/itinerary/customizesearch/ScooterRentalNetworkSelector.js @@ -83,11 +83,13 @@ const ScooterRentalNetworkSelector = ( ); ScooterRentalNetworkSelector.propTypes = { - currentOptions: PropTypes.array.isRequired, + currentOptions: PropTypes.arrayOf(PropTypes.string).isRequired, }; ScooterRentalNetworkSelector.contextTypes = { - config: PropTypes.object.isRequired, + config: PropTypes.shape({ + transportModes: PropTypes.arrayOf(PropTypes.string), + }).isRequired, getStore: PropTypes.func.isRequired, executeAction: PropTypes.func.isRequired, }; diff --git a/app/component/map/non-tile-layer/VehicleMarker.js b/app/component/map/non-tile-layer/VehicleMarker.js index 34fd9651a0..d2729b6095 100644 --- a/app/component/map/non-tile-layer/VehicleMarker.js +++ b/app/component/map/non-tile-layer/VehicleMarker.js @@ -49,7 +49,7 @@ export default class VehicleMarker extends React.Component { rental: PropTypes.oneOfType([vehicleRentalStationShape, rentalVehicleShape]) .isRequired, transit: PropTypes.bool, - mode: PropTypes.string, + mode: PropTypes.string.isRequired, }; static contextTypes = { diff --git a/app/component/routepage/TripLink.js b/app/component/routepage/TripLink.js index 6961fdf0e3..32aea27947 100644 --- a/app/component/routepage/TripLink.js +++ b/app/component/routepage/TripLink.js @@ -87,7 +87,7 @@ TripLink.propTypes = { pattern: PropTypes.shape({ code: PropTypes.string, }), - }), + }).isRequired, vehicle: vehicleShape.isRequired, shortName: PropTypes.string, vehicleState: PropTypes.string, diff --git a/app/util/shapes.js b/app/util/shapes.js index 017aebc461..c28039f1c1 100644 --- a/app/util/shapes.js +++ b/app/util/shapes.js @@ -100,7 +100,20 @@ export const vehicleRentalStationShape = PropTypes.shape({ }); export const rentalVehicleShape = PropTypes.shape({ - network: PropTypes.string.isRequired, + id: PropTypes.string, + vehicleId: PropTypes.string, + name: PropTypes.string, + network: PropTypes.string, + lat: PropTypes.number, + lon: PropTypes.number, + rentalUris: PropTypes.shape({ + android: PropTypes.string, + ios: PropTypes.string, + web: PropTypes.string, + }), + vehicleRentalSystem: PropTypes.shape({ + url: PropTypes.string, + }), }); export const routeShape = PropTypes.shape({ From 827c0dda3a726273f2c4c5d4aa4954e526a8e4a6 Mon Sep 17 00:00:00 2001 From: sharhio Date: Tue, 18 Jun 2024 15:12:13 +0300 Subject: [PATCH 40/79] DT-6182 transportmode shape fix --- .../customizesearch/ScooterRentalNetworkSelector.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/component/itinerary/customizesearch/ScooterRentalNetworkSelector.js b/app/component/itinerary/customizesearch/ScooterRentalNetworkSelector.js index b634d992ea..a7d34c0b67 100644 --- a/app/component/itinerary/customizesearch/ScooterRentalNetworkSelector.js +++ b/app/component/itinerary/customizesearch/ScooterRentalNetworkSelector.js @@ -88,7 +88,11 @@ ScooterRentalNetworkSelector.propTypes = { ScooterRentalNetworkSelector.contextTypes = { config: PropTypes.shape({ - transportModes: PropTypes.arrayOf(PropTypes.string), + transportModes: PropTypes.shape({ + scooter: PropTypes.shape({ + networks: PropTypes.arrayOf(PropTypes.string), + }), + }), }).isRequired, getStore: PropTypes.func.isRequired, executeAction: PropTypes.func.isRequired, From c082452e4ba370efeb2b7ea646c71d1b9321e98a Mon Sep 17 00:00:00 2001 From: sharhio Date: Tue, 18 Jun 2024 16:28:58 +0300 Subject: [PATCH 41/79] DT-6182 config fixes, comment removed --- app/configurations/config.hsl.js | 8 -------- app/configurations/config.matka.js | 1 - app/server.js | 3 --- 3 files changed, 12 deletions(-) diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index 6ae62be793..04670834b6 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -31,12 +31,6 @@ export default { RENTAL_STATION_MAP: { default: `${POI_MAP_PREFIX}/fi/rentalStations/`, }, - RENTAL_VEHICLE_MAP: { - default: `${POI_MAP_PREFIX}/fi/rentalVehicles/`, - }, - REALTIME_RENTAL_VEHICLE_MAP: { - default: `${POI_MAP_PREFIX}/fi/realtimeRentalVehicles/`, - }, REALTIME_RENTAL_STATION_MAP: { default: `${POI_MAP_PREFIX}/fi/realtimeRentalStations/`, }, @@ -133,8 +127,6 @@ export default { 'mode-citybike': '#f2b62d', 'mode-citybike-secondary': '#333333', 'mode-speedtram': '#007E79', - 'mode-scooter': '#BABABA', - 'mode-scooter-secondary': '#333333', }, }, getAutoSuggestIcons: { diff --git a/app/configurations/config.matka.js b/app/configurations/config.matka.js index 28baf9190a..d71b196857 100644 --- a/app/configurations/config.matka.js +++ b/app/configurations/config.matka.js @@ -63,7 +63,6 @@ export default { 'mode-citybike': '#FCBC19', 'mode-citybike-secondary': '#333333', 'mode-scooter': '#BABABA', - 'mode-scooter-secondary': '#333333', }, }, feedIds: [ diff --git a/app/server.js b/app/server.js index cc3e8fa603..1ffecd7fbd 100644 --- a/app/server.js +++ b/app/server.js @@ -327,9 +327,6 @@ export default async function serve(req, res, next) { res.write('\n'); res.write(`\n`); res.write('\n'); - /* res.write( - '\n', - ); */ // local storage emitter is used from a hidden iframe and these are not necessary for it if (req.url !== LOCAL_STORAGE_EMITTER_PATH) { From c7bc2d79cbb7db0e788dcdef0d1c805132fda72d Mon Sep 17 00:00:00 2001 From: sharhio Date: Tue, 18 Jun 2024 16:37:00 +0300 Subject: [PATCH 42/79] DT-6182 removed extra method call and comment --- app/component/itinerary/ItineraryPage.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/component/itinerary/ItineraryPage.js b/app/component/itinerary/ItineraryPage.js index b12e2145b6..9af95a5f8c 100644 --- a/app/component/itinerary/ItineraryPage.js +++ b/app/component/itinerary/ItineraryPage.js @@ -418,14 +418,12 @@ export default function ItineraryPage(props, context) { context.config, TransportMode.Scooter, ); - getPlanParams(config, match, PLANTYPE.TRANSIT, true); const planParams = getPlanParams( config, match, PLANTYPE.SCOOTERTRANSIT, true, // force relaxed settings - // true, // force scooter query ); const tunedParams = { From 75a1d29207ced4aa85b1389ee6cf7c4a1f4d54d6 Mon Sep 17 00:00:00 2001 From: sharhio Date: Tue, 18 Jun 2024 16:53:52 +0300 Subject: [PATCH 43/79] DT-6182 scooter color fix, conditional scooter --- app/component/map/tile-layer/RentalVehicles.js | 10 +++------- app/store/MapLayerStore.js | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/component/map/tile-layer/RentalVehicles.js b/app/component/map/tile-layer/RentalVehicles.js index 13507459e6..4c8d09cc21 100644 --- a/app/component/map/tile-layer/RentalVehicles.js +++ b/app/component/map/tile-layer/RentalVehicles.js @@ -154,16 +154,12 @@ class RentalVehicles { if (zoomedIn || isHilighted) { drawScooterIcon(this.tile, geom, iconName, isHilighted); } else { - this.drawSmallScooterMarker(geom, iconName); + this.drawSmallScooterMarker(geom); } }; - drawSmallScooterMarker = (geom, iconName) => { - const iconColor = - iconName.includes('secondary') && - this.config.colors.iconColors['mode-scooter-secondary'] - ? this.config.colors.iconColors['mode-scooter-secondary'] - : this.config.colors.iconColors['mode-scooter']; + drawSmallScooterMarker = geom => { + const iconColor = this.config.colors.iconColors['mode-scooter']; drawSmallVehicleRentalMarker( this.tile, geom, diff --git a/app/store/MapLayerStore.js b/app/store/MapLayerStore.js index e8702c309d..f32378f2e1 100644 --- a/app/store/MapLayerStore.js +++ b/app/store/MapLayerStore.js @@ -43,7 +43,7 @@ class MapLayerStore extends Store { TransportMode.Citybike, ); this.mapLayers.scooter = - config.transportModes.scooter.hideUnlessSelectedForRouting && + config.transportModes.scooter?.hideUnlessSelectedForRouting && showRentalVehiclesOfType( config.cityBike?.networks, config, From 7967e1c7f8f9fae6850eef02adce7c6a6a7c3b0a Mon Sep 17 00:00:00 2001 From: sharhio Date: Tue, 18 Jun 2024 18:23:06 +0300 Subject: [PATCH 44/79] DT-6182 support for selecting single scooter markers, cluster id fix --- .../map/tile-layer/MarkerSelectPopup.js | 50 +++++++++++++------ .../map/tile-layer/SelectVehicleRentalRow.js | 12 +++-- .../map/tile-layer/TileLayerContainer.js | 3 ++ 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/app/component/map/tile-layer/MarkerSelectPopup.js b/app/component/map/tile-layer/MarkerSelectPopup.js index 86a84e50bf..3ed0c47dc9 100644 --- a/app/component/map/tile-layer/MarkerSelectPopup.js +++ b/app/component/map/tile-layer/MarkerSelectPopup.js @@ -49,20 +49,37 @@ function MarkerSelectPopup(props, { intl }) { /> ); } - if (option.layer === 'scooter' && option.feature.properties.cluster) { - return ( - - ); + if (option.layer === 'scooter') { + if (option.feature.properties.cluster) { + return ( + + ); + } + // Too many scooter markers when zoomed in + if (props.zoom < 18) { + return ( + + ); + } } if ( @@ -128,6 +145,11 @@ MarkerSelectPopup.propTypes = { ).isRequired, selectRow: PropTypes.func.isRequired, colors: popupColorShape.isRequired, + zoom: PropTypes.number, +}; + +MarkerSelectPopup.defaultProps = { + zoom: undefined, }; MarkerSelectPopup.contextTypes = { diff --git a/app/component/map/tile-layer/SelectVehicleRentalRow.js b/app/component/map/tile-layer/SelectVehicleRentalRow.js index 2ba99408ef..1300de43db 100644 --- a/app/component/map/tile-layer/SelectVehicleRentalRow.js +++ b/app/component/map/tile-layer/SelectVehicleRentalRow.js @@ -13,12 +13,14 @@ import { getIdWithoutFeed } from '../../../util/feedScopedIdUtils'; /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ function SelectVehicleRentalRow( - { name, network, id, desc, prefix }, + { name, network, id, desc, prefix, icon }, { config }, ) { - const img = `${getVehicleRentalStationNetworkIcon( - getVehicleRentalStationNetworkConfig(network, config), - )}-stop-lollipop`; + const img = + icon || + `${getVehicleRentalStationNetworkIcon( + getVehicleRentalStationNetworkConfig(network, config), + )}-stop-lollipop`; const linkAddress = `/${prefix}/${encodeURIComponent(id)}`; @@ -52,11 +54,13 @@ SelectVehicleRentalRow.propTypes = { id: PropTypes.string.isRequired, desc: PropTypes.string, prefix: PropTypes.string.isRequired, + icon: PropTypes.string, }; SelectVehicleRentalRow.defaultProps = { desc: undefined, name: undefined, + icon: undefined, }; SelectVehicleRentalRow.contextTypes = { diff --git a/app/component/map/tile-layer/TileLayerContainer.js b/app/component/map/tile-layer/TileLayerContainer.js index ce1c48ec33..d1a5bdcbc3 100644 --- a/app/component/map/tile-layer/TileLayerContainer.js +++ b/app/component/map/tile-layer/TileLayerContainer.js @@ -34,6 +34,7 @@ const initialState = { selectableTargets: undefined, coords: undefined, showSpinner: true, + zoom: undefined, }; // TODO eslint doesn't know that TileLayerContainer is a react component, @@ -296,6 +297,7 @@ class TileLayerContainer extends GridLayer { isFeatureLayerEnabled(target.feature, target.layer, mapLayers), ), coords, + zoom: tile.coords.z, }); }; @@ -421,6 +423,7 @@ class TileLayerContainer extends GridLayer { selectRow={this.selectRow} options={this.state.selectableTargets} colors={this.context.config.colors} + zoom={this.state.zoom} /> ); From 3d4c6e0539c9306f60c98d3442dabbabdd1991fd Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Wed, 19 Jun 2024 07:59:29 +0300 Subject: [PATCH 45/79] fix: restore path change caused by incorrect code review --- app/configurations/config.hsl.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index 04670834b6..e47f969dce 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -34,6 +34,12 @@ export default { REALTIME_RENTAL_STATION_MAP: { default: `${POI_MAP_PREFIX}/fi/realtimeRentalStations/`, }, + RENTAL_VEHICLE_MAP: { + default: `${POI_MAP_PREFIX}/fi/rentalVehicles/`, + }, + REALTIME_RENTAL_VEHICLE_MAP: { + default: `${POI_MAP_PREFIX}/fi/realtimeRentalVehicles/`, + }, PARK_AND_RIDE_MAP: { default: `${POI_MAP_PREFIX}/en/vehicleParking/`, sv: `${POI_MAP_PREFIX}/sv/vehicleParking/`, From ea61289fa5d9a731ac5f28cd9e5723f3ce851b08 Mon Sep 17 00:00:00 2001 From: sharhio Date: Wed, 19 Jun 2024 12:00:37 +0300 Subject: [PATCH 46/79] DT-6182 scooter details styles --- app/component/rental-vehicle-content.scss | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/component/rental-vehicle-content.scss b/app/component/rental-vehicle-content.scss index 8b562a406c..040862d526 100644 --- a/app/component/rental-vehicle-content.scss +++ b/app/component/rental-vehicle-content.scss @@ -48,11 +48,12 @@ .scooter-page-container { margin: 0 3.75em; + padding-top: 3em; .scooter-cluster-back-button-container .icon-container .icon { - width: 48px; - height: 48px; - margin-top: 90px; + width: 1.3em; + height: 1.3em; + margin-top: 0.4em; } .scooter-box { @@ -61,7 +62,6 @@ border: solid 1px #ddd; padding: 16px 21px 16px 18px; width: 100%; - margin-top: 24px; font-family: $font-family; font-size: $font-size-normal; font-weight: normal; @@ -231,7 +231,6 @@ padding-top: 2px; &.scooters-available { - margin-top: 22px; padding-left: 5px; } } @@ -250,5 +249,10 @@ width: 48px; height: 48px; } + + .back-button .icon-container .icon { + width: 1.3em; + height: 1.3em; + } } } From 4aea712eaa0d8e758144042a9590fcedb66b2b4c Mon Sep 17 00:00:00 2001 From: sharhio Date: Thu, 20 Jun 2024 14:17:40 +0300 Subject: [PATCH 47/79] remove comments --- app/component/RentalVehicleContent.js | 1 - app/component/itinerary/ItineraryPage.js | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/component/RentalVehicleContent.js b/app/component/RentalVehicleContent.js index 6834b1479a..39b4658ef2 100644 --- a/app/component/RentalVehicleContent.js +++ b/app/component/RentalVehicleContent.js @@ -152,7 +152,6 @@ const connectedComponent = connectToStores( ); const containerComponent = createFragmentContainer(connectedComponent, { - /* mitä muita tietoja rentalvehicleltä */ rentalVehicle: graphql` fragment RentalVehicleContent_rentalVehicle on RentalVehicle { lat diff --git a/app/component/itinerary/ItineraryPage.js b/app/component/itinerary/ItineraryPage.js index 9af95a5f8c..b9f08a43fc 100644 --- a/app/component/itinerary/ItineraryPage.js +++ b/app/component/itinerary/ItineraryPage.js @@ -376,8 +376,8 @@ export default function ItineraryPage(props, context) { return; } - ariaRef.current = 'itinerary-page.loading-itineraries'; // might need to be changed - setScooterState({ ...scooterState, loading: LOADSTATE.LOADING }); // check somewhere + ariaRef.current = 'itinerary-page.loading-itineraries'; + setScooterState({ ...scooterState, loading: LOADSTATE.LOADING }); const planParams = getPlanParams( config, @@ -394,7 +394,7 @@ export default function ItineraryPage(props, context) { const plan = await iterateQuery(tunedParams); setScooterState({ ...scooterState, plan, loading: LOADSTATE.DONE }); resetItineraryPageSelection(); - ariaRef.current = 'itinerary-page.itineraries-loaded'; // fix? + ariaRef.current = 'itinerary-page.itineraries-loaded'; } catch (error) { reportError(error); setScooterState({ From 7d9f086a9e4fd8e68790010c800ada712b3aa7e9 Mon Sep 17 00:00:00 2001 From: sharhio Date: Thu, 20 Jun 2024 14:20:17 +0300 Subject: [PATCH 48/79] DT-6182 reverse show when routing logic --- app/component/map/tile-layer/RentalVehicles.js | 2 +- app/configurations/config.hsl.js | 2 +- app/store/MapLayerStore.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/component/map/tile-layer/RentalVehicles.js b/app/component/map/tile-layer/RentalVehicles.js index 4c8d09cc21..889c902d77 100644 --- a/app/component/map/tile-layer/RentalVehicles.js +++ b/app/component/map/tile-layer/RentalVehicles.js @@ -60,7 +60,7 @@ class RentalVehicles { settings.allowedScooterRentalNetworks; const scooterIconPrefix = `icon-icon_scooter`; const showAllNetworks = - !this.config.transportModes.scooter.hideUnlessSelectedForRouting; + !this.config.transportModes.scooter.showIfSelectedForRouting; if (layer) { for (let i = 0, ref = layer.length - 1; i <= ref; i++) { const feature = layer.feature(i); diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index e47f969dce..2f084bd254 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -198,7 +198,7 @@ export default { scooter: { availableForSelection: true, defaultValue: false, - hideUnlessSelectedForRouting: true, + showIfSelectedForRouting: true, }, airplane: { availableForSelection: false, diff --git a/app/store/MapLayerStore.js b/app/store/MapLayerStore.js index f32378f2e1..1506c332e8 100644 --- a/app/store/MapLayerStore.js +++ b/app/store/MapLayerStore.js @@ -43,7 +43,7 @@ class MapLayerStore extends Store { TransportMode.Citybike, ); this.mapLayers.scooter = - config.transportModes.scooter?.hideUnlessSelectedForRouting && + config.transportModes.scooter?.showIfSelectedForRouting && showRentalVehiclesOfType( config.cityBike?.networks, config, From d87bbac5eb89e8c77cbbb6e3203490ba41a57b3c Mon Sep 17 00:00:00 2001 From: sharhio Date: Thu, 20 Jun 2024 14:43:00 +0300 Subject: [PATCH 49/79] merge disclaimer divs --- app/component/itinerary/BicycleLeg.js | 28 +++-- app/component/itinerary/CustomizeSearch.js | 62 +++++----- app/component/itinerary/customize-search.scss | 111 +++++++++--------- app/component/itinerary/itinerary.scss | 20 ++-- 4 files changed, 110 insertions(+), 111 deletions(-) diff --git a/app/component/itinerary/BicycleLeg.js b/app/component/itinerary/BicycleLeg.js index 638e42cf47..3529410c03 100644 --- a/app/component/itinerary/BicycleLeg.js +++ b/app/component/itinerary/BicycleLeg.js @@ -277,18 +277,22 @@ export default function BicycleLeg( )} {isScooter && !scooterSettingsOn && (
-
-
- - -
+
+ +
{this.state.showEScooterDisclaimer && ( -
-
-
- -
{ - if ( - isKeyboardSelectionEvent(e) && - (e.keyCode === 13 || e.keyCode === 32) - ) { - this.handleEScooterDisclaimerClose(); - } - }} - onClick={this.handleEScooterDisclaimerClose} - role="button" - > - -
+
+
+ +
{ + if ( + isKeyboardSelectionEvent(e) && + (e.keyCode === 13 || e.keyCode === 32) + ) { + this.handleEScooterDisclaimerClose(); + } + }} + onClick={this.handleEScooterDisclaimerClose} + role="button" + > +
-
- - ), - }} - /> -
-
{' '} +
+
+ + ), + }} + /> +
)} Date: Thu, 20 Jun 2024 14:57:41 +0300 Subject: [PATCH 50/79] DT-6182 remove unnecessary fields from queries --- app/component/itinerary/Itinerary.js | 12 ------------ app/component/itinerary/ItineraryDetails.js | 9 --------- 2 files changed, 21 deletions(-) diff --git a/app/component/itinerary/Itinerary.js b/app/component/itinerary/Itinerary.js index 99b588e18d..527d80e3c9 100644 --- a/app/component/itinerary/Itinerary.js +++ b/app/component/itinerary/Itinerary.js @@ -1036,19 +1036,7 @@ const containerComponent = createFragmentContainer(ItineraryWithBreakpoint, { network } rentalVehicle { - vehicleId - name - lat - lon network - rentalUris { - android - ios - web - } - vehicleRentalSystem { - url - } } } to { diff --git a/app/component/itinerary/ItineraryDetails.js b/app/component/itinerary/ItineraryDetails.js index 9b1c286c5a..9f88750445 100644 --- a/app/component/itinerary/ItineraryDetails.js +++ b/app/component/itinerary/ItineraryDetails.js @@ -495,18 +495,9 @@ const withRelay = createFragmentContainer( } rentalVehicle { vehicleId - name lat lon network - rentalUris { - android - ios - web - } - vehicleRentalSystem { - url - } } stop { gtfsId From a1bb888775260e48de75ff7281f6f560b6c52ac8 Mon Sep 17 00:00:00 2001 From: sharhio Date: Thu, 20 Jun 2024 15:44:40 +0300 Subject: [PATCH 51/79] DT-6182 scooter detail box layout fixed --- app/component/rental-vehicle-content.scss | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/component/rental-vehicle-content.scss b/app/component/rental-vehicle-content.scss index 040862d526..fc2c84168c 100644 --- a/app/component/rental-vehicle-content.scss +++ b/app/component/rental-vehicle-content.scss @@ -33,13 +33,13 @@ } .scooter-content-container { - padding: 0 0 0 0; + padding: 0 0 13px 0; margin-bottom: 13px; + border-bottom: 1px solid #ddd; .icon-container { .icon { - width: 40px; - height: 40px; + width: 2.25rem; } } } @@ -236,13 +236,17 @@ } .scooter-content-container { - padding: 15px 14px 22px; display: flex; height: 48px; box-sizing: content-box; + padding: 0 0 13px 0; + margin-bottom: 13px; + border-bottom: 1px solid #ddd; &.cluster { padding: 20px 20px; + border-bottom: none; + margin-bottom: 0; } .icon-container .icon { From 7ecaa4306976fc1ff58c59d582c689e4924bc075 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Mon, 24 Jun 2024 16:09:35 +0300 Subject: [PATCH 52/79] fix: rename network list parameter Don't call it allowedBikeRentalNetworks because it is used for scooters too. --- app/component/itinerary/ItineraryPage.js | 12 ++++-------- app/component/itinerary/PlanConnection.js | 4 ++-- app/util/planParamUtil.js | 4 ++++ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/component/itinerary/ItineraryPage.js b/app/component/itinerary/ItineraryPage.js index b9f08a43fc..8732e020a6 100644 --- a/app/component/itinerary/ItineraryPage.js +++ b/app/component/itinerary/ItineraryPage.js @@ -368,7 +368,7 @@ export default function ItineraryPage(props, context) { } } - async function makeScooterQuery(settings) { + async function makeScooterQuery() { if (!planQueryNeeded(config, match, PLANTYPE.TRANSIT)) { setScooterState(emptyPlan); setCombinedRentalState(emptyPlan); @@ -386,12 +386,8 @@ export default function ItineraryPage(props, context) { false, // no relaxed settings ); - const tunedParams = { - ...planParams, - allowedBikeRentalNetworks: settings.allowedScooterRentalNetworks, - }; try { - const plan = await iterateQuery(tunedParams); + const plan = await iterateQuery(planParams); setScooterState({ ...scooterState, plan, loading: LOADSTATE.DONE }); resetItineraryPageSelection(); ariaRef.current = 'itinerary-page.itineraries-loaded'; @@ -428,7 +424,7 @@ export default function ItineraryPage(props, context) { const tunedParams = { ...planParams, - allowedBikeRentalNetworks: allScooterNetworks, + allowedRentalNetworks: allScooterNetworks, }; try { const plan = await iterateQuery(tunedParams); @@ -727,7 +723,7 @@ export default function ItineraryPage(props, context) { useEffect(() => { const settings = getSettings(context.config); if (settings.allowedScooterRentalNetworks?.length > 0) { - makeScooterQuery(settings); + makeScooterQuery(); } else { setScooterState(emptyPlan); setCombinedRentalState(emptyPlan); diff --git a/app/component/itinerary/PlanConnection.js b/app/component/itinerary/PlanConnection.js index eb11b8860e..e72a5836a8 100644 --- a/app/component/itinerary/PlanConnection.js +++ b/app/component/itinerary/PlanConnection.js @@ -13,7 +13,7 @@ const planConnection = graphql` $wheelchair: Boolean $transferPenalty: Cost $bikeSpeed: Speed - $allowedBikeRentalNetworks: [String!] + $allowedRentalNetworks: [String!] $after: String $first: Int $before: String @@ -33,7 +33,7 @@ const planConnection = graphql` street: { bicycle: { speed: $bikeSpeed - rental: { allowedNetworks: $allowedBikeRentalNetworks } + rental: { allowedNetworks: $allowedRentalNetworks } } walk: { speed: $walkSpeed diff --git a/app/util/planParamUtil.js b/app/util/planParamUtil.js index c4a1c87bad..55de04832a 100644 --- a/app/util/planParamUtil.js +++ b/app/util/planParamUtil.js @@ -370,6 +370,10 @@ export function getPlanParams( return { ...settings, + allowedRentalNetworks: + planType === PLANTYPE.SCOOTERTRANSIT + ? settings.allowedScooterRentalNetworks + : settings.allowedBikeRentalNetworks, fromPlace, toPlace, datetime, From 51f65a25398ff4cdca377a02f95e0eff4960e3df Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Mon, 24 Jun 2024 17:17:19 +0300 Subject: [PATCH 53/79] fix: remove unused query fields --- app/component/itinerary/PlanConnection.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/component/itinerary/PlanConnection.js b/app/component/itinerary/PlanConnection.js index e72a5836a8..b8cab9adaf 100644 --- a/app/component/itinerary/PlanConnection.js +++ b/app/component/itinerary/PlanConnection.js @@ -91,10 +91,6 @@ const planConnection = graphql` vehicleRentalStation { stationId } - rentalVehicle { - vehicleId - network - } } to { lat From 1f11f6fe2d283f365f923427088558a9908fe6eb Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Wed, 26 Jun 2024 08:55:02 +0300 Subject: [PATCH 54/79] fix: remove more unused query fields --- app/component/itinerary/Itinerary.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/component/itinerary/Itinerary.js b/app/component/itinerary/Itinerary.js index 527d80e3c9..132d5e5441 100644 --- a/app/component/itinerary/Itinerary.js +++ b/app/component/itinerary/Itinerary.js @@ -1035,9 +1035,6 @@ const containerComponent = createFragmentContainer(ItineraryWithBreakpoint, { } network } - rentalVehicle { - network - } } to { stop { From b8f4c7b42c811035913244ba14e6ee37d69e6658 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Wed, 26 Jun 2024 16:19:13 +0300 Subject: [PATCH 55/79] chore: simplify scooter itinerary querying - rename for consistency - scooters can use exactly similar state as other itinerary queries --- app/component/itinerary/ItineraryPage.js | 54 +++++++++--------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/app/component/itinerary/ItineraryPage.js b/app/component/itinerary/ItineraryPage.js index 8732e020a6..4664461109 100644 --- a/app/component/itinerary/ItineraryPage.js +++ b/app/component/itinerary/ItineraryPage.js @@ -103,6 +103,7 @@ const emptyState = { }; const emptyPlan = { plan: {}, loading: LOADSTATE.DONE }; +const unset = { plan: {}, loading: LOADSTATE.UNSET }; export default function ItineraryPage(props, context) { const headerRef = useRef(null); @@ -115,11 +116,10 @@ export default function ItineraryPage(props, context) { loading: LOADSTATE.UNSET, }); const [relaxState, setRelaxState] = useState(emptyPlan); - const [relaxRentalState, setRelaxRentalState] = useState(emptyPlan); - const [scooterState, setScooterState] = useState(emptyPlan); - const [combinedRentalState, setCombinedRentalState] = useState(emptyPlan); + const [relaxScooterState, setRelaxScooterState] = useState(emptyPlan); + const [scooterState, setScooterState] = useState(unset); + const [combinedScooterState, setCombinedScooterState] = useState(emptyPlan); - const unset = { plan: {}, loading: LOADSTATE.UNSET }; const altStates = { [PLANTYPE.WALK]: useState(unset), [PLANTYPE.BIKE]: useState(unset), @@ -215,15 +215,15 @@ export default function ItineraryPage(props, context) { if (relaxState.plan?.edges?.length > 0) { return relaxState.plan; } - if (relaxRentalState.plan?.edges?.length > 0) { - return relaxRentalState.plan; + if (relaxScooterState.plan?.edges?.length > 0) { + return relaxScooterState.plan; } } if ( scooterState.loading === LOADSTATE.DONE && - combinedRentalState.plan?.edges?.length > 0 + combinedScooterState.plan?.edges?.length > 0 ) { - return combinedRentalState.plan; + return combinedScooterState.plan; } return state.plan; } @@ -371,13 +371,9 @@ export default function ItineraryPage(props, context) { async function makeScooterQuery() { if (!planQueryNeeded(config, match, PLANTYPE.TRANSIT)) { setScooterState(emptyPlan); - setCombinedRentalState(emptyPlan); - resetItineraryPageSelection(); return; } - - ariaRef.current = 'itinerary-page.loading-itineraries'; - setScooterState({ ...scooterState, loading: LOADSTATE.LOADING }); + setScooterState({ loading: LOADSTATE.LOADING }); const planParams = getPlanParams( config, @@ -388,28 +384,21 @@ export default function ItineraryPage(props, context) { try { const plan = await iterateQuery(planParams); - setScooterState({ ...scooterState, plan, loading: LOADSTATE.DONE }); + setScooterState({ plan, loading: LOADSTATE.DONE }); resetItineraryPageSelection(); - ariaRef.current = 'itinerary-page.itineraries-loaded'; } catch (error) { reportError(error); - setScooterState({ - ...scooterState, - plan: {}, - loading: LOADSTATE.UNSET, - error, - }); + setScooterState(emptyPlan); } } async function makeRelaxedScooterQuery() { if (!planQueryNeeded(config, match, PLANTYPE.TRANSIT)) { - setRelaxRentalState(emptyState); - resetItineraryPageSelection(); + setRelaxScooterState(emptyPlan); return; } - setRelaxRentalState({ loading: LOADSTATE.LOADING }); + setRelaxScooterState({ loading: LOADSTATE.LOADING }); const allScooterNetworks = getAllNetworksOfType( context.config, TransportMode.Scooter, @@ -428,12 +417,9 @@ export default function ItineraryPage(props, context) { }; try { const plan = await iterateQuery(tunedParams); - setRelaxRentalState({ - plan, - loading: LOADSTATE.DONE, - }); + setRelaxScooterState({ plan, loading: LOADSTATE.DONE }); } catch (error) { - setRelaxRentalState(emptyPlan); + setRelaxScooterState(emptyPlan); } } @@ -726,7 +712,7 @@ export default function ItineraryPage(props, context) { makeScooterQuery(); } else { setScooterState(emptyPlan); - setCombinedRentalState(emptyPlan); + setCombinedScooterState(emptyPlan); } makeMainQuery(); Object.keys(altStates).forEach(key => makeAltQuery(key)); @@ -787,7 +773,7 @@ export default function ItineraryPage(props, context) { bikePublicState.plan, altStates[PLANTYPE.PARKANDRIDE][0].plan, location.state?.selectedItineraryIndex, - relaxRentalState.plan, + relaxScooterState.plan, scooterState.plan, ]); @@ -822,7 +808,7 @@ export default function ItineraryPage(props, context) { scooterState?.plan?.edges?.length > 0 ) { const plan = mergeScooterTransitPlan(scooterState.plan, state.plan); - setCombinedRentalState({ plan }); + setCombinedScooterState({ plan }); } }, [scooterState.plan, state.plan]); @@ -997,7 +983,7 @@ export default function ItineraryPage(props, context) { let plan = mapHashToPlan(); const showRelaxedPlanNotifier = plan === relaxState.plan; - const showRentalVehicleNotifier = plan === relaxRentalState.plan; + const showRentalVehicleNotifier = plan === relaxScooterState.plan; /* NOTE: as a temporary solution, do filtering by feedId in UI */ if (config.feedIdFiltering && plan) { plan = filterItinerariesByFeedId(plan, config); @@ -1059,7 +1045,7 @@ export default function ItineraryPage(props, context) { state.loading === LOADSTATE.LOADING || scooterState.loading === LOADSTATE.LOADING || (relaxState.loading === LOADSTATE.LOADING && hasNoTransitItineraries) || - (relaxRentalState.loading === LOADSTATE.LOADING && + (relaxScooterState.loading === LOADSTATE.LOADING && hasNoTransitItineraries) || waitAlternatives || (streetHashes.includes(hash) && loadingAlt); // viewing unfinished alt plan From 21fb52f27929bcbd409f8ee3d92470db0e4dbcff Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Wed, 26 Jun 2024 16:57:51 +0300 Subject: [PATCH 56/79] fix: put query need checks where they belong --- app/component/itinerary/ItineraryPage.js | 18 ++++++------------ app/util/planParamUtil.js | 4 +++- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/component/itinerary/ItineraryPage.js b/app/component/itinerary/ItineraryPage.js index 4664461109..9ad2b26a09 100644 --- a/app/component/itinerary/ItineraryPage.js +++ b/app/component/itinerary/ItineraryPage.js @@ -212,6 +212,8 @@ export default function ItineraryPage(props, context) { !filterWalk(state.plan?.edges).length && !settingsState.settingsChanged ) { + // Note: plan and scooter plan are merged, but relaxed ones are not + // Is this intended behavior ? if (relaxState.plan?.edges?.length > 0) { return relaxState.plan; } @@ -369,7 +371,7 @@ export default function ItineraryPage(props, context) { } async function makeScooterQuery() { - if (!planQueryNeeded(config, match, PLANTYPE.TRANSIT)) { + if (!planQueryNeeded(config, match, PLANTYPE.SCOOTERTRANSIT)) { setScooterState(emptyPlan); return; } @@ -393,7 +395,7 @@ export default function ItineraryPage(props, context) { } async function makeRelaxedScooterQuery() { - if (!planQueryNeeded(config, match, PLANTYPE.TRANSIT)) { + if (!planQueryNeeded(config, match, PLANTYPE.SCOOTERTRANSIT, true)) { setRelaxScooterState(emptyPlan); return; } @@ -707,21 +709,13 @@ export default function ItineraryPage(props, context) { }, []); useEffect(() => { - const settings = getSettings(context.config); - if (settings.allowedScooterRentalNetworks?.length > 0) { - makeScooterQuery(); - } else { - setScooterState(emptyPlan); - setCombinedScooterState(emptyPlan); - } + makeScooterQuery(); makeMainQuery(); Object.keys(altStates).forEach(key => makeAltQuery(key)); if (settingsLimitRouting(config) && !settingsState.settingsChanged) { makeRelaxedQuery(); - if (!settings.allowedScooterRentalNetworks.length) { - makeRelaxedScooterQuery(); - } + makeRelaxedScooterQuery(); } }, [ settingsState.settingsChanged, diff --git a/app/util/planParamUtil.js b/app/util/planParamUtil.js index 55de04832a..526f243c00 100644 --- a/app/util/planParamUtil.js +++ b/app/util/planParamUtil.js @@ -206,7 +206,9 @@ export function planQueryNeeded( return ( transitModes.length > 0 && !wheelchair && - settings.allowedScooterRentalNetworks > 0 + ((!relaxSettings && settings.allowedScooterRentalNetworks > 0) || + /* special logic: relaxed scooter query is made only if no networks allowed */ + (relaxSettings && !settings.allowedScooterRentalNetworks.length)) ); case PLANTYPE.PARKANDRIDE: From 93f7f8ac6c2b300cbaf4262e523eb1d848b2d338 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Wed, 26 Jun 2024 17:12:20 +0300 Subject: [PATCH 57/79] fix: scooter and transit plan merge logic --- app/component/itinerary/ItineraryPage.js | 20 +++++++------------- app/util/planParamUtil.js | 8 ++++---- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/app/component/itinerary/ItineraryPage.js b/app/component/itinerary/ItineraryPage.js index 9ad2b26a09..fbe50a5f0d 100644 --- a/app/component/itinerary/ItineraryPage.js +++ b/app/component/itinerary/ItineraryPage.js @@ -118,7 +118,7 @@ export default function ItineraryPage(props, context) { const [relaxState, setRelaxState] = useState(emptyPlan); const [relaxScooterState, setRelaxScooterState] = useState(emptyPlan); const [scooterState, setScooterState] = useState(unset); - const [combinedScooterState, setCombinedScooterState] = useState(emptyPlan); + const [combinedState, setCombinedState] = useState(emptyPlan); const altStates = { [PLANTYPE.WALK]: useState(unset), @@ -221,13 +221,7 @@ export default function ItineraryPage(props, context) { return relaxScooterState.plan; } } - if ( - scooterState.loading === LOADSTATE.DONE && - combinedScooterState.plan?.edges?.length > 0 - ) { - return combinedScooterState.plan; - } - return state.plan; + return combinedState.plan; } } @@ -713,6 +707,8 @@ export default function ItineraryPage(props, context) { makeMainQuery(); Object.keys(altStates).forEach(key => makeAltQuery(key)); + // note: relaxed scooter query is not made unless some modes are disabled + // so, if no itineraries are found with standard settings, scooter is not suggested if (settingsLimitRouting(config) && !settingsState.settingsChanged) { makeRelaxedQuery(); makeRelaxedScooterQuery(); @@ -762,13 +758,12 @@ export default function ItineraryPage(props, context) { } }, [ hash, - state.plan, + combinedState.plan, relaxState.plan, bikePublicState.plan, altStates[PLANTYPE.PARKANDRIDE][0].plan, location.state?.selectedItineraryIndex, relaxScooterState.plan, - scooterState.plan, ]); useEffect(() => { @@ -798,11 +793,10 @@ export default function ItineraryPage(props, context) { useEffect(() => { if ( state.loading === LOADSTATE.DONE && - scooterState.loading === LOADSTATE.DONE && - scooterState?.plan?.edges?.length > 0 + scooterState.loading === LOADSTATE.DONE ) { const plan = mergeScooterTransitPlan(scooterState.plan, state.plan); - setCombinedScooterState({ plan }); + setCombinedState({ plan }); } }, [scooterState.plan, state.plan]); diff --git a/app/util/planParamUtil.js b/app/util/planParamUtil.js index 526f243c00..f000e8c8b0 100644 --- a/app/util/planParamUtil.js +++ b/app/util/planParamUtil.js @@ -203,14 +203,14 @@ export function planQueryNeeded( ); case PLANTYPE.SCOOTERTRANSIT: + /* special logic: relaxed scooter query is made only if no networks allowed */ return ( transitModes.length > 0 && !wheelchair && - ((!relaxSettings && settings.allowedScooterRentalNetworks > 0) || - /* special logic: relaxed scooter query is made only if no networks allowed */ - (relaxSettings && !settings.allowedScooterRentalNetworks.length)) + (relaxSettings + ? settings.allowedScooterRentalNetworks.length === 0 + : settings.allowedScooterRentalNetworks.length > 0) ); - case PLANTYPE.PARKANDRIDE: return ( transitModes.length > 0 && From 7292d2ef7481b73362588ab11f25663aec2af811 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Thu, 27 Jun 2024 08:03:32 +0300 Subject: [PATCH 58/79] fix: check combinedState loading instead of scooter and transit separately This prevents intermediate rendering before plan merging --- app/component/itinerary/ItineraryPage.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/component/itinerary/ItineraryPage.js b/app/component/itinerary/ItineraryPage.js index fbe50a5f0d..f819cb2994 100644 --- a/app/component/itinerary/ItineraryPage.js +++ b/app/component/itinerary/ItineraryPage.js @@ -703,12 +703,14 @@ export default function ItineraryPage(props, context) { }, []); useEffect(() => { + setCombinedState({ ...emptyState, loading: LOADSTATE.LOADING }); makeScooterQuery(); makeMainQuery(); Object.keys(altStates).forEach(key => makeAltQuery(key)); // note: relaxed scooter query is not made unless some modes are disabled // so, if no itineraries are found with standard settings, scooter is not suggested + // maybe it should be? if (settingsLimitRouting(config) && !settingsState.settingsChanged) { makeRelaxedQuery(); makeRelaxedScooterQuery(); @@ -796,7 +798,7 @@ export default function ItineraryPage(props, context) { scooterState.loading === LOADSTATE.DONE ) { const plan = mergeScooterTransitPlan(scooterState.plan, state.plan); - setCombinedState({ plan }); + setCombinedState({ plan, loading: LOADSTATE.DONE }); } }, [scooterState.plan, state.plan]); @@ -1030,8 +1032,7 @@ export default function ItineraryPage(props, context) { const loadingAlt = altLoading(); const waitAlternatives = hasNoTransitItineraries && loadingAlt; const loading = - state.loading === LOADSTATE.LOADING || - scooterState.loading === LOADSTATE.LOADING || + combinedState.loading === LOADSTATE.LOADING || (relaxState.loading === LOADSTATE.LOADING && hasNoTransitItineraries) || (relaxScooterState.loading === LOADSTATE.LOADING && hasNoTransitItineraries) || From 9eb2986544baee009187d1b3a88282b11726eff8 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Thu, 27 Jun 2024 08:39:13 +0300 Subject: [PATCH 59/79] fix: triplink props --- app/component/routepage/TripLink.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/component/routepage/TripLink.js b/app/component/routepage/TripLink.js index d2da13b36f..15d4a09dba 100644 --- a/app/component/routepage/TripLink.js +++ b/app/component/routepage/TripLink.js @@ -87,7 +87,7 @@ TripLink.propTypes = { pattern: PropTypes.shape({ code: PropTypes.string, }), - }).isRequired, + }), vehicle: vehicleShape.isRequired, shortName: PropTypes.string, vehicleState: PropTypes.string, From a6db3b265d50a040ae82bbc9e43f3bf0aa52ab49 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Thu, 27 Jun 2024 10:54:54 +0300 Subject: [PATCH 60/79] fix: remove unused rentalVehicle fields from itineraryLine --- app/component/map/ItineraryLine.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/component/map/ItineraryLine.js b/app/component/map/ItineraryLine.js index 58ff450430..800d6baa5c 100644 --- a/app/component/map/ItineraryLine.js +++ b/app/component/map/ItineraryLine.js @@ -303,9 +303,6 @@ export default createFragmentContainer(ItineraryLine, { } rentalVehicle { vehicleId - name - lat - lon network } stop { From 373b257dfe91734a8c801f7176c0a186ce40cb15 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Thu, 27 Jun 2024 12:35:09 +0300 Subject: [PATCH 61/79] fix: remove OTP1 modes --- app/configurations/config.default.js | 2 -- app/configurations/config.hsl.js | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/configurations/config.default.js b/app/configurations/config.default.js index b2a2735826..daed546b6a 100644 --- a/app/configurations/config.default.js +++ b/app/configurations/config.default.js @@ -437,12 +437,10 @@ export default { tram: 'TRAM', rail: 'RAIL', subway: 'SUBWAY', - citybike: 'BICYCLE_RENT', airplane: 'AIRPLANE', ferry: 'FERRY', funicular: 'FUNICULAR', walk: 'WALK', - scooter: 'SCOOTER_RENT', }, // Control what transport modes that should be possible to select in the UI diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index 2f084bd254..56481fc919 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -35,10 +35,10 @@ export default { default: `${POI_MAP_PREFIX}/fi/realtimeRentalStations/`, }, RENTAL_VEHICLE_MAP: { - default: `${POI_MAP_PREFIX}/fi/rentalVehicles/`, + default: `${OTP_URL}vectorTiles/rentalVehicles/`, }, REALTIME_RENTAL_VEHICLE_MAP: { - default: `${POI_MAP_PREFIX}/fi/realtimeRentalVehicles/`, + default: `${OTP_URL}vectorTiles/realtimeRentalVehicles/`, }, PARK_AND_RIDE_MAP: { default: `${POI_MAP_PREFIX}/en/vehicleParking/`, From e747ee367b3f48f233948ff7beba0fb2e5c93749 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Thu, 27 Jun 2024 14:51:45 +0300 Subject: [PATCH 62/79] fix: refactor badly outdated transport mode handling UI contained lots of complex code for adding citybike and scooter to transport modes array. In the end, those were just filtered away before making a request. Most of the old mess is now cleaned away. There is still some strange code related to availableForSelection config switch; it should probably be erased as well. --- .../ScooterRentalNetworkSelector.js | 10 -- .../customizesearch/TransportModesSection.js | 107 +++++++++--------- .../VehicleRentalStationNetworkSelector.js | 10 -- app/configurations/config.hsl.js | 4 +- app/store/RoutingSettingsStore.js | 1 - app/store/localStorage.js | 16 ++- app/util/modeUtils.js | 53 ++------- app/util/planParamUtil.js | 26 ++--- 8 files changed, 88 insertions(+), 139 deletions(-) diff --git a/app/component/itinerary/customizesearch/ScooterRentalNetworkSelector.js b/app/component/itinerary/customizesearch/ScooterRentalNetworkSelector.js index a7d34c0b67..8d09eae942 100644 --- a/app/component/itinerary/customizesearch/ScooterRentalNetworkSelector.js +++ b/app/component/itinerary/customizesearch/ScooterRentalNetworkSelector.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React from 'react'; -import xor from 'lodash/xor'; import Toggle from '../../Toggle'; import { saveRoutingSettings } from '../../../action/SearchSettingsActions'; import Icon from '../../Icon'; @@ -11,7 +10,6 @@ import { updateVehicleNetworks, getScooterRentalNetworks, } from '../../../util/vehicleRentalUtils'; -import { getModes } from '../../../util/modeUtils'; import { TransportMode } from '../../../constants'; const ScooterRentalNetworkSelector = ( @@ -62,17 +60,9 @@ const ScooterRentalNetworkSelector = ( getScooterRentalNetworks(config), network.networkName, ); - const modes = getModes(config); const newSettings = { allowedScooterRentalNetworks: newNetworks, }; - if (newNetworks.length > 0) { - if (modes.indexOf(TransportMode.Scooter) === -1) { - newSettings.modes = xor(modes, [TransportMode.Scooter]); - } - } else if (modes.indexOf(TransportMode.Scooter) !== -1) { - newSettings.modes = xor(modes, [TransportMode.Scooter]); - } executeAction(saveRoutingSettings, newSettings); }} /> diff --git a/app/component/itinerary/customizesearch/TransportModesSection.js b/app/component/itinerary/customizesearch/TransportModesSection.js index e38d20bdd5..dc708fa79c 100644 --- a/app/component/itinerary/customizesearch/TransportModesSection.js +++ b/app/component/itinerary/customizesearch/TransportModesSection.js @@ -9,19 +9,16 @@ import { saveRoutingSettings } from '../../../action/SearchSettingsActions'; import Toggle from '../../Toggle'; import Icon from '../../Icon'; import { - getAvailableTransportModes, + getTransitModes, getModes, toggleTransportMode, } from '../../../util/modeUtils'; -const TransportModesSection = ( - { config }, - { executeAction }, - transportModes = getAvailableTransportModes(config), - modes = getModes(config), -) => { +const TransportModesSection = ({ config }, { executeAction }) => { const { iconColors } = config.colors; const alternativeNames = config.useAlternativeNameForModes || []; + const transitModes = getTransitModes(config); + const selectedModes = getModes(config); return (
@@ -32,58 +29,56 @@ const TransportModesSection = ( />
- {transportModes - .filter(mode => mode !== 'CITYBIKE' && mode !== 'SCOOTER') - .map(mode => ( -
( +
+
+
+ +
+ + executeAction(saveRoutingSettings, { + modes: toggleTransportMode(mode, config), + }) + } + /> + +
+ ))}
); diff --git a/app/component/itinerary/customizesearch/VehicleRentalStationNetworkSelector.js b/app/component/itinerary/customizesearch/VehicleRentalStationNetworkSelector.js index 2761221aa5..f0942185b5 100644 --- a/app/component/itinerary/customizesearch/VehicleRentalStationNetworkSelector.js +++ b/app/component/itinerary/customizesearch/VehicleRentalStationNetworkSelector.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React from 'react'; -import xor from 'lodash/xor'; import { configShape } from '../../../util/shapes'; import Toggle from '../../Toggle'; import { saveRoutingSettings } from '../../../action/SearchSettingsActions'; @@ -12,7 +11,6 @@ import { updateVehicleNetworks, getCitybikeRentalStationNetworks, } from '../../../util/vehicleRentalUtils'; -import { getModes } from '../../../util/modeUtils'; import { TransportMode } from '../../../constants'; const VehicleRentalStationNetworkSelector = ( @@ -63,15 +61,7 @@ const VehicleRentalStationNetworkSelector = ( getCitybikeRentalStationNetworks(config), network.networkName, ); - const modes = getModes(config); const newSettings = { allowedBikeRentalNetworks: newNetworks }; - if (newNetworks.length > 0) { - if (modes.indexOf(TransportMode.Citybike) === -1) { - newSettings.modes = xor(modes, [TransportMode.Citybike]); - } - } else if (modes.indexOf(TransportMode.Citybike) !== -1) { - newSettings.modes = xor(modes, [TransportMode.Citybike]); - } executeAction(saveRoutingSettings, newSettings); }} /> diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index 56481fc919..2f084bd254 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -35,10 +35,10 @@ export default { default: `${POI_MAP_PREFIX}/fi/realtimeRentalStations/`, }, RENTAL_VEHICLE_MAP: { - default: `${OTP_URL}vectorTiles/rentalVehicles/`, + default: `${POI_MAP_PREFIX}/fi/rentalVehicles/`, }, REALTIME_RENTAL_VEHICLE_MAP: { - default: `${OTP_URL}vectorTiles/realtimeRentalVehicles/`, + default: `${POI_MAP_PREFIX}/fi/realtimeRentalVehicles/`, }, PARK_AND_RIDE_MAP: { default: `${POI_MAP_PREFIX}/en/vehicleParking/`, diff --git a/app/store/RoutingSettingsStore.js b/app/store/RoutingSettingsStore.js index a2c3c83cdc..2781bea34f 100644 --- a/app/store/RoutingSettingsStore.js +++ b/app/store/RoutingSettingsStore.js @@ -10,7 +10,6 @@ class RoutingSettingsStore extends Store { // eslint-disable-next-line class-methods-use-this getRoutingSettings() { let settings = getSearchSettingsStorage(); - if (!settings) { settings = {}; setSearchSettingsStorage(settings); diff --git a/app/store/localStorage.js b/app/store/localStorage.js index 607bdfea91..99edc76011 100644 --- a/app/store/localStorage.js +++ b/app/store/localStorage.js @@ -83,7 +83,14 @@ export function removeItem(k) { } export function getCustomizedSettings() { - return getItemAsJson('customizedSettings', '{}'); + const settings = getItemAsJson('customizedSettings', '{}'); + // remove outdated settings + if (settings.modes) { + settings.modes = settings.modes.filter( + mode => mode !== 'CITYBIKE' && mode !== 'SCOOTER', + ); + } + return settings; } const getNumberValueOrDefault = (value, defaultValue) => @@ -142,7 +149,12 @@ export function setCustomizedSettings(data) { oldSettings.showBikeAndParkItineraries, ), }; - + if (newSettings.modes) { + // cleanup + newSettings.modes = newSettings.modes.filter( + mode => mode !== 'CITYBIKE' && mode !== 'SCOOTER', + ); + } setItem('customizedSettings', newSettings); } diff --git a/app/util/modeUtils.js b/app/util/modeUtils.js index 41313b4b0b..3d6625cc6d 100644 --- a/app/util/modeUtils.js +++ b/app/util/modeUtils.js @@ -145,10 +145,13 @@ export function getAvailableTransportModeConfigs(config) { : []; } -export function getDefaultModes(config) { +export function getTransitModes(config) { return getAvailableTransportModeConfigs(config) - .filter(tm => tm.defaultValue) - .map(tm => tm.name); + .filter( + tm => tm.defaultValue && tm.name !== 'scooter' && tm.name !== 'citybike', + ) + .map(tm => tm.name) + .sort(); } /** @@ -250,7 +253,7 @@ export function filterModes(config, modes, from, to, intermediatePlaces) { /** * Giving user an option to change mode settings when there are no - * alternative options does not makse sense. This function checks + * alternative options does not make sense. This function checks * if there are at least two available transport modes * * @param {*} config @@ -261,40 +264,21 @@ export function showModeSettings(config) { } /** - * Retrieves all transport modes and returns the currently available + * Retrieves all transit modes and returns the currently available * If user has no ability to change mode settings, always use default modes. * * @param {*} config The configuration for the software * @returns {String[]} returns user set modes or default modes */ export function getModes(config) { - const { modes, allowedBikeRentalNetworks } = getCustomizedSettings(); - const activeAndAllowedBikeRentalNetworks = allowedBikeRentalNetworks - ? allowedBikeRentalNetworks.filter(x => networkIsActive(config, x)) - : []; + const { modes } = getCustomizedSettings(); if (showModeSettings(config) && Array.isArray(modes)) { const transportModes = modes.filter(mode => isTransportModeAvailable(config, mode), ); - if ( - activeAndAllowedBikeRentalNetworks && - activeAndAllowedBikeRentalNetworks.length > 0 && - transportModes.indexOf(TransportMode.Citybike) === -1 && - transportModes.indexOf(TransportMode.Scooter) === -1 - ) { - // Assume citybike if no rental network mode found - transportModes.push(TransportMode.Citybike); - } return transportModes; } - const defaultModes = getDefaultModes(config); - if ( - Array.isArray(activeAndAllowedBikeRentalNetworks) && - activeAndAllowedBikeRentalNetworks.length > 0 - ) { - defaultModes.push(TransportMode.Citybike); - } - return defaultModes; + return getTransitModes(config); } /** @@ -319,20 +303,3 @@ export function toggleTransportMode(transportMode, config) { const modes = xor(getModes(config), [transportMode.toUpperCase()]); return modes; } - -/** - * Transforms array of mode strings into modern format OTP mode objects - * - * @param {String[]} modes modes to filter from - * @returns {Object[]} array of objects of format - * {mode: }, qualifier: } - */ -export function modesAsOTPModes(modes) { - return modes - .map(mode => mode.split('_')) - .map(modeAndQualifier => - modeAndQualifier.length > 1 - ? { mode: modeAndQualifier[0], qualifier: modeAndQualifier[1] } - : { mode: modeAndQualifier[0] }, - ); -} diff --git a/app/util/planParamUtil.js b/app/util/planParamUtil.js index f000e8c8b0..1c8ce93c28 100644 --- a/app/util/planParamUtil.js +++ b/app/util/planParamUtil.js @@ -1,10 +1,6 @@ import moment from 'moment'; import isEqual from 'lodash/isEqual'; -import { - getDefaultModes, - modesAsOTPModes, - isTransportModeAvailable, -} from './modeUtils'; +import { getTransitModes, isTransportModeAvailable } from './modeUtils'; import { otpToLocation, getIntermediatePlaces } from './otpStrings'; import { getAllNetworksOfType, getDefaultNetworks } from './vehicleRentalUtils'; import { getCustomizedSettings } from '../store/localStorage'; @@ -57,7 +53,7 @@ export function getDefaultSettings(config) { return { ...config.defaultSettings, - modes: getDefaultModes(config).sort(), + modes: getTransitModes(config), allowedBikeRentalNetworks: config.transportModes?.citybike?.defaultValue ? getDefaultNetworks(config) : [], @@ -112,16 +108,14 @@ export function getSettings(config) { }; } -function getTransitModes(modes, planType, config) { - let transitModes = modes.filter(m => m !== 'CITYBIKE' && m !== 'SCOOTER'); +function filterTransitModes(modes, planType, config) { if (planType === PLANTYPE.BIKETRANSIT) { if (config.bikeBoardingModes) { - transitModes = transitModes.filter(m => config.bikeBoardingModes[m]); - } else { - return []; + return modes.filter(m => config.bikeBoardingModes[m]); } + return []; } - return transitModes; + return modes; } export function planQueryNeeded( @@ -151,7 +145,7 @@ export function planQueryNeeded( const defaultSettings = getDefaultSettings(config); const settings = getSettings(config); - const transitModes = getTransitModes( + const transitModes = filterTransitModes( relaxSettings ? defaultSettings.modes : settings.modes, planType, config, @@ -279,13 +273,15 @@ export function getPlanParams( settings.allowedBikeRentalNetworks = null; } - const transitModes = getTransitModes( + const transitModes = filterTransitModes( relaxSettings ? defaultSettings.modes : settings.modes, planType, config, ); - let otpModes = modesAsOTPModes(transitModes); + let otpModes = transitModes.map(mode => { + return { mode }; + }); if (config.customWeights) { otpModes.forEach(m => { if (config.customWeights[m.mode]) { From a0752616bf7cedb58d7a2dac4bd2b879b956e31d Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Thu, 27 Jun 2024 15:08:02 +0300 Subject: [PATCH 63/79] chore: update modeUtil unit tests --- test/unit/util/modeUtils.test.js | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/test/unit/util/modeUtils.test.js b/test/unit/util/modeUtils.test.js index a437d71669..01f81f3fd8 100644 --- a/test/unit/util/modeUtils.test.js +++ b/test/unit/util/modeUtils.test.js @@ -513,14 +513,14 @@ describe('modeUtils', () => { }, }, }; - const result = utils.getDefaultModes(modeConfig); + const result = utils.getTransitModes(modeConfig); expect(result.length).to.equal(1); expect(result).to.contain('D'); }); }); - describe('getDefaultModes', () => { + describe('getTransitModes', () => { it('should include only modes that are both available and default', () => { const modeConfig = { transportModes: { @@ -538,7 +538,7 @@ describe('modeUtils', () => { }, }, }; - const result = utils.getDefaultModes(modeConfig); + const result = utils.getTransitModes(modeConfig); expect(result.length).to.equal(1); expect(result).to.contain('D'); @@ -633,17 +633,4 @@ describe('modeUtils', () => { expect(result).to.contain(TransportMode.Bus); }); }); - - describe('modesAsOTPModes', () => { - it('should convert modes into modern OTP format mode objects', () => { - const modes = ['RAIL', 'BICYCLE_RENT', 'WALK', 'SCOOTER_RENT']; - const result = utils.modesAsOTPModes(modes); - - expect(result.length).to.equal(4); - expect(result[0]).to.deep.equal({ mode: 'RAIL' }); - expect(result[1]).to.deep.equal({ mode: 'BICYCLE', qualifier: 'RENT' }); - expect(result[2]).to.deep.equal({ mode: 'WALK' }); - expect(result[3]).to.deep.equal({ mode: 'SCOOTER', qualifier: 'RENT' }); - }); - }); }); From 40fb2f1719d5bbb8805e27368fcfe5e87388bcb4 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Thu, 27 Jun 2024 15:20:23 +0300 Subject: [PATCH 64/79] fix: remove useless modeToOtp util func It does nothing. --- app/configurations/config.default.js | 11 --- app/util/modeUtils.js | 17 ---- test/unit/util/modeUtils.test.js | 116 --------------------------- 3 files changed, 144 deletions(-) diff --git a/app/configurations/config.default.js b/app/configurations/config.default.js index daed546b6a..e6e6e46797 100644 --- a/app/configurations/config.default.js +++ b/app/configurations/config.default.js @@ -432,17 +432,6 @@ export default { useTicketIcons: false, - modeToOTP: { - bus: 'BUS', - tram: 'TRAM', - rail: 'RAIL', - subway: 'SUBWAY', - airplane: 'AIRPLANE', - ferry: 'FERRY', - funicular: 'FUNICULAR', - walk: 'WALK', - }, - // Control what transport modes that should be possible to select in the UI // and whether the transport mode is used in trip planning by default. transportModes: { diff --git a/app/util/modeUtils.js b/app/util/modeUtils.js index 3d6625cc6d..dddaa24104 100644 --- a/app/util/modeUtils.js +++ b/app/util/modeUtils.js @@ -164,22 +164,6 @@ export function getAvailableTransportModes(config) { return getAvailableTransportModeConfigs(config).map(tm => tm.name); } -/** - * Retrieves the related OTP mode from the given configuration, if available. - * This will return undefined if the given mode cannot be mapped. - * - * @param {*} config The configuration for the software installation - * @param {String} mode The mode to map - * @returns The mapped mode, or undefined - */ -export function getOTPMode(config, mode) { - if (!isString(mode)) { - return undefined; - } - const otpMode = config.modeToOTP[mode.toLowerCase()]; - return otpMode ? otpMode.toUpperCase() : undefined; -} - /** * Checks if the given transport mode has been configured as availableForSelection. * @@ -245,7 +229,6 @@ export function filterModes(config, modes, from, to, intermediatePlaces) { ...intermediatePlaces, ]), ) - .map(mode => getOTPMode(config, mode)) .filter(mode => !!mode) .sort(), ); diff --git a/test/unit/util/modeUtils.test.js b/test/unit/util/modeUtils.test.js index 01f81f3fd8..f9c5fc3851 100644 --- a/test/unit/util/modeUtils.test.js +++ b/test/unit/util/modeUtils.test.js @@ -113,39 +113,6 @@ describe('modeUtils', () => { expect(modes.length).to.equal(1); expect(modes).to.contain(TransportMode.Rail); }); - - it('should retrieve all modes with "defaultValue": true from config if has only one available transport mode', () => { - setCustomizedSettings({ - modes: [ - TransportMode.Rail, - 'FOO', - StreetMode.Walk, - StreetMode.ParkAndRide, - ], - }); - - const modeConfig = { - modeToOTP: { - bus: 'BUS', - citybike: 'CITYBIKE', - walk: 'WALK', - }, - transportModes: { - bus: { - availableForSelection: true, - defaultValue: true, - }, - citybike: { - availableForSelection: false, - defaultValue: false, - }, - }, - }; - - const modes = utils.getModes(modeConfig); - expect(modes.length).to.equal(1); - expect(modes).to.contain(TransportMode.Bus); - }); }); describe('getAvailableTransportModes', () => { @@ -164,47 +131,6 @@ describe('modeUtils', () => { }); }); - describe('getOTPMode', () => { - it('should return undefined if the given mode is undefined', () => { - expect(utils.getOTPMode(config, undefined)).to.equal(undefined); - }); - - it('should return undefined if the given mode is not a string', () => { - expect(utils.getOTPMode(config, {})).to.equal(undefined); - }); - - it('should not matter if the given mode is in UPPERCASE or lowercase', () => { - const modeConfig = { - modeToOTP: { - walk: 'WALK', - }, - }; - const upperCaseMode = 'WALK'; - const lowerCaseMode = 'walk'; - - expect(utils.getOTPMode(modeConfig, upperCaseMode)).to.equal('WALK'); - expect(utils.getOTPMode(modeConfig, lowerCaseMode)).to.equal('WALK'); - }); - - it('should return the configured OTP mode in UPPERCASE', () => { - const modeConfig = { - modeToOTP: { - walk: 'walk', - }, - }; - - expect(utils.getOTPMode(modeConfig, StreetMode.Walk)).to.equal('WALK'); - }); - - it('should return undefined for a missing mode', () => { - const modeConfig = { - modeToOTP: {}, - }; - - expect(utils.getOTPMode(modeConfig, StreetMode.Walk)).to.equal(undefined); - }); - }); - describe('isTransportModeAvailable', () => { it('should return true if mode has availableForSelection true', () => { expect(utils.isTransportModeAvailable(config, 'BUS')).to.equal(true); @@ -240,10 +166,6 @@ describe('modeUtils', () => { it('should support a modes array', () => { const modeConfig = { - modeToOTP: { - bus: 'BUS', - walk: 'WALK', - }, transportModes: { bus: { availableForSelection: true, @@ -266,10 +188,6 @@ describe('modeUtils', () => { it('should support a single mode', () => { const modeConfig = { - modeToOTP: { - bus: 'BUS', - walk: 'WALK', - }, transportModes: { bus: { availableForSelection: true, @@ -285,16 +203,11 @@ describe('modeUtils', () => { to, intermediatePlaces, ); - expect(result.length).to.equal(0); }); it('should support a comma-separated modes string', () => { const modeConfig = { - modeToOTP: { - bus: 'BUS', - walk: 'WALK', - }, transportModes: { bus: { availableForSelection: true, @@ -317,10 +230,6 @@ describe('modeUtils', () => { it('should omit missing OTP modes', () => { const modeConfig = { - modeToOTP: { - bus: 'BUS', - walk: 'WALK', - }, transportModes: { bus: { availableForSelection: true, @@ -343,11 +252,6 @@ describe('modeUtils', () => { it('should return only distinct OTP modes', () => { const modeConfig = { - modeToOTP: { - bus: 'BUS', - public_transport: 'WALK', - walk: 'WALK', - }, transportModes: { bus: { availableForSelection: true, @@ -370,12 +274,6 @@ describe('modeUtils', () => { it('should prevent the use of unavailable street or transport modes', () => { const modeConfig = { - modeToOTP: { - bus: 'BUS', - car: 'CAR', - rail: 'RAIL', - walk: 'WALK', - }, transportModes: { bus: { availableForSelection: true, @@ -401,11 +299,6 @@ describe('modeUtils', () => { it('should keep FERRY when there is a place inside FERRY modePolygons', () => { const modeConfig = { - modeToOTP: { - bus: 'BUS', - ferry: 'FERRY', - walk: 'WALK', - }, transportModes: { bus: { availableForSelection: true, @@ -453,11 +346,6 @@ describe('modeUtils', () => { it('should filter out FERRY when no places are inside FERRY modePolygons', () => { const modeConfig = { - modeToOTP: { - bus: 'BUS', - ferry: 'FERRY', - walk: 'WALK', - }, transportModes: { bus: { availableForSelection: true, @@ -589,10 +477,6 @@ describe('modeUtils', () => { availableForSelection: true, }, }, - modeToOTP: { - bus: 'BUS', - walk: 'WALK', - }, modePolygons: {}, }; const modes = 'BUS,WALK,UNKNOWN'; From 2670b3a9eec906097afc5bec5b015c1039678b78 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Thu, 27 Jun 2024 16:13:48 +0300 Subject: [PATCH 65/79] chore: refactor MapLayersDialogContent.js --- app/component/MapLayersDialogContent.js | 40 ++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/app/component/MapLayersDialogContent.js b/app/component/MapLayersDialogContent.js index 0ca75497c5..2435655694 100644 --- a/app/component/MapLayersDialogContent.js +++ b/app/component/MapLayersDialogContent.js @@ -127,7 +127,8 @@ class MapLayersDialogContent extends React.Component { } const isTransportModeEnabled = transportMode => transportMode && transportMode.availableForSelection; - const transportModes = getTransportModes(this.context.config); + const { config } = this.context; + const transportModes = getTransportModes(config); return (