diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index aca25232d..000000000 --- a/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -# http://editorconfig.org -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -[*.md] -insert_final_newline = false -trim_trailing_whitespace = false \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..be7b1726d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +# Ignore www/dist, manual_lib, json +www/dist +www/manual_lib +www/json + +# This is the pattern to check only www directory +# Ignore all +/* +# but don't ignore all the files in www directory +!/www diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..5875d605a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": true, + "bracketSameLine": true, + "endOfLine": "lf", + "semi": true +} diff --git a/package.serve.json b/package.serve.json index 1a2ef6cb0..8597cb920 100644 --- a/package.serve.json +++ b/package.serve.json @@ -45,7 +45,8 @@ "typescript": "^5.0.3", "url-loader": "^4.1.1", "webpack": "^5.0.1", - "webpack-cli": "^5.0.1" + "webpack-cli": "^5.0.1", + "prettier": "3.0.3" }, "dependencies": { "@react-navigation/native": "^6.1.7", diff --git a/www/js/survey/enketo/AddedNotesList.tsx b/www/js/survey/enketo/AddedNotesList.tsx index e29278cca..f1563c4a9 100644 --- a/www/js/survey/enketo/AddedNotesList.tsx +++ b/www/js/survey/enketo/AddedNotesList.tsx @@ -2,22 +2,21 @@ Notes are added from the AddNoteButton and are derived from survey responses. */ -import React, { useContext, useState } from "react"; -import moment from "moment"; -import { Modal } from "react-native" -import { Text, Button, DataTable, Dialog } from "react-native-paper"; -import { LabelTabContext } from "../../diary/LabelTab"; -import { getFormattedDateAbbr, isMultiDay } from "../../diary/diaryHelper"; -import { Icon } from "../../components/Icon"; -import EnketoModal from "./EnketoModal"; -import { useTranslation } from "react-i18next"; +import React, { useContext, useState } from 'react'; +import moment from 'moment'; +import { Modal } from 'react-native'; +import { Text, Button, DataTable, Dialog } from 'react-native-paper'; +import { LabelTabContext } from '../../diary/LabelTab'; +import { getFormattedDateAbbr, isMultiDay } from '../../diary/diaryHelper'; +import { Icon } from '../../components/Icon'; +import EnketoModal from './EnketoModal'; +import { useTranslation } from 'react-i18next'; type Props = { - timelineEntry: any, - additionEntries: any[], -} + timelineEntry: any; + additionEntries: any[]; +}; const AddedNotesList = ({ timelineEntry, additionEntries }: Props) => { - const { t } = useTranslation(); const { repopulateTimelineEntry } = useContext(LabelTabContext); const [confirmDeleteModalVisible, setConfirmDeleteModalVisible] = useState(false); @@ -25,41 +24,46 @@ const AddedNotesList = ({ timelineEntry, additionEntries }: Props) => { const [editingEntry, setEditingEntry] = useState(null); function setDisplayDt(entry) { - const timezone = timelineEntry.start_local_dt?.timezone - || timelineEntry.enter_local_dt?.timezone - || timelineEntry.end_local_dt?.timezone - || timelineEntry.exit_local_dt?.timezone; + const timezone = + timelineEntry.start_local_dt?.timezone || + timelineEntry.enter_local_dt?.timezone || + timelineEntry.end_local_dt?.timezone || + timelineEntry.exit_local_dt?.timezone; const beginTs = entry.data.start_ts || entry.data.enter_ts; const stopTs = entry.data.end_ts || entry.data.exit_ts; let d; if (isMultiDay(beginTs, stopTs)) { - const beginTsZoned = moment.parseZone(beginTs*1000).tz(timezone); - const stopTsZoned = moment.parseZone(stopTs*1000).tz(timezone); + const beginTsZoned = moment.parseZone(beginTs * 1000).tz(timezone); + const stopTsZoned = moment.parseZone(stopTs * 1000).tz(timezone); d = getFormattedDateAbbr(beginTsZoned.toISOString(), stopTsZoned.toISOString()); } - const begin = moment.parseZone(beginTs*1000).tz(timezone).format('LT'); - const stop = moment.parseZone(stopTs*1000).tz(timezone).format('LT'); - return entry.displayDt = { + const begin = moment + .parseZone(beginTs * 1000) + .tz(timezone) + .format('LT'); + const stop = moment + .parseZone(stopTs * 1000) + .tz(timezone) + .format('LT'); + return (entry.displayDt = { date: d, - time: begin + " - " + stop - } + time: begin + ' - ' + stop, + }); } function deleteEntry(entry) { - console.log("Deleting entry", entry); + console.log('Deleting entry', entry); const dataKey = entry.key || entry.metadata.key; const data = entry.data; const index = additionEntries.indexOf(entry); data.status = 'DELETED'; - return window['cordova'].plugins.BEMUserCache - .putMessage(dataKey, data) - .then(() => { - additionEntries.splice(index, 1); - setConfirmDeleteModalVisible(false); - setEditingEntry(null); - }); + return window['cordova'].plugins.BEMUserCache.putMessage(dataKey, data).then(() => { + additionEntries.splice(index, 1); + setConfirmDeleteModalVisible(false); + setEditingEntry(null); + }); } function confirmDeleteEntry(entry) { @@ -90,66 +94,80 @@ const AddedNotesList = ({ timelineEntry, additionEntries }: Props) => { } const sortedEntries = additionEntries?.sort((a, b) => a.data.start_ts - b.data.start_ts); - return (<> - - {sortedEntries?.map((entry, index) => { - const isLastRow = (index == additionEntries.length - 1); - return ( - - editEntry(entry)} - style={[styles.cell, {flex: 5, pointerEvents: 'auto'}]} - textStyle={{fontSize: 12, fontWeight: 'bold'}}> - {entry.data.label} - - editEntry(entry)} - style={[styles.cell, {flex: 4}]} - textStyle={{fontSize: 12, lineHeight: 12}}> - {entry.displayDt?.date} - {entry.displayDt?.time || setDisplayDt(entry)} - - confirmDeleteEntry(entry)} - style={[styles.cell, {flex: 1}]}> - - - - ) - })} - - - - - { t('diary.delete-entry-confirm') } - - {editingEntry?.data?.label} - {editingEntry?.displayDt?.date} - {editingEntry?.displayDt?.time} - - - - - - - - ); + return ( + <> + + {sortedEntries?.map((entry, index) => { + const isLastRow = index == additionEntries.length - 1; + return ( + + editEntry(entry)} + style={[styles.cell, { flex: 5, pointerEvents: 'auto' }]} + textStyle={{ fontSize: 12, fontWeight: 'bold' }}> + {entry.data.label} + + editEntry(entry)} + style={[styles.cell, { flex: 4 }]} + textStyle={{ fontSize: 12, lineHeight: 12 }}> + {entry.displayDt?.date} + + {entry.displayDt?.time || setDisplayDt(entry)} + + + confirmDeleteEntry(entry)} + style={[styles.cell, { flex: 1 }]}> + + + + ); + })} + + + + + {t('diary.delete-entry-confirm')} + + {editingEntry?.data?.label} + {editingEntry?.displayDt?.date} + {editingEntry?.displayDt?.time} + + + + + + + + + ); }; -const styles:any = { +const styles: any = { row: (isLastRow) => ({ minHeight: 36, height: 36, - borderBottomWidth: (isLastRow ? 0 : 1), + borderBottomWidth: isLastRow ? 0 : 1, borderBottomColor: 'rgba(0,0,0,0.1)', pointerEvents: 'all', }), cell: { pointerEvents: 'auto', }, -} +}; export default AddedNotesList; diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 6e9147cf8..5899200fb 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -1,10 +1,10 @@ -import { getAngularService } from "../../angular-react-helper"; +import { getAngularService } from '../../angular-react-helper'; import { Form } from 'enketo-core'; import { XMLParser } from 'fast-xml-parser'; import i18next from 'i18next'; import { logDebug } from "../../plugin/logger"; -export type PrefillFields = {[key: string]: string}; +export type PrefillFields = { [key: string]: string }; export type SurveyOptions = { undismissable?: boolean; @@ -37,12 +37,10 @@ function getXmlWithPrefills(xmlModel: string, prefillFields: PrefillFields) { * @param opts object with options like 'prefilledSurveyResponse' or 'prefillFields' * @returns XML string of an existing or prefilled model response, or null if no response is available */ -export function getInstanceStr(xmlModel: string, opts: SurveyOptions): string|null { +export function getInstanceStr(xmlModel: string, opts: SurveyOptions): string | null { if (!xmlModel) return null; - if (opts.prefilledSurveyResponse) - return opts.prefilledSurveyResponse; - if (opts.prefillFields) - return getXmlWithPrefills(xmlModel, opts.prefillFields); + if (opts.prefilledSurveyResponse) return opts.prefilledSurveyResponse; + if (opts.prefillFields) return getXmlWithPrefills(xmlModel, opts.prefillFields); return null; } @@ -58,39 +56,39 @@ export function saveResponse(surveyName: string, enketoForm: Form, appConfig, op const xmlParser = new window.DOMParser(); const xmlResponse = enketoForm.getDataStr(); const xmlDoc = xmlParser.parseFromString(xmlResponse, 'text/xml'); - const xml2js = new XMLParser({ignoreAttributes: false, attributeNamePrefix: 'attr'}); + const xml2js = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: 'attr' }); const jsonDocResponse = xml2js.parse(xmlResponse); - return EnketoSurveyAnswer.resolveLabel(surveyName, xmlDoc).then(rsLabel => { - const data: any = { - label: rsLabel, - name: surveyName, - version: appConfig.survey_info.surveys[surveyName].version, - xmlResponse, - jsonDocResponse, - }; - if (opts.timelineEntry) { - let timestamps = EnketoSurveyAnswer.resolveTimestamps(xmlDoc, opts.timelineEntry); - if (timestamps === undefined) { - // timestamps were resolved, but they are invalid - return new Error(i18next.t('survey.enketo-timestamps-invalid')); //"Timestamps are invalid. Please ensure that the start time is before the end time."); + return EnketoSurveyAnswer.resolveLabel(surveyName, xmlDoc) + .then((rsLabel) => { + const data: any = { + label: rsLabel, + name: surveyName, + version: appConfig.survey_info.surveys[surveyName].version, + xmlResponse, + jsonDocResponse, + }; + if (opts.timelineEntry) { + let timestamps = EnketoSurveyAnswer.resolveTimestamps(xmlDoc, opts.timelineEntry); + if (timestamps === undefined) { + // timestamps were resolved, but they are invalid + return new Error(i18next.t('survey.enketo-timestamps-invalid')); //"Timestamps are invalid. Please ensure that the start time is before the end time."); + } + // if timestamps were not resolved from the survey, we will use the trip or place timestamps + timestamps ||= opts.timelineEntry; + data.start_ts = timestamps.start_ts || timestamps.enter_ts; + data.end_ts = timestamps.end_ts || timestamps.exit_ts; + // UUID generated using this method https://stackoverflow.com/a/66332305 + data.match_id = URL.createObjectURL(new Blob([])).slice(-36); + } else { + const now = Date.now(); + data.ts = now / 1000; // convert to seconds to be consistent with the server + data.fmt_time = new Date(now); } - // if timestamps were not resolved from the survey, we will use the trip or place timestamps - timestamps ||= opts.timelineEntry; - data.start_ts = timestamps.start_ts || timestamps.enter_ts; - data.end_ts = timestamps.end_ts || timestamps.exit_ts; - // UUID generated using this method https://stackoverflow.com/a/66332305 - data.match_id = URL.createObjectURL(new Blob([])).slice(-36); - } else { - const now = Date.now(); - data.ts = now/1000; // convert to seconds to be consistent with the server - data.fmt_time = new Date(now); - } - // use dataKey passed into opts if available, otherwise get it from the config - const dataKey = opts.dataKey || appConfig.survey_info.surveys[surveyName].dataKey; - return window['cordova'].plugins.BEMUserCache - .putMessage(dataKey, data) - .then(() => data); - }).then(data => data); + // use dataKey passed into opts if available, otherwise get it from the config + const dataKey = opts.dataKey || appConfig.survey_info.surveys[surveyName].dataKey; + return window['cordova'].plugins.BEMUserCache.putMessage(dataKey, data).then(() => data); + }) + .then((data) => data); } const _getMostRecent = (answers) => { diff --git a/www/js/survey/input-matcher.js b/www/js/survey/input-matcher.js index 2e3d5b908..6fc3178df 100644 --- a/www/js/survey/input-matcher.js +++ b/www/js/survey/input-matcher.js @@ -2,23 +2,37 @@ 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) { +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)} @@ -29,40 +43,40 @@ angular.module('emission.survey.inputmatcher', ['emission.plugin.logger']) || ${-(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 ( + ((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 + /* 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) { + 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) { + } + + 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)} @@ -73,141 +87,187 @@ angular.module('emission.survey.inputmatcher', ['emission.plugin.logger']) 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. */ - var endChecks = (userInput.data.end_ts <= entryEnd || - (userInput.data.end_ts - entryEnd) <= 15 * 60); - if (startChecks && !endChecks) { + 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); - } + 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); + // 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; + // 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, + } + 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; - } + return notDeletedActive; + }; - im.getUserInputForTrip = function(trip, nextTrip, userInputList) { - const logsEnabled = userInputList.length < 20; + im.getUserInputForTrip = function (trip, nextTrip, userInputList) { + const logsEnabled = userInputList.length < 20; - if (userInputList === undefined) { - Logger.log("In getUserInputForTrip, no user input, returning undefined"); + 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) { + 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 []"); + 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])); + 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) { + 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) => { + }); + 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"); - } + 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); + uniqueMap.set(e.data.match_id, e); } - }); - return Array.from(uniqueMap.values()); - } + }); + return Array.from(uniqueMap.values()); + }; - return im; -}); + return im; + });