Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add currentFuelPercent and currentRangeMeters to RentalVehichle in the GTFS GraphQL API #6272

Open
wants to merge 22 commits into
base: dev-2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f0ca862
add currentFuelPercent to VehicleRentalVehicle and gtfs graphql API
JustCris654 Nov 5, 2024
4e13710
add currentRangeMeters to VehicleRentalVehicle and gtfs graphql API
JustCris654 Nov 22, 2024
644f784
fix comment typo
JustCris654 Nov 22, 2024
062a907
use Distance class for currentRangeMeters
JustCris654 Dec 2, 2024
7d12fcd
add greater and less than methods for Distance
JustCris654 Dec 2, 2024
95cdf38
Distance class tests
JustCris654 Dec 2, 2024
a51398a
represent currentRangeMeters with integer type
JustCris654 Dec 3, 2024
22a40a1
format RentalVehicleImpl file
JustCris654 Dec 3, 2024
b0167fa
proper naming for static variables
JustCris654 Dec 3, 2024
a25e58a
use Ratio scalar for currentFuelPercent
JustCris654 Dec 4, 2024
5b94297
rename currentRangeMeters to currentRange
JustCris654 Dec 4, 2024
c8da0f4
move conversion of Distance to-from meters to the api and gbfs mapping
JustCris654 Dec 4, 2024
cd54bd1
remove unused code Distance class
JustCris654 Dec 4, 2024
f2b9f14
group range and percent in fuel type
JustCris654 Dec 9, 2024
57732a0
log warn if fuelPercent is invalid
JustCris654 Dec 9, 2024
86f88a6
check currentRangeMeters validity in free rental vehicle
JustCris654 Dec 11, 2024
2152235
Ratio class
JustCris654 Dec 11, 2024
9d8411b
Ratio class for fuel percent validation
JustCris654 Dec 17, 2024
c9c3a66
Ratio class and test format
JustCris654 Dec 17, 2024
086effd
fix check when range is required
JustCris654 Dec 17, 2024
ee50eee
format GbfsFreeVehicleStatusMapper
JustCris654 Dec 17, 2024
284133a
add range to scooter in GbfsFreeVehicleStatusMapperTest
JustCris654 Dec 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import java.util.Set;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.opentripplanner.ext.fares.model.Distance;
import org.opentripplanner.transit.model.basic.Distance;
import org.opentripplanner.ext.fares.model.FareDistance;
import org.opentripplanner.ext.fares.model.FareDistance.LinearDistance;
import org.opentripplanner.ext.fares.model.FareLegRule;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.opentripplanner.ext.fares.model;

import org.opentripplanner.transit.model.basic.Distance;

