diff --git a/app/component/Emissions.js b/app/component/Emissions.js new file mode 100644 index 0000000000..5cfcbd567e --- /dev/null +++ b/app/component/Emissions.js @@ -0,0 +1,86 @@ +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 Emissions = ({ itinerary, carItinerary, emissionsInfolink }) => { + const co2value = getCo2Value(itinerary); + const itineraryIsCar = itinerary.legs.every( + leg => leg.mode === 'CAR' || leg.mode === 'WALK', + ); + const carCo2Value = + !itineraryIsCar && carItinerary + ? Math.round(carItinerary?.emissionsPerPerson?.co2) + : null; + const useCo2SimpleDesc = !carCo2Value || itineraryIsCar; + const co2DescriptionId = useCo2SimpleDesc + ? 'itinerary-co2.description-simple' + : 'itinerary-co2.description'; + + return ( + co2value !== null && + co2value >= 0 && ( +
+
+
+
+
+ + + + + + + {emissionsInfolink && ( + + + + )} + +
+
+
+
+
+ ) + ); +}; + +Emissions.propTypes = { + itinerary: ItineraryShape.isRequired, + carItinerary: ItineraryShape.isRequired, + emissionsInfolink: PropTypes.string.isRequired, +}; + +export default Emissions; diff --git a/app/component/EmissionsInfo.js b/app/component/EmissionsInfo.js new file mode 100644 index 0000000000..616e8f3485 --- /dev/null +++ b/app/component/EmissionsInfo.js @@ -0,0 +1,51 @@ +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 62dd46870a..c760ced0bf 100644 --- a/app/component/ItinerarySummaryListContainer/ItinerarySummaryListContainer.js +++ b/app/component/ItinerarySummaryListContainer/ItinerarySummaryListContainer.js @@ -55,6 +55,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 ( @@ -334,6 +342,9 @@ const containerComponent = createFragmentContainer( walkDistance startTime endTime + emissionsPerPerson { + co2 + } legs { realTime realtimeState diff --git a/app/component/ItineraryTab.js b/app/component/ItineraryTab.js index 26a1ab4b15..33e55c62b8 100644 --- a/app/component/ItineraryTab.js +++ b/app/component/ItineraryTab.js @@ -42,6 +42,8 @@ import { import CityBikeDurationInfo from './CityBikeDurationInfo'; import { getCityBikeNetworkId } from '../util/citybikes'; import { FareShape } from '../util/shapes'; +import Emissions from './Emissions'; +import EmissionsInfo from './EmissionsInfo'; import FareDisclaimer from './FareDisclaimer'; const AlertShape = PropTypes.shape({ alertSeverityLevel: PropTypes.string }); @@ -66,6 +68,9 @@ const ItineraryShape = PropTypes.shape({ }), ), fares: PropTypes.arrayOf(FareShape), + emissionsPerPerson: PropTypes.shape({ + co2: PropTypes.number, + }), }); /* eslint-disable prettier/prettier */ @@ -80,6 +85,7 @@ class ItineraryTab extends React.Component { isMobile: PropTypes.bool.isRequired, currentTime: PropTypes.number.isRequired, hideTitle: PropTypes.bool, + carItinerary: ItineraryShape, currentLanguage: PropTypes.string, changeHash: PropTypes.func, }; @@ -183,7 +189,6 @@ class ItineraryTab extends React.Component { if (!itinerary || !itinerary.legs[0]) { return null; } - const fares = getFares(itinerary.fares, getRoutes(itinerary.legs), config); const extraProps = this.setExtraProps(itinerary); const legsWithRentalBike = compressLegs(itinerary.legs).filter(leg => @@ -349,6 +354,7 @@ class ItineraryTab extends React.Component { legs={itinerary.legs} /> )), + config.showCO2InItinerarySummary && ,
{config.showRouteInformation && }
+ {config.showCO2InItinerarySummary && + } {this.shouldShowDisclaimer(config) && (
{ const index = props.params.secondHash @@ -41,6 +42,7 @@ const MobileItineraryWrapper = (props, context) => { focusToLeg={props.focusToLeg} changeHash={props.changeHash} isMobile + carItinerary={props.carItinerary} />
); @@ -73,6 +75,7 @@ MobileItineraryWrapper.propTypes = { plan: PropTypes.object, serviceTimeRange: PropTypes.object.isRequired, onSwipe: PropTypes.func, + carItinerary: ItineraryShape, changeHash: PropTypes.func, }; diff --git a/app/component/SummaryPage.js b/app/component/SummaryPage.js index 7fb0be7be0..b8b25e24c8 100644 --- a/app/component/SummaryPage.js +++ b/app/component/SummaryPage.js @@ -737,6 +737,9 @@ class SummaryPage extends React.Component { endTime ...ItineraryTab_itinerary ...SummaryPlanContainer_itineraries + emissionsPerPerson { + co2 + } legs { mode ...ItineraryLine_legs @@ -790,6 +793,9 @@ class SummaryPage extends React.Component { endTime ...ItineraryTab_itinerary ...SummaryPlanContainer_itineraries + emissionsPerPerson { + co2 + } legs { mode ...ItineraryLine_legs @@ -849,6 +855,9 @@ class SummaryPage extends React.Component { endTime ...ItineraryTab_itinerary ...SummaryPlanContainer_itineraries + emissionsPerPerson { + co2 + } legs { mode ...ItineraryLine_legs @@ -1029,6 +1038,9 @@ class SummaryPage extends React.Component { endTime ...ItineraryTab_itinerary ...SummaryPlanContainer_itineraries + emissionsPerPerson { + co2 + } legs { mode ...ItineraryLine_legs @@ -2501,6 +2513,7 @@ class SummaryPage extends React.Component { focusToPoint={this.focusToPoint} focusToLeg={this.focusToLeg} isMobile={false} + carItinerary={carPlan?.itineraries[0]} />
); @@ -2732,6 +2745,7 @@ class SummaryPage extends React.Component { serviceTimeRange={this.props.serviceTimeRange} focusToLeg={this.focusToLeg} onSwipe={this.changeHash} + carItinerary={carPlan?.itineraries[0]} changeHash={this.changeHash} > {this.props.content && @@ -2961,6 +2975,9 @@ const containerComponent = createRefetchContainer( endTime ...ItineraryTab_itinerary ...SummaryPlanContainer_itineraries + emissionsPerPerson { + co2 + } legs { mode ...ItineraryLine_legs diff --git a/app/component/SummaryPlanContainer.js b/app/component/SummaryPlanContainer.js index 2643de8796..f56b8eef57 100644 --- a/app/component/SummaryPlanContainer.js +++ b/app/component/SummaryPlanContainer.js @@ -344,6 +344,9 @@ const connectedContainer = createFragmentContainer( itineraries { startTime endTime + emissionsPerPerson { + co2 + } legs { mode ...ItineraryLine_legs @@ -401,6 +404,9 @@ const connectedContainer = createFragmentContainer( ...ItinerarySummaryListContainer_itineraries endTime startTime + emissionsPerPerson { + co2 + } legs { mode to { diff --git a/app/component/SummaryRow.js b/app/component/SummaryRow.js index 3fec6dff1f..3f734d18e6 100644 --- a/app/component/SummaryRow.js +++ b/app/component/SummaryRow.js @@ -30,6 +30,7 @@ import { } from '../util/citybikes'; import { getRouteMode } from '../util/modeUtils'; import { getCapacityForLeg } from '../util/occupancyUtil'; +import { getCo2Value } from '../util/itineraryUtils'; const Leg = ({ mode, @@ -244,6 +245,7 @@ const SummaryRow = ( intermediatePlaces, zones, onlyHasWalkingItineraries, + lowestCo2value, ...props }, { intl, intl: { formatMessage }, config }, @@ -259,7 +261,7 @@ const SummaryRow = ( const startTime = moment(data.startTime); const endTime = moment(data.endTime); const duration = endTime.diff(startTime); - + const co2value = getCo2Value(data); const mobile = bp => !(bp === 'large'); const legs = []; let noTransitLegs = true; @@ -720,6 +722,17 @@ const SummaryRow = ( />
); + const co2summary = ( +
+ +
+ ); const ariaLabelMessage = intl.formatMessage( { @@ -755,6 +768,10 @@ const SummaryRow = ( /> {textSummary} + {config.showCO2InItinerarySummary && + co2value !== null && + co2value >= 0 && + co2summary}
)} + {config.showCO2InItinerarySummary && + co2value !== null && + co2value >= 0 && ( +
+ {lowestCo2value === co2value && ( + + )} +
{co2value} g
+
+ )}
@@ -898,10 +925,17 @@ SummaryRow.propTypes = { showCancelled: PropTypes.bool, zones: PropTypes.arrayOf(PropTypes.string), onlyHasWalkingItineraries: PropTypes.bool, + lowestCo2value: PropTypes.number, }; SummaryRow.defaultProps = { zones: [], + passive: false, + intermediatePlaces: [], + isCancelled: false, + showCancelled: false, + onlyHasWalkingItineraries: false, + lowestCo2value: 0, }; SummaryRow.contextTypes = { diff --git a/app/component/itinerary-profile.scss b/app/component/itinerary-profile.scss index a86f737619..ce2d8a4980 100644 --- a/app/component/itinerary-profile.scss +++ b/app/component/itinerary-profile.scss @@ -6,6 +6,7 @@ font-size: 15px; line-height: 18px; margin-top: 7px; + margin-bottom: 7px; &.small { font-size: 10pt; diff --git a/app/component/itinerary.scss b/app/component/itinerary.scss index 2205812ae7..865685b5de 100644 --- a/app/component/itinerary.scss +++ b/app/component/itinerary.scss @@ -135,7 +135,7 @@ $itinerary-tab-switch-height: 48px; } } - div.itinerary-ticket-information { + div.itinerary-ticket-information, .itinerary-co2-information { flex-grow: 0; flex-shrink: 0; } @@ -191,6 +191,160 @@ $itinerary-tab-switch-height: 48px; } } +.itinerary-co2-information { + &.mobile { + padding-left: 5px; + .divider-bottom { + width: 365px; + } + } +} + +.itinerary-co2-line { + position: relative; + + .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 { + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + padding: 2px 4px; + gap: 4px; + + min-width: 37px; + height: 20px; + right: 82.38px; + top: 416px; + overflow: visible; + + background: rgba(100, 190, 30, 0.15); + border-radius: 4px; + + .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; @@ -436,7 +590,7 @@ $itinerary-tab-switch-height: 48px; } } -.itinerary-ticket-information { +.itinerary-ticket-information, .itinerary-co2-information { display: flex; color: $gray; font-weight: $font-weight-book; @@ -468,7 +622,7 @@ $itinerary-tab-switch-height: 48px; } } - .itinerary-ticket-type { + .itinerary-ticket-type, .itinerary-co2-line { display: flex; flex-direction: column; flex-grow: 1; @@ -566,7 +720,7 @@ $itinerary-tab-switch-height: 48px; } .desktop { - .itinerary-ticket-information { + .itinerary-ticket-information, .itinerary-co2-information { font-size: 0.9375rem; width: 400px; margin: 0 60px; diff --git a/app/component/summary-row.scss b/app/component/summary-row.scss index a0f1961e2f..2202c6a3f5 100644 --- a/app/component/summary-row.scss +++ b/app/component/summary-row.scss @@ -114,6 +114,49 @@ flex-grow: 1; margin-right: 20px; } + + .itinerary-co2-value-container { + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + padding: 2px 4px; + gap: 4px; + margin-right: 6.38px; + 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 { + width: 11px; + height: 11px; + margin-left: 2px; + } + } + } + .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; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + letter-spacing: -0.03em; + } + } } .itinerary-end-time-and-distance { diff --git a/app/configurations/config.default.js b/app/configurations/config.default.js index a83aca4a59..7dbaac073b 100644 --- a/app/configurations/config.default.js +++ b/app/configurations/config.default.js @@ -865,4 +865,7 @@ export default { }, showAlternativeLegs: true, + + // Notice! Turning on this setting forces the search for car routes (for the CO2 comparison only). + showCO2InItinerarySummary: false, }; diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index cacb7e5d93..9ed0e83677 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -47,6 +47,11 @@ export default { BANNERS: BANNER_URL, HSL_FI_SUGGESTIONS: 'https://content.hsl.fi/api/v1/search/suggestions', EMBEDDED_SEARCH_GENERATION: '/reittiopas-elementti', + EMISSIONS_INFO: { + fi: 'https://www.hsl.fi/hsl/sahkobussit/ymparisto-lukuina', + sv: 'https://www.hsl.fi/sv/reseplaneraren_co2', + en: 'https://www.hsl.fi/en/journey_planner_co2', + }, }, indexPath: 'etusivu', @@ -502,6 +507,9 @@ export default { showBikeAndPublicItineraries: true, showBikeAndParkItineraries: true, + // DT-5325 Notice! Turning on this setting forces the search for car routes (for the CO2 comparison only). + showCO2InItinerarySummary: true, + includeCarSuggestions: false, includeParkAndRideSuggestions: true, // Include both bike and park and bike and public diff --git a/app/configurations/config.jyvaskyla.js b/app/configurations/config.jyvaskyla.js index 9ac75593e9..faf002b7fb 100644 --- a/app/configurations/config.jyvaskyla.js +++ b/app/configurations/config.jyvaskyla.js @@ -163,4 +163,6 @@ export default configMerger(walttiConfig, { stops: true, itinerary: true, }, + // DT-5325 Notice! Turning on this setting forces the search for car routes (for the CO2 comparison only). + showCO2InItinerarySummary: true, }); diff --git a/app/configurations/config.kela.js b/app/configurations/config.kela.js index 82e9903335..4137708d36 100644 --- a/app/configurations/config.kela.js +++ b/app/configurations/config.kela.js @@ -147,4 +147,5 @@ export default configMerger(matkaConfig, { }, ], }, + showCO2InItinerarySummary: false, }); diff --git a/app/configurations/config.matka.js b/app/configurations/config.matka.js index 037022a59f..83f774ba7a 100644 --- a/app/configurations/config.matka.js +++ b/app/configurations/config.matka.js @@ -524,4 +524,6 @@ export default { virtualMonitorBaseUrl: 'https://matkamonitori.digitransit.fi/', }, }, + // Notice! Turning on this setting forces the search for car routes (for the CO2 comparison only). + showCO2InItinerarySummary: true, }; diff --git a/app/prop-types/ItineraryShape.js b/app/prop-types/ItineraryShape.js index e7a5fb12b3..123c85a199 100644 --- a/app/prop-types/ItineraryShape.js +++ b/app/prop-types/ItineraryShape.js @@ -4,4 +4,7 @@ export default PropTypes.shape({ endTime: PropTypes.number, startTime: PropTypes.number, legs: PropTypes.arrayOf(PropTypes.object), + emissionsPerPerson: PropTypes.shape({ + co2: PropTypes.number, + }), }); diff --git a/app/translations.js b/app/translations.js index d61a23ecbb..4978a67162 100644 --- a/app/translations.js +++ b/app/translations.js @@ -1115,6 +1115,17 @@ const translations = { inquiry: 'How did you find the new Journey Planner? Please tell us!', instructions: 'Instructions', 'is-open': 'Open:', + 'itinerary-co2.description': + '{co2value} g of CO₂ emissions will be generated on this journey. A car would generate {carCo2Value} g of CO₂ on the same journey.', + 'itinerary-co2.description-simple': + '{co2value} g of CO₂ emissions will be generated on this journey.', + 'itinerary-co2.description-simple-sr': + '{co2value} g of carbondioxide emissions will be generated on this journey.', + 'itinerary-co2.description-sr': + '{co2value} g of carbondioxide emissions will be generated on this journey. A car would generate {carCo2Value} g of carbondioxide on the same journey.', + 'itinerary-co2.link': 'This is how we reduce emissions ›', + 'itinerary-co2.title': 'CO₂ emissions of the journey', + 'itinerary-co2.title-sr': 'Carbondioxide emissions of the journey', 'itinerary-details.biking-leg': 'At {time} cycle {distance} from {origin} to {to} {destination}. Estimated time {duration}', 'itinerary-details.car-leg': @@ -1611,6 +1622,7 @@ const translations = { 'travel-time-label': 'Travel time', 'travel-time-with-hours': '{h} h {min} min', 'trip-co2-emissions': 'CO2 emissions of the journey', + 'trip-co2-emissions-sr': 'Carbondioxide emissions of the journey', 'trip-information': 'Trip information', 'trip-page.description': 'Route {route_short_name} - {route_long_name}', 'trip-page.title': 'Route {route_short_name}', @@ -2280,6 +2292,17 @@ const translations = { inquiry: 'Mitä pidät uudesta Reittioppaasta? Kerro se meille! ', instructions: 'Ohjeet', 'is-open': 'Avoinna:', + 'itinerary-co2.description': + 'Tämän matkan CO₂-päästöt ovat {co2value} g. Autolla kuljettuna ne olisivat olleet {carCo2Value} g.', + 'itinerary-co2.description-simple': + 'Tämän matkan CO₂-päästöt ovat {co2value} g.', + 'itinerary-co2.description-simple-sr': + 'Tämän matkan hiilidioksidipäästöt ovat {co2value} g.', + 'itinerary-co2.description-sr': + 'Tämän matkan hiilidioksidipäästöt ovat {co2value} g. Autolla kuljettuna ne olisivat olleet {carCo2Value} g.', + 'itinerary-co2.link': 'Näin vähennämme päästöjä ›', + 'itinerary-co2.title': 'Matkan CO₂-päästöt', + 'itinerary-co2.title-sr': 'Matkan hiilidioksidipäästöt', 'itinerary-details.biking-leg': '{time} pyöräile kohteesta {origin} {distance} {to} {destination}. Matka-aika {duration}', 'itinerary-details.car-leg': @@ -2772,7 +2795,8 @@ const translations = { 'travel-time': '{min} min', 'travel-time-label': 'Matka-aika', 'travel-time-with-hours': '{h} h {min} min', - 'trip-co2-emissions': 'Reitin CO2 päästöt', + 'trip-co2-emissions': 'Reitin CO₂-päästöt', + 'trip-co2-emissions-sr': 'Reitin hiilidioksidipäästöt', 'trip-information': 'Lähdön tiedot', 'trip-page.description': 'Linja {shortName}, {longName}', 'trip-page.title': 'Linja {shortName}', @@ -4215,6 +4239,17 @@ const translations = { inquiry: 'Vad tycker du om den nya Reseplaneraren. Berätta för oss!', instructions: 'Anvisningar', 'is-open': 'Öppet:', + 'itinerary-co2.description': + 'CO₂-utsläppen för denna resa är {co2value} g. En bil skulle generera {carCo2Value} g CO₂ på samma resa.', + 'itinerary-co2.description-simple': + 'CO₂-utsläppen för denna resa är {co2value} g.', + 'itinerary-co2.description-simple-sr': + 'Koldioxidutsläppen för denna resa är {co2value} g.', + 'itinerary-co2.description-sr': + 'Koldioxidutsläppen för denna resa är {co2value} g. En bil skulle generera {carCo2Value} g koldioxid på samma resa.', + 'itinerary-co2.link': 'Så minskar vi utsläppen ›', + 'itinerary-co2.title': 'Rejsens CO₂-udslip', + 'itinerary-co2.title-sr': 'Rejsens koldioxidutsläpp', 'itinerary-details.biking-leg': '{time} cykla {distance} från {origin} till {to} {destination}. Restid {duration}', 'itinerary-details.car-leg': @@ -4716,6 +4751,7 @@ const translations = { 'travel-time-label': 'Restid', 'travel-time-with-hours': '{h} h {min} min', 'trip-co2-emissions': 'Resans CO2-utsläpp', + 'trip-co2-emissions-sr': 'Resans koldioxidutsläpp', 'trip-information': 'Visa avgång', 'trip-page.description': 'Linje {shortName} - {longName}', 'trip-page.title': 'Linje {shortName}', 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/app/util/planParamUtil.js b/app/util/planParamUtil.js index 49f4760968..17bb2f811a 100644 --- a/app/util/planParamUtil.js +++ b/app/util/planParamUtil.js @@ -174,11 +174,16 @@ const getShouldMakeCarQuery = ( settings, defaultSettings, ) => { + const forceCarRouting = config.showCO2InItinerarySummary + ? config.showCO2InItinerarySummary + : defaultSettings.showCO2InItinerarySummary; + + const includeCarSuggestions = settings.includeCarSuggestions + ? settings.includeCarSuggestions + : defaultSettings.includeCarSuggestions; return ( - linearDistance > config.suggestCarMinDistance && - (settings.includeCarSuggestions - ? settings.includeCarSuggestions - : defaultSettings.includeCarSuggestions) + forceCarRouting || + (linearDistance > config.suggestCarMinDistance && includeCarSuggestions) ); }; diff --git a/app/util/queryUtils.js b/app/util/queryUtils.js index 2b4008e09e..2a742170cc 100644 --- a/app/util/queryUtils.js +++ b/app/util/queryUtils.js @@ -264,6 +264,9 @@ export const moreItinerariesQuery = graphql` endTime ...ItineraryTab_itinerary ...SummaryPlanContainer_itineraries + emissionsPerPerson { + co2 + } legs { mode ...ItineraryLine_legs diff --git a/build/schema.graphql b/build/schema.graphql index 276a8ae6f5..22971eaff9 100644 --- a/build/schema.graphql +++ b/build/schema.graphql @@ -900,6 +900,13 @@ type elevationProfileComponent { elevation: Float } +type Emissions { + """ + CO₂ emissions in grams. + """ + co2: Grams +} + type fare { type: String @deprecated @@ -1001,6 +1008,8 @@ type Geometry { scalar GeoJson +scalar Grams + type StopGeometries { """Representation of the stop geometries as GeoJSON (https://geojson.org/)""" geoJson: GeoJson, @@ -1467,6 +1476,9 @@ type Itinerary { """How far the user has to walk, in meters.""" walkDistance: Float + """Emissions of this itinerary per traveler.""" + emissionsPerPerson: Emissions + """ A list of Legs. Each Leg is either a walking (cycling, car) portion of the itinerary, or a transit leg on a particular vehicle. So a itinerary where the diff --git a/build/schema.json b/build/schema.json index e913bb5c46..b7c98fb9f2 100644 --- a/build/schema.json +++ b/build/schema.json @@ -2173,6 +2173,29 @@ "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 + }, { "kind": "OBJECT", "name": "FareMedium", @@ -2586,6 +2609,16 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "Grams", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "SCALAR", "name": "ID", @@ -3149,6 +3182,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.", diff --git a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql index 276a8ae6f5..22971eaff9 100644 --- a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql +++ b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql @@ -900,6 +900,13 @@ type elevationProfileComponent { elevation: Float } +type Emissions { + """ + CO₂ emissions in grams. + """ + co2: Grams +} + type fare { type: String @deprecated @@ -1001,6 +1008,8 @@ type Geometry { scalar GeoJson +scalar Grams + type StopGeometries { """Representation of the stop geometries as GeoJSON (https://geojson.org/)""" geoJson: GeoJson, @@ -1467,6 +1476,9 @@ type Itinerary { """How far the user has to walk, in meters.""" walkDistance: Float + """Emissions of this itinerary per traveler.""" + emissionsPerPerson: Emissions + """ A list of Legs. Each Leg is either a walking (cycling, car) portion of the itinerary, or a transit leg on a particular vehicle. So a itinerary where the diff --git a/static/assets/svg-sprite.default.svg b/static/assets/svg-sprite.default.svg index 15c77b1938..6989b19c19 100644 --- a/static/assets/svg-sprite.default.svg +++ b/static/assets/svg-sprite.default.svg @@ -2789,5 +2789,8 @@ + + + diff --git a/static/assets/svg-sprite.hsl.svg b/static/assets/svg-sprite.hsl.svg index 6712ea0dda..4237874b61 100644 --- a/static/assets/svg-sprite.hsl.svg +++ b/static/assets/svg-sprite.hsl.svg @@ -2711,5 +2711,8 @@ + + + diff --git a/test/e2e/mock-data/SummaryPageQueryResponse.json b/test/e2e/mock-data/SummaryPageQueryResponse.json index dc3aee0732..3a77b2da03 100644 --- a/test/e2e/mock-data/SummaryPageQueryResponse.json +++ b/test/e2e/mock-data/SummaryPageQueryResponse.json @@ -33,6 +33,7 @@ "type":"regular" } ], + "emissions": 123.4, "legs":[ { "mode":"WALK", @@ -1389,6 +1390,7 @@ "type":"regular" } ], + "emissions": 123.4, "legs":[ { "mode":"WALK", @@ -3915,6 +3917,7 @@ "type":"regular" } ], + "emissions": 123.4, "legs":[ { "mode":"WALK", @@ -7257,6 +7260,7 @@ "type":"regular" } ], + "emissions": 123.4, "legs":[ { "mode":"WALK", @@ -10314,6 +10318,7 @@ "type":"regular" } ], + "emissions": 123.4, "legs":[ { "mode":"WALK", diff --git a/test/unit/ItineraryTab.test.js b/test/unit/ItineraryTab.test.js index db03bbd6f8..85de12847d 100644 --- a/test/unit/ItineraryTab.test.js +++ b/test/unit/ItineraryTab.test.js @@ -19,6 +19,7 @@ describe('', () => { }, isMobile: false, currentTime: 0, + lang: 'fi', }; const wrapper = shallowWithIntl(, { context: { ...mockContext },