Skip to content

Commit dc2af3a

Browse files
committed
feature: Add support for Emission data on trip legs
1 parent afb7501 commit dc2af3a

31 files changed

+916
-13
lines changed

application/src/ext-test/java/org/opentripplanner/ext/emission/internal/csvdata/route/RouteDataReaderTest.java

+6
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,10 @@ void handleMissingDdataSource() {
5757
var emissions = subject.read(emissionMissingFile(), FEED_ID);
5858
assertTrue(emissions.isEmpty());
5959
}
60+
61+
@Test
62+
void ignoreDataSourceIfHeadersDoesNotMatch() {
63+
var emissions = subject.read(emissionOnTripLegs(), FEED_ID);
64+
assertTrue(emissions.isEmpty());
65+
}
6066
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package org.opentripplanner.ext.emission.internal.csvdata.trip;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import java.util.List;
9+
import org.junit.jupiter.api.Test;
10+
import org.opentripplanner.framework.model.Gram;
11+
import org.opentripplanner.transit.model._data.TimetableRepositoryForTest;
12+
import org.opentripplanner.transit.model.framework.FeedScopedId;
13+
import org.opentripplanner.transit.model.site.StopLocation;
14+
15+
class EmissionAggregatorTest {
16+
17+
private static final String FEED_ID = "E";
18+
19+
private static final StopLocation STOP_A;
20+
private static final StopLocation STOP_B;
21+
private static final StopLocation STOP_C;
22+
private static final StopLocation STOP_D;
23+
24+
private static final String STOP_A_ID;
25+
private static final String STOP_B_ID;
26+
private static final String STOP_C_ID;
27+
28+
static {
29+
var builder = TimetableRepositoryForTest.of();
30+
STOP_A = builder.stop("A").build();
31+
STOP_B = builder.stop("B").build();
32+
STOP_C = builder.stop("C").build();
33+
STOP_D = builder.stop("D").build();
34+
35+
STOP_A_ID = STOP_A.getId().getId();
36+
STOP_B_ID = STOP_B.getId().getId();
37+
STOP_C_ID = STOP_C.getId().getId();
38+
}
39+
40+
private static final String TRIP_ID = "T:1";
41+
private static final FeedScopedId FEED_SCOPED_TRIP_ID = new FeedScopedId(FEED_ID, TRIP_ID);
42+
43+
private EmissionAggregator subject = new EmissionAggregator(
44+
FEED_SCOPED_TRIP_ID,
45+
List.of(STOP_A, STOP_B, STOP_C, STOP_D)
46+
);
47+
48+
@Test
49+
void mergeAFewRowsOk() {
50+
subject.mergeEmissionForleg(new TripLegsRow(TRIP_ID, STOP_A_ID, 1, Gram.of(3.0)));
51+
subject.mergeEmissionForleg(new TripLegsRow(TRIP_ID, STOP_B_ID, 2, Gram.of(7.0)));
52+
subject.mergeEmissionForleg(new TripLegsRow(TRIP_ID, STOP_C_ID, 3, Gram.of(10.0)));
53+
54+
assertTrue(subject.validate());
55+
assertEquals(List.of(), subject.listIssues());
56+
57+
assertEquals(
58+
"TripPatternEmission{emissions: [Emission{CO₂: 3.0g}, Emission{CO₂: 7.0g}, Emission{CO₂: 10.0g}]}",
59+
subject.build().toString()
60+
);
61+
}
62+
63+
@Test
64+
void mergeWithMissingLegs() {
65+
// Add same row twice, but no row for 2nd and 3rd leg
66+
subject.mergeEmissionForleg(new TripLegsRow(TRIP_ID, STOP_A_ID, 1, Gram.of(2.5)));
67+
subject.mergeEmissionForleg(new TripLegsRow(TRIP_ID, STOP_A_ID, 1, Gram.of(3.5)));
68+
69+
assertFalse(subject.validate());
70+
assertEquals(2, subject.listIssues().size(), () -> subject.listIssues().toString());
71+
assertEquals(
72+
"EmissionMissingLeg(All legs in a trip(E:T:1) must have an emission value. " +
73+
"Leg number 2 and 3 does not have emissions.)",
74+
subject.listIssues().get(0).toString()
75+
);
76+
assertEquals(
77+
"EmissionTripLegDuplicates(Warn! The emission import contains duplicate rows for the same " +
78+
"leg for trip(E:T:1). A average value is used.)",
79+
subject.listIssues().get(1).toString()
80+
);
81+
var ex = assertThrows(IllegalStateException.class, () -> subject.build());
82+
assertEquals("Can not build when there are issues!", ex.getMessage());
83+
}
84+
85+
@Test
86+
void mergeWithStopIdMissmatch() {
87+
// Stop B and C are switched
88+
subject.mergeEmissionForleg(new TripLegsRow(TRIP_ID, STOP_A_ID, 1, Gram.of(3.0)));
89+
subject.mergeEmissionForleg(new TripLegsRow(TRIP_ID, STOP_C_ID, 2, Gram.of(7.0)));
90+
subject.mergeEmissionForleg(new TripLegsRow(TRIP_ID, STOP_B_ID, 3, Gram.of(10.0)));
91+
92+
assertFalse(subject.validate());
93+
assertEquals(2, subject.listIssues().size(), () -> subject.listIssues().toString());
94+
assertEquals(
95+
"EmissionStopIdMissmatch(Emission 'from_stop_id'(C) not found in stop pattern for trip(E:T:1): " +
96+
"TripLegsRow[tripId=T:1, fromStopId=C, fromStopSequence=2, co2=7.0g])",
97+
subject.listIssues().get(0).toString()
98+
);
99+
assertEquals(
100+
"EmissionStopIdMissmatch(Emission 'from_stop_id'(B) not found in stop pattern for trip(E:T:1): " +
101+
"TripLegsRow[tripId=T:1, fromStopId=B, fromStopSequence=3, co2=10.0g])",
102+
subject.listIssues().get(1).toString()
103+
);
104+
var ex = assertThrows(IllegalStateException.class, () -> subject.build());
105+
assertEquals("Can not build when there are issues!", ex.getMessage());
106+
}
107+
108+
@Test
109+
void mergeWithStopIndexOutOfBound() {
110+
subject.mergeEmissionForleg(new TripLegsRow(TRIP_ID, STOP_A_ID, -1, Gram.of(3.0)));
111+
subject.mergeEmissionForleg(new TripLegsRow(TRIP_ID, STOP_C_ID, 4, Gram.of(3.0)));
112+
113+
assertFalse(subject.validate());
114+
assertEquals(2, subject.listIssues().size(), () -> subject.listIssues().toString());
115+
assertEquals(
116+
"EmissionStopSeqNr(The emission 'from_stop_sequence'(-1) is out of bounds[1 - 3]: " +
117+
"TripLegsRow[tripId=T:1, fromStopId=A, fromStopSequence=-1, co2=3.0g])",
118+
subject.listIssues().get(0).toString()
119+
);
120+
assertEquals(
121+
"EmissionStopIdMissmatch(Emission 'from_stop_id'(C) not found in stop pattern for trip(E:T:1): " +
122+
"TripLegsRow[tripId=T:1, fromStopId=C, fromStopSequence=4, co2=3.0g])",
123+
subject.listIssues().get(1).toString()
124+
);
125+
}
126+
127+
@Test
128+
void mergeWithoutCallingValidationMethod() {
129+
var ex = assertThrows(IllegalStateException.class, () -> subject.build());
130+
assertEquals("Forgot to call validate()?", ex.getMessage());
131+
}
132+
133+
@Test
134+
void addRowsAfterValidationIsCalled() {
135+
subject.validate();
136+
var ex = assertThrows(IllegalStateException.class, () ->
137+
subject.mergeEmissionForleg(new TripLegsRow(TRIP_ID, STOP_A_ID, 1, Gram.of(2.5)))
138+
);
139+
assertEquals("Rows can not be added after validate() is called.", ex.getMessage());
140+
}
141+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.opentripplanner.ext.emission.internal.csvdata.trip;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import java.io.FileNotFoundException;
7+
import java.util.List;
8+
import org.junit.jupiter.api.Test;
9+
import org.opentripplanner.ext.emission.EmissionTestData;
10+
import org.opentripplanner.graph_builder.issue.service.DefaultDataImportIssueStore;
11+
12+
class TripDataReaderTest implements EmissionTestData {
13+
14+
private final DefaultDataImportIssueStore issueStore = new DefaultDataImportIssueStore();
15+
private final TripDataReader subject = new TripDataReader(issueStore);
16+
17+
@Test
18+
void testCo2EmissionsFromGtfsDataSource() throws FileNotFoundException {
19+
var emissions = subject.read(emissionOnTripLegs());
20+
21+
assertEquals(
22+
"TripLegsRow[tripId=T1, fromStopId=A, fromStopSequence=1, co2=5.0g]",
23+
emissions.getFirst().toString()
24+
);
25+
assertEquals(
26+
"TripLegsRow[tripId=T2, fromStopId=B, fromStopSequence=2, co2=17.0g]",
27+
emissions.getLast().toString()
28+
);
29+
assertEquals(4, emissions.size());
30+
31+
var issues = issueStore.listIssues();
32+
33+
var expected = List.of(
34+
"The int value '-1' for from_stop_sequence is outside expected range [0 - 1000]: 'E1,A,-1,xyz,25.0' (@line:6)",
35+
"The double value '-0.01' for co2 is outside expected range [0.0 - 1.0E9): 'E2,B,1,xyz,-0.01' (@line:7)"
36+
);
37+
for (int i = 0; i < expected.size(); i++) {
38+
assertEquals(expected.get(i), issues.get(i).getMessage());
39+
}
40+
assertEquals(expected.size(), issues.size());
41+
}
42+
43+
@Test
44+
void handleMissingDdataSource() {
45+
var emissions = subject.read(emissionMissingFile());
46+
assertTrue(emissions.isEmpty());
47+
}
48+
49+
@Test
50+
void ignoreDataSourceIfHeadersDoesNotMatch() {
51+
var emissions = subject.read(emissionOnRoutes());
52+
assertTrue(emissions.isEmpty());
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package org.opentripplanner.ext.emission.internal.csvdata.trip;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.util.List;
8+
import java.util.Map;
9+
import org.junit.jupiter.api.Test;
10+
import org.opentripplanner.framework.model.Gram;
11+
import org.opentripplanner.graph_builder.issue.service.DefaultDataImportIssueStore;
12+
import org.opentripplanner.transit.model._data.TimetableRepositoryForTest;
13+
import org.opentripplanner.transit.model.framework.FeedScopedId;
14+
import org.opentripplanner.transit.model.site.StopLocation;
15+
16+
class TripLegMapperTest {
17+
18+
private static final String FEED_ID = "E";
19+
private static final String STOP_ID_A = "A";
20+
private static final String STOP_ID_B = "B";
21+
private static final String STOP_ID_C = "C";
22+
private static final String STOP_ID_D = "D";
23+
private static final StopLocation STOP_A;
24+
private static final StopLocation STOP_B;
25+
private static final StopLocation STOP_C;
26+
private static final StopLocation STOP_D;
27+
private static final String TRIP_ID_1 = "T:1";
28+
private static final String TRIP_ID_2 = "T:2";
29+
private static final FeedScopedId SCOPED_TRIP_ID_1 = new FeedScopedId(FEED_ID, TRIP_ID_1);
30+
private static final FeedScopedId SCOPED_TRIP_ID_2 = new FeedScopedId(FEED_ID, TRIP_ID_2);
31+
private static final Gram CO2_AB = Gram.of(2.0);
32+
private static final Gram CO2_BC = Gram.of(3.0);
33+
private static final Gram CO2_CD = Gram.of(4.0);
34+
private static final Gram CO2_AD = Gram.of(5.0);
35+
36+
static {
37+
var builder = TimetableRepositoryForTest.of();
38+
STOP_A = builder.stop(STOP_ID_A).build();
39+
STOP_B = builder.stop(STOP_ID_B).build();
40+
STOP_C = builder.stop(STOP_ID_C).build();
41+
STOP_D = builder.stop(STOP_ID_D).build();
42+
}
43+
44+
private final DefaultDataImportIssueStore issueStore = new DefaultDataImportIssueStore();
45+
private final Map<FeedScopedId, List<StopLocation>> stopsByTripId = Map.ofEntries(
46+
Map.entry(SCOPED_TRIP_ID_1, List.of(STOP_A, STOP_B, STOP_C, STOP_D)),
47+
Map.entry(SCOPED_TRIP_ID_2, List.of(STOP_A, STOP_D))
48+
);
49+
50+
private final TripLegMapper subject = new TripLegMapper(stopsByTripId, issueStore);
51+
52+
@Test
53+
void testCaseOk() {
54+
subject.setCurrentFeedId(FEED_ID);
55+
var result = subject.map(
56+
List.of(
57+
new TripLegsRow(TRIP_ID_1, STOP_ID_A, 1, CO2_AB),
58+
new TripLegsRow(TRIP_ID_1, STOP_ID_B, 2, CO2_BC),
59+
new TripLegsRow(TRIP_ID_1, STOP_ID_C, 3, CO2_CD),
60+
new TripLegsRow(TRIP_ID_2, STOP_ID_A, 1, CO2_AD)
61+
)
62+
);
63+
64+
assertEquals(2, result.size(), () -> result.toString());
65+
assertEquals(
66+
"TripPatternEmission{emissions: [Emission{CO₂: 2.0g}, Emission{CO₂: 3.0g}, Emission{CO₂: 4.0g}]}",
67+
result.get(SCOPED_TRIP_ID_1).toString()
68+
);
69+
assertEquals(
70+
"TripPatternEmission{emissions: [Emission{CO₂: 5.0g}]}",
71+
result.get(SCOPED_TRIP_ID_2).toString()
72+
);
73+
}
74+
75+
@Test
76+
void testCaseError() {
77+
subject.setCurrentFeedId(FEED_ID);
78+
var result = subject.map(List.of(new TripLegsRow(TRIP_ID_2, STOP_ID_A, 2, CO2_AB)));
79+
assertTrue(result.isEmpty());
80+
assertEquals(
81+
"Emission 'from_stop_id'(A) not found in stop pattern for trip(E:T:2): " +
82+
"TripLegsRow[tripId=T:2, fromStopId=A, fromStopSequence=2, co2=2.0g]",
83+
issueStore.listIssues().getFirst().getMessage()
84+
);
85+
}
86+
87+
@Test
88+
void currentFeedIdNotSet() {
89+
var ex = assertThrows(IllegalStateException.class, () -> subject.map(List.of()));
90+
assertEquals("currentFeedId is not set", ex.getMessage());
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.opentripplanner.ext.emission.internal.csvdata.trip;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import com.csvreader.CsvReader;
8+
import org.junit.jupiter.api.Test;
9+
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
10+
11+
class TripLegsParserTest {
12+
13+
private static final String DATA =
14+
"""
15+
trip_id, from_stop_id, from_stop_sequence, not_used, co2
16+
F:1, NSR:Quay:1, 1, xyz, 28.0
17+
F:1, NSR:Quay:2, 2, abc, 38.0
18+
""";
19+
20+
@Test
21+
void test() {
22+
var subject = new TripLegsCsvParser(DataImportIssueStore.NOOP, CsvReader.parse(DATA));
23+
assertTrue(subject.headersMatch());
24+
assertTrue(subject.hasNext());
25+
assertEquals(
26+
"TripLegsRow[tripId=F:1, fromStopId=NSR:Quay:1, fromStopSequence=1, co2=28.0g]",
27+
subject.next().toString()
28+
);
29+
assertTrue(subject.hasNext());
30+
assertEquals(
31+
"TripLegsRow[tripId=F:1, fromStopId=NSR:Quay:2, fromStopSequence=2, co2=38.0g]",
32+
subject.next().toString()
33+
);
34+
assertFalse(subject.hasNext());
35+
}
36+
}

application/src/ext-test/java/org/opentripplanner/ext/emission/internal/graphbuilder/EmissionGraphBuilderTest.java

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.opentripplanner.gtfs.config.GtfsDefaultParameters;
1717
import org.opentripplanner.gtfs.config.GtfsFeedParameters;
1818
import org.opentripplanner.model.plan.Emission;
19+
import org.opentripplanner.transit.service.TimetableRepository;
1920

2021
public class EmissionGraphBuilderTest implements EmissionTestData {
2122

@@ -33,6 +34,7 @@ void testMultipleGtfsDataReading() {
3334
feedDataSources,
3435
EmissionParameters.DEFAULT,
3536
emissionRepository,
37+
new TimetableRepository(),
3638
DataImportIssueStore.NOOP
3739
);
3840
subject.buildGraph();

0 commit comments

Comments
 (0)