From 1260e76523c0de2be505e167aa823d27e4632c93 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 7 May 2024 13:40:49 -0400 Subject: [PATCH 1/4] fill in new BLE fields & update types `ble_sensed_mode` and `ble_sensed_summary` were added to sections and confirmed trips in https://github.com/e-mission/e-mission-server/pull/965. We are going to need `ble_sensed_mode` to determine vehicle info. The section summaries (`ble_sensed_summary`, `cleaned_section_summary` and `inferred_section_summary`) are not used in unprocessed trips currently, but I am adding them so there is less of a gap between composite trips and unprocessed trips --- package.cordovabuild.json | 1 + package.serve.json | 1 + www/__tests__/timelineHelper.test.ts | 3 ++- www/js/diary/timelineHelper.ts | 40 +++++++++++++++++++++++++--- www/js/types/diaryTypes.ts | 6 +++++ 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 282e3693d..f5d9b6d58 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -139,6 +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", "enketo-core": "^6.1.7", "enketo-transformer": "^4.0.0", "fast-xml-parser": "^4.2.2", diff --git a/package.serve.json b/package.serve.json index f6f5c2ae3..af33531ae 100644 --- a/package.serve.json +++ b/package.serve.json @@ -65,6 +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", "enketo-core": "^6.1.7", "enketo-transformer": "^4.0.0", "fast-xml-parser": "^4.2.2", diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index aafe13926..c40262aae 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -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/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 6e82c0fbf..f850f0074 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -17,12 +17,14 @@ 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 { AppConfig } from '../types/appConfigTypes'; import { Point, Feature } from 'geojson'; +import { ble_matching } from 'e-mission-common'; const cachedGeojsons: Map = new Map(); @@ -306,10 +308,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}`; @@ -369,6 +386,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 }, }; @@ -377,6 +400,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: [], @@ -395,7 +421,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 = { @@ -437,7 +466,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/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 From 71303475864c64635389ab607c48228ba6489bbd Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 7 May 2024 14:05:54 -0400 Subject: [PATCH 2/4] use only unprocessed BLE scans in label tab Since we have matching on the server, processed sections will already have BLE sensed modes filled in. We no longer need to query for all BLE scans; only unprocessed ones (ie newer than pipeline end ts). Then while we are constructing unprocessed trips, the list of unprocessed BLE scans will be used to determine BLE sensed modes. We no longer need timelineBleMap and won't treat BLE scans like user inputs anymore. For 'confirmedMode', instead of using timelineBleMap, we will use the ble_sensed_mode of the primary section. New simple function in diaryHelper to deternmine the primary section. --- www/js/diary/LabelTab.tsx | 28 +++++++++++----------------- www/js/diary/LabelTabContext.ts | 4 ++-- www/js/diary/diaryHelper.ts | 9 +++++++++ 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 938a861f3..1e9dd3521 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -29,11 +29,7 @@ import { getLabelOptions, labelOptionByValue } from '../survey/multilabel/confir import { displayError, displayErrorMsg, logDebug, logWarn } from '../plugin/logger'; import { useTheme } from 'react-native-paper'; import { getPipelineRangeTs } from '../services/commHelper'; -import { - getNotDeletedCandidates, - mapBleScansToTimelineEntries, - mapInputsToTimelineEntries, -} from '../survey/inputMatcher'; +import { getNotDeletedCandidates, mapInputsToTimelineEntries } from '../survey/inputMatcher'; import { configuredFilters as multilabelConfiguredFilters } from '../survey/multilabel/infinite_scroll_filters'; import { configuredFilters as enketoConfiguredFilters } from '../survey/enketo/infinite_scroll_filters'; import LabelTabContext, { @@ -45,6 +41,7 @@ import LabelTabContext, { import { readAllCompositeTrips, readUnprocessedTrips } from './timelineHelper'; import { LabelOptions, MultilabelKey } from '../types/labelTypes'; import { CompositeTrip, TimelineEntry, TimestampRange, UserInputEntry } from '../types/diaryTypes'; +import { primarySectionForTrip } from './diaryHelper'; let showPlaces; const ONE_DAY = 24 * 60 * 60; // seconds @@ -63,7 +60,6 @@ const LabelTab = () => { const [timelineMap, setTimelineMap] = useState(null); const [timelineLabelMap, setTimelineLabelMap] = useState(null); const [timelineNotesMap, setTimelineNotesMap] = useState(null); - const [timelineBleMap, setTimelineBleMap] = useState(null); const [displayedEntries, setDisplayedEntries] = useState(null); const [refreshTime, setRefreshTime] = useState(null); const [isLoading, setIsLoading] = useState('replace'); @@ -105,15 +101,8 @@ const LabelTab = () => { allEntries, appConfig, ); - setTimelineLabelMap(newTimelineLabelMap); setTimelineNotesMap(newTimelineNotesMap); - - if (appConfig.vehicle_identities?.length) { - const newTimelineBleMap = mapBleScansToTimelineEntries(allEntries, appConfig); - setTimelineBleMap(newTimelineBleMap); - } - applyFilters(timelineMap, newTimelineLabelMap); } catch (e) { displayError(e, t('errors.while-updating-timeline')); @@ -171,7 +160,7 @@ const LabelTab = () => { 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(`LabelTab: After updating unprocessedBleScans, @@ -301,7 +290,12 @@ const LabelTab = () => { .reverse() .find((trip) => trip.origin_key.includes('trip')) as CompositeTrip; } - readUnprocessedPromise = readUnprocessedTrips(pipelineRange.end_ts, nowTs, lastProcessedTrip); + readUnprocessedPromise = readUnprocessedTrips( + pipelineRange.end_ts, + nowTs, + appConfig, + lastProcessedTrip, + ); } else { readUnprocessedPromise = Promise.resolve([]); } @@ -336,8 +330,8 @@ const LabelTab = () => { * @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); diff --git a/www/js/diary/LabelTabContext.ts b/www/js/diary/LabelTabContext.ts index 791cb4cd5..2feb0cce7 100644 --- a/www/js/diary/LabelTabContext.ts +++ b/www/js/diary/LabelTabContext.ts @@ -1,5 +1,5 @@ import { createContext } from 'react'; -import { TimelineEntry, TimestampRange, UserInputEntry } from '../types/diaryTypes'; +import { CompositeTrip, TimelineEntry, TimestampRange, UserInputEntry } from '../types/diaryTypes'; import { LabelOption, LabelOptions, MultilabelKey } from '../types/labelTypes'; import { EnketoUserInputEntry } from '../survey/enketo/enketoHelper'; import { VehicleIdentity } from '../types/appConfigTypes'; @@ -35,7 +35,7 @@ type ContextProps = { userInputFor: (tlEntry: TimelineEntry) => UserInputMap | undefined; notesFor: (tlEntry: TimelineEntry) => UserInputEntry[] | undefined; labelFor: (tlEntry: TimelineEntry, labelType: MultilabelKey) => LabelOption | undefined; - confirmedModeFor: (tlEntry: TimelineEntry) => VehicleIdentity | LabelOption | undefined; + confirmedModeFor: (tlEntry: CompositeTrip) => VehicleIdentity | LabelOption | undefined; addUserInputToEntry: (oid: string, userInput: any, inputType: 'label' | 'note') => void; displayedEntries: TimelineEntry[] | null; filterInputs: LabelTabFilter[]; 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({ From 48eca9de7f44969c4c7dc00acab0552e6bcc0703 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 7 May 2024 14:16:13 -0400 Subject: [PATCH 3/4] use e-mission-common 0.4.4 I was just using the latest / master branch to test, but it should really be locked to a release --- package.cordovabuild.json | 2 +- package.serve.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.cordovabuild.json b/package.cordovabuild.json index f5d9b6d58..c1317a782 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -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", + "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", diff --git a/package.serve.json b/package.serve.json index af33531ae..b610d6121 100644 --- a/package.serve.json +++ b/package.serve.json @@ -65,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", + "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", From 2b0f4af73a90e9f3c0caa9e923be190c8517d83c Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 8 May 2024 16:56:29 -0400 Subject: [PATCH 4/4] don't exclude e-mission-common from transform before jest tests --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 939834e51..73521d81e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,7 +12,7 @@ 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"],