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 26 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
26 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
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,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;

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;
}
}
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);
}

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);
}

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

@Override
public int hashCode() {
return Objects.hash(meters);
}

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

import java.util.Objects;

/**
* 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;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
private final Double ratio;
private final double ratio;


public Ratio(Double ratio) {
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) {
Copy link
Member

Choose a reason for hiding this comment

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

Do not use 'instanceof' in equals methods, follw the same practise as other value-objects.

return ratio.ratio == this.ratio;
} else {
return false;
}
}

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

Choose a reason for hiding this comment

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

You should use the Double not Objects hash function here.

}

@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.

Copy link
Member

Choose a reason for hiding this comment

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

We discussed this today in the dev meeting and decided that the to string should just return the double as string, not a percent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, so it's fine as it is

}

public Double asDouble() {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public Double asDouble() {
public double asDouble() {

return ratio;
}
}
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,42 @@ public VehicleRentalVehicle mapFreeVehicleStatus(GBFSBike vehicle) {
vehicle.getLastReported() != null
? Instant.ofEpochSecond((long) (double) vehicle.getLastReported())
: null;
rentalVehicle.currentRangeMeters = vehicle.getCurrentRangeMeters();
Ratio fuelPercent = null;
try {
if (vehicle.getCurrentFuelPercent() != null) {
fuelPercent = new Ratio(vehicle.getCurrentFuelPercent());
}
} catch (IllegalArgumentException e) {
LOG.warn(
"Current fuel percent value not valid: {} - {}",
vehicle.getCurrentFuelPercent(),
e.getMessage()
);
}
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;
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.

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 the check to be compliant to the GBFS standard and I think it's better this way in the future but I see that it could be an issue if feeds get this wrong. I can remove the check if you say it's for the best, I don't think it would break anything.
Maybe I can add just a warning and return the object?

}
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