'use strict'; var loginErrorCodes = [ "7033", // errorAnotherSessionLoggedIn "7031", // errorAccountNotLoggedIn "7032" // errorAccountSessionExpired ]; var PaymentMethods = { CASH: 'CASH', CREDIT_CARD: 'CREDIT_CARD', VOUCHER: 'VOUCHER' }; var isLoginErrorCode = function (errorCode) { return loginErrorCodes.indexOf(errorCode) >= 0; }; /* Services */ var services = angular.module('afroApp.services', []); services.factory('loginErrorHttpInterceptor', ['$q', '$location', function ($q, $location) { // passwordResetDriverRoute and passwordResetPassengerRoute have to e checked differently as they contain dynamic elements in their urls var safeRoutes = [onBoardRoute, signInRoute, signUpRoute, forgotPasswordPassengerRoute, emailConfirmationRoute]; var service = { // TODO: currently, this will run on every HTTP call... but we may want a way of limiting to to GoCatch specific calls // eg our /api/v1/signedin call which is AppEngine specific... and may fall away if the interceptor works well. // Ref: the promise tracker which puts something in the config options of the HTTP call and checks for that. // NOTE: this also gets hit when angular is fetching the html templates. // NOTE: angular/js does not raise when accessing a non-existent property. // could also look at: https://github.com/witoldsz/angular-http-auth which uses event broadcasting and allows retrying the failed request "response": function (response) { // probably not a GoCatch response if (!response.data.hasOwnProperty("result")) { return response; } // if we have a good response, just return it if (response.data.result) { return response; } //TODO: handle rateDriver which is different - console.log(data.data.error); // this has a different way of reporting errors to the other API calls if (!response.data.hasOwnProperty("errorCode")) { return response; } console.log("Error in: " + response.config.url); console.log(response.data.errorCode + ": " + response.data.errorMessage); // unhandled errorCode if (!isLoginErrorCode(response.data.errorCode)) { return response; } if (safeRoutes.indexOf($location.path()) < 0 && $location.path().indexOf('/passwordreset/') < 0) { // alert("Sorry! It looks like you aren't logged in, or your session has expired. Please login again."); $location.path(onBoardRoute); } return response; } }; return service; }]); services.factory('UserSvc', ['$http', '$q', 'promiseTracker', function ($http, $q, promiseTracker) { var service = { "isSignedIn": false, // TODO: Check which if any of these properties are used and which depend on the UserSvc having tried to log in "accountId": null, "name": null, "surname": null, "emailAddress": null, "mobileNumber": null, "mobileCountryCode": null, "paymentMethods": [], "vouchers": [], "signIn": function (emailAddress, password) { var instance = this; return $http.post( apiSignInRoute, { 'email_address': emailAddress, 'password': password, 'timezone': getGoCatchTimezoneOffsetInSeconds(), 'installationId': 'web' // Commented because we don't know that we will have access to user's location // 'lat':, // 'lng': } ).success(function (data, status, headers, config) { // EXAMPLE_SUCCESS_RESPONSE = {"result":True,"data":{"user":{"accountId":"5e581acaac8852c0535d88e1f0378b8731698bbe","installationId":"testdevicetestdevice","sessionId":"bec188e125489e8172c07ac8f254784477e1150b","validated":False,"emailVerified":False,"isMappedAccount":False,"signupComplete":False,"isBlocked":False,"ifKnownDevice":False,"name":"David","mobileNumber":"0828297212","emailAddress":"campey@labs.ws","pin":None,"referral":""},"config":{"id":2,"device_id":None,"pollingInterval":3,"returnDelay":30,"driverLocationInterval":10,"driverLocationIntervalPluggedIn":3,"driverLocationIntervalInJob":3,"driverBackgroundLocationInterval":300,"driverBackgroundLocationIntervalPluggedIn":60,"passengerLocationInterval":3,"noShowRadius":500,"reverseGeocodeRetryCount":5,"reverseGeocodeStalePeriod":600,"flagSpecificWait":90,"flagAllWait":300,"directionsQueryWait":60,"staleDriverThreshold":60,"staleJobThreshold":1,"driverDimTimeout":10,"advanceBookingSupported":0,"futureBookingsCutoffSeconds":1800,"supportNumber":"+2347036877100","abPollingInterval":10}}} // TODO: check if account is blocked? if (data.result) { instance.isSignedIn = true; instance.accountId = data.data.user.accountId; instance.name = data.data.user.name; instance.surname = data.data.user.surname; instance.emailAddress = data.data.user.emailAddress; instance.mobileCountryCode = data.data.user.mobileCountryCode; instance.mobileNumber = data.data.user.mobileNumber; if (ga && typeof(ga) == typeof(Function)) { ga('set', {'userId': data.data.user.accountId}); } } }).error(function (data, status, headers, config) { alert('The call to signIn on the Afro API failed'); }); }, "socialSignIn": function () { var instance = this; return $http.post( apiSocialSignInRoute, { 'timezone': getGoCatchTimezoneOffsetInSeconds(), 'installationId': 'web', 'signinType': 'FACEBOOK' // Commented because we don't know that we will have access to user's location // "lng": 18.508577, // "lat": -33.889682, } ).success(function (data, status, headers, config) { // TODO: check if account is blocked? if (data.result) { instance.isSignedIn = true; instance.accountId = data.data.user.accountId; instance.name = data.data.user.name; instance.surname = data.data.user.surname; instance.emailAddress = data.data.user.emailAddress; instance.mobileCountryCode = data.data.user.mobileCountryCode; instance.mobileNumber = data.data.user.mobileNumber; if (ga && typeof(ga) == typeof(Function)) { ga('set', {'userId': data.data.user.accountId}); } } }).error(function (data, status, headers, config) { alert('The call to socialSignIn on the Afro API failed'); }); }, "phonegapSocialSignIn": function (fbToken) { var instance = this; return $http.post( apiPhonegapSocialSignInRoute, { 'timezone': getGoCatchTimezoneOffsetInSeconds(), 'installationId': 'web', 'signinType': 'FACEBOOK', 'fbToken': fbToken // Commented because we don't know that we will have access to user's location // "lng": 18.508577, // "lat": -33.889682, } ).success(function (data, status, headers, config) { // TODO: check if account is blocked? if (data.result) { instance.isSignedIn = true; instance.accountId = data.data.user.accountId; instance.name = data.data.user.name; instance.surname = data.data.user.surname; instance.emailAddress = data.data.user.emailAddress; instance.mobileCountryCode = data.data.user.mobileCountryCode; instance.mobileNumber = data.data.user.mobileNumber; if (ga && typeof(ga) == typeof(Function)) { ga('set', {'userId': data.data.user.accountId}); } } }).error(function (data, status, headers, config) { alert('The call to phonegapSocialSignIn on the Afro API failed'); }); }, "signUp": function (name, surname, emailAddress, password, mobileNumber, mobileCountryCode) { var instance = this; var timezoneOffset = getGoCatchTimezoneOffsetInSeconds(); return $http.post( apiSignUpRoute, { 'name': name, 'surname': surname, 'email_address': emailAddress, 'password': password, 'mobile_country_code': mobileCountryCode, 'mobile': mobileNumber, 'installationId': 'web', // Commented because we don't know that we will have access to user's location // 'lat':, // 'lng': 'timezone': timezoneOffset, 'appVersion': 'webapp_0.1' } ).success(function (data, status, headers, config) { //EXAMPLE_SUCCESS = {"result":True,"data":{"user":{"accountId":"123456","installationId":"testdevicetestdevice","sessionId":"2090ccb9ddfbe3d9ab3a13f01fc954269f4267b8","validated":False,"emailVerified":False,"isMappedAccount":False,"signupComplete":False,"isBlocked":False,"ifKnownDevice":False,"name":"hello","mobileNumber":"0412345678","emailAddress":"testperson@labs.ws","pin":None,"referral":""},"config":{"id":2,"device_id":None,"pollingInterval":3,"returnDelay":30,"driverLocationInterval":10,"driverLocationIntervalPluggedIn":3,"driverLocationIntervalInJob":3,"driverBackgroundLocationInterval":300,"driverBackgroundLocationIntervalPluggedIn":60,"passengerLocationInterval":3,"noShowRadius":500,"reverseGeocodeRetryCount":5,"reverseGeocodeStalePeriod":600,"flagSpecificWait":90,"flagAllWait":300,"directionsQueryWait":60,"staleDriverThreshold":60,"staleJobThreshold":1,"driverDimTimeout":10,"advanceBookingSupported":0,"futureBookingsCutoffSeconds":1800,"supportNumber":"+2347036877100","abPollingInterval":10}}} // TODO: check if account is blocked? if (data.result) { instance.isSignedIn = true; instance.accountId = data.data.user.accountId; instance.name = data.data.user.name; instance.surname = data.data.user.surname; instance.emailAddress = data.data.user.emailAddress; instance.mobileCountryCode = data.data.user.mobileCountryCode; instance.mobileNumber = data.data.user.mobileNumber; if (ga && typeof(ga) == typeof(Function)) { ga('set', {'userId': data.data.user.accountId}); } } }).error(function (data, status, headers, config) { alert('The call to signUp on the Afro API failed'); }); }, "socialSignUp": function (name, emailAddress, mobileNumber) { var instance = this; var timezoneOffset = getGoCatchTimezoneOffsetInSeconds(); return $http.post( apiSocialSignUpRoute, { 'name': name, 'email_address': emailAddress, // TODO: add mobile country code 'mobile': mobileNumber, 'installationId': 'web', // Commented because we don't know that we will have access to user's location // 'lng': 18.508577, // 'lat': -33.889682, 'timezone': timezoneOffset, 'appVersion': 'webapp_0.1' } ).success(function (data, status, headers, config) { // TODO: check if account is blocked? if (data.result) { instance.isSignedIn = true; instance.accountId = data.data.user.accountId; instance.name = data.data.user.name; instance.surname = data.data.user.surname; instance.emailAddress = data.data.user.emailAddress; instance.mobileCountryCode = data.data.user.mobileCountryCode; instance.mobileNumber = data.data.user.mobileNumber; if (ga && typeof(ga) == typeof(Function)) { ga('set', {'userId': data.data.user.accountId}); } } }).error(function (data, status, headers, config) { alert('The call to socialSignUp on the Afro API failed'); }); }, "phonegapSocialSignUp": function (name, emailAddress, mobileNumber, fbToken) { var instance = this; var timezoneOffset = getGoCatchTimezoneOffsetInSeconds(); return $http.post( apiPhonegapSocialSignUpRoute, { 'name': name, 'email_address': emailAddress, // TODO: add mobile country code 'mobile': mobileNumber, 'fbToken': fbToken, 'installationId': 'web', // Commented because we don't know that we will have access to user's location // 'lng': 18.508577, // 'lat': -33.889682, 'timezone': timezoneOffset, 'appVersion': 'webapp_0.1' } ).success(function (data, status, headers, config) { // TODO: check if account is blocked? if (data.result) { instance.isSignedIn = true; instance.accountId = data.data.user.accountId; instance.name = data.data.user.name; instance.surname = data.data.user.surname; instance.emailAddress = data.data.user.emailAddress; instance.mobileNumber = data.data.user.mobileNumber; instance.mobileCountryCode = data.data.user.mobileCountryCode; if (ga && typeof(ga) == typeof(Function)) { ga('set', {'userId': data.data.user.accountId}); } } }).error(function (data, status, headers, config) { alert('The call to phonegapSocialSignUp on the Afro API failed'); }); }, "signOut": function () { var instance = this; return $http.post( apiSignOutRoute, {} ).success(function (data, status, headers, config) { // EXAMPLE_SUCCESS_RESPONSE = {"result":True} if (data.result) { instance.isSignedIn = false; instance.accountId = null; instance.name = null; instance.surname = null; instance.emailAddress = null; instance.mobileCountryCode = null; instance.mobileNumber = null; instance.paymentMethods = []; instance.vouchers = []; } }).error(function (data, status, headers, config) { alert('The call to signOut on the Afro API failed'); }); }, "completeProfile": function (name, surname, emailAddress, mobileCountryCode, mobileNumber) { var instance = this; return $http.post( apiCompletePassengerProfileRoute, { "name": name, "surname": surname, "email_address": emailAddress, "mobile_country_code": mobileCountryCode, "mobile_number": mobileNumber } ).success(function (data, status, headers, config) { // EXAMPLE_SUCCESS_RESPONSE = {"result":True} if (data.result) { instance.name = data.data.user.name; instance.surname = data.data.user.surname; instance.emailAddress = data.data.user.emailAddress; instance.mobileCountryCode = data.data.user.mobileCountryCode; instance.mobileNumber = data.data.user.mobileNumber; } }).error(function (data, status, headers, config) { alert('The call to completePassengerProfile on the Afro API failed'); }); }, "getPassengerVouchers": function () { var instance = this; if (instance.vouchers.length > 0) { var response = { data: { result: true, data: { vouchers: instance.vouchers } } }; var deferred = $q.defer(); deferred.resolve(response); return deferred.promise; } return $http.get(apiGetPassengerVouchers) .success(function (data) { instance.vouchers = data.data.vouchers; }) .error(function (data, status, headers, config) { console.log('[ERROR] TripSvc.getPassengerVouchers: ' + status); }); }, "redeemVoucher": function (voucherCode, userHasValidCreditCard) { var instance = this; return $http.post( apiRedeemVoucher, { "voucher_code": voucherCode, "user_has_valid_credit_card": userHasValidCreditCard } ).success(function (data, status, headers, config) { if (data.result) { instance.vouchers = data.data.vouchers; } return data; }).error(function (data, status, headers, config) { alert('The call to redeemVoucher on the Afro API failed'); }); }, "getPassengerDetails": function () { var instance = this; if (instance.emailAddress) { var response = { data: { result: true, data: { profile: { name: instance.name, surname: instance.surname, email_address: instance.emailAddress, mobile_country_code: instance.mobileCountryCode, mobile_number: instance.mobileNumber } } } }; var deferred = $q.defer(); deferred.resolve(response); return deferred.promise; } return $http.get(apiEditPassengerProfile) .success(function (data) { instance.name = data.profile.name; instance.surname = data.profile.surname; instance.emailAddress = data.profile.email_address; instance.mobileCountryCode = data.profile.mobile_country_code; instance.mobileNumber = data.profile.mobile_number; }) .error(function (data, status, headers, config) { console.log('[ERROR] UserSvc.getPassengerDetails: ' + status); }); }, "editProfile": function (mobileCountryCode) { //TODO: fix weirdness of only passing in mobileCountryCode, see caller var instance = this; return $http.post( apiEditPassengerProfile, { "name": instance.name, "surname": instance.surname, "email_address": instance.emailAddress, "mobile_country_code": mobileCountryCode, "mobile_number": instance.mobileNumber } ).success(function (data, status, headers, config) { // EXAMPLE_SUCCESS_RESPONSE = {"result":True} if (data.result) { instance.name = data.data.user.name; instance.surname = data.data.user.surname; instance.emailAddress = data.data.user.emailAddress; instance.mobileCountryCode = data.data.user.mobileCountryCode; instance.mobileNumber = data.data.user.mobileNumber; } }).error(function (data, status, headers, config) { alert('The call to editPassengerProfile on the Afro API failed'); }); }, "forgotPassword": function (email_address) { var instance = this; return $http.post( apiForgotPasswordPassenger, { "email_address": email_address } ).success(function (data, status, headers, config) { // EXAMPLE_SUCCESS_RESPONSE = {"result":True} }).error(function (data, status, headers, config) { alert('The call to forgotPassword on the Afro API failed'); }); }, "resetPassword": function (kind, id, validationToken, new_password, password_confirmation) { var instance = this; var url = ''; if (kind == 'driver') { url = apiResetPasswordDriver; } if (kind == 'passenger') { url = apiResetPasswordPassenger; } return $http.post( url, { "user_id": id, "reset_validation_token": validationToken, "new_password": new_password, "confirmation_password": password_confirmation } ).success(function (data, status, headers, config) { // EXAMPLE_SUCCESS_RESPONSE = {"result":True} }).error(function (data, status, headers, config) { alert('The call to resetPassword on the Afro API failed'); }); }, "stripeTracker": promiseTracker(), "getStripePaymentMethods": function () { var instance = this; if (instance.paymentMethods.length > 0) { var response = { data: { result: true, data: { stripe_payment_methods: instance.paymentMethods } } }; var deferred = $q.defer(); deferred.resolve(response); return deferred.promise; } return $http.get( apiStripePaymentMethods, { tracker: instance.stripeTracker } ).success(function (data, status, headers, config) { if (data.result) { instance.paymentMethods = data.data.stripe_payment_methods; } }).error(function (data, status, headers, config) { alert('The call to getStripeCustomerDetails on the Afro API failed'); }); }, "setStripePaymentMethod": function (stripeToken) { var instance = this; return $http.post( apiStripeCard, { stripeToken: stripeToken }, { tracker: instance.stripeTracker } ).success(function (data, status, headers, config) { if (data.result) { instance.paymentMethods = data.data.stripe_payment_methods; } }).error(function (data, status, headers, config) { alert('The call to setStripePaymentMethod on the Afro API failed'); }); }, "deleteStripePaymentMethod": function (stripeToken) { var instance = this; var url = apiStripeCard + "?token=" + stripeToken; return $http.delete( url, { tracker: instance.stripeTracker } ).success(function (data, status, headers, config) { if (data.result) { instance.paymentMethods = data.data.stripe_payment_methods; } }).error(function (data, status, headers, config) { alert('The call to deleteStripePaymentMethod on the Afro API failed'); }); }, "getDefaultCreditCard": function () { // NB: currently callers must only call this in a .then on getStripePaymentMethods() !!! // TODO: what if there is no default but there are payment methods? shouldn't happen - how can we guarantee it? // this would probably cause weird behaviour, for example FareCtrl would take you to the payment methods screen because there is no default // card, but then you would see cards on that screen... // TODO: make this return a promise like getDefaultCreditCardToken and make it do the payment method fetch // will require restructuring checkVoucher & checkCreditCard in FareCtrl var instance = this; var defaultCreditCard = null; instance.paymentMethods.forEach(function (creditCard) { if (creditCard.default) { defaultCreditCard = creditCard; } }); return defaultCreditCard; }, "getDefaultCreditCardToken": function () { var instance = this; return this.getStripePaymentMethods().then(function (result) { var defaultCreditToken = null; instance.paymentMethods.forEach(function (creditCard) { if (creditCard.default) { defaultCreditToken = creditCard.token; } }); return defaultCreditToken; }); } }; return service; }]); services.factory('TripSvc', ['$http', '$q', '$timeout', '$location', '$rootScope', 'promiseTracker', 'MapSvc', function ($http, $q, $timeout, $location, $rootScope, promiseTracker, MapSvc) { var service = { "eventScope": $rootScope, "branch": {}, "fromLatLng": MapSvc.mapOptions.center, "fromAddress": null, "fromAddressComponents": [], "destinationLatLng": null, "destinationAddress": null, "destinationAddressComponents": [], "taxiType": "0", "driverDeviceId": null, "tripDistance": 0, "minFare": 0, "minEstFare": 0, "estFare": 0, "maxEstFare": 0, "currencyCode": '', "currencySymbol": '', "fareStep": 50, "absoluteMinimumFare": 0, "absoluteMaximumFare": 0, "suggestedFare": 0, "myBid": 0, "processingFee": 0, "paymentMethod": PaymentMethods.CASH, "creditCardToken": null, "jobId": null, "jobDriversSentTo": 0, "jobDriversViewedBy": 0, "jobDriversRejected": 0, "messageHandler": null, "driverHandler": null, "recoveryHandler": null, "driverPhotoUrl": null, "vehiclePhotoUrl": null, "driverDetails": null, "driverDistanceFromPickup": 0, "driverDistanceFromDestination": 0, // TODO: should live on UserSvc? "trips": [], "taxiTypeList": function () { var displayList = []; if (angular.isUndefined(this.branch.taxi_types)) { return displayList; } for (var i = 0; i < this.branch.taxi_types.length; i++) { displayList.push(this.branch.taxi_types[i].display_name); } return displayList; }, "mappedTaxiType": function (rangeInputTaxiType) { if (angular.isUndefined(this.branch.taxi_types_range_lookup)) { return "UNKNOWN"; } return this.branch.taxi_types_range_lookup[rangeInputTaxiType].taxi_type; }, "generateTaxiTypeRangeLookup": function (branch) { branch.taxi_types_range_lookup = {}; for (var i = 0; i < branch.taxi_types.length; i++) { branch.taxi_types_range_lookup[String(i)] = branch.taxi_types[i]; branch.taxi_types_range_lookup[branch.taxi_types[i].taxi_type] = branch.taxi_types[i]; } }, "getBranchInfo": function () { var instance = this; var parms = { 'lat': instance.fromLatLng.lat(), 'lng': instance.fromLatLng.lng() }; var url = apiBranchInfo + '?'; url += $.param(parms); return $http.get( url ).success(function (data, status, headers, config) { if (data.result) { instance.generateTaxiTypeRangeLookup(data.data.branch); instance.branch = data.data.branch; } else { alert(data.error_message); } }).error(function (data, status, headers, config) { alert('The call to getBranchInfo on the Afro API failed'); }); }, "getAbsoluteMinimumFare": function () { return Math.max(this.minEstFare, this.minFare); }, "getAbsoluteMaximumFare": function () { var absoluteMinFare = this.getAbsoluteMinimumFare(); return Math.max(absoluteMinFare + 100, this.maxEstFare); // we don't want this to be equal and the step size is 50 }, "getSuggestedFare": function () { var absoluteMinFare = this.getAbsoluteMinimumFare(); if (absoluteMinFare >= this.estFare) { return absoluteMinFare; } return this.estFare; }, "setDistanceDrawRouteAndSetEstimatedFare": function () { var instance = this; var deferred = $q.defer(); this.getJobRouteDistance(this.fromLatLng, this.destinationLatLng).then( function (response) { instance.tripDistance = response.routes[0].legs[0].distance.value / 1000; instance.displayDistance = response.routes[0].legs[0].distance.text; MapSvc.drawRoute(response); instance.setEstimatedFare().then( function (response) { deferred.resolve(); }, function (error) { deferred.reject(); } ); }, function (error) { alert('The call to route on the directions service failed'); deferred.reject(); } ); return deferred.promise; }, "getCrowDistance": function (fromLatLng, toLatLng) { // compute distance in km as the crow flies if (!fromLatLng || !toLatLng) { return NaN; } var distance = google.maps.geometry.spherical.computeDistanceBetween(fromLatLng, toLatLng, 6378.137); return distance; }, "getJobRouteDistance": function (from, to) { var instance = this; // promise to be resolved in directionsService.route callback below // need this to chain setEstimatedFare which depends on distance var deferred = $q.defer(); // get distance from directions service var directionsService = new google.maps.DirectionsService(); var request = { origin: this.fromLatLng, destination: this.destinationLatLng, travelMode: google.maps.TravelMode.DRIVING, avoidFerries: true, unitSystem: google.maps.UnitSystem.METRIC }; directionsService.route(request, function (response, status) { if (status == google.maps.DirectionsStatus.OK) { deferred.resolve(response); } else { deferred.reject(String.format("The call to the directions service failed with status: {0}", status)); } }); return deferred.promise; }, "estimatingTracker": promiseTracker(), "setEstimatedFare": function () { var instance = this; return $http.post( apiEstimateRoute, { 'tripDistance': instance.tripDistance * 1000, 'driverType': instance.mappedTaxiType(instance.taxiType), 'lat': instance.fromLatLng.lat(), 'lng': instance.fromLatLng.lng() }, { tracker: instance.estimatingTracker } ).success(function (data, status, headers, config) { // EXAMPLE_SUCCESS_RESPONSE = {"result": True, "data": {"minEstFare": 199, "estFare": 299, "maxEstFare": 499, "minFare": 499}} instance.minFare = data.data.minFare; instance.minEstFare = data.data.minEstFare; instance.estFare = data.data.estFare; instance.maxEstFare = data.data.maxEstFare; instance.currencyCode = data.data.currency; instance.currencySymbol = data.data.currencySymbol; instance.fareStep = data.data.fareStep; // TODO: optimize calculations, they call each other internally instance.absoluteMinimumFare = instance.getAbsoluteMinimumFare(); instance.absoluteMaximumFare = instance.getAbsoluteMaximumFare(); instance.suggestedFare = instance.getSuggestedFare(); // don't overwrite bid if there is one (from recovery or if the user has navigated back and forward) // if there is a bid, check that it is within the valid range, otherwise set it to suggestedFare (route changed) // hopefully the bid from recovery is never outside of the valid range - that would be weird ja if (!instance.myBid || (instance.myBid < instance.absoluteMinimumFare || instance.myBid > instance.absoluteMaximumFare)) { instance.myBid = instance.suggestedFare; } }).error(function (data, status, headers, config) { alert('The call to getEstimatedFare on the Afro API failed'); }); }, "updateProcessingFee": function (payment_methods) { if (angular.isUndefined(payment_methods)) { payment_methods = [this.paymentMethod]; } this.processingFee = 0; if (angular.equals(this.branch, {})) { return this.processingFee; } var instance = this; var filtered_list = this.branch.allowed_payment_methods.filter(function (x) { return payment_methods.indexOf(x.payment_method) >= 0 }); for (var i = 0; i < filtered_list.length; i++) { this.processingFee += filtered_list[i].processing_fee; } return this.processingFee; }, "createJob": function () { var instance = this; console.log("calling create job on TripSvc..."); var addressHandler = new AddressHandler(); return $http.post( apiCreateJobRoute, { "pickupLng": instance.fromLatLng.lng(), "pickupLat": instance.fromLatLng.lat(), "destinationLng": instance.destinationLatLng.lng(), "destinationLat": instance.destinationLatLng.lat(), "pickupStreet": addressHandler.getStreetName(instance.fromAddressComponents), "pickupStreetNumber": addressHandler.getStreetNumber(instance.fromAddressComponents), "pickupSuburb": addressHandler.getSuburb(instance.fromAddressComponents), "destinationSuburb": addressHandler.getSuburb(instance.destinationAddressComponents), "bid": instance.myBid * 100, "bidCurrencyCode": instance.currencyCode, "paymentMethod": instance.paymentMethod, "creditCardToken": instance.creditCardToken, "driverType": instance.mappedTaxiType(instance.taxiType), "passengerLng": instance.fromLatLng.lng(), "passengerLat": instance.fromLatLng.lat(), "tripDistance": instance.tripDistance * 1000 } ).success(function (data, status, headers, config) { // EXAMPLE_SUCCESS_RESPONSE = {"result":True,"data":{"jobId":2485,"dispatchId":2485}} if (data.result) { instance.jobId = data.data.jobId; console.log("Job ID from response: " + data.data.jobId); } console.log("Job ID on service: " + instance.jobId); }).error(function (data, status, headers, config) { alert('The call to createjob on the Afro API failed'); }); }, "collectJob": function () { if (!this.jobId) { return; } console.log("calling collect job on TripSvc..."); var instance = this; $http.post( apiCollectJobRoute, { "jobId": instance.jobId } ).success(function (data, status, headers, config) { // EXAMPLE_SUCCESS_RESPONSE = {"result":True} if (data.result) { // instance.jobId = null; // TODO: not sure what to do here... do we still need the jobId? Probably: because of completeJob } }).error(function (data, status, headers, config) { alert('The call to collectjob on the Afro API failed'); }); }, "completeJob": function () { if (!this.jobId) { return; } console.log("calling complete job on TripSvc..."); var instance = this; $http.post( apiCompleteJobRoute, { "jobId": instance.jobId } ).success(function (data, status, headers, config) { // EXAMPLE_SUCCESS_RESPONSE = {"result":True} if (data.result) { // we still need the jobId for the rating } }).error(function (data, status, headers, config) { alert('The call to completejob on the Afro API failed'); }); }, "cancelJob": function (jobId) { if (angular.isUndefined(jobId)) { jobId = this.jobId; } if (!jobId) { return; } console.log("calling cancel job on TripSvc for JobId " + jobId); var instance = this; $http.post( apiCancelFlaggingJobRoute, { "jobId": jobId } ).success(function (data, status, headers, config) { // EXAMPLE_SUCCESS_RESPONSE = {"result":True} if (data.result) { instance.jobId = null; instance.driverDeviceId = null; instance.resetJobViewedByCounters(); } }).error(function (data, status, headers, config) { alert('The call to cancelflaggingjob on the Afro API failed'); }); }, "abandonJob": function (jobId) { if (angular.isUndefined(jobId)) { jobId = this.jobId; } if (!jobId) { return; } console.log("calling abandon job on TripSvc for JobId " + jobId); var instance = this; $http.post( apiAbandonJobRoute, { "jobId": jobId } ).success(function (data, status, headers, config) { // EXAMPLE_SUCCESS_RESPONSE = {"result":True} if (data.result) { instance.jobId = null; instance.driverDeviceId = null; instance.resetJobViewedByCounters(); } }).error(function (data, status, headers, config) { alert('The call to abandonjob on the Afro API failed'); }); }, "driverPhotoUrlTracker": promiseTracker(), "getDriverPhotoUrl": function () { var instance = this; var url = String.format("{0}/{1}", apiGetDriverPhotoUrl, instance.driverDeviceId); return $http.get( url, { tracker: instance.driverPhotoUrlTracker } ).success(function (data, status, headers, config) { if (data.result) { instance.driverPhotoUrl = data.data.url; } else { console.log(data.errorCode + ": " + data.errorMessage); } }).error(function (data, status, headers, config) { console.log('The call to getDriverPhotoUrl on the Afro API failed'); }); }, "vehiclePhotoUrlTracker": promiseTracker(), "getVehiclePhotoUrl": function () { var instance = this; var url = String.format("{0}/{1}/{2}", apiGetVehiclePhotoUrl, instance.driverDeviceId, instance.mappedTaxiType(instance.taxiType)); return $http.get( url, { tracker: instance.vehiclePhotoUrlTracker } ).success(function (data, status, headers, config) { if (data.result) { instance.vehiclePhotoUrl = data.data.url; } else { console.log(data.errorCode + ": " + data.errorMessage); } }).error(function (data, status, headers, config) { console.log('The call to getVehiclePhotoUrl on the Afro API failed'); }); }, "rateDriver": function (rating) { var instance = this; if (angular.isString(rating)) { rating = parseInt(rating); } return $http.post( apiRateDriver, { "driver_device_id": instance.driverDeviceId, "rating": rating, "job_id": instance.jobId } ).success(function (data, status, headers, config) { if (data.result) { // do nothing } else { console.log(data.data.error); // this has a different way of reporting errors to the other API calls } }).error(function (data, status, headers, config) { console.log('The call to rateDriver on the Afro API failed'); }); }, "resetJobViewedByCounters": function () { this.jobDriversSentTo = 0; this.jobDriversViewedBy = 0; this.jobDriversRejected = 0; }, "finishJobAndClearState": function () { this.branch = {}; // this.fromLatLng = this.destinationLatLng; this.fromLatLng = MapSvc.mapOptions.center; this.fromAddress = null; this.fromAddressComponents = []; this.destinationLatLng = null; this.destinationAddress = null; this.destinationAddressComponents = []; this.taxiType = "0"; this.driverDeviceId = null; this.tripDistance = 0; this.minFare = 0; this.minEstFare = 0; this.estFare = 0; this.maxEstFare = 0; this.currencyCode = ''; this.currencySymbol = ''; this.fareStep = 50; this.absoluteMinimumFare = 0; this.absoluteMaximumFare = 0; this.suggestedFare = 0; this.myBid = 0; this.processingFee = 0; this.paymentMethod = PaymentMethods.CASH; this.creditCardToken = null; this.jobId = null; this.jobDriversSentTo = 0; this.jobDriversViewedBy = 0; this.jobDriversRejected = 0; this.driverPhotoUrl = null; this.vehiclePhotoUrl = null; this.driverDetails = null; this.driverDistanceFromPickup = 0; this.driverDistanceFromDestination = 0; MapSvc.clearExistingDriverMarkers(); }, "driverDetailsTracker": promiseTracker(), // TODO: should live on UserSvc? "fetchPassengerTrips": function () { var instance = this; return $http.get(apiTripHistory) .success(function (data) { instance.trips = data.trips; }) .error(function (data, status, headers, config) { console.log('[ERROR] TripSvc.fetchPassengerTrips: ' + status); }); }, // TODO: should live on UserSvc? "fetchSingleTrip": function (tripKey) { var instance = this; var url = String.format(apiTripView, tripKey); return $http.get(url) .success(function (data) { instance.trips = data.trips; }) .error(function (data, status, headers, config) { console.log('[ERROR] TripSvc.fetchSingleTrip: ' + status); }); }, "getDriverDetails": function () { var instance = this; return $http.post( apiGetDriverDetails, { "device_ids": [instance.driverDeviceId] }, { tracker: instance.driverDetailsTracker } ).success(function (data, status, headers, config) { if (data.result) { // see Appengine API tests for example responses instance.driverDetails = data.data.drivers[0]; } else { console.log(data.errorCode + ": " + data.errorMessage); } }).error(function (data, status, headers, config) { console.log('The call to getDriverDetails on the Afro API failed'); }); }, "showTaxisOnMapFromPolling": true, "getActiveTaxisPollingTimer": null, "pollingGetActiveTaxis": false, "startGetActiveTaxisPolling": function (showTaxisOnMap) { // TODO: use config object returned in SignIn to determine the polling time if (angular.isUndefined(showTaxisOnMap)) { showTaxisOnMap = true; } var instance = this; instance.showTaxisOnMapFromPolling = showTaxisOnMap; instance.stopGetFlaggedTaxiPolling(); // we can only have one or the other polling happening if (instance.pollingGetActiveTaxis) { return; } instance.pollingGetActiveTaxis = true; console.log("calling start get active taxis on TripSvc..."); var handler = function (data) { instance.getActiveTaxisPollingTimer = $timeout(tick, 3000); return instance.getActiveTaxisPollingTimer; }; var tick = function () { if (!instance.pollingGetActiveTaxis) { return null; } console.log('starting getActiveTaxis tick'); instance.getActiveTaxis() .then(handler, handler); }; tick(); }, "stopGetActiveTaxisPolling": function () { if (this.getActiveTaxisPollingTimer) { $timeout.cancel(this.getActiveTaxisPollingTimer); } this.pollingGetActiveTaxis = false; }, "getActiveTaxis": function () { var instance = this; var mapCenter = MapSvc.map.getCenter(); var mapBoundsSpan = MapSvc.map.getBounds().toSpan(); return $http.post( apiGetActiveTaxis, { "clng": mapCenter.lng(), "clat": mapCenter.lat(), "slng": mapBoundsSpan.lng(), "slat": mapBoundsSpan.lat(), "ulng": instance.fromLatLng.lng(), "ulat": instance.fromLatLng.lat() } ).success(function (data, status, headers, config) { if (data.result) { //alert('Number of Drivers: ' + data.data.drivers.length); var selectedTaxiType = instance.mappedTaxiType(instance.taxiType); instance.messageHandler.handleMessages(data.data.messages); instance.driverHandler.handleDrivers(data.data.drivers, selectedTaxiType); } else { if (isLoginErrorCode(data.errorCode)) { console.log("Got a logged out error: Stopping polling getActiveTaxis"); instance.stopGetActiveTaxisPolling(); } } }).error(function (data, status, headers, config) { // alert('The call to getactivetaxis on the Afro API failed'); }); }, "getFlaggedTaxiPollingTimer": null, "pollingGetFlaggedTaxi": false, "startGetFlaggedTaxiPolling": function (showTaxisOnMap) { // TODO: use config object returned in SignIn to determine the polling time if (angular.isUndefined(showTaxisOnMap)) { showTaxisOnMap = true; } var instance = this; instance.showTaxisOnMapFromPolling = showTaxisOnMap; instance.stopGetActiveTaxisPolling(); // we can only have one or the other polling happening if (instance.pollingGetFlaggedTaxi) { return; } instance.pollingGetFlaggedTaxi = true; console.log("calling start get flagged taxi on TripSvc..."); var handler = function (data) { instance.getFlaggedTaxiPollingTimer = $timeout(tick, 3000); return instance.getFlaggedTaxiPollingTimer; }; var tick = function () { if (!instance.pollingGetFlaggedTaxi) { return null; } console.log('starting getFlaggedTaxi tick'); instance.getFlaggedTaxi() .then(handler, handler); }; tick(); }, "stopGetFlaggedTaxiPolling": function () { if (this.getFlaggedTaxiPollingTimer) { $timeout.cancel(this.getFlaggedTaxiPollingTimer); } this.pollingGetFlaggedTaxi = false; }, "getFlaggedTaxi": function () { var instance = this; return $http.post( apiGetFlaggedTaxi, { "driver_device_id": instance.driverDeviceId, "ulng": instance.fromLatLng.lng(), "ulat": instance.fromLatLng.lat() } ).success(function (data, status, headers, config) { if (data.result) { // can we just assume that the selected taxi type is the same as the flagged driver? seems sensible. instance.driverHandler.handleDrivers([data.data.driver], data.data.driver.driverType); instance.messageHandler.handleMessages(data.data.messages); var driverLatLng = new google.maps.LatLng(data.data.driver.lat, data.data.driver.lng); instance.driverDistanceFromPickup = instance.getCrowDistance(driverLatLng, instance.fromLatLng); instance.driverDistanceFromDestination = instance.getCrowDistance(driverLatLng, instance.destinationLatLng); // TODO: check if we can remove this event and talk to MapSvc directly instance.eventScope.$broadcast(flaggedDriverUpdatedEvent, data.data.driver); } else { if (isLoginErrorCode(data.errorCode)) { console.log("Got a logged out error: Stopping polling getFlaggedTaxi"); instance.stopGetFlaggedTaxiPolling(); } } }).error(function (data, status, headers, config) { // alert('The call to getflaggedtaxi on the Afro API failed'); }); }, "getPassengerFlagsAndStatus": function () { var instance = this; console.debug('calling recovery api'); return $http.post( apiPassengerFlagsAndStatus, {} ).success(function (data, status, headers, config) { if (data.result && data.data) { // instance.recoveryHandler.recoverState(data); // handled in .then of places that call this } else { // do nothing } }).error(function (data, status, headers, config) { console.log('The call to getPassengerFlagsAndStatus on the Afro API failed') }); } }; service.messageHandler = new MessageHandler(service); service.driverHandler = new DriverHandler(service, MapSvc); service.recoveryHandler = new RecoveryHandler(service, MapSvc, $location); return service; }]); services.factory('MapSvc', ['$http', '$q', '$compile', function ($http, $q, $compile) { // Default location is Lagos var Lagos = new google.maps.LatLng(6.4531, 3.3958); var service = { "map": null, "mapOptions": { center: Lagos, zoom: 13, panControl: false, streetViewControl: false, zoomControl: false, mapTypeControl: false }, "infowindow": null, "infowindowOptions": { pixelOffset: new google.maps.Size(0, -52) }, "infobox": null, "driverMarkers": [], "marker_fromLocation": new google.maps.Marker({ icon: "/img/Marker.svg" }), "marker_destinationLocation": new google.maps.Marker(), "directionsRenderer": null, "init": function () { var deferred = $q.defer(); var noPoi = [ { featureType: "poi", stylers: [ {visibility: "off"} ] } ]; this.map = new google.maps.Map(document.getElementById("map-canvas"), this.mapOptions); this.infowindow = new google.maps.InfoWindow(this.infowindowOptions); this.directionsRenderer = new google.maps.DirectionsRenderer(); this.directionsRenderer.setMap(this.map); this.map.setOptions({styles: noPoi}); google.maps.event.addListenerOnce(this.map, 'idle', function (event) { deferred.resolve(); }); return deferred.promise; }, "getLocationFromBrowser": function (addressCallback) { var deferred = $q.defer(); if (!navigator.geolocation) { deferred.reject("Your browser does not support location detection."); return; } var instance = this; var locationSuccessHandler = function (position) { if (!instance.map) { return; } var latLng = new google.maps.LatLng(position.coords.latitude, position.coords.longitude); instance.map.setCenter(latLng); instance.map.setZoom(17); instance.reverseGeocode(latLng, addressCallback); deferred.resolve(position); }; var locationErrorHandler = function (error) { switch (error.code) { case error.PERMISSION_DENIED: console.log("Geolocation: User denied the request."); deferred.reject("Your browser is denying location requests. Please check your settings."); break; case error.POSITION_UNAVAILABLE: console.log("Geolocation: Location information is unavailable."); deferred.reject("We were unable to determine your location."); break; case error.TIMEOUT: console.log("Geolocation: The request to get user location timed out."); deferred.reject("We were unable to determine your location."); break; case error.UNKNOWN_ERROR: console.log("Geolocation: An unknown error occurred."); deferred.reject("We were unable to determine your location."); break; } }; var options = { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }; navigator.geolocation.getCurrentPosition(locationSuccessHandler, locationErrorHandler, options); return deferred.promise; }, "geocode": function (address, countryRestriction, bounds, callback) { var geocoder = new google.maps.Geocoder(); var request = { 'address': address, 'bounds': bounds, 'componentRestrictions': { 'country': countryRestriction } }; geocoder.geocode(request, function (results, status) { if (status != google.maps.GeocoderStatus.OK) { console.log("geocode failed due to: " + status); return; } if (!results[0]) { console.log("geocode found no results"); return; } console.log('calling callback from geocode'); callback(results[0]); }); }, "reverseGeocode": function (latLng, callback) { var geocoder = new google.maps.Geocoder(); geocoder.geocode({'latLng': latLng}, function (results, status) { if (status != google.maps.GeocoderStatus.OK) { console.log("reverseGeocode failed due to: " + status); return; } if (!results[0]) { console.log("reverseGeocode found no results"); return; } console.log('calling callback from reverse geocode'); callback(results[0]); }); }, "openInfoWindow": function (content, scope) { var compiled = angular.element($compile(content)(scope)); this.infowindow.setContent(compiled[0]); var infowindowPosition = new google.maps.MVCObject(); infowindowPosition.set('position', this.map.getCenter()); this.infowindow.open(this.map, infowindowPosition); }, "openInfobox": function (content, scope) { var compiled = angular.element($compile(content)(scope)); var infoboxOptions = { content: compiled[0], disableAutoPan: false, maxWidth: 0, pixelOffset: new google.maps.Size(-100, -95), zIndex: null, boxStyle: { width: "200px" }, closeBoxMargin: "10px 2px 2px 2px", closeBoxURL: "", infoBoxClearance: new google.maps.Size(1, 1), isHidden: false, pane: "floatPane", enableEventPropagation: false }; this.infobox = new InfoBox(infoboxOptions); var infoboxPosition = new google.maps.Marker({position: this.map.getCenter()}); this.infobox.open(this.map, infoboxPosition); }, "makeDriverMarker": function (driver, visible) { var marker = new google.maps.Marker({ icon: driver.iconImage, position: new google.maps.LatLng(driver.lat, driver.lng), visible: visible }); return { "driver": driver, "marker": marker } }, "findDriverMarker": function (driverDeviceId) { var currentDriverMarker = null; angular.forEach(this.driverMarkers, function (driverMarker) { if (driverMarker.driver.device_id != driverDeviceId) { return; } currentDriverMarker = driverMarker; }); return currentDriverMarker; }, "addOrUpdateDriverMarker": function (driver, visible) { var currentDriverMarker = this.findDriverMarker(driver.device_id); var currentDriverPosition = new google.maps.LatLng(driver.lat, driver.lng); if (!currentDriverMarker) { currentDriverMarker = this.makeDriverMarker(driver, visible); this.driverMarkers.push(currentDriverMarker); this.addMarker(currentDriverMarker.marker, currentDriverPosition); } if (currentDriverMarker.marker.getMap() != this.map) { currentDriverMarker.marker.setMap(this.map); } currentDriverMarker.marker.setVisible(visible); if (!currentDriverPosition.equals(currentDriverMarker.marker.getPosition()) && currentDriverMarker.marker.getVisible()) { this.addMarker(currentDriverMarker.marker, currentDriverPosition); } currentDriverMarker.driver = driver; }, "addMarker": function (marker, position) { marker.setPosition(position); marker.setMap(this.map); }, "associateExistingDriverMarkersWithMap": function () { // we have to do this after controller changes as the map is re-initialized // we don't do it in the MapSvc.init as we don't always want to show the driverMarkers (controller dependent) var instance = this; angular.forEach(this.driverMarkers, function (driverMarker) { driverMarker.marker.setMap(instance.map); }); }, "clearExistingDriverMarkers": function () { var driverMarkersToClear = this.driverMarkers; this.driverMarkers = []; angular.forEach(driverMarkersToClear, function (driverMarker) { driverMarker.marker.setVisible(false); driverMarker.marker.setMap(null); }); }, "filterVisibleDriverMarkers": function (driverType) { angular.forEach(this.driverMarkers, function (driverMarker) { var visible = driverMarker.driver.driverType == driverType; driverMarker.marker.setVisible(visible); }); }, "addFromLocationMarker": function (position) { this.addMarker(this.marker_fromLocation, position); }, "addDestinationLocationMarker": function (position) { this.addMarker(this.marker_destinationLocation, position); }, "removeLocationMarkers": function () { this.marker_fromLocation.setMap(null); this.marker_destinationLocation.setMap(null); }, "drawRoute": function (response) { if (!response) { return; } this.removeLocationMarkers(); this.directionsRenderer.setDirections(response); this.map.fitBounds(response.routes[0].bounds); } }; return service; }]); services.factory('ReceiverSvc', ['$http', function ($http) { var map; var branchMarker; var driverMarker; var customerMarker; var service = { "fetchDeliveryDetails": function (tripId) { var url = String.format(apiDeliveryView, tripId); return $http.get(url); }, "rateExperience": function (rating, tripId) { var url = String.format(apiRateExperience, tripId); if (angular.isString(rating)) { rating = parseInt(rating); } return $http.post(url, { "rating": rating } ); }, "renderMap": function (branchLocation, driverLocation, customerLocation) { var mapOptions = { center: new google.maps.LatLng(driverLocation.latitude, driverLocation.longitude), zoom: 15, panControl: false, streetViewControl: false, zoomControl: true, mapTypeControl: false }; map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions); branchMarker = new google.maps.Marker({ icon: '/img/branch.png', position: new google.maps.LatLng(branchLocation.latitude, branchLocation.longitude), visible: true, map: map }); driverMarker = new SlidingMarker({ icon: '/img/driver.png', position: new google.maps.LatLng(driverLocation.latitude, driverLocation.longitude), visible: true, map: map, duration: 10000, easing: "linear" }); customerMarker = new google.maps.Marker({ icon: '/img/customer.png', position: new google.maps.LatLng(customerLocation.latitude, customerLocation.longitude), visible: true, map: map, label: {text: ':)'} }); }, "setDriverLocationMarkerPosition": function (driverLocation) { if (angular.isUndefined(driverMarker)) { return; } driverMarker.setPosition(new google.maps.LatLng(driverLocation.latitude, driverLocation.longitude)); }, "removeDriverLocationMarker": function () { if (angular.isUndefined(map) || angular.isUndefined(driverMarker)) { return; } driverMarker.setMap(null); }, "setCustomerLocationMarkerLabel": function (text) { if (angular.isUndefined(customerMarker)) { return; } customerMarker.set('label', {text: text, color: "#fff", fontSize: "9px"}); } }; return service; }]);