diff --git a/app/component/EmissionsInfo.js b/app/component/EmissionsInfo.js
new file mode 100644
index 0000000000..0323911f81
--- /dev/null
+++ b/app/component/EmissionsInfo.js
@@ -0,0 +1,55 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import cx from 'classnames';
+import { FormattedMessage } from 'react-intl';
+import Icon from './Icon';
+import ItineraryShape from '../prop-types/ItineraryShape';
+import { getCo2Value } from '../util/itineraryUtils';
+
+const EmissionsInfo = ({ itinerary, isMobile }) => {
+ const co2value = getCo2Value(itinerary);
+ return (
+ co2value !== null &&
+ co2value >= 0 && (
+
+ )
+ );
+};
+
+EmissionsInfo.propTypes = {
+ itinerary: ItineraryShape.isRequired,
+ isMobile: PropTypes.bool.isRequired,
+};
+
+export default EmissionsInfo;
diff --git a/app/component/ItinerarySummaryListContainer/ItinerarySummaryListContainer.js b/app/component/ItinerarySummaryListContainer/ItinerarySummaryListContainer.js
index 1945506959..1e81ddb37d 100644
--- a/app/component/ItinerarySummaryListContainer/ItinerarySummaryListContainer.js
+++ b/app/component/ItinerarySummaryListContainer/ItinerarySummaryListContainer.js
@@ -61,6 +61,13 @@ function ItinerarySummaryListContainer(
itineraries.length > 0 &&
!itineraries.includes(undefined)
) {
+ const lowestCo2value = Math.round(
+ itineraries
+ .filter(itinerary => itinerary.emissionsPerPerson?.co2 >= 0)
+ .reduce((a, b) => {
+ return a.emissionsPerPerson?.co2 < b.emissionsPerPerson?.co2 ? a : b;
+ }, 0).emissionsPerPerson?.co2,
+ );
const summaries = itineraries.map((itinerary, i) => (
));
if (
@@ -339,6 +347,9 @@ const containerComponent = createFragmentContainer(
walkDistance
startTime
endTime
+ emissionsPerPerson {
+ co2
+ }
legs {
# Temporarilly commented out, still needed in upstream OTP
# alerts {
diff --git a/app/component/ItineraryTab.js b/app/component/ItineraryTab.js
index a7b89105f4..8d9f43cc89 100644
--- a/app/component/ItineraryTab.js
+++ b/app/component/ItineraryTab.js
@@ -13,6 +13,7 @@ import RouteInformation from './RouteInformation';
import ItinerarySummary from './ItinerarySummary';
import ItineraryLegs from './ItineraryLegs';
import BackButton from './BackButton';
+import EmissionsInfo from './EmissionsInfo';
import {
getRoutes,
getZones,
@@ -58,6 +59,9 @@ const ItineraryShape = PropTypes.shape({
trip: TripShape,
distance: PropTypes.number,
fares: PropTypes.arrayOf(FareShape),
+ emissionsPerPerson: PropTypes.shape({
+ co2: PropTypes.number,
+ }),
}),
),
fares: PropTypes.arrayOf(FareShape),
@@ -344,6 +348,12 @@ class ItineraryTab extends React.Component {
)}
+ {config.showCO2InItinerarySummary && (
+
+ )}
)}
-
{shouldShowFareInfo(config) && (
= props.delayThreshold;
+ const co2value = getCo2Value(data);
const mobile = bp => !(bp === 'large');
const legs = [];
let noTransitLegs = true;
@@ -704,6 +707,18 @@ const SummaryRow = (
);
+ const co2summary = (
+
+
+
+ );
+
const ariaLabelMessage = intl.formatMessage(
{
id: 'itinerary-page.show-details-label',
@@ -738,6 +753,10 @@ const SummaryRow = (
/>
{textSummary}
+ {config.showCO2InItinerarySummary &&
+ co2value !== null &&
+ co2value >= 0 &&
+ co2summary}
{itineraryStartAndEndTime}
+
+ {config.showCO2InItinerarySummary &&
+ co2value !== null &&
+ co2value >= 0 && (
+
+ {lowestCo2value === co2value && (
+
+ )}
+
{co2value} g
+
+ )}
@@ -875,10 +905,12 @@ SummaryRow.propTypes = {
zones: PropTypes.arrayOf(PropTypes.string),
delayThreshold: PropTypes.number,
onlyHasWalkingItineraries: PropTypes.bool,
+ lowestCo2value: PropTypes.number,
};
SummaryRow.defaultProps = {
zones: [],
+ lowestCo2value: 0,
};
SummaryRow.contextTypes = {
diff --git a/app/component/itinerary.scss b/app/component/itinerary.scss
index 13cce3e9e8..574b2f7bc6 100644
--- a/app/component/itinerary.scss
+++ b/app/component/itinerary.scss
@@ -222,6 +222,175 @@ $itinerary-tab-switch-height: 48px;
}
}
+.itinerary-co2-information {
+ &.mobile {
+ padding-left: 5px;
+ .divider-bottom {
+ width: 365px;
+ }
+ }
+}
+
+.itinerary-co2-line {
+ position: relative;
+ flex: 1;
+
+ .divider-top,
+ .divider-bottom {
+ border-bottom: 1px solid #dddddd;
+ margin-left: 10px;
+ margin-right: 10px;
+
+ @media print {
+ border: none;
+ }
+ }
+
+ .co2-description-container {
+ display: flex;
+ gap: 20.01px;
+ justify-content: space-between;
+ align-items: start;
+ margin-top: 5px;
+ margin-bottom: 5px;
+
+ .icon-container {
+ .icon {
+ &.co2-leaf {
+ height: 25.06px;
+ width: 25.6px;
+ margin-top: 5px;
+ margin-left: 5px;
+ }
+ }
+ }
+
+ .itinerary-co2-description {
+ width: auto;
+ left: 70px;
+ top: 528px;
+ &.simple {
+ width: auto;
+ }
+ font-family: $font-family;
+ font-style: normal;
+ font-weight: 325;
+ font-size: 15px;
+ line-height: 20px;
+ letter-spacing: -0.03em;
+ display: flex;
+ flex-direction: column;
+ color: #666666;
+ }
+ }
+
+ .emissions-info-link {
+ text-decoration: none;
+ font-weight: $font-weight-medium;
+ color: $primary-color;
+ font-size: $font-size-small;
+ }
+
+ .co2-container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: 11px 10px 10px 15px;
+ &.mobile {
+ flex-direction: row;
+ margin: 11px 10px 10px 6px
+ }
+ @include min-width(tablet) {
+ margin-right: 15px;
+ }
+
+ .co2-title-container {
+ display: flex;
+ gap: 11.34px;
+ justify-content: space-between;
+ align-items: center;
+
+ .icon-container {
+ .icon {
+ &.co2-leaf {
+ height: 13.91px;
+ width: 14.22px;
+ }
+ }
+ }
+
+ .itinerary-co2-title {
+ max-width: 250px;
+ height: 18px;
+ left: 48px;
+ top: 528px;
+
+ font-family: $font-family;
+ font-style: normal;
+ font-weight: 325;
+ font-size: 15px;
+ line-height: 18px;
+ /* identical to box height */
+ display: flex;
+ align-items: center;
+ letter-spacing: -0.03em;
+
+ color: #666666;
+ }
+ }
+ }
+
+ .itinerary-co2-value-container {
+ margin-right: initial;
+ }
+}
+
+.itinerary-co2-value-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+ padding: 2px 4px;
+ margin-right: 6.38px;
+ gap: 4px;
+
+ min-width: 37px;
+ height: 20px;
+ right: 82.38px;
+ top: 416px;
+ overflow: visible;
+
+ background: rgba(100, 190, 30, 0.15);
+ border-radius: 4px;
+
+ .icon-container {
+ .icon {
+ &.co2-leaf {
+ height: 12px;
+ width: 12px;
+ }
+ }
+ }
+
+ .itinerary-co2-value {
+ min-width: 29px;
+ height: 16px;
+
+ font-family: $font-family;
+ font-style: normal;
+ font-weight: 325;
+ font-size: 13px;
+ line-height: 16px;
+ color: #3B7F00;
+ /* identical to box height */
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ letter-spacing: -0.03em;
+ }
+}
+
@media print {
.itinerary-main {
display: block;
@@ -461,7 +630,7 @@ $itinerary-tab-switch-height: 48px;
}
}
-.itinerary-ticket-information {
+.itinerary-ticket-information, .itinerary-co2-information {
display: flex;
color: $gray;
font-weight: $font-weight-book;
diff --git a/app/component/summary-row.scss b/app/component/summary-row.scss
index bf47ea85ba..d7c6350d4b 100644
--- a/app/component/summary-row.scss
+++ b/app/component/summary-row.scss
@@ -85,6 +85,7 @@
}
.itinerary-duration-container {
+ display: flex;
width: 100%;
overflow: hidden;
text-align: right;
diff --git a/app/configurations/config.herrenberg.js b/app/configurations/config.herrenberg.js
index 1931cfc329..a85537792f 100644
--- a/app/configurations/config.herrenberg.js
+++ b/app/configurations/config.herrenberg.js
@@ -401,4 +401,6 @@ export default configMerger(parentConfig, {
// live bus locations
vehicles: true,
+
+ showCO2InItinerarySummary: true,
});
diff --git a/app/translations.js b/app/translations.js
index d908417e6e..f1771778ba 100644
--- a/app/translations.js
+++ b/app/translations.js
@@ -294,6 +294,7 @@ const translations = {
'hsl_ticket': 'HSL ticket',
'hsl_travel_card': 'HSL card',
'is-open': 'Geöffnet:',
+ 'itinerary-co2.title': 'CO₂-Emissionen der Reise',
'itinerary-details.via-leg': '{arrivalTime} Ankunft am Zwischenziel {viaPoint}. {leaveAction}',
'itinerary-in-the-past': 'Die Verbindung liegt in der Vergangenheit.',
'itinerary-in-the-past-link': 'Abfahrt jetzt ›',
@@ -1667,6 +1668,7 @@ const translations = {
inquiry: 'How did you find the new Journey Planner? Please tell us!',
instructions: 'Instructions',
'is-open': 'Open:',
+ 'itinerary-co2.title': 'CO₂ emissions of the journey',
'itinerary-details.biking-leg':
'At {time} cycle {distance} from {origin} to {to} {destination}. Estimated time {duration}',
'itinerary-details.car-leg':
diff --git a/app/util/itineraryUtils.js b/app/util/itineraryUtils.js
new file mode 100644
index 0000000000..7bca13fd3b
--- /dev/null
+++ b/app/util/itineraryUtils.js
@@ -0,0 +1,8 @@
+export const getCo2Value = itinerary => {
+ return typeof itinerary.emissionsPerPerson?.co2 === 'number' &&
+ itinerary.emissionsPerPerson?.co2 >= 0
+ ? Math.round(itinerary.emissionsPerPerson?.co2)
+ : null;
+};
+
+export default getCo2Value;
diff --git a/build/schema.json b/build/schema.json
index fa677f54b9..9fe0224d7c 100644
--- a/build/schema.json
+++ b/build/schema.json
@@ -2275,6 +2275,16 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "SCALAR",
+ "name": "Grams",
+ "description": "",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "SCALAR",
"name": "ID",
@@ -2838,6 +2848,18 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "emissionsPerPerson",
+ "description": "Emissions of this itinerary per traveler.",
+ "args": [],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Emissions",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "legs",
"description": "A list of Legs. Each Leg is either a walking (cycling, car) portion of the\nitinerary, or a transit leg on a particular vehicle. So a itinerary where the\nuser walks to the Q train, transfers to the 6, then walks to their\ndestination, has four legs.",
@@ -12067,6 +12089,29 @@
"interfaces": [],
"enumValues": null,
"possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Emissions",
+ "description": null,
+ "fields": [
+ {
+ "name": "co2",
+ "description": "CO₂ emissions in grams.",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Grams",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [],
+ "enumValues": null,
+ "possibleTypes": null
}
],
"directives": [
diff --git a/static/svg-icons/stadtnavi/icon-icon_co2_leaf.svg b/static/svg-icons/stadtnavi/icon-icon_co2_leaf.svg
new file mode 100644
index 0000000000..0a868d7358
--- /dev/null
+++ b/static/svg-icons/stadtnavi/icon-icon_co2_leaf.svg
@@ -0,0 +1 @@
+
\ No newline at end of file