diff --git a/www/__mocks__/fakeLabels.json b/www/__mocks__/fakeLabels.json new file mode 100644 index 000000000..676dc97b6 --- /dev/null +++ b/www/__mocks__/fakeLabels.json @@ -0,0 +1,208 @@ +{ + "MODE": [ + { + "value": "walk", + "baseMode": "WALKING", + "met_equivalent": "WALKING", + "kgCo2PerKm": 0 + }, + { + "value": "e-bike", + "baseMode": "E_BIKE", + "met": { + "ALL": { + "range": [0, -1], + "mets": 4.9 + } + }, + "kgCo2PerKm": 0.00728 + }, + { + "value": "bike", + "baseMode": "BICYCLING", + "met_equivalent": "BICYCLING", + "kgCo2PerKm": 0 + }, + { + "value": "bikeshare", + "baseMode": "BICYCLING", + "met_equivalent": "BICYCLING", + "kgCo2PerKm": 0 + }, + { + "value": "scootershare", + "baseMode": "E_SCOOTER", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.00894 + }, + { + "value": "drove_alone", + "baseMode": "CAR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.22031 + }, + { + "value": "shared_ride", + "baseMode": "CAR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.11015 + }, + { + "value": "hybrid_drove_alone", + "baseMode": "CAR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.127 + }, + { + "value": "hybrid_shared_ride", + "baseMode": "CAR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.0635 + }, + { + "value": "e_car_drove_alone", + "baseMode": "E_CAR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.08216 + }, + { + "value": "e_car_shared_ride", + "baseMode": "E_CAR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.04108 + }, + { + "value": "taxi", + "baseMode": "TAXI", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.30741 + }, + { + "value": "bus", + "baseMode": "BUS", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.20727 + }, + { + "value": "train", + "baseMode": "TRAIN", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.12256 + }, + { + "value": "free_shuttle", + "baseMode": "BUS", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.20727 + }, + { + "value": "air", + "baseMode": "AIR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.09975 + }, + { + "value": "not_a_trip", + "baseMode": "UNKNOWN", + "met_equivalent": "UNKNOWN", + "kgCo2PerKm": 0 + }, + { + "value": "other", + "baseMode": "OTHER", + "met_equivalent": "UNKNOWN", + "kgCo2PerKm": 0 + } + ], + "PURPOSE": [ + { + "value": "home" + }, + { + "value": "work" + }, + { + "value": "at_work" + }, + { + "value": "school" + }, + { + "value": "transit_transfer" + }, + { + "value": "shopping" + }, + { + "value": "meal" + }, + { + "value": "pick_drop_person" + }, + { + "value": "pick_drop_item" + }, + { + "value": "personal_med" + }, + { + "value": "access_recreation" + }, + { + "value": "exercise" + }, + { + "value": "entertainment" + }, + { + "value": "religious" + }, + { + "value": "other" + } + ], + "REPLACED_MODE": [ + { + "value": "no_travel" + }, + { + "value": "walk" + }, + { + "value": "bike" + }, + { + "value": "bikeshare" + }, + { + "value": "scootershare" + }, + { + "value": "drove_alone" + }, + { + "value": "shared_ride" + }, + { + "value": "e_car_drove_alone" + }, + { + "value": "e_car_shared_ride" + }, + { + "value": "taxi" + }, + { + "value": "bus" + }, + { + "value": "train" + }, + { + "value": "free_shuttle" + }, + { + "value": "other" + } + ] +} diff --git a/www/__tests__/customMetricsHelper.test.ts b/www/__tests__/customMetricsHelper.test.ts new file mode 100644 index 000000000..0ae025bff --- /dev/null +++ b/www/__tests__/customMetricsHelper.test.ts @@ -0,0 +1,51 @@ +import { getConfig } from '../js/config/dynamicConfig'; +import { + getCustomFootprint, + getCustomMETs, + initCustomDatasetHelper, +} from '../js/metrics/customMetricsHelper'; +import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; +import { mockLogger } from '../__mocks__/globalMocks'; +import fakeLabels from '../__mocks__/fakeLabels.json'; + +mockBEMUserCache(); +mockLogger(); + +global.fetch = (url: string) => + new Promise((rs, rj) => { + setTimeout(() => + rs({ + text: () => + new Promise((rs, rj) => { + let myJSON = JSON.stringify(fakeLabels); + setTimeout(() => rs(myJSON), 100); + }), + }), + ); + }) as any; + +it('gets the custom mets', async () => { + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); + expect(getCustomMETs()).toMatchObject({ + walk: expect.any(Object), + bike: expect.any(Object), + bikeshare: expect.any(Object), + 'e-bike': expect.any(Object), + scootershare: expect.any(Object), + drove_alone: expect.any(Object), + }); +}); + +it('gets the custom footprint', async () => { + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); + expect(getCustomFootprint()).toMatchObject({ + walk: expect.any(Number), + bike: expect.any(Number), + bikeshare: expect.any(Number), + 'e-bike': expect.any(Number), + scootershare: expect.any(Number), + drove_alone: expect.any(Number), + }); +}); diff --git a/www/__tests__/footprintHelper.test.ts b/www/__tests__/footprintHelper.test.ts new file mode 100644 index 000000000..842442153 --- /dev/null +++ b/www/__tests__/footprintHelper.test.ts @@ -0,0 +1,63 @@ +import { initCustomDatasetHelper } from '../js/metrics/customMetricsHelper'; +import { + clearHighestFootprint, + getFootprintForMetrics, + getHighestFootprint, + getHighestFootprintForDistance, +} from '../js/metrics/footprintHelper'; +import { getConfig } from '../js/config/dynamicConfig'; +import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; +import { mockLogger } from '../__mocks__/globalMocks'; +import fakeLabels from '../__mocks__/fakeLabels.json'; + +mockBEMUserCache(); +mockLogger(); + +global.fetch = (url: string) => + new Promise((rs, rj) => { + setTimeout(() => + rs({ + text: () => + new Promise((rs, rj) => { + let myJSON = JSON.stringify(fakeLabels); + setTimeout(() => rs(myJSON), 100); + }), + }), + ); + }) as any; + +beforeEach(() => { + clearHighestFootprint(); +}); + +const custom_metrics = [ + { key: 'walk', values: 3000 }, + { key: 'bike', values: 6500 }, + { key: 'drove_alone', values: 10000 }, + { key: 'scootershare', values: 25000 }, + { key: 'unicycle', values: 5000 }, +]; + +it('gets footprint for metrics (custom, fallback 0)', async () => { + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); + expect(getFootprintForMetrics(custom_metrics, 0)).toBe(2.4266); +}); + +it('gets footprint for metrics (custom, fallback 0.1)', async () => { + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); + expect(getFootprintForMetrics(custom_metrics, 0.1)).toBe(2.4266 + 0.5); +}); + +it('gets the highest footprint from the dataset, custom', async () => { + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); + expect(getHighestFootprint()).toBe(0.30741); +}); + +it('gets the highest footprint for distance, custom', async () => { + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); + expect(getHighestFootprintForDistance(12345)).toBe(0.30741 * (12345 / 1000)); +}); diff --git a/www/__tests__/metHelper.test.ts b/www/__tests__/metHelper.test.ts new file mode 100644 index 000000000..bc477daa0 --- /dev/null +++ b/www/__tests__/metHelper.test.ts @@ -0,0 +1,40 @@ +import { getMet } from '../js/metrics/metHelper'; +import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; +import { mockLogger } from '../__mocks__/globalMocks'; +import fakeLabels from '../__mocks__/fakeLabels.json'; +import { getConfig } from '../js/config/dynamicConfig'; +import { initCustomDatasetHelper } from '../js/metrics/customMetricsHelper'; + +mockBEMUserCache(); +mockLogger(); + +global.fetch = (url: string) => + new Promise((rs, rj) => { + setTimeout(() => + rs({ + text: () => + new Promise((rs, rj) => { + let myJSON = JSON.stringify(fakeLabels); + setTimeout(() => rs(myJSON), 100); + }), + }), + ); + }) as any; + +it('gets met for mode and speed', () => { + expect(getMet('WALKING', 1.47523, 0)).toBe(4.3); + expect(getMet('BICYCLING', 4.5, 0)).toBe(6.8); + expect(getMet('UNICYCLE', 100, 0)).toBe(0); + expect(getMet('CAR', 25, 1)).toBe(0); +}); + +it('gets custom met for mode and speed', async () => { + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); + expect(getMet('walk', 1.47523, 0)).toBe(4.3); + expect(getMet('bike', 4.5, 0)).toBe(6.8); + expect(getMet('unicycle', 100, 0)).toBe(0); + expect(getMet('drove_alone', 25, 1)).toBe(0); + expect(getMet('e-bike', 6, 1)).toBe(4.9); + expect(getMet('e-bike', 12, 1)).toBe(4.9); +}); diff --git a/www/index.js b/www/index.js index 74ebc4c5d..997141073 100644 --- a/www/index.js +++ b/www/index.js @@ -13,6 +13,4 @@ import './js/i18n-utils.js'; import './js/main.js'; import './js/diary.js'; import './js/diary/services.js'; -import './js/metrics-factory.js'; -import './js/metrics-mappings.js'; import './js/plugin/logger.ts'; diff --git a/www/js/App.tsx b/www/js/App.tsx index a955b032d..2eece7f55 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -19,6 +19,7 @@ import { initPushNotify } from './splash/pushNotifySettings'; import { initStoreDeviceSettings } from './splash/storeDeviceSettings'; import { initRemoteNotifyHandler } from './splash/remoteNotifyHandler'; import { withErrorBoundary } from './plugin/ErrorBoundary'; +import { initCustomDatasetHelper } from './metrics/customMetricsHelper'; const defaultRoutes = (t) => [ { @@ -77,6 +78,7 @@ const App = () => { initPushNotify(); initStoreDeviceSettings(); initRemoteNotifyHandler(); + initCustomDatasetHelper(appConfig); }, [appConfig]); const appContextValue = { diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index 4ebf49c24..257eb3cf6 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -138,7 +138,9 @@ const Chart = ({ logDebug(`Horizontal axis callback: i = ${i}; chartDatasets = ${JSON.stringify(chartDatasets)}; chartDatasets[0].data = ${JSON.stringify(chartDatasets[0].data)}`); - const label = chartDatasets[0].data[i].y; + //account for different data possiblities + const label = + chartDatasets[0].data[i]?.y || chartDatasets[i].data[0]?.y; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); return label; @@ -175,7 +177,9 @@ const Chart = ({ logDebug(`Vertical axis callback: i = ${i}; chartDatasets = ${JSON.stringify(chartDatasets)}; chartDatasets[0].data = ${JSON.stringify(chartDatasets[0].data)}`); - const label = chartDatasets[0].data[i].x; + //account for different data possiblities - one mode per week, one mode both weeks, mixed weeks + const label = + chartDatasets[0].data[i]?.x || chartDatasets[i].data[0]?.x; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); return label; diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index f239b8b29..914c97d82 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -43,7 +43,6 @@ const ProfileSettings = () => { const { setPermissionsPopupVis } = useContext(AppContext); //angular services needed - const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); const NotificationScheduler = getAngularService('NotificationScheduler'); const ControlHelper = getAngularService('ControlHelper'); @@ -54,7 +53,6 @@ const ProfileSettings = () => { //states and variables used to control/create the settings const [opCodeVis, setOpCodeVis] = useState(false); const [nukeSetVis, setNukeVis] = useState(false); - const [carbonDataVis, setCarbonDataVis] = useState(false); const [forceStateVis, setForceStateVis] = useState(false); const [logoutVis, setLogoutVis] = useState(false); const [invalidateSuccessVis, setInvalidateSuccessVis] = useState(false); @@ -82,9 +80,6 @@ const ProfileSettings = () => { const [uploadReason, setUploadReason] = useState(''); const appVersion = useRef(); - let carbonDatasetString = - t('general-settings.carbon-dataset') + ': ' + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); - const carbonOptions = CarbonDatasetHelper.getCarbonDatasetOptions(); const stateActions = [ { text: 'Initialize', transition: 'INITIALIZE' }, { text: 'Start trip', transition: 'EXITED_GEOFENCE' }, @@ -358,16 +353,6 @@ const ProfileSettings = () => { forceTransition(stateObject.transition); }; - const onSelectCarbon = function (carbonObject) { - console.log('changeCarbonDataset(): chose locale ' + carbonObject.value); - CarbonDatasetHelper.saveCurrentCarbonDatasetLocale(carbonObject.value); //there's some sort of error here - //Unhandled Promise Rejection: While logging, error -[NSNull UTF8String]: unrecognized selector sent to instance 0x7fff8a625fb0 - carbonDatasetString = - i18next.t('general-settings.carbon-dataset') + - ': ' + - CarbonDatasetHelper.getCurrentCarbonDatasetCode(); - }; - //conditional creation of setting sections let logUploadSection; @@ -438,10 +423,6 @@ const ProfileSettings = () => { textKey="control.medium-accuracy" action={toggleLowAccuracy} switchValue={collectSettings.lowAccuracy}> - setCarbonDataVis(true)}> { - {/* menu for "set carbon dataset - only somewhat working" */} - clearNotifications()}> - {/* force state sheet */} 999 ? Math.round(v / 1000) + 'k kg CO₂' : Math.round(v) + ' kg CO₂'; - }; - fh.getFootprintForMetrics = function (userMetrics, defaultIfMissing = 0) { - var footprint = fh.getFootprint(); - var result = 0; - for (var i in userMetrics) { - var mode = userMetrics[i].key; - if (mode == 'ON_FOOT') { - mode = 'WALKING'; - } - - if (mode in footprint) { - result += footprint[mode] * mtokm(userMetrics[i].values); - } else if (mode == 'IN_VEHICLE') { - result += - ((footprint['CAR'] + - footprint['BUS'] + - footprint['LIGHT_RAIL'] + - footprint['TRAIN'] + - footprint['TRAM'] + - footprint['SUBWAY']) / - 6) * - mtokm(userMetrics[i].values); - } else { - console.warn( - 'WARNING FootprintHelper.getFootprintFromMetrics() was requested for an unknown mode: ' + - mode + - ' metrics JSON: ' + - JSON.stringify(userMetrics), - ); - result += defaultIfMissing * mtokm(userMetrics[i].values); - } - } - return result; - }; - fh.getLowestFootprintForDistance = function (distance) { - var footprint = fh.getFootprint(); - var lowestFootprint = Number.MAX_SAFE_INTEGER; - for (var mode in footprint) { - if (mode == 'WALKING' || mode == 'BICYCLING') { - // these modes aren't considered when determining the lowest carbon footprint - } else { - lowestFootprint = Math.min(lowestFootprint, footprint[mode]); - } - } - return lowestFootprint * mtokm(distance); - }; - - fh.getHighestFootprint = function () { - if (!highestFootprint) { - var footprint = fh.getFootprint(); - let footprintList = []; - for (var mode in footprint) { - footprintList.push(footprint[mode]); - } - highestFootprint = Math.max(...footprintList); - } - return highestFootprint; - }; - - fh.getHighestFootprintForDistance = function (distance) { - return fh.getHighestFootprint() * mtokm(distance); - }; - - var getLowestMotorizedNonAirFootprint = function (footprint, rlmCO2) { - var lowestFootprint = Number.MAX_SAFE_INTEGER; - for (var mode in footprint) { - if (mode == 'AIR_OR_HSR' || mode == 'air') { - console.log('Air mode, ignoring'); - } else { - if (footprint[mode] == 0 || footprint[mode] <= rlmCO2) { - console.log( - 'Non motorized mode or footprint <= range_limited_motorized', - mode, - footprint[mode], - rlmCO2, - ); - } else { - lowestFootprint = Math.min(lowestFootprint, footprint[mode]); - } - } - } - return lowestFootprint; - }; - - fh.getOptimalDistanceRanges = function () { - const FIVE_KM = 5 * 1000; - const SIX_HUNDRED_KM = 600 * 1000; - if (!fh.useCustom) { - const defaultFootprint = CarbonDatasetHelper.getCurrentCarbonDatasetFootprint(); - const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint(defaultFootprint); - const airFootprint = defaultFootprint['AIR_OR_HSR']; - return [ - { low: 0, high: FIVE_KM, optimal: 0 }, - { low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir }, - { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, - ]; - } else { - // custom footprint, let's get the custom values - const customFootprint = CustomDatasetHelper.getCustomFootprint(); - let airFootprint = customFootprint['air']; - if (!airFootprint) { - // 2341 BTU/PMT from - // https://tedb.ornl.gov/wp-content/uploads/2021/02/TEDB_Ed_39.pdf#page=68 - // 159.25 lb per million BTU from EIA - // https://www.eia.gov/environment/emissions/co2_vol_mass.php - // (2341 * (159.25/1000000))/(1.6*2.2) = 0.09975, rounded up a bit - console.log('No entry for air in ', customFootprint, ' using default'); - airFootprint = 0.1; - } - const rlm = CustomDatasetHelper.range_limited_motorized; - if (!rlm) { - return [ - { low: 0, high: FIVE_KM, optimal: 0 }, - { low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir }, - { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, - ]; - } else { - console.log('Found range_limited_motorized mode', rlm); - const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint( - customFootprint, - rlm.kgCo2PerKm, - ); - return [ - { low: 0, high: FIVE_KM, optimal: 0 }, - { low: FIVE_KM, high: rlm.range_limit_km * 1000, optimal: rlm.kgCo2PerKm }, - { - low: rlm.range_limit_km * 1000, - high: SIX_HUNDRED_KM, - optimal: lowestMotorizedNonAir, - }, - { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, - ]; - } - } - }; - - return fh; - }) - - .factory('CalorieCal', function (METDatasetHelper, CustomDatasetHelper) { - var cc = {}; - var highestMET = 0; - var USER_DATA_KEY = 'user-data'; - cc.useCustom = false; - - cc.setUseCustomFootprint = function () { - cc.useCustom = true; - }; - - cc.getMETs = function () { - if (this.useCustom == true) { - return CustomDatasetHelper.getCustomMETs(); - } else { - return METDatasetHelper.getStandardMETs(); - } - }; - - cc.set = function (info) { - return storageSet(USER_DATA_KEY, info); - }; - cc.get = function () { - return storageGet(USER_DATA_KEY); - }; - cc.delete = function () { - return storageRemove(USER_DATA_KEY); - }; - Number.prototype.between = function (min, max) { - return this >= min && this <= max; - }; - cc.getHighestMET = function () { - if (!highestMET) { - var met = cc.getMETs(); - let metList = []; - for (var mode in met) { - var rangeList = met[mode]; - for (var range in rangeList) { - metList.push(rangeList[range].mets); - } - } - highestMET = Math.max(...metList); - } - return highestMET; - }; - cc.getMet = function (mode, speed, defaultIfMissing) { - if (mode == 'ON_FOOT') { - console.log("CalorieCal.getMet() converted 'ON_FOOT' to 'WALKING'"); - mode = 'WALKING'; - } - let currentMETs = cc.getMETs(); - if (!currentMETs[mode]) { - console.warn('CalorieCal.getMet() Illegal mode: ' + mode); - return defaultIfMissing; //So the calorie sum does not break with wrong return type - } - for (var i in currentMETs[mode]) { - if (mpstomph(speed).between(currentMETs[mode][i].range[0], currentMETs[mode][i].range[1])) { - return currentMETs[mode][i].mets; - } else if (mpstomph(speed) < 0) { - console.log('CalorieCal.getMet() Negative speed: ' + mpstomph(speed)); - return 0; - } - } - }; - var mpstomph = function (mps) { - return 2.23694 * mps; - }; - var lbtokg = function (lb) { - return lb * 0.453592; - }; - var fttocm = function (ft) { - return ft * 30.48; - }; - cc.getCorrectedMet = function (met, gender, age, height, heightUnit, weight, weightUnit) { - var height = heightUnit == 0 ? fttocm(height) : height; - var weight = weightUnit == 0 ? lbtokg(weight) : weight; - if (gender == 1) { - //male - var met = - (met * 3.5) / - (((66.473 + 5.0033 * height + 13.7516 * weight - 6.755 * age) / 1440 / 5 / weight) * - 1000); - return met; - } else if (gender == 0) { - //female - var met = - (met * 3.5) / - (((655.0955 + 1.8496 * height + 9.5634 * weight - 4.6756 * age) / 1440 / 5 / weight) * - 1000); - return met; - } - }; - cc.getuserCalories = function (durationInMin, met) { - return 65 * durationInMin * met; - }; - cc.getCalories = function (weightInKg, durationInMin, met) { - return weightInKg * durationInMin * met; - }; - return cc; - }); diff --git a/www/js/metrics-mappings.js b/www/js/metrics-mappings.js deleted file mode 100644 index 38836a3a1..000000000 --- a/www/js/metrics-mappings.js +++ /dev/null @@ -1,425 +0,0 @@ -import angular from 'angular'; -import { getLabelOptions } from './survey/multilabel/confirmHelper'; -import { getConfig } from './config/dynamicConfig'; -import { storageGet, storageSet } from './plugin/storage'; - -angular - .module('emission.main.metrics.mappings', ['emission.plugin.logger']) - - .service('CarbonDatasetHelper', function () { - var CARBON_DATASET_KEY = 'carbon_dataset_locale'; - - // Values are in Kg/PKm (kilograms per passenger-kilometer) - // Sources for EU values: - // - Tremod: 2017, CO2, CH4 and N2O in CO2-equivalent - // - HBEFA: 2020, CO2 (per country) - // German data uses Tremod. Other EU countries (and Switzerland) use HBEFA for car and bus, - // and Tremod for train and air (because HBEFA doesn't provide these). - // EU data is an average of the Tremod/HBEFA data for the countries listed; - // for this average the HBEFA data was used also in the German set (for car and bus). - var carbonDatasets = { - US: { - regionName: 'United States', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 267 / 1609, - BUS: 278 / 1609, - LIGHT_RAIL: 120 / 1609, - SUBWAY: 74 / 1609, - TRAM: 90 / 1609, - TRAIN: 92 / 1609, - AIR_OR_HSR: 217 / 1609, - }, - }, - EU: { - // Plain average of values for the countries below (using HBEFA for car and bus, Tremod for others) - regionName: 'European Union', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.14515, - BUS: 0.04751, - LIGHT_RAIL: 0.064, - SUBWAY: 0.064, - TRAM: 0.064, - TRAIN: 0.048, - AIR_OR_HSR: 0.201, - }, - }, - DE: { - regionName: 'Germany', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.139, // Tremod (passenger car) - BUS: 0.0535, // Tremod (average city/coach) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - FR: { - regionName: 'France', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13125, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04838, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - AT: { - regionName: 'Austria', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.14351, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04625, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - SE: { - regionName: 'Sweden', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13458, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04557, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - NO: { - regionName: 'Norway', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13265, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04185, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - CH: { - regionName: 'Switzerland', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.17638, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04866, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - }; - - var defaultCarbonDatasetCode = 'US'; - var currentCarbonDatasetCode = defaultCarbonDatasetCode; - - // we need to call the method from within a promise in initialize() - // and using this.setCurrentCarbonDatasetLocale doesn't seem to work - var setCurrentCarbonDatasetLocale = function (localeCode) { - for (var code in carbonDatasets) { - if (code == localeCode) { - currentCarbonDatasetCode = localeCode; - break; - } - } - }; - - this.loadCarbonDatasetLocale = function () { - return storageGet(CARBON_DATASET_KEY).then(function (localeCode) { - Logger.log( - 'CarbonDatasetHelper.loadCarbonDatasetLocale() obtained value from storage [' + - localeCode + - ']', - ); - if (!localeCode) { - localeCode = defaultCarbonDatasetCode; - Logger.log( - 'CarbonDatasetHelper.loadCarbonDatasetLocale() no value in storage, using [' + - localeCode + - '] instead', - ); - } - setCurrentCarbonDatasetLocale(localeCode); - }); - }; - - this.saveCurrentCarbonDatasetLocale = function (localeCode) { - setCurrentCarbonDatasetLocale(localeCode); - storageSet(CARBON_DATASET_KEY, currentCarbonDatasetCode); - Logger.log( - 'CarbonDatasetHelper.saveCurrentCarbonDatasetLocale() saved value [' + - currentCarbonDatasetCode + - '] to storage', - ); - }; - - this.getCarbonDatasetOptions = function () { - var options = []; - for (var code in carbonDatasets) { - options.push({ - text: code, //carbonDatasets[code].regionName, - value: code, - }); - } - return options; - }; - - this.getCurrentCarbonDatasetCode = function () { - return currentCarbonDatasetCode; - }; - - this.getCurrentCarbonDatasetFootprint = function () { - return carbonDatasets[currentCarbonDatasetCode].footprintData; - }; - }) - .service('METDatasetHelper', function () { - var standardMETs = { - WALKING: { - VERY_SLOW: { - range: [0, 2.0], - mets: 2.0, - }, - SLOW: { - range: [2.0, 2.5], - mets: 2.8, - }, - MODERATE_0: { - range: [2.5, 2.8], - mets: 3.0, - }, - MODERATE_1: { - range: [2.8, 3.2], - mets: 3.5, - }, - FAST: { - range: [3.2, 3.5], - mets: 4.3, - }, - VERY_FAST_0: { - range: [3.5, 4.0], - mets: 5.0, - }, - 'VERY_FAST_!': { - range: [4.0, 4.5], - mets: 6.0, - }, - VERY_VERY_FAST: { - range: [4.5, 5], - mets: 7.0, - }, - SUPER_FAST: { - range: [5, 6], - mets: 8.3, - }, - RUNNING: { - range: [6, Number.MAX_VALUE], - mets: 9.8, - }, - }, - BICYCLING: { - VERY_VERY_SLOW: { - range: [0, 5.5], - mets: 3.5, - }, - VERY_SLOW: { - range: [5.5, 10], - mets: 5.8, - }, - SLOW: { - range: [10, 12], - mets: 6.8, - }, - MODERATE: { - range: [12, 14], - mets: 8.0, - }, - FAST: { - range: [14, 16], - mets: 10.0, - }, - VERT_FAST: { - range: [16, 19], - mets: 12.0, - }, - RACING: { - range: [20, Number.MAX_VALUE], - mets: 15.8, - }, - }, - UNKNOWN: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - IN_VEHICLE: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - CAR: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - BUS: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - LIGHT_RAIL: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - TRAIN: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - TRAM: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - SUBWAY: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - AIR_OR_HSR: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - }; - this.getStandardMETs = function () { - return standardMETs; - }; - }) - .factory('CustomDatasetHelper', function (METDatasetHelper, Logger, $ionicPlatform) { - var cdh = {}; - - cdh.getCustomMETs = function () { - console.log('Getting custom METs', cdh.customMETs); - return cdh.customMETs; - }; - - cdh.getCustomFootprint = function () { - console.log('Getting custom footprint', cdh.customPerKmFootprint); - return cdh.customPerKmFootprint; - }; - - cdh.populateCustomMETs = function () { - let standardMETs = METDatasetHelper.getStandardMETs(); - let modeOptions = cdh.inputParams['MODE']; - let modeMETEntries = modeOptions.map((opt) => { - if (opt.met_equivalent) { - let currMET = standardMETs[opt.met_equivalent]; - return [opt.value, currMET]; - } else { - if (opt.met) { - let currMET = opt.met; - // if the user specifies a custom MET, they can't specify - // Number.MAX_VALUE since it is not valid JSON - // we assume that they specify -1 instead, and we will - // map -1 to Number.MAX_VALUE here by iterating over all the ranges - for (const rangeName in currMET) { - // console.log("Handling range ", rangeName); - currMET[rangeName].range = currMET[rangeName].range.map((i) => - i == -1 ? Number.MAX_VALUE : i, - ); - } - return [opt.value, currMET]; - } else { - console.warn( - 'Did not find either met_equivalent or met for ' + opt.value + ' ignoring entry', - ); - return undefined; - } - } - }); - cdh.customMETs = Object.fromEntries(modeMETEntries.filter((e) => angular.isDefined(e))); - console.log('After populating, custom METs = ', cdh.customMETs); - }; - - cdh.populateCustomFootprints = function () { - let modeOptions = cdh.inputParams['MODE']; - let modeCO2PerKm = modeOptions - .map((opt) => { - if (opt.range_limit_km) { - if (cdh.range_limited_motorized) { - Logger.displayError('Found two range limited motorized options', { - first: cdh.range_limited_motorized, - second: opt, - }); - } - cdh.range_limited_motorized = opt; - console.log('Found range limited motorized mode', cdh.range_limited_motorized); - } - if (angular.isDefined(opt.kgCo2PerKm)) { - return [opt.value, opt.kgCo2PerKm]; - } else { - return undefined; - } - }) - .filter((modeCO2) => angular.isDefined(modeCO2)); - cdh.customPerKmFootprint = Object.fromEntries(modeCO2PerKm); - console.log('After populating, custom perKmFootprint', cdh.customPerKmFootprint); - }; - - cdh.init = function (newConfig) { - try { - getLabelOptions(newConfig).then((inputParams) => { - console.log('Input params = ', inputParams); - cdh.inputParams = inputParams; - cdh.populateCustomMETs(); - cdh.populateCustomFootprints(); - }); - } catch (e) { - setTimeout(() => { - Logger.displayError( - 'Error in metrics-mappings while initializing custom dataset helper', - e, - ); - }, 1000); - } - }; - - $ionicPlatform.ready().then(function () { - getConfig().then((newConfig) => cdh.init(newConfig)); - }); - - return cdh; - }); diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 7c9bf3891..835f20a22 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -3,6 +3,11 @@ import { View } from 'react-native'; import { Card, Text, useTheme } from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; +import { + getFootprintForMetrics, + getHighestFootprint, + getHighestFootprintForDistance, +} from './footprintHelper'; import { formatDateRangeOfDays, parseDataFromMetrics, @@ -13,13 +18,11 @@ import { } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; -import { getAngularService } from '../angular-react-helper'; import ChangeIndicator from './ChangeIndicator'; import color from 'color'; type Props = { userMetrics: MetricsData; aggMetrics: MetricsData }; const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { - const FootprintHelper = getAngularService('FootprintHelper'); const { colors } = useTheme(); const { t } = useTranslation(); @@ -49,20 +52,12 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //setting up data to be displayed let graphRecords = []; - //set custon dataset, if the labels are custom - if (isCustomLabels(userThisWeekModeMap)) { - FootprintHelper.setUseCustomFootprint(); - } - //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) let userPrevWeek; if (userLastWeekSummaryMap[0]) { userPrevWeek = { - low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics( - userLastWeekSummaryMap, - FootprintHelper.getHighestFootprint(), - ), + low: getFootprintForMetrics(userLastWeekSummaryMap, 0), + high: getFootprintForMetrics(userLastWeekSummaryMap, getHighestFootprint()), }; graphRecords.push({ label: t('main-metrics.unlabeled'), @@ -78,11 +73,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //calculate low-high and format range for past week (7 days ago -> yesterday) let userPastWeek = { - low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics( - userThisWeekSummaryMap, - FootprintHelper.getHighestFootprint(), - ), + low: getFootprintForMetrics(userThisWeekSummaryMap, 0), + high: getFootprintForMetrics(userThisWeekSummaryMap, getHighestFootprint()), }; graphRecords.push({ label: t('main-metrics.unlabeled'), @@ -100,7 +92,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { } //calculate worst-case carbon footprint - let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); + let worstCarbon = getHighestFootprintForDistance(worstDistance); graphRecords.push({ label: t('main-metrics.labeled'), x: worstCarbon, @@ -138,11 +130,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { let groupRecords = []; let aggCarbon = { - low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), - high: FootprintHelper.getFootprintForMetrics( - aggCarbonData, - FootprintHelper.getHighestFootprint(), - ), + low: getFootprintForMetrics(aggCarbonData, 0), + high: getFootprintForMetrics(aggCarbonData, getHighestFootprint()), }; console.log('testing group past week', aggCarbon); groupRecords.push({ diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index 9f1b4490f..bf40c4a61 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -4,6 +4,11 @@ import { Card, Text, useTheme } from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; import { useTranslation } from 'react-i18next'; +import { + getFootprintForMetrics, + getHighestFootprint, + getHighestFootprintForDistance, +} from './footprintHelper'; import { formatDateRangeOfDays, parseDataFromMetrics, @@ -17,7 +22,6 @@ type Props = { userMetrics: MetricsData; aggMetrics: MetricsData }; const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { const { colors } = useTheme(); const { t } = useTranslation(); - const FootprintHelper = getAngularService('FootprintHelper'); const userText = useMemo(() => { if (userMetrics?.distance?.length > 0) { @@ -46,11 +50,8 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) if (userLastWeekSummaryMap[0]) { let userPrevWeek = { - low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics( - userLastWeekSummaryMap, - FootprintHelper.getHighestFootprint(), - ), + low: getFootprintForMetrics(userLastWeekSummaryMap, 0), + high: getFootprintForMetrics(userLastWeekSummaryMap, getHighestFootprint()), }; const label = `${t('main-metrics.prev-week')} (${formatDateRangeOfDays(lastWeekDistance)})`; if (userPrevWeek.low == userPrevWeek.high) @@ -64,11 +65,8 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { //calculate low-high and format range for past week (7 days ago -> yesterday) let userPastWeek = { - low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics( - userThisWeekSummaryMap, - FootprintHelper.getHighestFootprint(), - ), + low: getFootprintForMetrics(userThisWeekSummaryMap, 0), + high: getFootprintForMetrics(userThisWeekSummaryMap, getHighestFootprint()), }; const label = `${t('main-metrics.past-week')} (${formatDateRangeOfDays(thisWeekDistance)})`; if (userPastWeek.low == userPastWeek.high) @@ -80,7 +78,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { }); //calculate worst-case carbon footprint - let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); + let worstCarbon = getHighestFootprintForDistance(worstDistance); textList.push({ label: t('main-metrics.worst-case'), value: Math.round(worstCarbon) }); return textList; @@ -113,11 +111,8 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { let groupText = []; let aggCarbon = { - low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), - high: FootprintHelper.getFootprintForMetrics( - aggCarbonData, - FootprintHelper.getHighestFootprint(), - ), + low: getFootprintForMetrics(aggCarbonData, 0), + high: getFootprintForMetrics(aggCarbonData, getHighestFootprint()), }; console.log('testing group past week', aggCarbon); const label = t('main-metrics.average'); diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index bdc426e62..3c28a4df0 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState, useMemo } from 'react'; -import { getAngularService } from '../angular-react-helper'; import { View, ScrollView, useWindowDimensions } from 'react-native'; import { Appbar } from 'react-native-paper'; import NavBarButton from '../components/NavBarButton'; @@ -18,10 +17,16 @@ import CarbonTextCard from './CarbonTextCard'; import ActiveMinutesTableCard from './ActiveMinutesTableCard'; import { getAggregateData, getMetrics } from '../services/commHelper'; import { displayError, logDebug, logWarn } from '../plugin/logger'; +import useAppConfig from '../useAppConfig'; +import { ServerConnConfig } from '../types/appConfigTypes'; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; -async function fetchMetricsFromServer(type: 'user' | 'aggregate', dateRange: DateTime[]) { +async function fetchMetricsFromServer( + type: 'user' | 'aggregate', + dateRange: DateTime[], + serverConnConfig: ServerConnConfig, +) { const query = { freq: 'D', start_time: dateRange[0].toSeconds(), @@ -30,7 +35,7 @@ async function fetchMetricsFromServer(type: 'user' | 'aggregate', dateRange: Dat is_return_aggregate: type == 'aggregate', }; if (type == 'user') return getMetrics('timestamp', query); - return getAggregateData('result/metrics/timestamp', query); + return getAggregateData('result/metrics/timestamp', query, serverConnConfig); } function getLastTwoWeeksDtRange() { @@ -41,6 +46,7 @@ function getLastTwoWeeksDtRange() { } const MetricsTab = () => { + const appConfig = useAppConfig(); const { t } = useTranslation(); const { getFormattedSpeed, speedSuffix, getFormattedDistance, distanceSuffix } = useImperialConfig(); @@ -50,15 +56,16 @@ const MetricsTab = () => { const [userMetrics, setUserMetrics] = useState(null); useEffect(() => { + if (!appConfig?.server) return; loadMetricsForPopulation('user', dateRange); loadMetricsForPopulation('aggregate', dateRange); - }, [dateRange]); + }, [dateRange, appConfig?.server]); async function loadMetricsForPopulation(population: 'user' | 'aggregate', dateRange: DateTime[]) { try { logDebug(`MetricsTab: fetching metrics for population ${population}' in date range ${JSON.stringify(dateRange)}`); - const serverResponse = await fetchMetricsFromServer(population, dateRange); + const serverResponse = await fetchMetricsFromServer(population, dateRange, appConfig.server); logDebug('MetricsTab: received metrics: ' + JSON.stringify(serverResponse)); const metrics = {}; const dataKey = population == 'user' ? 'user_metrics' : 'aggregate_metrics'; diff --git a/www/js/metrics/customMetricsHelper.ts b/www/js/metrics/customMetricsHelper.ts new file mode 100644 index 000000000..317113327 --- /dev/null +++ b/www/js/metrics/customMetricsHelper.ts @@ -0,0 +1,112 @@ +import angular from 'angular'; +import { getLabelOptions } from '../survey/multilabel/confirmHelper'; +import { displayError, displayErrorMsg, logDebug, logWarn } from '../plugin/logger'; +import { standardMETs } from './metDataset'; +import { AppConfig } from '../types/appConfigTypes'; + +//variables to store values locally +let _customMETs; +let _customPerKmFootprint; +let _range_limited_motorized; +let _labelOptions; + +/** + * @function gets custom mets, must be initialized + * @returns the custom mets stored locally + */ +export function getCustomMETs() { + logDebug('Getting custom METs ' + JSON.stringify(_customMETs)); + return _customMETs; +} + +/** + * @function gets the custom footprint, must be initialized + * @returns custom footprint + */ +export function getCustomFootprint() { + logDebug('Getting custom footprint ' + JSON.stringify(_customPerKmFootprint)); + return _customPerKmFootprint; +} + +/** + * @function stores custom mets in local var + * needs _labelOptions, stored after gotten from config + */ +function populateCustomMETs() { + let modeOptions = _labelOptions['MODE']; + let modeMETEntries = modeOptions.map((opt) => { + if (opt.met_equivalent) { + let currMET = standardMETs[opt.met_equivalent]; + return [opt.value, currMET]; + } else { + if (opt.met) { + let currMET = opt.met; + // if the user specifies a custom MET, they can't specify + // Number.MAX_VALUE since it is not valid JSON + // we assume that they specify -1 instead, and we will + // map -1 to Number.MAX_VALUE here by iterating over all the ranges + for (const rangeName in currMET) { + // console.log("Handling range ", rangeName); + currMET[rangeName].range = currMET[rangeName].range.map((i) => + i == -1 ? Number.MAX_VALUE : i, + ); + } + return [opt.value, currMET]; + } else { + logWarn(`Did not find either met_equivalent or met for ${opt.value} ignoring entry`); + return undefined; + } + } + }); + _customMETs = Object.fromEntries(modeMETEntries.filter((e) => angular.isDefined(e))); + logDebug('After populating, custom METs = ' + JSON.stringify(_customMETs)); +} + +/** + * @function stores custom footprint in local var + * needs _inputParams which is stored after gotten from config + */ +function populateCustomFootprints() { + let modeOptions = _labelOptions['MODE']; + let modeCO2PerKm = modeOptions + .map((opt) => { + if (opt.range_limit_km) { + if (_range_limited_motorized) { + displayErrorMsg( + JSON.stringify({ first: _range_limited_motorized, second: opt }), + 'Found two range limited motorized options', + ); + } + _range_limited_motorized = opt; + logDebug(`Found range limited motorized mode - ${_range_limited_motorized}`); + } + if (angular.isDefined(opt.kgCo2PerKm)) { + return [opt.value, opt.kgCo2PerKm]; + } else { + return undefined; + } + }) + .filter((modeCO2) => angular.isDefined(modeCO2)); + _customPerKmFootprint = Object.fromEntries(modeCO2PerKm); + logDebug('After populating, custom perKmFootprint' + JSON.stringify(_customPerKmFootprint)); +} + +/** + * @function initializes the datasets based on configured label options + * calls popuplateCustomMETs and populateCustomFootprint + * @param newConfig the app config file + */ +export async function initCustomDatasetHelper(newConfig: AppConfig) { + try { + logDebug('initializing custom datasets with config' + newConfig); + const labelOptions = await getLabelOptions(newConfig); + logDebug('In custom metrics, label options = ' + JSON.stringify(labelOptions)); + _labelOptions = labelOptions; + populateCustomMETs(); + populateCustomFootprints(); + } catch (e) { + setTimeout(() => { + displayError(e, 'Error while initializing custom dataset helper'); + }, 1000); + } +} diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts new file mode 100644 index 000000000..24677feaf --- /dev/null +++ b/www/js/metrics/footprintHelper.ts @@ -0,0 +1,99 @@ +import { displayErrorMsg, logDebug, logWarn } from '../plugin/logger'; +import { getCustomFootprint } from './customMetricsHelper'; + +//variables for the highest footprint in the set and if using custom +let highestFootprint = 0; + +/** + * @function converts meters to kilometers + * @param {number} v value in meters to be converted + * @returns {number} converted value in km + */ +const mtokm = (v) => v / 1000; + +/** + * @function clears the stored highest footprint + */ +export function clearHighestFootprint() { + //need to clear for testing + highestFootprint = undefined; +} + +/** + * @function gets the footprint + * currently will only be custom, as all labels are "custom" + * fallback is json/label-options.json.sample, with MET and kgCO2 defined + * @returns the footprint or undefined + */ +function getFootprint() { + let footprint = getCustomFootprint(); + if (footprint) { + return footprint; + } else { + displayErrorMsg('failed to use custom labels', 'Error in Footprint Calculatons'); + return undefined; + } +} + +/** + * @function calculates footprint for given metrics + * @param {Array} userMetrics string mode + number distance in meters pairs + * ex: const custom_metrics = [ { key: 'walk', values: 3000 }, { key: 'bike', values: 6500 }, ]; + * @param {number} defaultIfMissing optional, carbon intensity if mode not in footprint + * @returns {number} the sum of carbon emissions for userMetrics given + */ +export function getFootprintForMetrics(userMetrics, defaultIfMissing = 0) { + const footprint = getFootprint(); + logDebug('getting footprint for ' + userMetrics + ' with ' + footprint); + let result = 0; + for (let i in userMetrics) { + let mode = userMetrics[i].key; + if (mode == 'ON_FOOT') { + mode = 'WALKING'; + } + + if (mode in footprint) { + result += footprint[mode] * mtokm(userMetrics[i].values); + } else if (mode == 'IN_VEHICLE') { + const sum = + footprint['CAR'] + + footprint['BUS'] + + footprint['LIGHT_RAIL'] + + footprint['TRAIN'] + + footprint['TRAM'] + + footprint['SUBWAY']; + result += (sum / 6) * mtokm(userMetrics[i].values); + } else { + logWarn( + `WARNING getFootprintFromMetrics() was requested for an unknown mode: ${mode} metrics JSON: ${JSON.stringify( + userMetrics, + )}`, + ); + result += defaultIfMissing * mtokm(userMetrics[i].values); + } + } + return result; +} + +/** + * @function gets highest co2 intensity in the footprint + * @returns {number} the highest co2 intensity in the footprint + */ +export function getHighestFootprint() { + if (!highestFootprint) { + const footprint = getFootprint(); + let footprintList = []; + for (let mode in footprint) { + footprintList.push(footprint[mode]); + } + highestFootprint = Math.max(...footprintList); + } + return highestFootprint; +} + +/** + * @function gets highest theoretical footprint for given distance + * @param {number} distance in meters to calculate max footprint + * @returns max footprint for given distance + */ +export const getHighestFootprintForDistance = (distance) => getHighestFootprint() * mtokm(distance); diff --git a/www/js/metrics/metDataset.ts b/www/js/metrics/metDataset.ts new file mode 100644 index 000000000..901c17ae6 --- /dev/null +++ b/www/js/metrics/metDataset.ts @@ -0,0 +1,128 @@ +export const standardMETs = { + WALKING: { + VERY_SLOW: { + range: [0, 2.0], + mets: 2.0, + }, + SLOW: { + range: [2.0, 2.5], + mets: 2.8, + }, + MODERATE_0: { + range: [2.5, 2.8], + mets: 3.0, + }, + MODERATE_1: { + range: [2.8, 3.2], + mets: 3.5, + }, + FAST: { + range: [3.2, 3.5], + mets: 4.3, + }, + VERY_FAST_0: { + range: [3.5, 4.0], + mets: 5.0, + }, + 'VERY_FAST_!': { + range: [4.0, 4.5], + mets: 6.0, + }, + VERY_VERY_FAST: { + range: [4.5, 5], + mets: 7.0, + }, + SUPER_FAST: { + range: [5, 6], + mets: 8.3, + }, + RUNNING: { + range: [6, Number.MAX_VALUE], + mets: 9.8, + }, + }, + BICYCLING: { + VERY_VERY_SLOW: { + range: [0, 5.5], + mets: 3.5, + }, + VERY_SLOW: { + range: [5.5, 10], + mets: 5.8, + }, + SLOW: { + range: [10, 12], + mets: 6.8, + }, + MODERATE: { + range: [12, 14], + mets: 8.0, + }, + FAST: { + range: [14, 16], + mets: 10.0, + }, + VERT_FAST: { + range: [16, 19], + mets: 12.0, + }, + RACING: { + range: [20, Number.MAX_VALUE], + mets: 15.8, + }, + }, + UNKNOWN: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + IN_VEHICLE: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + CAR: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + BUS: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + LIGHT_RAIL: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + TRAIN: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + TRAM: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + SUBWAY: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + AIR_OR_HSR: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, +}; diff --git a/www/js/metrics/metHelper.ts b/www/js/metrics/metHelper.ts new file mode 100644 index 000000000..25bcc2e7e --- /dev/null +++ b/www/js/metrics/metHelper.ts @@ -0,0 +1,59 @@ +import { logDebug, logWarn } from '../plugin/logger'; +import { getCustomMETs } from './customMetricsHelper'; +import { standardMETs } from './metDataset'; + +/** + * @function gets the METs object + * @returns {object} mets either custom or standard + */ +function getMETs() { + let custom_mets = getCustomMETs(); + if (custom_mets) { + return custom_mets; + } else { + return standardMETs; + } +} + +/** + * @function checks number agains bounds + * @param num the number to check + * @param min lower bound + * @param max upper bound + * @returns {boolean} if number is within given bounds + */ +const between = (num, min, max) => num >= min && num <= max; + +/** + * @function converts meters per second to miles per hour + * @param mps meters per second speed + * @returns speed in miles per hour + */ +const mpstomph = (mps) => 2.23694 * mps; + +/** + * @function gets met for a given mode and speed + * @param {string} mode of travel + * @param {number} speed of travel in meters per second + * @param {number} defaultIfMissing default MET if mode not in METs + * @returns + */ +export function getMet(mode, speed, defaultIfMissing) { + if (mode == 'ON_FOOT') { + logDebug("getMet() converted 'ON_FOOT' to 'WALKING'"); + mode = 'WALKING'; + } + let currentMETs = getMETs(); + if (!currentMETs[mode]) { + logWarn('getMet() Illegal mode: ' + mode); + return defaultIfMissing; //So the calorie sum does not break with wrong return type + } + for (let i in currentMETs[mode]) { + if (between(mpstomph(speed), currentMETs[mode][i].range[0], currentMETs[mode][i].range[1])) { + return currentMETs[mode][i].mets; + } else if (mpstomph(speed) < 0) { + logWarn('getMet() Negative speed: ' + mpstomph(speed)); + return 0; + } + } +} diff --git a/www/js/services/commHelper.ts b/www/js/services/commHelper.ts index 6dc71160a..206fc77c1 100644 --- a/www/js/services/commHelper.ts +++ b/www/js/services/commHelper.ts @@ -1,5 +1,6 @@ import { DateTime } from 'luxon'; import { logDebug } from '../plugin/logger'; +import { ServerConnConfig } from '../types/appConfigTypes'; /** * @param url URL endpoint for the request @@ -129,20 +130,17 @@ export function getMetrics(timeType: 'timestamp' | 'local_date', metricsQuery) { }); } -export function getAggregateData(path: string, data: any) { +export function getAggregateData(path: string, query, serverConnConfig: ServerConnConfig) { return new Promise((rs, rj) => { - const fullUrl = `${window['$rootScope'].connectUrl}/${path}`; - data['aggregate'] = true; + const fullUrl = `${serverConnConfig.connectUrl}/${path}`; + query['aggregate'] = true; - if (window['$rootScope'].aggregateAuth === 'no_auth') { - logDebug( - `getting aggregate data without user authentication from ${fullUrl} with arguments ${JSON.stringify( - data, - )}`, - ); + if (serverConnConfig.aggregate_call_auth == 'no_auth') { + logDebug(`getting aggregate data without user authentication from ${fullUrl} + with arguments ${JSON.stringify(query)}`); const options = { method: 'post', - data: data, + data: query, responseType: 'json', }; window['cordova'].plugin.http.sendRequest( @@ -156,14 +154,9 @@ export function getAggregateData(path: string, data: any) { }, ); } else { - logDebug( - `getting aggregate data with user authentication from ${fullUrl} with arguments ${JSON.stringify( - data, - )}`, - ); - const msgFiller = (message) => { - return Object.assign(message, data); - }; + logDebug(`getting aggregate data with user authentication from ${fullUrl} + with arguments ${JSON.stringify(query)}`); + const msgFiller = (message) => Object.assign(message, query); window['cordova'].plugins.BEMServerComm.pushGetJSON(`/${path}`, msgFiller, rs, rj); } }).catch((error) => { diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index 51674b0c3..f032f2f5a 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -37,7 +37,6 @@ export let inputDetails: InputDetails; export async function getLabelOptions(appConfigParam?) { if (appConfigParam) appConfig = appConfigParam; if (labelOptions) return labelOptions; - if (appConfig.label_options) { const labelOptionsJson = await fetchUrlCached(appConfig.label_options); logDebug(