diff --git a/www/css/main.diary.css b/www/css/main.diary.css index 4d74ada61..cb0468fc1 100644 --- a/www/css/main.diary.css +++ b/www/css/main.diary.css @@ -109,4 +109,45 @@ a.item-content { border-radius: 3px; margin-left: 3px; margin-top: 3px; -} \ No newline at end of file +} +.btn-mode-confirm-green, +.btn-mode-confirm-green:hover, +.btn-mode-confirm-green:active, +.btn-purpose-confirm-green, +.btn-purpose-confirm-green:hover, +.btn-purpose-confirm-green:active { + background-color: #30A64A; + color: white; +} +.btn-mode-confirm-white, +.btn-mode-confirm-white:hover, +.btn-mode-confirm-white:active, +.btn-purpose-confirm-white, +.btn-purpose-confirm-white:hover, +.btn-purpose-confirm-white:active { + background-color: #ddd; + color: #333; + font-size: 0.8em; +} +.btn-mode-confirm, +.btn-purpose-confirm { + line-height: 30px; + min-height: 30px; + font-size: 0.8em !important; + width: 115px; + padding: 0; +} +.mode-confirm-container, +.purpose-confirm-container { + margin-top: 5px; +} + +.popover { + height: 297px; + width: 230px; +} + +#diary-item { + padding: 0; + border-width: 0; +} diff --git a/www/css/style.css b/www/css/style.css index db2f99190..dd7328579 100644 --- a/www/css/style.css +++ b/www/css/style.css @@ -905,7 +905,7 @@ button.button.back-button.buttons.button-clear.header-item { font-size: 9px; position: absolute; left: 1%; - top: 275px; + bottom: 15px; line-height: 16px; } .stop-time-tag-lower { diff --git a/www/index.html b/www/index.html index af568513f..d4f39f03f 100644 --- a/www/index.html +++ b/www/index.html @@ -72,8 +72,9 @@ - - + + + diff --git a/www/js/common/map.js b/www/js/common/map.js index ef930d0ea..5101a003b 100644 --- a/www/js/common/map.js +++ b/www/js/common/map.js @@ -177,7 +177,7 @@ angular.module('emission.main.common.map',['emission.main.common.services', } console.log("got response, setting display name to "+name); - place_feature.properties.displayName = name; + place_feature.properties.display_name = name; }; diff --git a/www/js/common/services.js b/www/js/common/services.js index dd406fd21..722c767b3 100644 --- a/www/js/common/services.js +++ b/www/js/common/services.js @@ -164,13 +164,13 @@ angular.module('emission.main.common.services', []) console.log("got response, setting display name to "+name); switch (mode) { case 'place': - obj.properties.displayName = name; + obj.properties.display_name = name; break; case 'cplace': - obj.displayName = name; + obj.display_name = name; break; case 'ctrip': - obj.start_displayName = name; + obj.start_display_name = name; break; } @@ -193,7 +193,7 @@ angular.module('emission.main.common.services', []) } } console.log("got response, setting display name to "+name); - obj.end_displayName = name; + obj.end_display_name = name; }; switch (mode) { @@ -293,8 +293,8 @@ angular.module('emission.main.common.services', []) commonGraph.data.graph.common_places.forEach(function(cPlace, index, array) { commonGraph.data.cPlaceCountMap[cPlace._id.$oid] = cPlace.places.length; commonGraph.data.cPlaceId2ObjMap[cPlace._id.$oid] = cPlace; - if (angular.isDefined(cPlace.displayName)) { - console.log("For place "+cPlace.id+", already have displayName "+cPlace.displayName); + if (angular.isDefined(cPlace.display_name)) { + console.log("For place "+cPlace.id+", already have display_name "+cPlace.display_name); } else { console.log("Don't have display name for end place, going to query nominatim"); commonGraph.getDisplayName('cplace', cPlace); @@ -318,7 +318,7 @@ angular.module('emission.main.common.services', []) "id": place._id.$oid, "geometry": place.location, "properties": { - "displayName": place.displayName + "display_name": place.display_name } }; }); diff --git a/www/js/controllers.js b/www/js/controllers.js index 59c5d6179..ed19aa0de 100644 --- a/www/js/controllers.js +++ b/www/js/controllers.js @@ -6,7 +6,7 @@ angular.module('emission.controllers', ['emission.splash.updatecheck', 'emission.splash.localnotify', 'emission.survey.launch', 'emission.stats.clientstats', - 'emission.incident.posttrip.prompt']) + 'emission.tripconfirm.posttrip.prompt']) .controller('RootCtrl', function($scope) {}) diff --git a/www/js/diary/detail.js b/www/js/diary/detail.js index eebcada4c..ceb7170f5 100644 --- a/www/js/diary/detail.js +++ b/www/js/diary/detail.js @@ -61,15 +61,11 @@ angular.module('emission.main.diary.detail',['ui-leaflet', 'ng-walkthrough', $scope.getFormattedDistance = DiaryHelper.getFormattedDistance; $scope.getSectionDetails = DiaryHelper.getSectionDetails; $scope.getFormattedTime = DiaryHelper.getFormattedTime; + $scope.getLocalTimeString = DiaryHelper.getLocalTimeString; $scope.getFormattedTimeRange = DiaryHelper.getFormattedTimeRange; $scope.getFormattedDuration = DiaryHelper.getFormattedDuration; $scope.getTripDetails = DiaryHelper.getTripDetails - $scope.tripgj = DiaryHelper.directiveForTrip($scope.trip); - - $scope.getTripBackground = function() { - var ret_val = DiaryHelper.getTripBackground($scope.tripgj); - return ret_val; - } + $scope.tripgj = Timeline.getTripWrapper($stateParams.tripId); console.log("trip.start_place = " + JSON.stringify($scope.trip.start_place)); diff --git a/www/js/diary/list.js b/www/js/diary/list.js index 022e8a260..a063cbdb3 100644 --- a/www/js/diary/list.js +++ b/www/js/diary/list.js @@ -1,12 +1,22 @@ 'use strict'; +/* + * The general structure of this code is that all the timeline information for + * a particular day is retrieved from the Timeline factory and put into the scope. + * For best performance, all data should be loaded into the in-memory timeline, + * and in addition to writing to storage, the data should be written to memory. + * All UI elements should only use $scope variables. + */ + angular.module('emission.main.diary.list',['ui-leaflet', 'ionic-datepicker', 'emission.main.common.services', 'emission.incident.posttrip.manual', + 'emission.tripconfirm.services', 'emission.services', 'ng-walkthrough', 'nzTour', 'emission.plugin.kvstore', - 'emission.plugin.logger']) + 'emission.plugin.logger' + ]) .controller("DiaryListCtrl", function($window, $scope, $rootScope, $ionicPlatform, $state, $ionicScrollDelegate, $ionicPopup, @@ -14,8 +24,11 @@ angular.module('emission.main.diary.list',['ui-leaflet', $ionicActionSheet, ionicDatePicker, leafletData, Timeline, CommonGraph, DiaryHelper, - Config, PostTripManualMarker, nzTour, KVStore, Logger) { + Config, PostTripManualMarker, ConfirmHelper, nzTour, KVStore, Logger, UnifiedDataLoader, $ionicPopover) { console.log("controller DiaryListCtrl called"); + var MODE_CONFIRM_KEY = "manual/mode_confirm"; + var PURPOSE_CONFIRM_KEY = "manual/purpose_confirm"; + // Add option $scope.$on('leafletDirectiveMap.resize', function(event, data) { @@ -32,24 +45,6 @@ angular.module('emission.main.diary.list',['ui-leaflet', // CommonGraph.updateCurrent(); }; - $scope.$on('$ionicView.afterEnter', function() { - if($rootScope.barDetail){ - readAndUpdateForDay($rootScope.barDetailDate); - $rootScope.barDetail = false; - }; - if($rootScope.displayingIncident == true) { - if (angular.isDefined(Timeline.data.currDay)) { - // page was already loaded, reload it automatically - readAndUpdateForDay(Timeline.data.currDay); - } else { - Logger.log("currDay is not defined, load not complete"); - } - $rootScope.displayingIncident = false; - } - }); - - readAndUpdateForDay(moment().startOf('day')); - angular.extend($scope, { defaults: { zoomControl: false, @@ -87,19 +82,12 @@ angular.module('emission.main.diary.list',['ui-leaflet', * setHours here, while the currDay is a moment, since we use it to perform * +date and -date operations. */ - $scope.listExpandClass = function () { - return "earlier-later-expand"; - } - $scope.listLocationClass = function() { - return "item item-icon-left list-location"; - } - $scope.listTextClass = function() { - return "list-text"; - } - $scope.datePickerClass = function() { - } + $scope.listExpandClass = "earlier-later-expand"; + $scope.listLocationClass = "item item-icon-left list-location"; + $scope.listTextClass = "list-text"; + $scope.listCardClass = function(tripgj) { - var background = DiaryHelper.getTripBackground(tripgj); + var background = tripgj.background; if ($window.screen.width <= 320) { return "list card list-card "+ background +" list-card-sm"; } else if ($window.screen.width <= 375) { @@ -116,9 +104,8 @@ angular.module('emission.main.diary.list',['ui-leaflet', return "col-50 list-col-left-margin"; } } - $scope.listColRightClass = function() { - return "col-50 list-col-right"; - } + $scope.listColRightClass = "col-50 list-col-right" + $scope.differentCommon = function(tripgj) { return ($scope.isCommon(tripgj.id))? ((DiaryHelper.getEarlierOrLater(tripgj.data.properties.start_ts, tripgj.data.id) == '')? false : true) : false; } @@ -133,12 +120,6 @@ angular.module('emission.main.diary.list',['ui-leaflet', readAndUpdateForDay(moment(val)); } } - $scope.localTimeString = function(dt) { - var hr = ((dt.hour > 12))? dt.hour - 12 : dt.hour; - var post = ((dt.hour >= 12))? " pm" : " am"; - var min = (dt.minute.toString().length == 1)? "0" + dt.minute.toString() : dt.minute.toString(); - return hr + ":" + min + post; - } $scope.datepickerObject = { @@ -165,16 +146,108 @@ angular.module('emission.main.diary.list',['ui-leaflet', ionicDatePicker.openDatePicker($scope.datepickerObject); } + /** + * Embed 'mode' to the trip + */ + $scope.populateModeFromTimeline = function (tripgj, modeList) { + var userMode = DiaryHelper.getUserInputForTrip(tripgj.data.properties, modeList); + if (angular.isDefined(userMode)) { + // userMode is a mode object with data + metadata + // the label is the "value" from the options + var userModeEntry = $scope.value2entryMode[userMode.data.label]; + if (!angular.isDefined(userModeEntry)) { + userModeEntry = ConfirmHelper.getFakeEntry(userMode.data.label); + $scope.modeOptions.push(userModeEntry); + $scope.value2entryMode[userMode.data.label] = userModeEntry; + } + console.log("Mapped label "+userMode.data.label+" to entry "+JSON.stringify(userModeEntry)); + tripgj.usermode = userModeEntry; + } + Logger.log("Set mode" + JSON.stringify(userModeEntry) + " for trip id " + JSON.stringify(tripgj.data.id)); + $scope.modeTripgj = angular.undefined; + } + + /** + * Embed 'purpose' to the trip + */ + $scope.populatePurposeFromTimeline = function (tripgj, purposeList) { + var userPurpose = DiaryHelper.getUserInputForTrip(tripgj.data.properties, purposeList); + if (angular.isDefined(userPurpose)) { + // userPurpose is a purpose object with data + metadata + // the label is the "value" from the options + var userPurposeEntry = $scope.value2entryPurpose[userPurpose.data.label]; + if (!angular.isDefined(userPurposeEntry)) { + userPurposeEntry = ConfirmHelper.getFakeEntry(userPurpose.data.label); + $scope.purposeOptions.push(userPurposeEntry); + $scope.value2entryPurpose[userPurpose.data.label] = userPurposeEntry; + } + console.log("Mapped label "+userPurpose.data.label+" to entry "+JSON.stringify(userPurposeEntry)); + tripgj.userpurpose = userPurposeEntry; + } + Logger.log("Set purpose " + JSON.stringify(userPurposeEntry) + " for trip id " + JSON.stringify(tripgj.data.id)); + $scope.purposeTripgj = angular.undefined; + } + + $scope.populateBasicClasses = function(tripgj) { + tripgj.display_start_time = DiaryHelper.getLocalTimeString(tripgj.data.properties.start_local_dt); + tripgj.display_end_time = DiaryHelper.getLocalTimeString(tripgj.data.properties.end_local_dt); + tripgj.display_distance = $scope.getFormattedDistance(tripgj.data.properties.distance); + tripgj.display_time = $scope.getFormattedTimeRange(tripgj.data.properties.start_ts, + tripgj.data.properties.end_ts); + tripgj.isDraft = $scope.isDraft(tripgj); + tripgj.background = DiaryHelper.getTripBackground(tripgj); + tripgj.listCardClass = $scope.listCardClass(tripgj); + tripgj.percentages = $scope.getPercentages(tripgj) + } + + $scope.populateCommonInfo = function(tripgj) { + tripgj.common = {} + DiaryHelper.fillCommonTripCount(tripgj); + tripgj.common.different = $scope.differentCommon(tripgj); + tripgj.common.longerOrShorter = $scope.getLongerOrShorter(tripgj.data, tripgj.data.id); + tripgj.common.listColLeftClass = $scope.listColLeftClass(tripgj.common.longerOrShorter[0]); + tripgj.common.stopTimeTagClass = $scope.stopTimeTagClass(tripgj); + tripgj.common.arrowColor = $scope.arrowColor(tripgj.common.longerOrShorter[0]); + tripgj.common.arrowClass = $scope.getArrowClass(tripgj.common.longerOrShorter[0]); + + tripgj.common.earlierOrLater = $scope.getEarlierOrLater(tripgj.data.properties.start_ts, tripgj.data.id); + tripgj.common.displayEarlierLater = $scope.parseEarlierOrLater(tripgj.common.earlierOrLater); + } + + var isNotEmpty = function (obj) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) + return true; + } + return false; + }; + + $scope.explainDraft = function($event) { + $event.stopPropagation(); + $ionicPopup.alert({ + template: "This trip has not yet been analysed. If it stays in this state, please ask your sysadmin to check what is wrong." + }); + // don't want to go to the detail screen + } + $scope.$on(Timeline.UPDATE_DONE, function(event, args) { console.log("Got timeline update done event with args "+JSON.stringify(args)); $scope.$apply(function() { $scope.data = Timeline.data; $scope.datepickerObject.inputDate = Timeline.data.currDay.toDate(); $scope.data.currDayTrips.forEach(function(trip, index, array) { - PostTripManualMarker.addUnpushedIncidents(trip); + PostTripManualMarker.addUnpushedIncidents(trip); }); - $scope.data.currDayTripWrappers = Timeline.data.currDayTrips.map( + var currDayTripWrappers = Timeline.data.currDayTrips.map( DiaryHelper.directiveForTrip); + Timeline.setTripWrappers(currDayTripWrappers); + + $scope.data.currDayTripWrappers.forEach(function(tripgj, index, array) { + $scope.populateModeFromTimeline(tripgj, $scope.data.unifiedConfirmsResults.modes); + $scope.populatePurposeFromTimeline(tripgj, $scope.data.unifiedConfirmsResults.purposes); + $scope.populateBasicClasses(tripgj); + $scope.populateCommonInfo(tripgj); + }); $ionicScrollDelegate.scrollTop(true); }); }); @@ -188,28 +261,49 @@ angular.module('emission.main.diary.list',['ui-leaflet', // the counts, so let us do it here. if (!angular.isUndefined($scope.data) && !angular.isUndefined($scope.data.currDayTripWrappers)) { $scope.data.currDayTripWrappers.forEach(function(tripWrapper, index, array) { - DiaryHelper.fillCommonTripCount(tripWrapper); + $scope.populateCommonInfo(tripWrapper); }); }; }); }); $scope.setColor = function(mode) { - var colors = {"icon ion-android-bicycle":'green', + var colors = { + "icon ion-android-bicycle": 'green', "icon ion-android-walk":'brown', "icon ion-speedometer":'purple', "icon ion-android-bus": "purple", "icon ion-android-train": "navy", "icon ion-android-car": "salmon", - "icon ion-plane": "red"}; - return { color: colors[mode] }; + "icon ion-plane": "red" + }; + return { + color: colors[mode] + }; } var showNoTripsAlert = function() { - var buttons = [ - {text: 'New', type: 'button-balanced', onTap: function(e) { $state.go('root.main.recent.log'); }}, - {text: 'Force', type: 'button-balanced', onTap: function(e) { $state.go('root.main.control'); }}, - {text: 'OK', type: 'button-balanced', onTap: function(e) { return; }}, + var buttons = [{ + text: 'New', + type: 'button-balanced', + onTap: function (e) { + $state.go('root.main.recent.log'); + } + }, + { + text: 'Force', + type: 'button-balanced', + onTap: function (e) { + $state.go('root.main.control'); + } + }, + { + text: 'OK', + type: 'button-balanced', + onTap: function (e) { + return; + } + }, ]; console.log("No trips found for day "); var alertPopup = $ionicPopup.show({ @@ -237,11 +331,11 @@ angular.module('emission.main.diary.list',['ui-leaflet', */ $scope.refresh = function() { - if ($ionicScrollDelegate.getScrollPosition().top < 5) { + if ($ionicScrollDelegate.getScrollPosition().top < 20) { readAndUpdateForDay(Timeline.data.currDay); $scope.$broadcast('invalidateSize'); } - } + }; /* For UI control */ $scope.groups = []; @@ -285,7 +379,9 @@ angular.module('emission.main.diary.list',['ui-leaflet', // $scope.increaseRestElementsTranslate3d = DiaryHelper.increaseRestElementsTranslate3d; $scope.makeCurrent = function() { - $ionicPopup.alert({template: "Coming soon, after Shankari's quals in early March!"}); + $ionicPopup.alert({ + template: "Coming soon, after Shankari's quals in early March!" + }); } $scope.userModes = [ @@ -320,7 +416,8 @@ angular.module('emission.main.diary.list',['ui-leaflet', { target: '#map-fix-button', content: 'Use this to fix the map tiles if they have not loaded properly.' - }] + } + ] }; var startWalkthrough = function () { @@ -352,14 +449,6 @@ angular.module('emission.main.diary.list',['ui-leaflet', startWalkthrough(); } - $scope.$on('$ionicView.enter', function(ev) { - // Workaround from - // https://github.com/driftyco/ionic/issues/3433#issuecomment-195775629 - if(ev.targetScope !== $scope) - return; - checkDiaryTutorialDone(); - }); - $scope.prevDay = function() { console.log("Called prevDay when currDay = "+Timeline.data.currDay.format('YYYY-MM-DD')); var prevDay = moment(Timeline.data.currDay).subtract(1, 'days'); @@ -374,10 +463,218 @@ angular.module('emission.main.diary.list',['ui-leaflet', readAndUpdateForDay(nextDay); }; - $scope.toDetail = function() { - $state.go('root.main.detail'); + $scope.toDetail = function (param) { + $state.go('root.main.diary-detail', { + tripId: param + }); + }; + + $scope.showModes = DiaryHelper.showModes; + + $ionicPopover.fromTemplateUrl('templates/diary/mode-popover.html', { + scope: $scope + }).then(function (popover) { + $scope.modePopover = popover; + }); + + $scope.openModePopover = function ($event, tripgj) { + var userMode = tripgj.usermode; + if (angular.isDefined(userMode)) { + $scope.selected.mode.value = userMode.value; + } else { + $scope.selected.mode.value = ''; + } + $scope.draftMode = { + "start_ts": tripgj.data.properties.start_ts, + "end_ts": tripgj.data.properties.end_ts + }; + $scope.modeTripgj = tripgj; + Logger.log("in openModePopover, setting draftMode = " + JSON.stringify($scope.draftMode)); + $scope.modePopover.show($event); + }; + + var closeModePopover = function ($event, isOther) { + $scope.selected.mode = { + value: '' + }; + if (isOther == false) + $scope.draftMode = angular.undefined; + Logger.log("in closeModePopover, setting draftMode = " + JSON.stringify($scope.draftMode)); + $scope.modePopover.hide($event); }; + $ionicPopover.fromTemplateUrl('templates/diary/purpose-popover.html', { + scope: $scope + }).then(function (popover) { + $scope.purposePopover = popover; + }); + + $scope.openPurposePopover = function ($event, tripgj) { + var userPurpose = tripgj.userpurpose; + if (angular.isDefined(userPurpose)) { + $scope.selected.purpose.value = userPurpose.value; + } else { + $scope.selected.purpose.value = ''; + } + + $scope.draftPurpose = { + "start_ts": tripgj.data.properties.start_ts, + "end_ts": tripgj.data.properties.end_ts + }; + $scope.purposeTripgj = tripgj; + Logger.log("in openPurposePopover, setting draftPurpose = " + JSON.stringify($scope.draftPurpose)); + $scope.purposePopover.show($event); + }; + + var closePurposePopover = function ($event, isOther) { + $scope.selected.purpose = { + value: '' + }; + if (isOther == false) + $scope.draftPurpose = angular.undefined; + Logger.log("in closePurposePopover, setting draftPurpose = " + JSON.stringify($scope.draftPurpose)); + $scope.purposePopover.hide($event); + }; + + /** + * Store selected value for options + * $scope.selected is for display purpose only + * the value is displayed on popover selected option + */ + $scope.selected = { + mode: { + value: '' + }, + other: { + text: '', + value: '' + }, + purpose: { + 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, choice) { + return function (e) { + if (!$scope.selected.other.text) { + e.preventDefault(); + } else { + Logger.log("in choose other, other = " + JSON.stringify($scope.selected)); + if (choice.value == 'other_mode') { + $scope.storeMode($scope.selected.other, true /* isOther */); + $scope.selected.other = ''; + } else if (choice.value == 'other_purpose') { + $scope.storePurpose($scope.selected.other, true /* isOther */); + $scope.selected.other = ''; + } + return $scope.selected.other; + } + } + }; + + $scope.choosePurpose = function () { + var isOther = false; + if ($scope.selected.purpose.value != "other_purpose") { + $scope.storePurpose($scope.selected.purpose, isOther); + } else { + isOther = true + ConfirmHelper.checkOtherOption($scope.selected.purpose, checkOtherOptionOnTap, $scope); + } + closePurposePopover(); + }; + + $scope.chooseMode = function () { + var isOther = false + if ($scope.selected.mode.value != "other_mode") { + $scope.storeMode($scope.selected.mode, isOther); + } else { + isOther = true + ConfirmHelper.checkOtherOption($scope.selected.mode, checkOtherOptionOnTap, $scope); + } + closeModePopover(); + }; + + /* + * Convert the array of {text, value} objects to a {value: text} map so that + * we can look up quickly without iterating over the list for each trip + */ + + var arrayToMap = function(optionsArray) { + var text2entryMap = {}; + var value2entryMap = {}; + + optionsArray.forEach(function(text2val) { + text2entryMap[text2val.text] = text2val; + value2entryMap[text2val.value] = text2val; + }); + return [text2entryMap, value2entryMap]; + } + + $scope.$on('$ionicView.loaded', function() { + ConfirmHelper.getModeOptions().then(function(modeOptions) { + $scope.modeOptions = modeOptions; + var modeMaps = arrayToMap($scope.modeOptions); + $scope.text2entryMode = modeMaps[0]; + $scope.value2entryMode = modeMaps[1]; + }); + ConfirmHelper.getPurposeOptions().then(function(purposeOptions) { + $scope.purposeOptions = purposeOptions; + var purposeMaps = arrayToMap($scope.purposeOptions); + $scope.text2entryPurpose = purposeMaps[0]; + $scope.value2entryPurpose = purposeMaps[1]; + }); + }); + + $scope.storeMode = function (mode, isOther) { + if(isOther) { + // Let's make the value for user entered modes look consistent with our + // other values + mode.value = ConfirmHelper.otherTextToValue(mode.text); + } + $scope.draftMode.label = mode.value; + Logger.log("in storeMode, after setting mode.value = " + mode.value + ", draftMode = " + JSON.stringify($scope.draftMode)); + var tripToUpdate = $scope.modeTripgj; + $window.cordova.plugins.BEMUserCache.putMessage(MODE_CONFIRM_KEY, $scope.draftMode).then(function () { + $scope.$apply(function() { + if (isOther) { + tripToUpdate.usermode = ConfirmHelper.getFakeEntry(mode.value); + $scope.modeOptions.push(tripToUpdate.usermode); + $scope.value2entryMode[mode.value] = tripToUpdate.usermode; + } else { + tripToUpdate.usermode = $scope.value2entryMode[mode.value]; + } + }); + }); + if (isOther == true) + $scope.draftMode = angular.undefined; + } + + $scope.storePurpose = function (purpose, isOther) { + if (isOther) { + purpose.value = ConfirmHelper.otherTextToValue(purpose.text); + } + $scope.draftPurpose.label = purpose.value; + Logger.log("in storePurpose, after setting purpose.value = " + purpose.value + ", draftPurpose = " + JSON.stringify($scope.draftPurpose)); + var tripToUpdate = $scope.purposeTripgj; + $window.cordova.plugins.BEMUserCache.putMessage(PURPOSE_CONFIRM_KEY, $scope.draftPurpose).then(function () { + $scope.$apply(function() { + if (isOther) { + tripToUpdate.userpurpose = ConfirmHelper.getFakeEntry(purpose.value); + $scope.purposeOptions.push(tripToUpdate.userpurpose); + $scope.value2entryPurpose[purpose.value] = tripToUpdate.userpurpose; + } else { + tripToUpdate.userpurpose = $scope.value2entryPurpose[purpose.value]; + } + }); + }); + if (isOther == true) + $scope.draftPurpose = angular.undefined; + } + $scope.redirect = function(){ $state.go("root.main.current"); }; @@ -402,10 +699,37 @@ angular.module('emission.main.diary.list',['ui-leaflet', // to return bool value and using checkTripState function in ng-show // did not work. $scope.inTrip = function() { - $scope.checkTripState(); - return in_trip; + $ionicPlatform.ready().then(function() { + $scope.checkTripState(); + return in_trip; + }); }; - $scope.showModes = DiaryHelper.showModes; + $ionicPlatform.ready().then(function() { + readAndUpdateForDay(moment().startOf('day')); + + $scope.$on('$ionicView.enter', function(ev) { + // Workaround from + // https://github.com/driftyco/ionic/issues/3433#issuecomment-195775629 + if(ev.targetScope !== $scope) + return; + checkDiaryTutorialDone(); + }); + $scope.$on('$ionicView.afterEnter', function() { + if($rootScope.barDetail){ + readAndUpdateForDay($rootScope.barDetailDate); + $rootScope.barDetail = false; + } + if($rootScope.displayingIncident == true) { + if (angular.isDefined(Timeline.data.currDay)) { + // page was already loaded, reload it automatically + readAndUpdateForDay(Timeline.data.currDay); + } else { + Logger.log("currDay is not defined, load not complete"); + } + $rootScope.displayingIncident = false; + } + }); + }); }); diff --git a/www/js/diary/services.js b/www/js/diary/services.js index f62266cfa..f2c845bc2 100644 --- a/www/js/diary/services.js +++ b/www/js/diary/services.js @@ -3,7 +3,7 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', 'emission.services', 'emission.main.common.services', 'emission.incident.posttrip.manual']) -.factory('DiaryHelper', function(Timeline, CommonGraph, PostTripManualMarker){ +.factory('DiaryHelper', function(CommonGraph, PostTripManualMarker){ var dh = {}; // dh.expandEarlierOrLater = function(id) { // document.querySelector('#hidden-' + id.toString()).setAttribute('style', 'display: block;'); @@ -102,7 +102,8 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', } } dh.isDraft = function(tripgj) { - if (tripgj.data.features.length == 3 && + if (// tripgj.data.features.length == 3 && // reinstate after the local and remote paths are unified + angular.isDefined(tripgj.data.features[2].features) && tripgj.data.features[2].features[0].properties.feature_type == "section" && tripgj.data.features[2].features[0].properties.sensed_mode == "MotionTypes.UNPROCESSED") { return true; @@ -164,6 +165,14 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', dh.getHumanReadable(section.properties.sensed_mode)]; return retVal; }; + + dh.getLocalTimeString = function(dt) { + var hr = ((dt.hour > 12))? dt.hour - 12 : dt.hour; + var post = ((dt.hour >= 12))? " pm" : " am"; + var min = (dt.minute.toString().length == 1)? "0" + dt.minute.toString() : dt.minute.toString(); + return hr + ":" + min + post; + } + dh.getFormattedTime = function(ts_in_secs) { if (angular.isDefined(ts_in_secs)) { return moment(ts_in_secs * 1000).format('LT'); @@ -279,7 +288,7 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', dh.fillCommonTripCount = function(tripWrapper) { var cTrip = CommonGraph.trip2Common(tripWrapper.data.id); if (!angular.isUndefined(cTrip)) { - tripWrapper.common_count = cTrip.trips.length; + tripWrapper.common.count = cTrip.trips.length; } }; dh.directiveForTrip = function(trip) { @@ -293,12 +302,12 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', retVal.stops = trip.stops; retVal.sections = trip.sections; retVal.tripSummary = trip.tripSummary; - dh.fillCommonTripCount(retVal); // Hardcoding to avoid repeated nominatim calls - // retVal.start_place.properties.displayName = "Start"; - // retVal.start_place.properties.displayName = "End"; + // retVal.start_place.properties.display_name = "Start"; + // retVal.start_place.properties.display_name = "End"; return retVal; }; + dh.userModes = [ "walk", "bicycle", "car", "bus", "train", "unicorn" ]; @@ -346,8 +355,8 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', // console.log("onEachFeature called with "+JSON.stringify(feature)); switch(feature.properties.feature_type) { case "stop": layer.bindPopup(""+feature.properties.duration); break; - case "start_place": layer.bindPopup(""+feature.properties.displayName); break; - case "end_place": layer.bindPopup(""+feature.properties.displayName); break; + case "start_place": layer.bindPopup(""+feature.properties.display_name); break; + case "end_place": layer.bindPopup(""+feature.properties.display_name); break; case "section": layer.on('click', PostTripManualMarker.startAddingIncidentToSection(feature, layer)); break; case "incident": PostTripManualMarker.displayIncident(feature, layer); break; @@ -398,15 +407,44 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', } }; - return dh; + var printUserInput = function(ui) { + return ui.data.start_ts + " -> "+ ui.data.end_ts + + " " + ui.data.label + " logged at "+ ui.metadata.write_ts; + } + + dh.getUserInputForTrip = function(tripProp, userInputList) { + var potentialCandidates = userInputList.filter(function(userInput) { + return userInput.data.start_ts >= tripProp.start_ts && userInput.data.end_ts <= tripProp.end_ts; + }); + if (potentialCandidates.length === 0) { + Logger.log("In getUserInputForTripStartEnd, no potential candidates, returning []"); + return undefined; + } + + if (potentialCandidates.length === 1) { + Logger.log("In getUserInputForTripStartEnd, one potential candidate, returning "+ printUserInput(potentialCandidates[0])); + return potentialCandidates[0]; + } + + Logger.log("potentialCandidates are "+potentialCandidates.map(printUserInput)); + var sortedPC = potentialCandidates.sort(function(pc1, pc2) { + return pc2.metadata.write_ts - pc1.metadata.write_ts; + }); + var mostRecentEntry = sortedPC[0]; + Logger.log("Returning mostRecentEntry "+printUserInput(mostRecentEntry)); + return mostRecentEntry; + } + + return dh; }) .factory('Timeline', function(CommHelper, $http, $ionicLoading, $window, $rootScope, CommonGraph, UnifiedDataLoader, Logger) { - var timeline = {}; + var timeline = {}; // corresponds to the old $scope.data. Contains all state for the current // day, including the indication of the current day timeline.data = {}; + timeline.data.unifiedConfirmsResults = null; timeline.UPDATE_DONE = "TIMELINE_UPDATE_DONE"; // Internal function, not publicly exposed @@ -880,6 +918,8 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', // First, we try the server var tripsFromServerPromise = timeline.updateFromServer(day); var isProcessingCompletePromise = timeline.isProcessingComplete(day); + + // Deal with all the trip retrieval Promise.all([tripsFromServerPromise, isProcessingCompletePromise]) .then(function([processedTripList, completeStatus]) { console.log("Promise.all() finished successfully with length " @@ -897,6 +937,19 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', } else { return tripList; } + }).then(function(combinedTripList) { + var tq = { key: 'write_ts', startTs: 0, endTs: moment().endOf('day').unix(), }; + return Promise.all([ + UnifiedDataLoader.getUnifiedMessagesForInterval('manual/mode_confirm', tq), + UnifiedDataLoader.getUnifiedMessagesForInterval('manual/purpose_confirm', tq) + ]).then(function(results) { + timeline.data.unifiedConfirmsResults = { + modes: results[0], + purposes: results[1], + }; + return combinedTripList; + }); + return combinedTripList; }).then(function(combinedTripList) { processOrDisplayNone(day, combinedTripList); }).catch(function(error) { @@ -928,6 +981,10 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', return timeline.data.tripMap[tripId]; }; + timeline.getTripWrapper = function(tripId) { + return timeline.data.tripWrapperMap[tripId]; + }; + /* Let us assume that we have recieved a list of trips for that date from somewhere (either local usercache or the internet). Now, what do we need to process them? @@ -959,15 +1016,25 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', }); timeline.data.currDayTrips.forEach(function(trip, index, array) { - if (angular.isDefined(trip.start_place.properties.displayName)) { - console.log("Already have display name "+ dt.start_place.properties.displayName +" for start_place") + if (angular.isDefined(trip.start_place.properties.display_name)) { + if (trip.start_place.properties.display_name != ", ") { + console.log("Already have display name "+ trip.start_place.properties.display_name +" for start_place") + } else { + console.log("Got display name "+ trip.start_place.properties.display_name +" for start_place, but it is blank, trying OSM nominatim now..."); + CommonGraph.getDisplayName('place', trip.start_place); + } } else { console.log("Don't have display name for start place, going to query nominatim") CommonGraph.getDisplayName('place', trip.start_place); } - if (angular.isDefined(trip.end_place.properties.displayName)) { - console.log("Already have display name " + dt.end_place.properties.displayName + " for end_place") + if (angular.isDefined(trip.end_place.properties.display_name)) { + if (trip.end_place.properties.display_name != ", ") { + console.log("Already have display name " + trip.end_place.properties.display_name + " for end_place") + } else { + console.log("Got display name "+ trip.end_place.properties.display_name +" for end_place, but it is blank, trying OSM nominatim now..."); + CommonGraph.getDisplayName('place', trip.end_place); + } } else { console.log("Don't have display name for end place, going to query nominatim") CommonGraph.getDisplayName('place', trip.end_place); @@ -989,6 +1056,16 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', $ionicLoading.hide(); }; + timeline.setTripWrappers = function(tripWrapperList) { + timeline.data.currDayTripWrappers = tripWrapperList; + + timeline.data.tripWrapperMap = {}; + + timeline.data.currDayTripWrappers.forEach(function(tripw, index, array) { + timeline.data.tripWrapperMap[tripw.data.id] = tripw; + }); + } + // TODO: Should this be in the factory or in the scope? var generateDaySummary = function() { var dayMovingTime = 0; diff --git a/www/js/incident/post-trip-manual.js b/www/js/incident/post-trip-manual.js index 076650040..d2e52fc1c 100644 --- a/www/js/incident/post-trip-manual.js +++ b/www/js/incident/post-trip-manual.js @@ -279,10 +279,12 @@ angular.module('emission.incident.posttrip.manual', ['emission.plugin.logger', Logger.log("section "+feature.properties.start_fmt_time + " -> "+feature.properties.end_fmt_time + " bound incident addition "); - var allPoints = getSectionPoints(feature); - var trip = Timeline.getTrip(feature.properties.trip_id.$oid); - var featureArray = trip.features; - return ptmm.startAddingIncidentToPoints(layer, allPoints, featureArray); + return function(e) { + var allPoints = getSectionPoints(feature); + var trip = Timeline.getTrip(feature.properties.trip_id.$oid); + var featureArray = trip.features; + startAddingIncidentToPointsImpl(e, layer, allPoints, featureArray); + } } var getAllPointsForTrip = function(trip) { @@ -304,9 +306,11 @@ angular.module('emission.incident.posttrip.manual', ['emission.plugin.logger', Logger.log("section "+trip.properties.start_fmt_time + " -> "+trip.properties.end_fmt_time + " bound incident addition "); - var allPoints = getAllPointsForTrip(trip); - var featureArray = trip.features; - return ptmm.startAddingIncidentToPoints(map, allPoints, featureArray); + return function(e) { + var allPoints = getAllPointsForTrip(trip); + var featureArray = trip.features; + startAddingIncidentToPointsImpl(e, map, allPoints, featureArray); + } } /* @@ -334,6 +338,21 @@ angular.module('emission.incident.posttrip.manual', ['emission.plugin.logger', + " bound incident addition "); return function(e) { + startAddingIncidentToPointsImpl(e, layer, allPoints, geojsonFeatureArray); + } + }; + + /* + * This is the implementation of `startAddingIncidentToSection`, + * `startAddingIncidentToTrip` and `startAddingIncidentToPoints` + * It is typically wrapped in a curried function that takes only the event + * (`e`) as the input and gets the other parameters through currying. Before + * this, we used have only one curried function and have the others call it, + * but then the points were read when we registered the function instead of + * when it was invoked. + */ + + var startAddingIncidentToPointsImpl = function(e, layer, allPoints, geojsonFeatureArray) { Logger.log("points "+getFormattedTime(allPoints[0].ts) + " -> "+getFormattedTime(allPoints[allPoints.length -1].ts) + " received click event, adding stress popup at " @@ -398,8 +417,7 @@ angular.module('emission.incident.posttrip.manual', ['emission.plugin.logger', return true; } }); - } - }; + }; }; // END: Adding incidents diff --git a/www/js/main.js b/www/js/main.js index c62785966..a542bfbe7 100644 --- a/www/js/main.js +++ b/www/js/main.js @@ -7,7 +7,7 @@ angular.module('emission.main', ['emission.main.recent', 'emission.main.common', 'emission.main.heatmap', 'emission.main.metrics', - 'emission.incident.posttrip.map', + 'emission.tripconfirm.posttrip.map', 'emission.services']) .config(function($stateProvider, $ionicConfigProvider, $urlRouterProvider) { @@ -105,6 +105,20 @@ angular.module('emission.main', ['emission.main.recent', } }) + .state('root.main.tripconfirm', { + url: "/tripconfirm", + params: { + start_ts: null, + end_ts: null + }, + views: { + 'main-control': { + templateUrl: "templates/tripconfirm/map.html", + controller: 'PostTripMapCtrl' + } + } + }) + .state('root.main.log', { url: '/log', views: { diff --git a/www/js/splash/startprefs.js b/www/js/splash/startprefs.js index afa41765d..1a16bab03 100644 --- a/www/js/splash/startprefs.js +++ b/www/js/splash/startprefs.js @@ -168,8 +168,9 @@ angular.module('emission.splash.startprefs', ['emission.plugin.logger', if (temp == 'goals') { return 'root.main.goals'; } else if ($rootScope.displayingIncident == true) { + logger.log("Showing tripconfirm from startprefs"); $rootScope.displayingIncident = false; - return 'root.main.diary'; + return 'root.main.tripconfirm'; } else if (angular.isDefined($rootScope.redirectTo)) { var redirState = $rootScope.redirectTo; $rootScope.redirectTo = undefined; diff --git a/www/js/tripconfirm/post-trip-map-display.js b/www/js/tripconfirm/post-trip-map-display.js new file mode 100644 index 000000000..53aea4db4 --- /dev/null +++ b/www/js/tripconfirm/post-trip-map-display.js @@ -0,0 +1,306 @@ +'use strict'; +angular.module('emission.tripconfirm.posttrip.map',['ui-leaflet', 'ng-walkthrough', + 'emission.plugin.kvstore', + 'emission.services', + 'emission.tripconfirm.services', + 'emission.plugin.logger', + 'emission.main.diary.services']) + +.controller("PostTripMapCtrl", function($scope, $window, $state, + $stateParams, $ionicLoading, + leafletData, leafletMapEvents, nzTour, KVStore, + Logger, DiaryHelper, ConfirmHelper, Config, + UnifiedDataLoader, $ionicSlideBoxDelegate, $ionicPopup) { + Logger.log("controller PostTripMapDisplay called with params = "+ + JSON.stringify($stateParams)); + var MODE_CONFIRM_KEY = "manual/mode_confirm"; + var PURPOSE_CONFIRM_KEY = "manual/purpose_confirm"; + + $scope.mapCtrl = {}; + angular.extend($scope.mapCtrl, { + defaults: {} + }); + angular.extend($scope.mapCtrl.defaults, Config.getMapTiles()); + var LOC_KEY = "background/filtered_location"; + + $scope.mapCtrl.start_ts = $stateParams.start_ts; + $scope.mapCtrl.end_ts = $stateParams.end_ts; + + $scope.$on('$ionicView.enter', function() { + // we want to initialize these while entering the screen instead of while + // creating the controller, because the app may stick around for a while, + // and then when the user clicks on a notification, they will re-enter this + // screen. + Logger.log("entered post-trip map screen, prompting for values"); + $scope.draftMode = {"start_ts": $stateParams.start_ts, "end_ts": $stateParams.end_ts}; + $scope.draftPurpose = {"start_ts": $stateParams.start_ts, "end_ts": $stateParams.end_ts}; + }); + + $scope.$on('$ionicView.leave', function() { + Logger.log("entered post-trip map screen, prompting for values"); + $scope.draftMode = angular.undefined; + $scope.draftPurpose = angular.undefined; + }); + + /* + var mapEvents = leafletMapEvents.getAvailableMapEvents(); + for (var k in mapEvents) { + var eventName = 'leafletDirectiveMap.incident.' + mapEvents[k]; + $scope.$on(eventName, function(event, data){ + Logger.log("in mapEvents, event = "+JSON.stringify(event.name)+ + " leafletEvent = "+JSON.stringify(data.leafletEvent.type)+ + " leafletObject = "+JSON.stringify(data.leafletObject.getBounds())); + $scope.eventDetected = event.name; + }); + } + */ + + $scope.refreshMap = function(start_ts, end_ts) { + var db = $window.cordova.plugins.BEMUserCache; + var tq = {key: "write_ts", + startTs: start_ts, + endTs: end_ts}; + $scope.mapCtrl.cache = {}; + $scope.mapCtrl.server = {}; + // Clear everything before we start loading new data + $scope.mapCtrl.geojson = {}; + $scope.mapCtrl.geojson.pointToLayer = DiaryHelper.pointFormat; + $scope.mapCtrl.geojson.data = { + type: "Point", + coordinates: [-122, 37], + properties: { + feature_type: "location" + } + }; + Logger.log("About to query buffer for "+JSON.stringify(tq)); + $ionicLoading.show({ + template: 'Loading...' + }); + UnifiedDataLoader.getUnifiedSensorDataForInterval(LOC_KEY, tq) + .then(function(resultList) { + Logger.log("Read data of length "+resultList.length); + $ionicLoading.show({ + template: 'Mapping '+resultList.length+' points' + }); + if (resultList.length > 0) { + var pointCoords = resultList.map(function(locEntry) { + return [locEntry.data.longitude, locEntry.data.latitude]; + }); + var pointTimes = resultList.map(function(locEntry) { + return locEntry.data.ts; + }); + $scope.mapCtrl.cache.features = [{ + type: "Feature", + geometry: { + type: "LineString", + coordinates: pointCoords, + properties: { + times: pointTimes + } + } + }]; + $scope.mapCtrl.cache.data = { + type: "FeatureCollection", + features: $scope.mapCtrl.cache.features, + properties: { + start_ts: start_ts, + end_ts: end_ts + } + }; + + Logger.log("About to get section points"); + var points = resultList.map(function(locEntry) { + var coords = [locEntry.data.longitude, locEntry.data.latitude]; + // Logger.log("coords = "+coords); + return {loc: coords, + latlng: L.GeoJSON.coordsToLatLng(coords), + ts: locEntry.data.ts}; + }); + $scope.mapCtrl.cache.points = points; + Logger.log("Finished getting section points"); + /* + $scope.mapCtrl.cache.points = resultList.map(function(locEntry) { + Logger.log("locEntry = "+JSON.serialize(locEntry)); + var currMappedPoint = {loc: locEntry.data.loc, + latlng: L.GeoJSON.coordsToLatLng(locEntry.data.loc), + ts: locEntry.data.ts}; + Logger.log("Mapped point "+ JSON.stringify(locEntry)+" to "+currMappedPoint); + return currMappedPoint; + return locEntry.data.ts; + }); + Logger.log("Finished getting section points"); + */ + + $scope.mapCtrl.cache.loaded = true; + $scope.$apply(function() { + // data is in the cache, so we can just load it from there + // Logger.log("About to set geojson = "+JSON.stringify($scope.mapCtrl.cache.data)); + $scope.mapCtrl.geojson.data = $scope.mapCtrl.cache.data; + }); + + } + $ionicLoading.hide(); + }) + .catch(function(error) { + var errStr = JSON.stringify(error); + $ionicLoading.hide(); + Logger.log(errStr); + $ionicPopup.alert({ + title: "Unable to retrieve data", + template: errStr + }); + }); + } + + $scope.refreshWholeMap = function() { + $scope.refreshMap($scope.mapCtrl.start_ts, $scope.mapCtrl.end_ts); + } + + $scope.refreshTiles = function() { + $scope.$broadcast('invalidateSize'); + }; + + $scope.getFormattedDate = DiaryHelper.getFormattedDate; + $scope.getFormattedTime = DiaryHelper.getFormattedTime; + $scope.refreshMap($scope.mapCtrl.start_ts, $scope.mapCtrl.end_ts); + + /* START: ng-walkthrough code */ + // Tour steps + var tour = { + config: { + mask: { + visibleOnNoTarget: true, + clickExit: true + } + }, + steps: [{ + target: '#mode_list', + content: 'Scroll for more options' + }] + }; + + var startWalkthrough = function () { + nzTour.start(tour).then(function(result) { + Logger.log("post trip mode walkthrough start completed, no error"); + }).catch(function(err) { + Logger.log("post trip mode walkthrough start errored" + err); + }); + }; + + + var checkTripConfirmTutorialDone = function () { + var TRIP_CONFIRM_DONE_KEY = 'tripconfirm_tutorial_done'; + var tripconfirmTutorialDone = KVStore.getDirect(TRIP_CONFIRM_DONE_KEY); + if (!tripconfirmTutorialDone) { + startWalkthrough(); + KVStore.set(TRIP_CONFIRM_DONE_KEY, true); + } + }; + + $scope.startWalkthrough = function () { + startWalkthrough(); + } + + $scope.closeView = function () { + $state.go('root.main.control'); + } + + $scope.$on('$ionicView.afterEnter', function(ev) { + // Workaround from + // https://github.com/driftyco/ionic/issues/3433#issuecomment-195775629 + if(ev.targetScope !== $scope) + return; + checkTripConfirmTutorialDone(); + }); + /* END: ng-walkthrough code */ + + $scope.selected = {mode: {value: ''},purpose: {value: ''},other: {text: ''}, other_to_store:''}; + + var checkOtherOptionOnTap = function($scope, choice) { + return function(e) { + if (!$scope.selected.other.text) { + e.preventDefault(); + } else { + $scope.selected.other_to_store = $scope.selected.other.text; + $scope.selected.other.text = ''; + return $scope.selected.other; + } + }; + }; + + $scope.choosePurpose = function() { + if($scope.selected.purpose.value == "other_purpose"){ + ConfirmHelper.checkOtherOption($scope.selected.purpose, checkOtherOptionOnTap, $scope); + } + }; + + $scope.chooseMode = function (){ + if($scope.selected.mode.value == "other_mode"){ + ConfirmHelper.checkOtherOption($scope.selected.mode, checkOtherOptionOnTap, $scope); + } + }; + + $scope.secondSlide = false; + + $scope.nextSlide = function() { + if($scope.selected.mode.value == "other_mode" && $scope.selected.other_to_store.length > 0) { + $scope.secondSlide = true; + console.log($scope.selected.other_to_store); + // store other_to_store here + $scope.storeMode($scope.selected.other_to_store, true /* isOther */); + $ionicSlideBoxDelegate.next(); + } else if ($scope.selected.mode.value != "other_mode" && $scope.selected.mode.value.length > 0) { + $scope.secondSlide = true; + console.log($scope.selected.mode); + // This storeMode expects a string, not an object with a value string + $scope.storeMode($scope.selected.mode.value, false /* isOther */); + $ionicSlideBoxDelegate.next(); + } + }; + + $scope.doneSlide = function() { + if($scope.selected.purpose.value == "other_purpose" && $scope.selected.other_to_store.length > 0) { + console.log($scope.selected.other_to_store); + // store other_to_store here + $scope.storePurpose($scope.selected.other_to_store, true /* isOther */); + $scope.closeView(); + } else if ($scope.selected.purpose.value != "other_purpose" && $scope.selected.purpose.value.length > 0) { + console.log($scope.selected.purpose); + // This storePurpose expects a string, not an object with a value string + $scope.storePurpose($scope.selected.purpose.value, false /*is Other */); + $scope.closeView(); + } + }; + + $scope.disableSwipe = function() { + $ionicSlideBoxDelegate.enableSlide(false); + }; + + ConfirmHelper.getModeOptions().then(function(modeOptions) { + $scope.modeOptions = modeOptions; + }); + + ConfirmHelper.getPurposeOptions().then(function(purposeOptions) { + $scope.purposeOptions = purposeOptions; + }); + + $scope.storeMode = function(mode_val, isOther) { + if (isOther) { + mode_val = ConfirmHelper.otherTextToValue(mode_val); + } + $scope.draftMode.label = mode_val; + Logger.log("in storeMode, after setting mode_val = "+mode_val+", draftMode = "+JSON.stringify($scope.draftMode)); + $window.cordova.plugins.BEMUserCache.putMessage(MODE_CONFIRM_KEY, $scope.draftMode); + } + + $scope.storePurpose = function(purpose_val, isOther) { + if (isOther) { + purpose_val = ConfirmHelper.otherTextToValue(purpose_val); + } + $scope.draftPurpose.label = purpose_val; + Logger.log("in storePurpose, after setting purpose_val = "+purpose_val+", draftPurpose = "+JSON.stringify($scope.draftPurpose)); + $window.cordova.plugins.BEMUserCache.putMessage(PURPOSE_CONFIRM_KEY, $scope.draftPurpose); + } + + +}); diff --git a/www/js/tripconfirm/post-trip-prompt.js b/www/js/tripconfirm/post-trip-prompt.js new file mode 100644 index 000000000..68e38cfc9 --- /dev/null +++ b/www/js/tripconfirm/post-trip-prompt.js @@ -0,0 +1,253 @@ +'use strict'; + +angular.module('emission.tripconfirm.posttrip.prompt', ['emission.plugin.logger']) +.factory("PostTripAutoPrompt", function($window, $ionicPlatform, $rootScope, $state, + $ionicPopup, Logger) { + var ptap = {}; + var REPORT = 737678; // REPORT on the phone keypad + var TRIP_CONFIRM_TEXT = 'TRIP_CONFIRM'; + var TRIP_END_EVENT = "trip_ended"; + + var reportMessage = function(platform) { + var platformSpecificMessage = { + "ios": "Swipe left or tap to add information about this trip.", + "android": "See options or tap to add information about this trip." + }; + var selMessage = platformSpecificMessage[platform]; + if (!angular.isDefined(selMessage)) { + selMessage = "Tap to add information about this trip."; + } + return selMessage; + }; + + var getTripEndReportNotification = function() { + var actions = [{ + identifier: 'MUTE', + title: 'Mute', + icon: 'res://ic_moreoptions', + activationMode: 'background', + destructive: false, + authenticationRequired: false + }, { + identifier: 'SNOOZE', + title: 'Snooze', + icon: 'res://ic_moreoptions', + activationMode: 'background', + destructive: false, + authenticationRequired: false + }, { + identifier: 'CHOOSE', + title: 'Choose', + icon: 'res://ic_signin', + activationMode: 'foreground', + destructive: false, + authenticationRequired: false + }]; + + var reportNotifyConfig = { + id: REPORT, + title: "How and why did you come here?", + text: reportMessage(ionic.Platform.platform()), + icon: 'file://img/icon.png', + smallIcon: 'res://ic_mood_question.png', + sound: null, + actions: actions, + category: TRIP_CONFIRM_TEXT, + autoClear: true + }; + Logger.log("Returning notification config "+JSON.stringify(reportNotifyConfig)); + return reportNotifyConfig; + } + + ptap.registerTripEnd = function() { + Logger.log( "registertripEnd received!" ); + // iOS + var notifyPlugin = $window.cordova.plugins.BEMTransitionNotification; + notifyPlugin.addEventListener(notifyPlugin.TRIP_END, getTripEndReportNotification()) + .then(function(result) { + // $window.broadcaster.addEventListener("TRANSITION_NAME", function(result) { + Logger.log("Finished registering "+notifyPlugin.TRIP_END+" with result "+JSON.stringify(result)); + }) + .catch(function(error) { + Logger.log(JSON.stringify(error)); + $ionicPopup.alert({ + title: "Unable to register notifications for trip end", + template: JSON.stringify(error) + }); + });; + } + + var getFormattedTime = function(ts_in_secs) { + if (angular.isDefined(ts_in_secs)) { + return moment(ts_in_secs * 1000).format('LT'); + } else { + return "---"; + } + }; + + var promptReport = function(notification, state, data) { + Logger.log("About to prompt choose the mode for the trip"); + var newScope = $rootScope.$new(); + angular.extend(newScope, notification.data); + newScope.getFormattedTime = getFormattedTime; + Logger.log("notification = "+JSON.stringify(notification)); + Logger.log("state = "+JSON.stringify(state)); + Logger.log("data = "+JSON.stringify(data)); + return $ionicPopup.show({title: "Choose the travel mode and purpose of this trip", + scope: newScope, + template: "{{getFormattedTime(start_ts)}} -> {{getFormattedTime(end_ts)}}", + buttons: [{ + text: 'Choose Mode', + type: 'button-positive', + onTap: function(e) { + // e.preventDefault() will stop the popup from closing when tapped. + return true; + } + }, { + text: 'Skip', + type: 'button-positive', + onTap: function(e) { + return false; + } + }] + }) + } + + var cleanDataIfNecessary = function(notification, state, data) { + if ($ionicPlatform.is('ios') && angular.isDefined(notification.data)) { + Logger.log("About to parse "+notification.data); + notification.data = JSON.parse(notification.data); + } + }; + + var displayCompletedTrip = function(notification, state, data) { + $rootScope.displayingIncident = true; + Logger.log("About to display completed trip from Notification"); + $state.go("root.main.tripconfirm", notification.data); + }; + + var checkCategory = function(notification) { + if (notification.category == TRIP_CONFIRM_TEXT) { + return true; + } else { + return false; + } + } + + ptap.registerUserResponse = function() { + Logger.log( "registerUserResponse received!" ); + $window.cordova.plugins.notification.local.on('action', function (notification, state, data) { + if (!checkCategory(notification)) { + Logger.log("notification "+notification+" is not an mode choice, returning..."); + return; + } + if (data.identifier === 'CHOOSE') { + Logger.log("Notification, action event"); + cleanDataIfNecessary(notification, state, data); + displayCompletedTrip(notification, state, data); + } else if (data.identifier == 'SNOOZE') { + var now = new Date().getTime(), + _30_mins_from_now = new Date(now + 30 * 60 * 1000); + var after_30_mins_prompt = getTripEndReportNotification(); + after_30_mins_prompt.at = _30_mins_from_now; + $window.cordova.plugins.notification.local.schedule([after_30_mins_prompt]); + if ($ionicPlatform.is('android')) { + $ionicPopup.alert({ + title: "Snoozed reminder", + template: "Will reappear in 30 mins" + }); + } + } else if (data.identifier === 'MUTE') { + var now = new Date().getTime(), + _1_min_from_now = new Date(now + 60 * 1000); + var notifyPlugin = $window.cordova.plugins.BEMTransitionNotification; + notifyPlugin.disableEventListener(notifyPlugin.TRIP_END, notification).then(function() { + if ($ionicPlatform.is('ios')) { + $window.cordova.plugins.notification.local.schedule([{ + id: REPORT, + title: "Notifications for TRIP_END incident report muted", + text: "Can be re-enabled from the Profile -> Developer Zone screen. Select to re-enable now, clear to ignore", + at: _1_min_from_now, + data: {redirectTo: "root.main.control"} + }]); + } else if ($ionicPlatform.is('android')) { + $ionicPopup.show({ + title: "Muted", + template: "Notifications for TRIP_END incident report muted", + buttons: [{ + text: 'Unmute', + type: 'button-positive', + onTap: function(e) { + return true; + } + }, { + text: 'Keep muted', + type: 'button-positive', + onTap: function(e) { + return false; + } + }] + }).then(function(res) { + if(res == true) { + notifyPlugin.enableEventListener(notifyPlugin.TRIP_END, notification); + } else { + Logger.log("User chose to keep the transition muted"); + } + }); + } + }).catch(function(error) { + $ionicPopup.alert({ + title: "Error while muting notifications for trip end. Try again later.", + template: JSON.stringify(error) + }); + }); + } + }); + $window.cordova.plugins.notification.local.on('clear', function (notification, state, data) { + // alert("notification cleared, no report"); + }); + $window.cordova.plugins.notification.local.on('cancel', function (notification, state, data) { + // alert("notification cancelled, no report"); + }); + $window.cordova.plugins.notification.local.on('trigger', function (notification, state, data) { + // alert("triggered, no action"); + Logger.log("Notification triggered"); + if (!checkCategory(notification)) { + Logger.log("notification "+notification+" is not an mode choice, returning..."); + return; + } + cleanDataIfNecessary(notification, state, data); + if($ionicPlatform.is('ios')) { + promptReport(notification, state, data).then(function(res) { + if (res == true) { + Logger.log("About to go to prompt page"); + displayCompletedTrip(notification, state, data); + } else { + Logger.log("Skipped confirmation reporting"); + } + }); + } else { + Logger.log("About to go to prompt page"); + displayCompletedTrip(notification, state, data); + } + }); + $window.cordova.plugins.notification.local.on('click', function (notification, state, data) { + // alert("clicked, no action"); + Logger.log("Notification, click event"); + if (!checkCategory(notification)) { + Logger.log("notification "+notification+" is not an mode choice, returning..."); + return; + } + cleanDataIfNecessary(notification, state, data); + displayCompletedTrip(notification, state, data); + }); + }; + + $ionicPlatform.ready().then(function() { + ptap.registerTripEnd(); + ptap.registerUserResponse(); + }); + + return ptap; + +}); diff --git a/www/js/tripconfirm/trip-confirm-services.js b/www/js/tripconfirm/trip-confirm-services.js new file mode 100644 index 000000000..e9475a5fa --- /dev/null +++ b/www/js/tripconfirm/trip-confirm-services.js @@ -0,0 +1,93 @@ +angular.module('emission.tripconfirm.services', ['ionic']) +.factory("ConfirmHelper", function($http, $ionicPopup) { + var ch = {}; + ch.otherModes = []; + ch.otherPurposes = []; + + var fillInOptions = function(confirmConfig) { + if(confirmConfig.data.length == 0) { + throw "blank string instead of missing file on dynamically served app"; + } + ch.modeOptions = confirmConfig.data.modeOptions; + ch.purposeOptions = confirmConfig.data.purposeOptions; + } + + var loadAndPopulateOptions = function(filename) { + return $http.get(filename) + .then(fillInOptions) + .catch(function(err) { + console.log("error "+JSON.stringify(err)+" while reading confirm options, reverting to defaults"); + return $http.get(filename+".sample") + .then(fillInOptions) + .catch(function(err) { + console.log("error "+JSON.stringify(err)+" while reading default confirm options, giving up"); + }); + }); + } + + /* + * Lazily loads the options and returns the chosen one. Using this option + * instead of an in-memory data structure so that we can return a promise + * and not have to worry about when the data is available. + */ + ch.getModeOptions = function() { + if (!angular.isDefined(ch.modeOptions)) { + return loadAndPopulateOptions("json/trip_confirm_options.json") + .then(function() { return ch.modeOptions; }); + } else { + return Promise.resolve(ch.modeOptions); + } + } + + ch.getPurposeOptions = function() { + if (!angular.isDefined(ch.purposeOptions)) { + return loadAndPopulateOptions("json/trip_confirm_options.json") + .then(function() { return ch.purposeOptions; }); + } else { + return Promise.resolve(ch.purposeOptions); + } + } + + ch.checkOtherOption = function(choice, onTapFn, $scope) { + if(choice.value == 'other_mode' || choice.value == 'other_purpose') { + var text = choice.value == 'other_mode' ? "mode" : "purpose"; + $ionicPopup.show({title: "Please fill in the " + text + " not listed.", + scope: $scope, + template: '', + buttons: [ + { text: 'Cancel', + onTap: function(e) { + $scope.selected.mode = ''; + $scope.selected.purpose = ''; + } + }, { + text: 'Save', + type: 'button-positive', + onTap: onTapFn($scope, choice) + } + ] + }); + } + } + + ch.otherTextToValue = function(otherText) { + return otherText.toLowerCase().replace(" ", "_"); + } + + ch.otherValueToText = function(otherValue) { + var words = otherValue.replace("_", " ").split(" "); + if (words.length == 0) { + return ""; + } + return words.map(function(word) { + return word[0].toUpperCase() + word.slice(1); + }).join(" "); + } + + ch.getFakeEntry = function(otherValue) { + return {text: ch.otherValueToText(otherValue), + value: otherValue}; + } + + return ch; +}) diff --git a/www/json/trip_confirm_options.json.sample b/www/json/trip_confirm_options.json.sample new file mode 100644 index 000000000..f93eebe41 --- /dev/null +++ b/www/json/trip_confirm_options.json.sample @@ -0,0 +1,25 @@ +{ + "modeOptions" : [ + {"text":"Walk", "value":"walk"}, + {"text":"Bike","value":"bike"}, + {"text":"Drove Alone","value":"drove_alone"}, + {"text":"Shared Ride","value":"shared_ride"}, + {"text":"Taxi/Uber/Lyft","value":"taxi"}, + {"text":"Bus","value":"bus"}, + {"text":"Train","value":"train"}, + {"text":"Free Shuttle","value":"free_shuttle"}, + {"text":"Other","value":"other_mode"}], + "purposeOptions" : [ + {"text":"Home", "value":"home"}, + {"text":"Work","value":"work"}, + {"text":"School","value":"school"}, + {"text":"Transit transfer", "value":"transit_transfer"}, + {"text":"Shopping","value":"shopping"}, + {"text":"Meal","value":"meal"}, + {"text":"Pick-up/Drop off","value":"pick_drop"}, + {"text":"Personal/Medical","value":"personal_med"}, + {"text":"Recreation/Exercise","value":"exercise"}, + {"text":"Entertainment/Social","value":"entertainment"}, + {"text":"Religious", "value":"religious"}, + {"text":"Other","value":"other_purpose"}] +} diff --git a/www/templates/common/place-detail.html b/www/templates/common/place-detail.html index 9da41897a..9b936d776 100644 --- a/www/templates/common/place-detail.html +++ b/www/templates/common/place-detail.html @@ -5,7 +5,7 @@