diff --git a/lib/actions/api.js b/lib/actions/api.js index 4f9198a33..e2c493510 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -317,50 +317,19 @@ export function parkAndRideQuery( // bike rental station query -export const bikeRentalError = createAction('BIKE_RENTAL_ERROR') -export const bikeRentalResponse = createAction('BIKE_RENTAL_RESPONSE') - -export function bikeRentalQuery( - params, - responseAction = bikeRentalResponse, - errorAction = bikeRentalError, - options = {} -) { - const paramsString = qs.stringify(params) - const endpoint = `bike_rental${paramsString ? `?${paramsString}` : ''}` - return createQueryAction(endpoint, responseAction, errorAction, options) +export function bikeRentalQuery() { + return executeOTPAction('findBikeRentalStations') } // Car rental (e.g. car2go) locations lookup query -export const carRentalResponse = createAction('CAR_RENTAL_RESPONSE') -export const carRentalError = createAction('CAR_RENTAL_ERROR') - -export function carRentalQuery(params) { - return createQueryAction('car_rental', carRentalResponse, carRentalError) +export function carRentalQuery() { + return executeOTPAction('findCarRentalStations') } -// Vehicle rental locations lookup query. For now, there are 3 separate -// "vehicle" rental endpoints - 1 for cars, 1 for bicycle rentals and another -// for micromobility. In the future, the hope is to consolidate these 3 -// endpoints into one. - -export const vehicleRentalResponse = createAction('VEHICLE_RENTAL_RESPONSE') -export const vehicleRentalError = createAction('VEHICLE_RENTAL_ERROR') - -export function vehicleRentalQuery( - params, - responseAction = vehicleRentalResponse, - errorAction = vehicleRentalError, - options = {} -) { - return executeOTPAction( - 'vehicleRentalQuery', - params, - responseAction, - errorAction, - options - ) +// Free-floating rental vehicles lookup query +export function rentalVehicleQuery() { + return executeOTPAction('findRentalVehicles') } // Nearby view lookup query diff --git a/lib/actions/apiV2.js b/lib/actions/apiV2.js index 4cefd77f4..bac845ed0 100644 --- a/lib/actions/apiV2.js +++ b/lib/actions/apiV2.js @@ -5,6 +5,7 @@ import { } from '@opentripplanner/trip-form' import { createAction } from 'redux-actions' import { decodeQueryParams, DelimitedArrayParam } from 'use-query-params' +import { FormFactor } from '@opentripplanner/types' import clone from 'clone' import coreUtils from '@opentripplanner/core-utils' @@ -184,60 +185,6 @@ const findTrip = (params) => } ) -export const vehicleRentalQuery = ( - params, - responseAction, - errorAction, - options -) => - // TODO: ErrorsByNetwork is missing - createGraphQLQueryAction( - `{ - rentalVehicles { - vehicleId - id - name - lat - lon - allowPickupNow - vehicleType { - formFactor - } - network - } - } - `, - {}, - responseAction, - errorAction, - { - noThrottle: true, - postprocess: (payload, dispatch) => { - if (payload.errors) { - return errorAction(payload.errors) - } - }, - // TODO: most of this rewrites the OTP2 response to match OTP1. - // we should re-write the rest of the UI to match OTP's behavior instead - rewritePayload: (payload) => { - return { - stations: payload?.data?.rentalVehicles?.map((vehicle) => { - return { - allowPickup: vehicle.allowPickupNow, - id: vehicle.vehicleId, - isFloatingBike: vehicle?.vehicleType?.formFactor === 'BICYCLE', - isFloatingVehicle: vehicle?.vehicleType?.formFactor === 'SCOOTER', - name: vehicle.name, - networks: [vehicle.network], - x: vehicle.lon, - y: vehicle.lat - } - }) - } - } - } - ) - // TODO: numberOfDepartures needs to come from config! const stopTimeGraphQLQuery = ` stopTimes: stoptimesForPatterns(numberOfDepartures: 3) { @@ -687,6 +634,127 @@ const getVehiclePositionsForRoute = (routeId) => ) } +const vehicleRentalStationsQuery = ` + query VehicleRentalStations { + vehicleRentalStations { + id + name + lat + lon + allowDropoff + allowPickup + rentalNetwork { + networkId + } + availableVehicles { + total + byType { + vehicleType { + formFactor + } + } + } + availableSpaces { + total + byType { + vehicleType { + formFactor + } + } + } + realtime + } + }` + +const vehicleRentalStationFilter = (formFactor) => (station) => + (station.availableVehicles && + station.availableVehicles.byType.some( + (av) => av.vehicleType.formFactor === formFactor + )) || + (station.availableSpaces && + station.availableSpaces.byType.some( + (as) => as.vehicleType.formFactor === formFactor + )) + +const bikeRentalError = createAction('BIKE_RENTAL_ERROR') +const bikeRentalResponse = createAction('BIKE_RENTAL_RESPONSE') + +export function findBikeRentalStations() { + return function (dispatch) { + dispatch( + createGraphQLQueryAction( + vehicleRentalStationsQuery, + {}, + bikeRentalResponse, + bikeRentalError, + { + rewritePayload: (payload) => + payload.data.vehicleRentalStations.filter( + vehicleRentalStationFilter('BICYCLE') + ) + } + ) + ) + } +} + +export const carRentalResponse = createAction('CAR_RENTAL_RESPONSE') +export const carRentalError = createAction('CAR_RENTAL_ERROR') + +export function findCarRentalStations() { + return function (dispatch) { + dispatch( + createGraphQLQueryAction( + vehicleRentalStationsQuery, + {}, + carRentalResponse, + carRentalError, + { + rewritePayload: (payload) => + payload.data.vehicleRentalStations.filter( + vehicleRentalStationFilter('CAR') + ) + } + ) + ) + } +} + +const rentalVehiclesQuery = ` + query RentalVehicles { + rentalVehicles { + allowPickupNow + id + lat + lon + name + operative + rentalNetwork { + networkId + } + vehicleType { + formFactor + } + } + }` + +const vehicleRentalResponse = createAction('VEHICLE_RENTAL_RESPONSE') +const vehicleRentalError = createAction('VEHICLE_RENTAL_ERROR') + +export function findRentalVehicles() { + return function (dispatch) { + dispatch( + createGraphQLQueryAction( + rentalVehiclesQuery, + {}, + vehicleRentalResponse, + vehicleRentalError, + {} + ) + ) + } +} + export const findRoute = (params) => function (dispatch, getState) { const { routeId } = params @@ -1369,8 +1437,11 @@ const retrieveServiceTimeRangeIfNeeded = () => export default { fetchNearby, + findBikeRentalStations, + findCarRentalStations, findFeeds, findPatternsForRoute, + findRentalVehicles, findRoute, findRoutes, findStopsWithinBBox, @@ -1378,6 +1449,5 @@ export default { findTrip, getVehiclePositionsForRoute, retrieveServiceTimeRangeIfNeeded, - routingQuery, - vehicleRentalQuery + routingQuery } diff --git a/lib/components/map/default-map.tsx b/lib/components/map/default-map.tsx index 3e0568e3e..8effba14e 100644 --- a/lib/components/map/default-map.tsx +++ b/lib/components/map/default-map.tsx @@ -2,27 +2,38 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck import { connect } from 'react-redux' +import { + FormFactor, + RentalVehicle, + VehicleRentalStation +} from '@opentripplanner/types/otp2' import { GeolocateControl, NavigationControl } from 'react-map-gl/maplibre' import { getCurrentDate } from '@opentripplanner/core-utils/lib/time' -import { injectIntl } from 'react-intl' +import { injectIntl, IntlShape } from 'react-intl' +import { Itinerary } from '@opentripplanner/types' import BaseMap from '@opentripplanner/base-map' import generateOTP2TileLayers from '@opentripplanner/otp2-tile-overlay' import React, { Component } from 'react' import styled from 'styled-components' +import { AppConfig, MapConfig } from '../../util/config-types' import { assembleBasePath, bikeRentalQuery, carRentalQuery, findFeeds, findStopTimesForStop, - vehicleRentalQuery + rentalVehicleQuery } from '../../actions/api' import { ComponentContext } from '../../util/contexts' import { getActiveItinerary, getActiveSearch } from '../../util/state' -import { getCurrentPosition } from '../../actions/location' +import { + getCurrentPosition, + GetCurrentPositionFunction +} from '../../actions/location' import { MainPanelContent } from '../../actions/ui-constants' import { setLocation, setMapPopupLocationAndGeocode } from '../../actions/map' +import { SetLocationHandler, SetViewedStopHandler } from '../util/types' import { setViewedStop } from '../../actions/ui' import { updateOverlayVisibility } from '../../actions/config' import TransitOperatorIcons from '../util/connected-transit-operator-icons' @@ -145,10 +156,29 @@ function getLayerName(overlay, config, intl) { } } -class DefaultMap extends Component { +interface DefaultMapProps { + bikeRentalQuery: () => void + bikeRentalStations: VehicleRentalStation[] + carRentalQuery: () => void + carRentalStations: VehicleRentalStation[] + config: AppConfig + getCurrentPosition: GetCurrentPositionFunction + intl: IntlShape + itinerary: Itinerary + mapConfig: MapConfig + nearbyViewActive: boolean + pending: boolean + rentalVehicleQuery: () => void + rentalVehicles: RentalVehicle[] + setLocation: SetLocationHandler + setViewedStop: SetViewedStopHandler + viewedRouteStops: string[] +} + +class DefaultMap extends Component { static contextType = ComponentContext - constructor(props) { + constructor(props: DefaultMapProps) { super(props) // We have to maintain the map state because the underlying map also (incorrectly?) uses a state. // Not maintaining a state causes re-renders to the map's configured coordinates. @@ -327,10 +357,10 @@ class DefaultMap extends Component { nearbyFilters, nearbyViewActive, pending, + rentalVehicleQuery, + rentalVehicles, setLocation, setViewedStop, - vehicleRentalQuery, - vehicleRentalStations, viewedRouteStops } = this.props const { getCustomMapOverlays, getTransitiveRouteLabel, ModeIcon } = @@ -342,17 +372,20 @@ class DefaultMap extends Component { config.api?.path }/vectorTiles` - const bikeStations = [ - ...bikeRentalStations.filter( - (station) => - !station.isFloatingVehicle || station.isFloatingVehicle === false - ), - ...vehicleRentalStations.filter( - (station) => station.isFloatingBike === true + const bikeStationsAndFloatingBikes = [ + ...bikeRentalStations, + ...rentalVehicles.filter( + (station) => station.vehicleType?.formFactor === 'BICYCLE' ) ] - const scooterStations = vehicleRentalStations.filter( - (station) => station.isFloatingBike === false && station.isFloatingVehicle + + const scooters = rentalVehicles.filter( + (vehicle) => vehicle.vehicleType?.formFactor === 'SCOOTER' + ) + + const micromobility = rentalVehicles.filter( + (vehicle) => + vehicle.vehicleType && vehicle.vehicleType.formFactor !== 'CAR' ) const baseLayersWithNames = baseLayers?.map((baseLayer) => ({ @@ -440,16 +473,16 @@ class DefaultMap extends Component { return ( ) case 'car-rental': return ( ) case 'park-and-ride': @@ -460,8 +493,8 @@ class DefaultMap extends Component { return ( ) case 'otp2-micromobility-rental': @@ -469,8 +502,8 @@ class DefaultMap extends Component { ) case 'otp2-bike-rental': @@ -478,8 +511,8 @@ class DefaultMap extends Component { ) case 'otp2': @@ -558,7 +591,7 @@ const mapStateToProps = (state) => { state.otp.ui.mainPanelContent === MainPanelContent.NEARBY_VIEW, pending: activeSearch ? Boolean(activeSearch.pending) : false, query: state.otp.currentQuery, - vehicleRentalStations: state.otp.overlay.vehicleRental.stations, + rentalVehicles: state.otp.overlay.vehicleRental.stations, viewedRouteStops } } @@ -569,11 +602,11 @@ const mapDispatchToProps = { findFeeds, findStopTimesForStop, getCurrentPosition, + rentalVehicleQuery, setLocation, setMapPopupLocationAndGeocode, setViewedStop, - updateOverlayVisibility, - vehicleRentalQuery + updateOverlayVisibility } export default connect( diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 1e7e775d3..9eb3e69c4 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -471,7 +471,7 @@ function createOtpReducer(config) { overlay: { bikeRental: { pending: { $set: false }, - stations: { $set: action.payload.stations } + stations: { $set: action.payload } } } }) @@ -507,7 +507,7 @@ function createOtpReducer(config) { overlay: { vehicleRental: { pending: { $set: false }, - stations: { $set: action.payload.stations } + stations: { $set: action.payload.data.rentalVehicles } } } })