forked from HSLdevcom/OpenTripPlanner
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add McMinimumNumberItineraryFilter - a multi-criteria version of the …
…MinNumIttnFilter
- Loading branch information
Showing
9 changed files
with
662 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
...in/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmin/Group.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmin; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
|
||
/** | ||
* The purpose of a group is to maintain a list of items, all optimal for a single | ||
* criteria/comparator. After the group is created, then the criteria is no longer needed, so we do | ||
* not keep a reference to the original criteria. | ||
*/ | ||
class Group implements Iterable<Item> { | ||
|
||
private final List<Item> items = new ArrayList<>(); | ||
|
||
public Group(Item firstItem) { | ||
add(firstItem); | ||
} | ||
|
||
Item first() { | ||
return items.getFirst(); | ||
} | ||
|
||
boolean isEmpty() { | ||
return items.isEmpty(); | ||
} | ||
|
||
|
||
boolean isSingleItemGroup() { | ||
return items.size() == 1; | ||
} | ||
|
||
void add(Item item) { | ||
item.incGroupCount(); | ||
items.add(item); | ||
} | ||
|
||
void removeAllItems() { | ||
items.forEach(Item::decGroupCount); | ||
items.clear(); | ||
} | ||
|
||
void addNewDominantItem(Item item) { | ||
removeAllItems(); | ||
add(item); | ||
} | ||
|
||
boolean contains(Item item) { | ||
return this.items.contains(item); | ||
} | ||
|
||
@Override | ||
public Iterator<Item> iterator() { | ||
return items.iterator(); | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
...ain/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmin/Item.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmin; | ||
|
||
import org.opentripplanner.model.plan.Itinerary; | ||
|
||
/** | ||
* An item is a decorated itinerary. The extra information added is the index in the input list | ||
* (sort order) and a groupCount. The sort order is used to break ties, while the group-count is | ||
* used to select the itinerary witch exist in the highest number of groups. The group dynamically | ||
* updates the group-count; The count is incremented when an item is added to a group, and | ||
* decremented when the group is removed from the State. | ||
*/ | ||
class Item { | ||
|
||
private final Itinerary item; | ||
private final int index; | ||
private int groupCount = 0; | ||
|
||
Item(Itinerary item, int index) { | ||
this.item = item; | ||
this.index = index; | ||
} | ||
|
||
/** | ||
* An item is better than another if the groupCount is higher, and in case of a tie, if the sort | ||
* index is lower. | ||
*/ | ||
public boolean betterThan(Item o) { | ||
return groupCount != o.groupCount ? groupCount > o.groupCount : index < o.index; | ||
} | ||
|
||
Itinerary item() { | ||
return item; | ||
} | ||
|
||
void incGroupCount() { | ||
++this.groupCount; | ||
} | ||
|
||
void decGroupCount() { | ||
--this.groupCount; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return "Item #%d {count:%d, %s}".formatted(index, groupCount, item.toStr()); | ||
} | ||
} |
102 changes: 102 additions & 0 deletions
102
...er/routing/algorithm/filterchain/filters/system/mcmin/McMinimumNumberItineraryFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmin; | ||
|
||
import java.util.List; | ||
import java.util.function.Predicate; | ||
import org.opentripplanner.model.plan.Itinerary; | ||
import org.opentripplanner.routing.algorithm.filterchain.filters.system.SingeCriteriaComparator; | ||
import org.opentripplanner.routing.algorithm.filterchain.framework.spi.RemoveItineraryFlagger; | ||
|
||
/** | ||
* This filter is used to reduce a set of itineraries down to the specified limit, if possible. | ||
* The filter is guaranteed to keep at least the given {@code minNumItineraries} and also | ||
* the best itinerary for each criterion. The criterion is defined using the list of | ||
* {@code comparators}. | ||
* <p> | ||
* The main usage of this filter is to combine it with a grouping filter and for each group | ||
* make sure there is at least {@code minNumItineraries} and that the best itinerary with respect | ||
* to each criterion is kept. So, if the grouping is based on time and riding common trips, then | ||
* this filter will use the reminding criterion (transfers, generalized-cost, | ||
* [transit-group-priority]) to filter the grouped set of itineraries. | ||
* <p> | ||
* <b>IMPLEMENTATION DETAILS</b> | ||
* <p> | ||
* This is not a trivial problem. In most cases, the best itinerary for a given criteria is unique, | ||
* but there might be ties - same number of transfers, same cost, and/or different priority groups. | ||
* In case of a tie, we will look if an itinerary is "best-in-group" for more than one criterion, | ||
* if so we pick the one witch is best in the highest number of groups. Again, if there is a tie | ||
* (best in the same number of groups), then we fall back to the given itinerary sorting order. | ||
* <p> | ||
* This filter will use the order of the input itineraries to break ties. So, make sure to call the | ||
* appropriate sort function before this filter is invoked. | ||
* <p> | ||
* Note! For a criteria like num-of-transfers or generalized-cost, there is only one set of "best" | ||
* itineraries, and usually there are only one or a few itineraries. In case there is more than one, | ||
* picking just one is fine. But, for transit-group-priority there might be more than on set of | ||
* itineraries. For each set, we need to pick one itinerary for the final result. Each of these | ||
* sets may or may not have more than one itinerary. | ||
* <p> | ||
* Let's discuss an example: | ||
* <pre> | ||
* minNumItineraries = 4 | ||
* comparators = [ generalized-cost, min-num-transfers, transit-group-priority ] | ||
* itineraries: [ | ||
* #0 : [ 1000, 2, (a) ] | ||
* #1 : [ 1000, 3, (a,b) ] | ||
* #2 : [ 1000, 3, (b) ] | ||
* #3 : [ 1200, 1, (a,b) ] | ||
* #4 : [ 1200, 1, (a) ] | ||
* #5 : [ 1300, 2, (c) ] | ||
* #6 : [ 1300, 3, (c) ] | ||
* ] | ||
* </pre> | ||
* The best itineraries by generalized-cost are (#0, #1, #2). The best itineraries by | ||
* min-num-transfers are (#3, #4). The best itineraries by transit-group-priority are | ||
* (a:(#0, #4), b:(#2), c:(#5, #6)). | ||
* <p> | ||
* So we need to pick one from each group (#0, #1, #2), (#3, #4), (#0, #4), (#2), and (#5, #6). | ||
* Since #2 is a single, we pick it first. Itinerary #2 is also one of the best | ||
* generalized-cost itineraries - so we are done with generalized-cost itineraries as well. The two | ||
* groups left are (#3, #4), (#0, #4), and (#5, #6). #4 exists in 2 groups, so we pick it next. Now | ||
* we are left with (#5, #6). To break the tie, we look at the sort-order. We pick | ||
* itinerary #5. Result: #2, #4, and #5. | ||
* <p> | ||
* The `minNumItineraries` limit is not met, so we need to pick another itinerary, we use the | ||
* sort-order again and add itinerary #0. The result returned is: [#0, #2, #4, #5] | ||
*/ | ||
public class McMinimumNumberItineraryFilter implements RemoveItineraryFlagger { | ||
|
||
private final String name; | ||
private final int minNumItineraries; | ||
private final List<SingeCriteriaComparator> comparators; | ||
|
||
public McMinimumNumberItineraryFilter( | ||
String name, | ||
int minNumItineraries, | ||
List<SingeCriteriaComparator> comparators | ||
) { | ||
this.name = name; | ||
this.minNumItineraries = minNumItineraries; | ||
this.comparators = comparators; | ||
} | ||
|
||
@Override | ||
public String name() { | ||
return name; | ||
} | ||
|
||
@Override | ||
public List<Itinerary> flagForRemoval(List<Itinerary> itineraries) { | ||
if (itineraries.size() <= minNumItineraries) { | ||
return List.of(); | ||
} | ||
var state = new State(itineraries, comparators); | ||
state.findAllSingleItemGroupsAndAddTheItemToTheResult(); | ||
state.findTheBestItemsUntilAllGroupsAreRepresentedInTheResult(); | ||
state.fillUpTheResultWithMinimumNumberOfItineraries(minNumItineraries); | ||
|
||
// We now have the itineraries we want, but we must invert this and return the | ||
// list of itineraries to drop - keeping the original order | ||
var ok = state.getResult(); | ||
return itineraries.stream().filter(Predicate.not(ok::contains)).toList(); | ||
} | ||
} |
Oops, something went wrong.