From e94532596f9797035ebfc132808966fe11305861 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 5 Oct 2023 10:25:30 -0700 Subject: [PATCH 01/21] Rewrite input-matcher to typescript (not done) --- www/js/survey/input-matcher.ts | 243 +++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 www/js/survey/input-matcher.ts diff --git a/www/js/survey/input-matcher.ts b/www/js/survey/input-matcher.ts new file mode 100644 index 000000000..dcc31afd2 --- /dev/null +++ b/www/js/survey/input-matcher.ts @@ -0,0 +1,243 @@ +import { logDebug, displayErrorMsg } from "../plugin/logger" +import moment from "moment"; + +type LocalDt = { + minute: number, + hour: number, + second: number, + day: number, + weekday: number, + month: number, + year: number, + timezone: string +} + +type UserInputForTrip = { + data: { + end_ts: number, + start_ts: number + label: string, + start_local_dt?: LocalDt + end_local_dt?: LocalDt + }, + metadata: { + time_zone: string, + plugin: string, + write_ts: number, + platform: string, + read_ts: number, + key: string + }, + key?: string, +} + +type Trip = { + end_ts: number, + start_ts: number +} + +type TlEntry = { + key: string, + origin_key: string, + start_ts: number, + end_ts: number, + enter_ts: number, + exit_ts: number, + duration: number, + getNextEntry: any +} + +const EPOCH_MAXIMUM = 2**31 - 1; + +const fmtTs = (ts_in_secs: number, tz: string): string => moment(ts_in_secs * 1000).tz(tz).format(); + +const printUserInput = (ui: UserInputForTrip): string => `${fmtTs(ui.data.start_ts, ui.metadata.time_zone)} (${ui.data.start_ts}) -> +${fmtTs(ui.data.end_ts, ui.metadata.time_zone)} (${ui.data.end_ts}) ${ui.data.label} logged at ${ui.metadata.write_ts}`; + +const validUserInputForDraftTrip = (trip: Trip, userInput: UserInputForTrip, logsEnabled: boolean): boolean => { + if(logsEnabled) { + logDebug(`Draft trip: + comparing user = ${fmtTs(userInput.data.start_ts, userInput.metadata.time_zone)} + -> ${fmtTs(userInput.data.end_ts, userInput.metadata.time_zone)} + trip = ${fmtTs(trip.start_ts, userInput.metadata.time_zone)} + -> ${fmtTs(trip.end_ts, userInput.metadata.time_zone)} + checks are (${userInput.data.start_ts >= trip.start_ts} + && ${userInput.data.start_ts < trip.end_ts} + || ${-(userInput.data.start_ts - trip.start_ts) <= 15 * 60}) + && ${userInput.data.end_ts <= trip.end_ts} + `); + } + + return (userInput.data.start_ts >= trip.start_ts && userInput.data.start_ts < trip.end_ts + || -(userInput.data.start_ts - trip.start_ts) <= 15 * 60) && userInput.data.end_ts <= trip.end_ts; +} + +const validUserInputForTimelineEntry = (tlEntry: TlEntry, userInput: UserInputForTrip, logsEnabled: boolean): boolean => { + if (!tlEntry.origin_key) return false; + if (tlEntry.origin_key.includes('UNPROCESSED')) return validUserInputForDraftTrip(tlEntry, userInput, logsEnabled); + + /* Place-level inputs always have a key starting with 'manual/place', and + trip-level inputs never have a key starting with 'manual/place' + So if these don't match, we can immediately return false */ + const entryIsPlace = tlEntry.origin_key === 'analysis/confirmed_place'; + const isPlaceInput = (userInput.key || userInput.metadata.key).startsWith('manual/place'); + + if (entryIsPlace !== isPlaceInput) return false; + + let entryStart = tlEntry.start_ts || tlEntry.enter_ts; + let entryEnd = tlEntry.end_ts || tlEntry.exit_ts; + + if (!entryStart && entryEnd) { + /* if a place has no enter time, this is the first start_place of the first composite trip object + so we will set the start time to the start of the day of the end time for the purpose of comparison */ + entryStart = moment.unix(entryEnd).startOf('day').unix(); + } + + if (!entryEnd) { + /* if a place has no exit time, the user hasn't left there yet + so we will set the end time as high as possible for the purpose of comparison */ + entryEnd = EPOCH_MAXIMUM; + } + + if (logsEnabled) { + logDebug(`Cleaned trip: + comparing user = ${fmtTs(userInput.data.start_ts, userInput.metadata.time_zone)} + -> ${fmtTs(userInput.data.end_ts, userInput.metadata.time_zone)} + trip = ${fmtTs(entryStart, userInput.metadata.time_zone)} + -> ${fmtTs(entryStart, userInput.metadata.time_zone)} + start checks are ${userInput.data.start_ts >= entryStart} + && ${userInput.data.start_ts < entryEnd} + end checks are ${userInput.data.end_ts <= entryEnd} + || ${userInput.data.end_ts - entryEnd <= 15 * 60}) + `); + } + + /* For this input to match, it must begin after the start of the timelineEntry (inclusive) + but before the end of the timelineEntry (exclusive) */ + const startChecks = userInput.data.start_ts >= entryStart && userInput.data.start_ts < entryEnd; + + /* A matching user input must also finish before the end of the timelineEntry, + or within 15 minutes. */ + let endChecks = (userInput.data.end_ts <= entryEnd || (userInput.data.end_ts - entryEnd) <= 15 * 60); + + if (startChecks && !endChecks) { + const nextEntryObj = tlEntry.getNextEntry(); + if (nextEntryObj) { + const nextEntryEnd = nextEntryObj.end_ts || nextEntryObj.exit_ts; + if (!nextEntryEnd) { // the last place will not have an exit_ts + endChecks = true; // so we will just skip the end check + } else { + endChecks = userInput.data.end_ts <= nextEntryEnd; + logDebug(`Second level of end checks when the next trip is defined(${userInput.data.end_ts} <= ${nextEntryEnd}) ${endChecks}`); + } + } else { + // next trip is not defined, last trip + endChecks = (userInput.data.end_local_dt.day == userInput.data.start_local_dt.day) + logDebug("Second level of end checks for the last trip of the day"); + logDebug(`compare ${userInput.data.end_local_dt.day} with ${userInput.data.start_local_dt.day} ${endChecks}`); + } + if (endChecks) { + // If we have flipped the values, check to see that there is sufficient overlap + const overlapDuration = Math.min(userInput.data.end_ts, entryEnd) - Math.max(userInput.data.start_ts, entryStart) + logDebug(`Flipped endCheck, overlap(${overlapDuration})/trip(${tlEntry.duration} (${overlapDuration} / ${tlEntry.duration})`); + endChecks = (overlapDuration/tlEntry.duration) > 0.5; + } + } + + return startChecks && endChecks; +} + +// parallels get_not_deleted_candidates() in trip_queries.py +const getNotDeletedCandidates = (candidates:any ):any => { + console.log('getNotDeletedCandidates called with ' + candidates.length + ' candidates'); + + // We want to retain all ACTIVE entries that have not been DELETED + const allActiveList = candidates.filter(c => !c.data.status || c.data.status == 'ACTIVE'); + const allDeletedIds = candidates.filter(c => c.data.status && c.data.status == 'DELETED').map(c => c.data['match_id']); + const notDeletedActive = allActiveList.filter(c => !allDeletedIds.includes(c.data['match_id'])); + + console.log(`Found ${allActiveList.length} active entries, ${allDeletedIds.length} deleted entries -> + ${notDeletedActive.length} non deleted active entries`); + + return notDeletedActive; +} + +export const getUserInputForTrip = (trip: TlEntry, nextTrip, userInputList: UserInputForTrip[]): undefined | UserInputForTrip => { + const logsEnabled = userInputList.length < 20; + + if (userInputList === undefined) { + logDebug("In getUserInputForTrip, no user input, returning undefined"); + return undefined; + } + + if (logsEnabled) console.log(`Input list = ${userInputList.map(printUserInput)}`); + + // undefined !== true, so this covers the label view case as well + const potentialCandidates = userInputList.filter((ui) => validUserInputForTimelineEntry(trip, ui, logsEnabled)); + + if (potentialCandidates.length === 0) { + if (logsEnabled) logDebug("In getUserInputForTripStartEnd, no potential candidates, returning []"); + return undefined; + } + + if (potentialCandidates.length === 1) { + logDebug(`In getUserInputForTripStartEnd, one potential candidate, returning ${printUserInput(potentialCandidates[0])}`); + return potentialCandidates[0]; + } + + logDebug(`potentialCandidates are ${potentialCandidates.map(printUserInput)}`); + + const sortedPC = potentialCandidates.sort((pc1, pc2) => pc2.metadata.write_ts - pc1.metadata.write_ts); + const mostRecentEntry = sortedPC[0]; + logDebug("Returning mostRecentEntry "+printUserInput(mostRecentEntry)); + + return mostRecentEntry; +} + +// return array of matching additions for a trip or place +export const getAdditionsForTimelineEntry = (entry, additionsList) => { + const logsEnabled = additionsList.length < 20; + + if (additionsList === undefined) { + logDebug("In getAdditionsForTimelineEntry, no addition input, returning []"); + return []; + } + + // get additions that have not been deleted and filter out additions that do not start within the bounds of the timeline entry + const notDeleted = getNotDeletedCandidates(additionsList); + const matchingAdditions = notDeleted.filter((ui) => validUserInputForTimelineEntry(entry, ui, logsEnabled)); + + if (logsEnabled) console.log(`Matching Addition list ${matchingAdditions.map(printUserInput)}`); + + return matchingAdditions; +} + +export const getUniqueEntries = (combinedList) => { + /* we should not get any non-ACTIVE entries here + since we have run filtering algorithms on both the phone and the server */ + const allDeleted = combinedList.filter(c => c.data.status && c.data.status == 'DELETED'); + + if (allDeleted.length > 0) { + displayErrorMsg("Found "+allDeletedEntries.length +" non-ACTIVE addition entries while trying to dedup entries", allDeletedEntries); + } + + const uniqueMap = new Map(); + combinedList.forEach((e) => { + const existingVal = uniqueMap.get(e.data.match_id); + /* if the existing entry and the input entry don't match and they are both active, we have an error + let's notify the user for now */ + if (existingVal) { + if ((existingVal.data.start_ts != e.data.start_ts) || + (existingVal.data.end_ts != e.data.end_ts) || + (existingVal.data.write_ts != e.data.write_ts)) { + displayErrorMsg(`Found two ACTIVE entries with the same match ID but different timestamps ${existingVal.data.match_id}` + , `${JSON.stringify(existingVal)} vs ${JSON.stringify(e)}`); + } else { + console.log(`Found two entries with match_id ${existingVal.data.match_id} but they are identical`); + } + } else { + uniqueMap.set(e.data.match_id, e); + } + }); + return Array.from(uniqueMap.values()); +} From 9d83cc18cfce05b03b893131a8f610847a34f951 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 12 Oct 2023 14:28:43 -0700 Subject: [PATCH 02/21] delete old angular service --- www/js/survey/input-matcher.js | 213 --------------------------------- 1 file changed, 213 deletions(-) delete mode 100644 www/js/survey/input-matcher.js diff --git a/www/js/survey/input-matcher.js b/www/js/survey/input-matcher.js deleted file mode 100644 index 2e3d5b908..000000000 --- a/www/js/survey/input-matcher.js +++ /dev/null @@ -1,213 +0,0 @@ -'use strict'; - -import angular from 'angular'; - -angular.module('emission.survey.inputmatcher', ['emission.plugin.logger']) -.factory('InputMatcher', function(Logger){ - var im = {}; - - const EPOCH_MAXIMUM = 2**31 - 1; - const fmtTs = function(ts_in_secs, tz) { - return moment(ts_in_secs * 1000).tz(tz).format(); - } - - var printUserInput = function(ui) { - return fmtTs(ui.data.start_ts, ui.metadata.time_zone) + "("+ui.data.start_ts + ") -> "+ - fmtTs(ui.data.end_ts, ui.metadata.time_zone) + "("+ui.data.end_ts + ")"+ - " " + ui.data.label + " logged at "+ ui.metadata.write_ts; - } - - im.validUserInputForDraftTrip = function(trip, userInput, logsEnabled) { - if (logsEnabled) { - Logger.log(`Draft trip: - comparing user = ${fmtTs(userInput.data.start_ts, userInput.metadata.time_zone)} - -> ${fmtTs(userInput.data.end_ts, userInput.metadata.time_zone)} - trip = ${fmtTs(trip.start_ts, userInput.metadata.time_zone)} - -> ${fmtTs(trip.end_ts, userInput.metadata.time_zone)} - checks are (${userInput.data.start_ts >= trip.start_ts} - && ${userInput.data.start_ts < trip.end_ts} - || ${-(userInput.data.start_ts - trip.start_ts) <= 15 * 60}) - && ${userInput.data.end_ts <= trip.end_ts} - `); - } - return (userInput.data.start_ts >= trip.start_ts - && userInput.data.start_ts < trip.end_ts - || -(userInput.data.start_ts - trip.start_ts) <= 15 * 60) - && userInput.data.end_ts <= trip.end_ts; - } - - im.validUserInputForTimelineEntry = function(tlEntry, userInput, logsEnabled) { - if (!tlEntry.origin_key) return false; - if (tlEntry.origin_key.includes('UNPROCESSED') == true) - return im.validUserInputForDraftTrip(tlEntry, userInput, logsEnabled); - - /* Place-level inputs always have a key starting with 'manual/place', and - trip-level inputs never have a key starting with 'manual/place' - So if these don't match, we can immediately return false */ - const entryIsPlace = tlEntry.origin_key == 'analysis/confirmed_place'; - const isPlaceInput = (userInput.key || userInput.metadata.key).startsWith('manual/place'); - if (entryIsPlace != isPlaceInput) - return false; - - let entryStart = tlEntry.start_ts || tlEntry.enter_ts; - let entryEnd = tlEntry.end_ts || tlEntry.exit_ts; - if (!entryStart && entryEnd) { - // if a place has no enter time, this is the first start_place of the first composite trip object - // so we will set the start time to the start of the day of the end time for the purpose of comparison - entryStart = moment.unix(entryEnd).startOf('day').unix(); - } - if (!entryEnd) { - // if a place has no exit time, the user hasn't left there yet - // so we will set the end time as high as possible for the purpose of comparison - entryEnd = EPOCH_MAXIMUM; - } - - if (logsEnabled) { - Logger.log(`Cleaned trip: - comparing user = ${fmtTs(userInput.data.start_ts, userInput.metadata.time_zone)} - -> ${fmtTs(userInput.data.end_ts, userInput.metadata.time_zone)} - trip = ${fmtTs(entryStart, userInput.metadata.time_zone)} - -> ${fmtTs(entryStart, userInput.metadata.time_zone)} - start checks are ${userInput.data.start_ts >= entryStart} - && ${userInput.data.start_ts < entryEnd} - end checks are ${userInput.data.end_ts <= entryEnd} - || ${userInput.data.end_ts - entryEnd <= 15 * 60}) - `); - } - - /* For this input to match, it must begin after the start of the timelineEntry (inclusive) - but before the end of the timelineEntry (exclusive) */ - const startChecks = userInput.data.start_ts >= entryStart && - userInput.data.start_ts < entryEnd; - /* A matching user input must also finish before the end of the timelineEntry, - or within 15 minutes. */ - var endChecks = (userInput.data.end_ts <= entryEnd || - (userInput.data.end_ts - entryEnd) <= 15 * 60); - if (startChecks && !endChecks) { - const nextEntryObj = tlEntry.getNextEntry(); - if (nextEntryObj) { - const nextEntryEnd = nextEntryObj.end_ts || nextEntryObj.exit_ts; - if (!nextEntryEnd) { // the last place will not have an exit_ts - endChecks = true; // so we will just skip the end check - } else { - endChecks = userInput.data.end_ts <= nextEntryEnd; - Logger.log("Second level of end checks when the next trip is defined("+userInput.data.end_ts+" <= "+ nextEntryEnd+") = "+endChecks); - } - } else { - // next trip is not defined, last trip - endChecks = (userInput.data.end_local_dt.day == userInput.data.start_local_dt.day) - Logger.log("Second level of end checks for the last trip of the day"); - Logger.log("compare "+userInput.data.end_local_dt.day + " with " + userInput.data.start_local_dt.day + " = " + endChecks); - } - if (endChecks) { - // If we have flipped the values, check to see that there - // is sufficient overlap - const overlapDuration = Math.min(userInput.data.end_ts, entryEnd) - Math.max(userInput.data.start_ts, entryStart) - Logger.log("Flipped endCheck, overlap("+overlapDuration+ - ")/trip("+tlEntry.duration+") = "+ (overlapDuration / tlEntry.duration)); - endChecks = (overlapDuration/tlEntry.duration) > 0.5; - } - } - return startChecks && endChecks; - } - - // parallels get_not_deleted_candidates() in trip_queries.py - const getNotDeletedCandidates = function(candidates) { - console.log('getNotDeletedCandidates called with ' + candidates.length + ' candidates'); - // We want to retain all ACTIVE entries that have not been DELETED - const allActiveList = candidates.filter(c => !c.data.status || c.data.status == 'ACTIVE'); - const allDeletedIds = candidates.filter(c => c.data.status && c.data.status == 'DELETED').map(c => c.data['match_id']); - const notDeletedActive = allActiveList.filter(c => !allDeletedIds.includes(c.data['match_id'])); - console.log(`Found ${allActiveList.length} active entries, - ${allDeletedIds.length} deleted entries -> - ${notDeletedActive.length} non deleted active entries`); - return notDeletedActive; - } - - im.getUserInputForTrip = function(trip, nextTrip, userInputList) { - const logsEnabled = userInputList.length < 20; - - if (userInputList === undefined) { - Logger.log("In getUserInputForTrip, no user input, returning undefined"); - return undefined; - } - - if (logsEnabled) { - console.log("Input list = "+userInputList.map(printUserInput)); - } - // undefined != true, so this covers the label view case as well - var potentialCandidates = userInputList.filter((ui) => im.validUserInputForTimelineEntry(trip, ui, logsEnabled)); - if (potentialCandidates.length === 0) { - if (logsEnabled) { - Logger.log("In getUserInputForTripStartEnd, no potential candidates, returning []"); - } - return undefined; - } - - if (potentialCandidates.length === 1) { - Logger.log("In getUserInputForTripStartEnd, one potential candidate, returning "+ printUserInput(potentialCandidates[0])); - return potentialCandidates[0]; - } - - Logger.log("potentialCandidates are "+potentialCandidates.map(printUserInput)); - var sortedPC = potentialCandidates.sort(function(pc1, pc2) { - return pc2.metadata.write_ts - pc1.metadata.write_ts; - }); - var mostRecentEntry = sortedPC[0]; - Logger.log("Returning mostRecentEntry "+printUserInput(mostRecentEntry)); - return mostRecentEntry; - } - - // return array of matching additions for a trip or place - im.getAdditionsForTimelineEntry = function(entry, additionsList) { - const logsEnabled = additionsList.length < 20; - - if (additionsList === undefined) { - Logger.log("In getAdditionsForTimelineEntry, no addition input, returning []"); - return []; - } - - // get additions that have not been deleted - // and filter out additions that do not start within the bounds of the timeline entry - const notDeleted = getNotDeletedCandidates(additionsList); - const matchingAdditions = notDeleted.filter((ui) => im.validUserInputForTimelineEntry(entry, ui, logsEnabled)); - - if (logsEnabled) { - console.log("Matching Addition list = "+matchingAdditions.map(printUserInput)); - } - return matchingAdditions; - } - - im.getUniqueEntries = function(combinedList) { - // we should not get any non-ACTIVE entries here - // since we have run filtering algorithms on both the phone and the server - const allDeleted = combinedList.filter(c => c.data.status && c.data.status == 'DELETED'); - if (allDeleted.length > 0) { - Logger.displayError("Found "+allDeletedEntries.length - +" non-ACTIVE addition entries while trying to dedup entries", - allDeletedEntries); - } - const uniqueMap = new Map(); - combinedList.forEach((e) => { - const existingVal = uniqueMap.get(e.data.match_id); - // if the existing entry and the input entry don't match - // and they are both active, we have an error - // let's notify the user for now - if (existingVal) { - if ((existingVal.data.start_ts != e.data.start_ts) || - (existingVal.data.end_ts != e.data.end_ts) || - (existingVal.data.write_ts != e.data.write_ts)) { - Logger.displayError("Found two ACTIVE entries with the same match ID but different timestamps "+existingVal.data.match_id, - JSON.stringify(existingVal) + " vs. "+ JSON.stringify(e)); - } else { - console.log("Found two entries with match_id "+existingVal.data.match_id+" but they are identical"); - } - } else { - uniqueMap.set(e.data.match_id, e); - } - }); - return Array.from(uniqueMap.values()); - } - - return im; -}); From bc74d12d07245cc7e5e6d7fc0f43dcdff61d52c6 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 12 Oct 2023 14:32:29 -0700 Subject: [PATCH 03/21] add new input-matcher service --- www/js/survey/input-matcher.ts | 241 +++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 www/js/survey/input-matcher.ts diff --git a/www/js/survey/input-matcher.ts b/www/js/survey/input-matcher.ts new file mode 100644 index 000000000..8b8e6d277 --- /dev/null +++ b/www/js/survey/input-matcher.ts @@ -0,0 +1,241 @@ +import { logDebug, displayErrorMsg } from "../plugin/logger" +import { DateTime } from "luxon"; + +export type LocalDt = { + minute: number, + hour: number, + second: number, + day: number, + weekday: number, + month: number, + year: number, + timezone: string +} + +export type UserInputForTrip = { + data: { + end_ts: number, + start_ts: number + label: string, + start_local_dt?: LocalDt + end_local_dt?: LocalDt + }, + metadata: { + time_zone: string, + plugin: string, + write_ts: number, + platform: string, + read_ts: number, + key: string + }, + key?: string, +} + +export type Trip = { + 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: any +} + + +const EPOCH_MAXIMUM = 2**31 - 1; + +export const fmtTs = (ts_in_secs: number, tz: string): string | null => DateTime.fromSeconds(ts_in_secs, {zone : tz}).toISO(); + +export const printUserInput = (ui: UserInputForTrip): string => `${fmtTs(ui.data.start_ts, ui.metadata.time_zone)} (${ui.data.start_ts}) -> +${fmtTs(ui.data.end_ts, ui.metadata.time_zone)} (${ui.data.end_ts}) ${ui.data.label} logged at ${ui.metadata.write_ts}`; + +export const validUserInputForDraftTrip = (trip: Trip, userInput: UserInputForTrip, logsEnabled: boolean): boolean => { + if(logsEnabled) { + logDebug(`Draft trip: + comparing user = ${fmtTs(userInput.data.start_ts, userInput.metadata.time_zone)} + -> ${fmtTs(userInput.data.end_ts, userInput.metadata.time_zone)} + trip = ${fmtTs(trip.start_ts, userInput.metadata.time_zone)} + -> ${fmtTs(trip.end_ts, userInput.metadata.time_zone)} + checks are (${userInput.data.start_ts >= trip.start_ts} + && ${userInput.data.start_ts < trip.end_ts} + || ${-(userInput.data.start_ts - trip.start_ts) <= 15 * 60}) + && ${userInput.data.end_ts <= trip.end_ts} + `); + } + + return (userInput.data.start_ts >= trip.start_ts && userInput.data.start_ts < trip.end_ts + || -(userInput.data.start_ts - trip.start_ts) <= 15 * 60) && userInput.data.end_ts <= trip.end_ts; +} + +export const validUserInputForTimelineEntry = (tlEntry: TlEntry, userInput: UserInputForTrip, logsEnabled: boolean): boolean => { + if (!tlEntry.origin_key) return false; + if (tlEntry.origin_key.includes('UNPROCESSED')) return validUserInputForDraftTrip(tlEntry, userInput, logsEnabled); + + /* Place-level inputs always have a key starting with 'manual/place', and + trip-level inputs never have a key starting with 'manual/place' + So if these don't match, we can immediately return false */ + const entryIsPlace = tlEntry.origin_key === 'analysis/confirmed_place'; + const isPlaceInput = (userInput.key || userInput.metadata.key).startsWith('manual/place'); + + if (entryIsPlace !== isPlaceInput) return false; + + let entryStart = tlEntry.start_ts || tlEntry.enter_ts; + let entryEnd = tlEntry.end_ts || tlEntry.exit_ts; + + if (!entryStart && entryEnd) { + /* if a place has no enter time, this is the first start_place of the first composite trip object + so we will set the start time to the start of the day of the end time for the purpose of comparison */ + entryStart = DateTime.fromSeconds(entryEnd).startOf('day').toUnixInteger(); + } + + if (!entryEnd) { + /* if a place has no exit time, the user hasn't left there yet + so we will set the end time as high as possible for the purpose of comparison */ + entryEnd = EPOCH_MAXIMUM; + } + + if (logsEnabled) { + logDebug(`Cleaned trip: + comparing user = ${fmtTs(userInput.data.start_ts, userInput.metadata.time_zone)} + -> ${fmtTs(userInput.data.end_ts, userInput.metadata.time_zone)} + trip = ${fmtTs(entryStart, userInput.metadata.time_zone)} + -> ${fmtTs(entryStart, userInput.metadata.time_zone)} + start checks are ${userInput.data.start_ts >= entryStart} + && ${userInput.data.start_ts < entryEnd} + end checks are ${userInput.data.end_ts <= entryEnd} + || ${userInput.data.end_ts - entryEnd <= 15 * 60}) + `); + } + + /* For this input to match, it must begin after the start of the timelineEntry (inclusive) + but before the end of the timelineEntry (exclusive) */ + const startChecks = userInput.data.start_ts >= entryStart && userInput.data.start_ts < entryEnd; + /* A matching user input must also finish before the end of the timelineEntry, + or within 15 minutes. */ + let endChecks = (userInput.data.end_ts <= entryEnd || (userInput.data.end_ts - entryEnd) <= 15 * 60); + + if (startChecks && !endChecks) { + const nextEntryObj = tlEntry.getNextEntry(); + if (nextEntryObj) { + const nextEntryEnd = nextEntryObj.end_ts || nextEntryObj.exit_ts; + if (!nextEntryEnd) { // the last place will not have an exit_ts + endChecks = true; // so we will just skip the end check + } else { + endChecks = userInput.data.end_ts <= nextEntryEnd; + logDebug(`Second level of end checks when the next trip is defined(${userInput.data.end_ts} <= ${nextEntryEnd}) ${endChecks}`); + } + } else { + // next trip is not defined, last trip + endChecks = (userInput.data.end_local_dt.day == userInput.data.start_local_dt.day) + logDebug("Second level of end checks for the last trip of the day"); + logDebug(`compare ${userInput.data.end_local_dt.day} with ${userInput.data.start_local_dt.day} ${endChecks}`); + } + if (endChecks) { + // If we have flipped the values, check to see that there is sufficient overlap + const overlapDuration = Math.min(userInput.data.end_ts, entryEnd) - Math.max(userInput.data.start_ts, entryStart) + logDebug(`Flipped endCheck, overlap(${overlapDuration})/trip(${tlEntry.duration} (${overlapDuration} / ${tlEntry.duration})`); + endChecks = (overlapDuration/tlEntry.duration) > 0.5; + } + } + return startChecks && endChecks; +} + +// parallels get_not_deleted_candidates() in trip_queries.py +export const getNotDeletedCandidates = (candidates) => { + console.log('getNotDeletedCandidates called with ' + candidates.length + ' candidates'); + + // We want to retain all ACTIVE entries that have not been DELETED + const allActiveList = candidates.filter(c => !c.data.status || c.data.status == 'ACTIVE'); + const allDeletedIds = candidates.filter(c => c.data.status && c.data.status == 'DELETED').map(c => c.data['match_id']); + const notDeletedActive = allActiveList.filter(c => !allDeletedIds.includes(c.data['match_id'])); + + console.log(`Found ${allActiveList.length} active entries, ${allDeletedIds.length} deleted entries -> + ${notDeletedActive.length} non deleted active entries`); + + return notDeletedActive; +} + +export const getUserInputForTrip = (trip: TlEntry, nextTrip: any, userInputList: UserInputForTrip[]): undefined | UserInputForTrip => { + const logsEnabled = userInputList.length < 20; + if (userInputList === undefined) { + logDebug("In getUserInputForTrip, no user input, returning undefined"); + return undefined; + } + + if (logsEnabled) console.log(`Input list = ${userInputList.map(printUserInput)}`); + + // undefined !== true, so this covers the label view case as well + const potentialCandidates = userInputList.filter((ui) => validUserInputForTimelineEntry(trip, ui, logsEnabled)); + + if (potentialCandidates.length === 0) { + if (logsEnabled) logDebug("In getUserInputForTripStartEnd, no potential candidates, returning []"); + return undefined; + } + + if (potentialCandidates.length === 1) { + logDebug(`In getUserInputForTripStartEnd, one potential candidate, returning ${printUserInput(potentialCandidates[0])}`); + return potentialCandidates[0]; + } + + logDebug(`potentialCandidates are ${potentialCandidates.map(printUserInput)}`); + + const sortedPC = potentialCandidates.sort((pc1, pc2) => pc2.metadata.write_ts - pc1.metadata.write_ts); + const mostRecentEntry = sortedPC[0]; + logDebug("Returning mostRecentEntry "+printUserInput(mostRecentEntry)); + + return mostRecentEntry; +} + +// return array of matching additions for a trip or place +export const getAdditionsForTimelineEntry = (entry, additionsList) => { + const logsEnabled = additionsList.length < 20; + + if (additionsList === undefined) { + logDebug("In getAdditionsForTimelineEntry, no addition input, returning []"); + return []; + } + + // get additions that have not been deleted and filter out additions that do not start within the bounds of the timeline entry + const notDeleted = getNotDeletedCandidates(additionsList); + const matchingAdditions = notDeleted.filter((ui) => validUserInputForTimelineEntry(entry, ui, logsEnabled)); + + if (logsEnabled) console.log(`Matching Addition list ${matchingAdditions.map(printUserInput)}`); + + return matchingAdditions; +} + +export const getUniqueEntries = (combinedList) => { + /* we should not get any non-ACTIVE entries here + since we have run filtering algorithms on both the phone and the server */ + const allDeleted = combinedList.filter(c => c.data.status && c.data.status == 'DELETED'); + + if (allDeleted.length > 0) { + displayErrorMsg("Found "+allDeleted.length +" non-ACTIVE addition entries while trying to dedup entries", JSON.stringify(allDeleted)); + } + + const uniqueMap = new Map(); + combinedList.forEach((e) => { + const existingVal = uniqueMap.get(e.data.match_id); + /* if the existing entry and the input entry don't match and they are both active, we have an error + let's notify the user for now */ + if (existingVal) { + if ((existingVal.data.start_ts != e.data.start_ts) || + (existingVal.data.end_ts != e.data.end_ts) || + (existingVal.data.write_ts != e.data.write_ts)) { + displayErrorMsg(`Found two ACTIVE entries with the same match ID but different timestamps ${existingVal.data.match_id}` + , `${JSON.stringify(existingVal)} vs ${JSON.stringify(e)}`); + } else { + console.log(`Found two entries with match_id ${existingVal.data.match_id} but they are identical`); + } + } else { + uniqueMap.set(e.data.match_id, e); + } + }); + return Array.from(uniqueMap.values()); +} From eb0d63e1f07ed32ec7ee59b1b4a5d53e77ae1c28 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 12 Oct 2023 14:33:40 -0700 Subject: [PATCH 04/21] call input matcher functions from the new input-matcher service --- www/js/survey/enketo/enketo-add-note-button.js | 10 +++++----- www/js/survey/enketo/enketo-trip-button.js | 9 ++++----- www/js/survey/multilabel/multi-label-ui.js | 4 ++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/www/js/survey/enketo/enketo-add-note-button.js b/www/js/survey/enketo/enketo-add-note-button.js index a2f0d1557..d0433b314 100644 --- a/www/js/survey/enketo/enketo-add-note-button.js +++ b/www/js/survey/enketo/enketo-add-note-button.js @@ -3,13 +3,13 @@ */ import angular from 'angular'; +import { getAdditionsForTimelineEntry, getUniqueEntries } from '../input-matcher'; angular.module('emission.survey.enketo.add-note-button', ['emission.stats.clientstats', 'emission.services', - 'emission.survey.enketo.answer', - 'emission.survey.inputmatcher']) -.factory("EnketoNotesButtonService", function(InputMatcher, EnketoSurveyAnswer, Logger, $timeout) { + 'emission.survey.enketo.answer']) +.factory("EnketoNotesButtonService", function(EnketoSurveyAnswer, Logger, $timeout) { var enbs = {}; console.log("Creating EnketoNotesButtonService"); enbs.SINGLE_KEY="NOTES"; @@ -77,9 +77,9 @@ angular.module('emission.survey.enketo.add-note-button', // be re-matching entries that have already been matched on the server // but the number of matched entries is likely to be small, so we can live // with the performance for now - const unprocessedAdditions = InputMatcher.getAdditionsForTimelineEntry(timelineEntry, inputList); + const unprocessedAdditions = getAdditionsForTimelineEntry(timelineEntry, inputList); const combinedPotentialAdditionList = timelineEntry.additions.concat(unprocessedAdditions); - const dedupedList = InputMatcher.getUniqueEntries(combinedPotentialAdditionList); + const dedupedList = getUniqueEntries(combinedPotentialAdditionList); Logger.log("After combining unprocessed ("+unprocessedAdditions.length+ ") with server ("+timelineEntry.additions.length+ ") for a combined ("+combinedPotentialAdditionList.length+ diff --git a/www/js/survey/enketo/enketo-trip-button.js b/www/js/survey/enketo/enketo-trip-button.js index 5b385a1ac..eacb7c61d 100644 --- a/www/js/survey/enketo/enketo-trip-button.js +++ b/www/js/survey/enketo/enketo-trip-button.js @@ -12,12 +12,12 @@ */ import angular from 'angular'; +import { getUserInputForTrip } from '../input-matcher'; angular.module('emission.survey.enketo.trip.button', ['emission.stats.clientstats', - 'emission.survey.enketo.answer', - 'emission.survey.inputmatcher']) -.factory("EnketoTripButtonService", function(InputMatcher, EnketoSurveyAnswer, Logger, $timeout) { + 'emission.survey.enketo.answer']) +.factory("EnketoTripButtonService", function(EnketoSurveyAnswer, Logger, $timeout) { var etbs = {}; console.log("Creating EnketoTripButtonService"); etbs.key = "manual/trip_user_input"; @@ -62,8 +62,7 @@ angular.module('emission.survey.enketo.trip.button', */ etbs.populateManualInputs = function (trip, nextTrip, inputType, inputList) { // Check unprocessed labels first since they are more recent - const unprocessedLabelEntry = InputMatcher.getUserInputForTrip(trip, nextTrip, - inputList); + const unprocessedLabelEntry = getUserInputForTrip(trip, nextTrip,inputList); var userInputEntry = unprocessedLabelEntry; if (!angular.isDefined(userInputEntry)) { userInputEntry = trip.user_input?.[etbs.inputType2retKey(inputType)]; diff --git a/www/js/survey/multilabel/multi-label-ui.js b/www/js/survey/multilabel/multi-label-ui.js index 313c8a3a9..8857f75c4 100644 --- a/www/js/survey/multilabel/multi-label-ui.js +++ b/www/js/survey/multilabel/multi-label-ui.js @@ -1,6 +1,7 @@ import angular from 'angular'; import { baseLabelInputDetails, getBaseLabelInputs, getFakeEntry, getLabelInputDetails, getLabelInputs, getLabelOptions } from './confirmHelper'; import { getConfig } from '../../config/dynamicConfig'; +import { getUserInputForTrip } from '../input-matcher'; angular.module('emission.survey.multilabel.buttons', ['emission.stats.clientstats', @@ -66,8 +67,7 @@ angular.module('emission.survey.multilabel.buttons', */ mls.populateManualInputs = function (trip, nextTrip, inputType, inputList) { // Check unprocessed labels first since they are more recent - const unprocessedLabelEntry = InputMatcher.getUserInputForTrip(trip, nextTrip, - inputList); + const unprocessedLabelEntry = getUserInputForTrip(trip, nextTrip, inputList); var userInputLabel = unprocessedLabelEntry? unprocessedLabelEntry.data.label : undefined; if (!angular.isDefined(userInputLabel)) { userInputLabel = trip.user_input?.[mls.inputType2retKey(inputType)]; From 9c518f65bd128140988f62597fe8d96d2f014efa Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 12 Oct 2023 14:35:32 -0700 Subject: [PATCH 05/21] add input-matcher unit test (not done) --- www/__tests__/input-matcher.test.ts | 143 ++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 www/__tests__/input-matcher.test.ts diff --git a/www/__tests__/input-matcher.test.ts b/www/__tests__/input-matcher.test.ts new file mode 100644 index 000000000..8dbb98a79 --- /dev/null +++ b/www/__tests__/input-matcher.test.ts @@ -0,0 +1,143 @@ +import { + TlEntry, + Trip, + UserInputForTrip, + fmtTs, + printUserInput, + validUserInputForDraftTrip, + validUserInputForTimelineEntry, + getNotDeletedCandidates +} from '../js/survey/input-matcher'; + +describe('input-matcher', () => { + let userTrip: UserInputForTrip; + + beforeEach(() => { + // create a userTrip object before each test case. + userTrip = { + data: { + end_ts: 1437604764, + start_ts: 1437601247, + label: "FOO" + }, + metadata: { + time_zone: "America/Los_Angeles", + plugin: "none", + write_ts: 1695921991.013001, + platform: "ios", + read_ts: 0, + key: "manual/mode_confirm" + }, + key: "manual/place" + } + }); + + it('tests fmtTs with valid input', () => { + const pstTime = fmtTs(1437601247.8459613, "America/Los_Angeles"); + const estTime = fmtTs(1437601247.8459613, "America/New_York"); + // Check if it contains correct year-mm-dd hr:mm + expect(pstTime).toContain("2015-07-22T14:40"); + expect(estTime).toContain("2015-07-22T17:40"); + }); + + it('tests fmtTs with invalid input', () => { + const formattedTeim = fmtTs(0, ""); + // Check if it contains correct year-mm-dd hr:mm + expect(formattedTeim).toBeFalsy(); + }); + + it('tests printUserInput prints the trip log correctly', () => { + const userTripLog = printUserInput(userTrip); + expect(userTripLog).toContain("1437604764"); + expect(userTripLog).toContain("1437601247"); + expect(userTripLog).toContain("FOO"); + }); + + it('tests validUserInputForDraftTrip with valid trip', () => { + const validTrp = { + end_ts: 1437604764, + start_ts: 1437601247 + } + const validUserInput = validUserInputForDraftTrip(validTrp, userTrip, false); + expect(validUserInput).toBeTruthy(); + }); + + it('tests validUserInputForDraftTrip with invalid trip', () => { + const invalidTrip = { + end_ts: 0, + start_ts: 0 + } + const invalidUserInput = validUserInputForDraftTrip(invalidTrip, userTrip, false); + expect(invalidUserInput).toBeFalsy(); + }); + + it('tests validUserInputForTimelineEntry with valid tlEntry object', () => { + const tlEntry: TlEntry = { + key: "analysis/confirmed_place", + origin_key: "analysis/confirmed_place", + start_ts: 1437601000, + end_ts: 1437605000, + enter_ts: 1437605000, + exit_ts: 1437605000, + duration: 100, + getNextEntry: jest.fn() + } + + const validTimelineEntry = validUserInputForTimelineEntry(tlEntry, userTrip, false); + expect(validTimelineEntry).toBeTruthy(); + }); + + it('tests validUserInputForTimelineEntry with invalid tlEntry key', () => { + const tlEntry: TlEntry = { + key: "FOO", + origin_key: "FOO", + start_ts: 1437601000, + end_ts: 1437605000, + enter_ts: 1437605000, + exit_ts: 1437605000, + duration: 100, + getNextEntry: jest.fn() + } + + const invalidTimelineEntry = validUserInputForTimelineEntry(tlEntry, userTrip, false); + expect(invalidTimelineEntry).toBeFalsy(); + }); + + it('tests validUserInputForTimelineEntry with invalid tlEntry start & end time', () => { + const tlEntry: TlEntry = { + key: "analysis/confirmed_place", + origin_key: "analysis/confirmed_place", + start_ts: 1, + end_ts: 1, + enter_ts: 1, + exit_ts: 1, + duration: 1, + getNextEntry: jest.fn() + } + + const invalidTimelineEntry = validUserInputForTimelineEntry(tlEntry, userTrip, false); + expect(invalidTimelineEntry).toBeFalsy(); + }); + + it('tests getNotDeletedCandidates called with 0 candidates', () => { + jest.spyOn(console, 'log'); + const candidates = getNotDeletedCandidates([]); + + // check if the log printed collectly with + expect(console.log).toHaveBeenCalledWith('getNotDeletedCandidates called with 0 candidates'); + expect(candidates).toStrictEqual([]); + + }); + + it('tests getUserInputForTrip', () => { + + }); + + it('tests getAdditionsForTimelineEntry', () => { + + }); + + it('tests getUniqueEntries', () => { + + }); +}) \ No newline at end of file From d9936f1b67e9b1bf1eb7bf38a1f5790475e794bb Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Fri, 13 Oct 2023 13:15:48 -0700 Subject: [PATCH 06/21] delete old input-matcher angular service --- www/js/survey/multilabel/multi-label-ui.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/www/js/survey/multilabel/multi-label-ui.js b/www/js/survey/multilabel/multi-label-ui.js index 8857f75c4..f9aa3b323 100644 --- a/www/js/survey/multilabel/multi-label-ui.js +++ b/www/js/survey/multilabel/multi-label-ui.js @@ -4,10 +4,9 @@ import { getConfig } from '../../config/dynamicConfig'; import { getUserInputForTrip } from '../input-matcher'; angular.module('emission.survey.multilabel.buttons', - ['emission.stats.clientstats', - 'emission.survey.inputmatcher']) + ['emission.stats.clientstats']) -.factory("MultiLabelService", function($rootScope, InputMatcher, $timeout, $ionicPlatform, Logger) { +.factory("MultiLabelService", function($rootScope, $timeout, $ionicPlatform, Logger) { var mls = {}; console.log("Creating MultiLabelService"); mls.init = function(config) { From 631de70d6cce9905608dec10ba4a2062a599f154 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Fri, 13 Oct 2023 13:17:26 -0700 Subject: [PATCH 07/21] Add getNotDeletedCandidates input & output type --- www/js/survey/input-matcher.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/www/js/survey/input-matcher.ts b/www/js/survey/input-matcher.ts index 8b8e6d277..f318ac255 100644 --- a/www/js/survey/input-matcher.ts +++ b/www/js/survey/input-matcher.ts @@ -19,6 +19,8 @@ export type UserInputForTrip = { label: string, start_local_dt?: LocalDt end_local_dt?: LocalDt + status?: string, + match_id?: string }, metadata: { time_zone: string, @@ -147,7 +149,7 @@ export const validUserInputForTimelineEntry = (tlEntry: TlEntry, userInput: User } // parallels get_not_deleted_candidates() in trip_queries.py -export const getNotDeletedCandidates = (candidates) => { +export const getNotDeletedCandidates = (candidates: UserInputForTrip[]): UserInputForTrip[] => { console.log('getNotDeletedCandidates called with ' + candidates.length + ' candidates'); // We want to retain all ACTIVE entries that have not been DELETED @@ -162,7 +164,7 @@ export const getNotDeletedCandidates = (candidates) => { } export const getUserInputForTrip = (trip: TlEntry, nextTrip: any, userInputList: UserInputForTrip[]): undefined | UserInputForTrip => { - const logsEnabled = userInputList.length < 20; + const logsEnabled = userInputList?.length < 20; if (userInputList === undefined) { logDebug("In getUserInputForTrip, no user input, returning undefined"); return undefined; @@ -193,8 +195,8 @@ export const getUserInputForTrip = (trip: TlEntry, nextTrip: any, userInputList } // return array of matching additions for a trip or place -export const getAdditionsForTimelineEntry = (entry, additionsList) => { - const logsEnabled = additionsList.length < 20; +export const getAdditionsForTimelineEntry = (entry: TlEntry, additionsList: UserInputForTrip[]): UserInputForTrip[] => { + const logsEnabled = additionsList?.length < 20; if (additionsList === undefined) { logDebug("In getAdditionsForTimelineEntry, no addition input, returning []"); From 7148ac6e23aa10a54f059a134c9866600ce2bc26 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Fri, 13 Oct 2023 13:17:48 -0700 Subject: [PATCH 08/21] remove input-matcher.js file --- www/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/www/index.js b/www/index.js index 55cb233b5..791c5f64a 100644 --- a/www/index.js +++ b/www/index.js @@ -17,7 +17,6 @@ import './js/controllers.js'; import './js/services.js'; import './js/i18n-utils.js'; import './js/main.js'; -import './js/survey/input-matcher.js'; import './js/survey/multilabel/infinite_scroll_filters.js'; import './js/survey/multilabel/multi-label-ui.js'; import './js/diary.js'; From 16673e805621566835ebef1321514fb21d2a88f1 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Fri, 13 Oct 2023 13:18:09 -0700 Subject: [PATCH 09/21] Done with input-matcher.test --- www/__tests__/input-matcher.test.ts | 222 ++++++++++++++++++++-------- 1 file changed, 164 insertions(+), 58 deletions(-) diff --git a/www/__tests__/input-matcher.test.ts b/www/__tests__/input-matcher.test.ts index 8dbb98a79..0a3fe4562 100644 --- a/www/__tests__/input-matcher.test.ts +++ b/www/__tests__/input-matcher.test.ts @@ -1,59 +1,76 @@ import { TlEntry, - Trip, UserInputForTrip, fmtTs, printUserInput, validUserInputForDraftTrip, validUserInputForTimelineEntry, - getNotDeletedCandidates + getNotDeletedCandidates, + getUserInputForTrip, + getAdditionsForTimelineEntry, + getUniqueEntries } from '../js/survey/input-matcher'; describe('input-matcher', () => { let userTrip: UserInputForTrip; + let trip: TlEntry; beforeEach(() => { - // create a userTrip object before each test case. + // create valid userTrip and trip object before each test case. userTrip = { data: { end_ts: 1437604764, start_ts: 1437601247, - label: "FOO" + label: 'FOO', + status: 'ACTIVE' }, metadata: { - time_zone: "America/Los_Angeles", - plugin: "none", - write_ts: 1695921991.013001, - platform: "ios", + time_zone: 'America/Los_Angeles', + plugin: 'none', + write_ts: 1695921991, + platform: 'ios', read_ts: 0, - key: "manual/mode_confirm" + key: 'manual/mode_confirm' }, - key: "manual/place" + key: 'manual/place' } + trip = { + key: 'FOO', + origin_key: 'FOO', + start_ts: 1437601000, + end_ts: 1437605000, + enter_ts: 1437605000, + exit_ts: 1437605000, + duration: 100, + getNextEntry: jest.fn() + } + + // mock Logger + window['Logger'] = { log: console.log }; }); it('tests fmtTs with valid input', () => { - const pstTime = fmtTs(1437601247.8459613, "America/Los_Angeles"); - const estTime = fmtTs(1437601247.8459613, "America/New_York"); + const pstTime = fmtTs(1437601247.8459613, 'America/Los_Angeles'); + const estTime = fmtTs(1437601247.8459613, 'America/New_York'); + // Check if it contains correct year-mm-dd hr:mm - expect(pstTime).toContain("2015-07-22T14:40"); - expect(estTime).toContain("2015-07-22T17:40"); + expect(pstTime).toContain('2015-07-22T14:40'); + expect(estTime).toContain('2015-07-22T17:40'); }); it('tests fmtTs with invalid input', () => { - const formattedTeim = fmtTs(0, ""); - // Check if it contains correct year-mm-dd hr:mm - expect(formattedTeim).toBeFalsy(); + const formattedTime = fmtTs(0, ''); + expect(formattedTime).toBeFalsy(); }); it('tests printUserInput prints the trip log correctly', () => { const userTripLog = printUserInput(userTrip); - expect(userTripLog).toContain("1437604764"); - expect(userTripLog).toContain("1437601247"); - expect(userTripLog).toContain("FOO"); + expect(userTripLog).toContain('1437604764'); + expect(userTripLog).toContain('1437601247'); + expect(userTripLog).toContain('FOO'); }); - it('tests validUserInputForDraftTrip with valid trip', () => { + it('tests validUserInputForDraftTrip with valid trip input', () => { const validTrp = { end_ts: 1437604764, start_ts: 1437601247 @@ -62,7 +79,7 @@ describe('input-matcher', () => { expect(validUserInput).toBeTruthy(); }); - it('tests validUserInputForDraftTrip with invalid trip', () => { + it('tests validUserInputForDraftTrip with invalid trip input', () => { const invalidTrip = { end_ts: 0, start_ts: 0 @@ -71,42 +88,24 @@ describe('input-matcher', () => { expect(invalidUserInput).toBeFalsy(); }); - it('tests validUserInputForTimelineEntry with valid tlEntry object', () => { - const tlEntry: TlEntry = { - key: "analysis/confirmed_place", - origin_key: "analysis/confirmed_place", - start_ts: 1437601000, - end_ts: 1437605000, - enter_ts: 1437605000, - exit_ts: 1437605000, - duration: 100, - getNextEntry: jest.fn() - } - - const validTimelineEntry = validUserInputForTimelineEntry(tlEntry, userTrip, false); + it('tests validUserInputForTimelineEntry with valid trip object', () => { + // we need valid key and origin_key for validUserInputForTimelineEntry test + trip['key'] = 'analysis/confirmed_place'; + trip['origin_key'] = 'analysis/confirmed_place'; + const validTimelineEntry = validUserInputForTimelineEntry(trip, userTrip, false); expect(validTimelineEntry).toBeTruthy(); }); - it('tests validUserInputForTimelineEntry with invalid tlEntry key', () => { - const tlEntry: TlEntry = { - key: "FOO", - origin_key: "FOO", - start_ts: 1437601000, - end_ts: 1437605000, - enter_ts: 1437605000, - exit_ts: 1437605000, - duration: 100, - getNextEntry: jest.fn() - } - - const invalidTimelineEntry = validUserInputForTimelineEntry(tlEntry, userTrip, false); + it('tests validUserInputForTimelineEntry with tlEntry with invalid key and origin_key', () => { + const invalidTlEntry = trip; + const invalidTimelineEntry = validUserInputForTimelineEntry(invalidTlEntry, userTrip, false); expect(invalidTimelineEntry).toBeFalsy(); }); - it('tests validUserInputForTimelineEntry with invalid tlEntry start & end time', () => { - const tlEntry: TlEntry = { - key: "analysis/confirmed_place", - origin_key: "analysis/confirmed_place", + it('tests validUserInputForTimelineEntry with tlEntry with invalie start & end time', () => { + const invalidTlEntry: TlEntry = { + key: 'analysis/confirmed_place', + origin_key: 'analysis/confirmed_place', start_ts: 1, end_ts: 1, enter_ts: 1, @@ -114,8 +113,7 @@ describe('input-matcher', () => { duration: 1, getNextEntry: jest.fn() } - - const invalidTimelineEntry = validUserInputForTimelineEntry(tlEntry, userTrip, false); + const invalidTimelineEntry = validUserInputForTimelineEntry(invalidTlEntry, userTrip, false); expect(invalidTimelineEntry).toBeFalsy(); }); @@ -129,15 +127,123 @@ describe('input-matcher', () => { }); - it('tests getUserInputForTrip', () => { - + it('tests getNotDeletedCandidates called with multiple candidates', () => { + const activeTrip = userTrip; + const deletedTrip = { + data: { + end_ts: 1437604764, + start_ts: 1437601247, + label: 'FOO', + status: 'DELETED', + match_id: 'FOO' + }, + metadata: { + time_zone: 'America/Los_Angeles', + plugin: 'none', + write_ts: 1695921991, + platform: 'ios', + read_ts: 0, + key: 'manual/mode_confirm' + }, + key: 'manual/place' + } + const candidates = [ activeTrip, deletedTrip ]; + const validCandidates = getNotDeletedCandidates(candidates); + + // check if the result has only 'ACTIVE' data + expect(validCandidates).toHaveLength(1); + expect(validCandidates[0]).toMatchObject(userTrip); + }); - it('tests getAdditionsForTimelineEntry', () => { + it('tests getUserInputForTrip with valid userInputList', () => { + const userInputWriteFirst = { + data: { + end_ts: 1437607732, + label: 'bus', + start_ts: 1437606026 + }, + metadata: { + time_zone: 'America/Los_Angeles', + plugin: 'none', + write_ts: 1695830232, + platform: 'ios', + read_ts: 0, + key:'manual/mode_confirm', + type:'message' + } + } + const userInputWriteSecond = { + data: { + end_ts: 1437598393, + label: 'e-bike', + start_ts: 1437596745 + }, + metadata: { + time_zone: 'America/Los_Angeles', + plugin: 'none', + write_ts: 1695838268, + platform: 'ios', + read_ts: 0, + key:'manual/mode_confirm', + type:'message' + } + } + const userInputWriteThird = { + data: { + end_ts: 1437604764, + label: 'e-bike', + start_ts: 1437601247 + }, + metadata: { + time_zone: 'America/Los_Angeles', + plugin: 'none', + write_ts: 1695921991, + platform: 'ios', + read_ts: 0, + key:'manual/mode_confirm', + type:'message' + } + } + + // make the linst unsorted and then check if userInputWriteThird(latest one) is return output + const userInputList = [userInputWriteSecond, userInputWriteThird, userInputWriteFirst]; + const mostRecentEntry = getUserInputForTrip(trip, {}, userInputList); + expect(mostRecentEntry).toMatchObject(userInputWriteThird); + }); + it('tests getUserInputForTrip with invalid userInputList', () => { + const userInputList = undefined; + const mostRecentEntry = getUserInputForTrip(trip, {}, userInputList); + expect(mostRecentEntry).toBe(undefined); }); - it('tests getUniqueEntries', () => { + it('tests getAdditionsForTimelineEntry with valid additionsList', () => { + const additionsList = new Array(5).fill(userTrip); + trip['key'] = 'analysis/confirmed_place'; + trip['origin_key'] = 'analysis/confirmed_place'; + + // check if the result keep the all valid userTrip items + const matchingAdditions = getAdditionsForTimelineEntry(trip, additionsList); + expect(matchingAdditions).toHaveLength(5); + }); + + it('tests getAdditionsForTimelineEntry with invalid additionsList', () => { + const additionsList = undefined; + const matchingAdditions = getAdditionsForTimelineEntry(trip, additionsList); + expect(matchingAdditions).toMatchObject([]); + }); + + it('tests getUniqueEntries with valid combinedList', () => { + const combinedList = new Array(5).fill(userTrip); + + // check if the result keeps only unique userTrip items + const uniqueEntires = getUniqueEntries(combinedList); + expect(uniqueEntires).toHaveLength(1); + }); + it('tests getUniqueEntries with empty combinedList', () => { + const uniqueEntires = getUniqueEntries([]); + expect(uniqueEntires).toMatchObject([]); }); }) \ No newline at end of file From b8ec27d7121d9386ec64281c6206376941287ed4 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Mon, 16 Oct 2023 11:52:02 -0700 Subject: [PATCH 10/21] add comments for test inputs --- www/__tests__/input-matcher.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/www/__tests__/input-matcher.test.ts b/www/__tests__/input-matcher.test.ts index 0a3fe4562..6a6c4c82a 100644 --- a/www/__tests__/input-matcher.test.ts +++ b/www/__tests__/input-matcher.test.ts @@ -16,7 +16,12 @@ describe('input-matcher', () => { let trip: TlEntry; beforeEach(() => { - // create valid userTrip and trip object before each test case. + /* + Create a valid userTrip and trip object before each test case. + The trip data is from the 'real_examples' data (shankari_2015-07-22) on the server. + For some test cases, I need to generate fake data, such as labels, keys, and origin_keys. + In such cases, I referred to 'TestUserInputFakeData.py' on the server. + */ userTrip = { data: { end_ts: 1437604764, From b43b6a0703a2d4ef59b05f6868d650698d629038 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 17 Oct 2023 10:45:27 -0700 Subject: [PATCH 11/21] remane input-matcher to inputMatcher (follow camelCase rule) --- www/__tests__/{input-matcher.test.ts => inputMatcher.test.ts} | 2 +- www/js/survey/enketo/enketo-add-note-button.js | 2 +- www/js/survey/enketo/enketo-trip-button.js | 2 +- www/js/survey/{input-matcher.ts => inputMatcher.ts} | 0 www/js/survey/multilabel/multi-label-ui.js | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename www/__tests__/{input-matcher.test.ts => inputMatcher.test.ts} (99%) rename www/js/survey/{input-matcher.ts => inputMatcher.ts} (100%) diff --git a/www/__tests__/input-matcher.test.ts b/www/__tests__/inputMatcher.test.ts similarity index 99% rename from www/__tests__/input-matcher.test.ts rename to www/__tests__/inputMatcher.test.ts index 6a6c4c82a..503eb456e 100644 --- a/www/__tests__/input-matcher.test.ts +++ b/www/__tests__/inputMatcher.test.ts @@ -9,7 +9,7 @@ import { getUserInputForTrip, getAdditionsForTimelineEntry, getUniqueEntries -} from '../js/survey/input-matcher'; +} from '../js/survey/inputMatcher'; describe('input-matcher', () => { let userTrip: UserInputForTrip; diff --git a/www/js/survey/enketo/enketo-add-note-button.js b/www/js/survey/enketo/enketo-add-note-button.js index d0433b314..e1db494d0 100644 --- a/www/js/survey/enketo/enketo-add-note-button.js +++ b/www/js/survey/enketo/enketo-add-note-button.js @@ -3,7 +3,7 @@ */ import angular from 'angular'; -import { getAdditionsForTimelineEntry, getUniqueEntries } from '../input-matcher'; +import { getAdditionsForTimelineEntry, getUniqueEntries } from '../inputMatcher'; angular.module('emission.survey.enketo.add-note-button', ['emission.stats.clientstats', diff --git a/www/js/survey/enketo/enketo-trip-button.js b/www/js/survey/enketo/enketo-trip-button.js index eacb7c61d..91c2d2a56 100644 --- a/www/js/survey/enketo/enketo-trip-button.js +++ b/www/js/survey/enketo/enketo-trip-button.js @@ -12,7 +12,7 @@ */ import angular from 'angular'; -import { getUserInputForTrip } from '../input-matcher'; +import { getUserInputForTrip } from '../inputMatcher'; angular.module('emission.survey.enketo.trip.button', ['emission.stats.clientstats', diff --git a/www/js/survey/input-matcher.ts b/www/js/survey/inputMatcher.ts similarity index 100% rename from www/js/survey/input-matcher.ts rename to www/js/survey/inputMatcher.ts diff --git a/www/js/survey/multilabel/multi-label-ui.js b/www/js/survey/multilabel/multi-label-ui.js index f9aa3b323..09d52e492 100644 --- a/www/js/survey/multilabel/multi-label-ui.js +++ b/www/js/survey/multilabel/multi-label-ui.js @@ -1,7 +1,7 @@ import angular from 'angular'; import { baseLabelInputDetails, getBaseLabelInputs, getFakeEntry, getLabelInputDetails, getLabelInputs, getLabelOptions } from './confirmHelper'; import { getConfig } from '../../config/dynamicConfig'; -import { getUserInputForTrip } from '../input-matcher'; +import { getUserInputForTrip } from '../inputMatcher'; angular.module('emission.survey.multilabel.buttons', ['emission.stats.clientstats']) From ffcc871d90ad78a3b95eac339292948632c04d02 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 17 Oct 2023 12:44:34 -0700 Subject: [PATCH 12/21] move trip related types to 'dairyTypes' file in 'types' directory --- www/__tests__/inputMatcher.test.ts | 7 ++- www/js/survey/inputMatcher.ts | 62 +++------------------------ www/js/{diary => types}/diaryTypes.ts | 56 ++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 63 deletions(-) rename www/js/{diary => types}/diaryTypes.ts (73%) diff --git a/www/__tests__/inputMatcher.test.ts b/www/__tests__/inputMatcher.test.ts index 503eb456e..ac14a506b 100644 --- a/www/__tests__/inputMatcher.test.ts +++ b/www/__tests__/inputMatcher.test.ts @@ -1,6 +1,4 @@ import { - TlEntry, - UserInputForTrip, fmtTs, printUserInput, validUserInputForDraftTrip, @@ -10,9 +8,10 @@ import { getAdditionsForTimelineEntry, getUniqueEntries } from '../js/survey/inputMatcher'; +import { TlEntry, UserInput } from '../js/types/diaryTypes'; describe('input-matcher', () => { - let userTrip: UserInputForTrip; + let userTrip: UserInput; let trip: TlEntry; beforeEach(() => { @@ -251,4 +250,4 @@ describe('input-matcher', () => { const uniqueEntires = getUniqueEntries([]); expect(uniqueEntires).toMatchObject([]); }); -}) \ No newline at end of file +}) diff --git a/www/js/survey/inputMatcher.ts b/www/js/survey/inputMatcher.ts index f318ac255..c6c8ed61c 100644 --- a/www/js/survey/inputMatcher.ts +++ b/www/js/survey/inputMatcher.ts @@ -1,63 +1,15 @@ import { logDebug, displayErrorMsg } from "../plugin/logger" import { DateTime } from "luxon"; - -export type LocalDt = { - minute: number, - hour: number, - second: number, - day: number, - weekday: number, - month: number, - year: number, - timezone: string -} - -export type UserInputForTrip = { - 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, -} - -export type Trip = { - 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: any -} - +import { UserInput, Trip, TlEntry } from "../types/diaryTypes"; const EPOCH_MAXIMUM = 2**31 - 1; export const fmtTs = (ts_in_secs: number, tz: string): string | null => DateTime.fromSeconds(ts_in_secs, {zone : tz}).toISO(); -export const printUserInput = (ui: UserInputForTrip): string => `${fmtTs(ui.data.start_ts, ui.metadata.time_zone)} (${ui.data.start_ts}) -> +export const printUserInput = (ui: UserInput): string => `${fmtTs(ui.data.start_ts, ui.metadata.time_zone)} (${ui.data.start_ts}) -> ${fmtTs(ui.data.end_ts, ui.metadata.time_zone)} (${ui.data.end_ts}) ${ui.data.label} logged at ${ui.metadata.write_ts}`; -export const validUserInputForDraftTrip = (trip: Trip, userInput: UserInputForTrip, logsEnabled: boolean): boolean => { +export const validUserInputForDraftTrip = (trip: Trip, userInput: UserInput, logsEnabled: boolean): boolean => { if(logsEnabled) { logDebug(`Draft trip: comparing user = ${fmtTs(userInput.data.start_ts, userInput.metadata.time_zone)} @@ -75,7 +27,7 @@ export const validUserInputForDraftTrip = (trip: Trip, userInput: UserInputForTr || -(userInput.data.start_ts - trip.start_ts) <= 15 * 60) && userInput.data.end_ts <= trip.end_ts; } -export const validUserInputForTimelineEntry = (tlEntry: TlEntry, userInput: UserInputForTrip, logsEnabled: boolean): boolean => { +export const validUserInputForTimelineEntry = (tlEntry: TlEntry, userInput: UserInput, logsEnabled: boolean): boolean => { if (!tlEntry.origin_key) return false; if (tlEntry.origin_key.includes('UNPROCESSED')) return validUserInputForDraftTrip(tlEntry, userInput, logsEnabled); @@ -149,7 +101,7 @@ export const validUserInputForTimelineEntry = (tlEntry: TlEntry, userInput: User } // parallels get_not_deleted_candidates() in trip_queries.py -export const getNotDeletedCandidates = (candidates: UserInputForTrip[]): UserInputForTrip[] => { +export const getNotDeletedCandidates = (candidates: UserInput[]): UserInput[] => { console.log('getNotDeletedCandidates called with ' + candidates.length + ' candidates'); // We want to retain all ACTIVE entries that have not been DELETED @@ -163,7 +115,7 @@ export const getNotDeletedCandidates = (candidates: UserInputForTrip[]): UserInp return notDeletedActive; } -export const getUserInputForTrip = (trip: TlEntry, nextTrip: any, userInputList: UserInputForTrip[]): undefined | UserInputForTrip => { +export const getUserInputForTrip = (trip: TlEntry, nextTrip: any, userInputList: UserInput[]): undefined | UserInput => { const logsEnabled = userInputList?.length < 20; if (userInputList === undefined) { logDebug("In getUserInputForTrip, no user input, returning undefined"); @@ -195,7 +147,7 @@ export const getUserInputForTrip = (trip: TlEntry, nextTrip: any, userInputList } // return array of matching additions for a trip or place -export const getAdditionsForTimelineEntry = (entry: TlEntry, additionsList: UserInputForTrip[]): UserInputForTrip[] => { +export const getAdditionsForTimelineEntry = (entry: TlEntry, additionsList: UserInput[]): UserInput[] => { const logsEnabled = additionsList?.length < 20; if (additionsList === undefined) { diff --git a/www/js/diary/diaryTypes.ts b/www/js/types/diaryTypes.ts similarity index 73% rename from www/js/diary/diaryTypes.ts rename to www/js/types/diaryTypes.ts index bcaeb83ae..b12e58543 100644 --- a/www/js/diary/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -21,7 +21,7 @@ export type CompositeTrip = { end_confirmed_place: ConfirmedPlace, end_fmt_time: string, end_loc: {type: string, coordinates: number[]}, - end_local_dt: any, // TODO + end_local_dt: LocalDt, end_place: {$oid: string}, end_ts: number, expectation: any, // TODO "{to_label: boolean}" @@ -38,10 +38,10 @@ export type CompositeTrip = { start_confirmed_place: ConfirmedPlace, start_fmt_time: string, start_loc: {type: string, coordinates: number[]}, - start_local_dt: any, // TODO + start_local_dt: LocalDt, start_place: {$oid: string}, start_ts: number, - user_input: any, // TODO + user_input: UserInput, } /* These properties aren't received from the server, but are derived from the above properties. @@ -67,6 +67,54 @@ export type PopulatedTrip = CompositeTrip & { finalInference?: any, // TODO geojson?: any, // TODO getNextEntry?: () => PopulatedTrip | ConfirmedPlace, - userInput?: any, // TODO + userInput?: UserInput, verifiability?: string, } + +export type UserInput = { + 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 +} + +export type LocalDt = { + minute: number, + hour: number, + second: number, + day: number, + weekday: number, + month: number, + year: number, + timezone: string, +} + +export type Trip = { + 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, +} From 3e6d2b0170f63a3e0ccd74656a9353abd156b3bf Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 2 Nov 2023 13:23:44 -0700 Subject: [PATCH 13/21] Add prettier to workflows --- .github/workflows/prettier.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/workflows/prettier.yml diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml new file mode 100644 index 000000000..05a8c551d --- /dev/null +++ b/.github/workflows/prettier.yml @@ -0,0 +1,11 @@ +name: prettier +on: + pull_request: + +jobs: + run-prettier: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npx prettier --check www + From b4bc7ab568b6bf2cfb4f3fb039e351b6583fc72f Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 2 Nov 2023 13:57:17 -0700 Subject: [PATCH 14/21] Run prettier --- www/__tests__/inputMatcher.test.ts | 484 +++++++++--------- .../survey/enketo/enketo-add-note-button.js | 180 +++---- www/js/survey/enketo/enketo-trip-button.js | 178 ++++--- www/js/survey/inputMatcher.ts | 357 +++++++------ www/js/survey/multilabel/multi-label-ui.js | 385 +++++++------- www/js/types/diaryTypes.ts | 156 +++--- 6 files changed, 895 insertions(+), 845 deletions(-) diff --git a/www/__tests__/inputMatcher.test.ts b/www/__tests__/inputMatcher.test.ts index ac14a506b..6033df444 100644 --- a/www/__tests__/inputMatcher.test.ts +++ b/www/__tests__/inputMatcher.test.ts @@ -1,253 +1,251 @@ -import { - fmtTs, - printUserInput, - validUserInputForDraftTrip, - validUserInputForTimelineEntry, - getNotDeletedCandidates, - getUserInputForTrip, - getAdditionsForTimelineEntry, - getUniqueEntries +import { + fmtTs, + printUserInput, + validUserInputForDraftTrip, + validUserInputForTimelineEntry, + getNotDeletedCandidates, + getUserInputForTrip, + getAdditionsForTimelineEntry, + getUniqueEntries, } from '../js/survey/inputMatcher'; import { TlEntry, UserInput } from '../js/types/diaryTypes'; describe('input-matcher', () => { - let userTrip: UserInput; - let trip: TlEntry; + let userTrip: UserInput; + let trip: TlEntry; - beforeEach(() => { - /* + beforeEach(() => { + /* Create a valid userTrip and trip object before each test case. The trip data is from the 'real_examples' data (shankari_2015-07-22) on the server. For some test cases, I need to generate fake data, such as labels, keys, and origin_keys. In such cases, I referred to 'TestUserInputFakeData.py' on the server. */ - userTrip = { - data: { - end_ts: 1437604764, - start_ts: 1437601247, - label: 'FOO', - status: 'ACTIVE' - }, - metadata: { - time_zone: 'America/Los_Angeles', - plugin: 'none', - write_ts: 1695921991, - platform: 'ios', - read_ts: 0, - key: 'manual/mode_confirm' - }, - key: 'manual/place' - } - trip = { - key: 'FOO', - origin_key: 'FOO', - start_ts: 1437601000, - end_ts: 1437605000, - enter_ts: 1437605000, - exit_ts: 1437605000, - duration: 100, - getNextEntry: jest.fn() - } - - // mock Logger - window['Logger'] = { log: console.log }; - }); - - it('tests fmtTs with valid input', () => { - const pstTime = fmtTs(1437601247.8459613, 'America/Los_Angeles'); - const estTime = fmtTs(1437601247.8459613, 'America/New_York'); - - // Check if it contains correct year-mm-dd hr:mm - expect(pstTime).toContain('2015-07-22T14:40'); - expect(estTime).toContain('2015-07-22T17:40'); - }); - - it('tests fmtTs with invalid input', () => { - const formattedTime = fmtTs(0, ''); - expect(formattedTime).toBeFalsy(); - }); - - it('tests printUserInput prints the trip log correctly', () => { - const userTripLog = printUserInput(userTrip); - expect(userTripLog).toContain('1437604764'); - expect(userTripLog).toContain('1437601247'); - expect(userTripLog).toContain('FOO'); - }); - - it('tests validUserInputForDraftTrip with valid trip input', () => { - const validTrp = { - end_ts: 1437604764, - start_ts: 1437601247 - } - const validUserInput = validUserInputForDraftTrip(validTrp, userTrip, false); - expect(validUserInput).toBeTruthy(); - }); - - it('tests validUserInputForDraftTrip with invalid trip input', () => { - const invalidTrip = { - end_ts: 0, - start_ts: 0 - } - const invalidUserInput = validUserInputForDraftTrip(invalidTrip, userTrip, false); - expect(invalidUserInput).toBeFalsy(); - }); - - it('tests validUserInputForTimelineEntry with valid trip object', () => { - // we need valid key and origin_key for validUserInputForTimelineEntry test - trip['key'] = 'analysis/confirmed_place'; - trip['origin_key'] = 'analysis/confirmed_place'; - const validTimelineEntry = validUserInputForTimelineEntry(trip, userTrip, false); - expect(validTimelineEntry).toBeTruthy(); - }); - - it('tests validUserInputForTimelineEntry with tlEntry with invalid key and origin_key', () => { - const invalidTlEntry = trip; - const invalidTimelineEntry = validUserInputForTimelineEntry(invalidTlEntry, userTrip, false); - expect(invalidTimelineEntry).toBeFalsy(); - }); - - it('tests validUserInputForTimelineEntry with tlEntry with invalie start & end time', () => { - const invalidTlEntry: TlEntry = { - key: 'analysis/confirmed_place', - origin_key: 'analysis/confirmed_place', - start_ts: 1, - end_ts: 1, - enter_ts: 1, - exit_ts: 1, - duration: 1, - getNextEntry: jest.fn() - } - const invalidTimelineEntry = validUserInputForTimelineEntry(invalidTlEntry, userTrip, false); - expect(invalidTimelineEntry).toBeFalsy(); - }); - - it('tests getNotDeletedCandidates called with 0 candidates', () => { - jest.spyOn(console, 'log'); - const candidates = getNotDeletedCandidates([]); - - // check if the log printed collectly with - expect(console.log).toHaveBeenCalledWith('getNotDeletedCandidates called with 0 candidates'); - expect(candidates).toStrictEqual([]); - - }); - - it('tests getNotDeletedCandidates called with multiple candidates', () => { - const activeTrip = userTrip; - const deletedTrip = { - data: { - end_ts: 1437604764, - start_ts: 1437601247, - label: 'FOO', - status: 'DELETED', - match_id: 'FOO' - }, - metadata: { - time_zone: 'America/Los_Angeles', - plugin: 'none', - write_ts: 1695921991, - platform: 'ios', - read_ts: 0, - key: 'manual/mode_confirm' - }, - key: 'manual/place' - } - const candidates = [ activeTrip, deletedTrip ]; - const validCandidates = getNotDeletedCandidates(candidates); - - // check if the result has only 'ACTIVE' data - expect(validCandidates).toHaveLength(1); - expect(validCandidates[0]).toMatchObject(userTrip); - - }); - - it('tests getUserInputForTrip with valid userInputList', () => { - const userInputWriteFirst = { - data: { - end_ts: 1437607732, - label: 'bus', - start_ts: 1437606026 - }, - metadata: { - time_zone: 'America/Los_Angeles', - plugin: 'none', - write_ts: 1695830232, - platform: 'ios', - read_ts: 0, - key:'manual/mode_confirm', - type:'message' - } - } - const userInputWriteSecond = { - data: { - end_ts: 1437598393, - label: 'e-bike', - start_ts: 1437596745 - }, - metadata: { - time_zone: 'America/Los_Angeles', - plugin: 'none', - write_ts: 1695838268, - platform: 'ios', - read_ts: 0, - key:'manual/mode_confirm', - type:'message' - } - } - const userInputWriteThird = { - data: { - end_ts: 1437604764, - label: 'e-bike', - start_ts: 1437601247 - }, - metadata: { - time_zone: 'America/Los_Angeles', - plugin: 'none', - write_ts: 1695921991, - platform: 'ios', - read_ts: 0, - key:'manual/mode_confirm', - type:'message' - } - } - - // make the linst unsorted and then check if userInputWriteThird(latest one) is return output - const userInputList = [userInputWriteSecond, userInputWriteThird, userInputWriteFirst]; - const mostRecentEntry = getUserInputForTrip(trip, {}, userInputList); - expect(mostRecentEntry).toMatchObject(userInputWriteThird); - }); - - it('tests getUserInputForTrip with invalid userInputList', () => { - const userInputList = undefined; - const mostRecentEntry = getUserInputForTrip(trip, {}, userInputList); - expect(mostRecentEntry).toBe(undefined); - }); - - it('tests getAdditionsForTimelineEntry with valid additionsList', () => { - const additionsList = new Array(5).fill(userTrip); - trip['key'] = 'analysis/confirmed_place'; - trip['origin_key'] = 'analysis/confirmed_place'; - - // check if the result keep the all valid userTrip items - const matchingAdditions = getAdditionsForTimelineEntry(trip, additionsList); - expect(matchingAdditions).toHaveLength(5); - }); - - it('tests getAdditionsForTimelineEntry with invalid additionsList', () => { - const additionsList = undefined; - const matchingAdditions = getAdditionsForTimelineEntry(trip, additionsList); - expect(matchingAdditions).toMatchObject([]); - }); - - it('tests getUniqueEntries with valid combinedList', () => { - const combinedList = new Array(5).fill(userTrip); - - // check if the result keeps only unique userTrip items - const uniqueEntires = getUniqueEntries(combinedList); - expect(uniqueEntires).toHaveLength(1); - }); - - it('tests getUniqueEntries with empty combinedList', () => { - const uniqueEntires = getUniqueEntries([]); - expect(uniqueEntires).toMatchObject([]); - }); -}) + userTrip = { + data: { + end_ts: 1437604764, + start_ts: 1437601247, + label: 'FOO', + status: 'ACTIVE', + }, + metadata: { + time_zone: 'America/Los_Angeles', + plugin: 'none', + write_ts: 1695921991, + platform: 'ios', + read_ts: 0, + key: 'manual/mode_confirm', + }, + key: 'manual/place', + }; + trip = { + key: 'FOO', + origin_key: 'FOO', + start_ts: 1437601000, + end_ts: 1437605000, + enter_ts: 1437605000, + exit_ts: 1437605000, + duration: 100, + getNextEntry: jest.fn(), + }; + + // mock Logger + window['Logger'] = { log: console.log }; + }); + + it('tests fmtTs with valid input', () => { + const pstTime = fmtTs(1437601247.8459613, 'America/Los_Angeles'); + const estTime = fmtTs(1437601247.8459613, 'America/New_York'); + + // Check if it contains correct year-mm-dd hr:mm + expect(pstTime).toContain('2015-07-22T14:40'); + expect(estTime).toContain('2015-07-22T17:40'); + }); + + it('tests fmtTs with invalid input', () => { + const formattedTime = fmtTs(0, ''); + expect(formattedTime).toBeFalsy(); + }); + + it('tests printUserInput prints the trip log correctly', () => { + const userTripLog = printUserInput(userTrip); + expect(userTripLog).toContain('1437604764'); + expect(userTripLog).toContain('1437601247'); + expect(userTripLog).toContain('FOO'); + }); + + it('tests validUserInputForDraftTrip with valid trip input', () => { + const validTrp = { + end_ts: 1437604764, + start_ts: 1437601247, + }; + const validUserInput = validUserInputForDraftTrip(validTrp, userTrip, false); + expect(validUserInput).toBeTruthy(); + }); + + it('tests validUserInputForDraftTrip with invalid trip input', () => { + const invalidTrip = { + end_ts: 0, + start_ts: 0, + }; + const invalidUserInput = validUserInputForDraftTrip(invalidTrip, userTrip, false); + expect(invalidUserInput).toBeFalsy(); + }); + + it('tests validUserInputForTimelineEntry with valid trip object', () => { + // we need valid key and origin_key for validUserInputForTimelineEntry test + trip['key'] = 'analysis/confirmed_place'; + trip['origin_key'] = 'analysis/confirmed_place'; + const validTimelineEntry = validUserInputForTimelineEntry(trip, userTrip, false); + expect(validTimelineEntry).toBeTruthy(); + }); + + it('tests validUserInputForTimelineEntry with tlEntry with invalid key and origin_key', () => { + const invalidTlEntry = trip; + const invalidTimelineEntry = validUserInputForTimelineEntry(invalidTlEntry, userTrip, false); + expect(invalidTimelineEntry).toBeFalsy(); + }); + + it('tests validUserInputForTimelineEntry with tlEntry with invalie start & end time', () => { + const invalidTlEntry: TlEntry = { + key: 'analysis/confirmed_place', + origin_key: 'analysis/confirmed_place', + start_ts: 1, + end_ts: 1, + enter_ts: 1, + exit_ts: 1, + duration: 1, + getNextEntry: jest.fn(), + }; + const invalidTimelineEntry = validUserInputForTimelineEntry(invalidTlEntry, userTrip, false); + expect(invalidTimelineEntry).toBeFalsy(); + }); + + it('tests getNotDeletedCandidates called with 0 candidates', () => { + jest.spyOn(console, 'log'); + const candidates = getNotDeletedCandidates([]); + + // check if the log printed collectly with + expect(console.log).toHaveBeenCalledWith('getNotDeletedCandidates called with 0 candidates'); + expect(candidates).toStrictEqual([]); + }); + + it('tests getNotDeletedCandidates called with multiple candidates', () => { + const activeTrip = userTrip; + const deletedTrip = { + data: { + end_ts: 1437604764, + start_ts: 1437601247, + label: 'FOO', + status: 'DELETED', + match_id: 'FOO', + }, + metadata: { + time_zone: 'America/Los_Angeles', + plugin: 'none', + write_ts: 1695921991, + platform: 'ios', + read_ts: 0, + key: 'manual/mode_confirm', + }, + key: 'manual/place', + }; + const candidates = [activeTrip, deletedTrip]; + const validCandidates = getNotDeletedCandidates(candidates); + + // check if the result has only 'ACTIVE' data + expect(validCandidates).toHaveLength(1); + expect(validCandidates[0]).toMatchObject(userTrip); + }); + + it('tests getUserInputForTrip with valid userInputList', () => { + const userInputWriteFirst = { + data: { + end_ts: 1437607732, + label: 'bus', + start_ts: 1437606026, + }, + metadata: { + time_zone: 'America/Los_Angeles', + plugin: 'none', + write_ts: 1695830232, + platform: 'ios', + read_ts: 0, + key: 'manual/mode_confirm', + type: 'message', + }, + }; + const userInputWriteSecond = { + data: { + end_ts: 1437598393, + label: 'e-bike', + start_ts: 1437596745, + }, + metadata: { + time_zone: 'America/Los_Angeles', + plugin: 'none', + write_ts: 1695838268, + platform: 'ios', + read_ts: 0, + key: 'manual/mode_confirm', + type: 'message', + }, + }; + const userInputWriteThird = { + data: { + end_ts: 1437604764, + label: 'e-bike', + start_ts: 1437601247, + }, + metadata: { + time_zone: 'America/Los_Angeles', + plugin: 'none', + write_ts: 1695921991, + platform: 'ios', + read_ts: 0, + key: 'manual/mode_confirm', + type: 'message', + }, + }; + + // make the linst unsorted and then check if userInputWriteThird(latest one) is return output + const userInputList = [userInputWriteSecond, userInputWriteThird, userInputWriteFirst]; + const mostRecentEntry = getUserInputForTrip(trip, {}, userInputList); + expect(mostRecentEntry).toMatchObject(userInputWriteThird); + }); + + it('tests getUserInputForTrip with invalid userInputList', () => { + const userInputList = undefined; + const mostRecentEntry = getUserInputForTrip(trip, {}, userInputList); + expect(mostRecentEntry).toBe(undefined); + }); + + it('tests getAdditionsForTimelineEntry with valid additionsList', () => { + const additionsList = new Array(5).fill(userTrip); + trip['key'] = 'analysis/confirmed_place'; + trip['origin_key'] = 'analysis/confirmed_place'; + + // check if the result keep the all valid userTrip items + const matchingAdditions = getAdditionsForTimelineEntry(trip, additionsList); + expect(matchingAdditions).toHaveLength(5); + }); + + it('tests getAdditionsForTimelineEntry with invalid additionsList', () => { + const additionsList = undefined; + const matchingAdditions = getAdditionsForTimelineEntry(trip, additionsList); + expect(matchingAdditions).toMatchObject([]); + }); + + it('tests getUniqueEntries with valid combinedList', () => { + const combinedList = new Array(5).fill(userTrip); + + // check if the result keeps only unique userTrip items + const uniqueEntires = getUniqueEntries(combinedList); + expect(uniqueEntires).toHaveLength(1); + }); + + it('tests getUniqueEntries with empty combinedList', () => { + const uniqueEntires = getUniqueEntries([]); + expect(uniqueEntires).toMatchObject([]); + }); +}); diff --git a/www/js/survey/enketo/enketo-add-note-button.js b/www/js/survey/enketo/enketo-add-note-button.js index 720427f2c..56a41cb04 100644 --- a/www/js/survey/enketo/enketo-add-note-button.js +++ b/www/js/survey/enketo/enketo-add-note-button.js @@ -5,77 +5,75 @@ import angular from 'angular'; import { getAdditionsForTimelineEntry, getUniqueEntries } from '../inputMatcher'; -angular.module('emission.survey.enketo.add-note-button', - ['emission.services', - 'emission.survey.enketo.answer']) -.factory("EnketoNotesButtonService", function(EnketoSurveyAnswer, Logger, $timeout) { - var enbs = {}; - console.log("Creating EnketoNotesButtonService"); - enbs.SINGLE_KEY="NOTES"; - enbs.MANUAL_KEYS = []; +angular + .module('emission.survey.enketo.add-note-button', [ + 'emission.services', + 'emission.survey.enketo.answer', + ]) + .factory('EnketoNotesButtonService', function (EnketoSurveyAnswer, Logger, $timeout) { + var enbs = {}; + console.log('Creating EnketoNotesButtonService'); + enbs.SINGLE_KEY = 'NOTES'; + enbs.MANUAL_KEYS = []; - /** - * Set the keys for trip and/or place additions whichever will be enabled, - * and sets the name of the surveys they will use. - */ - enbs.initConfig = function (tripSurveyName, placeSurveyName) { - enbs.tripSurveyName = tripSurveyName; - if (tripSurveyName) { - enbs.MANUAL_KEYS.push('manual/trip_addition_input'); - } - enbs.placeSurveyName = placeSurveyName; - if (placeSurveyName) { - enbs.MANUAL_KEYS.push('manual/place_addition_input'); - } - }; + /** + * Set the keys for trip and/or place additions whichever will be enabled, + * and sets the name of the surveys they will use. + */ + enbs.initConfig = function (tripSurveyName, placeSurveyName) { + enbs.tripSurveyName = tripSurveyName; + if (tripSurveyName) { + enbs.MANUAL_KEYS.push('manual/trip_addition_input'); + } + enbs.placeSurveyName = placeSurveyName; + if (placeSurveyName) { + enbs.MANUAL_KEYS.push('manual/place_addition_input'); + } + }; - /** - * Embed 'inputType' to the timelineEntry. - */ - enbs.extractResult = function (results) { - const resultsPromises = [ - EnketoSurveyAnswer.filterByNameAndVersion(enbs.timelineEntrySurveyName, results), - ]; - if (enbs.timelineEntrySurveyName != enbs.placeSurveyName) { - resultsPromises.push( - EnketoSurveyAnswer.filterByNameAndVersion(enbs.placeSurveyName, results), - ); - } - return Promise.all(resultsPromises); - }; + /** + * Embed 'inputType' to the timelineEntry. + */ + enbs.extractResult = function (results) { + const resultsPromises = [ + EnketoSurveyAnswer.filterByNameAndVersion(enbs.timelineEntrySurveyName, results), + ]; + if (enbs.timelineEntrySurveyName != enbs.placeSurveyName) { + resultsPromises.push( + EnketoSurveyAnswer.filterByNameAndVersion(enbs.placeSurveyName, results), + ); + } + return Promise.all(resultsPromises); + }; - enbs.processManualInputs = function (manualResults, resultMap) { - console.log('ENKETO: processManualInputs with ', manualResults, ' and ', resultMap); - const surveyResults = manualResults.flat(2); - resultMap[enbs.SINGLE_KEY] = surveyResults; - }; + enbs.processManualInputs = function (manualResults, resultMap) { + console.log('ENKETO: processManualInputs with ', manualResults, ' and ', resultMap); + const surveyResults = manualResults.flat(2); + resultMap[enbs.SINGLE_KEY] = surveyResults; + }; - enbs.populateInputsAndInferences = function (timelineEntry, manualResultMap) { - console.log( - 'ENKETO: populating timelineEntry,', - timelineEntry, - ' with result map', - manualResultMap, - ); - if (angular.isDefined(timelineEntry)) { - // initialize additions array as empty if it doesn't already exist - timelineEntry.additionsList ||= []; - enbs.populateManualInputs( - timelineEntry, - enbs.SINGLE_KEY, - manualResultMap[enbs.SINGLE_KEY], - ); - } else { - console.log('timelineEntry information not yet bound, skipping fill'); - } - }; + enbs.populateInputsAndInferences = function (timelineEntry, manualResultMap) { + console.log( + 'ENKETO: populating timelineEntry,', + timelineEntry, + ' with result map', + manualResultMap, + ); + if (angular.isDefined(timelineEntry)) { + // initialize additions array as empty if it doesn't already exist + timelineEntry.additionsList ||= []; + enbs.populateManualInputs(timelineEntry, enbs.SINGLE_KEY, manualResultMap[enbs.SINGLE_KEY]); + } else { + console.log('timelineEntry information not yet bound, skipping fill'); + } + }; - /** - * Embed 'inputType' to the timelineEntry - * This is the version that is called from the list, which focuses only on - * manual inputs. It also sets some additional values - */ - enbs.populateManualInputs = function (timelineEntry, inputType, inputList) { + /** + * Embed 'inputType' to the timelineEntry + * This is the version that is called from the list, which focuses only on + * manual inputs. It also sets some additional values + */ + enbs.populateManualInputs = function (timelineEntry, inputType, inputList) { // there is not necessarily just one addition per timeline entry, // so unlike user inputs, we don't want to replace the server entry with // the unprocessed entry @@ -92,28 +90,34 @@ angular.module('emission.survey.enketo.add-note-button', const unprocessedAdditions = getAdditionsForTimelineEntry(timelineEntry, inputList); const combinedPotentialAdditionList = timelineEntry.additions.concat(unprocessedAdditions); const dedupedList = getUniqueEntries(combinedPotentialAdditionList); - Logger.log("After combining unprocessed ("+unprocessedAdditions.length+ - ") with server ("+timelineEntry.additions.length+ - ") for a combined ("+combinedPotentialAdditionList.length+ - "), deduped entries are ("+dedupedList.length+")"); + Logger.log( + 'After combining unprocessed (' + + unprocessedAdditions.length + + ') with server (' + + timelineEntry.additions.length + + ') for a combined (' + + combinedPotentialAdditionList.length + + '), deduped entries are (' + + dedupedList.length + + ')', + ); - enbs.populateInput(timelineEntry.additionsList, inputType, dedupedList); - // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); - enbs.editingTrip = angular.undefined; - }; + enbs.populateInput(timelineEntry.additionsList, inputType, dedupedList); + // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); + enbs.editingTrip = angular.undefined; + }; - /** - * Insert the given userInputLabel into the given inputType's slot in inputField - */ - enbs.populateInput = function (timelineEntryField, inputType, userInputEntry) { - if (angular.isDefined(userInputEntry)) { - timelineEntryField.length = 0; - userInputEntry.forEach((ta) => { - timelineEntryField.push(ta); - }); - } - }; + /** + * Insert the given userInputLabel into the given inputType's slot in inputField + */ + enbs.populateInput = function (timelineEntryField, inputType, userInputEntry) { + if (angular.isDefined(userInputEntry)) { + timelineEntryField.length = 0; + userInputEntry.forEach((ta) => { + timelineEntryField.push(ta); + }); + } + }; - return enbs; - }, - ); + return enbs; + }); diff --git a/www/js/survey/enketo/enketo-trip-button.js b/www/js/survey/enketo/enketo-trip-button.js index 01761ab9d..89ae9dc29 100644 --- a/www/js/survey/enketo/enketo-trip-button.js +++ b/www/js/survey/enketo/enketo-trip-button.js @@ -14,106 +14,102 @@ import angular from 'angular'; import { getUserInputForTrip } from '../inputMatcher'; -angular.module('emission.survey.enketo.trip.button', - ['emission.survey.enketo.answer']) -.factory("EnketoTripButtonService", function(EnketoSurveyAnswer, Logger, $timeout) { - var etbs = {}; - console.log("Creating EnketoTripButtonService"); - etbs.key = "manual/trip_user_input"; - etbs.SINGLE_KEY="SURVEY"; - etbs.MANUAL_KEYS = [etbs.key]; +angular + .module('emission.survey.enketo.trip.button', ['emission.survey.enketo.answer']) + .factory('EnketoTripButtonService', function (EnketoSurveyAnswer, Logger, $timeout) { + var etbs = {}; + console.log('Creating EnketoTripButtonService'); + etbs.key = 'manual/trip_user_input'; + etbs.SINGLE_KEY = 'SURVEY'; + etbs.MANUAL_KEYS = [etbs.key]; - /** - * Embed 'inputType' to the trip. - */ - etbs.extractResult = (results) => - EnketoSurveyAnswer.filterByNameAndVersion('TripConfirmSurvey', results); + /** + * Embed 'inputType' to the trip. + */ + etbs.extractResult = (results) => + EnketoSurveyAnswer.filterByNameAndVersion('TripConfirmSurvey', results); - etbs.processManualInputs = function (manualResults, resultMap) { - if (manualResults.length > 1) { - Logger.displayError( - 'Found ' + manualResults.length + ' results expected 1', - manualResults, - ); - } else { - console.log('ENKETO: processManualInputs with ', manualResults, ' and ', resultMap); - const surveyResult = manualResults[0]; - resultMap[etbs.SINGLE_KEY] = surveyResult; - } - }; + etbs.processManualInputs = function (manualResults, resultMap) { + if (manualResults.length > 1) { + Logger.displayError('Found ' + manualResults.length + ' results expected 1', manualResults); + } else { + console.log('ENKETO: processManualInputs with ', manualResults, ' and ', resultMap); + const surveyResult = manualResults[0]; + resultMap[etbs.SINGLE_KEY] = surveyResult; + } + }; - etbs.populateInputsAndInferences = function (trip, manualResultMap) { - console.log('ENKETO: populating trip,', trip, ' with result map', manualResultMap); - if (angular.isDefined(trip)) { - // console.log("Expectation: "+JSON.stringify(trip.expectation)); - // console.log("Inferred labels from server: "+JSON.stringify(trip.inferred_labels)); - trip.userInput = {}; - etbs.populateManualInputs( - trip, - trip.getNextEntry(), - etbs.SINGLE_KEY, - manualResultMap[etbs.SINGLE_KEY], - ); - trip.finalInference = {}; - etbs.inferFinalLabels(trip); - etbs.updateVerifiability(trip); - } else { - console.log('Trip information not yet bound, skipping fill'); - } - }; + etbs.populateInputsAndInferences = function (trip, manualResultMap) { + console.log('ENKETO: populating trip,', trip, ' with result map', manualResultMap); + if (angular.isDefined(trip)) { + // console.log("Expectation: "+JSON.stringify(trip.expectation)); + // console.log("Inferred labels from server: "+JSON.stringify(trip.inferred_labels)); + trip.userInput = {}; + etbs.populateManualInputs( + trip, + trip.getNextEntry(), + etbs.SINGLE_KEY, + manualResultMap[etbs.SINGLE_KEY], + ); + trip.finalInference = {}; + etbs.inferFinalLabels(trip); + etbs.updateVerifiability(trip); + } else { + console.log('Trip information not yet bound, skipping fill'); + } + }; - /** - * Embed 'inputType' to the trip - * This is the version that is called from the list, which focuses only on - * manual inputs. It also sets some additional values - */ - etbs.populateManualInputs = function (trip, nextTrip, inputType, inputList) { + /** + * Embed 'inputType' to the trip + * This is the version that is called from the list, which focuses only on + * manual inputs. It also sets some additional values + */ + etbs.populateManualInputs = function (trip, nextTrip, inputType, inputList) { // Check unprocessed labels first since they are more recent - const unprocessedLabelEntry = getUserInputForTrip(trip, nextTrip,inputList); + const unprocessedLabelEntry = getUserInputForTrip(trip, nextTrip, inputList); var userInputEntry = unprocessedLabelEntry; if (!angular.isDefined(userInputEntry)) { - userInputEntry = trip.user_input?.[etbs.inputType2retKey(inputType)]; - } - etbs.populateInput(trip.userInput, inputType, userInputEntry); - // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); - etbs.editingTrip = angular.undefined; - }; + userInputEntry = trip.user_input?.[etbs.inputType2retKey(inputType)]; + } + etbs.populateInput(trip.userInput, inputType, userInputEntry); + // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); + etbs.editingTrip = angular.undefined; + }; - /** - * Insert the given userInputLabel into the given inputType's slot in inputField - */ - etbs.populateInput = function (tripField, inputType, userInputEntry) { - if (angular.isDefined(userInputEntry)) { - tripField[inputType] = userInputEntry; - } - }; + /** + * Insert the given userInputLabel into the given inputType's slot in inputField + */ + etbs.populateInput = function (tripField, inputType, userInputEntry) { + if (angular.isDefined(userInputEntry)) { + tripField[inputType] = userInputEntry; + } + }; - /** - * Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user. - * The algorithm below operationalizes these principles: - * - Never consider label tuples that contradict a green label - * - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before - * - After filtering, predict the most likely choices at the level of individual labels, not label tuples - * - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold - */ - etbs.inferFinalLabels = function (trip) { - // currently a NOP since we don't have any other trip properties - return; - }; + /** + * Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user. + * The algorithm below operationalizes these principles: + * - Never consider label tuples that contradict a green label + * - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before + * - After filtering, predict the most likely choices at the level of individual labels, not label tuples + * - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold + */ + etbs.inferFinalLabels = function (trip) { + // currently a NOP since we don't have any other trip properties + return; + }; - /** - * MODE (becomes manual/mode_confirm) becomes mode_confirm - */ - etbs.inputType2retKey = function (inputType) { - return etbs.key.split('/')[1]; - }; + /** + * MODE (becomes manual/mode_confirm) becomes mode_confirm + */ + etbs.inputType2retKey = function (inputType) { + return etbs.key.split('/')[1]; + }; - etbs.updateVerifiability = function (trip) { - // currently a NOP since we don't have any other trip properties - trip.verifiability = 'cannot-verify'; - return; - }; + etbs.updateVerifiability = function (trip) { + // currently a NOP since we don't have any other trip properties + trip.verifiability = 'cannot-verify'; + return; + }; - return etbs; - }, - ); + return etbs; + }); diff --git a/www/js/survey/inputMatcher.ts b/www/js/survey/inputMatcher.ts index c6c8ed61c..6203b5f27 100644 --- a/www/js/survey/inputMatcher.ts +++ b/www/js/survey/inputMatcher.ts @@ -1,17 +1,27 @@ -import { logDebug, displayErrorMsg } from "../plugin/logger" -import { DateTime } from "luxon"; -import { UserInput, Trip, TlEntry } from "../types/diaryTypes"; - -const EPOCH_MAXIMUM = 2**31 - 1; - -export const fmtTs = (ts_in_secs: number, tz: string): string | null => DateTime.fromSeconds(ts_in_secs, {zone : tz}).toISO(); - -export const printUserInput = (ui: UserInput): string => `${fmtTs(ui.data.start_ts, ui.metadata.time_zone)} (${ui.data.start_ts}) -> -${fmtTs(ui.data.end_ts, ui.metadata.time_zone)} (${ui.data.end_ts}) ${ui.data.label} logged at ${ui.metadata.write_ts}`; - -export const validUserInputForDraftTrip = (trip: Trip, userInput: UserInput, logsEnabled: boolean): boolean => { - if(logsEnabled) { - logDebug(`Draft trip: +import { logDebug, displayErrorMsg } from '../plugin/logger'; +import { DateTime } from 'luxon'; +import { UserInput, Trip, TlEntry } from '../types/diaryTypes'; + +const EPOCH_MAXIMUM = 2 ** 31 - 1; + +export const fmtTs = (ts_in_secs: number, tz: string): string | null => + DateTime.fromSeconds(ts_in_secs, { zone: tz }).toISO(); + +export const printUserInput = (ui: UserInput): string => `${fmtTs( + ui.data.start_ts, + ui.metadata.time_zone, +)} (${ui.data.start_ts}) -> +${fmtTs(ui.data.end_ts, ui.metadata.time_zone)} (${ui.data.end_ts}) ${ui.data.label} logged at ${ + ui.metadata.write_ts +}`; + +export const validUserInputForDraftTrip = ( + trip: Trip, + userInput: UserInput, + logsEnabled: boolean, +): boolean => { + if (logsEnabled) { + logDebug(`Draft trip: comparing user = ${fmtTs(userInput.data.start_ts, userInput.metadata.time_zone)} -> ${fmtTs(userInput.data.end_ts, userInput.metadata.time_zone)} trip = ${fmtTs(trip.start_ts, userInput.metadata.time_zone)} @@ -21,41 +31,49 @@ export const validUserInputForDraftTrip = (trip: Trip, userInput: UserInput, log || ${-(userInput.data.start_ts - trip.start_ts) <= 15 * 60}) && ${userInput.data.end_ts <= trip.end_ts} `); - } + } + + return ( + ((userInput.data.start_ts >= trip.start_ts && userInput.data.start_ts < trip.end_ts) || + -(userInput.data.start_ts - trip.start_ts) <= 15 * 60) && + userInput.data.end_ts <= trip.end_ts + ); +}; + +export const validUserInputForTimelineEntry = ( + tlEntry: TlEntry, + userInput: UserInput, + logsEnabled: boolean, +): boolean => { + if (!tlEntry.origin_key) return false; + if (tlEntry.origin_key.includes('UNPROCESSED')) + return validUserInputForDraftTrip(tlEntry, userInput, logsEnabled); + + /* Place-level inputs always have a key starting with 'manual/place', and + trip-level inputs never have a key starting with 'manual/place' + So if these don't match, we can immediately return false */ + const entryIsPlace = tlEntry.origin_key === 'analysis/confirmed_place'; + const isPlaceInput = (userInput.key || userInput.metadata.key).startsWith('manual/place'); - return (userInput.data.start_ts >= trip.start_ts && userInput.data.start_ts < trip.end_ts - || -(userInput.data.start_ts - trip.start_ts) <= 15 * 60) && userInput.data.end_ts <= trip.end_ts; -} + if (entryIsPlace !== isPlaceInput) return false; -export const validUserInputForTimelineEntry = (tlEntry: TlEntry, userInput: UserInput, logsEnabled: boolean): boolean => { - if (!tlEntry.origin_key) return false; - if (tlEntry.origin_key.includes('UNPROCESSED')) return validUserInputForDraftTrip(tlEntry, userInput, logsEnabled); + let entryStart = tlEntry.start_ts || tlEntry.enter_ts; + let entryEnd = tlEntry.end_ts || tlEntry.exit_ts; - /* Place-level inputs always have a key starting with 'manual/place', and - trip-level inputs never have a key starting with 'manual/place' - So if these don't match, we can immediately return false */ - const entryIsPlace = tlEntry.origin_key === 'analysis/confirmed_place'; - const isPlaceInput = (userInput.key || userInput.metadata.key).startsWith('manual/place'); - - if (entryIsPlace !== isPlaceInput) return false; - - let entryStart = tlEntry.start_ts || tlEntry.enter_ts; - let entryEnd = tlEntry.end_ts || tlEntry.exit_ts; - - if (!entryStart && entryEnd) { - /* if a place has no enter time, this is the first start_place of the first composite trip object + if (!entryStart && entryEnd) { + /* if a place has no enter time, this is the first start_place of the first composite trip object so we will set the start time to the start of the day of the end time for the purpose of comparison */ - entryStart = DateTime.fromSeconds(entryEnd).startOf('day').toUnixInteger(); - } + entryStart = DateTime.fromSeconds(entryEnd).startOf('day').toUnixInteger(); + } - if (!entryEnd) { - /* if a place has no exit time, the user hasn't left there yet + if (!entryEnd) { + /* if a place has no exit time, the user hasn't left there yet so we will set the end time as high as possible for the purpose of comparison */ - entryEnd = EPOCH_MAXIMUM; - } - - if (logsEnabled) { - logDebug(`Cleaned trip: + entryEnd = EPOCH_MAXIMUM; + } + + if (logsEnabled) { + logDebug(`Cleaned trip: comparing user = ${fmtTs(userInput.data.start_ts, userInput.metadata.time_zone)} -> ${fmtTs(userInput.data.end_ts, userInput.metadata.time_zone)} trip = ${fmtTs(entryStart, userInput.metadata.time_zone)} @@ -65,131 +83,168 @@ export const validUserInputForTimelineEntry = (tlEntry: TlEntry, userInput: User end checks are ${userInput.data.end_ts <= entryEnd} || ${userInput.data.end_ts - entryEnd <= 15 * 60}) `); - } + } - /* For this input to match, it must begin after the start of the timelineEntry (inclusive) + /* For this input to match, it must begin after the start of the timelineEntry (inclusive) but before the end of the timelineEntry (exclusive) */ - const startChecks = userInput.data.start_ts >= entryStart && userInput.data.start_ts < entryEnd; - /* A matching user input must also finish before the end of the timelineEntry, + const startChecks = userInput.data.start_ts >= entryStart && userInput.data.start_ts < entryEnd; + /* A matching user input must also finish before the end of the timelineEntry, or within 15 minutes. */ - let endChecks = (userInput.data.end_ts <= entryEnd || (userInput.data.end_ts - entryEnd) <= 15 * 60); - - if (startChecks && !endChecks) { - const nextEntryObj = tlEntry.getNextEntry(); - if (nextEntryObj) { - const nextEntryEnd = nextEntryObj.end_ts || nextEntryObj.exit_ts; - if (!nextEntryEnd) { // the last place will not have an exit_ts - endChecks = true; // so we will just skip the end check - } else { - endChecks = userInput.data.end_ts <= nextEntryEnd; - logDebug(`Second level of end checks when the next trip is defined(${userInput.data.end_ts} <= ${nextEntryEnd}) ${endChecks}`); - } - } else { - // next trip is not defined, last trip - endChecks = (userInput.data.end_local_dt.day == userInput.data.start_local_dt.day) - logDebug("Second level of end checks for the last trip of the day"); - logDebug(`compare ${userInput.data.end_local_dt.day} with ${userInput.data.start_local_dt.day} ${endChecks}`); - } - if (endChecks) { - // If we have flipped the values, check to see that there is sufficient overlap - const overlapDuration = Math.min(userInput.data.end_ts, entryEnd) - Math.max(userInput.data.start_ts, entryStart) - logDebug(`Flipped endCheck, overlap(${overlapDuration})/trip(${tlEntry.duration} (${overlapDuration} / ${tlEntry.duration})`); - endChecks = (overlapDuration/tlEntry.duration) > 0.5; - } + let endChecks = userInput.data.end_ts <= entryEnd || userInput.data.end_ts - entryEnd <= 15 * 60; + + if (startChecks && !endChecks) { + const nextEntryObj = tlEntry.getNextEntry(); + if (nextEntryObj) { + const nextEntryEnd = nextEntryObj.end_ts || nextEntryObj.exit_ts; + if (!nextEntryEnd) { + // the last place will not have an exit_ts + endChecks = true; // so we will just skip the end check + } else { + endChecks = userInput.data.end_ts <= nextEntryEnd; + logDebug( + `Second level of end checks when the next trip is defined(${userInput.data.end_ts} <= ${nextEntryEnd}) ${endChecks}`, + ); + } + } else { + // next trip is not defined, last trip + endChecks = userInput.data.end_local_dt.day == userInput.data.start_local_dt.day; + logDebug('Second level of end checks for the last trip of the day'); + logDebug( + `compare ${userInput.data.end_local_dt.day} with ${userInput.data.start_local_dt.day} ${endChecks}`, + ); + } + if (endChecks) { + // If we have flipped the values, check to see that there is sufficient overlap + const overlapDuration = + Math.min(userInput.data.end_ts, entryEnd) - Math.max(userInput.data.start_ts, entryStart); + logDebug( + `Flipped endCheck, overlap(${overlapDuration})/trip(${tlEntry.duration} (${overlapDuration} / ${tlEntry.duration})`, + ); + endChecks = overlapDuration / tlEntry.duration > 0.5; } - return startChecks && endChecks; -} + } + return startChecks && endChecks; +}; // parallels get_not_deleted_candidates() in trip_queries.py export const getNotDeletedCandidates = (candidates: UserInput[]): UserInput[] => { - console.log('getNotDeletedCandidates called with ' + candidates.length + ' candidates'); - - // We want to retain all ACTIVE entries that have not been DELETED - const allActiveList = candidates.filter(c => !c.data.status || c.data.status == 'ACTIVE'); - const allDeletedIds = candidates.filter(c => c.data.status && c.data.status == 'DELETED').map(c => c.data['match_id']); - const notDeletedActive = allActiveList.filter(c => !allDeletedIds.includes(c.data['match_id'])); - - console.log(`Found ${allActiveList.length} active entries, ${allDeletedIds.length} deleted entries -> - ${notDeletedActive.length} non deleted active entries`); - - return notDeletedActive; -} - -export const getUserInputForTrip = (trip: TlEntry, nextTrip: any, userInputList: UserInput[]): undefined | UserInput => { - const logsEnabled = userInputList?.length < 20; - if (userInputList === undefined) { - logDebug("In getUserInputForTrip, no user input, returning undefined"); - return undefined; - } - - if (logsEnabled) console.log(`Input list = ${userInputList.map(printUserInput)}`); - - // undefined !== true, so this covers the label view case as well - const potentialCandidates = userInputList.filter((ui) => validUserInputForTimelineEntry(trip, ui, logsEnabled)); - - if (potentialCandidates.length === 0) { - if (logsEnabled) logDebug("In getUserInputForTripStartEnd, no potential candidates, returning []"); - return undefined; - } + console.log('getNotDeletedCandidates called with ' + candidates.length + ' candidates'); - if (potentialCandidates.length === 1) { - logDebug(`In getUserInputForTripStartEnd, one potential candidate, returning ${printUserInput(potentialCandidates[0])}`); - return potentialCandidates[0]; - } + // We want to retain all ACTIVE entries that have not been DELETED + const allActiveList = candidates.filter((c) => !c.data.status || c.data.status == 'ACTIVE'); + const allDeletedIds = candidates + .filter((c) => c.data.status && c.data.status == 'DELETED') + .map((c) => c.data['match_id']); + const notDeletedActive = allActiveList.filter((c) => !allDeletedIds.includes(c.data['match_id'])); - logDebug(`potentialCandidates are ${potentialCandidates.map(printUserInput)}`); + console.log(`Found ${allActiveList.length} active entries, ${allDeletedIds.length} deleted entries -> + ${notDeletedActive.length} non deleted active entries`); - const sortedPC = potentialCandidates.sort((pc1, pc2) => pc2.metadata.write_ts - pc1.metadata.write_ts); - const mostRecentEntry = sortedPC[0]; - logDebug("Returning mostRecentEntry "+printUserInput(mostRecentEntry)); - - return mostRecentEntry; -} + return notDeletedActive; +}; + +export const getUserInputForTrip = ( + trip: TlEntry, + nextTrip: any, + userInputList: UserInput[], +): undefined | UserInput => { + const logsEnabled = userInputList?.length < 20; + if (userInputList === undefined) { + logDebug('In getUserInputForTrip, no user input, returning undefined'); + return undefined; + } + + if (logsEnabled) console.log(`Input list = ${userInputList.map(printUserInput)}`); + + // undefined !== true, so this covers the label view case as well + const potentialCandidates = userInputList.filter((ui) => + validUserInputForTimelineEntry(trip, ui, logsEnabled), + ); + + if (potentialCandidates.length === 0) { + if (logsEnabled) + logDebug('In getUserInputForTripStartEnd, no potential candidates, returning []'); + return undefined; + } + + if (potentialCandidates.length === 1) { + logDebug( + `In getUserInputForTripStartEnd, one potential candidate, returning ${printUserInput( + potentialCandidates[0], + )}`, + ); + return potentialCandidates[0]; + } + + logDebug(`potentialCandidates are ${potentialCandidates.map(printUserInput)}`); + + const sortedPC = potentialCandidates.sort( + (pc1, pc2) => pc2.metadata.write_ts - pc1.metadata.write_ts, + ); + const mostRecentEntry = sortedPC[0]; + logDebug('Returning mostRecentEntry ' + printUserInput(mostRecentEntry)); + + return mostRecentEntry; +}; // return array of matching additions for a trip or place -export const getAdditionsForTimelineEntry = (entry: TlEntry, additionsList: UserInput[]): UserInput[] => { - const logsEnabled = additionsList?.length < 20; +export const getAdditionsForTimelineEntry = ( + entry: TlEntry, + additionsList: UserInput[], +): UserInput[] => { + const logsEnabled = additionsList?.length < 20; - if (additionsList === undefined) { - logDebug("In getAdditionsForTimelineEntry, no addition input, returning []"); - return []; - } + if (additionsList === undefined) { + logDebug('In getAdditionsForTimelineEntry, no addition input, returning []'); + return []; + } - // get additions that have not been deleted and filter out additions that do not start within the bounds of the timeline entry - const notDeleted = getNotDeletedCandidates(additionsList); - const matchingAdditions = notDeleted.filter((ui) => validUserInputForTimelineEntry(entry, ui, logsEnabled)); + // get additions that have not been deleted and filter out additions that do not start within the bounds of the timeline entry + const notDeleted = getNotDeletedCandidates(additionsList); + const matchingAdditions = notDeleted.filter((ui) => + validUserInputForTimelineEntry(entry, ui, logsEnabled), + ); - if (logsEnabled) console.log(`Matching Addition list ${matchingAdditions.map(printUserInput)}`); + if (logsEnabled) console.log(`Matching Addition list ${matchingAdditions.map(printUserInput)}`); - return matchingAdditions; -} + return matchingAdditions; +}; export const getUniqueEntries = (combinedList) => { - /* we should not get any non-ACTIVE entries here + /* we should not get any non-ACTIVE entries here since we have run filtering algorithms on both the phone and the server */ - const allDeleted = combinedList.filter(c => c.data.status && c.data.status == 'DELETED'); - - if (allDeleted.length > 0) { - displayErrorMsg("Found "+allDeleted.length +" non-ACTIVE addition entries while trying to dedup entries", JSON.stringify(allDeleted)); - } - - const uniqueMap = new Map(); - combinedList.forEach((e) => { - const existingVal = uniqueMap.get(e.data.match_id); - /* if the existing entry and the input entry don't match and they are both active, we have an error + const allDeleted = combinedList.filter((c) => c.data.status && c.data.status == 'DELETED'); + + if (allDeleted.length > 0) { + displayErrorMsg( + 'Found ' + allDeleted.length + ' non-ACTIVE addition entries while trying to dedup entries', + JSON.stringify(allDeleted), + ); + } + + const uniqueMap = new Map(); + combinedList.forEach((e) => { + const existingVal = uniqueMap.get(e.data.match_id); + /* if the existing entry and the input entry don't match and they are both active, we have an error let's notify the user for now */ - if (existingVal) { - if ((existingVal.data.start_ts != e.data.start_ts) || - (existingVal.data.end_ts != e.data.end_ts) || - (existingVal.data.write_ts != e.data.write_ts)) { - displayErrorMsg(`Found two ACTIVE entries with the same match ID but different timestamps ${existingVal.data.match_id}` - , `${JSON.stringify(existingVal)} vs ${JSON.stringify(e)}`); - } else { - console.log(`Found two entries with match_id ${existingVal.data.match_id} but they are identical`); - } - } else { - uniqueMap.set(e.data.match_id, e); - } - }); - return Array.from(uniqueMap.values()); -} + if (existingVal) { + if ( + existingVal.data.start_ts != e.data.start_ts || + existingVal.data.end_ts != e.data.end_ts || + existingVal.data.write_ts != e.data.write_ts + ) { + displayErrorMsg( + `Found two ACTIVE entries with the same match ID but different timestamps ${existingVal.data.match_id}`, + `${JSON.stringify(existingVal)} vs ${JSON.stringify(e)}`, + ); + } else { + console.log( + `Found two entries with match_id ${existingVal.data.match_id} but they are identical`, + ); + } + } else { + uniqueMap.set(e.data.match_id, e); + } + }); + return Array.from(uniqueMap.values()); +}; diff --git a/www/js/survey/multilabel/multi-label-ui.js b/www/js/survey/multilabel/multi-label-ui.js index 904a77b68..8123272e4 100644 --- a/www/js/survey/multilabel/multi-label-ui.js +++ b/www/js/survey/multilabel/multi-label-ui.js @@ -10,212 +10,209 @@ import { import { getConfig } from '../../config/dynamicConfig'; import { getUserInputForTrip } from '../inputMatcher'; -angular.module('emission.survey.multilabel.buttons', []) - -.factory("MultiLabelService", function($rootScope, $timeout, $ionicPlatform, Logger) { - var mls = {}; - console.log("Creating MultiLabelService"); - mls.init = function(config) { - Logger.log("About to initialize the MultiLabelService"); +angular + .module('emission.survey.multilabel.buttons', []) + + .factory('MultiLabelService', function ($rootScope, $timeout, $ionicPlatform, Logger) { + var mls = {}; + console.log('Creating MultiLabelService'); + mls.init = function (config) { + Logger.log('About to initialize the MultiLabelService'); mls.ui_config = config; - getLabelOptions(config).then((inputParams) => mls.inputParams = inputParams); + getLabelOptions(config).then((inputParams) => (mls.inputParams = inputParams)); mls.MANUAL_KEYS = Object.values(getLabelInputDetails(config)).map((inp) => inp.key); - Logger.log("finished initializing the MultiLabelService"); - }; - - $ionicPlatform.ready().then(function () { - Logger.log('UI_CONFIG: about to call configReady function in MultiLabelService'); - getConfig() - .then((newConfig) => { - mls.init(newConfig); - }) - .catch((err) => - Logger.displayError('Error while handling config in MultiLabelService', err), - ); - }); - - /** - * Embed 'inputType' to the trip. - */ - - mls.extractResult = (results) => results; - - mls.processManualInputs = function (manualResults, resultMap) { - var mrString = - 'unprocessed manual inputs ' + - manualResults.map(function (item, index) { - return ` ${item.length} ${getLabelInputs()[index]}`; - }); - console.log(mrString); - manualResults.forEach(function (mr, index) { - resultMap[getLabelInputs()[index]] = mr; + Logger.log('finished initializing the MultiLabelService'); + }; + + $ionicPlatform.ready().then(function () { + Logger.log('UI_CONFIG: about to call configReady function in MultiLabelService'); + getConfig() + .then((newConfig) => { + mls.init(newConfig); + }) + .catch((err) => + Logger.displayError('Error while handling config in MultiLabelService', err), + ); + }); + + /** + * Embed 'inputType' to the trip. + */ + + mls.extractResult = (results) => results; + + mls.processManualInputs = function (manualResults, resultMap) { + var mrString = + 'unprocessed manual inputs ' + + manualResults.map(function (item, index) { + return ` ${item.length} ${getLabelInputs()[index]}`; }); - }; - - /** - * Embed 'inputType' to the trip - * This is the version that is called from the list, which focuses only on - * manual inputs. It also sets some additional values - */ - mls.populateManualInputs = function (trip, nextTrip, inputType, inputList) { + console.log(mrString); + manualResults.forEach(function (mr, index) { + resultMap[getLabelInputs()[index]] = mr; + }); + }; + + /** + * Embed 'inputType' to the trip + * This is the version that is called from the list, which focuses only on + * manual inputs. It also sets some additional values + */ + mls.populateManualInputs = function (trip, nextTrip, inputType, inputList) { // Check unprocessed labels first since they are more recent const unprocessedLabelEntry = getUserInputForTrip(trip, nextTrip, inputList); - var userInputLabel = unprocessedLabelEntry? unprocessedLabelEntry.data.label : undefined; + var userInputLabel = unprocessedLabelEntry ? unprocessedLabelEntry.data.label : undefined; if (!angular.isDefined(userInputLabel)) { - userInputLabel = trip.user_input?.[mls.inputType2retKey(inputType)]; - } - mls.populateInput(trip.userInput, inputType, userInputLabel); - // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); - mls.editingTrip = angular.undefined; - }; - - /** - * Insert the given userInputLabel into the given inputType's slot in inputField - */ - mls.populateInput = function (tripField, inputType, userInputLabel) { - if (angular.isDefined(userInputLabel)) { - console.log( - 'populateInput: looking in map of ' + - inputType + - ' for userInputLabel' + - userInputLabel, - ); - var userInputEntry = mls.inputParams[inputType].find((o) => o.value == userInputLabel); - if (!angular.isDefined(userInputEntry)) { - userInputEntry = getFakeEntry(userInputLabel); - mls.inputParams[inputType].push(userInputEntry); - } - console.log( - 'Mapped label ' + userInputLabel + ' to entry ' + JSON.stringify(userInputEntry), - ); - tripField[inputType] = userInputEntry; + userInputLabel = trip.user_input?.[mls.inputType2retKey(inputType)]; + } + mls.populateInput(trip.userInput, inputType, userInputLabel); + // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); + mls.editingTrip = angular.undefined; + }; + + /** + * Insert the given userInputLabel into the given inputType's slot in inputField + */ + mls.populateInput = function (tripField, inputType, userInputLabel) { + if (angular.isDefined(userInputLabel)) { + console.log( + 'populateInput: looking in map of ' + inputType + ' for userInputLabel' + userInputLabel, + ); + var userInputEntry = mls.inputParams[inputType].find((o) => o.value == userInputLabel); + if (!angular.isDefined(userInputEntry)) { + userInputEntry = getFakeEntry(userInputLabel); + mls.inputParams[inputType].push(userInputEntry); } - }; - - /** - * Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user. - * The algorithm below operationalizes these principles: - * - Never consider label tuples that contradict a green label - * - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before - * - After filtering, predict the most likely choices at the level of individual labels, not label tuples - * - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold - */ - mls.inferFinalLabels = function (trip) { - // Deep copy the possibility tuples - let labelsList = []; - if (angular.isDefined(trip.inferred_labels)) { - labelsList = JSON.parse(JSON.stringify(trip.inferred_labels)); + console.log( + 'Mapped label ' + userInputLabel + ' to entry ' + JSON.stringify(userInputEntry), + ); + tripField[inputType] = userInputEntry; + } + }; + + /** + * Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user. + * The algorithm below operationalizes these principles: + * - Never consider label tuples that contradict a green label + * - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before + * - After filtering, predict the most likely choices at the level of individual labels, not label tuples + * - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold + */ + mls.inferFinalLabels = function (trip) { + // Deep copy the possibility tuples + let labelsList = []; + if (angular.isDefined(trip.inferred_labels)) { + labelsList = JSON.parse(JSON.stringify(trip.inferred_labels)); + } + + // Capture the level of certainty so we can reconstruct it later + const totalCertainty = labelsList + .map((item) => item.p) + .reduce((item, rest) => item + rest, 0); + + // Filter out the tuples that are inconsistent with existing green labels + for (const inputType of getLabelInputs()) { + const userInput = trip.userInput[inputType]; + if (userInput) { + const retKey = mls.inputType2retKey(inputType); + labelsList = labelsList.filter((item) => item.labels[retKey] == userInput.value); } + } + + // Red labels if we have no possibilities left + if (labelsList.length == 0) { + for (const inputType of getLabelInputs()) + mls.populateInput(trip.finalInference, inputType, undefined); + } else { + // Normalize probabilities to previous level of certainty + const certaintyScalar = + totalCertainty / labelsList.map((item) => item.p).reduce((item, rest) => item + rest); + labelsList.forEach((item) => (item.p *= certaintyScalar)); - // Capture the level of certainty so we can reconstruct it later - const totalCertainty = labelsList - .map((item) => item.p) - .reduce((item, rest) => item + rest, 0); - - // Filter out the tuples that are inconsistent with existing green labels for (const inputType of getLabelInputs()) { - const userInput = trip.userInput[inputType]; - if (userInput) { - const retKey = mls.inputType2retKey(inputType); - labelsList = labelsList.filter((item) => item.labels[retKey] == userInput.value); + // For each label type, find the most probable value by binning by label value and summing + const retKey = mls.inputType2retKey(inputType); + let valueProbs = new Map(); + for (const tuple of labelsList) { + const labelValue = tuple.labels[retKey]; + if (!valueProbs.has(labelValue)) valueProbs.set(labelValue, 0); + valueProbs.set(labelValue, valueProbs.get(labelValue) + tuple.p); } - } - - // Red labels if we have no possibilities left - if (labelsList.length == 0) { - for (const inputType of getLabelInputs()) - mls.populateInput(trip.finalInference, inputType, undefined); - } else { - // Normalize probabilities to previous level of certainty - const certaintyScalar = - totalCertainty / labelsList.map((item) => item.p).reduce((item, rest) => item + rest); - labelsList.forEach((item) => (item.p *= certaintyScalar)); - - for (const inputType of getLabelInputs()) { - // For each label type, find the most probable value by binning by label value and summing - const retKey = mls.inputType2retKey(inputType); - let valueProbs = new Map(); - for (const tuple of labelsList) { - const labelValue = tuple.labels[retKey]; - if (!valueProbs.has(labelValue)) valueProbs.set(labelValue, 0); - valueProbs.set(labelValue, valueProbs.get(labelValue) + tuple.p); - } - let max = { p: 0, labelValue: undefined }; - for (const [thisLabelValue, thisP] of valueProbs) { - // In the case of a tie, keep the label with earlier first appearance in the labelsList (we used a Map to preserve this order) - if (thisP > max.p) max = { p: thisP, labelValue: thisLabelValue }; - } - - // Display a label as red if its most probable inferred value has a probability less than or equal to the trip's confidence_threshold - // Fails safe if confidence_threshold doesn't exist - if (max.p <= trip.confidence_threshold) max.labelValue = undefined; - - mls.populateInput(trip.finalInference, inputType, max.labelValue); + let max = { p: 0, labelValue: undefined }; + for (const [thisLabelValue, thisP] of valueProbs) { + // In the case of a tie, keep the label with earlier first appearance in the labelsList (we used a Map to preserve this order) + if (thisP > max.p) max = { p: thisP, labelValue: thisLabelValue }; } + + // Display a label as red if its most probable inferred value has a probability less than or equal to the trip's confidence_threshold + // Fails safe if confidence_threshold doesn't exist + if (max.p <= trip.confidence_threshold) max.labelValue = undefined; + + mls.populateInput(trip.finalInference, inputType, max.labelValue); } - }; - - /* - * Uses either 2 or 3 labels depending on the type of install (program vs. study) - * and the primary mode. - * This used to be in the controller, where it really should be, but we had - * to move it to the service because we need to invoke it from the list view - * as part of filtering "To Label" entries. - * - * TODO: Move it back later after the diary vs. label unification - */ - mls.expandInputsIfNecessary = function (trip) { - console.log('Reading expanding inputs for ', trip); - const inputValue = trip.userInput['MODE'] ? trip.userInput['MODE'].value : undefined; - console.log('Experimenting with expanding inputs for mode ' + inputValue); - if (mls.ui_config.intro.mode_studied) { - if (inputValue == mls.ui_config.intro.mode_studied) { - Logger.log( - 'Found ' + - mls.ui_config.intro.mode_studied + - ' mode in a program, displaying full details', - ); - trip.inputDetails = getLabelInputDetails(); - trip.INPUTS = getLabelInputs(); - } else { - Logger.log( - 'Found non ' + - mls.ui_config.intro.mode_studied + - ' mode in a program, displaying base details', - ); - trip.inputDetails = baseLabelInputDetails; - trip.INPUTS = getBaseLabelInputs(); - } - } else { - Logger.log('study, not program, displaying full details'); - trip.INPUTS = getLabelInputs(); + } + }; + + /* + * Uses either 2 or 3 labels depending on the type of install (program vs. study) + * and the primary mode. + * This used to be in the controller, where it really should be, but we had + * to move it to the service because we need to invoke it from the list view + * as part of filtering "To Label" entries. + * + * TODO: Move it back later after the diary vs. label unification + */ + mls.expandInputsIfNecessary = function (trip) { + console.log('Reading expanding inputs for ', trip); + const inputValue = trip.userInput['MODE'] ? trip.userInput['MODE'].value : undefined; + console.log('Experimenting with expanding inputs for mode ' + inputValue); + if (mls.ui_config.intro.mode_studied) { + if (inputValue == mls.ui_config.intro.mode_studied) { + Logger.log( + 'Found ' + + mls.ui_config.intro.mode_studied + + ' mode in a program, displaying full details', + ); trip.inputDetails = getLabelInputDetails(); + trip.INPUTS = getLabelInputs(); + } else { + Logger.log( + 'Found non ' + + mls.ui_config.intro.mode_studied + + ' mode in a program, displaying base details', + ); + trip.inputDetails = baseLabelInputDetails; + trip.INPUTS = getBaseLabelInputs(); } - }; - - /** - * MODE (becomes manual/mode_confirm) becomes mode_confirm - */ - mls.inputType2retKey = function (inputType) { - return getLabelInputDetails()[inputType].key.split('/')[1]; - }; - - mls.updateVerifiability = function (trip) { - var allGreen = true; - var someYellow = false; - for (const inputType of trip.INPUTS) { - const green = trip.userInput[inputType]; - const yellow = trip.finalInference[inputType] && !green; - if (yellow) someYellow = true; - if (!green) allGreen = false; - } - trip.verifiability = someYellow - ? 'can-verify' - : allGreen - ? 'already-verified' - : 'cannot-verify'; - }; - - return mls; - }, - ); + } else { + Logger.log('study, not program, displaying full details'); + trip.INPUTS = getLabelInputs(); + trip.inputDetails = getLabelInputDetails(); + } + }; + + /** + * MODE (becomes manual/mode_confirm) becomes mode_confirm + */ + mls.inputType2retKey = function (inputType) { + return getLabelInputDetails()[inputType].key.split('/')[1]; + }; + + mls.updateVerifiability = function (trip) { + var allGreen = true; + var someYellow = false; + for (const inputType of trip.INPUTS) { + const green = trip.userInput[inputType]; + const yellow = trip.finalInference[inputType] && !green; + if (yellow) someYellow = true; + if (!green) allGreen = false; + } + trip.verifiability = someYellow + ? 'can-verify' + : allGreen + ? 'already-verified' + : 'cannot-verify'; + }; + + return mls; + }); diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 6c0417d2c..1e71d1cd9 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -10,39 +10,39 @@ 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: 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; +}; /* 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. */ @@ -63,58 +63,58 @@ export type DerivedProperties = { 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?: UserInput, - verifiability?: string, -} + additionsList?: any[]; // TODO + finalInference?: any; // TODO + geojson?: any; // TODO + getNextEntry?: () => PopulatedTrip | ConfirmedPlace; + userInput?: UserInput; + verifiability?: string; +}; export type UserInput = { data: { - 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; + }; metadata: { - time_zone: string, - plugin: string, - write_ts: number, - platform: string, - read_ts: number, - key: string, - }, - key?: string -} + time_zone: string; + plugin: string; + write_ts: number; + platform: string; + read_ts: number; + key: string; + }; + key?: string; +}; 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 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, -} + key: string; + origin_key: string; + start_ts: number; + end_ts: number; + enter_ts: number; + exit_ts: number; + duration: number; + getNextEntry?: () => PopulatedTrip | ConfirmedPlace; +}; From 3a5a0f664de320af821b61f9c3037aa227357b7c Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Fri, 3 Nov 2023 13:05:58 -0700 Subject: [PATCH 15/21] Add html files in prettierignore --- .prettierignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.prettierignore b/.prettierignore index be7b1726d..988aead62 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,6 +3,9 @@ www/dist www/manual_lib www/json +# Ignore all HTML files: +**/*.html + # This is the pattern to check only www directory # Ignore all /* From b8cb34a892cbb3e48d6bb29899a5d78214343db0 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Fri, 3 Nov 2023 13:10:39 -0700 Subject: [PATCH 16/21] Revert html file before changing formatting --- 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 4c2bbf0d32736b80b935ed86dc3f4cc4bc78d593 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 9 Nov 2023 11:16:21 -0500 Subject: [PATCH 17/21] add ErrorBoundary to each tab --- www/js/App.tsx | 7 ++++--- www/js/plugin/ErrorBoundary.tsx | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 www/js/plugin/ErrorBoundary.tsx diff --git a/www/js/App.tsx b/www/js/App.tsx index ab4caebf7..54b677add 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -15,6 +15,7 @@ import { import { setServerConnSettings } from './config/serverConn'; import AppStatusModal from './control/AppStatusModal'; import usePermissionStatus from './usePermissionStatus'; +import { withErrorBoundary } from './plugin/ErrorBoundary'; const defaultRoutes = (t) => [ { @@ -55,9 +56,9 @@ const App = () => { }, [appConfig, t]); const renderScene = BottomNavigation.SceneMap({ - label: LabelTab, - metrics: MetricsTab, - control: ProfileSettings, + label: withErrorBoundary(LabelTab), + metrics: withErrorBoundary(MetricsTab), + control: withErrorBoundary(ProfileSettings), }); const refreshOnboardingState = () => getPendingOnboardingState().then(setOnboardingState); diff --git a/www/js/plugin/ErrorBoundary.tsx b/www/js/plugin/ErrorBoundary.tsx new file mode 100644 index 000000000..61fdf023e --- /dev/null +++ b/www/js/plugin/ErrorBoundary.tsx @@ -0,0 +1,36 @@ +// based on https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary + +import React from 'react'; +import { displayError } from './logger'; +import { Icon } from '../components/Icon'; + +class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + return { hasError: true }; + } + + componentDidCatch(error, info) { + displayError(error, info.componentStack); + } + + render() { + if (this.state.hasError) { + return ; + } + + return this.props.children; + } +} + +export const withErrorBoundary = (Component) => (props) => ( + + + +); + +export default ErrorBoundary; From 0be22344380c41dffd1ea45de0a08e20d1ff6b32 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 9 Nov 2023 13:35:05 -0500 Subject: [PATCH 18/21] add logDebug statements in Chart.tsx This may help with debugging https://github.com/e-mission/e-mission-docs/issues/986#issuecomment-1799229701 --- www/js/components/Chart.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index d7687e424..4ebf49c24 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -5,6 +5,7 @@ import { Chart as ChartJS, registerables } from 'chart.js'; import { Chart as ChartJSChart } from 'react-chartjs-2'; import Annotation, { AnnotationOptions, LabelPosition } from 'chartjs-plugin-annotation'; import { dedupColors, getChartHeight, darkenOrLighten } from './charting'; +import { logDebug } from '../plugin/logger'; ChartJS.register(...registerables, Annotation); @@ -134,6 +135,9 @@ const Chart = ({ ? {} : { callback: (value, i) => { + logDebug(`Horizontal axis callback: i = ${i}; + chartDatasets = ${JSON.stringify(chartDatasets)}; + chartDatasets[0].data = ${JSON.stringify(chartDatasets[0].data)}`); const label = chartDatasets[0].data[i].y; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); @@ -168,7 +172,9 @@ const Chart = ({ ? {} : { callback: (value, i) => { - console.log('testing vertical', chartData, i); + logDebug(`Vertical axis callback: i = ${i}; + chartDatasets = ${JSON.stringify(chartDatasets)}; + chartDatasets[0].data = ${JSON.stringify(chartDatasets[0].data)}`); const label = chartDatasets[0].data[i].x; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); From 1388eee2efdfbefa8dd14399665e74d02047ebcb Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 9 Nov 2023 13:40:34 -0500 Subject: [PATCH 19/21] add more logging in LabelTab -added several try/catches to cover all top-level calls -added logDebug statements throughout, using `` for multi-line so they are not ugly -replaced any uses of the old Logger service --- www/js/diary/LabelTab.tsx | 201 ++++++++++++++++++++++---------------- 1 file changed, 115 insertions(+), 86 deletions(-) diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 8b6e65d52..d5fa8a7c5 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -25,7 +25,7 @@ import { import { fillLocationNamesOfTrip, resetNominatimLimiter } from './addressNamesHelper'; import { SurveyOptions } from '../survey/survey'; import { getLabelOptions } from '../survey/multilabel/confirmHelper'; -import { displayError } from '../plugin/logger'; +import { displayError, displayErrorMsg, logDebug, logWarn } from '../plugin/logger'; import { useTheme } from 'react-native-paper'; import { getPipelineRangeTs } from '../commHelper'; @@ -58,55 +58,66 @@ const LabelTab = () => { // initialization, once the appConfig is loaded useEffect(() => { - if (!appConfig) return; - const surveyOptKey = appConfig.survey_info['trip-labels']; - const surveyOpt = SurveyOptions[surveyOptKey]; - setSurveyOpt(surveyOpt); - showPlaces = appConfig.survey_info?.buttons?.['place-notes']; - getLabelOptions(appConfig).then((labelOptions) => setLabelOptions(labelOptions)); - labelPopulateFactory = getAngularService(surveyOpt.service); - const tripSurveyName = appConfig.survey_info?.buttons?.['trip-notes']?.surveyName; - const placeSurveyName = appConfig.survey_info?.buttons?.['place-notes']?.surveyName; - enbs.initConfig(tripSurveyName, placeSurveyName); + try { + if (!appConfig) return; + const surveyOptKey = appConfig.survey_info['trip-labels']; + const surveyOpt = SurveyOptions[surveyOptKey]; + setSurveyOpt(surveyOpt); + showPlaces = appConfig.survey_info?.buttons?.['place-notes']; + getLabelOptions(appConfig).then((labelOptions) => setLabelOptions(labelOptions)); + labelPopulateFactory = getAngularService(surveyOpt.service); + const tripSurveyName = appConfig.survey_info?.buttons?.['trip-notes']?.surveyName; + const placeSurveyName = appConfig.survey_info?.buttons?.['place-notes']?.surveyName; + enbs.initConfig(tripSurveyName, placeSurveyName); - // we will show filters if 'additions' are not configured - // https://github.com/e-mission/e-mission-docs/issues/894 - if (appConfig.survey_info?.buttons == undefined) { - // initalize filters - const tripFilter = surveyOpt.filter; - const allFalseFilters = tripFilter.map((f, i) => ({ - ...f, - state: i == 0 ? true : false, // only the first filter will have state true on init - })); - setFilterInputs(allFalseFilters); + // we will show filters if 'additions' are not configured + // https://github.com/e-mission/e-mission-docs/issues/894 + if (appConfig.survey_info?.buttons == undefined) { + // initalize filters + const tripFilter = surveyOpt.filter; + const allFalseFilters = tripFilter.map((f, i) => ({ + ...f, + state: i == 0 ? true : false, // only the first filter will have state true on init + })); + setFilterInputs(allFalseFilters); + } + loadTimelineEntries(); + } catch (e) { + displayError(e, t('errors.while-initializing-label')); } - loadTimelineEntries(); }, [appConfig, refreshTime]); // whenever timelineMap is updated, update the displayedEntries // according to the active filter useEffect(() => { - if (!timelineMap) return setDisplayedEntries(null); - const allEntries = Array.from(timelineMap.values()); - const activeFilter = filterInputs?.find((f) => f.state == true); - let entriesToDisplay = allEntries; - if (activeFilter) { - const entriesAfterFilter = allEntries.filter( - (t) => t.justRepopulated || activeFilter?.filter(t), - ); - /* next, filter out any untracked time if the trips that came before and + try { + if (!timelineMap) return setDisplayedEntries(null); + const allEntries = Array.from(timelineMap.values()); + const activeFilter = filterInputs?.find((f) => f.state == true); + let entriesToDisplay = allEntries; + if (activeFilter) { + const entriesAfterFilter = allEntries.filter( + (t) => t.justRepopulated || activeFilter?.filter(t), + ); + /* next, filter out any untracked time if the trips that came before and after it are no longer displayed */ - entriesToDisplay = entriesAfterFilter.filter((tlEntry) => { - if (!tlEntry.origin_key.includes('untracked')) return true; - const prevTrip = allEntries[allEntries.indexOf(tlEntry) - 1]; - const nextTrip = allEntries[allEntries.indexOf(tlEntry) + 1]; - const prevTripDisplayed = entriesAfterFilter.includes(prevTrip); - const nextTripDisplayed = entriesAfterFilter.includes(nextTrip); - // if either the trip before or after is displayed, then keep the untracked time - return prevTripDisplayed || nextTripDisplayed; - }); + entriesToDisplay = entriesAfterFilter.filter((tlEntry) => { + if (!tlEntry.origin_key.includes('untracked')) return true; + const prevTrip = allEntries[allEntries.indexOf(tlEntry) - 1]; + const nextTrip = allEntries[allEntries.indexOf(tlEntry) + 1]; + const prevTripDisplayed = entriesAfterFilter.includes(prevTrip); + const nextTripDisplayed = entriesAfterFilter.includes(nextTrip); + // if either the trip before or after is displayed, then keep the untracked time + return prevTripDisplayed || nextTripDisplayed; + }); + logDebug('After filtering, entriesToDisplay = ' + JSON.stringify(entriesToDisplay)); + } else { + logDebug('No active filter, displaying all entries'); + } + setDisplayedEntries(entriesToDisplay); + } catch (e) { + displayError(e, t('errors.while-updating-timeline')); } - setDisplayedEntries(entriesToDisplay); }, [timelineMap, filterInputs]); async function loadTimelineEntries() { @@ -117,15 +128,12 @@ const LabelTab = () => { labelPopulateFactory, enbs, ); - Logger.log( - 'After reading unprocessedInputs, labelsResultMap =' + - JSON.stringify(labelsResultMap) + - '; notesResultMap = ' + - JSON.stringify(notesResultMap), - ); + logDebug(`LabelTab: After reading unprocessedInputs, + labelsResultMap = ${JSON.stringify(labelsResultMap)}; + notesResultMap = ${JSON.stringify(notesResultMap)}`); setPipelineRange(pipelineRange); - } catch (error) { - Logger.displayError('Error while loading pipeline range', error); + } catch (e) { + displayError(e, 'Error while loading pipeline range'); setIsLoading(false); } } @@ -138,15 +146,22 @@ const LabelTab = () => { }, [pipelineRange]); function refresh() { - setIsLoading('replace'); - resetNominatimLimiter(); - setQueriedRange(null); - setTimelineMap(null); - setRefreshTime(new Date()); + try { + logDebug('Refreshing LabelTab'); + setIsLoading('replace'); + resetNominatimLimiter(); + setQueriedRange(null); + setTimelineMap(null); + setRefreshTime(new Date()); + } catch (e) { + displayError(e, t('errors.while-refreshing-label')); + } } async function loadAnotherWeek(when: 'past' | 'future') { try { + logDebug('LabelTab: loadAnotherWeek into the ' + when); + const reachedPipelineStart = queriedRange?.start_ts && queriedRange.start_ts <= pipelineRange.start_ts; const reachedPipelineEnd = @@ -183,6 +198,7 @@ const LabelTab = () => { async function loadSpecificWeek(day: string) { try { + logDebug('LabelTab: loadSpecificWeek for day ' + day); if (!isLoading) setIsLoading('replace'); resetNominatimLimiter(); const threeDaysBefore = moment(day).subtract(3, 'days').unix(); @@ -197,6 +213,11 @@ const LabelTab = () => { } function handleFetchedTrips(ctList, utList, mode: 'prepend' | 'append' | 'replace') { + logDebug(`LabelTab: handleFetchedTrips with + mode = ${mode}; + ctList = ${JSON.stringify(ctList)}; + utList = ${JSON.stringify(utList)}`); + const tripsRead = ctList.concat(utList); populateCompositeTrips( tripsRead, @@ -214,6 +235,8 @@ const LabelTab = () => { fillLocationNamesOfTrip(trip); }); const readTimelineMap = compositeTrips2TimelineMap(tripsRead, showPlaces); + logDebug(`LabelTab: after composite trips converted, + readTimelineMap = ${JSON.stringify(readTimelineMap)}`); if (mode == 'append') { setTimelineMap(new Map([...timelineMap, ...readTimelineMap])); } else if (mode == 'prepend') { @@ -221,16 +244,13 @@ const LabelTab = () => { } else if (mode == 'replace') { setTimelineMap(readTimelineMap); } else { - return console.error('Unknown insertion mode ' + mode); + return displayErrorMsg('Unknown insertion mode ' + mode); } } async function fetchTripsInRange(startTs: number, endTs: number) { - if (!pipelineRange.start_ts) { - console.warn('trying to read data too early, early return'); - return; - } - + if (!pipelineRange.start_ts) return logWarn('No pipelineRange yet - early return'); + logDebug('LabelTab: fetchTripsInRange from ' + startTs + ' to ' + endTs); const readCompositePromise = Timeline.readAllCompositeTrips(startTs, endTs); let readUnprocessedPromise; if (endTs >= pipelineRange.end_ts) { @@ -249,6 +269,8 @@ const LabelTab = () => { readUnprocessedPromise = Promise.resolve([]); } const results = await Promise.all([readCompositePromise, readUnprocessedPromise]); + logDebug(`LabelTab: readCompositePromise resolved as: ${JSON.stringify(results[0])}; + readUnprocessedPromise resolved as: ${JSON.stringify(results[1])}`); return results; } @@ -260,34 +282,41 @@ 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'); - const [newLabels, newNotes] = await getLocalUnprocessedInputs( - pipelineRange, - labelPopulateFactory, - enbs, - ); - const repopTime = new Date().getTime(); - const newEntry = { ...timelineMap.get(oid), justRepopulated: repopTime }; - labelPopulateFactory.populateInputsAndInferences(newEntry, newLabels); - enbs.populateInputsAndInferences(newEntry, newNotes); - const newTimelineMap = new Map(timelineMap).set(oid, newEntry); - setTimelineMap(newTimelineMap); + try { + logDebug('LabelTab: Repopulating timeline entry with oid ' + oid); + if (!timelineMap.has(oid)) + return displayErrorMsg('Item with oid: ' + oid + ' not found in timeline'); + const [newLabels, newNotes] = await getLocalUnprocessedInputs( + pipelineRange, + labelPopulateFactory, + enbs, + ); + const repopTime = new Date().getTime(); + logDebug('LabelTab: creating new entry for oid ' + oid + ' with repopTime ' + repopTime); + const newEntry = { ...timelineMap.get(oid), justRepopulated: repopTime }; + labelPopulateFactory.populateInputsAndInferences(newEntry, newLabels); + enbs.populateInputsAndInferences(newEntry, newNotes); + logDebug('LabelTab: after repopulating, newEntry = ' + JSON.stringify(newEntry)); + const newTimelineMap = new Map(timelineMap).set(oid, newEntry); + setTimelineMap(newTimelineMap); - // after 30 seconds, remove the justRepopulated flag unless it was repopulated again since then - /* ref is needed to avoid stale closure: + // after 30 seconds, remove the justRepopulated flag unless it was repopulated again since then + /* ref is needed to avoid stale closure: https://legacy.reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function */ - timelineMapRef.current = newTimelineMap; - setTimeout(() => { - const entry = { ...timelineMapRef.current.get(oid) }; - if (entry.justRepopulated != repopTime) - return console.log('Entry ' + oid + ' was repopulated again, skipping'); - const newTimelineMap = new Map(timelineMapRef.current).set(oid, { - ...entry, - justRepopulated: false, - }); - setTimelineMap(newTimelineMap); - }, 30000); + timelineMapRef.current = newTimelineMap; + setTimeout(() => { + const entry = { ...timelineMapRef.current.get(oid) }; + if (entry.justRepopulated != repopTime) + return logDebug('Entry ' + oid + ' was repopulated again, skipping'); + const newTimelineMap = new Map(timelineMapRef.current).set(oid, { + ...entry, + justRepopulated: false, + }); + setTimelineMap(newTimelineMap); + }, 30000); + } catch (e) { + displayError(e, t('errors.while-repopulating-entry')); + } } const contextVals = { From 94d893337c39a297b446d15939f6d92a9d6d01ac Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 9 Nov 2023 13:41:35 -0500 Subject: [PATCH 20/21] add logs to MetricsTab When metrics are fetched and stored as state, we can have more log statements and a try/catch. --- www/js/metrics/MetricsTab.tsx | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index d23cdd454..392a0ef3b 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -17,6 +17,7 @@ import DailyActiveMinutesCard from './DailyActiveMinutesCard'; import CarbonTextCard from './CarbonTextCard'; import ActiveMinutesTableCard from './ActiveMinutesTableCard'; import { getAggregateData, getMetrics } from '../commHelper'; +import { displayError, logDebug } from '../plugin/logger'; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; @@ -54,17 +55,24 @@ const MetricsTab = () => { }, [dateRange]); async function loadMetricsForPopulation(population: 'user' | 'aggregate', dateRange: DateTime[]) { - const serverResponse = await fetchMetricsFromServer(population, dateRange); - console.debug('Got metrics = ', serverResponse); - const metrics = {}; - const dataKey = population == 'user' ? 'user_metrics' : 'aggregate_metrics'; - METRIC_LIST.forEach((metricName, i) => { - metrics[metricName] = serverResponse[dataKey][i]; - }); - if (population == 'user') { - setUserMetrics(metrics as MetricsData); - } else { - setAggMetrics(metrics as MetricsData); + try { + logDebug(`MetricsTab: fetching metrics for population ${population}' + in date range ${JSON.stringify(dateRange)}`); + const serverResponse = await fetchMetricsFromServer(population, dateRange); + logDebug('MetricsTab: received metrics: ' + JSON.stringify(serverResponse)); + const metrics = {}; + const dataKey = population == 'user' ? 'user_metrics' : 'aggregate_metrics'; + METRIC_LIST.forEach((metricName, i) => { + metrics[metricName] = serverResponse[dataKey][i]; + }); + logDebug('MetricsTab: parsed metrics: ' + JSON.stringify(metrics)); + if (population == 'user') { + setUserMetrics(metrics as MetricsData); + } else { + setAggMetrics(metrics as MetricsData); + } + } catch (e) { + displayError(e, t('errors.while-loading-metrics')); } } From 98a9db64d0088dc7301d314e14d2fb8c2c2fdc25 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 9 Nov 2023 13:41:53 -0500 Subject: [PATCH 21/21] translations for error msgs just added --- www/i18n/en.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/i18n/en.json b/www/i18n/en.json index 7f3798f16..af549d05e 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -398,9 +398,14 @@ "errors": { "registration-check-token": "User registration error. Please check your token and try again.", "not-registered-cant-contact": "User is not registered, so the server cannot be contacted.", + "while-initializing-label": "While initializing Label tab: ", "while-populating-composite": "Error while populating composite trips", "while-loading-another-week": "Error while loading travel of {{when}} week", "while-loading-specific-week": "Error while loading travel for the week of {{day}}", + "while-updating-timeline": "While updating timeline: ", + "while-refreshing-label": "While refreshing Label tab: ", + "while-repopulating-entry": "While repopulating timeline entry: ", + "while-loading-metrics": "While loading metrics: ", "while-log-messages": "While getting messages from the log ", "while-max-index": "While getting max index " },