From 834c0d2674643e6fe13cb2a244d836d49997ad8d Mon Sep 17 00:00:00 2001 From: Andreas Helms Date: Wed, 20 Nov 2024 12:33:27 +0100 Subject: [PATCH] feat(itinerary): show co2 emissions per person --- app/component/EmissionsInfo.js | 55 ++++++ .../ItinerarySummaryListContainer.js | 11 ++ app/component/ItineraryTab.js | 14 +- app/component/SummaryRow.js | 32 ++++ app/component/itinerary.scss | 171 +++++++++++++++++- app/component/summary-row.scss | 1 + app/configurations/config.herrenberg.js | 2 + app/translations.js | 2 + app/util/itineraryUtils.js | 8 + build/schema.json | 45 +++++ .../stadtnavi/icon-icon_co2_leaf.svg | 1 + 11 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 app/component/EmissionsInfo.js create mode 100644 app/util/itineraryUtils.js create mode 100644 static/svg-icons/stadtnavi/icon-icon_co2_leaf.svg 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 && ( +
+
+
+
+ + + + + +
+
+
{co2value} g
+
+
+
+
+ ) + ); +}; + +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