Skip to content

Commit

Permalink
Merge branch 'consolidated-stops' into shared-stops
Browse files Browse the repository at this point in the history
  • Loading branch information
leonardehrenfried committed Nov 14, 2023
2 parents 2d43aea + 49eea23 commit fdacab6
Show file tree
Hide file tree
Showing 67 changed files with 1,333 additions and 111 deletions.
56 changes: 56 additions & 0 deletions doc-templates/StopConsolidation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!--
NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc.
- Template directory is: /doc-templates
- Generated directory is: /docs
-->
# Stop consolidation

## Contact Info

- [Jon Campbell](mailto:[email protected]), Arcadis, USA

## Feature explanation

This sandbox feature allows you to "combine" equivalent stops from across several feeds into a single,
consolidated one.

It is achieved by defining a "primary" stop and one or more "secondary" stops. During the graph
build all trip patterns are modified so that the secondary ones are swapped out for their
primary equivalent.

## Effects

This has the following consequences

- When you query the departures for a primary stop you see a consolidated view of all the equivalent departures.
- When transferring at a consolidated stop you no longer get instructions like "walk 5 meters to stop X"

!!! warning "Downsides"

However, this feature has also severe downsides:

- It makes realtime trip updates referencing a stop id much more complicated and in many cases
impossible to resolve.
You can only reference a stop by its sequence, which only works in GTFS-RT, not Siri.
- Fare calculation and transfers are unlikely to work as expected.


## Configuration

To enable this feature you need to add a file to OTP's working directory and configure
its name like this:

<!-- INSERT: config -->

The additional config file must look like the following:

<!-- INSERT: file -->

The column names mean the following:

- `stop_group_id`: id to group several rows in the file together
- `feed_id`: feed id of the stop
- `stop_id`: id of the stop
- `is_primary`: whether the row represents a primary stop, `1` means yes and `0` means no

