From 5f53e5301e39ea53717f6ea3ac4b030b053c03fe Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 2 Apr 2024 23:53:12 -0400 Subject: [PATCH 1/4] include 'sections' in UnprocessedTrips To be consistent with processed trips, and so we can use them in the same way, unprocessed trips should have the 'sections' property. Unprocessed trips / 'draft' trips are assumed to be unimodal, so we can fairly easily reconstruct a section spanning the entirety of the trip and include that as the only section. In doing so, I modified points2TripProps. I realized that many of the props will actually be the same between the trip and the section. So I did a bit of refactoring; to construct the unprocessed trip I first construct baseProps (which are used in both the section and the trip); then I construct the section; then finally I construct the trip in the return statement. Something else I noticed is that all the trip props are computed in points2TripProps except the start_loc and end_loc which were computed in the outer function transitionTrip2TripObj. Since we already have variables for the start and end coordinates in the points2TripProps function, it seems more logical to handle start_loc and end_loc there with the rest of the trip props. Then we can declare the return type of points2TripProps as UnprocessedTrip; it has all the required properties now. --- www/js/diary/timelineHelper.ts | 62 +++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index f140f1750..81c32956a 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -14,6 +14,7 @@ import { TimestampRange, CompositeTrip, UnprocessedTrip, + SectionData, } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; import { LabelOptions } from '../types/labelTypes'; @@ -288,7 +289,7 @@ const dateTime2localdate = (currtime: DateTime, tz: string) => ({ second: currtime.second, }); -function points2TripProps(locationPoints: Array>) { +function points2TripProps(locationPoints: Array>): UnprocessedTrip { const startPoint = locationPoints[0]; const endPoint = locationPoints[locationPoints.length - 1]; const tripAndSectionId = `unprocessed_${startPoint.data.ts}_${endPoint.data.ts}`; @@ -318,24 +319,51 @@ function points2TripProps(locationPoints: Array>) { speed: speeds[i], })); - return { - _id: { $oid: tripAndSectionId }, - key: 'UNPROCESSED_trip', - origin_key: 'UNPROCESSED_trip', - additions: [], - confidence_threshold: 0, + // baseProps: these are the properties that are the same between the trip and its section + const baseProps = { distance: dists.reduce((a, b) => a + b, 0), duration: endPoint.data.ts - startPoint.data.ts, end_fmt_time: endTime.toISO() || displayErrorMsg('end_fmt_time: invalid DateTime') || '', + end_loc: { + type: 'Point', + coordinates: [endPoint.data.longitude, endPoint.data.latitude], + } as Point, end_local_dt: dateTime2localdate(endTime, endPoint.metadata.time_zone), end_ts: endPoint.data.ts, - expectation: { to_label: true }, - inferred_labels: [], - locations: locations, source: 'unprocessed', start_fmt_time: startTime.toISO() || displayErrorMsg('start_fmt_time: invalid DateTime') || '', + start_loc: { + type: 'Point', + coordinates: [startPoint.data.longitude, startPoint.data.latitude], + } as Point, start_local_dt: dateTime2localdate(startTime, startPoint.metadata.time_zone), start_ts: startPoint.data.ts, + }; + + // section: baseProps + some properties that are unique to the section + const singleSection: SectionData = { + ...baseProps, + _id: { $oid: `unprocessed_section_${tripAndSectionId}` }, + cleaned_section: { $oid: `unprocessed_section_${tripAndSectionId}` }, + key: 'UNPROCESSED_section', + origin_key: 'UNPROCESSED_section', + sensed_mode: 4, // MotionTypes.UNKNOWN (4) + sensed_mode_str: 'UNKNOWN', + trip_id: { $oid: tripAndSectionId }, + }; + + // the complete UnprocessedTrip: baseProps + properties that are unique to the trip, including the section + return { + ...baseProps, + _id: { $oid: tripAndSectionId }, + additions: [], + confidence_threshold: 0, + expectation: { to_label: true }, + inferred_labels: [], + key: 'UNPROCESSED_trip', + locations: locations, + origin_key: 'UNPROCESSED_trip', + sections: [singleSection], user_input: {}, }; } @@ -386,19 +414,7 @@ function transitionTrip2TripObj(trip: Array): Promise Date: Wed, 3 Apr 2024 00:39:36 -0400 Subject: [PATCH 2/4] update types relating to `UnprocessedTrip`s The main thing I was doing in this commit was adding 'sections' to the type signature of `UnprocessedTrip`. But I also noticed some other things amiss. `UnprocessedTrip` was missing some other properties; end_ts and start_fmt_time "LocationCoord" is not needed as it's the same as `Point` from 'geojson'. `SectionData` was missing a bunch of properties. Once those are filled in, the 'sections' property in `CompositeTrip` and `UnprocessedTrip` can be typed as `SectionData[]` --- www/js/types/diaryTypes.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 75c43b2d6..12102477d 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -45,11 +45,6 @@ export type TripTransition = { ts: number; }; -export type LocationCoord = { - type: string; // e.x., "Point" - coordinates: [number, number]; -}; - type CompTripLocations = { loc: { coordinates: number[]; // e.g. [1, 2.3] @@ -68,12 +63,15 @@ export type UnprocessedTrip = { end_fmt_time: string; end_loc: Point; end_local_dt: LocalDt; + end_ts: number; expectation: any; // TODO "{to_label: boolean}" inferred_labels: any[]; // TODO key: string; locations?: CompTripLocations[]; origin_key: string; // e.x., UNPROCESSED_trip + sections: SectionData[]; source: string; + start_fmt_time: string; start_local_dt: LocalDt; start_ts: number; start_loc: Point; @@ -107,7 +105,7 @@ export type CompositeTrip = { locations: any[]; // TODO origin_key: string; raw_trip: ObjectId; - sections: any[]; // TODO + sections: SectionData[]; source: string; start_confirmed_place: BEMData; start_fmt_time: string; @@ -188,23 +186,25 @@ export type Location = { latitude: number; fmt_time: string; // ISO mode: number; - loc: LocationCoord; + loc: Point; ts: number; // Unix altitude: number; distance: number; }; -// used in readAllCompositeTrips export type SectionData = { + _id: ObjectId; end_ts: number; // Unix time, e.x. 1696352498.804 - end_loc: LocationCoord; + end_loc: Point; start_fmt_time: string; // ISO time end_fmt_time: string; + key: string; + origin_key: string; trip_id: ObjectId; sensed_mode: number; source: string; // e.x., "SmoothedHighConfidenceMotion" start_ts: number; // Unix - start_loc: LocationCoord; + start_loc: Point; cleaned_section: ObjectId; start_local_dt: LocalDt; end_local_dt: LocalDt; @@ -213,7 +213,7 @@ export type SectionData = { distance: number; }; -// used in timelineHelper's `transitionTrip2TripObj` +// used in timelineHelper's `transitionTrip2UnprocessedTrip` export type FilteredLocation = { accuracy: number; altitude: number; From 12db91a30c1f3d0c1b852a2efe1e4512c34d9a5f Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 3 Apr 2024 00:47:12 -0400 Subject: [PATCH 3/4] rename functions related to unprocessed trips After a bunch of refactoring, I think these functions could use a naming update. I always found this part of the codebase fairly opaque anyway and I think it can now be made more easily comprehensible. 'points2TripProps' now returns a full UnprocessedTrip object so it is renamed 'points2UnprocessedTrip' And 'transitions2Trips' is renamed 'transitions2TripTransitions' because it doesn't really return trip objects; it returns pairs of transitions that represent trips. To follow suit, 'transitionTrip2TripObj' is renamed 'tripTransitions2UnprocessedTrip'. Added a bit of JSDoc to help clarify what these functions do. --- www/js/diary/timelineHelper.ts | 52 ++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 81c32956a..006cc0788 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -289,7 +289,10 @@ const dateTime2localdate = (currtime: DateTime, tz: string) => ({ second: currtime.second, }); -function points2TripProps(locationPoints: Array>): UnprocessedTrip { +/** + * @description Given an array of location points, creates an UnprocessedTrip object. + */ +function points2UnprocessedTrip(locationPoints: Array>): UnprocessedTrip { const startPoint = locationPoints[0]; const endPoint = locationPoints[locationPoints.length - 1]; const tripAndSectionId = `unprocessed_${startPoint.data.ts}_${endPoint.data.ts}`; @@ -371,7 +374,11 @@ function points2TripProps(locationPoints: Array>): Unp const tsEntrySort = (e1: BEMData, e2: BEMData) => e1.data.ts - e2.data.ts; // compare timestamps -function transitionTrip2TripObj(trip: Array): Promise { +/** + * @description Given an array of 2 transitions, queries the location data during that time and promises an UnprocessedTrip object. + * @param trip An array of transitions representing one trip; i.e. [start transition, end transition] + */ +function tripTransitions2UnprocessedTrip(trip: Array): Promise { const tripStartTransition = trip[0]; const tripEndTransition = trip[1]; const tq = { @@ -413,8 +420,7 @@ function transitionTrip2TripObj(trip: Array): Promise) { // Logger.log("Returning false"); return false; } -/* - * This is going to be a bit tricky. As we can see from - * https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163, - * when we read local transitions, they have a string for the transition - * (e.g. `T_DATA_PUSHED`), while the remote transitions have an integer - * (e.g. `2`). - * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286338606 - * - * Also, at least on iOS, it is possible for trip end to be detected way - * after the end of the trip, so the trip end transition of a processed - * trip may actually show up as an unprocessed transition. - * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163 - * - * Let's abstract this out into our own minor state machine. + +/** + * @description Given an array of transitions, finds which transitions represent the start and end of a detected trip and returns them as pairs. + * @returns An 2D array of transitions, where each inner array represents one trip; i.e. [start transition, end transition] */ -function transitions2Trips(transitionList: Array>) { +function transitions2TripTransitions(transitionList: Array>) { + /* This is going to be a bit tricky. As we can see from + * https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163, + * when we read local transitions, they have a string for the transition + * (e.g. `T_DATA_PUSHED`), while the remote transitions have an integer + * (e.g. `2`). + * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286338606 + * + * Also, at least on iOS, it is possible for trip end to be detected way + * after the end of the trip, so the trip end transition of a processed + * trip may actually show up as an unprocessed transition. + * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163 + * + * Let's abstract this out into our own minor state machine. + */ let inTrip = false; const tripList: [BEMData, BEMData][] = []; let currStartTransitionIndex = -1; @@ -536,12 +546,12 @@ export function readUnprocessedTrips( return []; } else { logDebug(`Found ${transitionList.length} transitions. yay!`); - const tripsList = transitions2Trips(transitionList); + const tripsList = transitions2TripTransitions(transitionList); logDebug(`Mapped into ${tripsList.length} trips. yay!`); tripsList.forEach((trip) => { logDebug(JSON.stringify(trip, null, 2)); }); - const tripFillPromises = tripsList.map(transitionTrip2TripObj); + const tripFillPromises = tripsList.map(tripTransitions2UnprocessedTrip); return Promise.all(tripFillPromises).then( (rawTripObjs: (UnprocessedTrip | undefined)[]) => { // Now we need to link up the trips. linking unprocessed trips From 826190a0f842a61c205fb206ef11d8939d8023d0 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 3 Apr 2024 01:29:03 -0400 Subject: [PATCH 4/4] flesh out more types Filled in more trip object types, and specifically some literal types that define unprocessed trips. (like "source" will always be 'unprocessed'). CompTripLocations changed from `number[]` for coordinates to geojson's `Position`, since that is more descriptive. Renamed CompTripLocations to CompositeTripLocation (this type represents only 1 location). Used the CompositeTripLocation type in timelineHelper. in locations2GeojsonTrajectory, the return type needed `properties` for it to be considered a Geojson `Feature`. formattedSectionProperties types as the return type of the function --- www/js/diary/timelineHelper.ts | 12 ++++++++---- www/js/types/diaryTypes.ts | 26 +++++++++++++------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 006cc0788..6c87cd0b9 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -15,6 +15,7 @@ import { CompositeTrip, UnprocessedTrip, SectionData, + CompositeTripLocation, } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; import { LabelOptions } from '../types/labelTypes'; @@ -216,10 +217,10 @@ const location2GeojsonPoint = (locationPoint: Point, featureType: string): Featu */ function locations2GeojsonTrajectory( trip: CompositeTrip, - locationList: Array, + locationList: CompositeTripLocation[], trajectoryColor?: string, -) { - let sectionsPoints; +): Feature[] { + let sectionsPoints: CompositeTripLocation[][]; if (!trip.sections) { // this is a unimodal trip so we put all the locations in one section sectionsPoints = [locationList]; @@ -243,6 +244,9 @@ function locations2GeojsonTrajectory( color for the sensed mode of this section, and fall back to dark grey */ color: trajectoryColor || getBaseModeByKey(section?.sensed_mode_str)?.color || '#333', }, + properties: { + feature_type: 'section_trajectory', + }, }; }); } @@ -341,7 +345,7 @@ function points2UnprocessedTrip(locationPoints: Array> } as Point, start_local_dt: dateTime2localdate(startTime, startPoint.metadata.time_zone), start_ts: startPoint.data.ts, - }; + } as const; // section: baseProps + some properties that are unique to the section const singleSection: SectionData = { diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 12102477d..8b8de469c 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -5,7 +5,7 @@ import { BaseModeKey, MotionTypeKey } from '../diary/diaryHelper'; import { MultilabelKey } from './labelTypes'; import { BEMData, LocalDt } from './serverData'; -import { FeatureCollection, Feature, Geometry, Point } from 'geojson'; +import { FeatureCollection, Feature, Geometry, Point, Position } from 'geojson'; type ObjectId = { $oid: string }; @@ -45,9 +45,9 @@ export type TripTransition = { ts: number; }; -type CompTripLocations = { +export type CompositeTripLocation = { loc: { - coordinates: number[]; // e.g. [1, 2.3] + coordinates: Position; // [lon, lat] }; speed: number; ts: number; @@ -56,7 +56,7 @@ type CompTripLocations = { // Used for return type of readUnprocessedTrips export type UnprocessedTrip = { _id: ObjectId; - additions: UserInputEntry[]; + additions: []; // unprocessed trips won't have any matched processed inputs, so this is always empty confidence_threshold: number; distance: number; duration: number; @@ -64,19 +64,19 @@ export type UnprocessedTrip = { end_loc: Point; end_local_dt: LocalDt; end_ts: number; - expectation: any; // TODO "{to_label: boolean}" - inferred_labels: any[]; // TODO - key: string; - locations?: CompTripLocations[]; - origin_key: string; // e.x., UNPROCESSED_trip + expectation: { to_label: true }; // unprocessed trips are always expected to be labeled + inferred_labels: []; // unprocessed trips won't have inferred labels + key: 'UNPROCESSED_trip'; + locations?: CompositeTripLocation[]; + origin_key: 'UNPROCESSED_trip'; sections: SectionData[]; - source: string; + source: 'unprocessed'; start_fmt_time: string; start_local_dt: LocalDt; start_ts: number; start_loc: Point; starting_trip?: any; - user_input: UserInput; + user_input: {}; // unprocessed trips won't have any matched processed inputs, so this is always empty }; /* These are the properties received from the server (basically matches Python code) @@ -96,13 +96,13 @@ export type CompositeTrip = { end_local_dt: LocalDt; end_place: ObjectId; end_ts: number; - expectation: any; // TODO "{to_label: boolean}" + expectation: { to_label: boolean }; expected_trip: ObjectId; inferred_labels: InferredLabels; inferred_section_summary: SectionSummary; inferred_trip: ObjectId; key: string; - locations: any[]; // TODO + locations: CompositeTripLocation[]; origin_key: string; raw_trip: ObjectId; sections: SectionData[];