Skip to content

Add support for importing CO₂ emissions per trip leg #6614

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

Draft
wants to merge 44 commits into
base: dev-2.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
501ea8b
feature: Add version 2.8 to OtpVersion
t2gran Mar 19, 2025
a9d1a9e
refactor: Split config parsing and parameters for emissions module
t2gran Mar 21, 2025
d680bd1
refactor: Move EmissionsConfigurationDocTest to emission module
t2gran Mar 21, 2025
60c5fc4
refactor: Update EmissionsConfigurationDocTest, used local build config
t2gran Mar 21, 2025
4560883
feature: Add the ability to configure emission sources
t2gran Mar 21, 2025
14db59e
feature: Load configured emission sources
t2gran Mar 24, 2025
1729f8c
test: Add test on standalone emission feed (outside GTFS bundle)
t2gran Mar 26, 2025
4034338
refactor: Cleanup GraphBuilder DI
t2gran Mar 27, 2025
d99cb6a
refactor: Use GIT to move Java classes into new packages
t2gran Apr 11, 2025
c021aac
refactor: Update package names and imports for moved emission classes
t2gran Apr 11, 2025
5ba2d41
refactor: Rename DecorateWithEmission to EmissionItineraryDecorator
t2gran Apr 11, 2025
97bef10
refactor: Rename Co2EmissionsDataReader to EmissionsDataReader
t2gran Apr 11, 2025
6bfc070
refactor: Rename EmissionsConfig to EmissionConfig
t2gran Apr 11, 2025
1c1a46e
refactor: Rename emissions to emission in config
t2gran Apr 11, 2025
ce33490
refactor: Git mv package emissions to emission
t2gran Apr 11, 2025
a997e67
refactor: Rename package emissions to emission
t2gran Apr 11, 2025
10f5618
refactor: Rename class Emissions to Emission
t2gran Apr 11, 2025
93f9f30
refactor: Rename class EmissionsConfigurationDocTest to EmissionConfi…
t2gran Apr 11, 2025
f6141da
refactor: Rename class EmissionsDataReader to EmissionDataReader
t2gran Apr 11, 2025
b904d7b
refactor: Rename class EmissionsDecorator to EmissionDecorator
t2gran Apr 11, 2025
65d87fd
refactor: Rename class EmissionsGraphBuilder to EmissionGraphBuilder
t2gran Apr 11, 2025
b01bddb
refactor: Rename class EmissionsGraphBuilderModule to EmissionGraphBu…
t2gran Apr 11, 2025
d6a2a3b
refactor: Rename class EmissionsRepository to EmissionRepository
t2gran Apr 11, 2025
2803147
refactor: Rename class Emissions*Module to Emission*Module
t2gran Apr 11, 2025
2053eb5
refactor: Rename class EmissionsService to EmissionService
t2gran Apr 11, 2025
67e55bf
refactor: Rename class AddEmissionsToItineraryTest to AddEmissionToIt…
t2gran Apr 11, 2025
d1825fa
refactor: Rename variable names with emissions to emission (not colle…
t2gran Apr 11, 2025
6ab82ff
refactor: Rename emissions to emission in doc
t2gran Apr 11, 2025
45a9964
BuildConfigurationDocTest config emission
t2gran Apr 11, 2025
f355cd0
refactor: Rename OTPFeature Co2Emissions to Emission
t2gran Apr 11, 2025
e67c822
refactor: Add unit-test and factory method to Gram
t2gran Apr 11, 2025
6135c92
refactor: Make Emissions Serializable, add math operations and unit-t…
t2gran Apr 11, 2025
3a51ec4
refactor: Add a generic DoubleRange
t2gran Apr 11, 2025
665a5a0
refactor: Move EmissionDataReaderTest to correct package
t2gran Apr 11, 2025
512e4a4
refactor: Rename EmissionDataReaderTest to RouteDataReaderTest
t2gran Apr 11, 2025
c8a50a1
refactor: Extract reusable logic from Route emission parser
t2gran Apr 11, 2025
12124d9
feature: Add a WordList
t2gran Apr 11, 2025
9a8a0ee
refactor: Add modify function to Box
t2gran Apr 11, 2025
acc1d6e
refactor: Cleanup EmissionItineraryDecorator
t2gran Apr 11, 2025
46906b3
refactor: Cleanup ItineraryListFilterChainTest
t2gran Apr 11, 2025
afb7501
refactor: Div code cleanup
t2gran Apr 11, 2025
ce25780
feature: Add support for Emission data on trip legs
t2gran Apr 11, 2025
f70137a
refactor: Fix GrpahQL stubs generation for Emission?
t2gran Apr 13, 2025
dc6159d
Merge remote-tracking branch 'otp/dev-2.x' into add_co2_emissions
t2gran Apr 14, 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
@@ -1,4 +1,4 @@
package org.opentripplanner.ext.emissions;
package org.opentripplanner.ext.emission;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
Expand All @@ -13,12 +13,12 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.opentripplanner._support.time.ZoneIds;
import org.opentripplanner.ext.emissions.internal.DefaultEmissionsRepository;
import org.opentripplanner.ext.emissions.internal.DefaultEmissionsService;
import org.opentripplanner.ext.emissions.itinerary.DecorateWithEmission;
import org.opentripplanner.ext.emission.internal.DefaultEmissionRepository;
import org.opentripplanner.ext.emission.internal.DefaultEmissionService;
import org.opentripplanner.ext.emission.internal.itinerary.EmissionItineraryDecorator;
import org.opentripplanner.framework.model.Cost;
import org.opentripplanner.framework.model.Gram;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.model.plan.Emission;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.model.plan.Leg;
import org.opentripplanner.model.plan.LegConstructionSupport;
Expand All @@ -34,10 +34,18 @@
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripTimesFactory;

class EmissionsTest {
/**
* @deprecated The purpose of this test is not clear?
* - Is it an integration test, it does notload data from file.
* - Or, is it a unit-test on the {@link EmissionItineraryDecorator}? The test package/name does not
* reflect this and there is realy no need to create a
*
*/
@Deprecated
class EmissionTest {

private static DefaultEmissionsService eService;
private static DecorateWithEmission decorateWithEmission;
private static DefaultEmissionService eService;
private static EmissionItineraryDecorator emissionDecorator;

static final ZonedDateTime TIME = OffsetDateTime.parse(
"2023-07-20T17:49:06+03:00"
Expand All @@ -53,7 +61,7 @@ class EmissionsTest {
private static final Route ROUTE_WITH_EMISSIONS = TimetableRepositoryForTest.route(
id("1")
).build();
private static final Route ROUTE_WITH_ZERO_EMISSIONS = TimetableRepositoryForTest.route(
private static final Route ROUTE_WITH_UNKNOWN_EMISSIONS = TimetableRepositoryForTest.route(
id("2")
).build();
private static final Route ROUTE_WITHOUT_EMISSIONS_CONFIGURED = TimetableRepositoryForTest.route(
Expand All @@ -62,57 +70,59 @@ class EmissionsTest {

@BeforeAll
static void SetUp() {
Map<FeedScopedId, Double> emissions = new HashMap<>();
emissions.put(new FeedScopedId("F", "1"), (0.12 / 12));
emissions.put(new FeedScopedId("F", "2"), 0.0);
EmissionsRepository emissionsRepository = new DefaultEmissionsRepository();
emissionsRepository.setCo2Emissions(emissions);
emissionsRepository.setCarAvgCo2PerMeter(0.131);
eService = new DefaultEmissionsService(emissionsRepository);
decorateWithEmission = new DecorateWithEmission(eService);
Map<FeedScopedId, Emission> emission = new HashMap<>();
emission.put(id("1"), Emission.co2_g(.12 / 12));
emission.put(id("2"), Emission.co2_g(0.0));
EmissionRepository emissionRepository = new DefaultEmissionRepository();
emissionRepository.addRouteEmissions(emission);
emissionRepository.setCarAvgCo2PerMeter(0.131);
eService = new DefaultEmissionService(emissionRepository);
emissionDecorator = new EmissionItineraryDecorator(eService);
}

@Test
void testGetEmissionsForItinerary() {
var i = createItinerary(createTransitLeg(ROUTE_WITH_EMISSIONS));
i = decorateWithEmission.decorate(i);
assertEquals(new Gram(2223.902), i.emissionsPerPerson().getCo2());
var itinerary = createItinerary(createTransitLeg(ROUTE_WITH_EMISSIONS));
itinerary = emissionDecorator.decorate(itinerary);
assertEquals(Emission.co2_g(2223.902), itinerary.emissionPerPerson());
}

@Test
void testGetEmissionsForCarRoute() {
var i = createItinerary(STREET_LEG);
i = decorateWithEmission.decorate(i);
assertEquals(new Gram(28.0864), i.emissionsPerPerson().getCo2());
var itinerary = createItinerary(STREET_LEG);
itinerary = emissionDecorator.decorate(itinerary);
assertEquals(Emission.co2_g(28.0864), itinerary.emissionPerPerson());
}

@Test
void testNoEmissionsForFeedWithoutEmissionsConfigured() {
var i = createItinerary(createTransitLeg(ROUTE_WITHOUT_EMISSIONS_CONFIGURED));
i = decorateWithEmission.decorate(i);
assertNull(i.emissionsPerPerson());
var itinerary = createItinerary(createTransitLeg(ROUTE_WITHOUT_EMISSIONS_CONFIGURED));
itinerary = emissionDecorator.decorate(itinerary);
assertNull(itinerary.emissionPerPerson());
}

@Test
void testZeroEmissionsForItineraryWithZeroEmissions() {
var i = createItinerary(createTransitLeg(ROUTE_WITH_ZERO_EMISSIONS));
i = decorateWithEmission.decorate(i);
assertEquals(new Gram(0.0), i.emissionsPerPerson().getCo2());
var itinerary = createItinerary(createTransitLeg(ROUTE_WITH_UNKNOWN_EMISSIONS));
itinerary = emissionDecorator.decorate(itinerary);
assertNull(itinerary.emissionPerPerson());
}

@Test
void testGetEmissionsForCombinedRoute() {
var i = createItinerary(createTransitLeg(ROUTE_WITH_EMISSIONS), STREET_LEG);
i = decorateWithEmission.decorate(i);
assertEquals(new Gram(2251.9884), i.emissionsPerPerson().getCo2());
var itinerary = createItinerary(createTransitLeg(ROUTE_WITH_EMISSIONS), STREET_LEG);
itinerary = emissionDecorator.decorate(itinerary);
assertEquals(Emission.co2_g(2251.9884), itinerary.emissionPerPerson());
}

@Test
void testNoEmissionsForCombinedRouteWithoutTransitEmissions() {
var i = createItinerary(createTransitLeg(ROUTE_WITHOUT_EMISSIONS_CONFIGURED), STREET_LEG);
i = decorateWithEmission.decorate(i);
var emissionsResult = i.emissionsPerPerson() != null ? i.emissionsPerPerson().getCo2() : null;
assertNull(emissionsResult);
var itinerary = createItinerary(
createTransitLeg(ROUTE_WITHOUT_EMISSIONS_CONFIGURED),
STREET_LEG
);
itinerary = emissionDecorator.decorate(itinerary);
assertNull(itinerary.emissionPerPerson());
}

private ScheduledTransitLeg createTransitLeg(Route route) {
Expand All @@ -127,10 +137,7 @@ private ScheduledTransitLeg createTransitLeg(Route route) {
var pattern = TimetableRepositoryForTest.tripPattern("1", route)
.withStopPattern(stopPattern)
.build();
var trip = Trip.of(FeedScopedId.parse("FOO:BAR"))
.withMode(TransitMode.BUS)
.withRoute(route)
.build();
var trip = Trip.of(id("FOO:BAR")).withMode(TransitMode.BUS).withRoute(route).build();
return new ScheduledTransitLegBuilder<>()
.withTripTimes(TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()))
.withTripPattern(pattern)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.opentripplanner.ext.emission;

import java.io.File;
import org.opentripplanner.datastore.api.CompositeDataSource;
import org.opentripplanner.datastore.api.DataSource;
import org.opentripplanner.datastore.api.FileType;
import org.opentripplanner.datastore.file.FileDataSource;
import org.opentripplanner.ext.emission.internal.csvdata.EmissionDataReader;
import org.opentripplanner.test.support.ResourceLoader;
import org.opentripplanner.transit.model.framework.FeedScopedId;

public interface EmissionTestData {
String FEED_FEED_ID = "em";
String GTFS_DIR_FEED_ID = "gd";
String GTFS_ZIP_FEED_ID = "gz";

FeedScopedId ROUTE_ID_EM_R1 = new FeedScopedId(FEED_FEED_ID, "R1");
FeedScopedId ROUTE_ID_GD_1001 = new FeedScopedId(GTFS_DIR_FEED_ID, "1001");
FeedScopedId ROUTE_ID_GZ_1002 = new FeedScopedId(GTFS_ZIP_FEED_ID, "1002");

default CompositeDataSource gtfsWithEmissionZip() {
return resource().catalogDataSource("gz-gtfs.zip", FileType.GTFS);
}

default CompositeDataSource gtfsWithEmissionDir() {
return resource().catalogDataSource("gd-gtfs/", FileType.GTFS);
}

default DataSource gtfsWithEmissionFile() {
return gtfsWithEmissionDir().entry(EmissionDataReader.EMISSION_FILE_NAME);
}

default DataSource emissionOnRoutes() {
return resource().dataSource("em-on-routes.txt", FileType.EMMISION);
}

default DataSource emissionOnTripLegs() {
return resource().dataSource("em-on-trip-legs.txt", FileType.EMMISION);
}

/**
* The DataSource framwork should prevent this from happening, but we add it here
* as a test-case so we can see that the parsers handle it gracefully.
*/
default DataSource emissionMissingFile() {
return new FileDataSource(new File("file-does-not-exist.txt"), FileType.EMMISION);
}

private ResourceLoader resource() {
return ResourceLoader.of(EmissionTestData.class);
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,36 @@
package org.opentripplanner.generate.doc;
package org.opentripplanner.ext.emission.doc;

import static org.opentripplanner.framework.application.OtpFileNames.BUILD_CONFIG_FILENAME;
import static org.opentripplanner.framework.io.FileUtils.assertFileEquals;
import static org.opentripplanner.framework.io.FileUtils.readFile;
import static org.opentripplanner.framework.io.FileUtils.writeFile;
import static org.opentripplanner.generate.doc.framework.DocsTestConstants.TEMPLATE_PATH;
import static org.opentripplanner.generate.doc.framework.DocsTestConstants.USER_DOC_PATH;
import static org.opentripplanner.generate.doc.framework.TemplateUtil.replaceSection;
import static org.opentripplanner.standalone.config.framework.json.JsonSupport.jsonNodeFromResource;
import static org.opentripplanner.utils.text.MarkdownFormatter.HEADER_4;

import java.io.File;
import org.junit.jupiter.api.Test;
import org.opentripplanner.framework.application.OtpFileNames;
import org.opentripplanner.generate.doc.framework.DocBuilder;
import org.opentripplanner.generate.doc.framework.DocsTestConstants;
import org.opentripplanner.generate.doc.framework.GeneratesDocumentation;
import org.opentripplanner.generate.doc.framework.ParameterDetailsList;
import org.opentripplanner.generate.doc.framework.ParameterSummaryTable;
import org.opentripplanner.generate.doc.framework.SkipNodes;
import org.opentripplanner.generate.doc.framework.TemplateUtil;
import org.opentripplanner.standalone.config.BuildConfig;
import org.opentripplanner.standalone.config.framework.json.JsonSupport;
import org.opentripplanner.standalone.config.framework.json.NodeAdapter;
import org.opentripplanner.test.support.ResourceLoader;

@GeneratesDocumentation
public class EmissionsConfigurationDocTest {
public class EmissionConfigurationDocTest implements DocsTestConstants {

private static final File TEMPLATE = new File(TEMPLATE_PATH, "Emissions.md");
private static final File OUT_FILE = new File(USER_DOC_PATH + "/sandbox", "Emissions.md");
private static final String CONFIG_JSON = OtpFileNames.BUILD_CONFIG_FILENAME;
private static final String CONFIG_PATH = "standalone/config/" + CONFIG_JSON;
private static final File TEMPLATE = new File(TEMPLATE_PATH, "Emission.md");
private static final File OUT_FILE = new File(SANDBOX_USER_DOC_PATH, "Emission.md");
private static final SkipNodes SKIP_NODES = SkipNodes.of().build();

@Test
public void updateEmissionsDoc() {
NodeAdapter node = readEmissionsConfig();
public void updateMapEmissionsConfigDoc() {
NodeAdapter node = readMapEmissionsConfigConfig();

String template = readFile(TEMPLATE);
String original = readFile(OUT_FILE);
Expand All @@ -43,10 +41,14 @@ public void updateEmissionsDoc() {
assertFileEquals(original, OUT_FILE);
}

private NodeAdapter readEmissionsConfig() {
var json = jsonNodeFromResource(CONFIG_PATH);
var conf = new BuildConfig(json, CONFIG_PATH, false);
return conf.asNodeAdapter().child("emissions");
private NodeAdapter readMapEmissionsConfigConfig() {
var buildConfigFile = ResourceLoader.of(EmissionConfigurationDocTest.class).extTestResourceFile(
BUILD_CONFIG_FILENAME
);

var json = JsonSupport.jsonNodeFromPath(buildConfigFile.toPath());
var conf = new BuildConfig(json, buildConfigFile.toString(), false);
return conf.asNodeAdapter().child("emission");
}

private String updaterDoc(NodeAdapter node) {
Expand All @@ -70,7 +72,7 @@ private void addDetailsSection(DocBuilder buf, NodeAdapter node) {
}

private void addExample(DocBuilder buf, NodeAdapter node) {
var root = TemplateUtil.jsonExampleBuilder(node.rawNode()).wrapInObject("emissions").build();
var root = TemplateUtil.jsonExampleBuilder(node.rawNode()).wrapInObject("emission").build();
buf.header(3, "Example configuration", null).addExample("build-config.json", root);
}
}
Loading
Loading