diff --git a/.github/workflows/android-automated-sdk-install.yml b/.github/workflows/android-automated-sdk-install.yml
index f4275a98d..2ece9029f 100644
--- a/.github/workflows/android-automated-sdk-install.yml
+++ b/.github/workflows/android-automated-sdk-install.yml
@@ -6,10 +6,12 @@ on:
push:
paths:
- 'setup/prereq_android_sdk_install.sh'
+ - 'setup/android_sdk_packages'
- '.github/workflows/android-automated-sdk-install.yml'
pull_request:
paths:
- 'setup/prereq_android_sdk_install.sh'
+ - 'setup/android_sdk_packages'
- '.github/workflows/android-automated-sdk-install.yml'
schedule:
# * is a special character in YAML so you have to quote this string
@@ -64,7 +66,6 @@ jobs:
ls -al $ANDROID_SDK_ROOT
if [ ! -d $ANDROID_SDK_ROOT/emulator ]; then exit 1; fi
if [ ! -d $ANDROID_SDK_ROOT/build-tools ]; then exit 1; fi
- if [ ! -d $ANDROID_SDK_ROOT/patcher ]; then exit 1; fi
if [ ! -d $ANDROID_SDK_ROOT/extras ]; then exit 1; fi
if [ ! -d $ANDROID_SDK_ROOT/platforms ]; then exit 1; fi
if [ ! -d $ANDROID_SDK_ROOT/platform-tools ]; then exit 1; fi
diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml
index 25eb65317..ddc6b2ee0 100644
--- a/.github/workflows/android-build.yml
+++ b/.github/workflows/android-build.yml
@@ -22,20 +22,24 @@ jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
- runs-on: macos-latest
+ runs-on: macos-14
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
+ # Runs a single command using the runners shell
+ - name: Prints related environment variables so that we can know what to set
+ run: env | egrep "JAVA|PATH|ANDROID"
+
# Runs a single command using the runners shell
- name: Print the java and gradle versions
run: |
echo "Default java version"
java -version
echo "Setting to Java 11 instead"
- export JAVA_HOME=$JAVA_HOME_11_X64
+ export JAVA_HOME=$JAVA_HOME_17_arm64
java -version
echo "Checking gradle"
which gradle
@@ -49,13 +53,13 @@ jobs:
- name: Setup the cordova environment
shell: bash -l {0}
run: |
- export JAVA_HOME=$JAVA_HOME_11_X64
+ export JAVA_HOME=$JAVA_HOME_17_arm64
bash setup/setup_android_native.sh
- name: Check tool versions
shell: bash -l {0}
run: |
- export JAVA_HOME=$JAVA_HOME_11_X64
+ export JAVA_HOME=$JAVA_HOME_17_arm64
source setup/activate_native.sh
echo "cordova version"
npx cordova -version
@@ -73,7 +77,7 @@ jobs:
gradle -version
echo "Let's rerun the activation"
source setup/activate_native.sh
- export JAVA_HOME=$JAVA_HOME_11_X64
+ export JAVA_HOME=$JAVA_HOME_17_arm64
echo $PATH
which gradle
gradle --version
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index b0e94db22..dc1af47ac 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/code-coverage.yml
@@ -27,8 +27,9 @@ jobs:
npx jest
- name: Upload coverage reports to Codecov
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v4
with:
+ token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/coverage-final.json
flags: unit
fail_ci_if_error: ${{ github.repository == 'e-mission/e-mission-phone' }}
diff --git a/.github/workflows/ios-build.yml b/.github/workflows/ios-build.yml
index ad0ce2f01..695ed02de 100644
--- a/.github/workflows/ios-build.yml
+++ b/.github/workflows/ios-build.yml
@@ -22,7 +22,7 @@ jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
- runs-on: macos-latest
+ runs-on: macos-14
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
diff --git a/README.md b/README.md
index 121684e0a..d75b62f6f 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone
Updating the UI only
---
-[![osx-serve-install](https://github.com/e-mission/e-mission-phone/workflows/osx-serve-install/badge.svg)](https://github.com/e-mission/e-mission-phone/actions?query=workflow%3Aosx-serve-install)
+[![osx-serve-install](https://github.com/e-mission/e-mission-phone/workflows/osx-serve-install/badge.svg?branch=master&event=push)](https://github.com/e-mission/e-mission-phone/actions?query=workflow%3Aosx-serve-install)
If you want to make only UI changes, (as opposed to modifying the existing plugins, adding new plugins, etc), you can use the **new and improved** (as of June 2018) [e-mission dev app](https://github.com/e-mission/e-mission-devapp/) and install the most recent version from [releases](https://github.com/e-mission/e-mission-devapp/releases).
@@ -87,9 +87,9 @@ If you wish to connect to a different server, create your own config file accord
Updating the e-mission-\* plugins or adding new plugins
---
-[![osx-build-ios](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml)
-[![osx-build-android](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml)
-[![osx-android-prereq-sdk-install](https://github.com/e-mission/e-mission-phone/actions/workflows/android-automated-sdk-install.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-automated-sdk-install.yml)
+[![osx-build-ios](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml/badge.svg?branch=master&event=push)](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml?event-push)
+[![osx-build-android](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml/badge.svg?branch=master&event=push)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml?event=push)
+[![osx-android-prereq-sdk-install](https://github.com/e-mission/e-mission-phone/actions/workflows/android-automated-sdk-install.yml/badge.svg?branch=master&event=push)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-automated-sdk-install.yml?event=push)
Pre-requisites
---
diff --git a/config.cordovabuild.xml b/config.cordovabuild.xml
index d3d562802..3401b9b9b 100644
--- a/config.cordovabuild.xml
+++ b/config.cordovabuild.xml
@@ -36,6 +36,8 @@
+
+
diff --git a/hooks/before_build/ios/ios_change_deployment.js b/hooks/before_build/ios/ios_change_deployment.js
new file mode 100644
index 000000000..ad381162d
--- /dev/null
+++ b/hooks/before_build/ios/ios_change_deployment.js
@@ -0,0 +1,37 @@
+const fs = require('fs');
+const path = require('path');
+
+function findFilePathsByFilename(directory, filename) {
+ const files = fs.readdirSync(directory);
+ const filePaths = [];
+
+ for (const file of files) {
+ const filePath = path.join(directory, file);
+ const stats = fs.statSync(filePath);
+
+ if (stats.isDirectory()) {
+ // Recursively search in subdirectories
+ const subdirectoryFilePaths = findFilePathsByFilename(filePath, filename);
+ filePaths.push(...subdirectoryFilePaths);
+ } else if (stats.isFile() && file === filename) {
+ // If the file matches the filename, add its path to the result
+ filePaths.push(filePath);
+ }
+ }
+ return filePaths;
+}
+
+
+const paths1 = findFilePathsByFilename('.', 'project.pbxproj');
+const paths2 = findFilePathsByFilename('.', 'Pods.xcodeproj');
+const paths = paths1.concat(paths2)
+
+console.log('Apply patch to', paths);
+
+for (let path of paths) {
+ let content = fs.readFileSync(path, { encoding: 'utf-8' });
+ content = content.replace(/IPHONEOS_DEPLOYMENT_TARGET = [0-9]+.0;/g, 'IPHONEOS_DEPLOYMENT_TARGET = 13.0;');
+ fs.writeFileSync(path, content);
+}
+
+console.log('Done setting IPHONEOS_DEPLOYMENT_TARGET');
diff --git a/jest.config.js b/jest.config.js
index 6a1dcd42b..73521d81e 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -12,10 +12,15 @@ module.exports = {
"^.+\\.(ts|tsx|js|jsx)$": "babel-jest"
},
transformIgnorePatterns: [
- "node_modules/(?!((enketo-transformer/dist/enketo-transformer/web)|(jest-)?react-native(-.*)?|@react-native(-community)?)/)",
+ "node_modules/(?!((enketo-transformer/dist/enketo-transformer/web)|(jest-)?react-native(-.*)?|@react-native(-community)?|e-mission-common)/)"
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
moduleDirectories: ["node_modules", "src"],
globals: {"__DEV__": false},
collectCoverage: true,
+ collectCoverageFrom: [
+ "www/js/**/*.{ts,tsx,js,jsx}",
+ "!www/js/**/index.{ts,tsx,js,jsx}",
+ "!www/js/types/**/*.{ts,tsx,js,jsx}",
+ ],
};
diff --git a/package.cordovabuild.json b/package.cordovabuild.json
index 0ca7717bd..c1317a782 100644
--- a/package.cordovabuild.json
+++ b/package.cordovabuild.json
@@ -26,7 +26,7 @@
"@babel/preset-typescript": "^7.21.4",
"@ionic/cli": "6.20.8",
"@types/luxon": "^3.3.0",
- "@types/react": "^18.2.20",
+ "@types/react": "~18.2.0",
"babel-loader": "^9.1.2",
"babel-plugin-optional-require": "^0.3.1",
"concurrently": "^8.0.1",
@@ -121,13 +121,13 @@
"cordova-plugin-app-version": "0.1.14",
"cordova-plugin-customurlscheme": "5.0.2",
"cordova-plugin-device": "2.1.0",
- "cordova-plugin-em-datacollection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.8.3",
+ "cordova-plugin-em-datacollection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.8.5",
"cordova-plugin-em-opcodeauth": "git+https://github.com/e-mission/cordova-jwt-auth.git#v1.7.2",
"cordova-plugin-em-server-communication": "git+https://github.com/e-mission/cordova-server-communication.git#v1.2.6",
"cordova-plugin-em-serversync": "git+https://github.com/e-mission/cordova-server-sync.git#v1.3.2",
"cordova-plugin-em-settings": "git+https://github.com/e-mission/cordova-connection-settings.git#v1.2.3",
"cordova-plugin-em-unifiedlogger": "git+https://github.com/e-mission/cordova-unified-logger.git#v1.3.6",
- "cordova-plugin-em-usercache": "git+https://github.com/e-mission/cordova-usercache.git#v1.1.8",
+ "cordova-plugin-em-usercache": "git+https://github.com/e-mission/cordova-usercache.git#v1.1.9",
"cordova-plugin-email-composer": "git+https://github.com/katzer/cordova-plugin-email-composer.git#0.10.1",
"cordova-plugin-file": "8.0.0",
"cordova-plugin-inappbrowser": "5.0.0",
@@ -139,7 +139,7 @@
"cordova-custom-config": "^5.1.1",
"cordova-plugin-ibeacon": "git+https://github.com/louisg1337/cordova-plugin-ibeacon.git",
"core-js": "^2.5.7",
- "e-mission-common": "git+https://github.com/JGreenlee/e-mission-common.git#0.3.2",
+ "e-mission-common": "git+https://github.com/JGreenlee/e-mission-common.git#0.4.4",
"enketo-core": "^6.1.7",
"enketo-transformer": "^4.0.0",
"fast-xml-parser": "^4.2.2",
@@ -153,9 +153,9 @@
"npm": "^9.6.3",
"phonegap-plugin-barcodescanner": "git+https://github.com/phonegap/phonegap-plugin-barcodescanner#v8.1.0",
"prop-types": "^15.8.1",
- "react": "^18.2.*",
+ "react": "~18.2.0",
"react-chartjs-2": "^5.2.0",
- "react-dom": "^18.2.*",
+ "react-dom": "~18.2.0",
"react-i18next": "^13.5.0",
"react-native-paper": "^5.11.0",
"react-native-paper-dates": "^0.18.12",
diff --git a/package.serve.json b/package.serve.json
index bf39fd68d..b610d6121 100644
--- a/package.serve.json
+++ b/package.serve.json
@@ -26,7 +26,7 @@
"@ionic/cli": "6.20.8",
"@testing-library/react-native": "^12.3.0",
"@types/luxon": "^3.3.0",
- "@types/react": "^18.2.20",
+ "@types/react": "~18.2.0",
"babel-jest": "^29.7.0",
"babel-loader": "^9.1.2",
"babel-plugin-optional-require": "^0.3.1",
@@ -40,6 +40,7 @@
"jest-environment-jsdom": "^29.7.0",
"phonegap": "9.0.0+cordova.9.0.0",
"process": "^0.11.10",
+ "react-test-renderer": "~18.2.0",
"sass": "^1.62.1",
"sass-loader": "^13.3.1",
"style-loader": "^3.3.3",
@@ -64,7 +65,7 @@
"chartjs-adapter-luxon": "^1.3.1",
"chartjs-plugin-annotation": "^3.0.1",
"core-js": "^2.5.7",
- "e-mission-common": "git+https://github.com/JGreenlee/e-mission-common.git#0.3.2",
+ "e-mission-common": "git+https://github.com/JGreenlee/e-mission-common.git#0.4.4",
"enketo-core": "^6.1.7",
"enketo-transformer": "^4.0.0",
"fast-xml-parser": "^4.2.2",
@@ -77,9 +78,9 @@
"luxon": "^3.3.0",
"npm": "^9.6.3",
"prop-types": "^15.8.1",
- "react": "^18.2.*",
+ "react": "~18.2.0",
"react-chartjs-2": "^5.2.0",
- "react-dom": "^18.2.*",
+ "react-dom": "~18.2.0",
"react-i18next": "^13.5.0",
"react-native-paper": "^5.11.0",
"react-native-paper-dates": "^0.18.12",
diff --git a/www/__tests__/confirmHelper.test.ts b/www/__tests__/confirmHelper.test.ts
index 12657d589..6642b6ed4 100644
--- a/www/__tests__/confirmHelper.test.ts
+++ b/www/__tests__/confirmHelper.test.ts
@@ -45,6 +45,16 @@ const fakeDefaultLabelOptions = {
},
},
};
+const fakeInputs = {
+ MODE: [
+ { data: { label: 'walk', start_ts: 1245, end_ts: 5678 } },
+ { data: { label: 'bike', start_ts: 1245, end_ts: 5678 } },
+ ],
+ PURPOSE: [
+ { data: { label: 'home', start_ts: 1245, end_ts: 5678 } },
+ { data: { label: 'work', start_ts: 1245, end_ts: 5678 } },
+ ],
+};
jest.mock('../js/services/commHelper', () => ({
...jest.requireActual('../js/services/commHelper'),
@@ -62,8 +72,8 @@ describe('confirmHelper', () => {
it('returns base labelInputDetails for a labelUserInput which does not have mode of study', () => {
const fakeLabelUserInput = {
- MODE: fakeDefaultLabelOptions.MODE[1],
- PURPOSE: fakeDefaultLabelOptions.PURPOSE[0],
+ MODE: fakeInputs.MODE[1],
+ PURPOSE: fakeInputs.PURPOSE[0],
};
const labelInputDetails = labelInputDetailsForTrip(
fakeLabelUserInput,
@@ -74,8 +84,8 @@ describe('confirmHelper', () => {
it('returns full labelInputDetails for a labelUserInput which has the mode of study', () => {
const fakeLabelUserInput = {
- MODE: fakeDefaultLabelOptions.MODE[0], // 'walk' is mode of study
- PURPOSE: fakeDefaultLabelOptions.PURPOSE[0],
+ MODE: fakeInputs.MODE[0], // 'walk' is mode of study
+ PURPOSE: fakeInputs.PURPOSE[0],
};
const labelInputDetails = labelInputDetailsForTrip(
fakeLabelUserInput,
diff --git a/www/__tests__/inputMatcher.test.ts b/www/__tests__/inputMatcher.test.ts
index 062951b35..9ea4d9b02 100644
--- a/www/__tests__/inputMatcher.test.ts
+++ b/www/__tests__/inputMatcher.test.ts
@@ -1,6 +1,6 @@
import { mockBEMUserCache } from '../__mocks__/cordovaMocks';
import { mockLogger } from '../__mocks__/globalMocks';
-import { unprocessedLabels, updateLocalUnprocessedInputs } from '../js/diary/timelineHelper';
+import { updateLocalUnprocessedInputs } from '../js/diary/timelineHelper';
import * as logger from '../js/plugin/logger';
import { EnketoUserInputEntry } from '../js/survey/enketo/enketoHelper';
import {
@@ -376,9 +376,9 @@ describe('mapInputsToTimelineEntries on an ENKETO configuration', () => {
user_input: {
trip_user_input: {
data: {
- name: 'TripConfirmSurvey',
+ name: 'MyCustomSurvey',
version: 1,
- xmlResponse: '',
+ xmlResponse: '',
start_ts: 1000,
end_ts: 3000,
},
@@ -417,6 +417,12 @@ describe('mapInputsToTimelineEntries on an ENKETO configuration', () => {
],
},
] as any as TimelineEntry[];
+
+ // reset local unprocessed inputs to ensure MUTLILABEL inputs don't leak into ENKETO tests
+ beforeAll(async () => {
+ await updateLocalUnprocessedInputs({ start_ts: 1000, end_ts: 5000 }, fakeConfigEnketo);
+ });
+
it('creates a map that has the processed responses and notes', () => {
const [labelMap, notesMap] = mapInputsToTimelineEntries(
timelineEntriesEnketo,
@@ -424,8 +430,8 @@ describe('mapInputsToTimelineEntries on an ENKETO configuration', () => {
);
expect(labelMap).toMatchObject({
trip1: {
- SURVEY: {
- data: { xmlResponse: '' },
+ MyCustomSurvey: {
+ data: { xmlResponse: '' },
},
},
});
@@ -460,12 +466,12 @@ describe('mapInputsToTimelineEntries on an ENKETO configuration', () => {
expect(labelMap).toMatchObject({
trip1: {
- SURVEY: {
- data: { xmlResponse: '' },
+ MyCustomSurvey: {
+ data: { xmlResponse: '' },
},
},
trip2: {
- SURVEY: {
+ TripConfirmSurvey: {
data: { xmlResponse: '' },
},
},
diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts
index aafe13926..c1c130272 100644
--- a/www/__tests__/timelineHelper.test.ts
+++ b/www/__tests__/timelineHelper.test.ts
@@ -165,10 +165,10 @@ describe('unprocessedLabels, unprocessedNotes', () => {
// update unprocessed inputs and check that the trip survey response shows up in unprocessedLabels
await updateAllUnprocessedInputs({ start_ts: 4, end_ts: 6 }, mockTLH.mockConfigEnketo);
- expect(unprocessedLabels['SURVEY'][0].data).toEqual(tripSurveyResponse);
+ expect(unprocessedLabels['TripConfirmSurvey'][0].data).toEqual(tripSurveyResponse);
// the second response is ignored for now because we haven't enabled place_user_input yet
// so the length is only 1
- expect(unprocessedLabels['SURVEY'].length).toEqual(1);
+ expect(unprocessedLabels['TripConfirmSurvey'].length).toEqual(1);
});
it('has some trip- and place- level additions after they were just recorded', async () => {
@@ -291,7 +291,7 @@ jest.mock('../js/services/unifiedDataLoader', () => ({
}));
it('works when there are no unprocessed trips...', async () => {
- expect(readUnprocessedTrips(-1, -1, {} as any)).resolves.toEqual([]);
+ expect(readUnprocessedTrips(-1, -1, {} as any, {} as any)).resolves.toEqual([]);
});
it('works when there are one or more unprocessed trips...', async () => {
@@ -299,6 +299,7 @@ it('works when there are one or more unprocessed trips...', async () => {
mockTLH.fakeStartTsOne,
mockTLH.fakeEndTsOne,
{} as any,
+ {} as any,
);
expect(testValueOne.length).toEqual(1);
expect(testValueOne[0]).toEqual(
diff --git a/www/i18n/en.json b/www/i18n/en.json
index 9a8b6bb61..2834219af 100644
--- a/www/i18n/en.json
+++ b/www/i18n/en.json
@@ -136,7 +136,6 @@
"choose-mode": "Mode",
"choose-replaced-mode": "Replaces",
"choose-purpose": "Purpose",
- "choose-survey": "Add Trip Details",
"select-mode-scroll": "Mode (scroll for more)",
"select-replaced-mode-scroll": "Replaces (scroll for more)",
"select-purpose-scroll": "Purpose (scroll for more)",
diff --git a/www/js/TimelineContext.ts b/www/js/TimelineContext.ts
index 7dc1e190b..bbef214bd 100644
--- a/www/js/TimelineContext.ts
+++ b/www/js/TimelineContext.ts
@@ -20,14 +20,11 @@ import {
isoDateRangeToTsRange,
} from './diary/timelineHelper';
import { getPipelineRangeTs } from './services/commHelper';
-import {
- getNotDeletedCandidates,
- mapBleScansToTimelineEntries,
- mapInputsToTimelineEntries,
-} from './survey/inputMatcher';
+import { getNotDeletedCandidates, mapInputsToTimelineEntries } from './survey/inputMatcher';
import { publish } from './customEventHandler';
import { EnketoUserInputEntry } from './survey/enketo/enketoHelper';
import { VehicleIdentity } from './types/appConfigTypes';
+import { primarySectionForTrip } from './diary/diaryHelper';
// initial query range is the past 7 days, including today
const today = DateTime.now().toISODate().substring(0, 10);
@@ -71,7 +68,6 @@ export const useTimelineContext = (): ContextProps => {
const [timelineIsLoading, setTimelineIsLoading] = useState('replace');
const [timelineLabelMap, setTimelineLabelMap] = useState(null);
const [timelineNotesMap, setTimelineNotesMap] = useState(null);
- const [timelineBleMap, setTimelineBleMap] = useState(null);
const [refreshTime, setRefreshTime] = useState(null);
// initialization, once the appConfig is loaded
@@ -139,12 +135,6 @@ export const useTimelineContext = (): ContextProps => {
);
setTimelineLabelMap(newTimelineLabelMap);
setTimelineNotesMap(newTimelineNotesMap);
-
- if (appConfig.vehicle_identities?.length) {
- const newTimelineBleMap = mapBleScansToTimelineEntries(allEntries, appConfig);
- setTimelineBleMap(newTimelineBleMap);
- }
-
publish('applyLabelTabFilters', {
timelineMap,
timelineLabelMap: newTimelineLabelMap,
@@ -161,7 +151,7 @@ export const useTimelineContext = (): ContextProps => {
unprocessedNotes = ${JSON.stringify(unprocessedNotes)}`);
if (appConfig.vehicle_identities?.length) {
await updateUnprocessedBleScans({
- start_ts: pipelineRange.start_ts,
+ start_ts: pipelineRange.end_ts,
end_ts: Date.now() / 1000,
});
logDebug(`Timeline: After updating unprocessedBleScans,
@@ -259,6 +249,7 @@ export const useTimelineContext = (): ContextProps => {
readUnprocessedPromise = readUnprocessedTrips(
Math.max(pipelineRange.end_ts, startTs),
endTs,
+ appConfig,
lastProcessedTrip,
);
} else {
@@ -303,8 +294,8 @@ export const useTimelineContext = (): ContextProps => {
* @returns Confirmed mode, which could be a vehicle identity as determined by Bluetooth scans,
* or the label option from a user-given 'MODE' label, or undefined if neither exists.
*/
- const confirmedModeFor = (tlEntry: TimelineEntry) =>
- timelineBleMap?.[tlEntry._id.$oid] || labelFor(tlEntry, 'MODE');
+ const confirmedModeFor = (tlEntry: CompositeTrip) =>
+ primarySectionForTrip(tlEntry)?.ble_sensed_mode || labelFor(tlEntry, 'MODE');
function addUserInputToEntry(oid: string, userInput: any, inputType: 'label' | 'note') {
const tlEntry = timelineMap?.get(oid);
@@ -312,9 +303,9 @@ export const useTimelineContext = (): ContextProps => {
return displayErrorMsg('Item with oid: ' + oid + ' not found in timeline');
const nowTs = new Date().getTime() / 1000; // epoch seconds
if (inputType == 'label') {
- const newLabels = {};
- for (const [inputType, labelValue] of Object.entries(userInput)) {
- newLabels[inputType] = { data: labelValue, metadata: nowTs };
+ const newLabels: UserInputMap = {};
+ for (const [inputType, labelValue] of Object.entries(userInput)) {
+ newLabels[inputType] = { data: labelValue, metadata: { write_ts: nowTs } as any };
}
logDebug('Timeline: newLabels = ' + JSON.stringify(newLabels));
const newTimelineLabelMap: TimelineLabelMap = {
@@ -369,13 +360,13 @@ export const useTimelineContext = (): ContextProps => {
};
export type UserInputMap = {
- /* if the key here is 'SURVEY', we are in the ENKETO configuration, meaning the user input
- value will have the raw 'xmlResponse' string */
- SURVEY?: EnketoUserInputEntry;
-} & {
- /* all other keys, (e.g. 'MODE', 'PURPOSE') are from the MULTILABEL configuration
- and will have the 'label' string but no 'xmlResponse' string */
+ /* If keys are 'MODE', 'PURPOSE', 'REPLACED_MODE', this is the MULTILABEL configuration.
+ Values are entries that have a 'label' value in their 'data' */
[k in MultilabelKey]?: UserInputEntry;
+} & {
+ /* Otherwise we are in the ENKETO configuration, and keys are names of surveys.
+ Values are entries that have an 'xmlResponse' value in their 'data' */
+ [k: string]: EnketoUserInputEntry | undefined;
};
export type TimelineMap = Map; // Todo: update to reflect unpacked trips (origin_Key, etc)
diff --git a/www/js/bluetooth/BluetoothScanPage.tsx b/www/js/bluetooth/BluetoothScanPage.tsx
index a4e5bda9e..b50bf5283 100644
--- a/www/js/bluetooth/BluetoothScanPage.tsx
+++ b/www/js/bluetooth/BluetoothScanPage.tsx
@@ -97,7 +97,6 @@ const BluetoothScanPage = ({ ...props }: any) => {
in_range: status,
},
}));
- let { monitorResult: _, in_range: _, ...noResultDevice } = sampleBLEDevices[uuid];
window['cordova']?.plugins?.BEMDataCollection.mockBLEObjects(
status ? 'REGION_ENTER' : 'REGION_EXIT',
uuid,
@@ -118,16 +117,6 @@ const BluetoothScanPage = ({ ...props }: any) => {
rangeResult: result,
},
}));
- // we don't want to exclude monitorResult and rangeResult from the values
- // that we save because they are the current or previous result, just
- // in a different format
- // https://stackoverflow.com/a/34710102
- let {
- monitorResult: _,
- rangeResult: _,
- in_range: _,
- ...noResultDevice
- } = sampleBLEDevices[uuid];
let parsedResult = JSON.parse(result);
parsedResult.beacons.forEach((beacon) => {
window['cordova']?.plugins?.BEMDataCollection.mockBLEObjects(
diff --git a/www/js/config/dynamicConfig.ts b/www/js/config/dynamicConfig.ts
index d9b9f3235..5843af3d2 100644
--- a/www/js/config/dynamicConfig.ts
+++ b/www/js/config/dynamicConfig.ts
@@ -92,11 +92,11 @@ function cacheResourcesFromConfig(config: AppConfig) {
if (config.survey_info?.surveys) {
Object.values(config.survey_info.surveys).forEach((survey) => {
if (!survey?.['formPath']) throw new Error(i18next.t('config.survey-missing-formpath'));
- fetchUrlCached(survey['formPath']);
+ fetchUrlCached(survey['formPath'], { cache: 'reload' });
});
}
if (config.label_options) {
- fetchUrlCached(config.label_options);
+ fetchUrlCached(config.label_options, { cache: 'reload' });
}
}
diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx
index 65526dc8a..f619fbf4e 100644
--- a/www/js/diary/LabelTab.tsx
+++ b/www/js/diary/LabelTab.tsx
@@ -76,7 +76,7 @@ const LabelTab = () => {
const labels = labelMap[e._id.$oid];
for (let labelValue of Object.values(labels || [])) {
logDebug(`LabelTab filtering: labelValue = ${JSON.stringify(labelValue)}`);
- if (labelValue?.metadata?.write_ts > cutoffTs) {
+ if (labelValue?.metadata?.write_ts || 0 > cutoffTs) {
logDebug('LabelTab filtering: entry has recent user input, keeping');
return true;
}
diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts
index f02797fff..12495742d 100644
--- a/www/js/diary/diaryHelper.ts
+++ b/www/js/diary/diaryHelper.ts
@@ -192,6 +192,15 @@ export function getFormattedSectionProperties(trip: CompositeTrip, imperialConfi
}));
}
+/**
+ * @param trip A composite trip object
+ * @return the primary section of the trip, i.e. the section with the greatest distance
+ */
+export function primarySectionForTrip(trip: CompositeTrip) {
+ if (!trip.sections?.length) return undefined;
+ return trip.sections.reduce((prev, curr) => (prev.distance > curr.distance ? prev : curr));
+}
+
export function getLocalTimeString(dt?: LocalDt) {
if (!dt) return;
const dateTime = DateTime.fromObject({
diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts
index 094dd0c58..d5c1e507b 100644
--- a/www/js/diary/timelineHelper.ts
+++ b/www/js/diary/timelineHelper.ts
@@ -17,12 +17,18 @@ import {
BluetoothBleData,
SectionData,
CompositeTripLocation,
+ SectionSummary,
} from '../types/diaryTypes';
import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper';
import { LabelOptions } from '../types/labelTypes';
-import { EnketoUserInputEntry, filterByNameAndVersion } from '../survey/enketo/enketoHelper';
+import {
+ EnketoUserInputEntry,
+ filterByNameAndVersion,
+ resolveSurveyButtonConfig,
+} from '../survey/enketo/enketoHelper';
import { AppConfig } from '../types/appConfigTypes';
import { Point, Feature } from 'geojson';
+import { ble_matching } from 'e-mission-common';
const cachedGeojsons: Map = new Map();
@@ -89,7 +95,8 @@ export function compositeTrips2TimelineMap(ctList: Array, unpackPlaces?: bo
}
/* 'LABELS' are 1:1 - each trip or place has a single label for each label type
- (e.g. 'MODE' and 'PURPOSE' for MULTILABEL configuration, or 'SURVEY' for ENKETO configuration) */
+ (e.g. 'MODE' and 'PURPOSE' for MULTILABEL configuration, or the name of the survey
+ for ENKETO configuration) */
export let unprocessedLabels: { [key: string]: UserInputEntry[] } = {};
/* 'NOTES' are 1:n - each trip or place can have any number of notes */
export let unprocessedNotes: EnketoUserInputEntry[] = [];
@@ -113,10 +120,14 @@ function updateUnprocessedInputs(
const labelResults = comboResults.slice(0, labelsPromises.length);
const notesResults = comboResults.slice(labelsPromises.length).flat(2);
// fill in the unprocessedLabels object with the labels we just read
+ unprocessedLabels = {};
labelResults.forEach((r, i) => {
if (appConfig.survey_info?.['trip-labels'] == 'ENKETO') {
- const filtered = filterByNameAndVersion('TripConfirmSurvey', r, appConfig);
- unprocessedLabels['SURVEY'] = filtered as UserInputEntry[];
+ const tripSurveys = resolveSurveyButtonConfig(appConfig, 'trip-label');
+ tripSurveys.forEach((survey) => {
+ const filtered = filterByNameAndVersion(survey.surveyName, r, appConfig);
+ unprocessedLabels[survey.surveyName] = filtered as UserInputEntry[];
+ });
} else {
unprocessedLabels[getLabelInputs()[i]] = r;
}
@@ -182,7 +193,7 @@ export async function updateUnprocessedBleScans(queryRange: TimestampRange) {
endTs: queryRange.end_ts,
};
const getMethod = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval;
- getUnifiedDataForInterval('background/bluetooth_ble', tq, getMethod).then(
+ await getUnifiedDataForInterval('background/bluetooth_ble', tq, getMethod).then(
(bleScans: BEMData[]) => {
logDebug(`Read ${bleScans.length} BLE scans`);
unprocessedBleScans = bleScans;
@@ -307,10 +318,25 @@ const dateTime2localdate = (currtime: DateTime, tz: string) => ({
second: currtime.second,
});
+/* Compute a section summary, which is really simple for unprocessed trips because they are
+ always assumed to be unimodal.
+/* maybe unify with eaum.get_section_summary on e-mission-server at some point */
+const getSectionSummaryForUnprocessed = (section: SectionData, modeProp): SectionSummary => {
+ const baseMode = section[modeProp] || 'UNKNOWN';
+ return {
+ count: { [baseMode]: 1 },
+ distance: { [baseMode]: section.distance },
+ duration: { [baseMode]: section.duration },
+ };
+};
+
/**
* @description Given an array of location points, creates an UnprocessedTrip object.
*/
-function points2UnprocessedTrip(locationPoints: Array>): UnprocessedTrip {
+function points2UnprocessedTrip(
+ locationPoints: Array>,
+ appConfig: AppConfig,
+): UnprocessedTrip {
const startPoint = locationPoints[0];
const endPoint = locationPoints[locationPoints.length - 1];
const tripAndSectionId = `unprocessed_${startPoint.data.ts}_${endPoint.data.ts}`;
@@ -370,6 +396,12 @@ function points2UnprocessedTrip(locationPoints: Array>
origin_key: 'UNPROCESSED_section',
sensed_mode: 4, // MotionTypes.UNKNOWN (4)
sensed_mode_str: 'UNKNOWN',
+ ble_sensed_mode: ble_matching.get_ble_sensed_vehicle_for_section(
+ unprocessedBleScans,
+ baseProps.start_ts,
+ baseProps.end_ts,
+ appConfig,
+ ),
trip_id: { $oid: tripAndSectionId },
};
@@ -378,6 +410,9 @@ function points2UnprocessedTrip(locationPoints: Array>
...baseProps,
_id: { $oid: tripAndSectionId },
additions: [],
+ ble_sensed_summary: getSectionSummaryForUnprocessed(singleSection, 'ble_sensed_mode'),
+ cleaned_section_summary: getSectionSummaryForUnprocessed(singleSection, 'sensed_mode_str'),
+ inferred_section_summary: getSectionSummaryForUnprocessed(singleSection, 'sensed_mode_str'),
confidence_threshold: 0,
expectation: { to_label: true },
inferred_labels: [],
@@ -396,7 +431,10 @@ const tsEntrySort = (e1: BEMData, e2: BEMData): Promise {
+function tripTransitions2UnprocessedTrip(
+ trip: Array,
+ appConfig: AppConfig,
+): Promise {
const tripStartTransition = trip[0];
const tripEndTransition = trip[1];
const tq = {
@@ -438,7 +476,7 @@ function tripTransitions2UnprocessedTrip(trip: Array): Promise {
logDebug(JSON.stringify(trip, null, 2));
});
- const tripFillPromises = tripsList.map(tripTransitions2UnprocessedTrip);
+ const tripFillPromises = tripsList.map((t) =>
+ tripTransitions2UnprocessedTrip(t, appConfig),
+ );
return Promise.all(tripFillPromises).then(
(rawTripObjs: (UnprocessedTrip | undefined)[]) => {
// Now we need to link up the trips. linking unprocessed trips
diff --git a/www/js/services/commHelper.ts b/www/js/services/commHelper.ts
index 26dce8056..ec2ee9d97 100644
--- a/www/js/services/commHelper.ts
+++ b/www/js/services/commHelper.ts
@@ -5,17 +5,18 @@ import { TimestampRange } from '../types/diaryTypes';
/**
* @param url URL endpoint for the request
+ * @param fetchOpts (optional) options for the fetch request. If 'cache' is set to 'reload', the cache will be ignored
* @returns Promise of the fetched response (as text) or cached text from local storage
*/
-export async function fetchUrlCached(url) {
+export async function fetchUrlCached(url: string, fetchOpts?: RequestInit) {
const stored = localStorage.getItem(url);
- if (stored) {
+ if (stored && fetchOpts?.cache != 'reload') {
logDebug(`fetchUrlCached: found cached data for url ${url}, returning`);
return Promise.resolve(stored);
}
try {
- logDebug(`fetchUrlCached: found no cached data for url ${url}, fetching`);
- const response = await fetch(url);
+ logDebug(`fetchUrlCached: cache had ${stored} for url ${url}, not using; fetching`);
+ const response = await fetch(url, fetchOpts);
const text = await response.text();
localStorage.setItem(url, text);
logDebug(`fetchUrlCached: fetched data for url ${url}, returning`);
diff --git a/www/js/survey/enketo/UserInputButton.tsx b/www/js/survey/enketo/UserInputButton.tsx
index dbf85ee15..c4d28eee8 100644
--- a/www/js/survey/enketo/UserInputButton.tsx
+++ b/www/js/survey/enketo/UserInputButton.tsx
@@ -18,6 +18,8 @@ import TimelineContext from '../../TimelineContext';
import useAppConfig from '../../useAppConfig';
import { getSurveyForTimelineEntry } from './conditionalSurveys';
import useDerivedProperties from '../../diary/useDerivedProperties';
+import { resolveSurveyButtonConfig } from './enketoHelper';
+import { SurveyButtonConfig } from '../../types/appConfigTypes';
type Props = {
timelineEntry: any;
@@ -33,27 +35,25 @@ const UserInputButton = ({ timelineEntry }: Props) => {
const derivedTripProps = useDerivedProperties(timelineEntry);
// which survey will this button launch?
- const [surveyName, notFilledInLabel] = useMemo(() => {
- const tripLabelConfig = appConfig?.survey_info?.buttons?.['trip-label'];
- if (!tripLabelConfig) {
- // config doesn't specify; use default
- return ['TripConfirmSurvey', t('diary.choose-survey')];
- }
- // config lists one or more surveys; find which one to use
- const s = getSurveyForTimelineEntry(tripLabelConfig, timelineEntry, derivedTripProps);
- const lang = i18n.resolvedLanguage || 'en';
- return [s?.surveyName, s?.['not-filled-in-label'][lang]];
+ const survey = useMemo(() => {
+ if (!appConfig) return null; // no config loaded yet; show blank for now
+ const possibleSurveysForButton = resolveSurveyButtonConfig(appConfig, 'trip-label');
+ // if there is only one survey, no need to check further
+ if (possibleSurveysForButton.length == 1) return possibleSurveysForButton[0];
+ // config lists one or more surveys; find which one to use for this timeline entry
+ return getSurveyForTimelineEntry(possibleSurveysForButton, timelineEntry, derivedTripProps);
}, [appConfig, timelineEntry, i18n.resolvedLanguage]);
- // the label resolved from the survey response, or null if there is no response yet
- const responseLabel = useMemo(
- () => userInputFor(timelineEntry)?.['SURVEY']?.data.label || undefined,
- [userInputFor(timelineEntry)?.['SURVEY']?.data.label],
- );
+ // the label resolved from the survey response, or undefined if there is no response yet
+ const responseLabel = useMemo(() => {
+ if (!survey) return undefined;
+ return userInputFor(timelineEntry)?.[survey.surveyName]?.data.label || undefined;
+ }, [survey, userInputFor(timelineEntry)?.[survey?.surveyName || '']?.data.label]);
function launchUserInputSurvey() {
+ if (!survey) return displayErrorMsg('UserInputButton: no survey to launch');
logDebug('UserInputButton: About to launch survey');
- const prevResponse = userInputFor(timelineEntry)?.['SURVEY'];
+ const prevResponse = userInputFor(timelineEntry)?.[survey.surveyName];
if (prevResponse?.data?.xmlResponse) {
setPrevSurveyResponse(prevResponse.data.xmlResponse);
}
@@ -64,27 +64,27 @@ const UserInputButton = ({ timelineEntry }: Props) => {
if (result) {
logDebug(`UserInputButton: response was saved, about to addUserInputToEntry;
result = ${JSON.stringify(result)}`);
- addUserInputToEntry(timelineEntry._id.$oid, { SURVEY: result }, 'label');
+ addUserInputToEntry(timelineEntry._id.$oid, { [result.name]: result }, 'label');
} else {
displayErrorMsg('UserInputButton: response was not saved, result=', result);
}
}
- if (!surveyName) return <>>; // no survey to launch
+ if (!survey) return <>>; // no survey to launch
return (
<>
launchUserInputSurvey()}>
- {responseLabel || notFilledInLabel}
+ {responseLabel || survey['not-filled-in-label'][i18n.resolvedLanguage || 'en']}
setModalVisible(false)}
onResponseSaved={onResponseSaved}
- surveyName={surveyName}
+ surveyName={survey.surveyName}
opts={{ timelineEntry, prefilledSurveyResponse: prevSurveyResponse }}
/>
>
diff --git a/www/js/survey/enketo/conditionalSurveys.ts b/www/js/survey/enketo/conditionalSurveys.ts
index 607b49431..a96ee2de8 100644
--- a/www/js/survey/enketo/conditionalSurveys.ts
+++ b/www/js/survey/enketo/conditionalSurveys.ts
@@ -29,26 +29,22 @@ const scopedEval = (script: string, scope: { [k: string]: any }) =>
// the first survey in the list that passes its condition will be returned
export function getSurveyForTimelineEntry(
- tripLabelConfig: SurveyButtonConfig | SurveyButtonConfig[],
+ possibleSurveys: SurveyButtonConfig[],
tlEntry: TimelineEntry,
derivedProperties: DerivedProperties,
) {
- // if only one survey is given, just return it
- if (!(tripLabelConfig instanceof Array)) return tripLabelConfig;
- if (tripLabelConfig.length == 1) return tripLabelConfig[0];
- // else we have an array of possible surveys, we need to find which one to use for this entry
- for (let surveyConfig of tripLabelConfig) {
- if (!surveyConfig.showsIf) return surveyConfig; // survey shows unconditionally
+ for (let survey of possibleSurveys) {
+ if (!survey.showsIf) return survey; // survey shows unconditionally
const scope = {
...tlEntry,
...derivedProperties,
...conditionalSurveyFunctions,
};
try {
- const evalResult = scopedEval(surveyConfig.showsIf, scope);
- if (evalResult) return surveyConfig;
+ const evalResult = scopedEval(survey.showsIf, scope);
+ if (evalResult) return survey;
} catch (e) {
- displayError(e, `Error evaluating survey condition "${surveyConfig.showsIf}"`);
+ displayError(e, `Error evaluating survey condition "${survey.showsIf}"`);
}
}
// TODO if none of the surveys passed conditions?? should we return null, throw error, or return a default?
diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts
index 2df2d3b2d..e90354856 100644
--- a/www/js/survey/enketo/enketoHelper.ts
+++ b/www/js/survey/enketo/enketoHelper.ts
@@ -8,7 +8,7 @@ import { getConfig } from '../../config/dynamicConfig';
import { DateTime } from 'luxon';
import { fetchUrlCached } from '../../services/commHelper';
import { getUnifiedDataForInterval } from '../../services/unifiedDataLoader';
-import { AppConfig, EnketoSurveyConfig } from '../../types/appConfigTypes';
+import { AppConfig, EnketoSurveyConfig, SurveyButtonConfig } from '../../types/appConfigTypes';
import {
CompositeTrip,
ConfirmedPlace,
@@ -315,6 +315,32 @@ export function loadPreviousResponseForSurvey(dataKey: string) {
);
}
+/**
+ * @description Returns an array of surveys that could be prompted for one button in the UI (trip label, trip notes, place label, or place notes)
+ * (If multiple are returned, they will show conditionally in the UI based on their `showsIf` field)
+ * Includes backwards compats for app config fields that didn't use to exist
+ */
+export function resolveSurveyButtonConfig(
+ config: AppConfig,
+ button: 'trip-label' | 'trip-notes' | 'place-label' | 'place-notes',
+): SurveyButtonConfig[] {
+ const buttonConfig = config.survey_info.buttons?.[button];
+ // backwards compat: default to the trip confirm survey if this button isn't configured
+ if (!buttonConfig) {
+ return [
+ {
+ surveyName: 'TripConfirmSurvey',
+ 'not-filled-in-label': {
+ en: 'Add Trip Details',
+ es: 'Agregar detalles del viaje',
+ lo: 'ເພີ່ມລາຍລະອຽດການເດີນທາງ',
+ },
+ },
+ ];
+ }
+ return buttonConfig instanceof Array ? buttonConfig : [buttonConfig];
+}
+
export async function fetchSurvey(url: string) {
const responseText = await fetchUrlCached(url);
if (!responseText) return;
diff --git a/www/js/survey/enketo/infinite_scroll_filters.ts b/www/js/survey/enketo/infinite_scroll_filters.ts
index d4b281713..512b272c4 100644
--- a/www/js/survey/enketo/infinite_scroll_filters.ts
+++ b/www/js/survey/enketo/infinite_scroll_filters.ts
@@ -8,7 +8,8 @@
import i18next from 'i18next';
-const unlabeledCheck = (trip, userInputForTrip) => !userInputForTrip?.['SURVEY'];
+const unlabeledCheck = (trip, userInputForTrip) =>
+ !userInputForTrip || !Object.values(userInputForTrip).some((input) => input);
const TO_LABEL = {
key: 'to_label',
diff --git a/www/js/survey/inputMatcher.ts b/www/js/survey/inputMatcher.ts
index 5bc7c87da..604c533b2 100644
--- a/www/js/survey/inputMatcher.ts
+++ b/www/js/survey/inputMatcher.ts
@@ -18,7 +18,7 @@ import {
inputType2retKey,
removeManualPrefix,
} from './multilabel/confirmHelper';
-import { TimelineLabelMap, TimelineNotesMap } from '../TimelineContext';
+import { TimelineLabelMap, TimelineNotesMap, UserInputMap } from '../TimelineContext';
import { MultilabelKey } from '../types/labelTypes';
import { EnketoUserInputEntry } from './enketo/enketoHelper';
import { AppConfig } from '../types/appConfigTypes';
@@ -216,9 +216,8 @@ export function getAdditionsForTimelineEntry(
return [];
}
- // get additions that have not been deleted and filter out additions that do not start within the bounds of the timeline entry
- const notDeleted = getNotDeletedCandidates(additionsList);
- const matchingAdditions = notDeleted.filter((ui) =>
+ // filter out additions that do not start within the bounds of the timeline entry
+ const matchingAdditions = additionsList.filter((ui) =>
validUserInputForTimelineEntry(entry, nextEntry, ui, logsEnabled),
);
@@ -280,16 +279,16 @@ export function mapInputsToTimelineEntries(
allEntries.forEach((tlEntry, i) => {
const nextEntry = i + 1 < allEntries.length ? allEntries[i + 1] : null;
if (appConfig?.survey_info?.['trip-labels'] == 'ENKETO') {
- // ENKETO configuration: just look for the 'SURVEY' key in the unprocessedInputs
+ // ENKETO configuration: consider reponses from all surveys in unprocessedLabels
const userInputForTrip = getUserInputForTimelineEntry(
tlEntry,
nextEntry,
- unprocessedLabels['SURVEY'],
+ Object.values(unprocessedLabels).flat(1),
) as EnketoUserInputEntry;
if (userInputForTrip) {
- timelineLabelMap[tlEntry._id.$oid] = { SURVEY: userInputForTrip };
+ timelineLabelMap[tlEntry._id.$oid] = { [userInputForTrip.data.name]: userInputForTrip };
} else {
- let processedSurveyResponse;
+ let processedSurveyResponse: EnketoUserInputEntry | undefined;
for (const dataKey of keysForLabelInputs(appConfig)) {
const key = removeManualPrefix(dataKey);
if (tlEntry.user_input?.[key]) {
@@ -297,12 +296,16 @@ export function mapInputsToTimelineEntries(
break;
}
}
- timelineLabelMap[tlEntry._id.$oid] = { SURVEY: processedSurveyResponse };
+ if (processedSurveyResponse) {
+ timelineLabelMap[tlEntry._id.$oid] = {
+ [processedSurveyResponse.data.name]: processedSurveyResponse,
+ };
+ }
}
} else {
// MULTILABEL configuration: use the label inputs from the labelOptions to determine which
// keys to look for in the unprocessedInputs
- const labelsForTrip: { [k: string]: UserInputEntry | undefined } = {};
+ const labelsForTrip: UserInputMap = {};
Object.keys(getLabelInputDetails(appConfig)).forEach((label: MultilabelKey) => {
// Check unprocessed labels first since they are more recent
const userInputForTrip = getUserInputForTimelineEntry(
diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts
index 8ba6acca4..bc3a2b717 100644
--- a/www/js/survey/multilabel/confirmHelper.ts
+++ b/www/js/survey/multilabel/confirmHelper.ts
@@ -91,12 +91,12 @@ export function getLabelInputDetails(appConfigParam?) {
export function labelInputDetailsForTrip(userInputForTrip, appConfigParam?) {
if (appConfigParam) appConfig = appConfigParam;
if (appConfig.intro.mode_studied) {
- if (userInputForTrip?.['MODE']?.value == appConfig.intro.mode_studied) {
- logDebug(`Found trip labeled with mode of study, ${appConfig.intro.mode_studied}.
+ if (userInputForTrip?.['MODE']?.data?.label == appConfig.intro.mode_studied) {
+ logDebug(`Found trip labeled with ${userInputForTrip?.['MODE']?.data?.label}, mode of study = ${appConfig.intro.mode_studied}.
Needs REPLACED_MODE`);
return getLabelInputDetails();
} else {
- logDebug(`Found trip not labeled with mode of study, ${appConfig.intro.mode_studied}.
+ logDebug(`Found trip labeled with ${userInputForTrip?.['MODE']?.data?.label}, not labeled with mode of study = ${appConfig.intro.mode_studied}.
Doesn't need REPLACED_MODE`);
return baseLabelInputDetails;
}
diff --git a/www/js/types/appConfigTypes.ts b/www/js/types/appConfigTypes.ts
index d5a15fe4a..e58b679f5 100644
--- a/www/js/types/appConfigTypes.ts
+++ b/www/js/types/appConfigTypes.ts
@@ -54,7 +54,7 @@ export type SurveyButtonConfig = {
'not-filled-in-label': {
[lang: string]: string;
};
- showsIf: string; // a JS expression that evaluates to a boolean
+ showsIf?: string; // a JS expression that evaluates to a boolean
};
export type SurveyButtonsConfig = {
[k in 'trip-label' | 'trip-notes' | 'place-label' | 'place-notes']:
diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts
index 9757e95cf..53b618be0 100644
--- a/www/js/types/diaryTypes.ts
+++ b/www/js/types/diaryTypes.ts
@@ -4,6 +4,7 @@
import { BaseModeKey, MotionTypeKey } from '../diary/diaryHelper';
import useDerivedProperties from '../diary/useDerivedProperties';
+import { VehicleIdentity } from './appConfigTypes';
import { MultilabelKey } from './labelTypes';
import { BEMData, LocalDt } from './serverData';
import { FeatureCollection, Feature, Geometry, Point, Position } from 'geojson';
@@ -58,6 +59,8 @@ export type CompositeTripLocation = {
export type UnprocessedTrip = {
_id: ObjectId;
additions: []; // unprocessed trips won't have any matched processed inputs, so this is always empty
+ ble_sensed_summary: SectionSummary;
+ cleaned_section_summary: SectionSummary;
confidence_threshold: number;
distance: number;
duration: number;
@@ -67,6 +70,7 @@ export type UnprocessedTrip = {
end_ts: number;
expectation: { to_label: true }; // unprocessed trips are always expected to be labeled
inferred_labels: []; // unprocessed trips won't have inferred labels
+ inferred_section_summary: SectionSummary;
key: 'UNPROCESSED_trip';
locations?: CompositeTripLocation[];
origin_key: 'UNPROCESSED_trip';
@@ -85,6 +89,7 @@ export type UnprocessedTrip = {
export type CompositeTrip = {
_id: ObjectId;
additions: UserInputEntry[];
+ ble_sensed_summary: SectionSummary;
cleaned_section_summary: SectionSummary;
cleaned_trip: ObjectId;
confidence_threshold: number;
@@ -202,6 +207,7 @@ export type SectionData = {
key: string;
origin_key: string;
trip_id: ObjectId;
+ ble_sensed_mode: VehicleIdentity;
sensed_mode: number;
source: string; // e.x., "SmoothedHighConfidenceMotion"
start_ts: number; // Unix
diff --git a/www/js/usePermissionStatus.ts b/www/js/usePermissionStatus.ts
index 8c3128bea..81c4abf54 100644
--- a/www/js/usePermissionStatus.ts
+++ b/www/js/usePermissionStatus.ts
@@ -291,6 +291,41 @@ const usePermissionStatus = () => {
setCheckList(tempChecks);
}
+ function setupAndroidBluetoothChecks() {
+ if (window['device'].version.split('.')[0] >= 10) {
+ let fixPerms = () => {
+ logDebug('fix and refresh bluetooth permissions');
+ return checkOrFix(
+ bluetoothPermissionsCheck,
+ window['cordova'].plugins.BEMDataCollection.fixBluetoothPermissions,
+ true,
+ ).then((error) => {
+ if (error) {
+ bluetoothPermissionsCheck.desc = error;
+ }
+ });
+ };
+ let checkPerms = () => {
+ logDebug('fix and refresh bluetooth permissions');
+ return checkOrFix(
+ bluetoothPermissionsCheck,
+ window['cordova'].plugins.BEMDataCollection.isValidBluetoothPermissions,
+ false,
+ );
+ };
+
+ let bluetoothPermissionsCheck = {
+ name: 'Bluetooth scan permission',
+ desc: 'Scan for BLE beacons to automatically match trips to vehicles',
+ fix: fixPerms,
+ refresh: checkPerms,
+ };
+ let tempChecks = checkList;
+ tempChecks.push(bluetoothPermissionsCheck);
+ setCheckList(tempChecks);
+ }
+ }
+
function setupAndroidNotificationChecks() {
let fixPerms = () => {
logDebug('fix and refresh notification permissions');
@@ -372,7 +407,11 @@ const usePermissionStatus = () => {
refresh: checkBatteryOpt,
};
let tempChecks = checkList;
- tempChecks.push(unusedAppsUnrestrictedCheck, ignoreBatteryOptCheck);
+ if (appConfig.tracking?.bluetooth_only) {
+ tempChecks.push(ignoreBatteryOptCheck);
+ } else {
+ tempChecks.push(unusedAppsUnrestrictedCheck, ignoreBatteryOptCheck);
+ }
setCheckList(tempChecks);
}
@@ -409,6 +448,9 @@ const usePermissionStatus = () => {
if (window['device'].platform.toLowerCase() == 'android') {
setupAndroidLocChecks();
setupAndroidFitnessChecks();
+ if (appConfig.tracking?.bluetooth_only) {
+ setupAndroidBluetoothChecks();
+ }
setupAndroidNotificationChecks();
setupAndroidBackgroundRestrictionChecks();
} else if (window['device'].platform.toLowerCase() == 'ios') {