From 6c23b2d7fcc141f5095b9d082f34d81088d10884 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 26 Oct 2023 10:34:47 -0700 Subject: [PATCH 01/52] Updated `diaryHelper.ts`, now uses luxon diaryHelper now only uses luxon, and does not rely on the momentjs library. --- www/js/diary/diaryHelper.ts | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index 0b834a485..e2816e153 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -1,9 +1,8 @@ // here we have some helper functions used throughout the label tab // these functions are being gradually migrated out of services.js -import moment from "moment"; import { DateTime } from "luxon"; -import { LabelOptions, readableLabelToKey } from "../survey/multilabel/confirmHelper"; +import { LabelOptions } from "../survey/multilabel/confirmHelper"; export const modeColors = { pink: '#c32e85', // oklch(56% 0.2 350) // e-car @@ -90,7 +89,7 @@ export function getBaseModeByText(text, labelOptions: LabelOptions) { */ export function isMultiDay(beginFmtTime: string, endFmtTime: string) { if (!beginFmtTime || !endFmtTime) return false; - return moment.parseZone(beginFmtTime).format('YYYYMMDD') != moment.parseZone(endFmtTime).format('YYYYMMDD'); + return DateTime.fromISO(beginFmtTime).toFormat('YYYYMMDD') != DateTime.fromISO(endFmtTime).toFormat('YYYYMMDD'); } /** @@ -105,11 +104,10 @@ export function getFormattedDate(beginFmtTime: string, endFmtTime?: string) { return `${getFormattedDate(beginFmtTime)} - ${getFormattedDate(endFmtTime)}`; } // only one day given, or both are the same day - const t = moment.parseZone(beginFmtTime || endFmtTime); - // We use ddd LL to get Wed, May 3, 2023 or equivalent - // LL only has the date, month and year - // LLLL has the day of the week, but also the time - return t.format('ddd LL'); + const t = DateTime.fromISO(beginFmtTime || endFmtTime); + // We use toLocale to get Wed May 3, 2023 or equivalent, + const tConversion = t.toLocaleString({weekday: 'short', month: 'long', day: '2-digit', year: 'numeric'}); + return tConversion.replace(',', ''); } /** @@ -135,9 +133,12 @@ export function getFormattedDateAbbr(beginFmtTime: string, endFmtTime?: string) */ export function getFormattedTimeRange(beginFmtTime: string, endFmtTime: string) { if (!beginFmtTime || !endFmtTime) return; - const beginMoment = moment.parseZone(beginFmtTime); - const endMoment = moment.parseZone(endFmtTime); - return endMoment.to(beginMoment, true); + const beginTime = DateTime.fromISO(beginFmtTime); + const endTime = DateTime.fromISO(endFmtTime); + const range = endTime.diff(beginTime, ['hours']); + const roundedHours = Math.round(range.as('hours')); // Round up or down to nearest hour + const formattedRange = `${roundedHours} hour${roundedHours !== 1 ? 's': ''}`; + return formattedRange; }; // Temporary function to avoid repear in getDetectedModes ret val. @@ -184,8 +185,9 @@ export function getFormattedSectionProperties(trip, ImperialConfig) { export function getLocalTimeString(dt) { if (!dt) return; - /* correcting the date of the processed trips knowing that local_dt months are from 1 -> 12 - and for the moment function they need to be between 0 -> 11 */ - const mdt = { ...dt, month: dt.month-1 }; - return moment(mdt).format("LT"); + const dateTime = DateTime.fromObject({ + hour: dt.hour, + minute: dt.minute, + }); + return dateTime.toFormat('hh:mm a') } From 062828ee6a2cb734d81148eb481434fb1574961c Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 26 Oct 2023 12:12:54 -0700 Subject: [PATCH 02/52] Removed moment from timelineHelper --- www/js/diary/timelineHelper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 579e3ac4f..50ef75ade 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -1,8 +1,8 @@ -import moment from "moment"; import { getAngularService } from "../angular-react-helper"; import { displayError, logDebug } from "../plugin/logger"; import { getBaseModeByKey, getBaseModeOfLabeledTrip } from "./diaryHelper"; import i18next from "i18next"; +import { DateTime } from "luxon"; const cachedGeojsons = new Map(); /** @@ -99,7 +99,7 @@ export function populateCompositeTrips(ctList, showPlaces, labelsFactory, labels const getUnprocessedInputQuery = (pipelineRange) => ({ key: "write_ts", startTs: pipelineRange.end_ts - 10, - endTs: moment().unix() + 10 + endTs: DateTime.now().toUnixInteger() + 10 }); function getUnprocessedResults(labelsFactory, notesFactory, labelsPromises, notesPromises) { From 5ade8b77081bee894d4faf8b1b734f256b1d6a8e Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 26 Oct 2023 13:19:33 -0700 Subject: [PATCH 03/52] Updated readAllCompositeTrips function --- www/js/diary/LabelTab.tsx | 4 ++-- www/js/diary/services.js | 39 -------------------------------- www/js/diary/timelineHelper.ts | 41 ++++++++++++++++++++++++++++++++++ www/js/types/serverData.ts | 33 +++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 41 deletions(-) create mode 100644 www/js/types/serverData.ts diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index f4677766d..f3924c691 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -16,7 +16,7 @@ import LabelListScreen from "./list/LabelListScreen"; import { createStackNavigator } from "@react-navigation/stack"; import LabelScreenDetails from "./details/LabelDetailsScreen"; import { NavigationContainer } from "@react-navigation/native"; -import { compositeTrips2TimelineMap, getAllUnprocessedInputs, getLocalUnprocessedInputs, populateCompositeTrips } from "./timelineHelper"; +import { compositeTrips2TimelineMap, getAllUnprocessedInputs, getLocalUnprocessedInputs, populateCompositeTrips, readAllCompositeTrips } from "./timelineHelper"; import { fillLocationNamesOfTrip, resetNominatimLimiter } from "./addressNamesHelper"; import { SurveyOptions } from "../survey/survey"; import { getLabelOptions } from "../survey/multilabel/confirmHelper"; @@ -202,7 +202,7 @@ const LabelTab = () => { return; } - const readCompositePromise = Timeline.readAllCompositeTrips(startTs, endTs); + const readCompositePromise = readAllCompositeTrips(startTs, endTs); let readUnprocessedPromise; if (endTs >= pipelineRange.end_ts) { const nowTs = new Date().getTime() / 1000; diff --git a/www/js/diary/services.js b/www/js/diary/services.js index 774273fa2..94d5fe292 100644 --- a/www/js/diary/services.js +++ b/www/js/diary/services.js @@ -27,45 +27,6 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', }); }); - // DB entries retrieved from the server have '_id', 'metadata', and 'data' fields. - // This function returns a shallow copy of the obj, which flattens the - // 'data' field into the top level, while also including '_id' and 'metadata.key' - const unpack = (obj) => ({ - ...obj.data, - _id: obj._id, - key: obj.metadata.key, - origin_key: obj.metadata.origin_key || obj.metadata.key, - }); - - timeline.readAllCompositeTrips = function(startTs, endTs) { - $ionicLoading.show({ - template: i18next.t('service.reading-server') - }); - const readPromises = [ - getRawEntries(["analysis/composite_trip"], - startTs, endTs, "data.end_ts"), - ]; - return Promise.all(readPromises) - .then(([ctList]) => { - $ionicLoading.hide(); - return ctList.phone_data.map((ct) => { - const unpackedCt = unpack(ct); - return { - ...unpackedCt, - start_confirmed_place: unpack(unpackedCt.start_confirmed_place), - end_confirmed_place: unpack(unpackedCt.end_confirmed_place), - locations: unpackedCt.locations?.map(unpack), - sections: unpackedCt.sections?.map(unpack), - } - }); - }) - .catch((err) => { - Logger.displayError("while reading confirmed trips", err); - $ionicLoading.hide(); - return []; - }); - }; - /* * 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, diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 50ef75ade..974baab35 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -1,6 +1,9 @@ import { getAngularService } from "../angular-react-helper"; import { displayError, logDebug } from "../plugin/logger"; import { getBaseModeByKey, getBaseModeOfLabeledTrip } from "./diaryHelper"; +import { getRawEntries } from "../commHelper"; +import { ServerResponse, ServerData } from "../types/serverData"; + import i18next from "i18next"; import { DateTime } from "luxon"; @@ -210,3 +213,41 @@ const locations2GeojsonTrajectory = (trip, locationList, trajectoryColor?) => { } }); } + +// Remaining functions from /diary/services.js +const unpackServerData = (obj: ServerData) => ({ + ...obj.data, + _id: obj._id, + key: obj.metadata.key, + origin_key: obj.metadata.origin_key || obj.metadata.key, +}); + +export const readAllCompositeTrips = function(startTs: number, endTs: number) { + const $ionicLoading = getAngularService('$ionicLoading'); + $ionicLoading.show({ + template: i18next.t('service.reading-server') + }); + const readPromises = [ + getRawEntries(["analysis/composite_trip"], + startTs, endTs, "data.end_ts"), + ]; + return Promise.all(readPromises) + .then(([ctList]: [ServerResponse]) => { + $ionicLoading.hide(); + return ctList.phone_data.map((ct) => { + const unpackedCt = unpackServerData(ct); + return { + ...unpackedCt, + start_confirmed_place: unpackServerData(unpackedCt.start_confirmed_place), + end_confirmed_place: unpackServerData(unpackedCt.end_confirmed_place), + locations: unpackedCt.locations?.map(unpackServerData), + sections: unpackedCt.sections?.map(unpackServerData), + } + }); + }) + .catch((err) => { + displayError(err, "while reading confirmed trips"); + $ionicLoading.hide(); + return []; + }); +}; \ No newline at end of file diff --git a/www/js/types/serverData.ts b/www/js/types/serverData.ts new file mode 100644 index 000000000..4b569206d --- /dev/null +++ b/www/js/types/serverData.ts @@ -0,0 +1,33 @@ +export type ServerResponse = { + phone_data: Array>, +} + +export type ServerData = { + data: Type, + metadata: MetaData, + key?: string, + user_id?: { $uuid: string, }, + _id?: { $oid: string, }, +}; + +export type MetaData = { + key: string, + platform: string, + write_ts: number, + time_zone: string, + write_fmt_time: string, + write_local_dt: LocalDt, + origin_key?: string, +}; + +export type LocalDt = { + minute: number, + hour: number, + second: number, + day: number, + weekday: number, + month: number, + year: number, + timezone: string, +}; + \ No newline at end of file From 7a4b6fb528f037b726b075efaa409c7426ec679c Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 2 Nov 2023 09:54:26 -0700 Subject: [PATCH 04/52] Rewrote readUnprocessedTrips, helper functions - moved functions to timelineHelper - updated momentJS code to luxon --- www/js/diary/LabelTab.tsx | 4 +- www/js/diary/services.js | 297 +------------------------------- www/js/diary/timelineHelper.ts | 299 ++++++++++++++++++++++++++++++++- www/js/types/serverData.ts | 1 + 4 files changed, 302 insertions(+), 299 deletions(-) diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index f3924c691..9f76e891c 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -16,7 +16,7 @@ import LabelListScreen from "./list/LabelListScreen"; import { createStackNavigator } from "@react-navigation/stack"; import LabelScreenDetails from "./details/LabelDetailsScreen"; import { NavigationContainer } from "@react-navigation/native"; -import { compositeTrips2TimelineMap, getAllUnprocessedInputs, getLocalUnprocessedInputs, populateCompositeTrips, readAllCompositeTrips } from "./timelineHelper"; +import { compositeTrips2TimelineMap, getAllUnprocessedInputs, getLocalUnprocessedInputs, populateCompositeTrips, readAllCompositeTrips, readUnprocessedTrips } from "./timelineHelper"; import { fillLocationNamesOfTrip, resetNominatimLimiter } from "./addressNamesHelper"; import { SurveyOptions } from "../survey/survey"; import { getLabelOptions } from "../survey/multilabel/confirmHelper"; @@ -209,7 +209,7 @@ const LabelTab = () => { const lastProcessedTrip = timelineMap && [...timelineMap?.values()].reverse().find( trip => trip.origin_key.includes('confirmed_trip') ); - readUnprocessedPromise = Timeline.readUnprocessedTrips(pipelineRange.end_ts, nowTs, lastProcessedTrip); + readUnprocessedPromise = readUnprocessedTrips(pipelineRange.end_ts, nowTs, lastProcessedTrip); } else { readUnprocessedPromise = Promise.resolve([]); } diff --git a/www/js/diary/services.js b/www/js/diary/services.js index c6fa90267..aebaf2518 100644 --- a/www/js/diary/services.js +++ b/www/js/diary/services.js @@ -3,8 +3,6 @@ import angular from 'angular'; import { SurveyOptions } from '../survey/survey'; import { getConfig } from '../config/dynamicConfig'; -import { getRawEntries } from '../commHelper'; -import { getUnifiedDataForInterval } from '../unifiedDataLoader' angular.module('emission.main.diary.services', ['emission.plugin.logger', 'emission.services']) @@ -27,304 +25,12 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', }); }); - /* - * 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. - */ - var transitions2Trips = function(transitionList) { - var inTrip = false; - var tripList = [] - var currStartTransitionIndex = -1; - var currEndTransitionIndex = -1; - var processedUntil = 0; - - while(processedUntil < transitionList.length) { - // Logger.log("searching within list = "+JSON.stringify(transitionList.slice(processedUntil))); - if(inTrip == false) { - var foundStartTransitionIndex = transitionList.slice(processedUntil).findIndex(isStartingTransition); - if (foundStartTransitionIndex == -1) { - Logger.log("No further unprocessed trips started, exiting loop"); - processedUntil = transitionList.length; - } else { - currStartTransitionIndex = processedUntil + foundStartTransitionIndex; - processedUntil = currStartTransitionIndex; - Logger.log("Unprocessed trip started at "+JSON.stringify(transitionList[currStartTransitionIndex])); - inTrip = true; - } - } else { - // Logger.log("searching within list = "+JSON.stringify(transitionList.slice(processedUntil))); - var foundEndTransitionIndex = transitionList.slice(processedUntil).findIndex(isEndingTransition); - if (foundEndTransitionIndex == -1) { - Logger.log("Can't find end for trip starting at "+JSON.stringify(transitionList[currStartTransitionIndex])+" dropping it"); - processedUntil = transitionList.length; - } else { - currEndTransitionIndex = processedUntil + foundEndTransitionIndex; - processedUntil = currEndTransitionIndex; - Logger.log("currEndTransitionIndex = "+currEndTransitionIndex); - Logger.log("Unprocessed trip starting at "+JSON.stringify(transitionList[currStartTransitionIndex])+" ends at "+JSON.stringify(transitionList[currEndTransitionIndex])); - tripList.push([transitionList[currStartTransitionIndex], - transitionList[currEndTransitionIndex]]) - inTrip = false; - } - } - } - return tripList; - } - - var isStartingTransition = function(transWrapper) { - // Logger.log("isStartingTransition: transWrapper.data.transition = "+transWrapper.data.transition); - if(transWrapper.data.transition == 'local.transition.exited_geofence' || - transWrapper.data.transition == 'T_EXITED_GEOFENCE' || - transWrapper.data.transition == 1) { - // Logger.log("Returning true"); - return true; - } - // Logger.log("Returning false"); - return false; - } - - var isEndingTransition = function(transWrapper) { - // Logger.log("isEndingTransition: transWrapper.data.transition = "+transWrapper.data.transition); - if(transWrapper.data.transition == 'T_TRIP_ENDED' || - transWrapper.data.transition == 'local.transition.stopped_moving' || - transWrapper.data.transition == 2) { - // Logger.log("Returning true"); - return true; - } - // Logger.log("Returning false"); - return false; - } - /* * Fill out place geojson after pulling trip location points. * Place is only partially filled out because we haven't linked the timeline yet */ - var moment2localdate = function(currMoment, tz) { - return { - timezone: tz, - year: currMoment.year(), - //the months of the draft trips match the one format needed for - //moment function however now that is modified we need to also - //modify the months value here - month: currMoment.month() + 1, - day: currMoment.date(), - weekday: currMoment.weekday(), - hour: currMoment.hour(), - minute: currMoment.minute(), - second: currMoment.second() - }; - } - - var points2TripProps = function(locationPoints) { - var startPoint = locationPoints[0]; - var endPoint = locationPoints[locationPoints.length - 1]; - var tripAndSectionId = "unprocessed_"+startPoint.data.ts+"_"+endPoint.data.ts; - var startMoment = moment.unix(startPoint.data.ts).tz(startPoint.metadata.time_zone); - var endMoment = moment.unix(endPoint.data.ts).tz(endPoint.metadata.time_zone); - - const speeds = [], dists = []; - let loc, locLatLng; - locationPoints.forEach((pt) => { - const ptLatLng = L.latLng([pt.data.latitude, pt.data.longitude]); - if (loc) { - const dist = locLatLng.distanceTo(ptLatLng); - const timeDelta = pt.data.ts - loc.data.ts; - dists.push(dist); - speeds.push(dist / timeDelta); - } - loc = pt; - locLatLng = ptLatLng; - }); - - const locations = locationPoints.map((point, i) => ({ - loc: { - coordinates: [point.data.longitude, point.data.latitude] - }, - ts: point.data.ts, - speed: speeds[i], - })); - - return { - _id: {$oid: tripAndSectionId}, - key: "UNPROCESSED_trip", - origin_key: "UNPROCESSED_trip", - additions: [], - confidence_threshold: 0, - distance: dists.reduce((a, b) => a + b, 0), - duration: endPoint.data.ts - startPoint.data.ts, - end_fmt_time: endMoment.format(), - end_local_dt: moment2localdate(endMoment, endPoint.metadata.time_zone), - end_ts: endPoint.data.ts, - expectation: {to_label: true}, - inferred_labels: [], - locations: locations, - source: "unprocessed", - start_fmt_time: startMoment.format(), - start_local_dt: moment2localdate(startMoment, startPoint.metadata.time_zone), - start_ts: startPoint.data.ts, - user_input: {}, - } - } - - var tsEntrySort = function(e1, e2) { - // compare timestamps - return e1.data.ts - e2.data.ts; - } - - var transitionTrip2TripObj = function(trip) { - var tripStartTransition = trip[0]; - var tripEndTransition = trip[1]; - var tq = {key: "write_ts", - startTs: tripStartTransition.data.ts, - endTs: tripEndTransition.data.ts - } - Logger.log("About to pull location data for range " - + moment.unix(tripStartTransition.data.ts).toString() + " -> " - + moment.unix(tripEndTransition.data.ts).toString()); - const getSensorData = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval; - return getUnifiedDataForInterval("background/filtered_location", tq, getSensorData) - .then(function(locationList) { - if (locationList.length == 0) { - return undefined; - } - var sortedLocationList = locationList.sort(tsEntrySort); - var retainInRange = function(loc) { - return (tripStartTransition.data.ts <= loc.data.ts) && - (loc.data.ts <= tripEndTransition.data.ts) - } - - var filteredLocationList = sortedLocationList.filter(retainInRange); - - // Fix for https://github.com/e-mission/e-mission-docs/issues/417 - if (filteredLocationList.length == 0) { - return undefined; - } - - var tripStartPoint = filteredLocationList[0]; - var tripEndPoint = filteredLocationList[filteredLocationList.length-1]; - Logger.log("tripStartPoint = "+JSON.stringify(tripStartPoint)+"tripEndPoint = "+JSON.stringify(tripEndPoint)); - // if we get a list but our start and end are undefined - // let's print out the complete original list to get a clue - // this should help with debugging - // https://github.com/e-mission/e-mission-docs/issues/417 - // if it ever occurs again - if (angular.isUndefined(tripStartPoint) || angular.isUndefined(tripEndPoint)) { - Logger.log("BUG 417 check: locationList = "+JSON.stringify(locationList)); - Logger.log("transitions: start = "+JSON.stringify(tripStartTransition.data) - + " end = "+JSON.stringify(tripEndTransition.data.ts)); - } - - const tripProps = points2TripProps(filteredLocationList); - - return { - ...tripProps, - start_loc: { - type: "Point", - coordinates: [tripStartPoint.data.longitude, tripStartPoint.data.latitude] - }, - end_loc: { - type: "Point", - coordinates: [tripEndPoint.data.longitude, tripEndPoint.data.latitude], - }, - } - }); - } - - var linkTrips = function(trip1, trip2) { - // complete trip1 - trip1.starting_trip = {$oid: trip2.id}; - trip1.exit_fmt_time = trip2.enter_fmt_time; - trip1.exit_local_dt = trip2.enter_local_dt; - trip1.exit_ts = trip2.enter_ts; - - // start trip2 - trip2.ending_trip = {$oid: trip1.id}; - trip2.enter_fmt_time = trip1.exit_fmt_time; - trip2.enter_local_dt = trip1.exit_local_dt; - trip2.enter_ts = trip1.exit_ts; - } - - timeline.readUnprocessedTrips = function(startTs, endTs, lastProcessedTrip) { - $ionicLoading.show({ - template: i18next.t('service.reading-unprocessed-data') - }); - - var tq = {key: "write_ts", - startTs, - endTs - } - Logger.log("about to query for unprocessed trips from " - +moment.unix(tq.startTs).toString()+" -> "+moment.unix(tq.endTs).toString()); - - const getMessageMethod = window['cordova'].plugins.BEMUserCache.getMessagesForInterval; - return getUnifiedDataForInterval("statemachine/transition", tq, getMessageMethod) - .then(function(transitionList) { - if (transitionList.length == 0) { - Logger.log("No unprocessed trips. yay!"); - $ionicLoading.hide(); - return []; - } else { - Logger.log("Found "+transitionList.length+" transitions. yay!"); - var sortedTransitionList = transitionList.sort(tsEntrySort); - /* - sortedTransitionList.forEach(function(transition) { - console.log(moment(transition.data.ts * 1000).format()+":" + JSON.stringify(transition.data)); - }); - */ - var tripsList = transitions2Trips(transitionList); - Logger.log("Mapped into"+tripsList.length+" trips. yay!"); - tripsList.forEach(function(trip) { - console.log(JSON.stringify(trip)); - }); - var tripFillPromises = tripsList.map(transitionTrip2TripObj); - return Promise.all(tripFillPromises).then(function(raw_trip_gj_list) { - // Now we need to link up the trips. linking unprocessed trips - // to one another is fairly simple, but we need to link the - // first unprocessed trip to the last processed trip. - // This might be challenging if we don't have any processed - // trips for the day. I don't want to go back forever until - // I find a trip. So if this is the first trip, we will start a - // new chain for now, since this is with unprocessed data - // anyway. - - Logger.log("mapped trips to trip_gj_list of size "+raw_trip_gj_list.length); - /* Filtering: we will keep trips that are 1) defined and 2) have a distance >= 100m or duration >= 5 minutes - https://github.com/e-mission/e-mission-docs/issues/966#issuecomment-1709112578 */ - const trip_gj_list = raw_trip_gj_list.filter((trip) => - trip && (trip.distance >= 100 || trip.duration >= 300) - ); - Logger.log("after filtering undefined and distance < 100, trip_gj_list size = "+raw_trip_gj_list.length); - // Link 0th trip to first, first to second, ... - for (var i = 0; i < trip_gj_list.length-1; i++) { - linkTrips(trip_gj_list[i], trip_gj_list[i+1]); - } - Logger.log("finished linking trips for list of size "+trip_gj_list.length); - if (lastProcessedTrip && trip_gj_list.length != 0) { - // Need to link the entire chain above to the processed data - Logger.log("linking unprocessed and processed trip chains"); - linkTrips(lastProcessedTrip, trip_gj_list[0]); - } - $ionicLoading.hide(); - Logger.log("Returning final list of size "+trip_gj_list.length); - return trip_gj_list; - }); - } - }); - } - - var localCacheReadFn = timeline.updateFromDatabase; + var localCacheReadFn = timeline.updateFromDatabase; timeline.getTrip = function(tripId) { return angular.isDefined(timeline.data.tripMap)? timeline.data.tripMap[tripId] : undefined; @@ -350,4 +56,3 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', return timeline; }) - diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 754de1a24..ca699499b 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -2,6 +2,9 @@ import { getAngularService } from "../angular-react-helper"; import { displayError, logDebug } from "../plugin/logger"; import { getBaseModeByKey, getBaseModeOfLabeledTrip } from "./diaryHelper"; import { getUnifiedDataForInterval} from "../unifiedDataLoader"; +import { getRawEntries } from "../commHelper"; +import { ServerResponse, ServerData } from "../types/serverData"; +import L from 'leaflet'; import i18next from "i18next"; import { DateTime } from "luxon"; @@ -249,4 +252,298 @@ export const readAllCompositeTrips = function(startTs: number, endTs: number) { $ionicLoading.hide(); return []; }); -}; \ No newline at end of file +}; + +const dateTime2localdate = function(currtime: DateTime, tz: string) { + return { + timezone: tz, + year: currtime.get('year'), + //the months of the draft trips match the one format needed for + //moment function however now that is modified we need to also + //modify the months value here + month: currtime.get('month') + 1, + day: currtime.get('day'), + weekday: currtime.get('weekday'), + hour: currtime.get('hour'), + minute: currtime.get('minute'), + second: currtime.get('second'), + }; +} +/* locationPoints are of form: + * ServerData + * Point = { + * currentState: string, + * transition: string, + * ts: number, // 1698433683.712 + * } + */ +const points2TripProps = function(locationPoints) { + const startPoint = locationPoints[0]; + const endPoint = locationPoints[locationPoints.length - 1]; + const tripAndSectionId = `unprocessed_${startPoint.data.ts}_${endPoint.data.ts}`; + const startTime = DateTime.fromSeconds(startPoint.data.ts).setZone(startPoint.metadata.time_zone); + const endTime = DateTime.fromSeconds(endPoint.data.ts).setZone(endPoint.metadata.time_zone); + + const speeds = [], dists = []; + var loc, locLatLng; + locationPoints.forEach((pt) => { + const ptLatLng = L.latLng([pt.data.latitude, pt.data.longitude]); + if (loc) { + const dist = locLatLng.distanceTo(ptLatLng); + const timeDelta = pt.data.ts - loc.data.ts; + dists.push(dist); + speeds.push(dist / timeDelta); + } + loc = pt; + locLatLng = ptLatLng; + }); + + const locations = locationPoints.map((point, i) => ({ + loc: { + coordinates: [point.data.longitude, point.data.latitude] + }, + ts: point.data.ts, + speed: speeds[i], + })); + + // used to mimic old momentJS moment.format() + const formatString = "yyyy-MM-dd'T'HH:mm:ssZZ"; + return { + _id: {$oid: tripAndSectionId}, + key: "UNPROCESSED_trip", + origin_key: "UNPROCESSED_trip", + additions: [], + confidence_threshold: 0, + distance: dists.reduce((a, b) => a + b, 0), + duration: endPoint.data.ts - startPoint.data.ts, + end_fmt_time: endTime.toFormat(formatString), + 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.toFormat(formatString), + start_local_dt: dateTime2localdate(startTime, startPoint.metadata.time_zone), + start_ts: startPoint.data.ts, + user_input: {}, + } +} +const tsEntrySort = function(e1, e2) { + // compare timestamps + return e1.data.ts - e2.data.ts; +} + +const transitionTrip2TripObj = function(trip) { + const tripStartTransition = trip[0]; + const tripEndTransition = trip[1]; + const tq = {key: "write_ts", + startTs: tripStartTransition.data.ts, + endTs: tripEndTransition.data.ts + } + logDebug('About to pull location data for range' + + DateTime.fromSeconds(tripStartTransition.data.ts) + .toLocaleString(DateTime.DATETIME_MED) + + DateTime.fromSeconds(tripEndTransition.data.ts) + .toLocaleString(DateTime.DATETIME_MED)); + const getSensorData = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval; + return getUnifiedDataForInterval("background/filtered_location", tq, getSensorData) + .then(function(locationList: Array) { // change 'any' later + if (locationList.length == 0) { + return undefined; + } + const sortedLocationList = locationList.sort(tsEntrySort); + const retainInRange = function(loc) { + return (tripStartTransition.data.ts <= loc.data.ts) && + (loc.data.ts <= tripEndTransition.data.ts) + } + + var filteredLocationList = sortedLocationList.filter(retainInRange); + + // Fix for https://github.com/e-mission/e-mission-docs/issues/417 + if (filteredLocationList.length == 0) { + return undefined; + } + + const tripStartPoint = filteredLocationList[0]; + const tripEndPoint = filteredLocationList[filteredLocationList.length-1]; + logDebug("tripStartPoint = "+JSON.stringify(tripStartPoint)+"tripEndPoint = "+JSON.stringify(tripEndPoint)); + // if we get a list but our start and end are undefined + // let's print out the complete original list to get a clue + // this should help with debugging + // https://github.com/e-mission/e-mission-docs/issues/417 + // if it ever occurs again + if (tripStartPoint === undefined || tripEndPoint === undefined) { + logDebug("BUG 417 check: locationList = "+JSON.stringify(locationList)); + logDebug("transitions: start = "+JSON.stringify(tripStartTransition.data) + + ' end = ' + JSON.stringify(tripEndTransition.data.ts)); + } + + const tripProps = points2TripProps(filteredLocationList); + + return { + ...tripProps, + start_loc: { + type: "Point", + coordinates: [tripStartPoint.data.longitude, tripStartPoint.data.latitude] + }, + end_loc: { + type: "Point", + coordinates: [tripEndPoint.data.longitude, tripEndPoint.data.latitude], + }, + } + }); +} +const isStartingTransition = function(transWrapper) { + if(transWrapper.data.transition == 'local.transition.exited_geofence' || + transWrapper.data.transition == 'T_EXITED_GEOFENCE' || + transWrapper.data.transition == 1) { + return true; + } + return false; +} + +const isEndingTransition = function(transWrapper) { + // Logger.log("isEndingTransition: transWrapper.data.transition = "+transWrapper.data.transition); + if(transWrapper.data.transition == 'T_TRIP_ENDED' || + transWrapper.data.transition == 'local.transition.stopped_moving' || + transWrapper.data.transition == 2) { + // Logger.log("Returning true"); + return true; + } + // 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. + */ +const transitions2Trips = function(transitionList) { + var inTrip = false; + var tripList = [] + var currStartTransitionIndex = -1; + var currEndTransitionIndex = -1; + var processedUntil = 0; + + while(processedUntil < transitionList.length) { + // Logger.log("searching within list = "+JSON.stringify(transitionList.slice(processedUntil))); + if(inTrip == false) { + const foundStartTransitionIndex = transitionList.slice(processedUntil).findIndex(isStartingTransition); + if (foundStartTransitionIndex == -1) { + logDebug("No further unprocessed trips started, exiting loop"); + processedUntil = transitionList.length; + } else { + currStartTransitionIndex = processedUntil + foundStartTransitionIndex; + processedUntil = currStartTransitionIndex; + logDebug("Unprocessed trip started at "+JSON.stringify(transitionList[currStartTransitionIndex])); + inTrip = true; + } + } else { + const foundEndTransitionIndex = transitionList.slice(processedUntil).findIndex(isEndingTransition); + if (foundEndTransitionIndex == -1) { + logDebug("Can't find end for trip starting at "+JSON.stringify(transitionList[currStartTransitionIndex])+" dropping it"); + processedUntil = transitionList.length; + } else { + currEndTransitionIndex = processedUntil + foundEndTransitionIndex; + processedUntil = currEndTransitionIndex; + logDebug(`currEndTransitionIndex ${currEndTransitionIndex}`); + logDebug("Unprocessed trip starting at "+JSON.stringify(transitionList[currStartTransitionIndex])+" ends at "+JSON.stringify(transitionList[currEndTransitionIndex])); + tripList.push([transitionList[currStartTransitionIndex], + transitionList[currEndTransitionIndex]]); + inTrip = false; + } + } + } + return tripList; +} + +const linkTrips = function(trip1, trip2) { + // complete trip1 + trip1.starting_trip = {$oid: trip2.id}; + trip1.exit_fmt_time = trip2.enter_fmt_time; + trip1.exit_local_dt = trip2.enter_local_dt; + trip1.exit_ts = trip2.enter_ts; + + // start trip2 + trip2.ending_trip = {$oid: trip1.id}; + trip2.enter_fmt_time = trip1.exit_fmt_time; + trip2.enter_local_dt = trip1.exit_local_dt; + trip2.enter_ts = trip1.exit_ts; +} + + +export const readUnprocessedTrips = function(startTs, endTs, lastProcessedTrip) { + const $ionicLoading = getAngularService('$ionicLoading'); + $ionicLoading.show({ + template: i18next.t('service.reading-unprocessed-data') + }); + + var tq = {key: 'write_ts', + startTs, + endTs + } + logDebug('about to query for unprocessed trips from ' + + DateTime.fromSeconds(tq.startTs).toLocaleString(DateTime.DATETIME_MED) + + DateTime.fromSeconds(tq.endTs).toLocaleString(DateTime.DATETIME_MED) + ); + + const getMessageMethod = window['cordova'].plugins.BEMUserCache.getMessagesForInterval; + return getUnifiedDataForInterval("statemachine/transition", tq, getMessageMethod) + .then(function(transitionList: Array) { + if (transitionList.length == 0) { + logDebug('No unprocessed trips. yay!'); + $ionicLoading.hide(); + return []; + } else { + logDebug(`Found ${transitionList.length} transitions. yay!`); + const tripsList = transitions2Trips(transitionList); + logDebug(`Mapped into ${tripsList.length} trips. yay!`); + tripsList.forEach(function(trip) { + console.log(JSON.stringify(trip)); + }); + var tripFillPromises = tripsList.map(transitionTrip2TripObj); + return Promise.all(tripFillPromises).then(function(raw_trip_gj_list) { + // Now we need to link up the trips. linking unprocessed trips + // to one another is fairly simple, but we need to link the + // first unprocessed trip to the last processed trip. + // This might be challenging if we don't have any processed + // trips for the day. I don't want to go back forever until + // I find a trip. So if this is the first trip, we will start a + // new chain for now, since this is with unprocessed data + // anyway. + + logDebug(`mapped trips to trip_gj_list of size ${raw_trip_gj_list.length}`); + /* Filtering: we will keep trips that are 1) defined and 2) have a distance >= 100m or duration >= 5 minutes + https://github.com/e-mission/e-mission-docs/issues/966#issuecomment-1709112578 */ + const trip_gj_list = raw_trip_gj_list.filter((trip) => + trip && (trip.distance >= 100 || trip.duration >= 300) + ); + logDebug(`after filtering undefined and distance < 100, trip_gj_list size = ${raw_trip_gj_list.length}`); + // Link 0th trip to first, first to second, ... + for (var i = 0; i < trip_gj_list.length-1; i++) { + linkTrips(trip_gj_list[i], trip_gj_list[i+1]); + } + logDebug(`finished linking trips for list of size ${trip_gj_list.length}`); + if (lastProcessedTrip && trip_gj_list.length != 0) { + // Need to link the entire chain above to the processed data + logDebug("linking unprocessed and processed trip chains"); + linkTrips(lastProcessedTrip, trip_gj_list[0]); + } + $ionicLoading.hide(); + logDebug(`Returning final list of size ${trip_gj_list.length}`); + return trip_gj_list; + }); + } + }); +}; diff --git a/www/js/types/serverData.ts b/www/js/types/serverData.ts index 5d10ddcbf..8b14b79df 100644 --- a/www/js/types/serverData.ts +++ b/www/js/types/serverData.ts @@ -18,6 +18,7 @@ export type MetaData = { write_fmt_time: string, write_local_dt: LocalDt, origin_key?: string, + read_ts?: number, }; export type LocalDt = { From b5736a1a45f18cb2aade5ad02904142d198b6b18 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:06:25 -0700 Subject: [PATCH 05/52] Fully removed angular service --- www/index.js | 1 - www/js/diary.js | 3 +- www/js/diary/LabelTab.tsx | 1 - www/js/diary/services.js | 58 --------------------------------------- 4 files changed, 1 insertion(+), 62 deletions(-) delete mode 100644 www/js/diary/services.js diff --git a/www/index.js b/www/index.js index 0a0c63708..952436799 100644 --- a/www/index.js +++ b/www/index.js @@ -18,7 +18,6 @@ import './js/main.js'; import './js/survey/input-matcher.js'; import './js/survey/multilabel/multi-label-ui.js'; import './js/diary.js'; -import './js/diary/services.js'; import './js/survey/enketo/answer.js'; import './js/survey/enketo/enketo-trip-button.js'; import './js/survey/enketo/enketo-add-note-button.js'; diff --git a/www/js/diary.js b/www/js/diary.js index c0b7bce35..d83aaee3e 100644 --- a/www/js/diary.js +++ b/www/js/diary.js @@ -1,8 +1,7 @@ import angular from 'angular'; import LabelTab from './diary/LabelTab'; -angular.module('emission.main.diary',['emission.main.diary.services', - 'emission.survey.multilabel.buttons', +angular.module('emission.main.diary',['emission.survey.multilabel.buttons', 'emission.survey.enketo.add-note-button', 'emission.survey.enketo.trip.button', 'emission.plugin.logger']) diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 9f76e891c..92d2d1ab0 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -48,7 +48,6 @@ const LabelTab = () => { const $state = getAngularService('$state'); const $ionicPopup = getAngularService('$ionicPopup'); const Logger = getAngularService('Logger'); - const Timeline = getAngularService('Timeline'); const enbs = getAngularService('EnketoNotesButtonService'); // initialization, once the appConfig is loaded diff --git a/www/js/diary/services.js b/www/js/diary/services.js deleted file mode 100644 index aebaf2518..000000000 --- a/www/js/diary/services.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -import angular from 'angular'; -import { SurveyOptions } from '../survey/survey'; -import { getConfig } from '../config/dynamicConfig'; - -angular.module('emission.main.diary.services', ['emission.plugin.logger', - 'emission.services']) -.factory('Timeline', function($http, $ionicLoading, $ionicPlatform, $window, - $rootScope, Logger, $injector) { - var timeline = {}; - // corresponds to the old $scope.data. Contains all state for the current - // day, including the indication of the current day - timeline.data = {}; - timeline.data.unifiedConfirmsResults = null; - timeline.UPDATE_DONE = "TIMELINE_UPDATE_DONE"; - - let manualInputFactory; - $ionicPlatform.ready(function () { - getConfig().then((configObj) => { - const surveyOptKey = configObj.survey_info['trip-labels']; - const surveyOpt = SurveyOptions[surveyOptKey]; - console.log('surveyOpt in services.js is', surveyOpt); - manualInputFactory = $injector.get(surveyOpt.service); - }); - }); - - /* - * Fill out place geojson after pulling trip location points. - * Place is only partially filled out because we haven't linked the timeline yet - */ - - var localCacheReadFn = timeline.updateFromDatabase; - - timeline.getTrip = function(tripId) { - return angular.isDefined(timeline.data.tripMap)? timeline.data.tripMap[tripId] : undefined; - }; - - timeline.getTripWrapper = function(tripId) { - return angular.isDefined(timeline.data.tripWrapperMap)? timeline.data.tripWrapperMap[tripId] : undefined; - }; - - timeline.getCompositeTrip = function(tripId) { - return angular.isDefined(timeline.data.infScrollCompositeTripMap)? timeline.data.infScrollCompositeTripMap[tripId] : undefined; - }; - - timeline.setInfScrollCompositeTripList = function(compositeTripList) { - timeline.data.infScrollCompositeTripList = compositeTripList; - - timeline.data.infScrollCompositeTripMap = {}; - - timeline.data.infScrollCompositeTripList.forEach(function(trip, index, array) { - timeline.data.infScrollCompositeTripMap[trip._id.$oid] = trip; - }); - } - - return timeline; - }) From 7c7f4d8ad9a0a9d7b76c227a59a6170563ea26a0 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 2 Nov 2023 15:06:33 -0700 Subject: [PATCH 06/52] Fixed error in prettier merge --- www/js/diary/LabelTab.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 0c9c20f7d..f58277bea 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -21,6 +21,8 @@ import { getAllUnprocessedInputs, getLocalUnprocessedInputs, populateCompositeTrips, + readAllCompositeTrips, + readUnprocessedTrips, } from './timelineHelper'; import { fillLocationNamesOfTrip, resetNominatimLimiter } from './addressNamesHelper'; import { SurveyOptions } from '../survey/survey'; @@ -239,7 +241,7 @@ const LabelTab = () => { [...timelineMap?.values()] .reverse() .find((trip) => trip.origin_key.includes('confirmed_trip')); - readUnprocessedPromise = Timeline.readUnprocessedTrips( + readUnprocessedPromise = readUnprocessedTrips( pipelineRange.end_ts, nowTs, lastProcessedTrip, @@ -261,7 +263,7 @@ const LabelTab = () => { const timelineMapRef = useRef(timelineMap); async function repopulateTimelineEntry(oid: string) { if (!timelineMap.has(oid)) - return console.error('Item with oid: ' + oid + ' not found in timeline'); + return console.error(`Item with oid: ${oid} not found in timeline`); const [newLabels, newNotes] = await getLocalUnprocessedInputs( pipelineRange, labelPopulateFactory, From 356dfcf200dc69d147f439e4fc9cae3a44380c7f Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:00:40 -0700 Subject: [PATCH 07/52] Ran prettier on remaining files --- www/js/controllers.js | 2 +- www/js/diary/LabelTab.tsx | 9 +- www/js/services.js | 13 +-- www/js/types/diaryTypes.ts | 124 ++++++++++++------------ www/js/types/fileShareTypes.ts | 12 +-- www/js/types/serverData.ts | 50 +++++----- www/js/unifiedDataLoader.ts | 167 +++++++++++++++++++-------------- 7 files changed, 195 insertions(+), 182 deletions(-) diff --git a/www/js/controllers.js b/www/js/controllers.js index 1f3e64312..17835f3f4 100644 --- a/www/js/controllers.js +++ b/www/js/controllers.js @@ -87,4 +87,4 @@ angular ); console.log('SplashCtrl invoke finished'); }, - ) + ); diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index f58277bea..bbd86d64a 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -241,11 +241,7 @@ const LabelTab = () => { [...timelineMap?.values()] .reverse() .find((trip) => trip.origin_key.includes('confirmed_trip')); - readUnprocessedPromise = readUnprocessedTrips( - pipelineRange.end_ts, - nowTs, - lastProcessedTrip, - ); + readUnprocessedPromise = readUnprocessedTrips(pipelineRange.end_ts, nowTs, lastProcessedTrip); readUnprocessedPromise = readUnprocessedTrips(pipelineRange.end_ts, nowTs, lastProcessedTrip); } else { readUnprocessedPromise = Promise.resolve([]); @@ -262,8 +258,7 @@ const LabelTab = () => { const timelineMapRef = useRef(timelineMap); async function repopulateTimelineEntry(oid: string) { - if (!timelineMap.has(oid)) - return console.error(`Item with oid: ${oid} not found in timeline`); + if (!timelineMap.has(oid)) return console.error(`Item with oid: ${oid} not found in timeline`); const [newLabels, newNotes] = await getLocalUnprocessedInputs( pipelineRange, labelPopulateFactory, diff --git a/www/js/services.js b/www/js/services.js index 891b47eae..aa2958d47 100644 --- a/www/js/services.js +++ b/www/js/services.js @@ -3,12 +3,10 @@ import angular from 'angular'; import { getRawEntries } from './commHelper'; -angular.module('emission.services', ['emission.plugin.logger']) -.service('ControlHelper', function($window, - $ionicPopup, - Logger) { - - this.writeFile = function(fileEntry, resultList) { +angular + .module('emission.services', ['emission.plugin.logger']) + .service('ControlHelper', function ($window, $ionicPopup, Logger) { + this.writeFile = function (fileEntry, resultList) { // Create a FileWriter object for our FileEntry (log.txt). }; @@ -126,5 +124,4 @@ angular.module('emission.services', ['emission.plugin.logger']) this.getSettings = function () { return window.cordova.plugins.BEMConnectionSettings.getSettings(); }; - -}); \ No newline at end of file + }); diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index b51725977..14d8acc07 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -1,75 +1,75 @@ -import { LocalDt, ServerData } from './serverData' +import { LocalDt, ServerData } from './serverData'; -export type UserInput = ServerData +export type UserInput = ServerData; export type UserInputData = { - end_ts: number, - start_ts: number - label: string, - start_local_dt?: LocalDt - end_local_dt?: LocalDt - status?: string, - match_id?: string, -} + end_ts: number; + start_ts: number; + label: string; + start_local_dt?: LocalDt; + end_local_dt?: LocalDt; + status?: string; + match_id?: string; +}; type ConfirmedPlace = any; // TODO export type CompositeTrip = { - _id: {$oid: string}, - additions: any[], // TODO - cleaned_section_summary: any, // TODO - cleaned_trip: {$oid: string}, - confidence_threshold: number, - confirmed_trip: {$oid: string}, - distance: number, - duration: number, - end_confirmed_place: ConfirmedPlace, - end_fmt_time: string, - end_loc: {type: string, coordinates: number[]}, - end_local_dt: LocalDt, - end_place: {$oid: string}, - end_ts: number, - expectation: any, // TODO "{to_label: boolean}" - expected_trip: {$oid: string}, - inferred_labels: any[], // TODO - inferred_section_summary: any, // TODO - inferred_trip: {$oid: string}, - key: string, - locations: any[], // TODO - origin_key: string, - raw_trip: {$oid: string}, - sections: any[], // TODO - source: string, - start_confirmed_place: ConfirmedPlace, - start_fmt_time: string, - start_loc: {type: string, coordinates: number[]}, - start_local_dt: LocalDt, - start_place: {$oid: string}, - start_ts: number, - user_input: UserInput, -} + _id: { $oid: string }; + additions: any[]; // TODO + cleaned_section_summary: any; // TODO + cleaned_trip: { $oid: string }; + confidence_threshold: number; + confirmed_trip: { $oid: string }; + distance: number; + duration: number; + end_confirmed_place: ConfirmedPlace; + end_fmt_time: string; + end_loc: { type: string; coordinates: number[] }; + end_local_dt: LocalDt; + end_place: { $oid: string }; + end_ts: number; + expectation: any; // TODO "{to_label: boolean}" + expected_trip: { $oid: string }; + inferred_labels: any[]; // TODO + inferred_section_summary: any; // TODO + inferred_trip: { $oid: string }; + key: string; + locations: any[]; // TODO + origin_key: string; + raw_trip: { $oid: string }; + sections: any[]; // TODO + source: string; + start_confirmed_place: ConfirmedPlace; + start_fmt_time: string; + start_loc: { type: string; coordinates: number[] }; + start_local_dt: LocalDt; + start_place: { $oid: string }; + start_ts: number; + user_input: UserInput; +}; export type PopulatedTrip = CompositeTrip & { - additionsList?: any[], // TODO - finalInference?: any, // TODO - geojson?: any, // TODO - getNextEntry?: () => PopulatedTrip | ConfirmedPlace, - userInput?: UserInput, - verifiability?: string, -} + additionsList?: any[]; // TODO + finalInference?: any; // TODO + geojson?: any; // TODO + getNextEntry?: () => PopulatedTrip | ConfirmedPlace; + userInput?: UserInput; + verifiability?: string; +}; export type Trip = { - end_ts: number, - start_ts: number, -} + end_ts: number; + start_ts: number; +}; export type TlEntry = { - key: string, - origin_key: string, - start_ts: number, - end_ts: number, - enter_ts: number, - exit_ts: number, - duration: number, -getNextEntry?: () => PopulatedTrip | ConfirmedPlace, -} \ No newline at end of file + key: string; + origin_key: string; + start_ts: number; + end_ts: number; + enter_ts: number; + exit_ts: number; + duration: number; + getNextEntry?: () => PopulatedTrip | ConfirmedPlace; +}; diff --git a/www/js/types/fileShareTypes.ts b/www/js/types/fileShareTypes.ts index 89481624d..03b41a161 100644 --- a/www/js/types/fileShareTypes.ts +++ b/www/js/types/fileShareTypes.ts @@ -1,11 +1,11 @@ -import { ServerData } from './serverData'; +import { ServerData } from './serverData'; export type TimeStampData = ServerData; export type RawTimelineData = { - name: string, - ts: number, - reading: number, + name: string; + ts: number; + reading: number; }; export interface FsWindow extends Window { @@ -13,10 +13,10 @@ export interface FsWindow extends Window { type: number, size: number, successCallback: (fs: any) => void, - errorCallback?: (error: any) => void + errorCallback?: (error: any) => void, ) => void; LocalFileSystem: { TEMPORARY: number; PERSISTENT: number; }; -}; +} diff --git a/www/js/types/serverData.ts b/www/js/types/serverData.ts index 8b14b79df..9a15ff996 100644 --- a/www/js/types/serverData.ts +++ b/www/js/types/serverData.ts @@ -1,39 +1,39 @@ export type ServerResponse = { - phone_data: Array>, -} + phone_data: Array>; +}; export type ServerData = { - data: Type, - metadata: MetaData, - key?: string, - user_id?: { $uuid: string, }, - _id?: { $oid: string, }, + data: Type; + metadata: MetaData; + key?: string; + user_id?: { $uuid: string }; + _id?: { $oid: string }; }; export type MetaData = { - key: string, - platform: string, - write_ts: number, - time_zone: string, - write_fmt_time: string, - write_local_dt: LocalDt, - origin_key?: string, - read_ts?: number, + key: string; + platform: string; + write_ts: number; + time_zone: string; + write_fmt_time: string; + write_local_dt: LocalDt; + origin_key?: string; + read_ts?: number; }; - + export type LocalDt = { - minute: number, - hour: number, - second: number, - day: number, - weekday: number, - month: number, - year: number, - timezone: string, + minute: number; + hour: number; + second: number; + day: number; + weekday: number; + month: number; + year: number; + timezone: string; }; export type TimeQuery = { key: string; startTs: number; endTs: number; -} \ No newline at end of file +}; diff --git a/www/js/unifiedDataLoader.ts b/www/js/unifiedDataLoader.ts index 15bcd341d..dc0866be3 100644 --- a/www/js/unifiedDataLoader.ts +++ b/www/js/unifiedDataLoader.ts @@ -1,103 +1,124 @@ -import { logDebug } from './plugin/logger' +import { logDebug } from './plugin/logger'; import { getRawEntries } from './commHelper'; import { ServerResponse, ServerData, TimeQuery } from './types/serverData'; /** - * combineWithDedup is a helper function for combinedPromises + * combineWithDedup is a helper function for combinedPromises * @param list1 values evaluated from a BEMUserCache promise - * @param list2 same as list1 + * @param list2 same as list1 * @returns a dedup array generated from the input lists */ -export const combineWithDedup = function(list1: Array>, list2: Array) { - const combinedList = list1.concat(list2); - return combinedList.filter(function(value, i, array) { - const firstIndexOfValue = array.findIndex(function(element) { - return element.metadata.write_ts == value.metadata.write_ts; - }); - return firstIndexOfValue == i; +export const combineWithDedup = function (list1: Array>, list2: Array) { + const combinedList = list1.concat(list2); + return combinedList.filter(function (value, i, array) { + const firstIndexOfValue = array.findIndex(function (element) { + return element.metadata.write_ts == value.metadata.write_ts; }); + return firstIndexOfValue == i; + }); }; /** - * combinedPromises is a recursive function that joins multiple promises - * @param promiseList 1 or more promises + * combinedPromises is a recursive function that joins multiple promises + * @param promiseList 1 or more promises * @param combiner a function that takes two arrays and joins them * @returns A promise which evaluates to a combined list of values or errors */ -export const combinedPromises = function(promiseList: Array>, - combiner: (list1: Array, list2: Array) => Array ) { - if (promiseList.length === 0) { - throw new RangeError('combinedPromises needs input array.length >= 1'); - } - return new Promise(function(resolve, reject) { - var firstResult = []; - var firstError = null; +export const combinedPromises = function ( + promiseList: Array>, + combiner: (list1: Array, list2: Array) => Array, +) { + if (promiseList.length === 0) { + throw new RangeError('combinedPromises needs input array.length >= 1'); + } + return new Promise(function (resolve, reject) { + var firstResult = []; + var firstError = null; - var nextResult = []; - var nextError = null; + var nextResult = []; + var nextError = null; - var firstPromiseDone = false; - var nextPromiseDone = false; + var firstPromiseDone = false; + var nextPromiseDone = false; - const checkAndResolve = function() { - if (firstPromiseDone && nextPromiseDone) { - if (firstError && nextError) { - reject([firstError, nextError]); - } else { - logDebug(`About to dedup firstResult = ${firstResult.length}` + - ` nextResult = ${nextResult.length}`); - const dedupedList = combiner(firstResult, nextResult); - logDebug(`Deduped list = ${dedupedList.length}`); - resolve(dedupedList); - } + const checkAndResolve = function () { + if (firstPromiseDone && nextPromiseDone) { + if (firstError && nextError) { + reject([firstError, nextError]); + } else { + logDebug( + `About to dedup firstResult = ${firstResult.length}` + + ` nextResult = ${nextResult.length}`, + ); + const dedupedList = combiner(firstResult, nextResult); + logDebug(`Deduped list = ${dedupedList.length}`); + resolve(dedupedList); } - }; - - if (promiseList.length === 1) { - return promiseList[0].then(function(result: Array) { + } + }; + + if (promiseList.length === 1) { + return promiseList[0].then( + function (result: Array) { resolve(result); - }, function (err) { + }, + function (err) { reject([err]); - }); - } + }, + ); + } - const firstPromise = promiseList[0]; - const nextPromise = combinedPromises(promiseList.slice(1), combiner); - - firstPromise.then(function(currentFirstResult: Array) { - firstResult = currentFirstResult; - firstPromiseDone = true; - }, function(error) { - firstResult = []; - firstError = error; - nextPromiseDone = true; - }).then(checkAndResolve); + const firstPromise = promiseList[0]; + const nextPromise = combinedPromises(promiseList.slice(1), combiner); - nextPromise.then(function(currentNextResult: Array) { - nextResult = currentNextResult; - nextPromiseDone = true; - }, function(error) { - nextResult = []; - nextError = error; - }).then(checkAndResolve); - }); + firstPromise + .then( + function (currentFirstResult: Array) { + firstResult = currentFirstResult; + firstPromiseDone = true; + }, + function (error) { + firstResult = []; + firstError = error; + nextPromiseDone = true; + }, + ) + .then(checkAndResolve); + + nextPromise + .then( + function (currentNextResult: Array) { + nextResult = currentNextResult; + nextPromiseDone = true; + }, + function (error) { + nextResult = []; + nextError = error; + }, + ) + .then(checkAndResolve); + }); }; /** - * getUnifiedDataForInterval is a generalized method to fetch data by its timestamps + * getUnifiedDataForInterval is a generalized method to fetch data by its timestamps * @param key string corresponding to a data entry * @param tq an object that contains interval start and end times * @param getMethod a BEMUserCache method that fetches certain data via a promise * @returns A promise that evaluates to the all values found within the queried data */ -export const getUnifiedDataForInterval = function(key: string, tq: TimeQuery, - getMethod: (key: string, tq: TimeQuery, flag: boolean) => Promise) { - const test = true; - const getPromise = getMethod(key, tq, test); - const remotePromise = getRawEntries([key], tq.startTs, tq.endTs) - .then(function(serverResponse: ServerResponse) { - return serverResponse.phone_data; - }); - var promiseList = [getPromise, remotePromise] - return combinedPromises(promiseList, combineWithDedup); -}; \ No newline at end of file +export const getUnifiedDataForInterval = function ( + key: string, + tq: TimeQuery, + getMethod: (key: string, tq: TimeQuery, flag: boolean) => Promise, +) { + const test = true; + const getPromise = getMethod(key, tq, test); + const remotePromise = getRawEntries([key], tq.startTs, tq.endTs).then(function ( + serverResponse: ServerResponse, + ) { + return serverResponse.phone_data; + }); + var promiseList = [getPromise, remotePromise]; + return combinedPromises(promiseList, combineWithDedup); +}; From 6dcf1abb6722931b69bf8cef807c42df4e62ede7 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:46:39 -0800 Subject: [PATCH 08/52] Removed $ionicLoading - Behavior remains unchanged when removing this component - labelTab and other parent components handle loading, so the ionic service is not needed. --- www/js/diary/diaryTypes.ts | 72 ---------------------------------- www/js/diary/timelineHelper.ts | 29 +++----------- www/js/types/labelTypes.ts | 24 ++++++++++++ 3 files changed, 30 insertions(+), 95 deletions(-) delete mode 100644 www/js/diary/diaryTypes.ts create mode 100644 www/js/types/labelTypes.ts diff --git a/www/js/diary/diaryTypes.ts b/www/js/diary/diaryTypes.ts deleted file mode 100644 index 5755c91ab..000000000 --- a/www/js/diary/diaryTypes.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* These type definitions are a work in progress. The goal is to have a single source of truth for - the types of the trip / place / untracked objects and all properties they contain. - Since we are using TypeScript now, we should strive to enforce type safety and also benefit from - IntelliSense and other IDE features. */ - -// Since it is WIP, these types are not used anywhere yet. - -type ConfirmedPlace = any; // TODO - -/* These are the properties received from the server (basically matches Python code) - This should match what Timeline.readAllCompositeTrips returns (an array of these objects) */ -export type CompositeTrip = { - _id: { $oid: string }; - additions: any[]; // TODO - cleaned_section_summary: any; // TODO - cleaned_trip: { $oid: string }; - confidence_threshold: number; - confirmed_trip: { $oid: string }; - distance: number; - duration: number; - end_confirmed_place: ConfirmedPlace; - end_fmt_time: string; - end_loc: { type: string; coordinates: number[] }; - end_local_dt: any; // TODO - end_place: { $oid: string }; - end_ts: number; - expectation: any; // TODO "{to_label: boolean}" - expected_trip: { $oid: string }; - inferred_labels: any[]; // TODO - inferred_section_summary: any; // TODO - inferred_trip: { $oid: string }; - key: string; - locations: any[]; // TODO - origin_key: string; - raw_trip: { $oid: string }; - sections: any[]; // TODO - source: string; - start_confirmed_place: ConfirmedPlace; - start_fmt_time: string; - start_loc: { type: string; coordinates: number[] }; - start_local_dt: any; // TODO - start_place: { $oid: string }; - start_ts: number; - user_input: any; // TODO -}; - -/* These properties aren't received from the server, but are derived from the above properties. - They are used in the UI to display trip/place details and are computed by the useDerivedProperties hook. */ -export type DerivedProperties = { - displayDate: string; - displayStartTime: string; - displayEndTime: string; - displayTime: string; - displayStartDateAbbr: string; - displayEndDateAbbr: string; - formattedDistance: string; - formattedSectionProperties: any[]; // TODO - distanceSuffix: string; - detectedModes: { mode: string; icon: string; color: string; pct: number | string }[]; -}; - -/* These are the properties that are still filled in by some kind of 'populate' mechanism. - It would simplify the codebase to just compute them where they're needed - (using memoization when apt so performance is not impacted). */ -export type PopulatedTrip = CompositeTrip & { - additionsList?: any[]; // TODO - finalInference?: any; // TODO - geojson?: any; // TODO - getNextEntry?: () => PopulatedTrip | ConfirmedPlace; - userInput?: any; // TODO - verifiability?: string; -}; diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 72917f00b..07cdb6dc8 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -7,12 +7,15 @@ import { ServerResponse, ServerData } from '../types/serverData'; import L from 'leaflet'; import i18next from 'i18next'; import { DateTime } from 'luxon'; +import { CompositeTrip } from '../types/diaryTypes'; +import { LabelOptions } from '../types/labelTypes'; const cachedGeojsons = new Map(); + /** * @description Gets a formatted GeoJSON object for a trip, including the start and end places and the trajectory. */ -export function useGeojsonForTrip(trip, labelOptions, labeledMode?) { +export function useGeojsonForTrip(trip: CompositeTrip, labelOptions: LabelOptions, labeledMode?) { if (!trip) return; const gjKey = `trip-${trip._id.$oid}-${labeledMode || 'detected'}`; if (cachedGeojsons.has(gjKey)) { @@ -230,14 +233,9 @@ const unpackServerData = (obj: ServerData) => ({ }); export const readAllCompositeTrips = function (startTs: number, endTs: number) { - const $ionicLoading = getAngularService('$ionicLoading'); - $ionicLoading.show({ - template: i18next.t('service.reading-server'), - }); const readPromises = [getRawEntries(['analysis/composite_trip'], startTs, endTs, 'data.end_ts')]; return Promise.all(readPromises) .then(([ctList]: [ServerResponse]) => { - $ionicLoading.hide(); return ctList.phone_data.map((ct) => { const unpackedCt = unpackServerData(ct); return { @@ -251,7 +249,6 @@ export const readAllCompositeTrips = function (startTs: number, endTs: number) { }) .catch((err) => { displayError(err, 'while reading confirmed trips'); - $ionicLoading.hide(); return []; }); }; @@ -271,14 +268,7 @@ const dateTime2localdate = function (currtime: DateTime, tz: string) { second: currtime.get('second'), }; }; -/* locationPoints are of form: - * ServerData - * Point = { - * currentState: string, - * transition: string, - * ts: number, // 1698433683.712 - * } - */ + const points2TripProps = function (locationPoints) { const startPoint = locationPoints[0]; const endPoint = locationPoints[locationPoints.length - 1]; @@ -520,11 +510,6 @@ const linkTrips = function (trip1, trip2) { }; export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) { - const $ionicLoading = getAngularService('$ionicLoading'); - $ionicLoading.show({ - template: i18next.t('service.reading-unprocessed-data'), - }); - var tq = { key: 'write_ts', startTs, endTs }; logDebug( 'about to query for unprocessed trips from ' + @@ -538,14 +523,13 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) ) { if (transitionList.length == 0) { logDebug('No unprocessed trips. yay!'); - $ionicLoading.hide(); return []; } else { logDebug(`Found ${transitionList.length} transitions. yay!`); const tripsList = transitions2Trips(transitionList); logDebug(`Mapped into ${tripsList.length} trips. yay!`); tripsList.forEach(function (trip) { - console.log(JSON.stringify(trip)); + logDebug(JSON.stringify(trip)); }); var tripFillPromises = tripsList.map(transitionTrip2TripObj); return Promise.all(tripFillPromises).then(function (raw_trip_gj_list) { @@ -577,7 +561,6 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) logDebug('linking unprocessed and processed trip chains'); linkTrips(lastProcessedTrip, trip_gj_list[0]); } - $ionicLoading.hide(); logDebug(`Returning final list of size ${trip_gj_list.length}`); return trip_gj_list; }); diff --git a/www/js/types/labelTypes.ts b/www/js/types/labelTypes.ts new file mode 100644 index 000000000..9719531a8 --- /dev/null +++ b/www/js/types/labelTypes.ts @@ -0,0 +1,24 @@ +export type LabelOptions = { + MODE: Array; + PURPOSE: Array; + REPLACE_MODE: Array; +}; + +export type ModeLabel = { + value: string; + baseMode: string; + met_equivalent?: string; + met?: { + ALL: { + range: Array; + mets: number; + }; + }; + kgCo2PerKm: number; + test: string; +}; + +export type PurposeReplaceLabel = { + value: string; + test: string; +}; From 2bcdac80931927378fd58b246c120d3d29cfe1ed Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:22:14 -0800 Subject: [PATCH 09/52] Added basic timelineHelper tests - Added test for each diaryService rewrite --- www/__mocks__/cordovaMocks.ts | 4 +- www/__tests__/timelineHelper.test.ts | 121 +++++++++++++++++++++++++++ www/js/diary/timelineHelper.ts | 13 +-- www/js/types/diaryTypes.ts | 4 +- 4 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 www/__tests__/timelineHelper.test.ts diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 62aa9be1a..f8d7ae74f 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -1,5 +1,4 @@ import packageJsonBuild from '../../package.cordovabuild.json'; - export const mockCordova = () => { window['cordova'] ||= {}; window['cordova'].platformId ||= 'ios'; @@ -116,6 +115,9 @@ export const mockBEMUserCache = () => { return false; } }, + getMessagesForInterval: () => { + // Used for getUnifiedDataForInterval + }, }; window['cordova'] ||= {}; window['cordova'].plugins ||= {}; diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts new file mode 100644 index 000000000..c6a4bcd67 --- /dev/null +++ b/www/__tests__/timelineHelper.test.ts @@ -0,0 +1,121 @@ +import { mockLogger } from '../__mocks__/globalMocks'; +import { readAllCompositeTrips, readUnprocessedTrips } from '../js/diary/timelineHelper'; +import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; + +import { MetaData, ServerResponse } from '../js/types/serverData'; +import { CompositeTrip } from '../js/types/diaryTypes'; + +mockLogger(); +mockBEMUserCache(); +const mockMetaData: MetaData = { + write_ts: -13885091, + key: 'test/value', + platform: 'test', + time_zone: 'America/Los_Angeles', + write_fmt_time: '1969-07-16T07:01:49.000Z', + write_local_dt: null, + origin_key: '12345', +}; +const mockData: ServerResponse = { + phone_data: [ + { + data: { + _id: null, + additions: [], + cleaned_section_summary: null, // TODO + cleaned_trip: null, //ObjId; + confidence_threshold: -1, + confirmed_trip: null, //ObjId; + distance: 777, + duration: 777, + end_confirmed_place: { + data: null, + metadata: JSON.parse(JSON.stringify(mockMetaData)), + }, + end_fmt_time: '2023-11-01T17:55:20.999397-07:00', + end_loc: { + type: 'Point', + coordinates: [-1, -1], + }, + end_local_dt: null, //LocalDt; + end_place: null, //ObjId; + end_ts: -1, + expectation: null, // TODO "{to_label: boolean}" + expected_trip: null, //ObjId; + inferred_labels: [], // TODO + inferred_section_summary: { + count: { + CAR: 1, + WALKING: 1, + }, + distance: { + CAR: 222, + WALKING: 222, + }, + duration: { + CAR: 333, + WALKING: 333, + }, + }, + inferred_trip: null, + key: '12345', + locations: [ + { + metadata: JSON.parse(JSON.stringify(mockMetaData)), + data: null, + }, + ], // LocationType + origin_key: '', + raw_trip: null, + sections: [ + { + metadata: JSON.parse(JSON.stringify(mockMetaData)), + data: null, + }, + ], // TODO + source: 'DwellSegmentationDistFilter', + start_confirmed_place: { + data: null, + metadata: JSON.parse(JSON.stringify(mockMetaData)), + }, + start_fmt_time: '2023-11-01T17:55:20.999397-07:00', + start_loc: { + type: 'Point', + coordinates: [-1, -1], + }, + start_local_dt: null, + start_place: null, + start_ts: null, + user_input: null, + }, + metadata: JSON.parse(JSON.stringify(mockMetaData)), + }, + ], +}; + +const testStart = -14576291; +const testEnd = -13885091; + +jest.mock('../js/commHelper', () => ({ + getRawEntries: jest.fn(() => mockData), +})); + +it('fetches a composite trip object and collapses it', async () => { + // When we have End-to-End testing, we can properly test with getRawEnteries + console.log(JSON.stringify(mockData, null, 2)); + expect(readAllCompositeTrips(testStart, testEnd)).resolves.not.toThrow(); +}); + +jest.mock('../js/unifiedDataLoader', () => ({ + getUnifiedDataForInterval: jest.fn(() => { + return Promise.resolve([]); + }), +})); + +it('works when there are no unprocessed trips...', async () => { + expect(readUnprocessedTrips(testStart, testEnd, null)).resolves.not.toThrow(); +}); + +it('works when there are no unprocessed trips...', async () => { + expect(readUnprocessedTrips(testStart, testEnd, null)).resolves.not.toThrow(); +}); diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 07cdb6dc8..83d9cbf1c 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -1,4 +1,3 @@ -import { getAngularService } from '../angular-react-helper'; import { displayError, logDebug } from '../plugin/logger'; import { getBaseModeByKey, getBaseModeOfLabeledTrip } from './diaryHelper'; import { getUnifiedDataForInterval } from '../unifiedDataLoader'; @@ -7,7 +6,7 @@ import { ServerResponse, ServerData } from '../types/serverData'; import L from 'leaflet'; import i18next from 'i18next'; import { DateTime } from 'luxon'; -import { CompositeTrip } from '../types/diaryTypes'; +import { CompositeTrip, TripTransition, SectionData, Trip } from '../types/diaryTypes'; import { LabelOptions } from '../types/labelTypes'; const cachedGeojsons = new Map(); @@ -235,7 +234,7 @@ const unpackServerData = (obj: ServerData) => ({ export const readAllCompositeTrips = function (startTs: number, endTs: number) { const readPromises = [getRawEntries(['analysis/composite_trip'], startTs, endTs, 'data.end_ts')]; return Promise.all(readPromises) - .then(([ctList]: [ServerResponse]) => { + .then(([ctList]: [ServerResponse]) => { return ctList.phone_data.map((ct) => { const unpackedCt = unpackServerData(ct); return { @@ -439,7 +438,7 @@ const isEndingTransition = function (transWrapper) { * * Let's abstract this out into our own minor state machine. */ -const transitions2Trips = function (transitionList) { +const transitions2Trips = function (transitionList: Array) { var inTrip = false; var tripList = []; var currStartTransitionIndex = -1; @@ -517,9 +516,11 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) DateTime.fromSeconds(tq.endTs).toLocaleString(DateTime.DATETIME_MED), ); + console.log('Testing...'); const getMessageMethod = window['cordova'].plugins.BEMUserCache.getMessagesForInterval; + console.log('Entering...'); return getUnifiedDataForInterval('statemachine/transition', tq, getMessageMethod).then(function ( - transitionList: Array, + transitionList: Array, ) { if (transitionList.length == 0) { logDebug('No unprocessed trips. yay!'); @@ -529,7 +530,7 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) const tripsList = transitions2Trips(transitionList); logDebug(`Mapped into ${tripsList.length} trips. yay!`); tripsList.forEach(function (trip) { - logDebug(JSON.stringify(trip)); + logDebug(JSON.stringify(trip, null, 2)); }); var tripFillPromises = tripsList.map(transitionTrip2TripObj); return Promise.all(tripFillPromises).then(function (raw_trip_gj_list) { diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 4f10fb50f..933e2db68 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -55,7 +55,7 @@ export type CompositeTrip = { confirmed_trip: ObjId; distance: number; duration: number; - end_confirmed_place: ConfirmedPlace; + end_confirmed_place: ServerData; end_fmt_time: string; end_loc: { type: string; coordinates: number[] }; end_local_dt: LocalDt; @@ -72,7 +72,7 @@ export type CompositeTrip = { raw_trip: ObjId; sections: any[]; // TODO source: string; - start_confirmed_place: ConfirmedPlace; + start_confirmed_place: ServerData; start_fmt_time: string; start_loc: { type: string; coordinates: number[] }; start_local_dt: LocalDt; From ae5ae954fa1d5afe86f5618a8a80840f3e41b1cf Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:22:51 -0800 Subject: [PATCH 10/52] Updated issue with index.html --- www/index.html | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/www/index.html b/www/index.html index b46904cca..72c75eb01 100644 --- a/www/index.html +++ b/www/index.html @@ -1,22 +1,18 @@ - - - + + + - + - + -
+
From efa96e0aacef2f1d8b1839fd45db8d92a5143983 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 9 Nov 2023 09:18:55 -0800 Subject: [PATCH 11/52] Improved mocks, added unit tests --- www/__tests__/timelineHelper.test.ts | 40 ++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index c6a4bcd67..15f63cdc3 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -7,6 +7,11 @@ import { CompositeTrip } from '../js/types/diaryTypes'; mockLogger(); mockBEMUserCache(); + +afterAll(() => { + jest.restoreAllMocks(); +}); + const mockMetaData: MetaData = { write_ts: -13885091, key: 'test/value', @@ -16,6 +21,7 @@ const mockMetaData: MetaData = { write_local_dt: null, origin_key: '12345', }; + const mockData: ServerResponse = { phone_data: [ { @@ -93,17 +99,35 @@ const mockData: ServerResponse = { ], }; -const testStart = -14576291; -const testEnd = -13885091; +// When called by mocks, pair 1 returns 1 value, Pair two 2, pair 3 returns none. +const fakeStartTsOne = -14576291; +const fakeEndTsOne = -13885091; +const fakeStartTsTwo = 1092844665; +const fakeEndTsTwo = 1277049465; +// Once we have end-to-end testing, we could utilize getRawEnteries. jest.mock('../js/commHelper', () => ({ - getRawEntries: jest.fn(() => mockData), + getRawEntries: jest.fn((val, startTs, endTs, valTwo) => { + if (startTs === fakeStartTsOne && endTs === fakeEndTsOne) return mockData; + if (startTs == fakeStartTsTwo && endTs === fakeEndTsTwo) { + console.log('Twoy!'); + let dataCopy = JSON.parse(JSON.stringify(mockData)); + let temp = [dataCopy.phone_data[0], dataCopy.phone_data[0]]; + dataCopy.phone_data = temp; + console.log(`This is phoneData: ${JSON.stringify(dataCopy.phone_data.length)}`); + return dataCopy; + } + return {}; + }), })); +it('works when there are no composite trip objects fetched', async () => { + expect(readAllCompositeTrips(-1, -1)).resolves.not.toThrow(); +}); + it('fetches a composite trip object and collapses it', async () => { - // When we have End-to-End testing, we can properly test with getRawEnteries - console.log(JSON.stringify(mockData, null, 2)); - expect(readAllCompositeTrips(testStart, testEnd)).resolves.not.toThrow(); + expect(readAllCompositeTrips(fakeStartTsOne, fakeEndTsOne)).resolves.not.toThrow(); + expect(readAllCompositeTrips(fakeStartTsTwo, fakeEndTsTwo)).resolves.not.toThrow(); }); jest.mock('../js/unifiedDataLoader', () => ({ @@ -113,9 +137,9 @@ jest.mock('../js/unifiedDataLoader', () => ({ })); it('works when there are no unprocessed trips...', async () => { - expect(readUnprocessedTrips(testStart, testEnd, null)).resolves.not.toThrow(); + expect(readUnprocessedTrips(fakeStartTsOne, fakeEndTsOne, null)).resolves.not.toThrow(); }); it('works when there are no unprocessed trips...', async () => { - expect(readUnprocessedTrips(testStart, testEnd, null)).resolves.not.toThrow(); + expect(readUnprocessedTrips(fakeStartTsOne, fakeEndTsOne, null)).resolves.not.toThrow(); }); From 13206e948a85dc0397f40eea7a70aebef077c91f Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 9 Nov 2023 10:50:30 -0800 Subject: [PATCH 12/52] Updated mocks, added tests --- www/__tests__/timelineHelper.test.ts | 42 ++++++++++++++++++---------- www/js/diary/timelineHelper.ts | 6 ++-- www/js/types/diaryTypes.ts | 2 +- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 15f63cdc3..0ae65d34c 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -2,8 +2,8 @@ import { mockLogger } from '../__mocks__/globalMocks'; import { readAllCompositeTrips, readUnprocessedTrips } from '../js/diary/timelineHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; -import { MetaData, ServerResponse } from '../js/types/serverData'; -import { CompositeTrip } from '../js/types/diaryTypes'; +import { MetaData, ServerData, ServerResponse } from '../js/types/serverData'; +import { CompositeTrip, TripTransition } from '../js/types/diaryTypes'; mockLogger(); mockBEMUserCache(); @@ -99,6 +99,22 @@ const mockData: ServerResponse = { ], }; +let mockDataTwo = mockData; +mockDataTwo.phone_data = [mockData.phone_data[0], mockData.phone_data[0]]; + +const mockTransition: Array> = [ + { + data: { + currstate: 'STATE_WAITING_FOR_TRIP_TO_START', + transition: 'T_NOP', + ts: 12345.6789, + }, + metadata: mockMetaData, + }, +]; + +const mockTransitionTwo = mockTransition.push(mockTransition[0]); + // When called by mocks, pair 1 returns 1 value, Pair two 2, pair 3 returns none. const fakeStartTsOne = -14576291; const fakeEndTsOne = -13885091; @@ -107,16 +123,9 @@ const fakeEndTsTwo = 1277049465; // Once we have end-to-end testing, we could utilize getRawEnteries. jest.mock('../js/commHelper', () => ({ - getRawEntries: jest.fn((val, startTs, endTs, valTwo) => { - if (startTs === fakeStartTsOne && endTs === fakeEndTsOne) return mockData; - if (startTs == fakeStartTsTwo && endTs === fakeEndTsTwo) { - console.log('Twoy!'); - let dataCopy = JSON.parse(JSON.stringify(mockData)); - let temp = [dataCopy.phone_data[0], dataCopy.phone_data[0]]; - dataCopy.phone_data = temp; - console.log(`This is phoneData: ${JSON.stringify(dataCopy.phone_data.length)}`); - return dataCopy; - } + getRawEntries: jest.fn((key, startTs, endTs, valTwo) => { + if (startTs === fakeStartTsOne) return mockData; + if (startTs == fakeStartTsTwo) return mockDataTwo; return {}; }), })); @@ -131,15 +140,18 @@ it('fetches a composite trip object and collapses it', async () => { }); jest.mock('../js/unifiedDataLoader', () => ({ - getUnifiedDataForInterval: jest.fn(() => { + getUnifiedDataForInterval: jest.fn((key, tq, combiner) => { + if (tq.startTs === fakeStartTsOne) return Promise.resolve(mockTransition); + if (tq.startTs === fakeStartTsTwo) return Promise.resolve(mockTransitionTwo); return Promise.resolve([]); }), })); it('works when there are no unprocessed trips...', async () => { - expect(readUnprocessedTrips(fakeStartTsOne, fakeEndTsOne, null)).resolves.not.toThrow(); + expect(readUnprocessedTrips(-1, -1, null)).resolves.not.toThrow(); }); -it('works when there are no unprocessed trips...', async () => { +it('works when there are one or more unprocessed trips...', async () => { expect(readUnprocessedTrips(fakeStartTsOne, fakeEndTsOne, null)).resolves.not.toThrow(); + expect(readUnprocessedTrips(fakeStartTsTwo, fakeEndTsTwo, null)).resolves.not.toThrow(); }); diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 83d9cbf1c..47f17e05e 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -6,7 +6,7 @@ import { ServerResponse, ServerData } from '../types/serverData'; import L from 'leaflet'; import i18next from 'i18next'; import { DateTime } from 'luxon'; -import { CompositeTrip, TripTransition, SectionData, Trip } from '../types/diaryTypes'; +import { CompositeTrip, TripTransition, SectionData } from '../types/diaryTypes'; import { LabelOptions } from '../types/labelTypes'; const cachedGeojsons = new Map(); @@ -438,7 +438,7 @@ const isEndingTransition = function (transWrapper) { * * Let's abstract this out into our own minor state machine. */ -const transitions2Trips = function (transitionList: Array) { +const transitions2Trips = function (transitionList: Array>) { var inTrip = false; var tripList = []; var currStartTransitionIndex = -1; @@ -520,7 +520,7 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) const getMessageMethod = window['cordova'].plugins.BEMUserCache.getMessagesForInterval; console.log('Entering...'); return getUnifiedDataForInterval('statemachine/transition', tq, getMessageMethod).then(function ( - transitionList: Array, + transitionList: Array>, ) { if (transitionList.length == 0) { logDebug('No unprocessed trips. yay!'); diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 6088eb64b..938b06851 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -3,7 +3,7 @@ As much as possible, these types parallel the types used in the server code. */ import { BaseModeKey, MotionTypeKey } from '../diary/diaryHelper'; -import { LocalDt } from './serverData'; +import { ServerData, LocalDt } from './serverData'; type ObjectId = { $oid: string }; type ConfirmedPlace = { From f031d331fb27fc60cfe6e12c735b54aa44653b32 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:40:52 -0800 Subject: [PATCH 13/52] Moved mockData to separate file for legibility --- www/__mocks__/timelineHelperMocks.ts | 111 ++++++++++++++++++++++ www/__tests__/timelineHelper.test.ts | 136 ++++----------------------- 2 files changed, 128 insertions(+), 119 deletions(-) create mode 100644 www/__mocks__/timelineHelperMocks.ts diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts new file mode 100644 index 000000000..0b951174d --- /dev/null +++ b/www/__mocks__/timelineHelperMocks.ts @@ -0,0 +1,111 @@ +import { MetaData, ServerData, ServerResponse } from '../js/types/serverData'; +import { CompositeTrip, TripTransition } from '../js/types/diaryTypes'; + +const mockMetaData: MetaData = { + write_ts: -13885091, + key: 'test/value', + platform: 'test', + time_zone: 'America/Los_Angeles', + write_fmt_time: '1969-07-16T07:01:49.000Z', + write_local_dt: null, + origin_key: '12345', +}; + +export const mockData: ServerResponse = { + phone_data: [ + { + data: { + _id: null, + additions: [], + cleaned_section_summary: null, + cleaned_trip: null, + confidence_threshold: -1, + confirmed_trip: null, + distance: 777, + duration: 777, + end_confirmed_place: { + data: null, + metadata: JSON.parse(JSON.stringify(mockMetaData)), + }, + end_fmt_time: '2023-11-01T17:55:20.999397-07:00', + end_loc: { + type: 'Point', + coordinates: [-1, -1], + }, + end_local_dt: null, + end_place: null, + end_ts: -1, + expectation: null, + expected_trip: null, + inferred_labels: [], + inferred_section_summary: { + count: { + CAR: 1, + WALKING: 1, + }, + distance: { + CAR: 222, + WALKING: 222, + }, + duration: { + CAR: 333, + WALKING: 333, + }, + }, + inferred_trip: null, + key: '12345', + locations: [ + { + metadata: JSON.parse(JSON.stringify(mockMetaData)), + data: null, + }, + ], + origin_key: '', + raw_trip: null, + sections: [ + { + metadata: JSON.parse(JSON.stringify(mockMetaData)), + data: null, + }, + ], + source: 'DwellSegmentationDistFilter', + start_confirmed_place: { + data: null, + metadata: JSON.parse(JSON.stringify(mockMetaData)), + }, + start_fmt_time: '2023-11-01T17:55:20.999397-07:00', + start_loc: { + type: 'Point', + coordinates: [-1, -1], + }, + start_local_dt: null, + start_place: null, + start_ts: null, + user_input: null, + }, + metadata: JSON.parse(JSON.stringify(mockMetaData)), + }, + ], +}; +export const mockDataTwo = { + phone_data: [mockData.phone_data[0], mockData.phone_data[0]], +}; + +export const mockTransition: Array> = [ + { + data: { + currstate: 'STATE_WAITING_FOR_TRIP_TO_START', + transition: 'T_NOP', + ts: 12345.6789, + }, + metadata: mockMetaData, + }, +]; + +export const mockTransitionTwo = mockTransition.push(mockTransition[0]); + +// When called by mocks, pair 1 returns 1 value, Pair two 2, pair 3 returns none. +export const fakeStartTsOne = -14576291; +export const fakeEndTsOne = -13885091; +export const fakeStartTsTwo = 1092844665; +export const fakeEndTsTwo = 1277049465; diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 0ae65d34c..0faac2c2e 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -2,8 +2,7 @@ import { mockLogger } from '../__mocks__/globalMocks'; import { readAllCompositeTrips, readUnprocessedTrips } from '../js/diary/timelineHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; -import { MetaData, ServerData, ServerResponse } from '../js/types/serverData'; -import { CompositeTrip, TripTransition } from '../js/types/diaryTypes'; +import * as mockTLH from '../__mocks__/timelineHelperMocks'; mockLogger(); mockBEMUserCache(); @@ -12,120 +11,11 @@ afterAll(() => { jest.restoreAllMocks(); }); -const mockMetaData: MetaData = { - write_ts: -13885091, - key: 'test/value', - platform: 'test', - time_zone: 'America/Los_Angeles', - write_fmt_time: '1969-07-16T07:01:49.000Z', - write_local_dt: null, - origin_key: '12345', -}; - -const mockData: ServerResponse = { - phone_data: [ - { - data: { - _id: null, - additions: [], - cleaned_section_summary: null, // TODO - cleaned_trip: null, //ObjId; - confidence_threshold: -1, - confirmed_trip: null, //ObjId; - distance: 777, - duration: 777, - end_confirmed_place: { - data: null, - metadata: JSON.parse(JSON.stringify(mockMetaData)), - }, - end_fmt_time: '2023-11-01T17:55:20.999397-07:00', - end_loc: { - type: 'Point', - coordinates: [-1, -1], - }, - end_local_dt: null, //LocalDt; - end_place: null, //ObjId; - end_ts: -1, - expectation: null, // TODO "{to_label: boolean}" - expected_trip: null, //ObjId; - inferred_labels: [], // TODO - inferred_section_summary: { - count: { - CAR: 1, - WALKING: 1, - }, - distance: { - CAR: 222, - WALKING: 222, - }, - duration: { - CAR: 333, - WALKING: 333, - }, - }, - inferred_trip: null, - key: '12345', - locations: [ - { - metadata: JSON.parse(JSON.stringify(mockMetaData)), - data: null, - }, - ], // LocationType - origin_key: '', - raw_trip: null, - sections: [ - { - metadata: JSON.parse(JSON.stringify(mockMetaData)), - data: null, - }, - ], // TODO - source: 'DwellSegmentationDistFilter', - start_confirmed_place: { - data: null, - metadata: JSON.parse(JSON.stringify(mockMetaData)), - }, - start_fmt_time: '2023-11-01T17:55:20.999397-07:00', - start_loc: { - type: 'Point', - coordinates: [-1, -1], - }, - start_local_dt: null, - start_place: null, - start_ts: null, - user_input: null, - }, - metadata: JSON.parse(JSON.stringify(mockMetaData)), - }, - ], -}; - -let mockDataTwo = mockData; -mockDataTwo.phone_data = [mockData.phone_data[0], mockData.phone_data[0]]; - -const mockTransition: Array> = [ - { - data: { - currstate: 'STATE_WAITING_FOR_TRIP_TO_START', - transition: 'T_NOP', - ts: 12345.6789, - }, - metadata: mockMetaData, - }, -]; - -const mockTransitionTwo = mockTransition.push(mockTransition[0]); - -// When called by mocks, pair 1 returns 1 value, Pair two 2, pair 3 returns none. -const fakeStartTsOne = -14576291; -const fakeEndTsOne = -13885091; -const fakeStartTsTwo = 1092844665; -const fakeEndTsTwo = 1277049465; - // Once we have end-to-end testing, we could utilize getRawEnteries. jest.mock('../js/commHelper', () => ({ getRawEntries: jest.fn((key, startTs, endTs, valTwo) => { - if (startTs === fakeStartTsOne) return mockData; - if (startTs == fakeStartTsTwo) return mockDataTwo; + if (startTs === mockTLH.fakeStartTsOne) return mockTLH.mockData; + if (startTs == mockTLH.fakeStartTsTwo) return mockTLH.mockDataTwo; return {}; }), })); @@ -135,14 +25,18 @@ it('works when there are no composite trip objects fetched', async () => { }); it('fetches a composite trip object and collapses it', async () => { - expect(readAllCompositeTrips(fakeStartTsOne, fakeEndTsOne)).resolves.not.toThrow(); - expect(readAllCompositeTrips(fakeStartTsTwo, fakeEndTsTwo)).resolves.not.toThrow(); + expect( + readAllCompositeTrips(mockTLH.fakeStartTsOne, mockTLH.fakeEndTsOne), + ).resolves.not.toThrow(); + expect( + readAllCompositeTrips(mockTLH.fakeStartTsTwo, mockTLH.fakeEndTsTwo), + ).resolves.not.toThrow(); }); jest.mock('../js/unifiedDataLoader', () => ({ getUnifiedDataForInterval: jest.fn((key, tq, combiner) => { - if (tq.startTs === fakeStartTsOne) return Promise.resolve(mockTransition); - if (tq.startTs === fakeStartTsTwo) return Promise.resolve(mockTransitionTwo); + if (tq.startTs === mockTLH.fakeStartTsOne) return Promise.resolve(mockTLH.mockTransition); + if (tq.startTs === mockTLH.fakeStartTsTwo) return Promise.resolve(mockTLH.mockTransitionTwo); return Promise.resolve([]); }), })); @@ -152,6 +46,10 @@ it('works when there are no unprocessed trips...', async () => { }); it('works when there are one or more unprocessed trips...', async () => { - expect(readUnprocessedTrips(fakeStartTsOne, fakeEndTsOne, null)).resolves.not.toThrow(); - expect(readUnprocessedTrips(fakeStartTsTwo, fakeEndTsTwo, null)).resolves.not.toThrow(); + expect( + readUnprocessedTrips(mockTLH.fakeStartTsOne, mockTLH.fakeEndTsOne, null), + ).resolves.not.toThrow(); + expect( + readUnprocessedTrips(mockTLH.fakeStartTsTwo, mockTLH.fakeEndTsTwo, null), + ).resolves.not.toThrow(); }); From 2e1a402d9d474f8bb5aba88d00781c5aedf16ade Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:47:32 -0800 Subject: [PATCH 14/52] Deepcopy wasn't necessary, removed Parse/stringify --- www/__mocks__/timelineHelperMocks.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index 0b951174d..eb9bc834c 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -25,7 +25,7 @@ export const mockData: ServerResponse = { duration: 777, end_confirmed_place: { data: null, - metadata: JSON.parse(JSON.stringify(mockMetaData)), + metadata: mockMetaData, }, end_fmt_time: '2023-11-01T17:55:20.999397-07:00', end_loc: { @@ -56,7 +56,7 @@ export const mockData: ServerResponse = { key: '12345', locations: [ { - metadata: JSON.parse(JSON.stringify(mockMetaData)), + metadata: mockMetaData, data: null, }, ], @@ -64,14 +64,14 @@ export const mockData: ServerResponse = { raw_trip: null, sections: [ { - metadata: JSON.parse(JSON.stringify(mockMetaData)), + metadata: mockMetaData, data: null, }, ], source: 'DwellSegmentationDistFilter', start_confirmed_place: { data: null, - metadata: JSON.parse(JSON.stringify(mockMetaData)), + metadata: mockMetaData, }, start_fmt_time: '2023-11-01T17:55:20.999397-07:00', start_loc: { @@ -83,7 +83,7 @@ export const mockData: ServerResponse = { start_ts: null, user_input: null, }, - metadata: JSON.parse(JSON.stringify(mockMetaData)), + metadata: mockMetaData, }, ], }; From adab677ab1ac07fb9cf215f92a8e8660773ce8dd Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 9 Nov 2023 13:17:46 -0800 Subject: [PATCH 15/52] Added clarifying comment, adjusted DateTime format --- www/js/diary/timelineHelper.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 47f17e05e..b6bcb1b3b 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -223,7 +223,9 @@ const locations2GeojsonTrajectory = (trip, locationList, trajectoryColor?) => { }); }; -// Remaining functions from /diary/services.js +// DB entries retrieved from the server have '_id', 'metadata', and 'data' fields. +// This function returns a shallow copy of the obj, which flattens the +// 'data' field into the top level, while also including '_id' and 'metadata.key' const unpackServerData = (obj: ServerData) => ({ ...obj.data, _id: obj._id, @@ -251,7 +253,6 @@ export const readAllCompositeTrips = function (startTs: number, endTs: number) { return []; }); }; - const dateTime2localdate = function (currtime: DateTime, tz: string) { return { timezone: tz, @@ -298,8 +299,6 @@ const points2TripProps = function (locationPoints) { speed: speeds[i], })); - // used to mimic old momentJS moment.format() - const formatString = "yyyy-MM-dd'T'HH:mm:ssZZ"; return { _id: { $oid: tripAndSectionId }, key: 'UNPROCESSED_trip', @@ -308,14 +307,14 @@ const points2TripProps = function (locationPoints) { confidence_threshold: 0, distance: dists.reduce((a, b) => a + b, 0), duration: endPoint.data.ts - startPoint.data.ts, - end_fmt_time: endTime.toFormat(formatString), + end_fmt_time: endTime.toISO(), 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.toFormat(formatString), + start_fmt_time: startTime.toISO, start_local_dt: dateTime2localdate(startTime, startPoint.metadata.time_zone), start_ts: startPoint.data.ts, user_input: {}, From 2d0d321676144633f4334a44b4fb88f0714566aa Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 9 Nov 2023 14:01:25 -0800 Subject: [PATCH 16/52] Expanded mocks, sured up tests - borrowed the windowAlert mocks from PR #1093, to allow error checking - added mockCheck, updated some tests to use to `toEqual()` --- www/__mocks__/globalMocks.ts | 16 ++++++ www/__mocks__/timelineHelperMocks.ts | 77 ++++++++++++++++++++++++++++ www/__tests__/timelineHelper.test.ts | 15 ++++-- 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/www/__mocks__/globalMocks.ts b/www/__mocks__/globalMocks.ts index f13cb274b..62ea1b935 100644 --- a/www/__mocks__/globalMocks.ts +++ b/www/__mocks__/globalMocks.ts @@ -1,3 +1,19 @@ export const mockLogger = () => { window['Logger'] = { log: console.log }; }; + +let alerts = []; + +export const mockAlert = () => { + window['alert'] = (message) => { + alerts.push(message); + }; +}; + +export const clearAlerts = () => { + alerts = []; +}; + +export const getAlerts = () => { + return alerts; +}; diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index eb9bc834c..53d687968 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -109,3 +109,80 @@ export const fakeStartTsOne = -14576291; export const fakeEndTsOne = -13885091; export const fakeStartTsTwo = 1092844665; export const fakeEndTsTwo = 1277049465; + +export const readAllCheck = [ + { + ...mockData.phone_data[0].data, + }, +]; + +export const readAllCompositeCheck = [ + { + additions: [], + cleaned_section_summary: null, + cleaned_trip: null, + confidence_threshold: -1, + confirmed_trip: null, + distance: 777, + duration: 777, + end_confirmed_place: { + key: 'test/value', + origin_key: '12345', + }, + end_fmt_time: '2023-11-01T17:55:20.999397-07:00', + end_loc: { + type: 'Point', + coordinates: [-1, -1], + }, + end_local_dt: null, + end_place: null, + end_ts: -1, + expectation: null, + expected_trip: null, + inferred_labels: [], + inferred_section_summary: { + count: { + CAR: 1, + WALKING: 1, + }, + distance: { + CAR: 222, + WALKING: 222, + }, + duration: { + CAR: 333, + WALKING: 333, + }, + }, + inferred_trip: null, + key: 'test/value', + locations: [ + { + key: 'test/value', + origin_key: '12345', + }, + ], + origin_key: '12345', + raw_trip: null, + sections: [ + { + key: 'test/value', + origin_key: '12345', + }, + ], + source: 'DwellSegmentationDistFilter', + start_confirmed_place: { + key: 'test/value', + origin_key: '12345', + }, + start_fmt_time: '2023-11-01T17:55:20.999397-07:00', + start_loc: { + type: 'Point', + coordinates: [-1, -1], + }, + start_local_dt: null, + start_place: null, + start_ts: null, + user_input: null, + }, +]; diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 0faac2c2e..e0f424fcf 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -1,12 +1,17 @@ -import { mockLogger } from '../__mocks__/globalMocks'; +import { clearAlerts, mockAlert, mockLogger } from '../__mocks__/globalMocks'; import { readAllCompositeTrips, readUnprocessedTrips } from '../js/diary/timelineHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import * as mockTLH from '../__mocks__/timelineHelperMocks'; mockLogger(); +mockAlert(); mockBEMUserCache(); +beforeEach(() => { + clearAlerts(); +}); + afterAll(() => { jest.restoreAllMocks(); }); @@ -21,13 +26,13 @@ jest.mock('../js/commHelper', () => ({ })); it('works when there are no composite trip objects fetched', async () => { - expect(readAllCompositeTrips(-1, -1)).resolves.not.toThrow(); + expect(readAllCompositeTrips(-1, -1)).resolves.toEqual([]); }); it('fetches a composite trip object and collapses it', async () => { - expect( - readAllCompositeTrips(mockTLH.fakeStartTsOne, mockTLH.fakeEndTsOne), - ).resolves.not.toThrow(); + expect(readAllCompositeTrips(mockTLH.fakeStartTsOne, mockTLH.fakeEndTsOne)).resolves.toEqual( + mockTLH.readAllCompositeCheck, + ); expect( readAllCompositeTrips(mockTLH.fakeStartTsTwo, mockTLH.fakeEndTsTwo), ).resolves.not.toThrow(); From bf913012c559a513719f2a88f8dade2f23a76cc2 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 9 Nov 2023 14:39:12 -0800 Subject: [PATCH 17/52] Minor adjustment to tests --- www/__mocks__/timelineHelperMocks.ts | 8 +------- www/__tests__/timelineHelper.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index 53d687968..eb0eb672e 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -110,13 +110,7 @@ export const fakeEndTsOne = -13885091; export const fakeStartTsTwo = 1092844665; export const fakeEndTsTwo = 1277049465; -export const readAllCheck = [ - { - ...mockData.phone_data[0].data, - }, -]; - -export const readAllCompositeCheck = [ +export const readAllCheckOne = [ { additions: [], cleaned_section_summary: null, diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index e0f424fcf..e5e37ca2d 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -31,7 +31,7 @@ it('works when there are no composite trip objects fetched', async () => { it('fetches a composite trip object and collapses it', async () => { expect(readAllCompositeTrips(mockTLH.fakeStartTsOne, mockTLH.fakeEndTsOne)).resolves.toEqual( - mockTLH.readAllCompositeCheck, + mockTLH.readAllCheckOne, ); expect( readAllCompositeTrips(mockTLH.fakeStartTsTwo, mockTLH.fakeEndTsTwo), @@ -47,7 +47,7 @@ jest.mock('../js/unifiedDataLoader', () => ({ })); it('works when there are no unprocessed trips...', async () => { - expect(readUnprocessedTrips(-1, -1, null)).resolves.not.toThrow(); + expect(readUnprocessedTrips(-1, -1, null)).resolves.toEqual([]); }); it('works when there are one or more unprocessed trips...', async () => { From f4b0e2d7134fc37df6cfc1ea5cee82653e27735d Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:14:46 -0800 Subject: [PATCH 18/52] Ran prettier on LabelTab merge --- www/js/diary/LabelTab.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 19f070bc5..7dde387a5 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -250,7 +250,6 @@ const LabelTab = () => { } async function fetchTripsInRange(startTs: number, endTs: number) { - if (!pipelineRange.start_ts) return logWarn('No pipelineRange yet - early return'); logDebug('LabelTab: fetchTripsInRange from ' + startTs + ' to ' + endTs); const readCompositePromise = Timeline.readAllCompositeTrips(startTs, endTs); From a2426c4a8b2c0fe2fb525cf6e74d07e2eea88387 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 15 Nov 2023 09:07:10 -0800 Subject: [PATCH 19/52] Fixed service imports, removed Angular import --- www/__tests__/timelineHelper.test.ts | 3 +-- www/js/diary.js | 1 - www/js/survey/enketo/enketoHelper.ts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index e5e37ca2d..40e6e65f4 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -1,7 +1,6 @@ import { clearAlerts, mockAlert, mockLogger } from '../__mocks__/globalMocks'; import { readAllCompositeTrips, readUnprocessedTrips } from '../js/diary/timelineHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; - import * as mockTLH from '../__mocks__/timelineHelperMocks'; mockLogger(); @@ -38,7 +37,7 @@ it('fetches a composite trip object and collapses it', async () => { ).resolves.not.toThrow(); }); -jest.mock('../js/unifiedDataLoader', () => ({ +jest.mock('../js/services/unifiedDataLoader', () => ({ getUnifiedDataForInterval: jest.fn((key, tq, combiner) => { if (tq.startTs === mockTLH.fakeStartTsOne) return Promise.resolve(mockTLH.mockTransition); if (tq.startTs === mockTLH.fakeStartTsTwo) return Promise.resolve(mockTLH.mockTransitionTwo); diff --git a/www/js/diary.js b/www/js/diary.js index c580ad8f2..bdf417e41 100644 --- a/www/js/diary.js +++ b/www/js/diary.js @@ -3,7 +3,6 @@ import LabelTab from './diary/LabelTab'; angular .module('emission.main.diary', [ - 'emission.main.diary.services', 'emission.plugin.logger', 'emission.survey.enketo.answer', ]) diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index fc5ea503e..424e364d2 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -3,7 +3,7 @@ import { Form } from 'enketo-core'; import { XMLParser } from 'fast-xml-parser'; import i18next from 'i18next'; import { logDebug } from '../../plugin/logger'; -import { getUnifiedDataForInterval } from '../../unifiedDataLoader'; +import { getUnifiedDataForInterval } from '../../services/unifiedDataLoader'; export type PrefillFields = { [key: string]: string }; From 08c10f2ba4e4ea502abf4fbcea2287f8f694a445 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 15 Nov 2023 10:23:42 -0800 Subject: [PATCH 20/52] Rewrote first tests, mocks, ran prettier --- www/__mocks__/timelineHelperMocks.ts | 136 ++++++++++++--------------- www/__tests__/timelineHelper.test.ts | 40 ++++++-- www/index.html | 2 +- www/js/diary.js | 5 +- www/js/diary/timelineHelper.ts | 1 - www/js/types/diaryTypes.ts | 9 +- 6 files changed, 98 insertions(+), 95 deletions(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index eb0eb672e..b535e4e48 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -1,16 +1,59 @@ import { MetaData, ServerData, ServerResponse } from '../js/types/serverData'; -import { CompositeTrip, TripTransition } from '../js/types/diaryTypes'; +import { CompositeTrip, ConfirmedPlace, TripTransition } from '../js/types/diaryTypes'; const mockMetaData: MetaData = { - write_ts: -13885091, - key: 'test/value', + write_ts: 1, + key: 'test/value/one', platform: 'test', time_zone: 'America/Los_Angeles', write_fmt_time: '1969-07-16T07:01:49.000Z', write_local_dt: null, - origin_key: '12345', + origin_key: '1', }; +const mockObjId = { + $oid: 'objID', +}; + +const mockConfirmedPlaceData: ConfirmedPlace = { + source: 'DwellSegmentationTimeFilter', + location: { + type: 'Point', + coordinates: [-122.0876886, 37.3887767], + }, + cleaned_place: { + $oid: '6553c3a0f27f16fbf9d1def1', + }, + additions: [], + user_input: {}, + enter_fmt_time: '2015-07-22T08:14:53.881000-07:00', + exit_fmt_time: '2015-07-22T08:14:53.881000-07:00', + starting_trip: { + $oid: '6553c3a1f27f16fbf9d1df15', + }, + ending_trip: { + $oid: '6553c3a1f27f16fbf9d1df15', + }, + enter_local_dt: null, + exit_local_dt: null, + raw_places: [ + { + $oid: '6553c39df27f16fbf9d1dcef', + }, + { + $oid: '6553c39df27f16fbf9d1dcef', + }, + ], + enter_ts: 1437578093.881, + exit_ts: 1437578093.881, +}; + +// using parse/stringify to deep copy & populate data +let tempMetaData = JSON.parse(JSON.stringify(mockMetaData)); +tempMetaData.write_ts = 2; +tempMetaData.origin_key = '2'; +export const mockMetaDataTwo = tempMetaData; + export const mockData: ServerResponse = { phone_data: [ { @@ -24,8 +67,9 @@ export const mockData: ServerResponse = { distance: 777, duration: 777, end_confirmed_place: { - data: null, + data: mockConfirmedPlaceData, metadata: mockMetaData, + _id: { $oid: 'endConfirmedPlace' }, }, end_fmt_time: '2023-11-01T17:55:20.999397-07:00', end_loc: { @@ -70,8 +114,9 @@ export const mockData: ServerResponse = { ], source: 'DwellSegmentationDistFilter', start_confirmed_place: { - data: null, + data: mockConfirmedPlaceData, metadata: mockMetaData, + _id: { $oid: 'startConfirmedPlace' }, }, start_fmt_time: '2023-11-01T17:55:20.999397-07:00', start_loc: { @@ -87,8 +132,14 @@ export const mockData: ServerResponse = { }, ], }; + +let newPhoneData = JSON.parse(JSON.stringify(mockData.phone_data[0])); +newPhoneData.metadata = mockMetaDataTwo; +newPhoneData.data.start_confirmed_place.metadata = mockMetaDataTwo; +newPhoneData.data.end_confirmed_place.metadata = mockMetaDataTwo; + export const mockDataTwo = { - phone_data: [mockData.phone_data[0], mockData.phone_data[0]], + phone_data: [mockData.phone_data[0], newPhoneData], }; export const mockTransition: Array> = [ @@ -109,74 +160,3 @@ export const fakeStartTsOne = -14576291; export const fakeEndTsOne = -13885091; export const fakeStartTsTwo = 1092844665; export const fakeEndTsTwo = 1277049465; - -export const readAllCheckOne = [ - { - additions: [], - cleaned_section_summary: null, - cleaned_trip: null, - confidence_threshold: -1, - confirmed_trip: null, - distance: 777, - duration: 777, - end_confirmed_place: { - key: 'test/value', - origin_key: '12345', - }, - end_fmt_time: '2023-11-01T17:55:20.999397-07:00', - end_loc: { - type: 'Point', - coordinates: [-1, -1], - }, - end_local_dt: null, - end_place: null, - end_ts: -1, - expectation: null, - expected_trip: null, - inferred_labels: [], - inferred_section_summary: { - count: { - CAR: 1, - WALKING: 1, - }, - distance: { - CAR: 222, - WALKING: 222, - }, - duration: { - CAR: 333, - WALKING: 333, - }, - }, - inferred_trip: null, - key: 'test/value', - locations: [ - { - key: 'test/value', - origin_key: '12345', - }, - ], - origin_key: '12345', - raw_trip: null, - sections: [ - { - key: 'test/value', - origin_key: '12345', - }, - ], - source: 'DwellSegmentationDistFilter', - start_confirmed_place: { - key: 'test/value', - origin_key: '12345', - }, - start_fmt_time: '2023-11-01T17:55:20.999397-07:00', - start_loc: { - type: 'Point', - coordinates: [-1, -1], - }, - start_local_dt: null, - start_place: null, - start_ts: null, - user_input: null, - }, -]; diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 40e6e65f4..74afe03ec 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -16,7 +16,7 @@ afterAll(() => { }); // Once we have end-to-end testing, we could utilize getRawEnteries. -jest.mock('../js/commHelper', () => ({ +jest.mock('../js/services/commHelper', () => ({ getRawEntries: jest.fn((key, startTs, endTs, valTwo) => { if (startTs === mockTLH.fakeStartTsOne) return mockTLH.mockData; if (startTs == mockTLH.fakeStartTsTwo) return mockTLH.mockDataTwo; @@ -28,15 +28,40 @@ it('works when there are no composite trip objects fetched', async () => { expect(readAllCompositeTrips(-1, -1)).resolves.toEqual([]); }); -it('fetches a composite trip object and collapses it', async () => { - expect(readAllCompositeTrips(mockTLH.fakeStartTsOne, mockTLH.fakeEndTsOne)).resolves.toEqual( - mockTLH.readAllCheckOne, +// Checks that `readAllCOmpositeTrips` properly unpacks & flattens the confirmedPlaces +const checkTripIsUnpacked = (obj) => { + expect(obj.metadata).toBeUndefined(); + expect(obj).toEqual( + expect.objectContaining({ + key: expect.any(String), + origin_key: expect.any(String), + start_confirmed_place: expect.objectContaining({ + origin_key: expect.any(String), + }), + end_confirmed_place: expect.objectContaining({ + origin_key: expect.any(String), + }), + locations: expect.any(Array), + sections: expect.any(Array), + }), ); - expect( - readAllCompositeTrips(mockTLH.fakeStartTsTwo, mockTLH.fakeEndTsTwo), - ).resolves.not.toThrow(); +}; + +it('fetches a composite trip object and collapses it', async () => { + const testValue = await readAllCompositeTrips(mockTLH.fakeStartTsOne, mockTLH.fakeEndTsOne); + expect(testValue.length).toEqual(1); + checkTripIsUnpacked(testValue[0]); }); +it('Works with multiple trips', async () => { + const testValue = await readAllCompositeTrips(mockTLH.fakeStartTsTwo, mockTLH.fakeEndTsTwo); + expect(testValue.length).toEqual(2); + checkTripIsUnpacked(testValue[0]); + checkTripIsUnpacked(testValue[1]); + expect(testValue[0].origin_key).toBe('1'); + expect(testValue[1].origin_key).toBe('2'); +}); +/* jest.mock('../js/services/unifiedDataLoader', () => ({ getUnifiedDataForInterval: jest.fn((key, tq, combiner) => { if (tq.startTs === mockTLH.fakeStartTsOne) return Promise.resolve(mockTLH.mockTransition); @@ -57,3 +82,4 @@ it('works when there are one or more unprocessed trips...', async () => { readUnprocessedTrips(mockTLH.fakeStartTsTwo, mockTLH.fakeEndTsTwo, null), ).resolves.not.toThrow(); }); +*/ diff --git a/www/index.html b/www/index.html index 72c75eb01..44fcb5bbf 100644 --- a/www/index.html +++ b/www/index.html @@ -15,4 +15,4 @@
- + \ No newline at end of file diff --git a/www/js/diary.js b/www/js/diary.js index bdf417e41..93fed24d4 100644 --- a/www/js/diary.js +++ b/www/js/diary.js @@ -2,10 +2,7 @@ import angular from 'angular'; import LabelTab from './diary/LabelTab'; angular - .module('emission.main.diary', [ - 'emission.plugin.logger', - 'emission.survey.enketo.answer', - ]) + .module('emission.main.diary', ['emission.plugin.logger', 'emission.survey.enketo.answer']) .config(function ($stateProvider) { $stateProvider.state('root.main.inf_scroll', { diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 45ea4312c..e32657903 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -4,7 +4,6 @@ import { getUnifiedDataForInterval } from '../services/unifiedDataLoader'; import { getRawEntries } from '../services/commHelper'; import { ServerResponse, ServerData } from '../types/serverData'; import L from 'leaflet'; -import i18next from 'i18next'; import { DateTime } from 'luxon'; import { UserInputEntry, CompositeTrip, TripTransition, SectionData } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 938b06851..08065c2a2 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -6,17 +6,14 @@ import { BaseModeKey, MotionTypeKey } from '../diary/diaryHelper'; import { ServerData, LocalDt } from './serverData'; type ObjectId = { $oid: string }; -type ConfirmedPlace = { - _id: ObjectId; +export type ConfirmedPlace = { additions: UserInputEntry[]; cleaned_place: ObjectId; ending_trip: ObjectId; enter_fmt_time: string; // ISO string 2023-10-31T12:00:00.000-04:00 enter_local_dt: LocalDt; enter_ts: number; // Unix timestamp - key: string; location: { type: string; coordinates: number[] }; - origin_key: string; raw_places: ObjectId[]; source: string; user_input: { @@ -27,6 +24,10 @@ type ConfirmedPlace = { as a string (e.g. 'walk', 'drove_alone') */ [k: `${string}confirm`]: string; }; + exit_fmt_time: string; + exit_ts: number; + exit_local_dt: LocalDt; + starting_trip: ObjectId; }; export type TripTransition = { From e6a21ca6e466972c824d9a0c7f5759a4a14ab634 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 15 Nov 2023 13:13:33 -0800 Subject: [PATCH 21/52] Updated second diaryServices test --- www/__tests__/timelineHelper.test.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 74afe03ec..7172345e3 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -1,5 +1,9 @@ import { clearAlerts, mockAlert, mockLogger } from '../__mocks__/globalMocks'; -import { readAllCompositeTrips, readUnprocessedTrips } from '../js/diary/timelineHelper'; +import { + useGeojsonForTrip, + readAllCompositeTrips, + readUnprocessedTrips, +} from '../js/diary/timelineHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import * as mockTLH from '../__mocks__/timelineHelperMocks'; @@ -15,6 +19,7 @@ afterAll(() => { jest.restoreAllMocks(); }); +// Tests for readAllCompositeTrips // Once we have end-to-end testing, we could utilize getRawEnteries. jest.mock('../js/services/commHelper', () => ({ getRawEntries: jest.fn((key, startTs, endTs, valTwo) => { @@ -28,7 +33,7 @@ it('works when there are no composite trip objects fetched', async () => { expect(readAllCompositeTrips(-1, -1)).resolves.toEqual([]); }); -// Checks that `readAllCOmpositeTrips` properly unpacks & flattens the confirmedPlaces +// Checks that `readAllCompositeTrips` properly unpacks & flattens the confirmedPlaces const checkTripIsUnpacked = (obj) => { expect(obj.metadata).toBeUndefined(); expect(obj).toEqual( @@ -61,7 +66,8 @@ it('Works with multiple trips', async () => { expect(testValue[0].origin_key).toBe('1'); expect(testValue[1].origin_key).toBe('2'); }); -/* + +// Tests for `readUnprocessedTrips` jest.mock('../js/services/unifiedDataLoader', () => ({ getUnifiedDataForInterval: jest.fn((key, tq, combiner) => { if (tq.startTs === mockTLH.fakeStartTsOne) return Promise.resolve(mockTLH.mockTransition); @@ -74,12 +80,13 @@ it('works when there are no unprocessed trips...', async () => { expect(readUnprocessedTrips(-1, -1, null)).resolves.toEqual([]); }); +// In manual testing, it seems that `trip_gj_list` always returns +// as an empty array - should find data where this is different... it('works when there are one or more unprocessed trips...', async () => { - expect( - readUnprocessedTrips(mockTLH.fakeStartTsOne, mockTLH.fakeEndTsOne, null), - ).resolves.not.toThrow(); - expect( - readUnprocessedTrips(mockTLH.fakeStartTsTwo, mockTLH.fakeEndTsTwo, null), - ).resolves.not.toThrow(); + const testValueOne = await readUnprocessedTrips( + mockTLH.fakeStartTsOne, + mockTLH.fakeEndTsOne, + null, + ); + expect(testValueOne).toEqual([]); }); -*/ From 361cc280f0e51c8737c8f38741154480cfb0992e Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 15 Nov 2023 13:44:39 -0800 Subject: [PATCH 22/52] Separated `labelTypes` into separate file --- www/js/diary/LabelTabContext.ts | 2 +- www/js/diary/diaryHelper.ts | 3 ++- www/js/survey/multilabel/confirmHelper.ts | 26 +---------------------- www/js/types/labelTypes.ts | 24 +++++++++++++++++++++ 4 files changed, 28 insertions(+), 27 deletions(-) create mode 100644 www/js/types/labelTypes.ts diff --git a/www/js/diary/LabelTabContext.ts b/www/js/diary/LabelTabContext.ts index 24d7ade41..717a4980d 100644 --- a/www/js/diary/LabelTabContext.ts +++ b/www/js/diary/LabelTabContext.ts @@ -1,6 +1,6 @@ import { createContext } from 'react'; import { TimelineEntry, UserInputEntry } from '../types/diaryTypes'; -import { LabelOption } from '../survey/multilabel/confirmHelper'; +import { LabelOption } from '../types/labelTypes'; export type TimelineMap = Map; export type TimelineLabelMap = { diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index 616974b7b..2fee7eccd 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -3,8 +3,9 @@ import moment from 'moment'; import { DateTime } from 'luxon'; -import { LabelOptions, readableLabelToKey } from '../survey/multilabel/confirmHelper'; +import { readableLabelToKey } from '../survey/multilabel/confirmHelper'; import { CompositeTrip } from '../types/diaryTypes'; +import { LabelOptions } from '../types/labelTypes'; export const modeColors = { pink: '#c32e85', // oklch(56% 0.2 350) // e-car diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index 51674b0c3..4c2be1012 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -4,31 +4,7 @@ import { getAngularService } from '../../angular-react-helper'; import { fetchUrlCached } from '../../services/commHelper'; import i18next from 'i18next'; import { logDebug } from '../../plugin/logger'; - -type InputDetails = { - [k in T]?: { - name: string; - labeltext: string; - choosetext: string; - key: string; - }; -}; -export type LabelOption = { - value: string; - baseMode: string; - met?: { range: any[]; mets: number }; - met_equivalent?: string; - kgCo2PerKm: number; - text?: string; -}; -export type MultilabelKey = 'MODE' | 'PURPOSE' | 'REPLACED_MODE'; -export type LabelOptions = { - [k in T]: LabelOption[]; -} & { - translations: { - [lang: string]: { [translationKey: string]: string }; - }; -}; +import { LabelOption, LabelOptions, MultilabelKey, InputDetails } from '../../types/labelTypes'; let appConfig; export let labelOptions: LabelOptions; diff --git a/www/js/types/labelTypes.ts b/www/js/types/labelTypes.ts new file mode 100644 index 000000000..8ac720adc --- /dev/null +++ b/www/js/types/labelTypes.ts @@ -0,0 +1,24 @@ +export type InputDetails = { + [k in T]?: { + name: string; + labeltext: string; + choosetext: string; + key: string; + }; +}; +export type LabelOption = { + value: string; + baseMode: string; + met?: { range: any[]; mets: number }; + met_equivalent?: string; + kgCo2PerKm: number; + text?: string; +}; +export type MultilabelKey = 'MODE' | 'PURPOSE' | 'REPLACED_MODE'; +export type LabelOptions = { + [k in T]: LabelOption[]; +} & { + translations: { + [lang: string]: { [translationKey: string]: string }; + }; +}; From 691aeb54200940d3f796117a001c8622492e81ec Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:59:15 -0800 Subject: [PATCH 23/52] Added test for useGeojsonForTrip --- www/__mocks__/timelineHelperMocks.ts | 12 ++++++++---- www/__tests__/timelineHelper.test.ts | 27 +++++++++++++++++++++++++++ www/js/diary/LabelTab.tsx | 1 - www/js/diary/LabelTabContext.ts | 2 +- www/js/diary/timelineHelper.ts | 10 +++++----- www/js/types/diaryTypes.ts | 21 ++++++++++++++++++++- 6 files changed, 61 insertions(+), 12 deletions(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index b535e4e48..9a16938f7 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -1,5 +1,6 @@ import { MetaData, ServerData, ServerResponse } from '../js/types/serverData'; import { CompositeTrip, ConfirmedPlace, TripTransition } from '../js/types/diaryTypes'; +import { LabelOptions } from '../js/types/labelTypes'; const mockMetaData: MetaData = { write_ts: 1, @@ -11,8 +12,11 @@ const mockMetaData: MetaData = { origin_key: '1', }; -const mockObjId = { - $oid: 'objID', +export const mockLabelOptions: LabelOptions = { + MODE: null, + PURPOSE: null, + REPLACED_MODE: null, + translations: null, }; const mockConfirmedPlaceData: ConfirmedPlace = { @@ -58,7 +62,7 @@ export const mockData: ServerResponse = { phone_data: [ { data: { - _id: null, + _id: { $oid: 'mockDataOne' }, additions: [], cleaned_section_summary: null, cleaned_trip: null, @@ -125,7 +129,7 @@ export const mockData: ServerResponse = { }, start_local_dt: null, start_place: null, - start_ts: null, + start_ts: 1, user_input: null, }, metadata: mockMetaData, diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 7172345e3..19a87255c 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -6,6 +6,7 @@ import { } from '../js/diary/timelineHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import * as mockTLH from '../__mocks__/timelineHelperMocks'; +import { GeoJSON, GjFeature } from '../js/types/diaryTypes'; mockLogger(); mockAlert(); @@ -19,6 +20,32 @@ afterAll(() => { jest.restoreAllMocks(); }); +describe('useGeojsonForTrip', () => { + it('work with an empty input', () => { + const testVal = useGeojsonForTrip(null, null, null); + expect(testVal).toBeFalsy; + }); + + const checkGeojson = (geoObj: GeoJSON) => { + expect(geoObj.data).toEqual( + expect.objectContaining({ + id: expect.any(String), + type: 'FeatureCollection', + features: expect.any(Array), + }), + ); + }; + + it('works without labelMode flag', () => { + const testValue = useGeojsonForTrip( + mockTLH.mockDataTwo.phone_data[1].data, + mockTLH.mockLabelOptions, + ); + checkGeojson(testValue); + expect(testValue.data.features.length).toBe(3); + }); +}); + // Tests for readAllCompositeTrips // Once we have end-to-end testing, we could utilize getRawEnteries. jest.mock('../js/services/commHelper', () => ({ diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 5c753b2b0..bcad5e5a3 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -255,7 +255,6 @@ const LabelTab = () => { .reverse() .find((trip) => trip.origin_key.includes('confirmed_trip')); readUnprocessedPromise = readUnprocessedTrips(pipelineRange.end_ts, nowTs, lastProcessedTrip); - readUnprocessedPromise = readUnprocessedTrips(pipelineRange.end_ts, nowTs, lastProcessedTrip); } else { readUnprocessedPromise = Promise.resolve([]); } diff --git a/www/js/diary/LabelTabContext.ts b/www/js/diary/LabelTabContext.ts index 717a4980d..18e157234 100644 --- a/www/js/diary/LabelTabContext.ts +++ b/www/js/diary/LabelTabContext.ts @@ -2,7 +2,7 @@ import { createContext } from 'react'; import { TimelineEntry, UserInputEntry } from '../types/diaryTypes'; import { LabelOption } from '../types/labelTypes'; -export type TimelineMap = Map; +export type TimelineMap = Map; // Todo: update to reflect unpacked trips (origin_Key, etc) export type TimelineLabelMap = { [k: string]: { /* if the key here is 'SURVEY', we are in the ENKETO configuration, meaning the user input diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index e32657903..a1beb6ff1 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -5,17 +5,17 @@ import { getRawEntries } from '../services/commHelper'; import { ServerResponse, ServerData } from '../types/serverData'; import L from 'leaflet'; import { DateTime } from 'luxon'; -import { UserInputEntry, CompositeTrip, TripTransition, SectionData } from '../types/diaryTypes'; +import { UserInputEntry, TripTransition, TimelineEntry, GeoJSON } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; -import { LabelOptions } from '../survey/multilabel/confirmHelper'; +import { LabelOptions } from '../types/labelTypes'; import { getNotDeletedCandidates, getUniqueEntries } from '../survey/inputMatcher'; -const cachedGeojsons = new Map(); +const cachedGeojsons: Map = new Map(); /** * @description Gets a formatted GeoJSON object for a trip, including the start and end places and the trajectory. */ -export function useGeojsonForTrip(trip: CompositeTrip, labelOptions: LabelOptions, labeledMode?) { +export function useGeojsonForTrip(trip, labelOptions: LabelOptions, labeledMode?: Boolean) { if (!trip) return; const gjKey = `trip-${trip._id.$oid}-${labeledMode || 'detected'}`; if (cachedGeojsons.has(gjKey)) { @@ -229,7 +229,7 @@ const unpackServerData = (obj: ServerData) => ({ export const readAllCompositeTrips = function (startTs: number, endTs: number) { const readPromises = [getRawEntries(['analysis/composite_trip'], startTs, endTs, 'data.end_ts')]; return Promise.all(readPromises) - .then(([ctList]: [ServerResponse]) => { + .then(([ctList]: [ServerResponse]) => { return ctList.phone_data.map((ct) => { const unpackedCt = unpackServerData(ct); return { diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 08065c2a2..99fa2b817 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -38,7 +38,7 @@ export type TripTransition = { export type LocationCoord = { type: string; // e.x., "Point" - coordinates: [number, number]; + coordinates: [number, number] | Array<[number, number]>; }; /* These are the properties received from the server (basically matches Python code) @@ -165,3 +165,22 @@ export type SectionData = { duration: number; distance: number; }; + +export type GjFeature = { + type: string; + geometry: LocationCoord; + properties?: { featureType: string }; // if geometry.coordinates.length == 1, property + style?: { color: string }; // otherwise, style (which is a hexcode) +}; + +export type GeoJSON = { + data: { + id: string; + type: string; + features: GjFeature[]; + properties: { + start_ts: number; + end_ts: number; + }; + }; +}; From 84d675a46f6a059cb3ac9d4ed71b53cf67422ece Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:40:11 -0800 Subject: [PATCH 24/52] Added test for compositeTrips2TimelineMap --- www/__mocks__/timelineHelperMocks.ts | 4 +++ www/__tests__/timelineHelper.test.ts | 44 ++++++++++++++++++++++++++++ www/js/diary/LabelTab.tsx | 2 +- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index 9a16938f7..4b7c9a96b 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -137,10 +137,14 @@ export const mockData: ServerResponse = { ], }; +// Setup for second mockData let newPhoneData = JSON.parse(JSON.stringify(mockData.phone_data[0])); +newPhoneData.data._id.$oid = 'mockDataTwo'; newPhoneData.metadata = mockMetaDataTwo; newPhoneData.data.start_confirmed_place.metadata = mockMetaDataTwo; +newPhoneData.data.start_confirmed_place._id.$oid = 'startConfirmedPlaceTwo'; newPhoneData.data.end_confirmed_place.metadata = mockMetaDataTwo; +newPhoneData.data.end_confirmed_place._id.$oid = 'endConfirmedPlaceTwo'; export const mockDataTwo = { phone_data: [mockData.phone_data[0], newPhoneData], diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 19a87255c..a9a059813 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -3,6 +3,7 @@ import { useGeojsonForTrip, readAllCompositeTrips, readUnprocessedTrips, + compositeTrips2TimelineMap, } from '../js/diary/timelineHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import * as mockTLH from '../__mocks__/timelineHelperMocks'; @@ -46,6 +47,49 @@ describe('useGeojsonForTrip', () => { }); }); +describe('compositeTrips2TimelineMap', () => { + const firstTripList = [mockTLH.mockData.phone_data[0].data]; + const secondTripList = [ + mockTLH.mockDataTwo.phone_data[0].data, + mockTLH.mockDataTwo.phone_data[1].data, + ]; + const firstKey = mockTLH.mockData.phone_data[0].data._id.$oid; + const secondKey = mockTLH.mockDataTwo.phone_data[1].data._id.$oid; + const thirdKey = mockTLH.mockData.phone_data[0].data._id.$oid; + let testValue; + + it('Works with an empty list', () => { + expect(Object.keys(compositeTrips2TimelineMap([])).length).toBe(0); + }); + + it('Works with a list of len = 1, no flag', () => { + testValue = compositeTrips2TimelineMap(firstTripList); + expect(testValue.size).toBe(1); + expect(testValue.get(firstKey)).toEqual(firstTripList[0]); + }); + + it('Works with a list of len = 1, with flag', () => { + testValue = compositeTrips2TimelineMap(firstTripList, true); + expect(testValue.size).toBe(3); + expect(testValue.get(firstKey)).toEqual(firstTripList[0]); + expect(testValue.get('startConfirmedPlace')).toEqual(firstTripList[0].start_confirmed_place); + expect(testValue.get('endConfirmedPlace')).toEqual(firstTripList[0].end_confirmed_place); + }); + + it('Works with a list of len >= 1, no flag', () => { + testValue = compositeTrips2TimelineMap(secondTripList); + expect(testValue.size).toBe(2); + expect(testValue.get(secondKey)).toEqual(secondTripList[1]); + expect(testValue.get(thirdKey)).toEqual(secondTripList[0]); + }); + + it('Works with a list of len >= 1, with flag', () => { + testValue = compositeTrips2TimelineMap(secondTripList, true); + console.log(`Len: ${testValue.size}`); + expect(testValue.size).toBe(6); + }); +}); + // Tests for readAllCompositeTrips // Once we have end-to-end testing, we could utilize getRawEnteries. jest.mock('../js/services/commHelper', () => ({ diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index bcad5e5a3..2e181859c 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -230,7 +230,7 @@ const LabelTab = () => { }); const readTimelineMap = compositeTrips2TimelineMap(tripsRead, showPlaces); logDebug(`LabelTab: after composite trips converted, - readTimelineMap = ${JSON.stringify(readTimelineMap)}`); + readTimelineMap = ${[...readTimelineMap.entries()]}`); if (mode == 'append') { setTimelineMap(new Map([...timelineMap, ...readTimelineMap])); } else if (mode == 'prepend') { From a2b5da53b8fbf2528387660e67a8dcc7c62bf1c5 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:08:11 -0800 Subject: [PATCH 25/52] Added tests for keysForNotesInputs --- www/__tests__/timelineHelper.test.ts | 51 ++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index a9a059813..2cf590e3c 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -4,6 +4,7 @@ import { readAllCompositeTrips, readUnprocessedTrips, compositeTrips2TimelineMap, + keysForLabelInputs, } from '../js/diary/timelineHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import * as mockTLH from '../__mocks__/timelineHelperMocks'; @@ -48,14 +49,14 @@ describe('useGeojsonForTrip', () => { }); describe('compositeTrips2TimelineMap', () => { - const firstTripList = [mockTLH.mockData.phone_data[0].data]; - const secondTripList = [ + const tripListOne = [mockTLH.mockData.phone_data[0].data]; + const tripListTwo = [ mockTLH.mockDataTwo.phone_data[0].data, mockTLH.mockDataTwo.phone_data[1].data, ]; - const firstKey = mockTLH.mockData.phone_data[0].data._id.$oid; - const secondKey = mockTLH.mockDataTwo.phone_data[1].data._id.$oid; - const thirdKey = mockTLH.mockData.phone_data[0].data._id.$oid; + const keyOne = mockTLH.mockData.phone_data[0].data._id.$oid; + const keyTwo = mockTLH.mockDataTwo.phone_data[1].data._id.$oid; + const keyThree = mockTLH.mockData.phone_data[0].data._id.$oid; let testValue; it('Works with an empty list', () => { @@ -63,33 +64,53 @@ describe('compositeTrips2TimelineMap', () => { }); it('Works with a list of len = 1, no flag', () => { - testValue = compositeTrips2TimelineMap(firstTripList); + testValue = compositeTrips2TimelineMap(tripListOne); expect(testValue.size).toBe(1); - expect(testValue.get(firstKey)).toEqual(firstTripList[0]); + expect(testValue.get(keyOne)).toEqual(tripListOne[0]); }); it('Works with a list of len = 1, with flag', () => { - testValue = compositeTrips2TimelineMap(firstTripList, true); + testValue = compositeTrips2TimelineMap(tripListOne, true); expect(testValue.size).toBe(3); - expect(testValue.get(firstKey)).toEqual(firstTripList[0]); - expect(testValue.get('startConfirmedPlace')).toEqual(firstTripList[0].start_confirmed_place); - expect(testValue.get('endConfirmedPlace')).toEqual(firstTripList[0].end_confirmed_place); + expect(testValue.get(keyOne)).toEqual(tripListOne[0]); + expect(testValue.get('startConfirmedPlace')).toEqual(tripListOne[0].start_confirmed_place); + expect(testValue.get('endConfirmedPlace')).toEqual(tripListOne[0].end_confirmed_place); }); it('Works with a list of len >= 1, no flag', () => { - testValue = compositeTrips2TimelineMap(secondTripList); + testValue = compositeTrips2TimelineMap(tripListTwo); expect(testValue.size).toBe(2); - expect(testValue.get(secondKey)).toEqual(secondTripList[1]); - expect(testValue.get(thirdKey)).toEqual(secondTripList[0]); + expect(testValue.get(keyTwo)).toEqual(tripListTwo[1]); + expect(testValue.get(keyThree)).toEqual(tripListTwo[0]); }); it('Works with a list of len >= 1, with flag', () => { - testValue = compositeTrips2TimelineMap(secondTripList, true); + testValue = compositeTrips2TimelineMap(tripListTwo, true); console.log(`Len: ${testValue.size}`); expect(testValue.size).toBe(6); }); }); +// updateAllUnprocessedinputs tests +it('can use an appConfig to get labelInputKeys', () => { + const mockAppConfigOne = { + survey_info: { + 'trip-labels': 'ENKETO', + }, + }; + const mockAppConfigTwo = { + survey_info: { + 'trip-labels': 'Other', + }, + intro: { + mode_studied: 'sample', + }, + }; + expect(keysForLabelInputs(mockAppConfigOne)).rejects; + expect(keysForLabelInputs(mockAppConfigOne)).toEqual(['manual/trip_user_input']); + expect(keysForLabelInputs(mockAppConfigTwo).length).toEqual(3); +}); + // Tests for readAllCompositeTrips // Once we have end-to-end testing, we could utilize getRawEnteries. jest.mock('../js/services/commHelper', () => ({ From 20f544115a2682aaa7413513858e5dd960540b15 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Fri, 17 Nov 2023 16:21:56 -0800 Subject: [PATCH 26/52] Minor review changes, updated GeoJSON Typing --- www/__tests__/timelineHelper.test.ts | 6 ++--- www/js/diary/timelineHelper.ts | 24 ++++++++---------- www/js/types/diaryTypes.ts | 37 ++++++++-------------------- 3 files changed, 23 insertions(+), 44 deletions(-) diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 2cf590e3c..115bf2d43 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -8,7 +8,7 @@ import { } from '../js/diary/timelineHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import * as mockTLH from '../__mocks__/timelineHelperMocks'; -import { GeoJSON, GjFeature } from '../js/types/diaryTypes'; +import { GeoJSONData, GeoJSONStyledFeature } from '../js/types/diaryTypes'; mockLogger(); mockAlert(); @@ -28,12 +28,12 @@ describe('useGeojsonForTrip', () => { expect(testVal).toBeFalsy; }); - const checkGeojson = (geoObj: GeoJSON) => { + const checkGeojson = (geoObj: GeoJSONData) => { expect(geoObj.data).toEqual( expect.objectContaining({ id: expect.any(String), type: 'FeatureCollection', - features: expect.any(Array), + features: expect.any(Array), }), ); }; diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index a1beb6ff1..707a126ad 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -5,12 +5,11 @@ import { getRawEntries } from '../services/commHelper'; import { ServerResponse, ServerData } from '../types/serverData'; import L from 'leaflet'; import { DateTime } from 'luxon'; -import { UserInputEntry, TripTransition, TimelineEntry, GeoJSON } from '../types/diaryTypes'; +import { UserInputEntry, TripTransition, TimelineEntry, GeoJSONData } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; import { LabelOptions } from '../types/labelTypes'; -import { getNotDeletedCandidates, getUniqueEntries } from '../survey/inputMatcher'; -const cachedGeojsons: Map = new Map(); +const cachedGeojsons: Map = new Map(); /** * @description Gets a formatted GeoJSON object for a trip, including the start and end places and the trajectory. @@ -34,7 +33,7 @@ export function useGeojsonForTrip(trip, labelOptions: LabelOptions, labeledMode? ...locations2GeojsonTrajectory(trip, trip.locations, trajectoryColor), ]; - const gj = { + const gj: GeoJSONData = { data: { id: gjKey, type: 'FeatureCollection', @@ -249,16 +248,16 @@ export const readAllCompositeTrips = function (startTs: number, endTs: number) { const dateTime2localdate = function (currtime: DateTime, tz: string) { return { timezone: tz, - year: currtime.get('year'), + year: currtime.year, //the months of the draft trips match the one format needed for //moment function however now that is modified we need to also //modify the months value here - month: currtime.get('month') + 1, - day: currtime.get('day'), - weekday: currtime.get('weekday'), - hour: currtime.get('hour'), - minute: currtime.get('minute'), - second: currtime.get('second'), + month: currtime.month, + day: currtime.day, + weekday: currtime.weekday, + hour: currtime.hour, + minute: currtime.minute, + second: currtime.second, }; }; @@ -507,10 +506,7 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) DateTime.fromSeconds(tq.startTs).toLocaleString(DateTime.DATETIME_MED) + DateTime.fromSeconds(tq.endTs).toLocaleString(DateTime.DATETIME_MED), ); - - console.log('Testing...'); const getMessageMethod = window['cordova'].plugins.BEMUserCache.getMessagesForInterval; - console.log('Entering...'); return getUnifiedDataForInterval('statemachine/transition', tq, getMessageMethod).then(function ( transitionList: Array>, ) { diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 99fa2b817..fd7f03b38 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -4,6 +4,7 @@ import { BaseModeKey, MotionTypeKey } from '../diary/diaryHelper'; import { ServerData, LocalDt } from './serverData'; +import { FeatureCollection, Feature, Geometry } from 'geojson'; type ObjectId = { $oid: string }; export type ConfirmedPlace = { @@ -13,7 +14,7 @@ export type ConfirmedPlace = { enter_fmt_time: string; // ISO string 2023-10-31T12:00:00.000-04:00 enter_local_dt: LocalDt; enter_ts: number; // Unix timestamp - location: { type: string; coordinates: number[] }; + location: Geometry; raw_places: ObjectId[]; source: string; user_input: { @@ -36,11 +37,6 @@ export type TripTransition = { ts: number; }; -export type LocationCoord = { - type: string; // e.x., "Point" - coordinates: [number, number] | Array<[number, number]>; -}; - /* These are the properties received from the server (basically matches Python code) This should match what Timeline.readAllCompositeTrips returns (an array of these objects) */ export type CompositeTrip = { @@ -54,7 +50,7 @@ export type CompositeTrip = { duration: number; end_confirmed_place: ServerData; end_fmt_time: string; - end_loc: { type: string; coordinates: number[] }; + end_loc: Geometry; end_local_dt: LocalDt; end_place: ObjectId; end_ts: number; @@ -71,7 +67,7 @@ export type CompositeTrip = { source: string; start_confirmed_place: ServerData; start_fmt_time: string; - start_loc: { type: string; coordinates: number[] }; + start_loc: Geometry; start_local_dt: LocalDt; start_place: ObjectId; start_ts: number; @@ -141,7 +137,7 @@ export type Location = { latitude: number; fmt_time: string; // ISO mode: number; - loc: LocationCoord; + loc: Geometry; ts: number; // Unix altitude: number; distance: number; @@ -150,14 +146,14 @@ export type Location = { // used in readAllCompositeTrips export type SectionData = { end_ts: number; // Unix time, e.x. 1696352498.804 - end_loc: LocationCoord; + end_loc: Geometry; start_fmt_time: string; // ISO time end_fmt_time: string; trip_id: ObjectId; sensed_mode: number; source: string; // e.x., "SmoothedHighConfidenceMotion" start_ts: number; // Unix - start_loc: LocationCoord; + start_loc: Geometry; cleaned_section: ObjectId; start_local_dt: LocalDt; end_local_dt: LocalDt; @@ -166,21 +162,8 @@ export type SectionData = { distance: number; }; -export type GjFeature = { - type: string; - geometry: LocationCoord; - properties?: { featureType: string }; // if geometry.coordinates.length == 1, property - style?: { color: string }; // otherwise, style (which is a hexcode) -}; +export type GeoJSONStyledFeature = Feature & { style?: { color: string } }; -export type GeoJSON = { - data: { - id: string; - type: string; - features: GjFeature[]; - properties: { - start_ts: number; - end_ts: number; - }; - }; +export type GeoJSONData = { + data: FeatureCollection & { id: string; properties: { start_ts: number; end_ts: number } }; }; From 25a064d20c26e3b0bf4e47011ba49a216e07a95b Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Mon, 20 Nov 2023 09:30:47 -0800 Subject: [PATCH 27/52] Fixes missing timezone conversions --- www/js/diary/diaryHelper.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index 79918207a..ed592427f 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -97,8 +97,8 @@ export function getBaseModeByText(text, labelOptions: LabelOptions) { export function isMultiDay(beginFmtTime: string, endFmtTime: string) { if (!beginFmtTime || !endFmtTime) return false; return ( - DateTime.fromISO(beginFmtTime).toFormat('YYYYMMDD') != - DateTime.fromISO(endFmtTime).toFormat('YYYYMMDD') + DateTime.fromISO(beginFmtTime, { setZone: true }).toFormat('YYYYMMDD') != + DateTime.fromISO(endFmtTime, { setZone: true }).toFormat('YYYYMMDD') ); } @@ -114,7 +114,7 @@ export function getFormattedDate(beginFmtTime: string, endFmtTime?: string) { return `${getFormattedDate(beginFmtTime)} - ${getFormattedDate(endFmtTime)}`; } // only one day given, or both are the same day - const t = DateTime.fromISO(beginFmtTime || endFmtTime); + const t = DateTime.fromISO(beginFmtTime || endFmtTime, { setZone: true }); // We use toLocale to get Wed May 3, 2023 or equivalent, const tConversion = t.toLocaleString({ weekday: 'short', @@ -148,8 +148,8 @@ export function getFormattedDateAbbr(beginFmtTime: string, endFmtTime?: string) */ export function getFormattedTimeRange(beginFmtTime: string, endFmtTime: string) { if (!beginFmtTime || !endFmtTime) return; - const beginTime = DateTime.fromISO(beginFmtTime); - const endTime = DateTime.fromISO(endFmtTime); + const beginTime = DateTime.fromISO(beginFmtTime, { setZone: true }); + const endTime = DateTime.fromISO(endFmtTime, { setZone: true }); const range = endTime.diff(beginTime, ['hours']); const roundedHours = Math.round(range.as('hours')); // Round up or down to nearest hour const formattedRange = `${roundedHours} hour${roundedHours !== 1 ? 's' : ''}`; From fd0c1ab5de27b381be218aac0ad0ae38a89efd86 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Mon, 20 Nov 2023 15:04:29 -0800 Subject: [PATCH 28/52] Added npm package, fixed HTML formatting - NPM package for GeoJSON types --- package.cordovabuild.json | 1 + package.serve.json | 1 + www/index.html | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 048f8f81d..9413912c7 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -105,6 +105,7 @@ "@react-navigation/native": "^6.1.7", "@react-navigation/stack": "^6.3.17", "@shopify/flash-list": "^1.3.1", + "@types/leaflet": "^1.9.4", "angular": "1.6.7", "angular-animate": "1.6.7", "angular-local-storage": "^0.7.1", diff --git a/package.serve.json b/package.serve.json index 6315b6a46..2ee9480dc 100644 --- a/package.serve.json +++ b/package.serve.json @@ -57,6 +57,7 @@ "@react-navigation/stack": "^6.3.17", "@shopify/flash-list": "^1.3.1", "@types/jest": "^29.5.5", + "@types/leaflet": "^1.9.4", "angular": "1.6.7", "angular-animate": "1.6.7", "angular-local-storage": "^0.7.1", diff --git a/www/index.html b/www/index.html index 44fcb5bbf..72c75eb01 100644 --- a/www/index.html +++ b/www/index.html @@ -15,4 +15,4 @@
- \ No newline at end of file + From 9a20378fe14677a045bb6d9593d700214c2fd905 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:48:25 -0800 Subject: [PATCH 29/52] Added localization for trip duration - Changes utilize "humanize-duration" package --- package.cordovabuild.json | 1 + package.serve.json | 1 + www/__tests__/diaryHelper.test.ts | 3 +++ www/js/diary/diaryHelper.ts | 15 ++++++++++----- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 9413912c7..d297819a3 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -144,6 +144,7 @@ "enketo-core": "^6.1.7", "fast-xml-parser": "^4.2.2", "fs-extra": "^9.0.1", + "humanize-duration": "3.31.0", "i18next": "^22.5.0", "install": "^0.13.0", "ionic-datepicker": "1.2.1", diff --git a/package.serve.json b/package.serve.json index 2ee9480dc..ea52b613f 100644 --- a/package.serve.json +++ b/package.serve.json @@ -75,6 +75,7 @@ "enketo-core": "^6.1.7", "fast-xml-parser": "^4.2.2", "fs-extra": "^9.0.1", + "humanize-duration": "3.31.0", "i18next": "^22.5.0", "install": "^0.13.0", "ionic-datepicker": "1.2.1", diff --git a/www/__tests__/diaryHelper.test.ts b/www/__tests__/diaryHelper.test.ts index 26ed03a8f..ee1f1649d 100644 --- a/www/__tests__/diaryHelper.test.ts +++ b/www/__tests__/diaryHelper.test.ts @@ -8,6 +8,9 @@ import { modeColors, } from '../js/diary/diaryHelper'; +import initializedI18next from '../js/i18nextInit'; +window['i18next'] = initializedI18next; + it('returns a formatted date', () => { expect(getFormattedDate('2023-09-18T00:00:00-07:00')).toBe('Mon September 18, 2023'); expect(getFormattedDate('')).toBeUndefined(); diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index ed592427f..5841bac1f 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -1,11 +1,13 @@ // here we have some helper functions used throughout the label tab // these functions are being gradually migrated out of services.js +import i18next from 'i18next'; import { DateTime } from 'luxon'; -import { readableLabelToKey } from '../survey/multilabel/confirmHelper'; import { CompositeTrip } from '../types/diaryTypes'; import { LabelOptions } from '../types/labelTypes'; +const humanizeDuration = require('humanize-duration'); + export const modeColors = { pink: '#c32e85', // oklch(56% 0.2 350) // e-car red: '#c21725', // oklch(52% 0.2 25) // car @@ -150,10 +152,13 @@ export function getFormattedTimeRange(beginFmtTime: string, endFmtTime: string) if (!beginFmtTime || !endFmtTime) return; const beginTime = DateTime.fromISO(beginFmtTime, { setZone: true }); const endTime = DateTime.fromISO(endFmtTime, { setZone: true }); - const range = endTime.diff(beginTime, ['hours']); - const roundedHours = Math.round(range.as('hours')); // Round up or down to nearest hour - const formattedRange = `${roundedHours} hour${roundedHours !== 1 ? 's' : ''}`; - return formattedRange; + const range = endTime.diff(beginTime, ['hours', 'minutes']); + const unitsToDisplay = range.hours < 1 ? ['m'] : ['h']; + return humanizeDuration(range.as('milliseconds'), { + language: i18next.language, + units: unitsToDisplay, + round: true, + }); } /** From d27cefe08a3df93f7e6585959e66fcab5912a284 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:56:19 -0800 Subject: [PATCH 30/52] Fixed trip start/end formatting error --- www/js/diary/diaryHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index 5841bac1f..afecf34d8 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -197,5 +197,5 @@ export function getLocalTimeString(dt) { hour: dt.hour, minute: dt.minute, }); - return dateTime.toFormat('hh:mm a'); + return dateTime.toFormat('h:mm a'); } From fe3d417881fe5198be459b536ffcb9f4c57b588d Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 22 Nov 2023 13:24:05 -0800 Subject: [PATCH 31/52] Filled out typing for timelineHelper functions --- package.cordovabuild.json | 2 +- package.serve.json | 2 +- www/js/diary/timelineHelper.ts | 10 ++++-- www/js/types/diaryTypes.ts | 61 +++++++++++++++++++++++++--------- 4 files changed, 55 insertions(+), 20 deletions(-) diff --git a/package.cordovabuild.json b/package.cordovabuild.json index d297819a3..ad55e2ccf 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -105,7 +105,7 @@ "@react-navigation/native": "^6.1.7", "@react-navigation/stack": "^6.3.17", "@shopify/flash-list": "^1.3.1", - "@types/leaflet": "^1.9.4", + "@types/leaflet": "1.9.4", "angular": "1.6.7", "angular-animate": "1.6.7", "angular-local-storage": "^0.7.1", diff --git a/package.serve.json b/package.serve.json index ea52b613f..c2a9706bb 100644 --- a/package.serve.json +++ b/package.serve.json @@ -57,7 +57,7 @@ "@react-navigation/stack": "^6.3.17", "@shopify/flash-list": "^1.3.1", "@types/jest": "^29.5.5", - "@types/leaflet": "^1.9.4", + "@types/leaflet": "1.9.4", "angular": "1.6.7", "angular-animate": "1.6.7", "angular-local-storage": "^0.7.1", diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 707a126ad..59bc6e964 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -5,7 +5,13 @@ import { getRawEntries } from '../services/commHelper'; import { ServerResponse, ServerData } from '../types/serverData'; import L from 'leaflet'; import { DateTime } from 'luxon'; -import { UserInputEntry, TripTransition, TimelineEntry, GeoJSONData } from '../types/diaryTypes'; +import { + UserInputEntry, + TripTransition, + TimelineEntry, + GeoJSONData, + UnprocessedTrip, +} from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; import { LabelOptions } from '../types/labelTypes'; @@ -534,7 +540,7 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) logDebug(`mapped trips to trip_gj_list of size ${raw_trip_gj_list.length}`); /* Filtering: we will keep trips that are 1) defined and 2) have a distance >= 100m or duration >= 5 minutes https://github.com/e-mission/e-mission-docs/issues/966#issuecomment-1709112578 */ - const trip_gj_list = raw_trip_gj_list.filter( + const trip_gj_list: UnprocessedTrip[] = raw_trip_gj_list.filter( (trip) => trip && (trip.distance >= 100 || trip.duration >= 300), ); logDebug( diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index fd7f03b38..534f1dfdc 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -7,6 +7,16 @@ import { ServerData, LocalDt } from './serverData'; import { FeatureCollection, Feature, Geometry } from 'geojson'; type ObjectId = { $oid: string }; + +type UserInput = { + /* for keys ending in 'user_input' (e.g. 'trip_user_input'), the server gives us the raw user + input object with 'data' and 'metadata' */ + [k: `${string}user_input`]: UserInputEntry; + /* for keys ending in 'confirm' (e.g. 'mode_confirm'), the server just gives us the user input value + as a string (e.g. 'walk', 'drove_alone') */ + [k: `${string}confirm`]: string; +}; + export type ConfirmedPlace = { additions: UserInputEntry[]; cleaned_place: ObjectId; @@ -17,14 +27,7 @@ export type ConfirmedPlace = { location: Geometry; raw_places: ObjectId[]; source: string; - user_input: { - /* for keys ending in 'user_input' (e.g. 'trip_user_input'), the server gives us the raw user - input object with 'data' and 'metadata' */ - [k: `${string}user_input`]: UserInputEntry; - /* for keys ending in 'confirm' (e.g. 'mode_confirm'), the server just gives us the user input value - as a string (e.g. 'walk', 'drove_alone') */ - [k: `${string}confirm`]: string; - }; + user_input: UserInput; exit_fmt_time: string; exit_ts: number; exit_local_dt: LocalDt; @@ -37,6 +40,39 @@ export type TripTransition = { ts: number; }; +type CompTripLocations = { + loc: { + coordinates: [number, number]; // [1,2.3] + }; + speed: number; + ts: number; +}; + +// Used for return type of readUnprocessedTrips +export type UnprocessedTrip = { + _id: ObjectId; + additions: UserInputEntry[]; + confidence_threshold: number; + distance: number; + duration: number; + end_fmt_time: string; + /* While the end_loc & start_loc objects are similar to GeoJSON's `Point` object, + they lack the additional GeoJSONObject methods, so `Point` cannot be used here. */ + end_loc: { type: string; coordinates: any[] }; + end_local_dt: LocalDt; + expectation: any; // TODO "{to_label: boolean}" + inferred_labels: any[]; // TODO + key: string; + locations?: CompTripLocations[]; + origin_key: string; // e.x., UNPROCESSED_trip + source: string; + start_local_dt: LocalDt; + start_ts: number; + start_loc: { type: string; coordinates: any[] }; + starting_trip?: any; + user_input: UserInput; +}; + /* These are the properties received from the server (basically matches Python code) This should match what Timeline.readAllCompositeTrips returns (an array of these objects) */ export type CompositeTrip = { @@ -71,14 +107,7 @@ export type CompositeTrip = { start_local_dt: LocalDt; start_place: ObjectId; start_ts: number; - user_input: { - /* for keys ending in 'user_input' (e.g. 'trip_user_input'), the server gives us the raw user - input object with 'data' and 'metadata' */ - [k: `${string}user_input`]: UserInputEntry; - /* for keys ending in 'confirm' (e.g. 'mode_confirm'), the server just gives us the user input value - as a string (e.g. 'walk', 'drove_alone') */ - [k: `${string}confirm`]: string; - }; + user_input: UserInput; }; /* The 'timeline' for a user is a list of their trips and places, From 667fec84a2bdb35b40bc13d2e80a060f6bb4cb37 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:46:57 -0800 Subject: [PATCH 32/52] Expanded diaryTypes --- www/js/diary/timelineHelper.ts | 8 ++++---- www/js/types/diaryTypes.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 59bc6e964..595fadf48 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -11,6 +11,7 @@ import { TimelineEntry, GeoJSONData, UnprocessedTrip, + FilteredLocation, } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; import { LabelOptions } from '../types/labelTypes'; @@ -338,8 +339,7 @@ const transitionTrip2TripObj = function (trip) { ); const getSensorData = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval; return getUnifiedDataForInterval('background/filtered_location', tq, getSensorData).then( - function (locationList: Array) { - // change 'any' later + function (locationList: Array>) { if (locationList.length == 0) { return undefined; } @@ -527,7 +527,7 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) logDebug(JSON.stringify(trip, null, 2)); }); var tripFillPromises = tripsList.map(transitionTrip2TripObj); - return Promise.all(tripFillPromises).then(function (raw_trip_gj_list) { + return Promise.all(tripFillPromises).then(function (raw_trip_gj_list: UnprocessedTrip[]) { // Now we need to link up the trips. linking unprocessed trips // to one another is fairly simple, but we need to link the // first unprocessed trip to the last processed trip. @@ -540,7 +540,7 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) logDebug(`mapped trips to trip_gj_list of size ${raw_trip_gj_list.length}`); /* Filtering: we will keep trips that are 1) defined and 2) have a distance >= 100m or duration >= 5 minutes https://github.com/e-mission/e-mission-docs/issues/966#issuecomment-1709112578 */ - const trip_gj_list: UnprocessedTrip[] = raw_trip_gj_list.filter( + const trip_gj_list = raw_trip_gj_list.filter( (trip) => trip && (trip.distance >= 100 || trip.duration >= 300), ); logDebug( diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 534f1dfdc..5fa81298d 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -191,6 +191,22 @@ export type SectionData = { distance: number; }; +// used in timelineHelper's `transitionTrip2TripObj` +export type FilteredLocation = { + accuracy: number; + altitude: number; + elapsedRealtimeNanos: number; + filter: number; + fmt_time: string; + heading: number; + latitude: number; + loc: Geometry; + local_dt: LocalDt; + longitude: number; + sensed_speed: number; + ts: number; +}; + export type GeoJSONStyledFeature = Feature & { style?: { color: string } }; export type GeoJSONData = { From 07c5f8012ef697bfb05a20bc0813f4a938947b75 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:01:21 -0800 Subject: [PATCH 33/52] Added test for readUnprocessedTrips --- www/__mocks__/timelineHelperMocks.ts | 79 +++++++++++++++++++++++++--- www/__tests__/timelineHelper.test.ts | 41 ++++++++++----- www/js/diary/timelineHelper.ts | 5 +- www/js/types/diaryTypes.ts | 2 +- 4 files changed, 106 insertions(+), 21 deletions(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index 4b7c9a96b..562e2c6c6 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -1,5 +1,11 @@ import { MetaData, ServerData, ServerResponse } from '../js/types/serverData'; -import { CompositeTrip, ConfirmedPlace, TripTransition } from '../js/types/diaryTypes'; +import { + CompositeTrip, + ConfirmedPlace, + FilteredLocation, + TripTransition, + UnprocessedTrip, +} from '../js/types/diaryTypes'; import { LabelOptions } from '../js/types/labelTypes'; const mockMetaData: MetaData = { @@ -56,8 +62,31 @@ const mockConfirmedPlaceData: ConfirmedPlace = { let tempMetaData = JSON.parse(JSON.stringify(mockMetaData)); tempMetaData.write_ts = 2; tempMetaData.origin_key = '2'; + export const mockMetaDataTwo = tempMetaData; +export const mockUnprocessedTrip: UnprocessedTrip = { + _id: { $oid: 'mockUnprocessedTrip' }, + additions: [], + confidence_threshold: 0.0, + distance: 1.0, + duration: 3.0, + end_fmt_time: '', + end_loc: { type: '', coordinates: [] }, + end_local_dt: null, + expectation: null, + inferred_labels: [], + key: 'mockUnprocessedTrip', + locations: [], + origin_key: '', + source: '', + start_local_dt: null, + start_ts: 0.1, + start_loc: { type: '', coordinates: [] }, + starting_trip: null, + user_input: null, +}; + export const mockData: ServerResponse = { phone_data: [ { @@ -150,18 +179,56 @@ export const mockDataTwo = { phone_data: [mockData.phone_data[0], newPhoneData], }; -export const mockTransition: Array> = [ +export const mockTransitions: Array> = [ + { + data: { + // mock of a startTransition + currstate: '', + transition: 'T_EXITED_GEOFENCE', + ts: 1, + }, + metadata: mockMetaData, + }, { data: { - currstate: 'STATE_WAITING_FOR_TRIP_TO_START', - transition: 'T_NOP', - ts: 12345.6789, + // mock of an endTransition + currstate: '', + transition: 'T_TRIP_ENDED', + ts: 9999, }, metadata: mockMetaData, }, ]; -export const mockTransitionTwo = mockTransition.push(mockTransition[0]); +const mockFilterLocation: FilteredLocation = { + accuracy: 0.1, + altitude: 100, + elapsedRealtimeNanos: 10000, + filter: 'time', + fmt_time: '', + heading: 1.0, + latitude: 1.0, + loc: null, + local_dt: null, + longitude: -1.0, + sensed_speed: 0, + ts: 100, +}; +let mockFilterLocationTwo = JSON.parse(JSON.stringify(mockFilterLocation)); +mockFilterLocationTwo.ts = 900; +mockFilterLocationTwo.longitude = 200; +mockFilterLocationTwo.longitude = -200; + +export const mockFilterLocations: Array> = [ + { + data: mockFilterLocation, + metadata: mockMetaData, + }, + { + data: mockFilterLocationTwo, + metadata: mockMetaDataTwo, + }, +]; // When called by mocks, pair 1 returns 1 value, Pair two 2, pair 3 returns none. export const fakeStartTsOne = -14576291; diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 115bf2d43..87e5995d3 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -86,13 +86,12 @@ describe('compositeTrips2TimelineMap', () => { it('Works with a list of len >= 1, with flag', () => { testValue = compositeTrips2TimelineMap(tripListTwo, true); - console.log(`Len: ${testValue.size}`); expect(testValue.size).toBe(6); }); }); -// updateAllUnprocessedinputs tests -it('can use an appConfig to get labelInputKeys', () => { +// Tests for updateLocalUnprocessedInputs & keysForLabelInputs +describe('The updateUnprocessedInput functions can ', () => { const mockAppConfigOne = { survey_info: { 'trip-labels': 'ENKETO', @@ -106,9 +105,15 @@ it('can use an appConfig to get labelInputKeys', () => { mode_studied: 'sample', }, }; - expect(keysForLabelInputs(mockAppConfigOne)).rejects; - expect(keysForLabelInputs(mockAppConfigOne)).toEqual(['manual/trip_user_input']); - expect(keysForLabelInputs(mockAppConfigTwo).length).toEqual(3); + // keysForLabelInputs tests + it('use an appConfig to get labelInputKeys', () => { + expect(keysForLabelInputs(mockAppConfigOne)).rejects; + expect(keysForLabelInputs(mockAppConfigOne)).toEqual(['manual/trip_user_input']); + expect(keysForLabelInputs(mockAppConfigTwo).length).toEqual(3); + }); + it('update the unprocessed labels', () => { + // TODO + }); }); // Tests for readAllCompositeTrips @@ -162,9 +167,13 @@ it('Works with multiple trips', async () => { // Tests for `readUnprocessedTrips` jest.mock('../js/services/unifiedDataLoader', () => ({ getUnifiedDataForInterval: jest.fn((key, tq, combiner) => { - if (tq.startTs === mockTLH.fakeStartTsOne) return Promise.resolve(mockTLH.mockTransition); - if (tq.startTs === mockTLH.fakeStartTsTwo) return Promise.resolve(mockTLH.mockTransitionTwo); - return Promise.resolve([]); + if (key === 'statemachine/transition') { + if (tq.startTs === mockTLH.fakeStartTsOne) return Promise.resolve(mockTLH.mockTransitions); + return Promise.resolve([]); + } + if (key === 'background/filtered_location') { + return Promise.resolve(mockTLH.mockFilterLocations); + } }), })); @@ -172,13 +181,21 @@ it('works when there are no unprocessed trips...', async () => { expect(readUnprocessedTrips(-1, -1, null)).resolves.toEqual([]); }); -// In manual testing, it seems that `trip_gj_list` always returns -// as an empty array - should find data where this is different... it('works when there are one or more unprocessed trips...', async () => { const testValueOne = await readUnprocessedTrips( mockTLH.fakeStartTsOne, mockTLH.fakeEndTsOne, null, ); - expect(testValueOne).toEqual([]); + expect(testValueOne.length).toEqual(1); + expect(testValueOne[0]).toEqual( + expect.objectContaining({ + origin_key: expect.any(String), + distance: expect.any(Number), + start_loc: expect.objectContaining({ + type: expect.any(String), + coordinates: expect.any(Array), + }), + }), + ); }); diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 595fadf48..6b5e072a4 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -333,8 +333,9 @@ const transitionTrip2TripObj = function (trip) { endTs: tripEndTransition.data.ts, }; logDebug( - 'About to pull location data for range' + + 'About to pull location data for range ' + DateTime.fromSeconds(tripStartTransition.data.ts).toLocaleString(DateTime.DATETIME_MED) + + ' to ' + DateTime.fromSeconds(tripEndTransition.data.ts).toLocaleString(DateTime.DATETIME_MED), ); const getSensorData = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval; @@ -544,7 +545,7 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) (trip) => trip && (trip.distance >= 100 || trip.duration >= 300), ); logDebug( - `after filtering undefined and distance < 100, trip_gj_list size = ${raw_trip_gj_list.length}`, + `after filtering undefined and distance < 100, trip_gj_list size = ${trip_gj_list.length}`, ); // Link 0th trip to first, first to second, ... for (var i = 0; i < trip_gj_list.length - 1; i++) { diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 5fa81298d..6cc1069ae 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -196,7 +196,7 @@ export type FilteredLocation = { accuracy: number; altitude: number; elapsedRealtimeNanos: number; - filter: number; + filter: string; fmt_time: string; heading: number; latitude: number; From 0d1717d38079a179c1b174a2522ab991729e31c4 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:12:52 -0800 Subject: [PATCH 34/52] Added tests for updateUnprocessedInputs - Added tests that utilize labelsPRomises - Cleaned up mocks, diaryTypes. --- www/__mocks__/timelineHelperMocks.ts | 79 ++++++++++++++++++++++++---- www/__tests__/timelineHelper.test.ts | 74 ++++++++++++++------------ www/js/diary/timelineHelper.ts | 10 ++-- www/js/types/diaryTypes.ts | 29 ++++------ 4 files changed, 126 insertions(+), 66 deletions(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index 562e2c6c6..4edcb2399 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -57,12 +57,10 @@ const mockConfirmedPlaceData: ConfirmedPlace = { enter_ts: 1437578093.881, exit_ts: 1437578093.881, }; - // using parse/stringify to deep copy & populate data let tempMetaData = JSON.parse(JSON.stringify(mockMetaData)); tempMetaData.write_ts = 2; tempMetaData.origin_key = '2'; - export const mockMetaDataTwo = tempMetaData; export const mockUnprocessedTrip: UnprocessedTrip = { @@ -87,7 +85,7 @@ export const mockUnprocessedTrip: UnprocessedTrip = { user_input: null, }; -export const mockData: ServerResponse = { +export const mockCompData: ServerResponse = { phone_data: [ { data: { @@ -165,18 +163,16 @@ export const mockData: ServerResponse = { }, ], }; - // Setup for second mockData -let newPhoneData = JSON.parse(JSON.stringify(mockData.phone_data[0])); +let newPhoneData = JSON.parse(JSON.stringify(mockCompData.phone_data[0])); newPhoneData.data._id.$oid = 'mockDataTwo'; newPhoneData.metadata = mockMetaDataTwo; newPhoneData.data.start_confirmed_place.metadata = mockMetaDataTwo; newPhoneData.data.start_confirmed_place._id.$oid = 'startConfirmedPlaceTwo'; newPhoneData.data.end_confirmed_place.metadata = mockMetaDataTwo; newPhoneData.data.end_confirmed_place._id.$oid = 'endConfirmedPlaceTwo'; - -export const mockDataTwo = { - phone_data: [mockData.phone_data[0], newPhoneData], +export const mockCompDataTwo = { + phone_data: [mockCompData.phone_data[0], newPhoneData], }; export const mockTransitions: Array> = [ @@ -230,7 +226,72 @@ export const mockFilterLocations: Array> = [ }, ]; -// When called by mocks, pair 1 returns 1 value, Pair two 2, pair 3 returns none. +export const mockAppConfigOne = { + survey_info: { + 'trip-labels': 'ENKETO', + }, +}; +export const mockAppConfigTwo = { + survey_info: { + 'trip-labels': 'Other', + }, + intro: { + mode_studied: 'sample_study', + }, +}; +export const mockAppConfigThree = { + survey_info: { + 'trip-labels': 'Other', + }, + intro: { + mode_studied: false, + }, +}; + +export const mockLabelDataPromises = [ + Promise.resolve([ + // Mode + { + data: { + end_ts: 1681438322.981, + label: 'walk', + start_ts: 1681437527.4971218, + }, + metadata: mockMetaData, + }, + { + data: { + end_ts: 1681439339.983, + label: 'walk', + start_ts: 1681438918.6598706, + }, + metadata: mockMetaDataTwo, + }, + ]), + Promise.resolve([ + // Purpose + { + data: { + end_ts: 1681438322.981, + label: 'test', + start_ts: 1681437527.4971218, + }, + metadata: mockMetaData, + }, + { + data: { + end_ts: 1681438322.983, + label: 'testValue', + start_ts: 1681438918.6598706, + }, + metadata: mockMetaDataTwo, + }, + ]), + Promise.resolve([]), // Replaced_Mode +]; +//let mockLabelDataPromisesTwo = JSON.parse(JSON.stringify(mockLabelDataPromises)); + +// Used by jest.mocks() to return a various mocked objects. export const fakeStartTsOne = -14576291; export const fakeEndTsOne = -13885091; export const fakeStartTsTwo = 1092844665; diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 87e5995d3..3f2158936 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -4,11 +4,13 @@ import { readAllCompositeTrips, readUnprocessedTrips, compositeTrips2TimelineMap, + updateUnprocessedInputs, keysForLabelInputs, } from '../js/diary/timelineHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import * as mockTLH from '../__mocks__/timelineHelperMocks'; -import { GeoJSONData, GeoJSONStyledFeature } from '../js/types/diaryTypes'; +import { unprocessedLabels } from '../js/diary/timelineHelper'; +import { GeoJSONData, GeoJSONStyledFeature, UserInputEntry } from '../js/types/diaryTypes'; mockLogger(); mockAlert(); @@ -40,7 +42,7 @@ describe('useGeojsonForTrip', () => { it('works without labelMode flag', () => { const testValue = useGeojsonForTrip( - mockTLH.mockDataTwo.phone_data[1].data, + mockTLH.mockCompDataTwo.phone_data[1].data, mockTLH.mockLabelOptions, ); checkGeojson(testValue); @@ -49,14 +51,14 @@ describe('useGeojsonForTrip', () => { }); describe('compositeTrips2TimelineMap', () => { - const tripListOne = [mockTLH.mockData.phone_data[0].data]; + const tripListOne = [mockTLH.mockCompData.phone_data[0].data]; const tripListTwo = [ - mockTLH.mockDataTwo.phone_data[0].data, - mockTLH.mockDataTwo.phone_data[1].data, + mockTLH.mockCompDataTwo.phone_data[0].data, + mockTLH.mockCompDataTwo.phone_data[1].data, ]; - const keyOne = mockTLH.mockData.phone_data[0].data._id.$oid; - const keyTwo = mockTLH.mockDataTwo.phone_data[1].data._id.$oid; - const keyThree = mockTLH.mockData.phone_data[0].data._id.$oid; + const keyOne = mockTLH.mockCompData.phone_data[0].data._id.$oid; + const keyTwo = mockTLH.mockCompDataTwo.phone_data[1].data._id.$oid; + const keyThree = mockTLH.mockCompData.phone_data[0].data._id.$oid; let testValue; it('Works with an empty list', () => { @@ -90,38 +92,42 @@ describe('compositeTrips2TimelineMap', () => { }); }); -// Tests for updateLocalUnprocessedInputs & keysForLabelInputs -describe('The updateUnprocessedInput functions can ', () => { - const mockAppConfigOne = { - survey_info: { - 'trip-labels': 'ENKETO', - }, - }; - const mockAppConfigTwo = { - survey_info: { - 'trip-labels': 'Other', - }, - intro: { - mode_studied: 'sample', - }, - }; - // keysForLabelInputs tests - it('use an appConfig to get labelInputKeys', () => { - expect(keysForLabelInputs(mockAppConfigOne)).rejects; - expect(keysForLabelInputs(mockAppConfigOne)).toEqual(['manual/trip_user_input']); - expect(keysForLabelInputs(mockAppConfigTwo).length).toEqual(3); - }); - it('update the unprocessed labels', () => { - // TODO - }); +it('use an appConfig to get labelInputKeys', () => { + expect(keysForLabelInputs(mockTLH.mockAppConfigOne)).toEqual(['manual/trip_user_input']); + expect(keysForLabelInputs(mockTLH.mockAppConfigTwo).length).toEqual(3); +}); + +// updateUnprocessedInputs Tests +jest.mock('../js/survey/multilabel/confirmHelper', () => ({ + ...jest.requireActual('../js/survey/multilabel/confirmHelper'), + getLabelInputs: jest.fn(() => ['MODE', 'PURPOSE', 'REPLACED_MODE']), +})); + +it('processed empty labels', async () => { + await updateUnprocessedInputs([], [], mockTLH.mockAppConfigThree); + expect(unprocessedLabels).toEqual({}); +}); + +it('updates unprocessed labels', async () => { + await updateUnprocessedInputs(mockTLH.mockLabelDataPromises, [], mockTLH.mockAppConfigThree); + expect(unprocessedLabels).toEqual( + expect.objectContaining({ + MODE: expect.any(Array), + PURPOSE: expect.any(Array), + REPLACED_MODE: expect.any(Array), + }), + ); + expect(unprocessedLabels.MODE.length).toEqual(2); + expect(unprocessedLabels.PURPOSE.length).toEqual(2); + expect(unprocessedLabels.REPLACED_MODE.length).toEqual(0); }); // Tests for readAllCompositeTrips // Once we have end-to-end testing, we could utilize getRawEnteries. jest.mock('../js/services/commHelper', () => ({ getRawEntries: jest.fn((key, startTs, endTs, valTwo) => { - if (startTs === mockTLH.fakeStartTsOne) return mockTLH.mockData; - if (startTs == mockTLH.fakeStartTsTwo) return mockTLH.mockDataTwo; + if (startTs === mockTLH.fakeStartTsOne) return mockTLH.mockCompData; + if (startTs == mockTLH.fakeStartTsTwo) return mockTLH.mockCompDataTwo; return {}; }), })); diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 6b5e072a4..616a20d1d 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -95,8 +95,12 @@ const getUnprocessedInputQuery = (pipelineRange) => ({ endTs: DateTime.now().toUnixInteger() + 10, }); -function updateUnprocessedInputs(labelsPromises, notesPromises, appConfig) { - Promise.all([...labelsPromises, ...notesPromises]).then((comboResults) => { +/** + * updateUnprocessedInputs is a helper function for updateLocalUnprocessedInputs + * and updateAllUnprocessedInputs, exported for unit testing. + */ +export function updateUnprocessedInputs(labelsPromises, notesPromises, appConfig) { + return Promise.all([...labelsPromises, ...notesPromises]).then((comboResults) => { 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 @@ -121,7 +125,6 @@ function updateUnprocessedInputs(labelsPromises, notesPromises, appConfig) { * @param pipelineRange an object with start_ts and end_ts representing the range of time * for which travel data has been processed through the pipeline on the server * @param appConfig the app configuration - * @returns Promise an array with 1) results for labels and 2) results for notes */ export async function updateLocalUnprocessedInputs(pipelineRange, appConfig) { const BEMUserCache = window['cordova'].plugins.BEMUserCache; @@ -141,7 +144,6 @@ export async function updateLocalUnprocessedInputs(pipelineRange, appConfig) { * @param pipelineRange an object with start_ts and end_ts representing the range of time * for which travel data has been processed through the pipeline on the server * @param appConfig the app configuration - * @returns Promise an array with 1) results for labels and 2) results for notes */ export async function updateAllUnprocessedInputs(pipelineRange, appConfig) { const tq = getUnprocessedInputQuery(pipelineRange); diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 6cc1069ae..ca629dbe2 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -135,27 +135,18 @@ export type SectionSummary = { duration: { [k: MotionTypeKey | BaseModeKey]: number }; }; -export type UserInputEntry = { - data: { - end_ts: number; - start_ts: number; - label: string; - start_local_dt?: LocalDt; - end_local_dt?: LocalDt; - status?: string; - match_id?: string; - }; - metadata: { - time_zone: string; - plugin: string; - write_ts: number; - platform: string; - read_ts: number; - key: string; - }; - key?: string; +type UserInputData = { + end_ts: number; + start_ts: number; + label: string; + start_local_dt?: LocalDt; + end_local_dt?: LocalDt; + status?: string; + match_id?: string; }; +export type UserInputEntry = ServerData; + export type Location = { speed: number; heading: number; From d93da0760512b64448faaf38c03e4a9ab0334dd7 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Fri, 1 Dec 2023 12:00:05 -0800 Subject: [PATCH 35/52] Bug fix for Enekto Survey --- www/js/survey/enketo/enketoHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 424e364d2..4a46e8fb3 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -108,7 +108,7 @@ const _getMostRecent = (answers) => { export function loadPreviousResponseForSurvey(dataKey: string) { const tq = window['cordova'].plugins.BEMUserCache.getAllTimeQuery(); logDebug('loadPreviousResponseForSurvey: dataKey = ' + dataKey + '; tq = ' + tq); - const getMethod = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval; + const getMethod = window['cordova'].plugins.BEMUserCache.getMessagesForInterval; return getUnifiedDataForInterval(dataKey, tq, getMethod).then((answers) => _getMostRecent(answers), ); From b59c65f1735300892109b4b3acc2138ed17153a4 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Mon, 4 Dec 2023 08:00:56 -0800 Subject: [PATCH 36/52] Update www/js/diary/diaryHelper.ts Co-authored-by: Jack Greenlee --- www/js/diary/diaryHelper.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index afecf34d8..efdad3349 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -5,8 +5,7 @@ import i18next from 'i18next'; import { DateTime } from 'luxon'; import { CompositeTrip } from '../types/diaryTypes'; import { LabelOptions } from '../types/labelTypes'; - -const humanizeDuration = require('humanize-duration'); +import humanizeDuration from 'humanize-duration'; export const modeColors = { pink: '#c32e85', // oklch(56% 0.2 350) // e-car From 92f4a64b82b5e5709c995db44d37f144e426dd4e Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Mon, 4 Dec 2023 09:06:55 -0800 Subject: [PATCH 37/52] Updated tests for luxon time conversion --- www/__tests__/diaryHelper.test.ts | 4 ++-- www/js/diary/diaryHelper.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/www/__tests__/diaryHelper.test.ts b/www/__tests__/diaryHelper.test.ts index ee1f1649d..0eb4a8628 100644 --- a/www/__tests__/diaryHelper.test.ts +++ b/www/__tests__/diaryHelper.test.ts @@ -12,10 +12,10 @@ import initializedI18next from '../js/i18nextInit'; window['i18next'] = initializedI18next; it('returns a formatted date', () => { - expect(getFormattedDate('2023-09-18T00:00:00-07:00')).toBe('Mon September 18, 2023'); + expect(getFormattedDate('2023-09-18T00:00:00-07:00')).toBe('Mon, September 18, 2023'); expect(getFormattedDate('')).toBeUndefined(); expect(getFormattedDate('2023-09-18T00:00:00-07:00', '2023-09-21T00:00:00-07:00')).toBe( - 'Mon September 18, 2023 - Thu September 21, 2023', + 'Mon, September 18, 2023 - Thu, September 21, 2023', ); }); diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index efdad3349..36f57f192 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -123,7 +123,7 @@ export function getFormattedDate(beginFmtTime: string, endFmtTime?: string) { day: '2-digit', year: 'numeric', }); - return tConversion.replace(',', ''); + return tConversion; } /** From e3dc66f79df5bef1c606326a048fa97ad8016093 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Mon, 4 Dec 2023 09:28:11 -0800 Subject: [PATCH 38/52] Cleaned up code --- www/js/diary/diaryHelper.ts | 5 ++--- www/js/diary/timelineHelper.ts | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index 36f57f192..1ac36281e 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -152,10 +152,9 @@ export function getFormattedTimeRange(beginFmtTime: string, endFmtTime: string) const beginTime = DateTime.fromISO(beginFmtTime, { setZone: true }); const endTime = DateTime.fromISO(endFmtTime, { setZone: true }); const range = endTime.diff(beginTime, ['hours', 'minutes']); - const unitsToDisplay = range.hours < 1 ? ['m'] : ['h']; return humanizeDuration(range.as('milliseconds'), { language: i18next.language, - units: unitsToDisplay, + largest: 1, round: true, }); } @@ -196,5 +195,5 @@ export function getLocalTimeString(dt) { hour: dt.hour, minute: dt.minute, }); - return dateTime.toFormat('h:mm a'); + return dateTime.toLocaleString(DateTime.TIME_SIMPLE); } diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 616a20d1d..44c0547a4 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -277,9 +277,9 @@ const points2TripProps = function (locationPoints) { const startTime = DateTime.fromSeconds(startPoint.data.ts).setZone(startPoint.metadata.time_zone); const endTime = DateTime.fromSeconds(endPoint.data.ts).setZone(endPoint.metadata.time_zone); - const speeds = [], - dists = []; - var loc, locLatLng; + const speeds = []; + const dists = []; + let loc, locLatLng; locationPoints.forEach((pt) => { const ptLatLng = L.latLng([pt.data.latitude, pt.data.longitude]); if (loc) { @@ -353,7 +353,7 @@ const transitionTrip2TripObj = function (trip) { ); }; - var filteredLocationList = sortedLocationList.filter(retainInRange); + const filteredLocationList = sortedLocationList.filter(retainInRange); // Fix for https://github.com/e-mission/e-mission-docs/issues/417 if (filteredLocationList.length == 0) { @@ -439,11 +439,11 @@ const isEndingTransition = function (transWrapper) { * Let's abstract this out into our own minor state machine. */ const transitions2Trips = function (transitionList: Array>) { - var inTrip = false; - var tripList = []; - var currStartTransitionIndex = -1; - var currEndTransitionIndex = -1; - var processedUntil = 0; + let inTrip = false; + const tripList = []; + let currStartTransitionIndex = -1; + let currEndTransitionIndex = -1; + let processedUntil = 0; while (processedUntil < transitionList.length) { // Logger.log("searching within list = "+JSON.stringify(transitionList.slice(processedUntil))); @@ -509,7 +509,7 @@ const linkTrips = function (trip1, trip2) { }; export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) { - var tq = { key: 'write_ts', startTs, endTs }; + const tq = { key: 'write_ts', startTs, endTs }; logDebug( 'about to query for unprocessed trips from ' + DateTime.fromSeconds(tq.startTs).toLocaleString(DateTime.DATETIME_MED) + @@ -529,7 +529,7 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) tripsList.forEach(function (trip) { logDebug(JSON.stringify(trip, null, 2)); }); - var tripFillPromises = tripsList.map(transitionTrip2TripObj); + const tripFillPromises = tripsList.map(transitionTrip2TripObj); return Promise.all(tripFillPromises).then(function (raw_trip_gj_list: UnprocessedTrip[]) { // Now we need to link up the trips. linking unprocessed trips // to one another is fairly simple, but we need to link the @@ -550,7 +550,7 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) `after filtering undefined and distance < 100, trip_gj_list size = ${trip_gj_list.length}`, ); // Link 0th trip to first, first to second, ... - for (var i = 0; i < trip_gj_list.length - 1; i++) { + for (let i = 0; i < trip_gj_list.length - 1; i++) { linkTrips(trip_gj_list[i], trip_gj_list[i + 1]); } logDebug(`finished linking trips for list of size ${trip_gj_list.length}`); From d844466e8af7e0a861f85746b22df45ff96fee1b Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 4 Dec 2023 12:46:13 -0500 Subject: [PATCH 39/52] add TimestampRange type --- www/js/types/diaryTypes.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index ca629dbe2..722d42d8d 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -114,6 +114,8 @@ export type CompositeTrip = { so a 'timeline entry' is either a trip or a place. */ export type TimelineEntry = ConfirmedPlace | CompositeTrip; +export type TimestampRange = { start_ts: number; end_ts: number }; + /* These properties aren't received from the server, but are derived from the above properties. They are used in the UI to display trip/place details and are computed by the useDerivedProperties hook. */ export type DerivedProperties = { From c42665f1839a067a7e0d563ee208d02771b2fb81 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 4 Dec 2023 12:48:31 -0500 Subject: [PATCH 40/52] add getMessagesForInterval to BEMUserCache mock This is much like getMessages, but also filters by time in addition to by key. Also modified both of these functions to support the `withMetadata` argument in mocks --- www/__mocks__/cordovaMocks.ts | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 3d4a3e202..2954c1351 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -33,9 +33,11 @@ export const mockFile = () => { //for consent document const _storage = {}; +type MessageData = any; +type Message = { key: string; data: MessageData; metadata: { write_ts: number; [k: string]: any } }; export const mockBEMUserCache = () => { const _cache = {}; - const messages = []; + const messages: Message[] = []; const mockBEMUserCache = { getLocalStorage: (key: string, isSecure: boolean) => { return new Promise((rs, rj) => @@ -86,17 +88,35 @@ export const mockBEMUserCache = () => { putMessage: (key: string, value: any) => { return new Promise((rs, rj) => setTimeout(() => { - messages.push({ key, value }); + messages.push({ + key, + data: value, + // write_ts is epoch time in seconds + metadata: { write_ts: Math.floor(Date.now() / 1000) }, + }); rs(); }, 100), ); }, getAllMessages: (key: string, withMetadata?: boolean) => { - return new Promise((rs, rj) => + return new Promise((rs, rj) => + setTimeout(() => { + rs(messages.filter((m) => m.key == key).map((m) => (withMetadata ? m : m.data))); + }, 100), + ); + }, + getMessagesForInterval: (key: string, tq, withMetadata?: boolean) => { + return new Promise((rs, rj) => setTimeout(() => { - rs(messages.filter((m) => m.key == key).map((m) => m.value)); + rs( + messages + .filter((m) => m.key == key) + .filter((m) => m.metadata[tq.key] >= tq.startTs && m.metadata.write_ts <= tq.endTs) + .map((m) => (withMetadata ? m : m.data)), + ); }, 100), ); + // Used for getUnifiedDataForInterval }, getDocument: (key: string, withMetadata?: boolean) => { return new Promise((rs, rj) => @@ -116,9 +136,6 @@ export const mockBEMUserCache = () => { return false; } }, - getMessagesForInterval: () => { - // Used for getUnifiedDataForInterval - }, }; window['cordova'] ||= {}; window['cordova'].plugins ||= {}; From 650564a2f197be930bdf8f5f1e9c57619f621265 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 4 Dec 2023 12:52:55 -0500 Subject: [PATCH 41/52] add tests for updateLocalUnprocessedInputs The stategy here is to use the mocked BEMUserCache to actually record some inputs (across both labels¬es and for trip&places), and then check that the new inputs show up in `unprocessedLabels` and `unprocessedNotes` after processing. --- www/__mocks__/timelineHelperMocks.ts | 51 +---------- www/__tests__/timelineHelper.test.ts | 121 +++++++++++++++++++++++---- www/js/diary/timelineHelper.ts | 7 +- 3 files changed, 115 insertions(+), 64 deletions(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index 4edcb2399..d6720f88b 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -226,20 +226,20 @@ export const mockFilterLocations: Array> = [ }, ]; -export const mockAppConfigOne = { +export const mockConfigEnketo = { survey_info: { 'trip-labels': 'ENKETO', }, }; -export const mockAppConfigTwo = { +export const mockConfigModeOfStudy = { survey_info: { - 'trip-labels': 'Other', + 'trip-labels': 'MULTILABEL', }, intro: { mode_studied: 'sample_study', }, }; -export const mockAppConfigThree = { +export const mockConfigNoModeOfStudy = { survey_info: { 'trip-labels': 'Other', }, @@ -248,49 +248,6 @@ export const mockAppConfigThree = { }, }; -export const mockLabelDataPromises = [ - Promise.resolve([ - // Mode - { - data: { - end_ts: 1681438322.981, - label: 'walk', - start_ts: 1681437527.4971218, - }, - metadata: mockMetaData, - }, - { - data: { - end_ts: 1681439339.983, - label: 'walk', - start_ts: 1681438918.6598706, - }, - metadata: mockMetaDataTwo, - }, - ]), - Promise.resolve([ - // Purpose - { - data: { - end_ts: 1681438322.981, - label: 'test', - start_ts: 1681437527.4971218, - }, - metadata: mockMetaData, - }, - { - data: { - end_ts: 1681438322.983, - label: 'testValue', - start_ts: 1681438918.6598706, - }, - metadata: mockMetaDataTwo, - }, - ]), - Promise.resolve([]), // Replaced_Mode -]; -//let mockLabelDataPromisesTwo = JSON.parse(JSON.stringify(mockLabelDataPromises)); - // Used by jest.mocks() to return a various mocked objects. export const fakeStartTsOne = -14576291; export const fakeEndTsOne = -13885091; diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 3f2158936..1990a5427 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -6,6 +6,7 @@ import { compositeTrips2TimelineMap, updateUnprocessedInputs, keysForLabelInputs, + updateLocalUnprocessedInputs, } from '../js/diary/timelineHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import * as mockTLH from '../__mocks__/timelineHelperMocks'; @@ -93,8 +94,8 @@ describe('compositeTrips2TimelineMap', () => { }); it('use an appConfig to get labelInputKeys', () => { - expect(keysForLabelInputs(mockTLH.mockAppConfigOne)).toEqual(['manual/trip_user_input']); - expect(keysForLabelInputs(mockTLH.mockAppConfigTwo).length).toEqual(3); + expect(keysForLabelInputs(mockTLH.mockConfigEnketo)).toEqual(['manual/trip_user_input']); + expect(keysForLabelInputs(mockTLH.mockConfigModeOfStudy).length).toEqual(3); }); // updateUnprocessedInputs Tests @@ -104,22 +105,114 @@ jest.mock('../js/survey/multilabel/confirmHelper', () => ({ })); it('processed empty labels', async () => { - await updateUnprocessedInputs([], [], mockTLH.mockAppConfigThree); + await updateUnprocessedInputs([], [], mockTLH.mockConfigNoModeOfStudy); expect(unprocessedLabels).toEqual({}); }); -it('updates unprocessed labels', async () => { - await updateUnprocessedInputs(mockTLH.mockLabelDataPromises, [], mockTLH.mockAppConfigThree); - expect(unprocessedLabels).toEqual( - expect.objectContaining({ - MODE: expect.any(Array), - PURPOSE: expect.any(Array), - REPLACED_MODE: expect.any(Array), - }), +it('processes some mode and purpose labels after they were just recorded', async () => { + // record some labels + await window['cordova'].plugins.BEMUserCache.putMessage('manual/mode_confirm', { + start_ts: 2, + end_ts: 3, + label: 'tricycle', + }); + await window['cordova'].plugins.BEMUserCache.putMessage('manual/purpose_confirm', { + start_ts: 2, + end_ts: 3, + label: 'shopping', + }); + + // update unprocessed inputs and check that the new labels show up in unprocessedLabels + await updateLocalUnprocessedInputs({ start_ts: 2, end_ts: 3 }, mockTLH.mockConfigNoModeOfStudy); + expect(unprocessedLabels['MODE'].length).toEqual(1); + expect(unprocessedLabels['MODE'][0].data.label).toEqual('tricycle'); + expect(unprocessedLabels['PURPOSE'].length).toEqual(1); + expect(unprocessedLabels['PURPOSE'][0].data.label).toEqual('shopping'); +}); + +it('processes trip- and place- survey responses after they were just recorded', async () => { + // record two survey responses, one for trip_user_input and one for place_user_input + const tripSurveyResponse = { + start_ts: 4, + end_ts: 5, + name: 'FooBarSurvey', + version: 1.2, + label: '1 foobar', + match_id: 'd263935e-9163-4072-9909-9d3e1edb31be', + key: 'manual/trip_user_input', + xmlResponse: ` 2023-12-04T12:12:38.968-05:00 2023-12-04T12:12:38.970-05:00 bar uuid:75dc7b18-2a9d-4356-b66e-d63dfa7568ca `, + }; + const placeSurveyResponse = { + ...tripSurveyResponse, + start_ts: 5, + end_ts: 6, + key: 'manual/place_user_input', + }; + await window['cordova'].plugins.BEMUserCache.putMessage( + 'manual/trip_user_input', + tripSurveyResponse, ); - expect(unprocessedLabels.MODE.length).toEqual(2); - expect(unprocessedLabels.PURPOSE.length).toEqual(2); - expect(unprocessedLabels.REPLACED_MODE.length).toEqual(0); + await window['cordova'].plugins.BEMUserCache.putMessage( + 'manual/place_user_input', + placeSurveyResponse, + ); + + // update unprocessed inputs and check that the trip survey response shows up in unprocessedLabels + await updateLocalUnprocessedInputs({ start_ts: 4, end_ts: 6 }, mockTLH.mockConfigEnketo); + expect(unprocessedLabels['SURVEY'][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); +}); + +it('processes some trip- and place- level additions after they were just recorded', async () => { + // record two additions, one for trip_addition_input and one for place_addition_input + const tripAdditionOne = { + start_ts: 6, + end_ts: 7, + key: 'manual/trip_addition_input', + data: { foo: 'bar' }, + }; + const tripAdditionTwo = { + ...tripAdditionOne, + data: { foo: 'baz' }, + }; + const placeAdditionOne = { + ...tripAdditionOne, + start_ts: 7, + end_ts: 8, + key: 'manual/place_addition_input', + }; + const placeAdditionTwo = { + ...placeAdditionOne, + data: { foo: 'baz' }, + }; + Promise.all([ + window['cordova'].plugins.BEMUserCache.putMessage( + 'manual/trip_addition_input', + tripAdditionOne, + ), + window['cordova'].plugins.BEMUserCache.putMessage( + 'manual/place_addition_input', + tripAdditionTwo, + ), + window['cordova'].plugins.BEMUserCache.putMessage( + 'manual/trip_addition_input', + placeAdditionOne, + ), + window['cordova'].plugins.BEMUserCache.putMessage( + 'manual/place_addition_input', + placeAdditionTwo, + ), + ]).then(() => { + // update unprocessed inputs and check that all additions show up in unprocessedNotes + updateLocalUnprocessedInputs({ start_ts: 6, end_ts: 8 }, mockTLH.mockConfigEnketo); + expect(unprocessedLabels['NOTES'].length).toEqual(4); + expect(unprocessedLabels['NOTES'][0].data).toEqual(tripAdditionOne); + expect(unprocessedLabels['NOTES'][1].data).toEqual(tripAdditionTwo); + expect(unprocessedLabels['NOTES'][2].data).toEqual(placeAdditionOne); + expect(unprocessedLabels['NOTES'][3].data).toEqual(placeAdditionTwo); + }); }); // Tests for readAllCompositeTrips diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 616a20d1d..593020517 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -12,6 +12,7 @@ import { GeoJSONData, UnprocessedTrip, FilteredLocation, + TimestampRange, } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; import { LabelOptions } from '../types/labelTypes'; @@ -89,7 +90,7 @@ export let unprocessedLabels: { [key: string]: UserInputEntry[] } = {}; /* 'NOTES' are 1:n - each trip or place can have any number of notes */ export let unprocessedNotes: UserInputEntry[] = []; -const getUnprocessedInputQuery = (pipelineRange) => ({ +const getUnprocessedInputQuery = (pipelineRange: TimestampRange) => ({ key: 'write_ts', startTs: pipelineRange.end_ts - 10, endTs: DateTime.now().toUnixInteger() + 10, @@ -126,7 +127,7 @@ export function updateUnprocessedInputs(labelsPromises, notesPromises, appConfig * for which travel data has been processed through the pipeline on the server * @param appConfig the app configuration */ -export async function updateLocalUnprocessedInputs(pipelineRange, appConfig) { +export async function updateLocalUnprocessedInputs(pipelineRange: TimestampRange, appConfig) { const BEMUserCache = window['cordova'].plugins.BEMUserCache; const tq = getUnprocessedInputQuery(pipelineRange); const labelsPromises = keysForLabelInputs(appConfig).map((key) => @@ -145,7 +146,7 @@ export async function updateLocalUnprocessedInputs(pipelineRange, appConfig) { * for which travel data has been processed through the pipeline on the server * @param appConfig the app configuration */ -export async function updateAllUnprocessedInputs(pipelineRange, appConfig) { +export async function updateAllUnprocessedInputs(pipelineRange: TimestampRange, appConfig) { const tq = getUnprocessedInputQuery(pipelineRange); const getMethod = window['cordova'].plugins.BEMUserCache.getMessagesForInterval; const labelsPromises = keysForLabelInputs(appConfig).map((key) => From 356336c6e7ebb73f8e11a4ff4098591d10b27e25 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 4 Dec 2023 13:17:27 -0500 Subject: [PATCH 42/52] fix diaryTypes A few properties duplicated while resolving merge conflicts --- www/js/types/diaryTypes.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index ef520881a..92cc28d74 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -33,9 +33,6 @@ export type ConfirmedPlace = { raw_places: ObjectId[]; source: string; user_input: UserInput; - exit_fmt_time: string; - exit_ts: number; - exit_local_dt: LocalDt; starting_trip: ObjectId; }; From f80deb98dfb43c00b7571c5612ebf9aad5e47ab2 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 4 Dec 2023 14:03:30 -0500 Subject: [PATCH 43/52] fix timelineHelper test - made enketoHelper's filterByNameAndVersion accept appConfig as a parameter instead of retrieving it itself. This is 1) simpler, 2) doesn't have to be async anymore and 3) easier to test by swapping out configs Then, fixed the timelineHelper test by changing the mock config to have "TripConfirmSurvey" and the mock survey response to have the name "TripConfirmSurvey". Now it doesn't get filtered out and the test passes. Note that eventually we don't want this to be hardcoded, but it will require changes to the structure of the config and maybe a backwards-compat --- www/__mocks__/timelineHelperMocks.ts | 1 + www/__tests__/enketoHelper.test.ts | 6 +++--- www/__tests__/timelineHelper.test.ts | 2 +- www/js/diary/timelineHelper.ts | 5 ++--- www/js/survey/enketo/enketoHelper.ts | 14 +++++++------- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index d6720f88b..32502e0c6 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -229,6 +229,7 @@ export const mockFilterLocations: Array> = [ export const mockConfigEnketo = { survey_info: { 'trip-labels': 'ENKETO', + surveys: { TripConfirmSurvey: { compatibleWith: 1.2 } }, }, }; export const mockConfigModeOfStudy = { diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index c4fda7dc4..89aff8548 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -276,7 +276,7 @@ it('loads the previous response to a given survey', () => { */ it('filters the survey responses by their name and version', () => { //no response -> no filtered responses - expect(filterByNameAndVersion('TimeUseSurvey', [])).resolves.toStrictEqual([]); + expect(filterByNameAndVersion('TimeUseSurvey', [], fakeConfig)).toStrictEqual([]); const response = [ { @@ -294,7 +294,7 @@ it('filters the survey responses by their name and version', () => { ]; //one response -> that response - expect(filterByNameAndVersion('TimeUseSurvey', response)).resolves.toStrictEqual(response); + expect(filterByNameAndVersion('TimeUseSurvey', response, fakeConfig)).toStrictEqual(response); const responses = [ { @@ -336,5 +336,5 @@ it('filters the survey responses by their name and version', () => { ]; //several responses -> only the one that has a name match - expect(filterByNameAndVersion('TimeUseSurvey', responses)).resolves.toStrictEqual(response); + expect(filterByNameAndVersion('TimeUseSurvey', responses, fakeConfig)).toStrictEqual(response); }); diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 1990a5427..f7e8d2ba8 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -135,7 +135,7 @@ it('processes trip- and place- survey responses after they were just recorded', const tripSurveyResponse = { start_ts: 4, end_ts: 5, - name: 'FooBarSurvey', + name: 'TripConfirmSurvey', // for now, the name of this survey must be hardcoded (see note in UserInputButton.tsx) version: 1.2, label: '1 foobar', match_id: 'd263935e-9163-4072-9909-9d3e1edb31be', diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 5115e5588..0fd278876 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -108,9 +108,8 @@ export function updateUnprocessedInputs(labelsPromises, notesPromises, appConfig // fill in the unprocessedLabels object with the labels we just read labelResults.forEach((r, i) => { if (appConfig.survey_info?.['trip-labels'] == 'ENKETO') { - filterByNameAndVersion('TripConfirmSurvey', r).then((filtered) => { - unprocessedLabels['SURVEY'] = filtered; - }); + const filtered = filterByNameAndVersion('TripConfirmSurvey', r, appConfig); + unprocessedLabels['SURVEY'] = filtered as UserInputEntry[]; } else { unprocessedLabels[getLabelInputs()[i]] = r; } diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index c2fd3314d..73b8ab823 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -22,6 +22,8 @@ export type SurveyOptions = { }; type EnketoResponseData = { + start_ts?: number; //start timestamp (in seconds) + end_ts?: number; //end timestamp (in seconds) label: string; //display label (this value is use for displaying on the button) ts: string; //the timestamp at which the survey was filled out (in seconds) fmt_time: string; //the formatted timestamp at which the survey was filled out @@ -94,16 +96,14 @@ let _config: EnketoSurveyConfig; * @param {string} name survey name (defined in enketo survey config) * @param {EnketoResponse[]} responses An array of previously recorded responses to Enketo surveys * (presumably having been retrieved from unifiedDataLoader) + * @param {AppConfig} appConfig the dynamic config file for the app * @return {Promise} filtered survey responses */ -export function filterByNameAndVersion(name: string, responses: EnketoResponse[]) { - return getConfig().then((config) => - responses.filter( - (r) => - r.data.name === name && r.data.version >= config.survey_info.surveys[name].compatibleWith, - ), +export const filterByNameAndVersion = (name: string, responses: EnketoResponse[], appConfig) => + responses.filter( + (r) => + r.data.name === name && r.data.version >= appConfig.survey_info.surveys[name].compatibleWith, ); -} /** * resolve a label for the survey response From 4d664ad32fad41fb36584e43b1e048aa69a66624 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 6 Dec 2023 13:15:27 -0500 Subject: [PATCH 44/52] fix up timelineHelper tests - wrap the tests related to unprocessed inputs in a describe(...) - on the other mocks in this file, fallback to the original implementation - ensure both updateAllUnprocessedInputs and updateLocalUnprocessedInputs are used - tidy up the mock configs --- www/__mocks__/timelineHelperMocks.ts | 16 +- www/__tests__/timelineHelper.test.ts | 225 ++++++++++++++------------- www/js/diary/timelineHelper.ts | 4 +- 3 files changed, 128 insertions(+), 117 deletions(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index 32502e0c6..b2d999f7b 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -226,12 +226,6 @@ export const mockFilterLocations: Array> = [ }, ]; -export const mockConfigEnketo = { - survey_info: { - 'trip-labels': 'ENKETO', - surveys: { TripConfirmSurvey: { compatibleWith: 1.2 } }, - }, -}; export const mockConfigModeOfStudy = { survey_info: { 'trip-labels': 'MULTILABEL', @@ -242,10 +236,14 @@ export const mockConfigModeOfStudy = { }; export const mockConfigNoModeOfStudy = { survey_info: { - 'trip-labels': 'Other', + 'trip-labels': 'MULTILABEL', }, - intro: { - mode_studied: false, + intro: {}, +}; +export const mockConfigEnketo = { + survey_info: { + 'trip-labels': 'ENKETO', + surveys: { TripConfirmSurvey: { compatibleWith: 1.2 } }, }, }; diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index f7e8d2ba8..5e94dbd50 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -4,13 +4,14 @@ import { readAllCompositeTrips, readUnprocessedTrips, compositeTrips2TimelineMap, - updateUnprocessedInputs, keysForLabelInputs, + updateAllUnprocessedInputs, updateLocalUnprocessedInputs, + unprocessedLabels, + unprocessedNotes, } from '../js/diary/timelineHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import * as mockTLH from '../__mocks__/timelineHelperMocks'; -import { unprocessedLabels } from '../js/diary/timelineHelper'; import { GeoJSONData, GeoJSONStyledFeature, UserInputEntry } from '../js/types/diaryTypes'; mockLogger(); @@ -104,114 +105,119 @@ jest.mock('../js/survey/multilabel/confirmHelper', () => ({ getLabelInputs: jest.fn(() => ['MODE', 'PURPOSE', 'REPLACED_MODE']), })); -it('processed empty labels', async () => { - await updateUnprocessedInputs([], [], mockTLH.mockConfigNoModeOfStudy); - expect(unprocessedLabels).toEqual({}); -}); - -it('processes some mode and purpose labels after they were just recorded', async () => { - // record some labels - await window['cordova'].plugins.BEMUserCache.putMessage('manual/mode_confirm', { - start_ts: 2, - end_ts: 3, - label: 'tricycle', - }); - await window['cordova'].plugins.BEMUserCache.putMessage('manual/purpose_confirm', { - start_ts: 2, - end_ts: 3, - label: 'shopping', +describe('unprocessedLabels, unprocessedNotes', () => { + it('has no labels or notes when nothing has been recorded', async () => { + await updateAllUnprocessedInputs({ start_ts: 0, end_ts: 99 }, mockTLH.mockConfigNoModeOfStudy); + Object.values(unprocessedLabels).forEach((value) => { + expect(value).toEqual([]); + }); + expect(unprocessedNotes).toEqual([]); }); - // update unprocessed inputs and check that the new labels show up in unprocessedLabels - await updateLocalUnprocessedInputs({ start_ts: 2, end_ts: 3 }, mockTLH.mockConfigNoModeOfStudy); - expect(unprocessedLabels['MODE'].length).toEqual(1); - expect(unprocessedLabels['MODE'][0].data.label).toEqual('tricycle'); - expect(unprocessedLabels['PURPOSE'].length).toEqual(1); - expect(unprocessedLabels['PURPOSE'][0].data.label).toEqual('shopping'); -}); + it('has some mode and purpose labels after they were just recorded', async () => { + // record some labels + await window['cordova'].plugins.BEMUserCache.putMessage('manual/mode_confirm', { + start_ts: 2, + end_ts: 3, + label: 'tricycle', + }); + await window['cordova'].plugins.BEMUserCache.putMessage('manual/purpose_confirm', { + start_ts: 2, + end_ts: 3, + label: 'shopping', + }); -it('processes trip- and place- survey responses after they were just recorded', async () => { - // record two survey responses, one for trip_user_input and one for place_user_input - const tripSurveyResponse = { - start_ts: 4, - end_ts: 5, - name: 'TripConfirmSurvey', // for now, the name of this survey must be hardcoded (see note in UserInputButton.tsx) - version: 1.2, - label: '1 foobar', - match_id: 'd263935e-9163-4072-9909-9d3e1edb31be', - key: 'manual/trip_user_input', - xmlResponse: ` 2023-12-04T12:12:38.968-05:00 2023-12-04T12:12:38.970-05:00 bar uuid:75dc7b18-2a9d-4356-b66e-d63dfa7568ca `, - }; - const placeSurveyResponse = { - ...tripSurveyResponse, - start_ts: 5, - end_ts: 6, - key: 'manual/place_user_input', - }; - await window['cordova'].plugins.BEMUserCache.putMessage( - 'manual/trip_user_input', - tripSurveyResponse, - ); - await window['cordova'].plugins.BEMUserCache.putMessage( - 'manual/place_user_input', - placeSurveyResponse, - ); + // update unprocessed inputs and check that the new labels show up in unprocessedLabels + await updateLocalUnprocessedInputs({ start_ts: 2, end_ts: 3 }, mockTLH.mockConfigNoModeOfStudy); + expect(unprocessedLabels['MODE'].length).toEqual(1); + expect(unprocessedLabels['MODE'][0].data.label).toEqual('tricycle'); + expect(unprocessedLabels['PURPOSE'].length).toEqual(1); + expect(unprocessedLabels['PURPOSE'][0].data.label).toEqual('shopping'); + }); - // update unprocessed inputs and check that the trip survey response shows up in unprocessedLabels - await updateLocalUnprocessedInputs({ start_ts: 4, end_ts: 6 }, mockTLH.mockConfigEnketo); - expect(unprocessedLabels['SURVEY'][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); -}); + it('has some trip- and place- survey responses after they were just recorded', async () => { + // record two survey responses, one for trip_user_input and one for place_user_input + const tripSurveyResponse = { + start_ts: 4, + end_ts: 5, + name: 'TripConfirmSurvey', // for now, the name of this survey must be hardcoded (see note in UserInputButton.tsx) + version: 1.2, + label: '1 foobar', + match_id: 'd263935e-9163-4072-9909-9d3e1edb31be', + key: 'manual/trip_user_input', + xmlResponse: ` 2023-12-04T12:12:38.968-05:00 2023-12-04T12:12:38.970-05:00 bar uuid:75dc7b18-2a9d-4356-b66e-d63dfa7568ca `, + }; + const placeSurveyResponse = { + ...tripSurveyResponse, + start_ts: 5, + end_ts: 6, + key: 'manual/place_user_input', + }; + await window['cordova'].plugins.BEMUserCache.putMessage( + 'manual/trip_user_input', + tripSurveyResponse, + ); + await window['cordova'].plugins.BEMUserCache.putMessage( + 'manual/place_user_input', + placeSurveyResponse, + ); -it('processes some trip- and place- level additions after they were just recorded', async () => { - // record two additions, one for trip_addition_input and one for place_addition_input - const tripAdditionOne = { - start_ts: 6, - end_ts: 7, - key: 'manual/trip_addition_input', - data: { foo: 'bar' }, - }; - const tripAdditionTwo = { - ...tripAdditionOne, - data: { foo: 'baz' }, - }; - const placeAdditionOne = { - ...tripAdditionOne, - start_ts: 7, - end_ts: 8, - key: 'manual/place_addition_input', - }; - const placeAdditionTwo = { - ...placeAdditionOne, - data: { foo: 'baz' }, - }; - Promise.all([ - window['cordova'].plugins.BEMUserCache.putMessage( - 'manual/trip_addition_input', - tripAdditionOne, - ), - window['cordova'].plugins.BEMUserCache.putMessage( - 'manual/place_addition_input', - tripAdditionTwo, - ), - window['cordova'].plugins.BEMUserCache.putMessage( - 'manual/trip_addition_input', - placeAdditionOne, - ), - window['cordova'].plugins.BEMUserCache.putMessage( - 'manual/place_addition_input', - placeAdditionTwo, - ), - ]).then(() => { - // update unprocessed inputs and check that all additions show up in unprocessedNotes - updateLocalUnprocessedInputs({ start_ts: 6, end_ts: 8 }, mockTLH.mockConfigEnketo); - expect(unprocessedLabels['NOTES'].length).toEqual(4); - expect(unprocessedLabels['NOTES'][0].data).toEqual(tripAdditionOne); - expect(unprocessedLabels['NOTES'][1].data).toEqual(tripAdditionTwo); - expect(unprocessedLabels['NOTES'][2].data).toEqual(placeAdditionOne); - expect(unprocessedLabels['NOTES'][3].data).toEqual(placeAdditionTwo); + // 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); + // 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); + }); + + it('has some trip- and place- level additions after they were just recorded', async () => { + // record two additions, one for trip_addition_input and one for place_addition_input + const tripAdditionOne = { + start_ts: 6, + end_ts: 7, + key: 'manual/trip_addition_input', + data: { foo: 'bar' }, + }; + const tripAdditionTwo = { + ...tripAdditionOne, + data: { foo: 'baz' }, + }; + const placeAdditionOne = { + ...tripAdditionOne, + start_ts: 7, + end_ts: 8, + key: 'manual/place_addition_input', + }; + const placeAdditionTwo = { + ...placeAdditionOne, + data: { foo: 'baz' }, + }; + Promise.all([ + window['cordova'].plugins.BEMUserCache.putMessage( + 'manual/trip_addition_input', + tripAdditionOne, + ), + window['cordova'].plugins.BEMUserCache.putMessage( + 'manual/place_addition_input', + tripAdditionTwo, + ), + window['cordova'].plugins.BEMUserCache.putMessage( + 'manual/trip_addition_input', + placeAdditionOne, + ), + window['cordova'].plugins.BEMUserCache.putMessage( + 'manual/place_addition_input', + placeAdditionTwo, + ), + ]).then(() => { + // update unprocessed inputs and check that all additions show up in unprocessedNotes + updateAllUnprocessedInputs({ start_ts: 6, end_ts: 8 }, mockTLH.mockConfigEnketo); + expect(unprocessedLabels['NOTES'].length).toEqual(4); + expect(unprocessedLabels['NOTES'][0].data).toEqual(tripAdditionOne); + expect(unprocessedLabels['NOTES'][1].data).toEqual(tripAdditionTwo); + expect(unprocessedLabels['NOTES'][2].data).toEqual(placeAdditionOne); + expect(unprocessedLabels['NOTES'][3].data).toEqual(placeAdditionTwo); + }); }); }); @@ -221,7 +227,10 @@ jest.mock('../js/services/commHelper', () => ({ getRawEntries: jest.fn((key, startTs, endTs, valTwo) => { if (startTs === mockTLH.fakeStartTsOne) return mockTLH.mockCompData; if (startTs == mockTLH.fakeStartTsTwo) return mockTLH.mockCompDataTwo; - return {}; + // the original implementation of `getRawEntries` for all other inputs + return jest + .requireActual('../js/services/commHelper') + .getRawEntries(key, startTs, endTs, valTwo); }), })); @@ -273,6 +282,10 @@ jest.mock('../js/services/unifiedDataLoader', () => ({ if (key === 'background/filtered_location') { return Promise.resolve(mockTLH.mockFilterLocations); } + // the original implementation of `getUnifiedDataForInterval` for other keys + return jest + .requireActual('../js/services/unifiedDataLoader') + .getUnifiedDataForInterval(key, tq, combiner); }), })); diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 0fd278876..383a78bc4 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -99,9 +99,9 @@ const getUnprocessedInputQuery = (pipelineRange: TimestampRange) => ({ /** * updateUnprocessedInputs is a helper function for updateLocalUnprocessedInputs - * and updateAllUnprocessedInputs, exported for unit testing. + * and updateAllUnprocessedInputs */ -export function updateUnprocessedInputs(labelsPromises, notesPromises, appConfig) { +function updateUnprocessedInputs(labelsPromises, notesPromises, appConfig) { return Promise.all([...labelsPromises, ...notesPromises]).then((comboResults) => { const labelResults = comboResults.slice(0, labelsPromises.length); const notesResults = comboResults.slice(labelsPromises.length).flat(2); From aa8cbe4312bc10234dec02271cb34139ba9b23a2 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 6 Dec 2023 13:38:02 -0500 Subject: [PATCH 45/52] fix broken start time on draft trips This was caused by missing parentheses - instead of passing the result of toISO(), we were passing the toISO function itself! --- www/js/diary/timelineHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 383a78bc4..00d376a52 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -318,7 +318,7 @@ const points2TripProps = function (locationPoints) { inferred_labels: [], locations: locations, source: 'unprocessed', - start_fmt_time: startTime.toISO, + start_fmt_time: startTime.toISO(), start_local_dt: dateTime2localdate(startTime, startPoint.metadata.time_zone), start_ts: startPoint.data.ts, user_input: {}, From 1fb7c950b1cc95533032923f3ef24237141458ec Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Sat, 20 Jan 2024 14:47:20 -0800 Subject: [PATCH 46/52] Fix formatting error introduced by merge code --- www/__mocks__/cordovaMocks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 715711806..923f29752 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -128,7 +128,7 @@ export const mockBEMUserCache = (config?) => { ); }, 100), ); - // Used for getUnifiedDataForInterval + }, // Used for getUnifiedDataForInterval putRWDocument: (key: string, value: any) => { if (key == 'config/app_ui_config') { return new Promise((rs, rj) => From a89f35681b72f2075c088036befc8f17e0998e16 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:49:20 -0800 Subject: [PATCH 47/52] Update func param types, fix func formatting - Expanded types for several function parameters - Changed exported functions of `() => {}` format to `function () {}` --- www/__mocks__/timelineHelperMocks.ts | 43 ++++++++++-------- www/js/diary/diaryHelper.ts | 7 +-- www/js/diary/timelineHelper.ts | 68 ++++++++++++++++++++-------- www/js/survey/enketo/enketoHelper.ts | 6 +-- www/js/types/appConfigTypes.ts | 1 + www/js/types/diaryTypes.ts | 10 ++-- 6 files changed, 85 insertions(+), 50 deletions(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index b2d999f7b..0c6c6c7c6 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -7,6 +7,7 @@ import { UnprocessedTrip, } from '../js/types/diaryTypes'; import { LabelOptions } from '../js/types/labelTypes'; +import { AppConfig } from '../js/types/appConfigTypes'; const mockMetaData: MetaData = { write_ts: 1, @@ -27,32 +28,24 @@ export const mockLabelOptions: LabelOptions = { const mockConfirmedPlaceData: ConfirmedPlace = { source: 'DwellSegmentationTimeFilter', + key: null, + origin_key: null, location: { type: 'Point', coordinates: [-122.0876886, 37.3887767], }, - cleaned_place: { - $oid: '6553c3a0f27f16fbf9d1def1', - }, + cleaned_place: null, additions: [], user_input: {}, enter_fmt_time: '2015-07-22T08:14:53.881000-07:00', exit_fmt_time: '2015-07-22T08:14:53.881000-07:00', - starting_trip: { - $oid: '6553c3a1f27f16fbf9d1df15', - }, - ending_trip: { - $oid: '6553c3a1f27f16fbf9d1df15', - }, + starting_trip: null, + ending_trip: null, enter_local_dt: null, exit_local_dt: null, raw_places: [ - { - $oid: '6553c39df27f16fbf9d1dcef', - }, - { - $oid: '6553c39df27f16fbf9d1dcef', - }, + null, + null, ], enter_ts: 1437578093.881, exit_ts: 1437578093.881, @@ -226,24 +219,36 @@ export const mockFilterLocations: Array> = [ }, ]; -export const mockConfigModeOfStudy = { +export const mockConfigModeOfStudy: AppConfig = { + server: null, survey_info: { 'trip-labels': 'MULTILABEL', + surveys: null, }, intro: { mode_studied: 'sample_study', }, }; -export const mockConfigNoModeOfStudy = { +export const mockConfigNoModeOfStudy: AppConfig = { + server: null, survey_info: { 'trip-labels': 'MULTILABEL', + surveys: null, }, intro: {}, }; -export const mockConfigEnketo = { +export const mockConfigEnketo: AppConfig = { + server: null, survey_info: { 'trip-labels': 'ENKETO', - surveys: { TripConfirmSurvey: { compatibleWith: 1.2 } }, + surveys: { + TripConfirmSurvey: { + compatibleWith: 1.2, + formPath: null, + labelTemplate: null, + version: null, + }, + }, }, }; diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index 1ac36281e..9ad17693e 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -6,6 +6,7 @@ import { DateTime } from 'luxon'; import { CompositeTrip } from '../types/diaryTypes'; import { LabelOptions } from '../types/labelTypes'; import humanizeDuration from 'humanize-duration'; +import { AppConfig } from '../types/appConfigTypes'; export const modeColors = { pink: '#c32e85', // oklch(56% 0.2 350) // e-car @@ -84,7 +85,7 @@ export function getBaseModeByValue(value, labelOptions: LabelOptions) { return getBaseModeByKey(modeOption?.baseMode || 'OTHER'); } -export function getBaseModeByText(text, labelOptions: LabelOptions) { +export function getBaseModeByText(text: string, labelOptions: LabelOptions) { const modeOption = labelOptions?.MODE?.find((opt) => opt.text == text); return getBaseModeByKey(modeOption?.baseMode || 'OTHER'); } @@ -178,7 +179,7 @@ export function getDetectedModes(trip: CompositeTrip) { })); } -export function getFormattedSectionProperties(trip, ImperialConfig) { +export function getFormattedSectionProperties(trip: CompositeTrip, ImperialConfig: AppConfig) { return trip.sections?.map((s) => ({ startTime: getLocalTimeString(s.start_local_dt), duration: getFormattedTimeRange(s.start_fmt_time, s.end_fmt_time), @@ -189,7 +190,7 @@ export function getFormattedSectionProperties(trip, ImperialConfig) { })); } -export function getLocalTimeString(dt) { +export function getLocalTimeString(dt: DateTime) { if (!dt) return; const dateTime = DateTime.fromObject({ hour: dt.hour, diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 00d376a52..853fc40b7 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -10,20 +10,26 @@ import { TripTransition, TimelineEntry, GeoJSONData, - UnprocessedTrip, FilteredLocation, TimestampRange, + CompositeTrip, } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; import { LabelOptions } from '../types/labelTypes'; import { filterByNameAndVersion } from '../survey/enketo/enketoHelper'; +import { AppConfig } from '../types/appConfigTypes'; +import { Point, Feature } from 'geojson'; const cachedGeojsons: Map = new Map(); /** * @description Gets a formatted GeoJSON object for a trip, including the start and end places and the trajectory. */ -export function useGeojsonForTrip(trip, labelOptions: LabelOptions, labeledMode?: Boolean) { +export function useGeojsonForTrip( + trip: CompositeTrip, + labelOptions: LabelOptions, + labeledMode?: Boolean, +) { if (!trip) return; const gjKey = `trip-${trip._id.$oid}-${labeledMode || 'detected'}`; if (cachedGeojsons.has(gjKey)) { @@ -65,7 +71,7 @@ export function useGeojsonForTrip(trip, labelOptions: LabelOptions, labeledMode? * @param unpackPlaces whether to unpack the start and end places of each composite trip into the Map * @returns a Map() of timeline items, by id */ -export function compositeTrips2TimelineMap(ctList: any[], unpackPlaces?: boolean) { +export function compositeTrips2TimelineMap(ctList: Array, unpackPlaces?: boolean) { const timelineEntriesMap = new Map(); ctList.forEach((cTrip) => { if (unpackPlaces) { @@ -101,7 +107,11 @@ const getUnprocessedInputQuery = (pipelineRange: TimestampRange) => ({ * updateUnprocessedInputs is a helper function for updateLocalUnprocessedInputs * and updateAllUnprocessedInputs */ -function updateUnprocessedInputs(labelsPromises, notesPromises, appConfig) { +function updateUnprocessedInputs( + labelsPromises: Array>, + notesPromises: Array>, + appConfig: AppConfig, +) { return Promise.all([...labelsPromises, ...notesPromises]).then((comboResults) => { const labelResults = comboResults.slice(0, labelsPromises.length); const notesResults = comboResults.slice(labelsPromises.length).flat(2); @@ -129,7 +139,10 @@ function updateUnprocessedInputs(labelsPromises, notesPromises, appConfig) { * for which travel data has been processed through the pipeline on the server * @param appConfig the app configuration */ -export async function updateLocalUnprocessedInputs(pipelineRange: TimestampRange, appConfig) { +export async function updateLocalUnprocessedInputs( + pipelineRange: TimestampRange, + appConfig: AppConfig, +) { const BEMUserCache = window['cordova'].plugins.BEMUserCache; const tq = getUnprocessedInputQuery(pipelineRange); const labelsPromises = keysForLabelInputs(appConfig).map((key) => @@ -148,7 +161,10 @@ export async function updateLocalUnprocessedInputs(pipelineRange: TimestampRange * for which travel data has been processed through the pipeline on the server * @param appConfig the app configuration */ -export async function updateAllUnprocessedInputs(pipelineRange: TimestampRange, appConfig) { +export async function updateAllUnprocessedInputs( + pipelineRange: TimestampRange, + appConfig: AppConfig, +) { const tq = getUnprocessedInputQuery(pipelineRange); const getMethod = window['cordova'].plugins.BEMUserCache.getMessagesForInterval; const labelsPromises = keysForLabelInputs(appConfig).map((key) => @@ -160,7 +176,7 @@ export async function updateAllUnprocessedInputs(pipelineRange: TimestampRange, await updateUnprocessedInputs(labelsPromises, notesPromises, appConfig); } -export function keysForLabelInputs(appConfig) { +export function keysForLabelInputs(appConfig: AppConfig) { if (appConfig.survey_info?.['trip-labels'] == 'ENKETO') { return ['manual/trip_user_input']; } else { @@ -168,7 +184,7 @@ export function keysForLabelInputs(appConfig) { } } -function keysForNotesInputs(appConfig) { +function keysForNotesInputs(appConfig: AppConfig) { const notesKeys = []; if (appConfig.survey_info?.buttons?.['trip-notes']) notesKeys.push('manual/trip_addition_input'); if (appConfig.survey_info?.buttons?.['place-notes']) @@ -181,7 +197,10 @@ function keysForNotesInputs(appConfig) { * @param featureType a string describing the feature, e.g. "start_place" * @returns a GeoJSON feature with type "Point", the given location's coordinates and the given feature type */ -const location2GeojsonPoint = (locationPoint: any, featureType: string) => ({ +const location2GeojsonPoint = ( + locationPoint: { type: string; coordinates: number[] }, + featureType: string, +): Feature => ({ type: 'Feature', geometry: { type: 'Point', @@ -198,7 +217,11 @@ const location2GeojsonPoint = (locationPoint: any, featureType: string) => ({ * @param trajectoryColor The color to use for the whole trajectory, if any. Otherwise, a color will be lookup up for the sensed mode of each section. * @returns for each section of the trip, a GeoJSON feature with type "LineString" and an array of coordinates. */ -const locations2GeojsonTrajectory = (trip, locationList, trajectoryColor?) => { +const locations2GeojsonTrajectory = ( + trip: CompositeTrip, + locationList: Array, + trajectoryColor?: string, +) => { let sectionsPoints; if (!trip.sections) { // this is a unimodal trip so we put all the locations in one section @@ -237,7 +260,7 @@ const unpackServerData = (obj: ServerData) => ({ origin_key: obj.metadata.origin_key || obj.metadata.key, }); -export const readAllCompositeTrips = function (startTs: number, endTs: number) { +export function readAllCompositeTrips(startTs: number, endTs: number) { const readPromises = [getRawEntries(['analysis/composite_trip'], startTs, endTs, 'data.end_ts')]; return Promise.all(readPromises) .then(([ctList]: [ServerResponse]) => { @@ -256,7 +279,7 @@ export const readAllCompositeTrips = function (startTs: number, endTs: number) { displayError(err, 'while reading confirmed trips'); return []; }); -}; +} const dateTime2localdate = function (currtime: DateTime, tz: string) { return { timezone: tz, @@ -273,7 +296,7 @@ const dateTime2localdate = function (currtime: DateTime, tz: string) { }; }; -const points2TripProps = function (locationPoints) { +const points2TripProps = function (locationPoints: Array>) { const startPoint = locationPoints[0]; const endPoint = locationPoints[locationPoints.length - 1]; const tripAndSectionId = `unprocessed_${startPoint.data.ts}_${endPoint.data.ts}`; @@ -324,12 +347,13 @@ const points2TripProps = function (locationPoints) { user_input: {}, }; }; -const tsEntrySort = function (e1, e2) { + +const tsEntrySort = function (e1: ServerData, e2: ServerData) { // compare timestamps return e1.data.ts - e2.data.ts; }; -const transitionTrip2TripObj = function (trip) { +const transitionTrip2TripObj = function (trip: Array) { const tripStartTransition = trip[0]; const tripEndTransition = trip[1]; const tq = { @@ -402,7 +426,7 @@ const transitionTrip2TripObj = function (trip) { }, ); }; -const isStartingTransition = function (transWrapper) { +const isStartingTransition = function (transWrapper: ServerData) { if ( transWrapper.data.transition == 'local.transition.exited_geofence' || transWrapper.data.transition == 'T_EXITED_GEOFENCE' || @@ -413,7 +437,7 @@ const isStartingTransition = function (transWrapper) { return false; }; -const isEndingTransition = function (transWrapper) { +const isEndingTransition = function (transWrapper: ServerData) { // Logger.log("isEndingTransition: transWrapper.data.transition = "+transWrapper.data.transition); if ( transWrapper.data.transition == 'T_TRIP_ENDED' || @@ -511,7 +535,11 @@ const linkTrips = function (trip1, trip2) { trip2.enter_ts = trip1.exit_ts; }; -export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) { +export function readUnprocessedTrips( + startTs: number, + endTs: number, + lastProcessedTrip: CompositeTrip, +) { const tq = { key: 'write_ts', startTs, endTs }; logDebug( 'about to query for unprocessed trips from ' + @@ -533,7 +561,7 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) logDebug(JSON.stringify(trip, null, 2)); }); const tripFillPromises = tripsList.map(transitionTrip2TripObj); - return Promise.all(tripFillPromises).then(function (raw_trip_gj_list: UnprocessedTrip[]) { + return Promise.all(tripFillPromises).then(function (raw_trip_gj_list) { // Now we need to link up the trips. linking unprocessed trips // to one another is fairly simple, but we need to link the // first unprocessed trip to the last processed trip. @@ -567,4 +595,4 @@ export const readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) }); } }); -}; +} diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 73b8ab823..1cd88dd01 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -99,12 +99,12 @@ let _config: EnketoSurveyConfig; * @param {AppConfig} appConfig the dynamic config file for the app * @return {Promise} filtered survey responses */ -export const filterByNameAndVersion = (name: string, responses: EnketoResponse[], appConfig) => - responses.filter( +export function filterByNameAndVersion(name: string, responses: EnketoResponse[], appConfig) { + return responses.filter( (r) => r.data.name === name && r.data.version >= appConfig.survey_info.surveys[name].compatibleWith, ); - +} /** * resolve a label for the survey response * @param {string} name survey name diff --git a/www/js/types/appConfigTypes.ts b/www/js/types/appConfigTypes.ts index f99000e90..27e03a458 100644 --- a/www/js/types/appConfigTypes.ts +++ b/www/js/types/appConfigTypes.ts @@ -6,6 +6,7 @@ export type AppConfig = { survey_info: { 'trip-labels': 'MULTILABEL' | 'ENKETO'; surveys: EnketoSurveyConfig; + buttons?: any; }; reminderSchemes?: ReminderSchemeConfig; [k: string]: any; // TODO fill in all the other fields diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 92cc28d74..87c4b1653 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -38,7 +38,7 @@ export type ConfirmedPlace = { export type TripTransition = { currstate: string; - transition: string; + transition: string | number; ts: number; }; @@ -60,7 +60,7 @@ export type UnprocessedTrip = { end_fmt_time: string; /* While the end_loc & start_loc objects are similar to GeoJSON's `Point` object, they lack the additional GeoJSONObject methods, so `Point` cannot be used here. */ - end_loc: { type: string; coordinates: any[] }; + end_loc: { type: string; coordinates: number[] }; end_local_dt: LocalDt; expectation: any; // TODO "{to_label: boolean}" inferred_labels: any[]; // TODO @@ -70,7 +70,7 @@ export type UnprocessedTrip = { source: string; start_local_dt: LocalDt; start_ts: number; - start_loc: { type: string; coordinates: any[] }; + start_loc: { type: string; coordinates: number[] }; starting_trip?: any; user_input: UserInput; }; @@ -88,7 +88,7 @@ export type CompositeTrip = { duration: number; end_confirmed_place: ServerData; end_fmt_time: string; - end_loc: Geometry; + end_loc: { type: string; coordinates: number[] }; end_local_dt: LocalDt; end_place: ObjectId; end_ts: number; @@ -105,7 +105,7 @@ export type CompositeTrip = { source: string; start_confirmed_place: ServerData; start_fmt_time: string; - start_loc: Geometry; + start_loc: { type: string; coordinates: number[] }; start_local_dt: LocalDt; start_place: ObjectId; start_ts: number; From d54ce29082d36171fdc0f91f07242d5338edddc6 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:56:51 -0800 Subject: [PATCH 48/52] Renamed `ServerData` to BEMData This better reflects the origin of this type: both BEMServerComm and BEMUserCache return data of this structure. --- www/__mocks__/timelineHelperMocks.ts | 6 +++--- www/__tests__/unifiedDataLoader.test.ts | 6 +++--- www/js/diary/timelineHelper.ts | 18 +++++++++--------- www/js/services/unifiedDataLoader.ts | 4 ++-- www/js/types/diaryTypes.ts | 8 ++++---- www/js/types/fileShareTypes.ts | 4 ++-- www/js/types/serverData.ts | 4 ++-- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index 0c6c6c7c6..78a217ead 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -1,4 +1,4 @@ -import { MetaData, ServerData, ServerResponse } from '../js/types/serverData'; +import { MetaData, BEMData, ServerResponse } from '../js/types/serverData'; import { CompositeTrip, ConfirmedPlace, @@ -168,7 +168,7 @@ export const mockCompDataTwo = { phone_data: [mockCompData.phone_data[0], newPhoneData], }; -export const mockTransitions: Array> = [ +export const mockTransitions: Array> = [ { data: { // mock of a startTransition @@ -208,7 +208,7 @@ mockFilterLocationTwo.ts = 900; mockFilterLocationTwo.longitude = 200; mockFilterLocationTwo.longitude = -200; -export const mockFilterLocations: Array> = [ +export const mockFilterLocations: Array> = [ { data: mockFilterLocation, metadata: mockMetaData, diff --git a/www/__tests__/unifiedDataLoader.test.ts b/www/__tests__/unifiedDataLoader.test.ts index 57b1023da..b99f2a69e 100644 --- a/www/__tests__/unifiedDataLoader.test.ts +++ b/www/__tests__/unifiedDataLoader.test.ts @@ -1,10 +1,10 @@ import { mockLogger } from '../__mocks__/globalMocks'; import { removeDup, combinedPromises } from '../js/services/unifiedDataLoader'; -import { ServerData } from '../js/types/serverData'; +import { BEMData } from '../js/types/serverData'; mockLogger(); -const testOne: ServerData = { +const testOne: BEMData = { data: '', metadata: { key: '', @@ -42,7 +42,7 @@ describe('removeDup can', () => { }); // combinedPromises tests -const promiseGenerator = (values: Array>) => { +const promiseGenerator = (values: Array>) => { return Promise.resolve(values); }; const badPromiseGenerator = (input: string) => { diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 853fc40b7..1d583cd73 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -2,7 +2,7 @@ import { displayError, logDebug } from '../plugin/logger'; import { getBaseModeByKey, getBaseModeByValue } from './diaryHelper'; import { getUnifiedDataForInterval } from '../services/unifiedDataLoader'; import { getRawEntries } from '../services/commHelper'; -import { ServerResponse, ServerData } from '../types/serverData'; +import { ServerResponse, BEMData } from '../types/serverData'; import L from 'leaflet'; import { DateTime } from 'luxon'; import { @@ -253,7 +253,7 @@ const locations2GeojsonTrajectory = ( // DB entries retrieved from the server have '_id', 'metadata', and 'data' fields. // This function returns a shallow copy of the obj, which flattens the // 'data' field into the top level, while also including '_id' and 'metadata.key' -const unpackServerData = (obj: ServerData) => ({ +const unpackServerData = (obj: BEMData) => ({ ...obj.data, _id: obj._id, key: obj.metadata.key, @@ -296,7 +296,7 @@ const dateTime2localdate = function (currtime: DateTime, tz: string) { }; }; -const points2TripProps = function (locationPoints: Array>) { +const points2TripProps = function (locationPoints: Array>) { const startPoint = locationPoints[0]; const endPoint = locationPoints[locationPoints.length - 1]; const tripAndSectionId = `unprocessed_${startPoint.data.ts}_${endPoint.data.ts}`; @@ -348,7 +348,7 @@ const points2TripProps = function (locationPoints: Array, e2: ServerData) { +const tsEntrySort = function (e1: BEMData, e2: BEMData) { // compare timestamps return e1.data.ts - e2.data.ts; }; @@ -369,7 +369,7 @@ const transitionTrip2TripObj = function (trip: Array) { ); const getSensorData = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval; return getUnifiedDataForInterval('background/filtered_location', tq, getSensorData).then( - function (locationList: Array>) { + function (locationList: Array>) { if (locationList.length == 0) { return undefined; } @@ -426,7 +426,7 @@ const transitionTrip2TripObj = function (trip: Array) { }, ); }; -const isStartingTransition = function (transWrapper: ServerData) { +const isStartingTransition = function (transWrapper: BEMData) { if ( transWrapper.data.transition == 'local.transition.exited_geofence' || transWrapper.data.transition == 'T_EXITED_GEOFENCE' || @@ -437,7 +437,7 @@ const isStartingTransition = function (transWrapper: ServerData) return false; }; -const isEndingTransition = function (transWrapper: ServerData) { +const isEndingTransition = function (transWrapper: BEMData) { // Logger.log("isEndingTransition: transWrapper.data.transition = "+transWrapper.data.transition); if ( transWrapper.data.transition == 'T_TRIP_ENDED' || @@ -465,7 +465,7 @@ const isEndingTransition = function (transWrapper: ServerData) { * * Let's abstract this out into our own minor state machine. */ -const transitions2Trips = function (transitionList: Array>) { +const transitions2Trips = function (transitionList: Array>) { let inTrip = false; const tripList = []; let currStartTransitionIndex = -1; @@ -548,7 +548,7 @@ export function readUnprocessedTrips( ); const getMessageMethod = window['cordova'].plugins.BEMUserCache.getMessagesForInterval; return getUnifiedDataForInterval('statemachine/transition', tq, getMessageMethod).then(function ( - transitionList: Array>, + transitionList: Array>, ) { if (transitionList.length == 0) { logDebug('No unprocessed trips. yay!'); diff --git a/www/js/services/unifiedDataLoader.ts b/www/js/services/unifiedDataLoader.ts index 00f6e3027..5c06d33a5 100644 --- a/www/js/services/unifiedDataLoader.ts +++ b/www/js/services/unifiedDataLoader.ts @@ -1,12 +1,12 @@ import { getRawEntries } from './commHelper'; -import { ServerResponse, ServerData, TimeQuery } from '../types/serverData'; +import { ServerResponse, BEMData, TimeQuery } from '../types/serverData'; /** * removeDup is a helper function for combinedPromises * @param list An array of values from a BEMUserCache promise * @returns an array with duplicate values removed */ -export const removeDup = function (list: Array>) { +export const removeDup = function (list: Array>) { return list.filter(function (value, i, array) { const firstIndexOfValue = array.findIndex(function (element) { return element.metadata.write_ts == value.metadata.write_ts; diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 87c4b1653..bfaee2a14 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -3,7 +3,7 @@ As much as possible, these types parallel the types used in the server code. */ import { BaseModeKey, MotionTypeKey } from '../diary/diaryHelper'; -import { ServerData, LocalDt } from './serverData'; +import { BEMData, LocalDt } from './serverData'; import { FeatureCollection, Feature, Geometry } from 'geojson'; type ObjectId = { $oid: string }; @@ -86,7 +86,7 @@ export type CompositeTrip = { confirmed_trip: ObjectId; distance: number; duration: number; - end_confirmed_place: ServerData; + end_confirmed_place: BEMData; end_fmt_time: string; end_loc: { type: string; coordinates: number[] }; end_local_dt: LocalDt; @@ -103,7 +103,7 @@ export type CompositeTrip = { raw_trip: ObjectId; sections: any[]; // TODO source: string; - start_confirmed_place: ServerData; + start_confirmed_place: BEMData; start_fmt_time: string; start_loc: { type: string; coordinates: number[] }; start_local_dt: LocalDt; @@ -154,7 +154,7 @@ type UserInputData = { match_id?: string; }; -export type UserInputEntry = ServerData; +export type UserInputEntry = BEMData; export type Location = { speed: number; diff --git a/www/js/types/fileShareTypes.ts b/www/js/types/fileShareTypes.ts index 03b41a161..ee8d9a14e 100644 --- a/www/js/types/fileShareTypes.ts +++ b/www/js/types/fileShareTypes.ts @@ -1,6 +1,6 @@ -import { ServerData } from './serverData'; +import { BEMData } from './serverData'; -export type TimeStampData = ServerData; +export type TimeStampData = BEMData; export type RawTimelineData = { name: string; diff --git a/www/js/types/serverData.ts b/www/js/types/serverData.ts index 5c7aa38bf..b68078552 100644 --- a/www/js/types/serverData.ts +++ b/www/js/types/serverData.ts @@ -1,8 +1,8 @@ export type ServerResponse = { - phone_data: Array>; + phone_data: Array>; }; -export type ServerData = { +export type BEMData = { data: Type; metadata: MetaData; key?: string; From cdb71383b96d69d07a7ecd768c293daef669d60b Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:24:26 -0800 Subject: [PATCH 49/52] Imporved test in `timelineHelper.test.ts` Test now compares the unpacked object map to original object. --- www/__tests__/timelineHelper.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index 5e94dbd50..b93015b13 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -91,6 +91,9 @@ describe('compositeTrips2TimelineMap', () => { it('Works with a list of len >= 1, with flag', () => { testValue = compositeTrips2TimelineMap(tripListTwo, true); expect(testValue.size).toBe(6); + for (const [key,value] of Object.entries(testValue)) { + expect(value).toBe(tripListTwo[0][key] || tripListTwo[1][key]) + } }); }); From 95d6255f5bcd2d7278074571bb3b877fa19633d4 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:29:01 -0800 Subject: [PATCH 50/52] Ran prettier from `www/` Was running on `js/`, but not mocks; fixed formatting in mocks and tests! --- www/__mocks__/cordovaMocks.ts | 2 +- www/__mocks__/timelineHelperMocks.ts | 9 +++------ www/__tests__/timelineHelper.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 923f29752..60ea4e0c1 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -128,7 +128,7 @@ export const mockBEMUserCache = (config?) => { ); }, 100), ); - }, // Used for getUnifiedDataForInterval + }, // Used for getUnifiedDataForInterval putRWDocument: (key: string, value: any) => { if (key == 'config/app_ui_config') { return new Promise((rs, rj) => diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index 78a217ead..b18c2854a 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -43,10 +43,7 @@ const mockConfirmedPlaceData: ConfirmedPlace = { ending_trip: null, enter_local_dt: null, exit_local_dt: null, - raw_places: [ - null, - null, - ], + raw_places: [null, null], enter_ts: 1437578093.881, exit_ts: 1437578093.881, }; @@ -241,8 +238,8 @@ export const mockConfigEnketo: AppConfig = { server: null, survey_info: { 'trip-labels': 'ENKETO', - surveys: { - TripConfirmSurvey: { + surveys: { + TripConfirmSurvey: { compatibleWith: 1.2, formPath: null, labelTemplate: null, diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index b93015b13..83b25be01 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -91,8 +91,8 @@ describe('compositeTrips2TimelineMap', () => { it('Works with a list of len >= 1, with flag', () => { testValue = compositeTrips2TimelineMap(tripListTwo, true); expect(testValue.size).toBe(6); - for (const [key,value] of Object.entries(testValue)) { - expect(value).toBe(tripListTwo[0][key] || tripListTwo[1][key]) + for (const [key, value] of Object.entries(testValue)) { + expect(value).toBe(tripListTwo[0][key] || tripListTwo[1][key]); } }); }); From 9f73f74acdeea4a2ad478d6e9e94ef525cb3d890 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:48:37 -0800 Subject: [PATCH 51/52] Updated diaryTypes to use GeoJSON `Point` --- www/__mocks__/timelineHelperMocks.ts | 4 ++-- www/js/diary/timelineHelper.ts | 2 +- www/js/types/diaryTypes.ts | 12 +++++------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/www/__mocks__/timelineHelperMocks.ts b/www/__mocks__/timelineHelperMocks.ts index b18c2854a..014e892cb 100644 --- a/www/__mocks__/timelineHelperMocks.ts +++ b/www/__mocks__/timelineHelperMocks.ts @@ -60,7 +60,7 @@ export const mockUnprocessedTrip: UnprocessedTrip = { distance: 1.0, duration: 3.0, end_fmt_time: '', - end_loc: { type: '', coordinates: [] }, + end_loc: { type: 'Point', coordinates: [] }, end_local_dt: null, expectation: null, inferred_labels: [], @@ -70,7 +70,7 @@ export const mockUnprocessedTrip: UnprocessedTrip = { source: '', start_local_dt: null, start_ts: 0.1, - start_loc: { type: '', coordinates: [] }, + start_loc: { type: 'Point', coordinates: [] }, starting_trip: null, user_input: null, }; diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 1d583cd73..1db03cd2d 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -198,7 +198,7 @@ function keysForNotesInputs(appConfig: AppConfig) { * @returns a GeoJSON feature with type "Point", the given location's coordinates and the given feature type */ const location2GeojsonPoint = ( - locationPoint: { type: string; coordinates: number[] }, + locationPoint: Point, featureType: string, ): Feature => ({ type: 'Feature', diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index bfaee2a14..64cd87e3b 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -4,7 +4,7 @@ import { BaseModeKey, MotionTypeKey } from '../diary/diaryHelper'; import { BEMData, LocalDt } from './serverData'; -import { FeatureCollection, Feature, Geometry } from 'geojson'; +import { FeatureCollection, Feature, Geometry, Point } from 'geojson'; type ObjectId = { $oid: string }; @@ -58,9 +58,7 @@ export type UnprocessedTrip = { distance: number; duration: number; end_fmt_time: string; - /* While the end_loc & start_loc objects are similar to GeoJSON's `Point` object, - they lack the additional GeoJSONObject methods, so `Point` cannot be used here. */ - end_loc: { type: string; coordinates: number[] }; + end_loc: Point; end_local_dt: LocalDt; expectation: any; // TODO "{to_label: boolean}" inferred_labels: any[]; // TODO @@ -70,7 +68,7 @@ export type UnprocessedTrip = { source: string; start_local_dt: LocalDt; start_ts: number; - start_loc: { type: string; coordinates: number[] }; + start_loc: Point; starting_trip?: any; user_input: UserInput; }; @@ -88,7 +86,7 @@ export type CompositeTrip = { duration: number; end_confirmed_place: BEMData; end_fmt_time: string; - end_loc: { type: string; coordinates: number[] }; + end_loc: Point; end_local_dt: LocalDt; end_place: ObjectId; end_ts: number; @@ -105,7 +103,7 @@ export type CompositeTrip = { source: string; start_confirmed_place: BEMData; start_fmt_time: string; - start_loc: { type: string; coordinates: number[] }; + start_loc: Point; start_local_dt: LocalDt; start_place: ObjectId; start_ts: number; From f54257b520ef68d4cd7b4b03501d42c1d373d687 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:49:45 -0800 Subject: [PATCH 52/52] Prettier after `Point` change --- www/js/diary/timelineHelper.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 1db03cd2d..5805fbe07 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -197,10 +197,7 @@ function keysForNotesInputs(appConfig: AppConfig) { * @param featureType a string describing the feature, e.g. "start_place" * @returns a GeoJSON feature with type "Point", the given location's coordinates and the given feature type */ -const location2GeojsonPoint = ( - locationPoint: Point, - featureType: string, -): Feature => ({ +const location2GeojsonPoint = (locationPoint: Point, featureType: string): Feature => ({ type: 'Feature', geometry: { type: 'Point',