2 changes: 2 additions & 0 deletions docs/BuildConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Sections follow that describe particular settings in more depth.
| [readCachedElevations](#readCachedElevations) | `boolean` | Whether to read cached elevation data. | *Optional* | `true` | 2.0 |
| staticBikeParkAndRide | `boolean` | Whether we should create bike P+R stations from OSM data. | *Optional* | `false` | 1.5 |
| staticParkAndRide | `boolean` | Whether we should create car P+R stations from OSM data. | *Optional* | `true` | 1.5 |
| stopConsolidationFile | `string` | Name of the CSV-formatted file in the build directory which contains the configuration for stop consolidation. | *Optional* | | 2.5 |
| [streetGraph](#streetGraph) | `uri` | URI to the street graph object file for reading and writing. | *Optional* | | 2.0 |
| [subwayAccessTime](#subwayAccessTime) | `double` | Minutes necessary to reach stops served by trips on routes of route_type=1 (subway) from the street. | *Optional* | `2.0` | 1.5 |
| [transitModelTimeZone](#transitModelTimeZone) | `time-zone` | Time zone for the graph. | *Optional* | | 2.2 |
Expand Down Expand Up @@ -1165,6 +1166,7 @@ case where this is not the case.
}
}
],
"stopConsolidationFile" : "consolidated-stops.csv",
"emissions" : {
"carAvgCo2PerKm" : 170,
"carAvgOccupancy" : 1.3
Expand Down
5 changes: 5 additions & 0 deletions docs/examples/ibi/seattle/build-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"transitModelTimeZone": "America/Los_Angeles",
"fares": "orca",
"stopConsolidationFile": "consolidated-stops.csv"
}
78 changes: 78 additions & 0 deletions docs/sandbox/StopConsolidation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<!--
NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc.
- Template directory is: /doc-templates
- Generated directory is: /docs
-->
# Stop consolidation

## Contact Info

- [Jon Campbell](mailto:[email protected]), Arcadis, USA

## Feature explanation

This sandbox feature allows you to "combine" equivalent stops from across several feeds into a single,
consolidated one.

It is achieved by defining a "primary" stop and one or more "secondary" stops. During the graph
build all trip patterns are modified so that the secondary ones are swapped out for their
primary equivalent.

## Effects

This has the following consequences

- When you query the departures for a primary stop you see a consolidated view of all the equivalent departures.
- When transferring at a consolidated stop you no longer get instructions like "walk 5 meters to stop X"

!!! warning "Downsides"

However, this feature has also severe downsides:

- It makes realtime trip updates referencing a stop id much more complicated and in many cases
impossible to resolve.
You can only reference a stop by its sequence, which only works in GTFS-RT, not Siri.
- Fare calculation and transfers are unlikely to work as expected.


## Configuration

To enable this feature you need to add a file to OTP's working directory and configure
its name like this:

<!-- config BEGIN -->
<!-- NOTE! This section is auto-generated. Do not change, change doc in code instead. -->

```JSON
// build-config.json
{
"stopConsolidationFile" : "consolidated-stops.csv"
}
```

<!-- config END -->

The additional config file must look like the following:

<!-- file BEGIN -->
<!-- NOTE! This section is auto-generated. Do not change, change doc in code instead. -->

```
stop_group_id,feed_id,stop_id,is_primary
1,pierce,1705867009,0
1,kcm,10225,1
2,pierce,1569,0
2,commtrans,473,0
2,kcm,1040,1
```

<!-- file END -->

The column names mean the following:

- `stop_group_id`: id to group several rows in the file together
- `feed_id`: feed id of the stop
- `stop_id`: id of the stop
- `is_primary`: whether the row represents a primary stop, `1` means yes and `0` means no

2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ markdown_extensions:
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences
- admonition

# MkDocs will automatically discover pages if you don't list them here.
# In that case subdirectories can be used to organize pages.
Expand Down Expand Up @@ -112,3 +113,4 @@ nav:
- Fares: 'sandbox/Fares.md'
- Ride Hailing: 'sandbox/RideHailing.md'
- Emissions: 'sandbox/Emissions.md'
- Stop Consolidation: 'sandbox/StopConsolidation.md'
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.opentripplanner.ext.stopconsolidation;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.STOP_C;
import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.STOP_D;
import static org.opentripplanner.model.plan.PlanTestConstants.T11_05;
import static org.opentripplanner.model.plan.PlanTestConstants.T11_12;

import java.util.List;
import org.junit.jupiter.api.Test;
import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationRepository;
import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationService;
import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup;
import org.opentripplanner.model.plan.Place;
import org.opentripplanner.model.plan.PlanTestConstants;
import org.opentripplanner.model.plan.TestItineraryBuilder;

class ConsolidatedStopNameFilterTest {

@Test
void changeNames() {
var transitModel = TestStopConsolidationModel.buildTransitModel();

var groups = List.of(new ConsolidatedStopGroup(STOP_C.getId(), List.of(STOP_D.getId())));
var repo = new DefaultStopConsolidationRepository();
repo.addGroups(groups);

var service = new DefaultStopConsolidationService(repo, transitModel);
var filter = new ConsolidatedStopNameFilter(service);

var itinerary = TestItineraryBuilder
.newItinerary(Place.forStop(STOP_C))
.bus(TestStopConsolidationModel.ROUTE, 1, T11_05, T11_12, Place.forStop(STOP_C))
.bus(1, T11_05, T11_12, PlanTestConstants.E)
.bus(1, T11_05, T11_12, PlanTestConstants.F)
.build();

var filtered = filter.filter(List.of(itinerary));
assertFalse(filtered.isEmpty());

var updatedLeg = filtered.get(0).getLegs().get(0);
assertEquals(STOP_D.getName(), updatedLeg.getFrom().name);
assertEquals(STOP_D.getName(), updatedLeg.getTo().name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.opentripplanner.ext.stopconsolidation;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.PATTERN;
import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.STOP_A;
import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.STOP_B;
import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.STOP_C;
import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.STOP_D;

import java.util.List;
import org.junit.jupiter.api.Test;
import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationRepository;
import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup;
import org.opentripplanner.transit.model.network.TripPattern;

class StopConsolidationModuleTest {

@Test
void replacePatterns() {
var groups = List.of(new ConsolidatedStopGroup(STOP_D.getId(), List.of(STOP_B.getId())));

var transitModel = TestStopConsolidationModel.buildTransitModel();
transitModel.addTripPattern(PATTERN.getId(), PATTERN);
var repo = new DefaultStopConsolidationRepository();
var module = new StopConsolidationModule(transitModel, repo, groups);
module.buildGraph();

var modifiedPattern = transitModel.getTripPatternForId(PATTERN.getId());
assertFalse(modifiedPattern.getRoutingTripPattern().getPattern().sameAs(PATTERN));
assertFalse(modifiedPattern.sameAs(PATTERN));

var modifiedStop = modifiedPattern
.getRoutingTripPattern()
.getPattern()
.getStopPattern()
.getStop(1);
assertEquals(modifiedStop, STOP_D);

var patterns = List.copyOf(transitModel.getAllTripPatterns());

var stops = patterns.stream().map(TripPattern::getStops).toList();
assertEquals(List.of(List.of(STOP_A, STOP_D, STOP_C)), stops);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.opentripplanner.ext.stopconsolidation;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.IOException;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.opentripplanner.test.support.ResourceLoader;
import org.opentripplanner.transit.model.framework.FeedScopedId;

class StopConsolidationParserTest {

@Test
void parse() {
try (var file = ResourceLoader.of(this).inputStream("consolidated-stops.csv")) {
var groups = StopConsolidationParser.parseGroups(file);
assertEquals(20, groups.size());

var first = groups.get(0);
assertEquals("kcm:10225", first.primary().toString());
assertEquals(
List.of("pierce:1705867009"),
first.secondaries().stream().map(FeedScopedId::toString).toList()
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.opentripplanner.ext.stopconsolidation;

import static org.opentripplanner.transit.model._data.TransitModelForTest.id;

import java.util.List;
import org.opentripplanner.transit.model._data.TransitModelForTest;
import org.opentripplanner.transit.model.framework.Deduplicator;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.network.Route;
import org.opentripplanner.transit.model.network.StopPattern;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.organization.Agency;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.service.TransitModel;

class TestStopConsolidationModel {

private static final TransitModelForTest testModel = TransitModelForTest.of();
public static final RegularStop STOP_A = testModel.stop("A").withCoordinate(1, 1).build();
public static final RegularStop STOP_B = testModel.stop("B").withCoordinate(1.1, 1.1).build();
public static final RegularStop STOP_C = testModel.stop("C").withCoordinate(1.2, 1.2).build();
public static final StopPattern STOP_PATTERN = TransitModelForTest.stopPattern(
STOP_A,
STOP_B,
STOP_C
);
static final String SECONDARY_FEED_ID = "secondary";
static final Agency AGENCY = TransitModelForTest
.agency("agency")
.copy()
.withId(new FeedScopedId(SECONDARY_FEED_ID, "agency"))
.build();
static final Route ROUTE = TransitModelForTest
.route(new FeedScopedId(SECONDARY_FEED_ID, "route-33"))
.withAgency(AGENCY)
.build();
static final RegularStop STOP_D = testModel
.stop("D")
.withId(new FeedScopedId(SECONDARY_FEED_ID, "secondary-stop-D"))
.build();

static final TripPattern PATTERN = TripPattern
.of(id("123"))
.withRoute(ROUTE)
.withStopPattern(STOP_PATTERN)
.build();

static TransitModel buildTransitModel() {
var stopModelBuilder = testModel.stopModelBuilder();
List.of(STOP_A, STOP_B, STOP_C, STOP_D).forEach(stopModelBuilder::withRegularStop);
return new TransitModel(stopModelBuilder.build(), new Deduplicator());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.opentripplanner.ext.stopconsolidation.internal;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.opentripplanner.transit.model._data.TransitModelForTest.id;

import java.util.List;
import org.junit.jupiter.api.Test;
import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup;

class DefaultStopConsolidationRepositoryTest {

private static final ConsolidatedStopGroup GROUP = new ConsolidatedStopGroup(
id("123"),
List.of(id("456"))
);

@Test
void add() {
var subject = new DefaultStopConsolidationRepository();
assertEquals(List.of(), subject.groups());
subject.addGroups(List.of(GROUP));

assertEquals(List.of(GROUP), subject.groups());
}

@Test
void groupsAreImmutable() {
var subject = new DefaultStopConsolidationRepository();
assertThrows(Exception.class, () -> subject.groups().add(GROUP));
}
}
Loading

0 comments on commit fdacab6

Please sign in to comment.