From b4bc7ab568b6bf2cfb4f3fb039e351b6583fc72f Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 2 Nov 2023 13:57:17 -0700 Subject: [PATCH] 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; +};