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 option to include stations in nearest search #5390

Merged
merged 34 commits into from Dec 19, 2023

Conversation

nurmAV
Copy link
Contributor

@nurmAV nurmAV commented Oct 3, 2023

Summary

Adds an option to include both stops and stations when searching for stops. If both are included, only the parent station of a stop is returned if one exists.

Unit tests

The code was tested manually

@nurmAV nurmAV requested a review from a team as a code owner October 3, 2023 13:30
@codecov
Copy link

codecov bot commented Oct 3, 2023

Codecov Report

Attention: 11 lines in your changes are missing coverage. Please review.

Comparison is base (3163631) 67.36% compared to head (fd1973d) 67.45%.
Report is 2 commits behind head on dev-2.x.

Files Patch % Lines
...ntripplanner/apis/gtfs/generated/GraphQLTypes.java 40.00% 3 Missing ⚠️
...outing/graphfinder/PlaceFinderTraverseVisitor.java 89.65% 1 Missing and 2 partials ⚠️
...pplanner/apis/gtfs/datafetchers/QueryTypeImpl.java 71.42% 1 Missing and 1 partial ⚠️
...va/org/opentripplanner/apis/gtfs/GraphQLUtils.java 0.00% 1 Missing ⚠️
.../gtfs/datafetchers/PlaceInterfaceTypeResolver.java 0.00% 0 Missing and 1 partial ⚠️
...anner/apis/transmodel/TransmodelGraphQLSchema.java 0.00% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             dev-2.x    #5390      +/-   ##
=============================================
+ Coverage      67.36%   67.45%   +0.08%     
- Complexity     16167    16193      +26     
=============================================
  Files           1858     1858              
  Lines          71093    71126      +33     
  Branches        7403     7412       +9     
=============================================
+ Hits           47893    47976      +83     
+ Misses         20743    20678      -65     
- Partials        2457     2472      +15     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Comment on lines 53 to 55
if (o instanceof Station) {
return schema.getObjectType("Stop");
}
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 the the if statement above this can be expanded to check for Station also and this block can be removed.

Copy link
Member

Choose a reason for hiding this comment

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

Run the optimize imports thing on intellij to get rid of extra imports here.

Copy link
Member

Choose a reason for hiding this comment

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

Same here

