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 36 commits into
base: dev-2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7fb3810
add currentFuelPercent to VehicleRentalVehicle and gtfs graphql API
JustCris654 Nov 5, 2024
b61c9f3
add currentRangeMeters to VehicleRentalVehicle and gtfs graphql API
JustCris654 Nov 22, 2024
1fa55b0
fix comment typo
JustCris654 Nov 22, 2024
4686f7c
use Distance class for currentRangeMeters
JustCris654 Dec 2, 2024
e93c828
add greater and less than methods for Distance
JustCris654 Dec 2, 2024
49c4682
Distance class tests
JustCris654 Dec 2, 2024
54292e2
represent currentRangeMeters with integer type
JustCris654 Dec 3, 2024
851c8b5
format RentalVehicleImpl file
JustCris654 Dec 3, 2024
d26799f
proper naming for static variables
JustCris654 Dec 3, 2024
e0bd36a
use Ratio scalar for currentFuelPercent
JustCris654 Dec 4, 2024
e9a572d
rename currentRangeMeters to currentRange
JustCris654 Dec 4, 2024
498d547
move conversion of Distance to-from meters to the api and gbfs mapping
JustCris654 Dec 4, 2024
eda75c8
remove unused code Distance class
JustCris654 Dec 4, 2024
3e7bf5b
group range and percent in fuel type
JustCris654 Dec 9, 2024
7d15f00
log warn if fuelPercent is invalid
JustCris654 Dec 9, 2024
9b9b585
check currentRangeMeters validity in free rental vehicle
JustCris654 Dec 11, 2024
c90bdd4
Ratio class
JustCris654 Dec 11, 2024
346449b
Ratio class for fuel percent validation
JustCris654 Dec 17, 2024
6edf98b
Ratio class and test format
JustCris654 Dec 17, 2024
0b1920d
fix check when range is required
JustCris654 Dec 17, 2024
54b0bc0
format GbfsFreeVehicleStatusMapper
JustCris654 Dec 17, 2024
f3e0a71
add range to scooter in GbfsFreeVehicleStatusMapperTest
JustCris654 Dec 17, 2024
26d8d7d
general fixes Ratio.java and Distance.java
JustCris654 Jan 7, 2025
4788055
RentalvehicleFuel properties docs comments
JustCris654 Jan 8, 2025
ff8411a
javadoc comment for Ratio class
JustCris654 Jan 9, 2025
6778169
check getCurrentFuelPercent null value and drop NPE
JustCris654 Jan 9, 2025
62e671f
Example on factory method with validation error handler passed in as …
t2gran Jan 9, 2025
f7d6b36
Cleanup Ratio implementation
t2gran Jan 9, 2025
4312caf
throttle invalid current fuel percent log
JustCris654 Jan 16, 2025
cd38992
Merge remote-tracking branch 'otp_master/pr-6272' into rentalVehicle_…
JustCris654 Jan 16, 2025
17a9d79
use Ratio refactor in tests
JustCris654 Jan 16, 2025
c941f62
Merge branch 'dev-2.x' into rentalVehicle_new_gbfs_fields
JustCris654 Jan 16, 2025
742bcd0
change distance value from meters to millimeters
JustCris654 Jan 21, 2025
c5a998d
factory method implementation for Distance
JustCris654 Jan 22, 2025
9e30029
expose Ratio and Distance from RentalVehicleFuel class
JustCris654 Jan 22, 2025
c00e615
Merge branch 'dev-2.x' into rentalVehicle_new_gbfs_fields
JustCris654 Jan 22, 2025
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 @@ -908,6 +909,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 @@ -935,6 +938,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,40 @@
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.
*/
@Nullable
public final Ratio percent;

/**
* Current fuel percentage, expressed from 0 to 1.
*/
@Nullable
public final Distance range;
JustCris654 marked this conversation as resolved.
Show resolved Hide resolved

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

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