/** Represents a distance metric used in distance-based fare computation*/
public sealed interface FareDistance {
/** Represents the number of stops as a distance metric in fare computation */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers;
import org.opentripplanner.service.vehiclerental.model.RentalVehicleFuel;
import org.opentripplanner.service.vehiclerental.model.RentalVehicleType;
import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris;
import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem;
Expand All @@ -16,6 +17,11 @@ public DataFetcher<Boolean> allowPickupNow() {
return environment -> getSource(environment).allowPickupNow();
}

@Override
public DataFetcher<RentalVehicleFuel> fuel() {
return environment -> getSource(environment).getFuel();
}

@Override
public DataFetcher<Relay.ResolvedGlobalId> id() {
return environment ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import org.opentripplanner.service.vehicleparking.model.VehicleParkingSpaces;
import org.opentripplanner.service.vehicleparking.model.VehicleParkingState;
import org.opentripplanner.service.vehiclerental.model.RentalVehicleEntityCounts;
import org.opentripplanner.service.vehiclerental.model.RentalVehicleFuel;
import org.opentripplanner.service.vehiclerental.model.RentalVehicleType;
import org.opentripplanner.service.vehiclerental.model.RentalVehicleTypeCount;
import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace;
Expand Down Expand Up @@ -897,6 +898,8 @@ public interface GraphQLRentalPlace extends TypeResolver {}
public interface GraphQLRentalVehicle {
public DataFetcher<Boolean> allowPickupNow();

public DataFetcher<RentalVehicleFuel> fuel();

public DataFetcher<graphql.relay.Relay.ResolvedGlobalId> id();

public DataFetcher<Double> lat();
Expand Down Expand Up @@ -924,6 +927,13 @@ public interface GraphQLRentalVehicleEntityCounts {
public DataFetcher<Integer> total();
}

/** Rental vehicle fuel represent the current status of the battery or fuel of a rental vehicle */
public interface GraphQLRentalVehicleFuel {
public DataFetcher<Double> percent();

public DataFetcher<Integer> range();
}

public interface GraphQLRentalVehicleType {
public DataFetcher<org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLFormFactor> formFactor();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,5 @@ config:
CallRealTime: org.opentripplanner.apis.gtfs.model.CallRealTime#CallRealTime
RentalPlace: org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace#VehicleRentalPlace
CallSchedule: org.opentripplanner.apis.gtfs.model.CallSchedule#CallSchedule
RentalVehicleFuel: org.opentripplanner.service.vehiclerental.model.RentalVehicleFuel#RentalVehicleFuel

Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public static GraphQLObjectType create(
.name("currentRangeMeters")
.type(Scalars.GraphQLFloat)
.dataFetcher(environment ->
((VehicleRentalVehicle) environment.getSource()).currentRangeMeters
((VehicleRentalVehicle) environment.getSource()).getFuel().getRange()
)
.build()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

import java.util.Collection;
import java.util.Objects;
import org.opentripplanner.ext.fares.model.Distance;
import org.opentripplanner.ext.fares.model.FareDistance;
import org.opentripplanner.ext.fares.model.FareLegRule;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
import org.opentripplanner.transit.model.basic.Distance;
import org.opentripplanner.transit.model.framework.FeedScopedId;

public final class FareLegRuleMapper {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.opentripplanner.service.vehiclerental.model;

import javax.annotation.Nullable;
import org.opentripplanner.transit.model.basic.Distance;
import org.opentripplanner.transit.model.basic.Ratio;

/**
* Contains information about the current battery or fuel status.
* See the <a href="https://github.com/MobilityData/gbfs/blob/v3.0/gbfs.md#vehicle_statusjson">GBFS
* vehicle_status specification</a> for more details.
*/
public class RentalVehicleFuel {

/**
* Current fuel percentage, expressed from 0 to 1.
* <p>
* May be {@code null}.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's enough to mark the field as nullable.

*/
@Nullable
public final Ratio percent;

/**
* Distance that the vehicle can travel with the current charge or fuel.
* <p>
* May be {@code null}.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

*/
@Nullable
public final Distance range;

public RentalVehicleFuel(@Nullable Ratio fuelPercent, @Nullable Distance range) {
this.percent = fuelPercent;
this.range = range;
}

@Nullable
public Double getPercent() {
return this.percent != null ? this.percent.ratio : null;
}

@Nullable
public Integer getRange() {
return this.range != null ? this.range.toMeters() : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ public class VehicleRentalVehicle implements VehicleRentalPlace {
public boolean isReserved = false;
public boolean isDisabled = false;
public Instant lastReported;
public Double currentRangeMeters;
public VehicleRentalStation station;
public String pricingPlanId;
public RentalVehicleFuel fuel;

@Override
public FeedScopedId getId() {
Expand Down Expand Up @@ -133,4 +133,8 @@ public VehicleRentalStationUris getRentalUris() {
public VehicleRentalSystem getVehicleRentalSystem() {
return system;
}

public RentalVehicleFuel getFuel() {
return fuel;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.opentripplanner.ext.fares.model;
package org.opentripplanner.transit.model.basic;

import java.util.Objects;
import org.opentripplanner.utils.tostring.ValueObjectToStringBuilder;

public class Distance {
Expand All @@ -8,23 +9,35 @@ public class Distance {
private final double meters;

/** Returns a Distance object representing the given number of meters */
public Distance(double value) {
this.meters = value;
public Distance(double distanceInMeters) {
if (distanceInMeters < 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constructor should probably be private.

throw new IllegalArgumentException("Distance cannot be negative");
}

this.meters = distanceInMeters;
}

/** Returns a Distance object representing the given number of meters */
public static Distance ofMeters(double value) {
public static Distance ofMeters(double value) throws IllegalArgumentException {
if (value < 0) {
throw new IllegalArgumentException("Distance cannot be negative");
}

Comment on lines +22 to +24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to do the validation here as it's done in the constructor.

return new Distance(value);
}

/** Returns a Distance object representing the given number of kilometers */
public static Distance ofKilometers(double value) {
if (value < 0) {
throw new IllegalArgumentException("Distance cannot be negative");
}

Comment on lines +31 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

return new Distance(value * METERS_PER_KM);
}

/** Returns the distance in meters */
public double toMeters() {
return this.meters;
public int toMeters() {
return (int) Math.round(this.meters);
}

@Override
Expand All @@ -36,6 +49,11 @@ public boolean equals(Object other) {
}
}

@Override
public int hashCode() {
return Objects.hash(meters, "Distance");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's enough to just hash the field.


@Override
public String toString() {
if (meters < METERS_PER_KM) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.opentripplanner.transit.model.basic;

import java.util.Objects;

public class Ratio {

public final Double ratio;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field should be private and it should be exposed through a "asDouble()" or "toDouble()" method (I don't know if we have a naming convention for this, seems like we have mixed usage).


public Ratio(Double ratio) throws IllegalArgumentException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally don't tell that something throws an IllegalArgumentException.

if (ratio < 0d || ratio > 1d) {
throw new IllegalArgumentException("Ratio must be in range [0,1]");
}

this.ratio = ratio;
}

@Override
public boolean equals(Object other) {
if (other instanceof Ratio ratio) {
return ratio.ratio == this.ratio;
} else {
return false;
}
}

@Override
public int hashCode() {
return Objects.hash(this.ratio, "Ratio");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just hash the field.

}

@Override
public String toString() {
return this.ratio.toString();
Copy link
Member

@optionsome optionsome Dec 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the toString output the number as a percent (like 24.5%), I'm not sure. With cost for example we have a custom toString format.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@
import org.mobilitydata.gbfs.v2_3.free_bike_status.GBFSBike;
import org.mobilitydata.gbfs.v2_3.free_bike_status.GBFSRentalUris;
import org.opentripplanner.framework.i18n.NonLocalizedString;
import org.opentripplanner.service.vehiclerental.model.RentalVehicleFuel;
import org.opentripplanner.service.vehiclerental.model.RentalVehicleType;
import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris;
import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem;
import org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle;
import org.opentripplanner.transit.model.basic.Distance;
import org.opentripplanner.transit.model.basic.Ratio;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GbfsFreeVehicleStatusMapper {

private static final Logger LOG = LoggerFactory.getLogger(GbfsFreeVehicleStatusMapper.class);

private final VehicleRentalSystem system;

private final Map<String, RentalVehicleType> vehicleTypes;
Expand Down Expand Up @@ -52,7 +59,40 @@ public VehicleRentalVehicle mapFreeVehicleStatus(GBFSBike vehicle) {
vehicle.getLastReported() != null
? Instant.ofEpochSecond((long) (double) vehicle.getLastReported())
: null;
rentalVehicle.currentRangeMeters = vehicle.getCurrentRangeMeters();
Ratio fuelPercent = null;
try {
fuelPercent = new Ratio(vehicle.getCurrentFuelPercent());
} catch (IllegalArgumentException e) {
LOG.warn(
"Current fuel percent value not valid: {} - {}",
vehicle.getCurrentFuelPercent(),
e.getMessage()
);
} catch (NullPointerException e) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to catch npe, when would it happen?

Distance rangeMeters = null;
try {
rangeMeters =
vehicle.getCurrentRangeMeters() != null
? Distance.ofMeters(vehicle.getCurrentRangeMeters())
: null;
} catch (IllegalArgumentException e) {
LOG.warn(
"Current range meter value not valid: {} - {}",
vehicle.getCurrentRangeMeters(),
e.getMessage()
);
}
// if the propulsion type has an engine current_range_meters is required
if (
vehicle.getVehicleTypeId() != null &&
vehicleTypes.get(vehicle.getVehicleTypeId()) != null &&
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this check, can you double check if it's right?
I'm not sure but vehicle_type_id is REQUIRED if the vehicle_types.json file is defined, that file is REQUIRED for systems with free_bike_status.json and if this file is not included then all vehicles are non motorized bicycles.
Therefore if the vehicleTypeId is not present in the vehicleTypes map I can assume that the file vehicle_types.json is not present and all vehicles are not motorized, so the propulsion type is human and range is not needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is correct, but this is in an area where a lot of feeds get things wrong so this validation might cause issues but I'm not sure if I'm against this or not.

vehicleTypes.get(vehicle.getVehicleTypeId()).propulsionType !=
RentalVehicleType.PropulsionType.HUMAN &&
rangeMeters == null
) {
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should log a warning here since the whole vehicle is ignored.

}
rentalVehicle.fuel = new RentalVehicleFuel(fuelPercent, rangeMeters);
rentalVehicle.pricingPlanId = vehicle.getPricingPlanId();
GBFSRentalUris rentalUris = vehicle.getRentalUris();
if (rentalUris != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,8 @@ type RealTimeEstimate {
type RentalVehicle implements Node & PlaceInterface {
"If true, vehicle is currently available for renting."
allowPickupNow: Boolean
"Fuel or battery status of the rental vehicle"
fuel: RentalVehicleFuel
"Global object ID provided by Relay. This value can be used to refetch this object using **node** query."
id: ID!
"Latitude of the vehicle (WGS 84)"
Expand Down Expand Up @@ -1903,6 +1905,14 @@ type RentalVehicleEntityCounts {
total: Int!
}

"Rental vehicle fuel represent the current status of the battery or fuel of a rental vehicle"
type RentalVehicleFuel {
"Fuel or battery power remaining in the vehicle. Expressed from 0 to 1."
percent: Ratio
"Range in meters that the vehicle can travel with the current charge or fuel."
range: Int
}

type RentalVehicleType {
"The vehicle's general form factor"
formFactor: FormFactor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,17 @@ class GraphQLIntegrationTest {
.withSystem("Network-1", "https://foo.bar")
.build();

private static final VehicleRentalVehicle RENTAL_VEHICLE = new TestFreeFloatingRentalVehicleBuilder()
private static final VehicleRentalVehicle RENTAL_VEHICLE_1 = new TestFreeFloatingRentalVehicleBuilder()
.withSystem("Network-1", "https://foo.bar")
.build();

private static final VehicleRentalVehicle RENTAL_VEHICLE_2 = new TestFreeFloatingRentalVehicleBuilder()
.withSystem("Network-2", "https://foo.bar.baz")
.withNetwork("Network-2")
.withCurrentRangeMeters(null)
.withCurrentFuelPercent(null)
.build();

static final Graph GRAPH = new Graph();

static final Instant ALERT_START_TIME = OffsetDateTime
Expand Down Expand Up @@ -344,7 +351,8 @@ public Set<Route> findRoutes(StopLocation stop) {

DefaultVehicleRentalService defaultVehicleRentalService = new DefaultVehicleRentalService();
defaultVehicleRentalService.addVehicleRentalStation(VEHICLE_RENTAL_STATION);
defaultVehicleRentalService.addVehicleRentalStation(RENTAL_VEHICLE);
defaultVehicleRentalService.addVehicleRentalStation(RENTAL_VEHICLE_1);
defaultVehicleRentalService.addVehicleRentalStation(RENTAL_VEHICLE_2);

context =
new GraphQLRequestContext(
Expand Down Expand Up @@ -511,7 +519,7 @@ public List<PlaceAtDistance> findClosestPlaces(
return List.of(
new PlaceAtDistance(stop, 0),
new PlaceAtDistance(VEHICLE_RENTAL_STATION, 30),
new PlaceAtDistance(RENTAL_VEHICLE, 50)
new PlaceAtDistance(RENTAL_VEHICLE_1, 50)
);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
import org.onebusaway.gtfs.model.FareLegRule;
import org.onebusaway.gtfs.model.FareMedium;
import org.onebusaway.gtfs.model.FareProduct;
import org.opentripplanner.ext.fares.model.Distance;
import org.opentripplanner.ext.fares.model.FareDistance;
import org.opentripplanner.ext.fares.model.FareDistance.LinearDistance;
import org.opentripplanner.ext.fares.model.FareDistance.Stops;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
import org.opentripplanner.transit.model.basic.Distance;

class FareLegRuleMapperTest {

Expand Down
Loading
Loading