Skip to content

Commit

Permalink
Labeling code: infinite scroll list -> directive
Browse files Browse the repository at this point in the history
Move all the labeling code from the infinite scroll list to the directive
Concretely:
- remove the input detail initialization
- move out the code which initializes the inputs and replace it with the
  nextTrip link
- move the manual input population code as well. this is now a separate
  implementation from the diary initialization code; we will need to unify it later
- move related functions to the directive:
    - `populateInput`,
    - `populateManualInputs`,
    - `updateVisibilityAfterDelay`,
    - `updateTripProperties`,
    - `inferFinalLabels`,
    - `updateVerifiability`
- delete functions that were already copied from the diary, with minor
  modifications as necessary:
    - create `$scope.popovers`,
    - `$scope.openPopover`
    - `$scope.choose`
    - `closePopover`
    - `$scope.verifyTrip`

also copied `trip.properties.*_ts` into `trip.*_ts` so we can have the same
implementation for both data structures (e-mission/e-mission-docs#674 (comment))

In addition, in the directive, we do the following:
- add functions to find the view element, its state and scope.
    - We will use this state to indicate which view we are working with, given
      that we plan to support labeling assist in the diary view as well (e-mission/e-mission-docs#674 (comment))
    - We will use the scope to callback to the view and filter the trips accordingly (e-mission/e-mission-docs#674 (comment))

This almost works.

The one pending issue is that of filtering to set `$scope.displayTrips`. In
`$scope.setupInfScroll`, we read entries from the server
(`$scope.readDataFromServer`), which sets `$scope.data.allTrips`. It then calls
`$scope.recomputeDisplayTrip` which uses the userInputs to decide which trips
should be in `$scope.displayTrips`.

The problem is that we set the userInputs in the directive. However, the
directives are not executed until they are displayed. So until we compute which
trips go into `displayTrips`, we will not execute the directives, which means
that we will not have the fields we need to compute the directives. We need to
resolve this circular dependency.
  • Loading branch information
shankari committed Oct 4, 2021
1 parent 0f22450 commit 37439dc
Show file tree
Hide file tree
Showing 4 changed files with 282 additions and 333 deletions.
312 changes: 6 additions & 306 deletions www/js/diary/infinite_scroll_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet',
ionicDatePicker,
leafletData, Timeline, CommonGraph, DiaryHelper,
InfScrollFilters,
Config, PostTripManualMarker, ConfirmHelper, nzTour, KVStore, Logger, UnifiedDataLoader, $ionicPopover, $ionicModal, $translate, $q) {
Config, PostTripManualMarker, nzTour, KVStore, Logger, UnifiedDataLoader, $ionicPopover, $ionicModal, $translate, $q) {

// TODO: load only a subset of entries instead of everything

Expand All @@ -37,13 +37,6 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet',

const placeLimiter = new Bottleneck({ maxConcurrent: 2, minTime: 500 });

$scope.userInputDetails = [];
ConfirmHelper.INPUTS.forEach(function(item, index) {
const currInput = angular.copy(ConfirmHelper.inputDetails[item]);
currInput.name = item;
$scope.userInputDetails.push(currInput);
});

$scope.data = {};

$scope.getActiveFilters = function() {
Expand Down Expand Up @@ -106,15 +99,7 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet',
Logger.log("Received batch of size "+ctList.length);
ctList.forEach($scope.populateBasicClasses);
ctList.forEach((trip, tIndex) => {
// console.log("Expectation: "+JSON.stringify(trip.expectation));
// console.log("Inferred labels from server: "+JSON.stringify(trip.inferred_labels));
trip.userInput = {};
ConfirmHelper.INPUTS.forEach(function(item, index) {
$scope.populateManualInputs(trip, ctList[tIndex+1], item, $scope.data.manualResultMap[item]);
});
trip.finalInference = {};
$scope.inferFinalLabels(trip);
$scope.updateVerifiability(trip);
trip.nextTrip = ctList[tIndex+1];
});
// Fill places on a reversed copy of the list so we fill from the bottom up
ctList.slice().reverse().forEach(function(trip, index) {
Expand Down Expand Up @@ -159,7 +144,10 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet',
$scope.data.displayTrips = [];
Timeline.getUnprocessedLabels().then(([pipelineRange, manualResultMap]) => {
if (pipelineRange.end_ts) {
$scope.data.manualResultMap = manualResultMap;
$scope.$apply(() => {
$scope.data.manualResultMap = manualResultMap;
});
console.log("After reading in the label controller, manualResultMap "+JSON.stringify($scope.manualResultMap), $scope.data.manualResultMap);
$scope.infScrollControl.pipelineRange = pipelineRange;
$scope.infScrollControl.currentEnd = pipelineRange.end_ts;
$scope.infScrollControl.callback = function() {
Expand Down Expand Up @@ -317,159 +305,6 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet',
return ($scope.differentCommon(tripgj))? "stop-time-tag-lower" : "stop-time-tag";
}

/**
* MODE (becomes manual/mode_confirm) becomes mode_confirm
*/
$scope.inputType2retKey = function(inputType) {
return ConfirmHelper.inputDetails[inputType].key.split("/")[1];
}

/**
* Insert the given userInputLabel into the given inputType's slot in inputField
*/
$scope.populateInput = function(tripField, inputType, userInputLabel) {
if (angular.isDefined(userInputLabel)) {
var userInputEntry = $scope.inputParams[inputType].value2entry[userInputLabel];
if (!angular.isDefined(userInputEntry)) {
userInputEntry = ConfirmHelper.getFakeEntry(userInputLabel);
$scope.inputParams[inputType].options.push(userInputEntry);
$scope.inputParams[inputType].value2entry[userInputLabel] = userInputEntry;
}
// console.log("Mapped label "+userInputLabel+" to entry "+JSON.stringify(userInputEntry));
tripField[inputType] = userInputEntry;
}
}

/**
* Embed 'inputType' to the trip
*/
$scope.populateManualInputs = function (tripgj, nextTripgj, inputType, inputList) {
// Check unprocessed labels first since they are more recent
// Massage the input to meet getUserInputForTrip expectations
const unprocessedLabelEntry = DiaryHelper.getUserInputForTrip(
{data: {properties: tripgj, features: [{}, {}, {}]}},
{data: {properties: nextTripgj, features: [{}, {}, {}]}},
inputList);
var userInputLabel = unprocessedLabelEntry? unprocessedLabelEntry.data.label : undefined;
if (!angular.isDefined(userInputLabel)) {
userInputLabel = tripgj.user_input[$scope.inputType2retKey(inputType)];
}
$scope.populateInput(tripgj.userInput, inputType, userInputLabel);
// Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(tripgj.start_fmt_time));
$scope.editingTrip = angular.undefined;
}

/*
* Embody the logic for delayed update:
* the recompute logic already keeps trips that are waitingForModification
* even if they would be filtered otherwise.
* so here:
* - set the trip as waiting for potential modifications
* - create a one minute timeout that will remove the wait and recompute
* - clear the existing timeout (if any)
*/
$scope.updateVisibilityAfterDelay = function(trip) {
// We have just edited this trip, and are now waiting to see if the user
// is going to modify it further
trip.waitingForMod = true;
let currTimeoutPromise = trip.timeoutPromise;
let THIRTY_SECS = 30 * 1000;
Logger.log("trip starting at "+trip.start_fmt_time+": creating new timeout");
trip.timeoutPromise = $timeout(function() {
Logger.log("trip starting at "+trip.start_fmt_time+": executing recompute");
trip.waitingForMod = false;
trip.timeoutPromise = undefined;
$scope.recomputeDisplayTrips();
}, THIRTY_SECS);
Logger.log("trip starting at "+trip.start_fmt_time+": cancelling existing timeout "+currTimeoutPromise);
$timeout.cancel(currTimeoutPromise);
}

$scope.updateTripProperties = function(trip) {
$scope.inferFinalLabels(trip);
$scope.updateVerifiability(trip);
$scope.updateVisibilityAfterDelay(trip);
}

/**
* 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
*/
$scope.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 ConfirmHelper.INPUTS) {
const userInput = trip.userInput[inputType];
if (userInput) {
const retKey = $scope.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 ConfirmHelper.INPUTS) $scope.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 ConfirmHelper.INPUTS) {
// For each label type, find the most probable value by binning by label value and summing
const retKey = $scope.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;

$scope.populateInput(trip.finalInference, inputType, max.labelValue);
}
}
}

/**
* For a given trip, compute how the "verify" button should behave.
* If the trip has at least one yellow label, the button should be clickable.
* If the trip has all green labels, the button should be disabled because everything has already been verified.
* If the trip has all red labels or a mix of red and green, the button should be disabled because we need more detailed user input.
*/
$scope.updateVerifiability = function(trip) {
var allGreen = true;
var someYellow = false;
for (const inputType of ConfirmHelper.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");
}


$scope.getFormattedDistanceInMiles = function(input) {
return (0.621371 * $scope.getFormattedDistance(input)).toFixed(1);
}
Expand Down Expand Up @@ -753,141 +588,6 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet',

$scope.showModes = DiaryHelper.showModes;

$scope.popovers = {};
ConfirmHelper.INPUTS.forEach(function(item, index) {
let popoverPath = 'templates/diary/'+item.toLowerCase()+'-popover.html';
return $ionicPopover.fromTemplateUrl(popoverPath, {
scope: $scope
}).then(function (popover) {
$scope.popovers[item] = popover;
});
});

$scope.openPopover = function ($event, tripgj, inputType) {
var userInput = tripgj.userInput[inputType];
if (angular.isDefined(userInput)) {
$scope.selected[inputType].value = userInput.value;
} else {
$scope.selected[inputType].value = '';
}
$scope.draftInput = {
"start_ts": tripgj.start_ts,
"end_ts": tripgj.end_ts
};
$scope.editingTrip = tripgj;
Logger.log("in openPopover, setting draftInput = " + JSON.stringify($scope.draftInput));
$scope.popovers[inputType].show($event);
};

var closePopover = function (inputType) {
$scope.selected[inputType] = {
value: ''
};
$scope.popovers[inputType].hide();
};

/**
* verifyTrip turns all of a given trip's yellow labels green
*/
$scope.verifyTrip = function($event, trip) {
if (trip.verifiability != "can-verify") {
ClientStats.addReading(ClientStats.getStatKeys().VERIFY_TRIP, {"verifiable": false});
return;
}
ClientStats.addReading(ClientStats.getStatKeys().VERIFY_TRIP, {"verifiable": true, "userInput": angular.toJson(trip.userInput), "finalInference": angular.toJson(trip.finalInference)});

$scope.draftInput = {
"start_ts": trip.start_ts,
"end_ts": trip.end_ts
};
$scope.editingTrip = trip;

for (const inputType of ConfirmHelper.INPUTS) {
const inferred = trip.finalInference[inputType];
// TODO: figure out what to do with "other". For now, do not verify.
if (inferred && !trip.userInput[inputType] && inferred != "other") $scope.store(inputType, inferred, false);
}
}

/**
* Store selected value for options
* $scope.selected is for display only
* the value is displayed on popover selected option
*/
$scope.selected = {}
ConfirmHelper.INPUTS.forEach(function(item, index) {
$scope.selected[item] = {value: ''};
});
$scope.selected.other = {text: '', value: ''};

/*
* This is a curried function that curries the `$scope` variable
* while returing a function that takes `e` as the input
*/
var checkOtherOptionOnTap = function ($scope, inputType) {
return function (e) {
if (!$scope.selected.other.text) {
e.preventDefault();
} else {
Logger.log("in choose other, other = " + JSON.stringify($scope.selected));
$scope.store(inputType, $scope.selected.other, true /* isOther */);
$scope.selected.other = '';
return $scope.selected.other;
}
}
};

$scope.choose = function (inputType) {
ClientStats.addReading(ClientStats.getStatKeys().SELECT_LABEL, {
"userInput": angular.toJson($scope.editingTrip.userInput),
"finalInference": angular.toJson($scope.editingTrip.finalInference),
"inputKey": inputType,
"inputVal": $scope.selected[inputType].value
});
var isOther = false
if ($scope.selected[inputType].value != "other") {
$scope.store(inputType, $scope.selected[inputType], isOther);
} else {
isOther = true
ConfirmHelper.checkOtherOption(inputType, checkOtherOptionOnTap, $scope);
}
closePopover(inputType);
};

$scope.$on('$ionicView.loaded', function() {
$scope.inputParams = {}
ConfirmHelper.INPUTS.forEach(function(item) {
ConfirmHelper.getOptionsAndMaps(item).then(function(omObj) {
$scope.inputParams[item] = omObj;
});
});
});

$scope.store = function (inputType, input, isOther) {
if(isOther) {
// Let's make the value for user entered inputs look consistent with our
// other values
input.value = ConfirmHelper.otherTextToValue(input.text);
}
$scope.draftInput.label = input.value;
Logger.log("in storeInput, after setting input.value = " + input.value + ", draftInput = " + JSON.stringify($scope.draftInput));
var tripToUpdate = $scope.editingTrip;
$window.cordova.plugins.BEMUserCache.putMessage(ConfirmHelper.inputDetails[inputType].key, $scope.draftInput).then(function () {
$scope.$apply(function() {
if (isOther) {
tripToUpdate.userInput[inputType] = ConfirmHelper.getFakeEntry(input.value);
$scope.inputParams[inputType].options.push(tripToUpdate.userInput[inputType]);
$scope.inputParams[inputType].value2entry[input.value] = tripToUpdate.userInput[inputType];
} else {
tripToUpdate.userInput[inputType] = $scope.inputParams[inputType].value2entry[input.value];
}
$scope.updateTripProperties(tripToUpdate); // Redo our inferences, filters, etc. based on this new information
});
});
if (isOther == true)
$scope.draftInput = angular.undefined;
}

$scope.redirect = function(){
$state.go("root.main.current");
};
Expand Down
Loading

0 comments on commit 37439dc

Please sign in to comment.