@Nullable
public Integer getRange() {
return this.range != null ? this.range.toMeters() : null;
}
JustCris654 marked this conversation as resolved.
Show resolved Hide resolved
}
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,12 +9,16 @@ public class Distance {
private final double meters;

/** Returns a Distance object representing the given number of meters */
public Distance(double value) {
this.meters = value;
private Distance(double distanceInMeters) {
if (distanceInMeters < 0) {
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 {
return new Distance(value);
JustCris654 marked this conversation as resolved.
Show resolved Hide resolved
}

Expand All @@ -23,8 +28,8 @@ public static Distance ofKilometers(double value) {
}

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

Choose a reason for hiding this comment

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

Since we are always rounding this to int, should we just do the rounding when reading in the value and store it as int in this class since it requires less space? Do we have a need for less than meter precision from this class later?

Copy link
Contributor Author

@JustCris654 JustCris654 Jan 20, 2025

Choose a reason for hiding this comment

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

You're right, we don't need more precision than int. The reason I chose to use a Double was that the GBFS standard uses a float for it.

Copy link
Member

Choose a reason for hiding this comment

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

@t2gran suggested we should store the distance as millimeter integer. You can then convert it to int meters in this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this case aren't more suited centimeters or decimeters? I don't think we need a millimeter of precision.
With centimeter we could represent more kilometers (21.000 against 2.100) with integer and we would still have enough precision.


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

@Override
public int hashCode() {
return Objects.hash(meters);
}
JustCris654 marked this conversation as resolved.
Show resolved Hide resolved

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

import java.util.Optional;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import org.opentripplanner.utils.lang.DoubleUtils;

/**
* Represents a ratio within the range [0, 1].
* The class ensures that the ratio value, represented as a double,
* falls withing the specified range.
*/
public class Ratio {

private final double ratio;

private Ratio(double ratio) {
this.ratio = DoubleUtils.roundTo3Decimals(ratio);
}

/**
* This method is similar to {@link #of(double, Consumer)}, but throws an
* {@link IllegalArgumentException} if the ratio is not valid.
*/
public static Ratio of(double ratio) {
return of(
ratio,
errMsg -> {
throw new IllegalArgumentException(errMsg);
}
)
.orElseThrow();
}

public static Optional<Ratio> of(double ratio, Consumer<String> validationErrorHandler) {
if (ratio >= 0d && ratio <= 1d) {
return Optional.of(new Ratio(ratio));
} else {
validationErrorHandler.accept("Ratio must be in range [0,1], but was: " + ratio);
return Optional.empty();
}
}

public static Optional<Ratio> ofBoxed(
@Nullable Double ratio,
Consumer<String> validationErrorHandler
) {
if (ratio == null) {
return Optional.empty();
}
return of(ratio, validationErrorHandler);
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
var other = (Ratio) o;
return Double.compare(ratio, other.ratio) == 0;
}

@Override
public int hashCode() {
return Double.hashCode(ratio);
}

@Override
public String toString() {
return Double.toString(ratio);
}

public double asDouble() {
return ratio;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,23 @@
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.opentripplanner.utils.logging.Throttle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GbfsFreeVehicleStatusMapper {

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

private final VehicleRentalSystem system;

private final Map<String, RentalVehicleType> vehicleTypes;
Expand Down Expand Up @@ -52,7 +61,41 @@ public VehicleRentalVehicle mapFreeVehicleStatus(GBFSBike vehicle) {
vehicle.getLastReported() != null
? Instant.ofEpochSecond((long) (double) vehicle.getLastReported())
: null;
rentalVehicle.currentRangeMeters = vehicle.getCurrentRangeMeters();

var fuelRatio = Ratio
.ofBoxed(
vehicle.getCurrentFuelPercent(),
validationErrorMessage ->
FUEL_PERCENT_LOG_THROTTLE.throttle(() ->
LOG.warn("'currentFuelPercent' is not valid. Details: " + validationErrorMessage)
)
)
.orElse(null);

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.

Copy link
Member

Choose a reason for hiding this comment

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

I spoke to @hbruch about this and he said that there are a number of feeds that don't include it where they should but he is in favour of enforcing the spec anyway. If we are not strict with data producers, they will never learn.

vehicleTypes.get(vehicle.getVehicleTypeId()).propulsionType !=
RentalVehicleType.PropulsionType.HUMAN &&
rangeMeters == null
) {
return null;
JustCris654 marked this conversation as resolved.
Show resolved Hide resolved
}
rentalVehicle.fuel = new RentalVehicleFuel(fuelRatio, 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 @@ -1889,6 +1889,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 @@ -1918,6 +1920,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
Loading
Loading