placesFound.add(new PlaceAtDistance(stop, distance));
seenStops.add(stop.getId());
if (
includeStopsAndStations &&
Copy link
Member

Choose a reason for hiding this comment

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

This code is now inside the includeStops block. I think we should separate these so includeStops doesn't need to be and probably shouldn't be true when includeStopsAndStations is true (at least on the GraphQL argument level). I'm not quite sure what should happen if someone has defined both filters as true, should we include the stations in addition to their stops or should we just throw some exception that this combination of filters is not allowed.

includeStopsAndStations &&
stop.getParentStation() != null &&
!seenStops.contains(stop.getParentStation().getId()) &&
!seenStops.contains(stop.getId())
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this check unnecessary because if the stop has a parent station, we add the parent stations id to the seenStops, not the stop's.

Comment on lines 190 to 197
} else {
// Do not accumulate the same station many times
if (
stop.getParentStation() != null && seenStops.contains(stop.getParentStation().getId())
) return;
seenStops.add(stop.getId());
placesFound.add(new PlaceAtDistance(stop, 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 took a while for me to understand this logic. I think it would be simpler if we first check if a stop has a parent station or not and then inside those blocks do other checks etc.

@@ -986,6 +986,10 @@ enum FilterPlaceType {

"""Parking lots that contain spaces for cars"""
CAR_PARK


"""Stops or stations"""
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 we should add more documentation here what this actually does and also it's relation to the STOP filter (and vice versa on the STOP filter).

Copy link
Member

Choose a reason for hiding this comment

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

I would vote to rename this to STATION and the following semantics:

  • STATION: only return stations, not stops. if a single stop is within radius then its station is also.
  • STOP: only return stops, not stations
  • STATION,STOP: return all stations but only those stops that don't have a parent station. if a single stop is within radius then its parent station is also.

Copy link
Member

Choose a reason for hiding this comment

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

Ok I think agree with you, lets do it like that. We need to document this behaviour well here in the schema.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, good documentation is important because it's going to be confusing no matter what the semantics will be.

@@ -80,6 +81,8 @@ public PlaceFinderTraverseVisitor(
includeVehicleRentals = shouldInclude(filterByPlaceTypes, PlaceType.VEHICLE_RENT);
includeCarParking = shouldInclude(filterByPlaceTypes, PlaceType.CAR_PARK);
includeBikeParking = shouldInclude(filterByPlaceTypes, PlaceType.BIKE_PARK);
includeStopsAndStations = shouldInclude(filterByPlaceTypes, PlaceType.STOP_OR_STATION);
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 now this is true by default if no filters are defined. However, I think we maybe shouldn't have this true by default as the client software might have existing handling for when Stop type objects are returned which might break as now this sometimes would return stations which also use Stop type but have some crucial fields as null. Also again, now by default we have both STOP and STOP_OR_STATION on by default and it's a bit unclear what it means.

// Do not accumulate the same station many times
if (
stop.getParentStation() != null && seenStops.contains(stop.getParentStation().getId())
) return;
Copy link
Member

Choose a reason for hiding this comment

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

Try to always wrap things inside an if in a {} block and preferable also split that block to a new line as it's easy to miss something happening on the same line where there is an if statement.

@optionsome
Copy link
Member

I wonder if we already have some existing tests for this part of the code and how difficult it would be to write a test for the GraphQL API. @leonardehrenfried probably has a better idea.

@leonardehrenfried
Copy link
Member

leonardehrenfried commented Oct 3, 2023

I wonder if we already have some existing tests for this part of the code and how difficult it would be to write a test for the GraphQL API. @leonardehrenfried probably has a better idea.

There are tests for other visitors which you can use as a template:

  • MaxCountSkipEdgeStrategyTest
  • StopFinderTraverseVisitorTest

You will likely have to add methods to TestStateBuilder.

@leonardehrenfried leonardehrenfried changed the title Add option to include stations in stop search Add option to include stations in nearby search Oct 10, 2023
@leonardehrenfried leonardehrenfried changed the title Add option to include stations in nearby search Add option to include stations in nearby search Oct 10, 2023
@t2gran t2gran added this to the 2.5 (next release) milestone Oct 10, 2023
@leonardehrenfried
Copy link
Member

@miles-grant-ibigroup @daniel-heppner-ibigroup @binh-dam-ibigroup You should probably know that this change is coming.

@leonardehrenfried leonardehrenfried changed the title Add option to include stations in nearby search Add option to include stations in nearest search Oct 10, 2023
@miles-grant-ibigroup
Copy link
Contributor

This can be disabled right? Or like not included in the query

@leonardehrenfried
Copy link
Member

If you actively select what kind of items you want, nothing will change.

I don't think we haven't quite decided if the stations will be included by default or not.

@daniel-heppner-ibigroup
Copy link
Contributor

If the default changes to include stations but we aren't looking for the station type on the nearest response, I think that means we could be missing stops with a parent station, correct? Our options would be to either support stations or set the default to only include stops, it seems. It isn't a big deal to update that, especially since the nearby view isn't merged yet on our end.

@leonardehrenfried
Copy link
Member

If the default changes to include stations but we aren't looking for the station type on the nearest response, I think that means we could be missing stops with a parent station, correct?

Yes, it will start returning Station from which you have to fetch the child stops.

The safest option for you would be to explicitly select what you want and then nothing will change.

@optionsome
Copy link
Member

I don't think we should return stations by default, it's not the most completely obvious selection and it has some backwards compatibility issues as stations and stops use the same type but don't contain the same information.

@@ -80,6 +81,8 @@ public PlaceFinderTraverseVisitor(
includeVehicleRentals = shouldInclude(filterByPlaceTypes, PlaceType.VEHICLE_RENT);
includeCarParking = shouldInclude(filterByPlaceTypes, PlaceType.CAR_PARK);
includeBikeParking = shouldInclude(filterByPlaceTypes, PlaceType.BIKE_PARK);
includeStations = shouldInclude(filterByPlaceTypes, PlaceType.STATION);
Copy link
Member

Choose a reason for hiding this comment

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

We need special handling for this as we don't want to return stations by default.

This fact needs to also documented in the query docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there some better way to do so than to simply add a check for whether filterByPlaceTypes is empty? Where exactly are the query docs that should be changed?

Copy link
Member

Choose a reason for hiding this comment

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

No, that's fine.

The docs to mentioned this are here:

"""
Only return places that are one of these types, e.g. `STOP` or `VEHICLE_RENT`
"""
filterByPlaceTypes: [FilterPlaceType]

Copy link
Member

Choose a reason for hiding this comment

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

Please also add a test for this slightly strange behaviour.

Copy link
Member

Choose a reason for hiding this comment

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

Please make sure that stations are only included if you explicitly enable them.

Co-authored-by: Leonard Ehrenfried <mail@leonard.io>
@leonardehrenfried
Copy link
Member

Can you please also write a test for the GraphQL aspect?

You will have to write a query like this: https://github.com/opentripplanner/OpenTripPlanner/blob/621b802d93a2eb595b645b77be21a36da7314842/src/test/resources/org/opentripplanner/apis/gtfs/queries/patterns.graphql#L1-L0 but for the nearest query.

You then need to run GraphQLIntegrationTest.

In order to get useful results for the query, you might have to write a test implementation of that interface:

Copy link
Member

@optionsome optionsome left a comment

Choose a reason for hiding this comment

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

The stop ids filtering doesn't seem to work. Also, while testing this, I realized that the filtering is broken(?) also upstream. In OTP1 the nearest search only returned the entities that were included in the filters. In OTP2, the nearest search returns only the entities of a type that had some id filter if they were included in it but also additionally all entities of different types if there were no filters for those. Based on the documentation and how it was OTP1, I think we should only return the elements that were included in id filters if any id filters are specified for any type. I think that behaviour should be fixed within the scope of this pr to match the OTP1 behaviour.

Comment on lines 210 to 211
!seenStops.contains(stop.getParentStation().getId()) &&
stopIsIncludedByStationFilter(stop)
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 these two checks as they are already checked above?

placesFound.add(new PlaceAtDistance(stop, distance));
seenStops.add(stop.getParentStation().getId());
placesFound.add(new PlaceAtDistance(stop.getParentStation(), distance));
} else if (includeStops && !seenStops.contains(stop.getId())) {
Copy link
Member

Choose a reason for hiding this comment

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

Same with this seenStops check.

Copy link
Member

Choose a reason for hiding this comment

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

This file is in the old resource path before the api was moved into the core code. Is this file used in tests anymore? I saw there was another test file for this query that was slightly different.

Copy link
Member

Choose a reason for hiding this comment

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

You could potentially have this is a separate test or merge this and the other test so that both DEPARTURE_ROW and STOP are returned.

Copy link
Member

Choose a reason for hiding this comment

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

The whitespace usage in this file is slightly inconsistent, there is whitespace before the last } for example.

Comment on lines 200 to 202
(includeStations && !stop.isPartOfStation() && !stopIsIncludedByStopFilter(stop)) ||
(!includeStations && !stopIsIncludedByStopFilter(stop)) ||
(stop.isPartOfStation() && !stopIsIncludedByStationFilter(stop)) ||
Copy link
Member

Choose a reason for hiding this comment

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

This part is a bit hard to follow but I don't know what to do about it.

Copy link
Member

Choose a reason for hiding this comment

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

I would put each part in between || into a separate method which has a name and possibly a bit of Javadoc.

This doesn't make the logic less complicated but helps someone else understand what each piece does.

Comment on lines 217 to 223
} else if (includeStations && stop.getParentStation() != null) {
seenStops.add(stop.getParentStation().getId());
placesFound.add(new PlaceAtDistance(stop.getParentStation(), distance));
} else if (includeStops) {
seenStops.add(stop.getId());
placesFound.add(new PlaceAtDistance(stop, distance));
}
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it enough to have just these two checks and remove the first if? If I'm not missing anything, I think this also covers the case where both includeStops and includeStations is true.

Co-authored-by: Leonard Ehrenfried <mail@leonard.io>
Copy link
Member

@leonardehrenfried leonardehrenfried left a comment

Choose a reason for hiding this comment

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

We are getting close - just the refactoring of the complex if statement.

@optionsome
Copy link
Member

While testing that the transmodel API still works, I noticed there is a multiModalMode argument in the transmodel API's nearest query. What does that do @t2gran . Is it similar to what we are trying to do in the scope of this pr? The search also outputs StopPlace, is that in practice the same thing as a station?

@optionsome
Copy link
Member

Also, should we also mark the transmodel APIs filterByIds as deprecated?

@optionsome
Copy link
Member

I tested this a bit more and checked the transmodel API code. Transmodel API returns both the quay (stop in gtfs terminology) that belongs to a StopPlace (sort of the same thing as station in gtfs) and the StopPlace itself. This differs from the behaviour we designed for the GTFS API in this pr.

The code that makes the transmodel API return the StopPlaces resides in the transmodel API code and it modifies the results returned by the method that is used by the GTFS API also.

It might make sense to remove the transmodel API specific code and reuse the logic in the shared core code but I guess we need to keep the transmodel API behaviour as is and it would complicate the core code even more. I don't really want to put the burden of refactoring this to @nurmAV within the scope of this pr.

@optionsome optionsome merged commit 5ed8b70 into opentripplanner:dev-2.x Dec 19, 2023
5 checks passed
@optionsome optionsome deleted the DT-5799 branch December 19, 2023 11:51
t2gran pushed a commit that referenced this pull request Dec 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants