diff --git a/404.html b/404.html index b801417f..f7a17719 100644 --- a/404.html +++ b/404.html @@ -125,7 +125,7 @@
Your browser is out of date!
- + diff --git a/assets/images/iaac.png b/assets/images/iaac.png index e527cb96..3c0c9c02 100644 Binary files a/assets/images/iaac.png and b/assets/images/iaac.png differ diff --git a/index.html b/index.html index b801417f..f7a17719 100644 --- a/index.html +++ b/index.html @@ -125,7 +125,7 @@
Your browser is out of date!
- + diff --git a/scripts/app-1ff5977f0f.js b/scripts/app-4cd2f18a1c.js similarity index 100% rename from scripts/app-1ff5977f0f.js rename to scripts/app-4cd2f18a1c.js index eae0796d..04dd890c 100644 --- a/scripts/app-1ff5977f0f.js +++ b/scripts/app-4cd2f18a1c.js @@ -9,1824 +9,1824 @@ 'use strict'; angular.module('app.components') - .factory('User', ['COUNTRY_CODES', function(COUNTRY_CODES) { + .controller('KitController', KitController); - /** - * User constructor - * @param {Object} userData - User data sent from API - * @property {number} id - User ID - * @property {string} username - Username - * @property {string} profile_picture - Avatar URL of user - * @property {Array} devices - Kits that belongs to this user - * @property {string} url - URL - * @property {string} city - User city - * @property {string} country - User country - */ + KitController.$inject = ['$state','$scope', '$stateParams', + 'sensor', 'FullDevice', '$mdDialog', 'belongsToUser', + 'timeUtils', 'animation', 'auth', + '$timeout', 'alert', '$q', 'device', + 'HasSensorDevice', 'geolocation', 'PreviewDevice']; + function KitController($state, $scope, $stateParams, + sensor, FullDevice, $mdDialog, belongsToUser, + timeUtils, animation, auth, + $timeout, alert, $q, device, + HasSensorDevice, geolocation, PreviewDevice) { - function User(userData) { - this.id = userData.id; - this.username = userData.username; - this.profile_picture = userData.profile_picture; - this.devices = userData.devices; - this.url = userData.url; - this.city = userData.location.city; - /*jshint camelcase: false */ - this.country = COUNTRY_CODES[userData.location.country_code]; - } - return User; - }]); + var vm = this; + var sensorsData = []; -})(); + var mainSensorID, compareSensorID; + var picker; + vm.deviceID = $stateParams.id; + vm.battery = {}; + vm.downloadData = downloadData; + vm.geolocate = geolocate; + vm.device = undefined; + vm.deviceBelongsToUser = belongsToUser; + vm.deviceWithoutData = false; + vm.legacyApiKey = belongsToUser ? + auth.getCurrentUser().data.key : + undefined; + vm.loadingChart = true; + vm.moveChart = moveChart; + vm.allowUpdateChart = true; + vm.ownerDevices = []; + vm.removeDevice = removeDevice; + vm.resetTimeOpts = resetTimeOpts; + vm.sampleDevices = []; + vm.selectedSensor = undefined; + vm.selectedSensorData = {}; + vm.selectedSensorToCompare = undefined; + vm.selectedSensorToCompareData = {}; + vm.sensors = []; + vm.chartSensors = []; + vm.sensorsToCompare = []; + vm.setFromLast = setFromLast; + vm.showSensorOnChart = showSensorOnChart; + vm.showStore = showStore; + vm.slide = slide; + vm.showRaw = false; + vm.timeOpt = ['60 minutes', 'day' , 'month']; + vm.timeOptSelected = timeOptSelected; + vm.updateInterval = 15000; + vm.hasRaw; + vm.sensorNames = {}; -(function() { - 'use strict'; + var focused = true; - angular.module('app.components') - .factory('NonAuthUser', ['User', function(User) { + // event listener on change of value of main sensor selector + $scope.$watch('vm.selectedSensor', function(newVal) { - function NonAuthUser(userData) { - User.call(this, userData); + // Prevents undisered calls if selected sensor is not yet defined + if (!newVal) { + return; } - NonAuthUser.prototype = Object.create(User.prototype); - NonAuthUser.prototype.constructor = User; - - return NonAuthUser; - }]); -})(); - -(function() { - 'use strict'; - - angular.module('app.components') - .factory('AuthUser', ['User', function(User) { - /** - * AuthUser constructor. Used for authenticated users - * @extends User - * @param {Object} userData - Contains user data sent from API - * @property {string} email - User email - * @property {string} role - User role. Ex: admin - * @property {string} key - Personal API Key - */ + vm.selectedSensorToCompare = undefined; + vm.selectedSensorToCompareData = {}; + vm.chartDataCompare = []; + compareSensorID = undefined; - function AuthUser(userData) { - User.call(this, userData); + setSensorSideChart(); - this.email = userData.email; - this.role = userData.role; - /*jshint camelcase: false */ - this.key = userData.legacy_api_key; - } - AuthUser.prototype = Object.create(User.prototype); - AuthUser.prototype.constructor = User; + vm.sensorsToCompare = getSensorsToCompare(); - return AuthUser; - }]); -})(); + $timeout(function() { + // TODO: Improvement, change how we set the colors + colorSensorCompareName(); + setSensor({type: 'main', value: newVal}); -(function() { - 'use strict'; + if (picker){ + changeChart([mainSensorID]); + } + }, 100); - angular.module('app.components') - .factory('Sensor', ['sensorUtils', 'timeUtils', function(sensorUtils, timeUtils) { + }); - /** - * Sensor constructor - * @param {Object} sensorData - Contains the data of a sensor sent from the API - * @property {string} name - Name of sensor - * @property {number} id - ID of sensor - * @property {string} unit - Unit of sensor. Ex: % - * @property {string} value - Last value sent. Ex: 95 - * @property {string} prevValue - Previous value before last value - * @property {string} lastReadingAt - last_reading_at for the sensor reading - * @property {string} icon - Icon URL for sensor - * @property {string} arrow - Icon URL for sensor trend(up, down or equal) - * @property {string} color - Color that belongs to sensor - * @property {object} measurement - Measurement - * @property {string} fullDescription - Full Description for popup - * @property {string} previewDescription - Short Description for dashboard. Max 140 chars - * @property {string} tags - Contains sensor tags for filtering the view - */ - function Sensor(sensorData) { + // event listener on change of value of compare sensor selector + $scope.$watch('vm.selectedSensorToCompare', function(newVal, oldVal) { + vm.sensorsToCompare.forEach(function(sensor) { + if(sensor.id === newVal) { + _.extend(vm.selectedSensorToCompareData, sensor); + } + }); - this.id = sensorData.id; - this.name = sensorData.name; - this.unit = sensorData.unit; - this.value = sensorUtils.getSensorValue(sensorData); - this.prevValue = sensorUtils.getSensorPrevValue(sensorData); - this.lastReadingAt = timeUtils.parseDate(sensorData.last_reading_at); - this.icon = sensorUtils.getSensorIcon(this.name); - this.arrow = sensorUtils.getSensorArrow(this.value, this.prevValue); - this.color = sensorUtils.getSensorColor(this.name); - this.measurement = sensorData.measurement; + $timeout(function() { + colorSensorCompareName(); + setSensor({type: 'compare', value: newVal}); - // Some sensors don't have measurements because they are ancestors - if (sensorData.measurement) { - var description = sensorData.measurement.description; - this.fullDescription = description; - this.previewDescription = description.length > 140 ? description.slice( - 0, 140).concat(' ... ') : description; - this.is_ancestor = false; - } else { - this.is_ancestor = true; + if(oldVal === undefined && newVal === undefined) { + return; } + changeChart([compareSensorID]); + }, 100); - // Get sensor tags - this.tags = sensorData.tags; - } + }); - return Sensor; - }]); -})(); -(function() { - 'use strict'; + $scope.$on('hideChartSpinner', function() { + vm.loadingChart = false; + }); - angular.module('app.components') - .factory('SearchResultLocation', ['SearchResult', function(SearchResult) { + $scope.$on('$destroy', function() { + focused = false; + $timeout.cancel(vm.updateTimeout); + }); - /** - * Search Result Location constructor - * @extends SearchResult - * @param {Object} object - Object that contains the search result data from API - * @property {number} lat - Latitude - * @property {number} lng - Longitude - */ - function SearchResultLocation(object) { - SearchResult.call(this, object); + $scope.$on('$viewContentLoaded', function(event){ + initialize(); + }); - this.lat = object.latitude; - this.lng = object.longitude; - this.layer = object.layer; - } - return SearchResultLocation; - }]); + function initialize() { + animation.viewLoaded(); + updatePeriodically(); + } -})(); + function pollAndUpdate(){ + vm.updateTimeout = $timeout(function() { + updatePeriodically(); + }, vm.updateInterval); + } -(function() { - 'use strict'; + function updatePeriodically(){ + getAndUpdateDevice().then(function(){ + pollAndUpdate(); + }); + } - angular.module('app.components') - .factory('SearchResult', ['searchUtils', function(searchUtils) { + function getAndUpdateDevice(){ + // TODO: Improvement UX Change below to && to avoid constant unhandled error + // Through reject is not possible + if (vm.deviceID || !isNaN(vm.deviceID)){ + return device.getDevice(vm.deviceID) + .then(function(deviceData) { + if (deviceData.is_private) { + deviceIsPrivate(); + } + var newDevice = new FullDevice(deviceData); + vm.prevDevice = vm.device; - /** - * Search Result constructor - * @param {Object} object - Object that belongs to a search result from API - * @property {string} type - Type of search result. Ex: Country, City, User, Device - * @property {number} id - ID of search result, only for user & device - * @property {string} name - Name of search result, only for user & device - * @property {string} location - Location of search result. Ex: 'Paris, France' - * @property {string} icon - URL for the icon that belongs to this search result - * @property {string} iconType - Type of icon. Can be either img or div - */ - - function SearchResult(object) { - this.type = object.type; - this.id = object.id; - this.name = searchUtils.parseName(object); - this.location = searchUtils.parseLocation(object); - this.icon = searchUtils.parseIcon(object, this.type); - this.iconType = searchUtils.parseIconType(this.type); - } - return SearchResult; - }]); -})(); - -(function () { - 'use strict'; + if (vm.prevDevice) { + /* Kit already loaded. We are waiting for updates */ + if (vm.prevDevice.state.name !== 'has published' && newDevice.state.name === 'has published'){ + /* The kit has just published data for the first time. Fully reload the view */ + return $q.reject({justPublished: true}); + } else if(new Date(vm.prevDevice.lastReadingAt.raw) >= new Date(newDevice.lastReadingAt.raw)) { + /* Break if there's no new data*/ + return $q.reject(); + } + } - angular.module('app.components') - .factory('PreviewDevice', ['Device', function (Device) { + vm.device = newDevice; + setOwnerSampleDevices(); - /** - * Preview Device constructor. - * Used for devices stacked in a list, like in User Profile or Device states - * @extends Device - * @constructor - * @param {Object} object - Object with all the data about the device from the API - */ - function PreviewDevice(object) { - Device.call(this, object); + if (vm.device.state.name === 'has published') { + /* Device has data */ + setDeviceOnMap(); + setChartTimeRange(); + deviceAnnouncements(); - this.dropdownOptions = []; - this.dropdownOptions.push({ text: 'EDIT', value: '1', href: 'kits/' + this.id + '/edit', icon: 'fa fa-edit' }); - this.dropdownOptions.push({ text: 'SD CARD UPLOAD', value: '2', href: 'kits/' + this.id + '/upload', icon: 'fa fa-sd-card' }); + /*Load sensor if it has already published*/ + return $q.all([getMainSensors(vm.device), getCompareSensors(vm.device)]); + } else { + /* Device just loaded and has no data yet */ + return $q.reject({noSensorData: true}); + } + }) + .then(setSensors, killSensorsLoading); } - PreviewDevice.prototype = Object.create(Device.prototype); - PreviewDevice.prototype.constructor = Device; - return PreviewDevice; - }]); -})(); - -(function() { - 'use strict'; + } - angular.module('app.components') - .factory('HasSensorDevice', ['Device', function(Device) { + function killSensorsLoading(error){ + if(error) { + if(error.status === 404) { + $state.go('layout.404'); + } + else if (error.justPublished) { + $state.transitionTo($state.current, {reloadMap: true, id: vm.deviceID}, { + reload: true, inherit: false, notify: true + }); + } + else if (error.noSensorData) { + deviceHasNoData(); + } + else if (error.status === 403){ + deviceIsPrivate(); + } + } + } - function HasSensorDevice(object) { - Device.call(this, object); + function deviceAnnouncements(){ + if(!timeUtils.isWithin(1, 'months', vm.device.lastReadingAt.raw)) { + //TODO: Cosmetic Update the message + alert.info.longTime(); + } + /* The device has just published data after not publishing for 15min */ + else if(vm.prevDevice && timeUtils.isDiffMoreThan15min(vm.prevDevice.lastReadingAt.raw, vm.device.lastReadingAt.raw)) { + alert.success('Your Kit just published again!'); + } + } - this.sensors = object.data.sensors; - this.longitude = object.data.location.longitude; - this.latitude = object.data.location.latitude; + function deviceHasNoData() { + vm.deviceWithoutData = true; + animation.deviceWithoutData({device: vm.device, belongsToUser:vm.deviceBelongsToUser}); + if(vm.deviceBelongsToUser) { + alert.info.noData.owner($stateParams.id); + } else { + alert.info.noData.visitor(); } + } - HasSensorDevice.prototype = Object.create(Device.prototype); - HasSensorDevice.prototype.constructor = Device; + function deviceIsPrivate() { + alert.info.noData.private(); + } - HasSensorDevice.prototype.sensorsHasData = function() { - var parsedSensors = this.sensors.map(function(sensor) { - return sensor.value; + function setOwnerSampleDevices() { + // TODO: Refactor - this information is in the user, no need to go to devices + getOwnerDevices(vm.device, -6) + .then(function(ownerDevices){ + vm.sampleDevices = ownerDevices; }); + } - return _.some(parsedSensors, function(sensorValue) { - return !!sensorValue; - }); - }; + function setChartTimeRange() { + if(vm.allowUpdateChart) { + /* Init the chart range to default if doesn't exist of the user hasn't interacted */ + picker = initializePicker(); + } + } - return HasSensorDevice; - }]); -})(); + function setDeviceOnMap() { + animation.deviceLoaded({lat: vm.device.latitude, lng: vm.device.longitude, + id: vm.device.id}); + } -(function() { - 'use strict'; + function setSensors(sensorsRes){ - angular.module('app.components') - .factory('FullDevice', ['Device', 'Sensor', 'deviceUtils', function(Device, Sensor, deviceUtils) { + var mainSensors = sensorsRes[0]; + var compareSensors = sensorsRes[1]; - /** - * Full Device constructor. - * @constructor - * @extends Device - * @param {Object} object - Object with all the data about the device from the API - * @property {Object} owner - Device owner data - * @property {Array} data - Device sensor's data - * @property {Array} sensors - Device sensors data - * @property {Array} postProcessing - Device postprocessing - */ - function FullDevice(object) { - Device.call(this, object); + vm.battery = _.find(mainSensors, {name: 'battery'}); + vm.sensors = mainSensors.reverse(); + vm.sensors.forEach(checkRaw); + vm.sensors.forEach(getHardwareName); - this.owner = deviceUtils.parseOwner(object); - this.postProcessing = object.postprocessing; - this.data = object.data; - this.sensors = object.data.sensors; - } + setSensorSideChart(); - FullDevice.prototype = Object.create(Device.prototype); - FullDevice.prototype.constructor = FullDevice; + if (!vm.selectedSensor) { + vm.chartSensors = vm.sensors; + vm.sensorsToCompare = compareSensors; + vm.selectedSensor = (vm.sensors && vm.sensors[0]) ? vm.sensors[0].id : undefined; + } - FullDevice.prototype.getSensors = function(options) { - var sensors = _(this.data.sensors) - .chain() - .map(function(sensor) { - return new Sensor(sensor); - }).sort(function(a, b) { - /* This is a temporary hack to set always PV panel at the end*/ - if (a.id === 18){ return -1;} - if (b.id === 18){ return 1;} - /* This is a temporary hack to set always the Battery at the end*/ - if (a.id === 17){ return -1;} - if (b.id === 17){ return 1;} - /* This is a temporary hack to set always the Battery at the end*/ - if (a.id === 10){ return -1;} - if (b.id === 10){ return 1;} - /* After the hacks, sort the sensors by id */ - return b.id - a.id; - }) - .tap(function(sensors) { - if(options.type === 'compare') { - sensors.unshift({ - name: 'NONE', - color: 'white', - id: -1 - }); - } - }) - .value(); - return sensors; - }; + animation.mapStateLoaded(); + } - return FullDevice; - }]); -})(); + function checkRaw(value){ + vm.hasRaw |= (value.tags.indexOf('raw') !== -1); + } -(function() { - 'use strict'; + function getHardwareName(value) { + vm.sensorNames[value.id] = vm.device.sensors.find(element => element.id === value.id).name; + } + function setSensorSideChart() { + if(vm.sensors){ + vm.sensors.forEach(function(sensor) { + if(sensor.id === vm.selectedSensor) { + _.extend(vm.selectedSensorData, sensor); + } + }); + } + } - angular.module('app.components') - .factory('Device', ['deviceUtils', 'timeUtils', function(deviceUtils, timeUtils) { + function removeDevice() { + var confirm = $mdDialog.confirm() + .title('Delete this kit?') + .textContent('Are you sure you want to delete this kit?') + .ariaLabel('') + .ok('DELETE') + .cancel('Cancel') + .theme('primary') + .clickOutsideToClose(true); - /** - * Device constructor. - * @constructor - * @param {Object} object - Object with all the data about the device from the API - * @property {number} id - ID of the device - * @property {string} name - Name of the device - * @property {string} state - State of the device. Ex: Never published - * @property {string} description - Device description - * @property {Array} systemTags - System tags - * @property {Array} userTags - User tags. Ex: '' - * @property {bool} isPrivate - True if private device - * @property {Array} notifications - Notifications for low battery and stopped publishing - * @property {Object} lastReadingAt - last_reading_at: raw, ago, and parsed - * @property {Object} createdAt - created_at: raw, ago, and parsed - * @property {Object} updatedAt - updated_at: raw, ago, and parsed - * @property {Object} location - Location of device. Object with lat, long, elevation, city, country, country_code - * @property {string} locationString - Location of device. Ex: Madrid, Spain; Germany; Paris, France - * @property {Object} hardware - Device hardware field. Contains type, version, info, slug and name - * @property {string} hardwareName - Device hardware name - * @property {bool} isLegacy - True if legacy device - * @property {bool} isSCK - True if SC device - * @property {string} avatar - URL that contains the user avatar - */ - function Device(object) { - // Basic information - this.id = object.id; - this.name = object.name; - this.state = deviceUtils.parseState(object); - this.description = object.description; - this.token = object.device_token; - this.macAddress = object.mac_address; + $mdDialog + .show(confirm) + .then(function(){ + device + .removeDevice(vm.device.id) + .then(function(){ + alert.success('Your kit was deleted successfully'); + device.updateContext().then(function(){ + $state.transitionTo('layout.myProfile.kits', $stateParams, + { reload: false, + inherit: false, + notify: true + }); + }); + }) + .catch(function(){ + alert.error('Error trying to delete your kit.'); + }); + }); + } - // Tags and dates - this.systemTags = deviceUtils.parseSystemTags(object); - this.userTags = deviceUtils.parseUserTags(object); - this.isPrivate = deviceUtils.isPrivate(object); - this.preciseLocation = deviceUtils.preciseLocation(object); - this.enableForwarding = deviceUtils.enableForwarding(object); - this.notifications = deviceUtils.parseNotifications(object); - this.lastReadingAt = timeUtils.parseDate(object.last_reading_at); - this.createdAt = timeUtils.parseDate(object.created_at); - this.updatedAt = timeUtils.parseDate(object.updated_at); + function showSensorOnChart(sensorID) { + vm.selectedSensor = sensorID; + } - // Location - this.location = object.location; - this.locationString = deviceUtils.parseLocation(object); + function slide(direction) { + var slideContainer = angular.element('.sensors_container'); + var scrollPosition = slideContainer.scrollLeft(); + var width = slideContainer.width(); + var slideStep = width/2; - // Hardware - this.hardware = deviceUtils.parseHardware(object); - this.hardwareName = deviceUtils.parseHardwareName(this); - this.isLegacy = deviceUtils.isLegacyVersion(this); - this.isSCK = deviceUtils.isSCKHardware(this); - // this.class = deviceUtils.classify(object); // TODO - Do we need this? + if(direction === 'left') { + slideContainer.animate({'scrollLeft': scrollPosition + slideStep}, + {duration: 250, queue:false}); + } else if(direction === 'right') { + slideContainer.animate({'scrollLeft': scrollPosition - slideStep}, + {duration: 250, queue:false}); + } + } - this.avatar = deviceUtils.parseAvatar(); - /*jshint camelcase: false */ + function getSensorsToCompare() { + return vm.sensors ? vm.sensors.filter(function(sensor) { + return sensor.id !== vm.selectedSensor; + }) : []; + } + + function changeChart(sensorsID, options) { + if(!sensorsID[0]) { + return; } - return Device; - }]); -})(); + if(!options) { + options = {}; + } + options.from = options && options.from || picker.getValuePickerFrom(); + options.to = options && options.to || picker.getValuePickerTo(); -(function() { - 'use strict'; + //show spinner + vm.loadingChart = true; + //grab chart data and save it - angular.module('app.components') - .factory('Marker', ['deviceUtils', 'markerUtils', 'timeUtils', '$state', function(deviceUtils, markerUtils, timeUtils, $state) { - /** - * Marker constructor - * @constructor - * @param {Object} deviceData - Object with data about marker from API - * @property {number} lat - Latitude - * @property {number} lng - Longitude - * @property {string} message - Message inside marker popup - * @property {Object} icon - Object with classname, size and type of marker icon - * @property {string} layer - Map layer that icons belongs to - * @property {boolean} focus - Whether marker popup is opened - * @property {Object} myData - Marker id and labels - */ - function Marker(deviceData) { - let linkStart = '', linkEnd = ''; - const id = deviceData.id; - if ($state.$current.name === 'embbed') { - linkStart = ''; - linkEnd = ''; - } - this.lat = deviceUtils.parseCoordinates(deviceData).lat; - this.lng = deviceUtils.parseCoordinates(deviceData).lng; - // TODO: Bug, pop-up lastreading at doesn't get updated by publication - this.message = ''; + // it can be either 2 sensors or 1 sensor, so we use $q.all to wait for all + $q.all( + _.map(sensorsID, function(sensorID) { + return getChartData($stateParams.id, sensorID, options.from, options.to) + .then(function(data) { + return data; + }); + }) + ).then(function() { + // after all sensors resolve, prepare data and attach it to scope + // the variable on the scope will pass the data to the chart directive + vm.chartDataMain = prepareChartData([mainSensorID, compareSensorID]); + }); + } + // calls api to get sensor data and saves it to sensorsData array + function getChartData(deviceID, sensorID, dateFrom, dateTo, options) { + return sensor.getSensorsData(deviceID, sensorID, dateFrom, dateTo) + .then(function(data) { + //save sensor data of this kit so that it can be reused + sensorsData[sensorID] = data.readings; + return data; + }); + } - this.icon = markerUtils.getIcon(deviceData); - this.layer = 'devices'; - this.focus = false; - this.myData = { - id: id, - labels: deviceUtils.parseSystemTags(deviceData), - tags: deviceUtils.parseUserTags(deviceData) + function prepareChartData(sensorsID) { + var compareSensor; + var parsedDataMain = parseSensorData(sensorsData, sensorsID[0]); + var mainSensor = { + data: parsedDataMain, + color: vm.selectedSensorData.color, + unit: vm.selectedSensorData.unit + }; + if(sensorsID[1] && sensorsID[1] !== -1) { + var parsedDataCompare = parseSensorData(sensorsData, sensorsID[1]); + + compareSensor = { + data: parsedDataCompare, + color: vm.selectedSensorToCompareData.color, + unit: vm.selectedSensorToCompareData.unit }; } - return Marker; - - function createTagsTemplate(tagsArr, tagType, clickable) { - if(typeof(clickable) === 'undefined'){ - clickable = false; - } - var clickablTag = ''; - if(clickable){ - clickablTag = 'clickable'; - } + var newChartData = [mainSensor, compareSensor]; + return newChartData; + } - if(!tagType){ - tagType = 'tag'; - } + function parseSensorData(data, sensorID) { + if(data.length === 0) { + return []; + } + return data[sensorID].map(function(dataPoint) { + var time = timeUtils.formatDate(dataPoint[0]); + var value = dataPoint[1]; + var count = value === null ? 0 : value; + return { + time: time, + count: count, + value: value + }; + }); + } - return _.reduce(tagsArr, function(acc, label) { - var element =''; - if(tagType === 'tag'){ - element = ''; - }else{ - element = ''+label+''; - } - return acc.concat(element); - }, ''); + function setSensor(options) { + var sensorID = options.value; + if(sensorID === undefined) { + return; + } + if(options.type === 'main') { + mainSensorID = sensorID; + } else if(options.type === 'compare') { + compareSensorID = sensorID; } + } - }]); -})(); + function colorSensorCompareName() { + var name = angular.element('.sensor_compare').find('md-select-label').find('span'); + name.css('color', vm.selectedSensorToCompareData.color || 'white'); + var icon = angular.element('.sensor_compare').find('md-select-label').find('.md-select-icon'); + icon.css('color', 'white'); + } -(function() { - 'use strict'; + function getCurrentRange() { + var to = moment(picker.getValuePickerTo()); + var from = moment(picker.getValuePickerFrom()); + return to.diff(from)/1000; + } - angular.module('app.components') - .directive('noDataBackdrop', noDataBackdrop); + function moveChart(direction) { - /** - * Backdrop for chart section when kit has no data - * - */ - noDataBackdrop.$inject = []; - - function noDataBackdrop() { - return { - restrict: 'A', - scope: {}, - templateUrl: 'app/core/animation/backdrop/noDataBackdrop.html', - controller: function($scope, $timeout) { - var vm = this; - - vm.deviceWithoutData = false; - vm.scrollToComments = scrollToComments; - - $scope.$on('deviceWithoutData', function(ev, data) { + var valueTo, valueFrom; + //grab current date range + var currentRange = getCurrentRange(); - $timeout(function() { - vm.device = data.device; - vm.deviceWithoutData = true; + /*jshint camelcase: false*/ + var from_picker = angular.element('#picker_from').pickadate('picker'); + var to_picker = angular.element('#picker_to').pickadate('picker'); - if (data.belongsToUser) { - vm.user = 'owner'; - } else { - vm.user = 'visitor'; - } - }, 0); + if(direction === 'left') { + //set both from and to pickers to prev range + valueTo = moment(picker.getValuePickerFrom()); + valueFrom = moment(picker.getValuePickerFrom()).subtract(currentRange, 'seconds'); - }); + picker.setValuePickers([valueFrom.toDate(), valueTo.toDate()]); - function scrollToComments(){ - location.hash = ''; - location.hash = '#disqus_thread'; + } else if(direction === 'right') { + var today = timeUtils.getToday(); + var currentValueTo = picker.getValuePickerTo(); + if( timeUtils.isSameDay(today, timeUtils.getMillisFromDate(currentValueTo)) ) { + return; } - }, - controllerAs: 'vm' - }; - } -})(); -(function() { - 'use strict'; - - angular.module('app.components') - .directive('loadingBackdrop', loadingBackdrop); - - /** - * Backdrop for app initialization and between states - * - */ - loadingBackdrop.$inject = []; - function loadingBackdrop() { - return { - templateUrl: 'app/core/animation/backdrop/loadingBackdrop.html', - controller: function($scope) { - var vm = this; - vm.isViewLoading = true; - vm.mapStateLoading = false; + valueFrom = moment(picker.getValuePickerTo()); + valueTo = moment(picker.getValuePickerTo()).add(currentRange, 'seconds'); - // listen for app loading event - $scope.$on('viewLoading', function() { - vm.isViewLoading = true; - }); + picker.setValuePickers([valueFrom.toDate(), valueTo.toDate()]); - $scope.$on('viewLoaded', function() { - vm.isViewLoading = false; - }); + } + resetTimeOpts(); + } - // listen for map state loading event - $scope.$on('mapStateLoading', function() { - if(vm.isViewLoading) { - return; - } - vm.mapStateLoading = true; - }); + //hide everything but the functions to interact with the pickers + function initializePicker() { + var range = {}; + /*jshint camelcase: false*/ + var from_$input = angular.element('#picker_from').pickadate({ + onOpen: function(){ + vm.resetTimeOpts(); + }, + onClose: function(){ + angular.element(document.activeElement).blur(); + }, + container: 'body', + klass: { + holder: 'picker__holder picker_container' + } + }); + var from_picker = from_$input.pickadate('picker'); - $scope.$on('mapStateLoaded', function() { - vm.mapStateLoading = false; - }); + var to_$input = angular.element('#picker_to').pickadate({ + onOpen: function(){ + vm.resetTimeOpts(); }, - controllerAs: 'vm' - }; - } -})(); + onClose: function(){ + angular.element(document.activeElement).blur(); + }, + container: 'body', + klass: { + holder: 'picker__holder picker_container' + } + }); -(function() { - 'use strict'; + var to_picker = to_$input.pickadate('picker'); - angular.module('app.components') - .controller('KitController', KitController); + if( from_picker.get('value') ) { + to_picker.set('min', from_picker.get('select') ); + } + if( to_picker.get('value') ) { + from_picker.set('max', to_picker.get('select') ); + } - KitController.$inject = ['$state','$scope', '$stateParams', - 'sensor', 'FullDevice', '$mdDialog', 'belongsToUser', - 'timeUtils', 'animation', 'auth', - '$timeout', 'alert', '$q', 'device', - 'HasSensorDevice', 'geolocation', 'PreviewDevice']; - function KitController($state, $scope, $stateParams, - sensor, FullDevice, $mdDialog, belongsToUser, - timeUtils, animation, auth, - $timeout, alert, $q, device, - HasSensorDevice, geolocation, PreviewDevice) { + from_picker.on('close', function(event) { + setFromRange(getCalculatedFrom(from_picker.get('value'))); + }); - var vm = this; - var sensorsData = []; + to_picker.on('close', function(event) { + setToRange(getCalculatedTo(to_picker.get('value'))); + }); - var mainSensorID, compareSensorID; - var picker; - vm.deviceID = $stateParams.id; - vm.battery = {}; - vm.downloadData = downloadData; - vm.geolocate = geolocate; - vm.device = undefined; - vm.deviceBelongsToUser = belongsToUser; - vm.deviceWithoutData = false; - vm.legacyApiKey = belongsToUser ? - auth.getCurrentUser().data.key : - undefined; - vm.loadingChart = true; - vm.moveChart = moveChart; - vm.allowUpdateChart = true; - vm.ownerDevices = []; - vm.removeDevice = removeDevice; - vm.resetTimeOpts = resetTimeOpts; - vm.sampleDevices = []; - vm.selectedSensor = undefined; - vm.selectedSensorData = {}; - vm.selectedSensorToCompare = undefined; - vm.selectedSensorToCompareData = {}; - vm.sensors = []; - vm.chartSensors = []; - vm.sensorsToCompare = []; - vm.setFromLast = setFromLast; - vm.showSensorOnChart = showSensorOnChart; - vm.showStore = showStore; - vm.slide = slide; - vm.showRaw = false; - vm.timeOpt = ['60 minutes', 'day' , 'month']; - vm.timeOptSelected = timeOptSelected; - vm.updateInterval = 15000; - vm.hasRaw; - vm.sensorNames = {}; + from_picker.on('set', function(event) { + if(event.select) { + to_picker.set('min', getFromRange()); + } else if( 'clear' in event) { + to_picker.set('min', false); + } + }); - var focused = true; + to_picker.on('set', function(event) { + if(event.select) { + from_picker.set('max', getToRange()); + } else if( 'clear' in event) { + from_picker.set('max', false); + } + }); - // event listener on change of value of main sensor selector - $scope.$watch('vm.selectedSensor', function(newVal) { + //set to-picker max to today + to_picker.set('max', getLatestUpdated()); - // Prevents undisered calls if selected sensor is not yet defined - if (!newVal) { - return; + function getSevenDaysAgoFromLatestUpdate() { + var lastTime = moment(vm.device.lastReadingAt.raw); + return lastTime.subtract(7, 'days').valueOf(); } - vm.selectedSensorToCompare = undefined; - vm.selectedSensorToCompareData = {}; - vm.chartDataCompare = []; - compareSensorID = undefined; + function getLatestUpdated() { + return moment(vm.device.lastReadingAt.raw).toDate(); + } - setSensorSideChart(); + function getCalculatedFrom(pickerTimeFrom) { + var from, + pickerTime; - vm.sensorsToCompare = getSensorsToCompare(); + pickerTime = moment(pickerTimeFrom, 'D MMMM, YYYY'); + from = pickerTime.startOf('day'); - $timeout(function() { - // TODO: Improvement, change how we set the colors - colorSensorCompareName(); - setSensor({type: 'main', value: newVal}); + return from; + } - if (picker){ - changeChart([mainSensorID]); - } - }, 100); + function getCalculatedTo(pickerTimeTo) { + var to, + pickerTime; - }); + pickerTime = moment(pickerTimeTo, 'D MMMM, YYYY'); - // event listener on change of value of compare sensor selector - $scope.$watch('vm.selectedSensorToCompare', function(newVal, oldVal) { - vm.sensorsToCompare.forEach(function(sensor) { - if(sensor.id === newVal) { - _.extend(vm.selectedSensorToCompareData, sensor); - } - }); + to = pickerTime.endOf('day'); + if (moment().diff(to) < 0) { + var now = moment(); + to = pickerTime.set({ + 'hour' : now.get('hour'), + 'minute' : now.get('minute'), + 'second' : now.get('second') + }); + } - $timeout(function() { - colorSensorCompareName(); - setSensor({type: 'compare', value: newVal}); + return to; + } - if(oldVal === undefined && newVal === undefined) { - return; - } - changeChart([compareSensorID]); - }, 100); + function updateChart() { + var sensors = [mainSensorID, compareSensorID]; + sensors = sensors.filter(function(sensor) { + return sensor; + }); + changeChart(sensors, { + from: range.from, + to: range.to + }); + } - }); + function setFromRange(from) { + range.from = from; + from_picker.set('select', getFromRange()); + updateChart(); + } - $scope.$on('hideChartSpinner', function() { - vm.loadingChart = false; - }); + function setToRange(to) { + range.to = to; + to_picker.set('select', getToRange()); + updateChart(); + } - $scope.$on('$destroy', function() { - focused = false; - $timeout.cancel(vm.updateTimeout); - }); + function getFromRange() { + return moment(range.from).toDate(); + } - $scope.$on('$viewContentLoaded', function(event){ - initialize(); - }); + function getToRange() { + return moment(range.to).toDate(); + } - function initialize() { - animation.viewLoaded(); - updatePeriodically(); - } + function setRange(from, to) { + range.from = from; + range.to = to; + from_picker.set('select', getFromRange()); + to_picker.set('select', getToRange()); + updateChart(); + } - function pollAndUpdate(){ - vm.updateTimeout = $timeout(function() { - updatePeriodically(); - }, vm.updateInterval); - } + if(vm.device){ + if(vm.device.systemTags.includes('new')){ + var lastUpdate = getLatestUpdated(); + setRange(timeUtils.getHourBefore(lastUpdate), lastUpdate); + } else if (timeUtils.isWithin(7, 'days', vm.device.lastReadingAt.raw) || !vm.device.lastReadingAt.raw) { + //set from-picker to seven days ago and set to-picker to today + setRange(timeUtils.getSevenDaysAgo(), timeUtils.getToday()); + } else { + // set from-picker to and set to-picker to today + setRange(getSevenDaysAgoFromLatestUpdate(), getLatestUpdated()); + } + } - function updatePeriodically(){ - getAndUpdateDevice().then(function(){ - pollAndUpdate(); - }); + // api to interact with the picker from outside + return { + getValuePickerFrom: function() { + return getFromRange(); + }, + setValuePickerFrom: function(newValue) { + setFromRange(newValue); + }, + getValuePickerTo: function() { + return getToRange(); + }, + setValuePickerTo: function(newValue) { + setToRange(newValue); + }, + setValuePickers: function(newValues) { + var from = newValues[0]; + var to = newValues[1]; + setRange(from, to); + } + }; } - function getAndUpdateDevice(){ - // TODO: Improvement UX Change below to && to avoid constant unhandled error - // Through reject is not possible - if (vm.deviceID || !isNaN(vm.deviceID)){ - return device.getDevice(vm.deviceID) - .then(function(deviceData) { - if (deviceData.is_private) { - deviceIsPrivate(); - } - var newDevice = new FullDevice(deviceData); - vm.prevDevice = vm.device; - - if (vm.prevDevice) { - /* Kit already loaded. We are waiting for updates */ - if (vm.prevDevice.state.name !== 'has published' && newDevice.state.name === 'has published'){ - /* The kit has just published data for the first time. Fully reload the view */ - return $q.reject({justPublished: true}); - } else if(new Date(vm.prevDevice.lastReadingAt.raw) >= new Date(newDevice.lastReadingAt.raw)) { - /* Break if there's no new data*/ - return $q.reject(); - } - } + function geolocate() { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(function(position){ + if(!position){ + alert.error('Please, allow smartcitizen to geolocate your' + + 'position so we can find a kit near you.'); + return; + } - vm.device = newDevice; - setOwnerSampleDevices(); + geolocation.grantHTML5Geolocation(); - if (vm.device.state.name === 'has published') { - /* Device has data */ - setDeviceOnMap(); - setChartTimeRange(); - deviceAnnouncements(); + var location = { + lat:position.coords.latitude, + lng:position.coords.longitude + }; + device.getDevices(location) + .then(function(data){ + data = data.plain(); - /*Load sensor if it has already published*/ - return $q.all([getMainSensors(vm.device), getCompareSensors(vm.device)]); - } else { - /* Device just loaded and has no data yet */ - return $q.reject({noSensorData: true}); - } - }) - .then(setSensors, killSensorsLoading); + _(data) + .chain() + .map(function(device) { + return new HasSensorDevice(device); + }) + .filter(function(device) { + return !!device.longitude && !!device.latitude; + }) + .find(function(device) { + return _.includes(device.labels, 'online'); + }) + .tap(function(closestDevice) { + if(focused){ + if(closestDevice) { + $state.go('layout.home.kit', {id: closestDevice.id}); + } else { + $state.go('layout.home.kit', {id: data[0].id}); + } + } + }) + .value(); + }); + }); } } - function killSensorsLoading(error){ - if(error) { - if(error.status === 404) { - $state.go('layout.404'); - } - else if (error.justPublished) { - $state.transitionTo($state.current, {reloadMap: true, id: vm.deviceID}, { - reload: true, inherit: false, notify: true - }); - } - else if (error.noSensorData) { - deviceHasNoData(); - } - else if (error.status === 403){ - deviceIsPrivate(); + function downloadData(device){ + $mdDialog.show({ + hasBackdrop: true, + controller: 'DownloadModalController', + controllerAs: 'vm', + templateUrl: 'app/components/download/downloadModal.html', + clickOutsideToClose: true, + locals: {thisDevice: device} + }).then(function(){ + var alert = $mdDialog.alert() + .title('SUCCESS') + .textContent('We are processing your data. Soon you will be notified in your inbox') + .ariaLabel('') + .ok('OK!') + .theme('primary') + .clickOutsideToClose(true); + + $mdDialog.show(alert); + }).catch(function(err){ + if (!err){ + return; } - } - } + var errorAlert = $mdDialog.alert() + .title('ERROR') + .textContent('Uh-oh, something went wrong') + .ariaLabel('') + .ok('D\'oh') + .theme('primary') + .clickOutsideToClose(false); - function deviceAnnouncements(){ - if(!timeUtils.isWithin(1, 'months', vm.device.lastReadingAt.raw)) { - //TODO: Cosmetic Update the message - alert.info.longTime(); - } - /* The device has just published data after not publishing for 15min */ - else if(vm.prevDevice && timeUtils.isDiffMoreThan15min(vm.prevDevice.lastReadingAt.raw, vm.device.lastReadingAt.raw)) { - alert.success('Your Kit just published again!'); - } + $mdDialog.show(errorAlert); + }); } - function deviceHasNoData() { - vm.deviceWithoutData = true; - animation.deviceWithoutData({device: vm.device, belongsToUser:vm.deviceBelongsToUser}); - if(vm.deviceBelongsToUser) { - alert.info.noData.owner($stateParams.id); - } else { - alert.info.noData.visitor(); + function getMainSensors(deviceData) { + if(!deviceData) { + return undefined; } + return deviceData.getSensors({type: 'main'}); } - - function deviceIsPrivate() { - alert.info.noData.private(); + function getCompareSensors(deviceData) { + if(!vm.device) { + return undefined; + } + deviceData.getSensors({type: 'compare'}); } - - function setOwnerSampleDevices() { - // TODO: Refactor - this information is in the user, no need to go to devices - getOwnerDevices(vm.device, -6) - .then(function(ownerDevices){ - vm.sampleDevices = ownerDevices; - }); + function getOwnerDevices(deviceData, sampling) { + if(!deviceData) { + return undefined; + } + var deviceIDs = deviceData.owner.devices.slice(sampling); + // var ownerID = deviceData.owner.id; + // TODO: Refactor This is in the user endpoint, no need to query devices + return $q.all( + deviceIDs.map(function(id) { + return device.getDevice(id) + .then(function(data) { + return new PreviewDevice(data); + }); + }) + ); } - function setChartTimeRange() { - if(vm.allowUpdateChart) { - /* Init the chart range to default if doesn't exist of the user hasn't interacted */ - picker = initializePicker(); + function setFromLast(what){ + /* This will not show the last 60 minutes or 24 hours, + instead it will show the last hour or day*/ + var to, from; + if (what === '60 minutes') { + to = moment(vm.device.lastReadingAt.raw); + from = moment(vm.device.lastReadingAt.raw).subtract(60, 'minutes'); + } else { + to = moment(vm.device.lastReadingAt.raw).endOf(what); + from = moment(vm.device.lastReadingAt.raw).startOf(what); + } + // Check if we are in the future + if (moment().diff(to) < 0){ + to = moment(vm.device.lastReadingAt.raw); } + picker.setValuePickers([from.toDate(), to.toDate()]); } - function setDeviceOnMap() { - animation.deviceLoaded({lat: vm.device.latitude, lng: vm.device.longitude, - id: vm.device.id}); + function timeOptSelected(){ + vm.allowUpdateChart = false; + if (vm.dropDownSelection){ + setFromLast(vm.dropDownSelection); + } + } + function resetTimeOpts(){ + vm.allowUpdateChart = false; + vm.dropDownSelection = undefined; } - function setSensors(sensorsRes){ + function showStore() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'StoreModalController', + templateUrl: 'app/components/store/storeModal.html', + clickOutsideToClose: true + }); + } + } +})(); - var mainSensors = sensorsRes[0]; - var compareSensors = sensorsRes[1]; +(function() { + 'use strict'; - vm.battery = _.find(mainSensors, {name: 'battery'}); - vm.sensors = mainSensors.reverse(); - vm.sensors.forEach(checkRaw); - vm.sensors.forEach(getHardwareName); + angular.module('app.components') + .controller('NewKitController', NewKitController); - setSensorSideChart(); + NewKitController.$inject = ['$scope', '$state', 'animation', 'device', 'tag', 'alert', 'auth', '$stateParams', '$timeout']; + function NewKitController($scope, $state, animation, device, tag, alert, auth, $stateParams, $timeout) { + var vm = this; - if (!vm.selectedSensor) { - vm.chartSensors = vm.sensors; - vm.sensorsToCompare = compareSensors; - vm.selectedSensor = (vm.sensors && vm.sensors[0]) ? vm.sensors[0].id : undefined; - } + vm.step = 1; + vm.submitStepOne = submitStepOne; + vm.backToProfile = backToProfile; - animation.mapStateLoaded(); - } + // FORM INFO + vm.deviceForm = { + name: undefined, + exposure: undefined, + location: { + lat: undefined, + lng: undefined, + zoom: 16 + }, + is_private: false, + legacyVersion: '1.1', + tags: [] + }; - function checkRaw(value){ - vm.hasRaw |= (value.tags.indexOf('raw') !== -1); - } + // EXPOSURE SELECT + vm.exposure = [ + {name: 'indoor', value: 1}, + {name: 'outdoor', value: 2} + ]; - function getHardwareName(value) { - vm.sensorNames[value.id] = vm.device.sensors.find(element => element.id === value.id).name; - } - function setSensorSideChart() { - if(vm.sensors){ - vm.sensors.forEach(function(sensor) { - if(sensor.id === vm.selectedSensor) { - _.extend(vm.selectedSensorData, sensor); - } - }); - } - } + // VERSION SELECT + vm.version = [ + {name: 'Smart Citizen Kit 1.0', value: '1.0'}, + {name: 'Smart Citizen Kit 1.1', value: '1.1'} + ]; - function removeDevice() { - var confirm = $mdDialog.confirm() - .title('Delete this kit?') - .textContent('Are you sure you want to delete this kit?') - .ariaLabel('') - .ok('DELETE') - .cancel('Cancel') - .theme('primary') - .clickOutsideToClose(true); + $scope.$on('leafletDirectiveMarker.dragend', function(event, args){ + vm.deviceForm.location.lat = args.model.lat; + vm.deviceForm.location.lng = args.model.lng; + }); - $mdDialog - .show(confirm) - .then(function(){ - device - .removeDevice(vm.device.id) - .then(function(){ - alert.success('Your kit was deleted successfully'); - device.updateContext().then(function(){ - $state.transitionTo('layout.myProfile.kits', $stateParams, - { reload: false, - inherit: false, - notify: true - }); - }); - }) - .catch(function(){ - alert.error('Error trying to delete your kit.'); - }); + // TAGS SELECT + vm.tags = []; + $scope.$watch('vm.tag', function(newVal, oldVal) { + if(!newVal) { + return; + } + // remove selected tag from select element + vm.tag = undefined; + + var alreadyPushed = _.some(vm.deviceForm.tags, function(tag) { + return tag.id === newVal; }); - } + if(alreadyPushed) { + return; + } - function showSensorOnChart(sensorID) { - vm.selectedSensor = sensorID; - } + var tag = _.find(vm.tags, function(tag) { + return tag.id === newVal; + }); + vm.deviceForm.tags.push(tag); + }); + vm.removeTag = removeTag; - function slide(direction) { - var slideContainer = angular.element('.sensors_container'); - var scrollPosition = slideContainer.scrollLeft(); - var width = slideContainer.width(); - var slideStep = width/2; + // MAP CONFIGURATION + var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; - if(direction === 'left') { - slideContainer.animate({'scrollLeft': scrollPosition + slideStep}, - {duration: 250, queue:false}); - } else if(direction === 'right') { - slideContainer.animate({'scrollLeft': scrollPosition - slideStep}, - {duration: 250, queue:false}); - } - } + vm.getLocation = getLocation; + vm.markers = { + main: { + lat: undefined, + lng: undefined, + draggable: true + } + }; + vm.tiles = { + url: 'https://api.mapbox.com/styles/v1/mapbox/streets-v10/tiles/{z}/{x}/{y}?access_token=' + mapBoxToken + }; + vm.defaults = { + scrollWheelZoom: false + }; - function getSensorsToCompare() { - return vm.sensors ? vm.sensors.filter(function(sensor) { - return sensor.id !== vm.selectedSensor; - }) : []; - } + vm.macAddress = undefined; - function changeChart(sensorsID, options) { - if(!sensorsID[0]) { - return; + initialize(); + + ////////////// + + function initialize() { + animation.viewLoaded(); + getTags(); + vm.userRole = auth.getCurrentUser().data.role; } - if(!options) { - options = {}; + function getLocation() { + window.navigator.geolocation.getCurrentPosition(function(position) { + $scope.$apply(function() { + var lat = position.coords.latitude; + var lng = position.coords.longitude; + vm.deviceForm.location.lat = lat; + vm.deviceForm.location.lng = lng; + vm.markers.main.lat = lat; + vm.markers.main.lng = lng; + }); + }); } - options.from = options && options.from || picker.getValuePickerFrom(); - options.to = options && options.to || picker.getValuePickerTo(); - //show spinner - vm.loadingChart = true; - //grab chart data and save it + function removeTag(tagID) { + vm.deviceForm.tags = _.filter(vm.deviceForm.tags, function(tag) { + return tag.id !== tagID; + }); + } - // it can be either 2 sensors or 1 sensor, so we use $q.all to wait for all - $q.all( - _.map(sensorsID, function(sensorID) { - return getChartData($stateParams.id, sensorID, options.from, options.to) - .then(function(data) { - return data; - }); - }) - ).then(function() { - // after all sensors resolve, prepare data and attach it to scope - // the variable on the scope will pass the data to the chart directive - vm.chartDataMain = prepareChartData([mainSensorID, compareSensorID]); - }); - } - // calls api to get sensor data and saves it to sensorsData array - function getChartData(deviceID, sensorID, dateFrom, dateTo, options) { - return sensor.getSensorsData(deviceID, sensorID, dateFrom, dateTo) - .then(function(data) { - //save sensor data of this kit so that it can be reused - sensorsData[sensorID] = data.readings; - return data; - }); - } - - function prepareChartData(sensorsID) { - var compareSensor; - var parsedDataMain = parseSensorData(sensorsData, sensorsID[0]); - var mainSensor = { - data: parsedDataMain, - color: vm.selectedSensorData.color, - unit: vm.selectedSensorData.unit - }; - if(sensorsID[1] && sensorsID[1] !== -1) { - var parsedDataCompare = parseSensorData(sensorsData, sensorsID[1]); - - compareSensor = { - data: parsedDataCompare, - color: vm.selectedSensorToCompareData.color, - unit: vm.selectedSensorToCompareData.unit + function submitStepOne() { + var data = { + name: vm.deviceForm.name, + description: vm.deviceForm.description, + exposure: findExposure(vm.deviceForm.exposure), + latitude: vm.deviceForm.location.lat, + longitude: vm.deviceForm.location.lng, + is_private: vm.deviceForm.is_private, + hardware_version_override: vm.deviceForm.legacyVersion, + /*jshint camelcase: false */ + user_tags: _.map(vm.deviceForm.tags, 'name').join(',') }; + + device.createDevice(data) + .then( + function(response) { + device.updateContext().then(function(){ + auth.setCurrentUser('appLoad').then(function(){ + $timeout($state.go('layout.kitEdit', {id:response.id, step:2}), 2000); + }); + }); + }, + function(err) { + vm.errors = err.data.errors; + alert.error('There has been an error during kit set up'); + }); } - var newChartData = [mainSensor, compareSensor]; - return newChartData; - } - function parseSensorData(data, sensorID) { - if(data.length === 0) { - return []; + function getTags() { + tag.getTags() + .then(function(tagsData) { + vm.tags = tagsData; + }); } - return data[sensorID].map(function(dataPoint) { - var time = timeUtils.formatDate(dataPoint[0]); - var value = dataPoint[1]; - var count = value === null ? 0 : value; - return { - time: time, - count: count, - value: value - }; - }); - } - function setSensor(options) { - var sensorID = options.value; - if(sensorID === undefined) { - return; + function toProfile(){ + $state.transitionTo('layout.myProfile.kits', $stateParams, + { reload: false, + inherit: false, + notify: true + }); } - if(options.type === 'main') { - mainSensorID = sensorID; - } else if(options.type === 'compare') { - compareSensorID = sensorID; + + function backToProfile(){ + // TODO: Refactor Check + toProfile(); } - } - function colorSensorCompareName() { - var name = angular.element('.sensor_compare').find('md-select-label').find('span'); - name.css('color', vm.selectedSensorToCompareData.color || 'white'); - var icon = angular.element('.sensor_compare').find('md-select-label').find('.md-select-icon'); - icon.css('color', 'white'); - } + //TODO: move to utils + function findExposure(nameOrValue) { + var findProp, resultProp; + //if it's a string + if(isNaN(parseInt(nameOrValue))) { + findProp = 'name'; + resultProp = 'value'; + } else { + findProp = 'value'; + resultProp = 'name'; + } - function getCurrentRange() { - var to = moment(picker.getValuePickerTo()); - var from = moment(picker.getValuePickerFrom()); - return to.diff(from)/1000; + var option = _.find(vm.exposure, function(exposureFromList) { + return exposureFromList[findProp] === nameOrValue; + }); + if(option) { + return option[resultProp]; + } + } } +})(); - function moveChart(direction) { +(function() { + 'use strict'; - var valueTo, valueFrom; - //grab current date range - var currentRange = getCurrentRange(); + // Taken from this answer on SO: + // https://stackoverflow.com/questions/17893708/angularjs-textarea-bind-to-json-object-shows-object-object + angular.module('app.components').directive('jsonText', function() { + return { + restrict: 'A', + require: 'ngModel', + link: function(scope, element, attr, ngModel){ + function into(input) { + return JSON.parse(input); + } + function out(data) { + return JSON.stringify(data); + } + ngModel.$parsers.push(into); + ngModel.$formatters.push(out); + } + }; + }); - /*jshint camelcase: false*/ - var from_picker = angular.element('#picker_from').pickadate('picker'); - var to_picker = angular.element('#picker_to').pickadate('picker'); + angular.module('app.components') + .controller('EditKitController', EditKitController); - if(direction === 'left') { - //set both from and to pickers to prev range - valueTo = moment(picker.getValuePickerFrom()); - valueFrom = moment(picker.getValuePickerFrom()).subtract(currentRange, 'seconds'); + EditKitController.$inject = ['$scope', '$element', '$location', '$timeout', '$state', + 'animation','auth','device', 'tag', 'alert', 'step', '$stateParams', 'FullDevice']; + function EditKitController($scope, $element, $location, $timeout, $state, animation, + auth, device, tag, alert, step, $stateParams, FullDevice) { - picker.setValuePickers([valueFrom.toDate(), valueTo.toDate()]); + var vm = this; - } else if(direction === 'right') { - var today = timeUtils.getToday(); - var currentValueTo = picker.getValuePickerTo(); - if( timeUtils.isSameDay(today, timeUtils.getMillisFromDate(currentValueTo)) ) { - return; - } + // WAIT INTERVAL FOR USER FEEDBACK and TRANSITIONS (This will need to change) + var timewait = { + long: 5000, + normal: 2000, + short: 1000 + }; - valueFrom = moment(picker.getValuePickerTo()); - valueTo = moment(picker.getValuePickerTo()).add(currentRange, 'seconds'); + vm.step = step; - picker.setValuePickers([valueFrom.toDate(), valueTo.toDate()]); + // KEY USER ACTIONS + vm.submitFormAndKit = submitFormAndKit; + vm.backToProfile = backToProfile; + vm.backToDevice = backToDevice; + vm.submitForm = submitForm; + vm.goToStep = goToStep; + vm.nextAction = 'save'; - } - resetTimeOpts(); - } + // EXPOSURE SELECT + vm.exposure = [ + {name: 'indoor', value: 1}, + {name: 'outdoor', value: 2} + ]; - //hide everything but the functions to interact with the pickers - function initializePicker() { - var range = {}; - /*jshint camelcase: false*/ - var from_$input = angular.element('#picker_from').pickadate({ - onOpen: function(){ - vm.resetTimeOpts(); - }, - onClose: function(){ - angular.element(document.activeElement).blur(); - }, - container: 'body', - klass: { - holder: 'picker__holder picker_container' - } + // FORM INFO + vm.deviceForm = {}; + vm.device = undefined; + + $scope.clearSearchTerm = function() { + $scope.searchTerm = ''; + }; + // The md-select directive eats keydown events for some quick select + // logic. Since we have a search input here, we don't need that logic. + $element.find('input').on('keydown', function(ev) { + ev.stopPropagation(); }); - var from_picker = from_$input.pickadate('picker'); - var to_$input = angular.element('#picker_to').pickadate({ - onOpen: function(){ - vm.resetTimeOpts(); - }, - onClose: function(){ - angular.element(document.activeElement).blur(); - }, - container: 'body', - klass: { - holder: 'picker__holder picker_container' - } + $scope.$on('leafletDirectiveMarker.dragend', function(event, args){ + vm.deviceForm.location.lat = args.model.lat; + vm.deviceForm.location.lng = args.model.lng; }); - var to_picker = to_$input.pickadate('picker'); + // MAP CONFIGURATION + var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; - if( from_picker.get('value') ) { - to_picker.set('min', from_picker.get('select') ); - } - if( to_picker.get('value') ) { - from_picker.set('max', to_picker.get('select') ); - } + vm.getLocation = getLocation; + vm.markers = {}; + vm.tiles = { + url: 'https://api.mapbox.com/styles/v1/mapbox/streets-v10/tiles/{z}/{x}/{y}?access_token=' + mapBoxToken + }; + vm.defaults = { + scrollWheelZoom: false + }; - from_picker.on('close', function(event) { - setFromRange(getCalculatedFrom(from_picker.get('value'))); - }); + initialize(); - to_picker.on('close', function(event) { - setToRange(getCalculatedTo(to_picker.get('value'))); - }); + ///////////////// - from_picker.on('set', function(event) { - if(event.select) { - to_picker.set('min', getFromRange()); - } else if( 'clear' in event) { - to_picker.set('min', false); - } - }); + function initialize() { + var deviceID = $stateParams.id; - to_picker.on('set', function(event) { - if(event.select) { - from_picker.set('max', getToRange()); - } else if( 'clear' in event) { - from_picker.set('max', false); + animation.viewLoaded(); + getTags(); + + if (!deviceID || deviceID === ''){ + return; } - }); + device.getDevice(deviceID) + .then(function(deviceData) { + vm.device = new FullDevice(deviceData); + vm.userRole = auth.getCurrentUser().data.role; + vm.deviceForm = { + name: vm.device.name, + exposure: findExposureFromLabels(vm.device.systemTags), + location: { + lat: vm.device.location.latitude, + lng: vm.device.location.longitude, + zoom: 16 + }, + is_private: vm.device.isPrivate, + precise_location: vm.device.preciseLocation, + enable_forwarding: vm.device.enableForwarding, + notify_low_battery: vm.device.notifications.lowBattery, + notify_stopped_publishing: vm.device.notifications.stopPublishing, + tags: vm.device.userTags, + postprocessing: vm.device.postProcessing, + description: vm.device.description, + hardwareName: vm.device.hardware.name + }; + vm.markers = { + main: { + lat: vm.device.location.latitude, + lng: vm.device.location.longitude, + draggable: true + } + }; - //set to-picker max to today - to_picker.set('max', getLatestUpdated()); + if (vm.device.isLegacy) { + vm.deviceForm.macAddress = vm.device.macAddress; + } + }); + } - function getSevenDaysAgoFromLatestUpdate() { - var lastTime = moment(vm.device.lastReadingAt.raw); - return lastTime.subtract(7, 'days').valueOf(); + // Return tags in a comma separated list + function joinSelectedTags(){ + let tmp = [] + $scope.selectedTags.forEach(function(e){ + tmp.push(e.name) + }) + return tmp.join(', '); } - function getLatestUpdated() { - return moment(vm.device.lastReadingAt.raw).toDate(); + function getLocation() { + window.navigator.geolocation.getCurrentPosition(function(position) { + $scope.$apply(function() { + var lat = position.coords.latitude; + var lng = position.coords.longitude; + vm.deviceForm.location.lat = lat; + vm.deviceForm.location.lng = lng; + vm.markers.main.lat = lat; + vm.markers.main.lng = lng; + }); + }); } - function getCalculatedFrom(pickerTimeFrom) { - var from, - pickerTime; + function submitFormAndKit(){ + submitForm(backToProfile, timewait.normal); + } - pickerTime = moment(pickerTimeFrom, 'D MMMM, YYYY'); - from = pickerTime.startOf('day'); + function submitForm(next, delayTransition) { + var data = { + name: vm.deviceForm.name, + description: vm.deviceForm.description, + postprocessing_attributes: vm.deviceForm.postprocessing, + exposure: findExposure(vm.deviceForm.exposure), + latitude: vm.deviceForm.location.lat, + longitude: vm.deviceForm.location.lng, + is_private: vm.deviceForm.is_private, + enable_forwarding: vm.deviceForm.enable_forwarding, + precise_location: vm.deviceForm.precise_location, + notify_low_battery: vm.deviceForm.notify_low_battery, + notify_stopped_publishing: vm.deviceForm.notify_stopped_publishing, + mac_address: "", + /*jshint camelcase: false */ + user_tags: joinSelectedTags(), + }; - return from; - } + vm.errors={}; - function getCalculatedTo(pickerTimeTo) { - var to, - pickerTime; + if(!vm.device.isSCK) { + data.hardware_name_override = vm.deviceForm.hardwareName; + } - pickerTime = moment(pickerTimeTo, 'D MMMM, YYYY'); + // Workaround for the mac_address bypass + // If mac_address is "", we get an error on the request -> we use it for the newKit + // If mac_address is null, no problem -> we use it for the + if ($stateParams.step === "2") { + data.mac_address = vm.deviceForm.macAddress ? vm.deviceForm.macAddress : ""; + } else { + data.mac_address = vm.deviceForm.macAddress ? vm.deviceForm.macAddress : null; + } - to = pickerTime.endOf('day'); - if (moment().diff(to) < 0) { - var now = moment(); - to = pickerTime.set({ - 'hour' : now.get('hour'), - 'minute' : now.get('minute'), - 'second' : now.get('second') + device.updateDevice(vm.device.id, data) + .then( + function() { + + if (next){ + alert.success('Your kit was updated!'); + } + + device.updateContext().then(function(){ + if (next){ + $timeout(next, delayTransition); + } + }); + }) + .catch(function(err) { + if(err.data.errors) { + vm.errors = err.data.errors; + var message = Object.keys(vm.errors).map(function (key, _) { + return [key, vm.errors[key][0]].join(' '); }).join(''); + alert.error('Oups! Check the input. Something went wrong!'); + throw new Error('[Client:error] ' + message); + } + $timeout(function(){ }, timewait.long); }); - } + } - return to; + function findExposureFromLabels(labels){ + var label = vm.exposure.filter(function(n) { + return labels.indexOf(n.name) !== -1; + })[0]; + if(label) { + return findExposure(label.name); + } else { + return findExposure(vm.exposure[0].name); + } } - function updateChart() { - var sensors = [mainSensorID, compareSensorID]; - sensors = sensors.filter(function(sensor) { - return sensor; - }); - changeChart(sensors, { - from: range.from, - to: range.to + function findExposure(nameOrValue) { + var findProp, resultProp; + + //if it's a string + if(isNaN(parseInt(nameOrValue))) { + findProp = 'name'; + resultProp = 'value'; + } else { + findProp = 'value'; + resultProp = 'name'; + } + + var option = _.find(vm.exposure, function(exposureFromList) { + return exposureFromList[findProp] === nameOrValue; }); + if(option) { + return option[resultProp]; + } else { + return vm.exposure[0][resultProp]; + } } - function setFromRange(from) { - range.from = from; - from_picker.set('select', getFromRange()); - updateChart(); + function getTags() { + tag.getTags() + .then(function(tagsData) { + vm.tags = tagsData; + }); } - function setToRange(to) { - range.to = to; - to_picker.set('select', getToRange()); - updateChart(); + function backToProfile(){ + $state.transitionTo('layout.myProfile.kits', $stateParams, + { reload: false, + inherit: false, + notify: true + }); } - function getFromRange() { - return moment(range.from).toDate(); + function backToDevice(){ + $state.transitionTo('layout.home.kit', $stateParams, + { reload: false, + inherit: false, + notify: true + }); } - function getToRange() { - return moment(range.to).toDate(); + function goToStep(step) { + vm.step = step; + $state.transitionTo('layout.kitEdit', { id:$stateParams.id, step: step} , + { + reload: false, + inherit: false, + notify: false + }); } + } +})(); - function setRange(from, to) { - range.from = from; - range.to = to; - from_picker.set('select', getFromRange()); - to_picker.set('select', getToRange()); - updateChart(); - } +(function() { + 'use strict'; - if(vm.device){ - if(vm.device.systemTags.includes('new')){ - var lastUpdate = getLatestUpdated(); - setRange(timeUtils.getHourBefore(lastUpdate), lastUpdate); - } else if (timeUtils.isWithin(7, 'days', vm.device.lastReadingAt.raw) || !vm.device.lastReadingAt.raw) { - //set from-picker to seven days ago and set to-picker to today - setRange(timeUtils.getSevenDaysAgo(), timeUtils.getToday()); - } else { - // set from-picker to and set to-picker to today - setRange(getSevenDaysAgoFromLatestUpdate(), getLatestUpdated()); - } - } + angular.module('app.components') + .factory('User', ['COUNTRY_CODES', function(COUNTRY_CODES) { - // api to interact with the picker from outside - return { - getValuePickerFrom: function() { - return getFromRange(); - }, - setValuePickerFrom: function(newValue) { - setFromRange(newValue); - }, - getValuePickerTo: function() { - return getToRange(); - }, - setValuePickerTo: function(newValue) { - setToRange(newValue); - }, - setValuePickers: function(newValues) { - var from = newValues[0]; - var to = newValues[1]; - setRange(from, to); - } - }; - } + /** + * User constructor + * @param {Object} userData - User data sent from API + * @property {number} id - User ID + * @property {string} username - Username + * @property {string} profile_picture - Avatar URL of user + * @property {Array} devices - Kits that belongs to this user + * @property {string} url - URL + * @property {string} city - User city + * @property {string} country - User country + */ - function geolocate() { - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition(function(position){ - if(!position){ - alert.error('Please, allow smartcitizen to geolocate your' + - 'position so we can find a kit near you.'); - return; - } + function User(userData) { + this.id = userData.id; + this.username = userData.username; + this.profile_picture = userData.profile_picture; + this.devices = userData.devices; + this.url = userData.url; + this.city = userData.location.city; + /*jshint camelcase: false */ + this.country = COUNTRY_CODES[userData.location.country_code]; + } + return User; + }]); - geolocation.grantHTML5Geolocation(); +})(); - var location = { - lat:position.coords.latitude, - lng:position.coords.longitude - }; - device.getDevices(location) - .then(function(data){ - data = data.plain(); +(function() { + 'use strict'; - _(data) - .chain() - .map(function(device) { - return new HasSensorDevice(device); - }) - .filter(function(device) { - return !!device.longitude && !!device.latitude; - }) - .find(function(device) { - return _.includes(device.labels, 'online'); - }) - .tap(function(closestDevice) { - if(focused){ - if(closestDevice) { - $state.go('layout.home.kit', {id: closestDevice.id}); - } else { - $state.go('layout.home.kit', {id: data[0].id}); - } - } - }) - .value(); - }); - }); + angular.module('app.components') + .factory('NonAuthUser', ['User', function(User) { + + function NonAuthUser(userData) { + User.call(this, userData); } - } + NonAuthUser.prototype = Object.create(User.prototype); + NonAuthUser.prototype.constructor = User; - function downloadData(device){ - $mdDialog.show({ - hasBackdrop: true, - controller: 'DownloadModalController', - controllerAs: 'vm', - templateUrl: 'app/components/download/downloadModal.html', - clickOutsideToClose: true, - locals: {thisDevice: device} - }).then(function(){ - var alert = $mdDialog.alert() - .title('SUCCESS') - .textContent('We are processing your data. Soon you will be notified in your inbox') - .ariaLabel('') - .ok('OK!') - .theme('primary') - .clickOutsideToClose(true); + return NonAuthUser; + }]); +})(); - $mdDialog.show(alert); - }).catch(function(err){ - if (!err){ - return; - } - var errorAlert = $mdDialog.alert() - .title('ERROR') - .textContent('Uh-oh, something went wrong') - .ariaLabel('') - .ok('D\'oh') - .theme('primary') - .clickOutsideToClose(false); +(function() { + 'use strict'; - $mdDialog.show(errorAlert); - }); - } + angular.module('app.components') + .factory('AuthUser', ['User', function(User) { - function getMainSensors(deviceData) { - if(!deviceData) { - return undefined; - } - return deviceData.getSensors({type: 'main'}); - } - function getCompareSensors(deviceData) { - if(!vm.device) { - return undefined; - } - deviceData.getSensors({type: 'compare'}); - } - function getOwnerDevices(deviceData, sampling) { - if(!deviceData) { - return undefined; - } - var deviceIDs = deviceData.owner.devices.slice(sampling); - // var ownerID = deviceData.owner.id; - // TODO: Refactor This is in the user endpoint, no need to query devices - return $q.all( - deviceIDs.map(function(id) { - return device.getDevice(id) - .then(function(data) { - return new PreviewDevice(data); - }); - }) - ); - } + /** + * AuthUser constructor. Used for authenticated users + * @extends User + * @param {Object} userData - Contains user data sent from API + * @property {string} email - User email + * @property {string} role - User role. Ex: admin + * @property {string} key - Personal API Key + */ - function setFromLast(what){ - /* This will not show the last 60 minutes or 24 hours, - instead it will show the last hour or day*/ - var to, from; - if (what === '60 minutes') { - to = moment(vm.device.lastReadingAt.raw); - from = moment(vm.device.lastReadingAt.raw).subtract(60, 'minutes'); - } else { - to = moment(vm.device.lastReadingAt.raw).endOf(what); - from = moment(vm.device.lastReadingAt.raw).startOf(what); - } - // Check if we are in the future - if (moment().diff(to) < 0){ - to = moment(vm.device.lastReadingAt.raw); - } - picker.setValuePickers([from.toDate(), to.toDate()]); - } + function AuthUser(userData) { + User.call(this, userData); - function timeOptSelected(){ - vm.allowUpdateChart = false; - if (vm.dropDownSelection){ - setFromLast(vm.dropDownSelection); + this.email = userData.email; + this.role = userData.role; + /*jshint camelcase: false */ + this.key = userData.legacy_api_key; } - } - function resetTimeOpts(){ - vm.allowUpdateChart = false; - vm.dropDownSelection = undefined; - } + AuthUser.prototype = Object.create(User.prototype); + AuthUser.prototype.constructor = User; - function showStore() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'StoreModalController', - templateUrl: 'app/components/store/storeModal.html', - clickOutsideToClose: true - }); - } - } + return AuthUser; + }]); })(); (function() { 'use strict'; angular.module('app.components') - .controller('NewKitController', NewKitController); + .factory('Sensor', ['sensorUtils', 'timeUtils', function(sensorUtils, timeUtils) { - NewKitController.$inject = ['$scope', '$state', 'animation', 'device', 'tag', 'alert', 'auth', '$stateParams', '$timeout']; - function NewKitController($scope, $state, animation, device, tag, alert, auth, $stateParams, $timeout) { - var vm = this; + /** + * Sensor constructor + * @param {Object} sensorData - Contains the data of a sensor sent from the API + * @property {string} name - Name of sensor + * @property {number} id - ID of sensor + * @property {string} unit - Unit of sensor. Ex: % + * @property {string} value - Last value sent. Ex: 95 + * @property {string} prevValue - Previous value before last value + * @property {string} lastReadingAt - last_reading_at for the sensor reading + * @property {string} icon - Icon URL for sensor + * @property {string} arrow - Icon URL for sensor trend(up, down or equal) + * @property {string} color - Color that belongs to sensor + * @property {object} measurement - Measurement + * @property {string} fullDescription - Full Description for popup + * @property {string} previewDescription - Short Description for dashboard. Max 140 chars + * @property {string} tags - Contains sensor tags for filtering the view + */ + function Sensor(sensorData) { - vm.step = 1; - vm.submitStepOne = submitStepOne; - vm.backToProfile = backToProfile; + this.id = sensorData.id; + this.name = sensorData.name; + this.unit = sensorData.unit; + this.value = sensorUtils.getSensorValue(sensorData); + this.prevValue = sensorUtils.getSensorPrevValue(sensorData); + this.lastReadingAt = timeUtils.parseDate(sensorData.last_reading_at); + this.icon = sensorUtils.getSensorIcon(this.name); + this.arrow = sensorUtils.getSensorArrow(this.value, this.prevValue); + this.color = sensorUtils.getSensorColor(this.name); + this.measurement = sensorData.measurement; - // FORM INFO - vm.deviceForm = { - name: undefined, - exposure: undefined, - location: { - lat: undefined, - lng: undefined, - zoom: 16 - }, - is_private: false, - legacyVersion: '1.1', - tags: [] - }; - - // EXPOSURE SELECT - vm.exposure = [ - {name: 'indoor', value: 1}, - {name: 'outdoor', value: 2} - ]; - - // VERSION SELECT - vm.version = [ - {name: 'Smart Citizen Kit 1.0', value: '1.0'}, - {name: 'Smart Citizen Kit 1.1', value: '1.1'} - ]; - - $scope.$on('leafletDirectiveMarker.dragend', function(event, args){ - vm.deviceForm.location.lat = args.model.lat; - vm.deviceForm.location.lng = args.model.lng; - }); - - // TAGS SELECT - vm.tags = []; - $scope.$watch('vm.tag', function(newVal, oldVal) { - if(!newVal) { - return; + // Some sensors don't have measurements because they are ancestors + if (sensorData.measurement) { + var description = sensorData.measurement.description; + this.fullDescription = description; + this.previewDescription = description.length > 140 ? description.slice( + 0, 140).concat(' ... ') : description; + this.is_ancestor = false; + } else { + this.is_ancestor = true; } - // remove selected tag from select element - vm.tag = undefined; - var alreadyPushed = _.some(vm.deviceForm.tags, function(tag) { - return tag.id === newVal; - }); - if(alreadyPushed) { - return; - } + // Get sensor tags + this.tags = sensorData.tags; + } - var tag = _.find(vm.tags, function(tag) { - return tag.id === newVal; - }); - vm.deviceForm.tags.push(tag); - }); - vm.removeTag = removeTag; + return Sensor; + }]); +})(); +(function() { + 'use strict'; - // MAP CONFIGURATION - var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; + angular.module('app.components') + .factory('SearchResultLocation', ['SearchResult', function(SearchResult) { - vm.getLocation = getLocation; - vm.markers = { - main: { - lat: undefined, - lng: undefined, - draggable: true - } - }; - vm.tiles = { - url: 'https://api.mapbox.com/styles/v1/mapbox/streets-v10/tiles/{z}/{x}/{y}?access_token=' + mapBoxToken - }; - vm.defaults = { - scrollWheelZoom: false - }; + /** + * Search Result Location constructor + * @extends SearchResult + * @param {Object} object - Object that contains the search result data from API + * @property {number} lat - Latitude + * @property {number} lng - Longitude + */ + function SearchResultLocation(object) { + SearchResult.call(this, object); - vm.macAddress = undefined; + this.lat = object.latitude; + this.lng = object.longitude; + this.layer = object.layer; + } + return SearchResultLocation; + }]); - initialize(); +})(); - ////////////// +(function() { + 'use strict'; - function initialize() { - animation.viewLoaded(); - getTags(); - vm.userRole = auth.getCurrentUser().data.role; - } + angular.module('app.components') + .factory('SearchResult', ['searchUtils', function(searchUtils) { - function getLocation() { - window.navigator.geolocation.getCurrentPosition(function(position) { - $scope.$apply(function() { - var lat = position.coords.latitude; - var lng = position.coords.longitude; - vm.deviceForm.location.lat = lat; - vm.deviceForm.location.lng = lng; - vm.markers.main.lat = lat; - vm.markers.main.lng = lng; - }); - }); + /** + * Search Result constructor + * @param {Object} object - Object that belongs to a search result from API + * @property {string} type - Type of search result. Ex: Country, City, User, Device + * @property {number} id - ID of search result, only for user & device + * @property {string} name - Name of search result, only for user & device + * @property {string} location - Location of search result. Ex: 'Paris, France' + * @property {string} icon - URL for the icon that belongs to this search result + * @property {string} iconType - Type of icon. Can be either img or div + */ + + function SearchResult(object) { + this.type = object.type; + this.id = object.id; + this.name = searchUtils.parseName(object); + this.location = searchUtils.parseLocation(object); + this.icon = searchUtils.parseIcon(object, this.type); + this.iconType = searchUtils.parseIconType(this.type); } + return SearchResult; + }]); +})(); - function removeTag(tagID) { - vm.deviceForm.tags = _.filter(vm.deviceForm.tags, function(tag) { - return tag.id !== tagID; - }); - } +(function() { + 'use strict'; - function submitStepOne() { - var data = { - name: vm.deviceForm.name, - description: vm.deviceForm.description, - exposure: findExposure(vm.deviceForm.exposure), - latitude: vm.deviceForm.location.lat, - longitude: vm.deviceForm.location.lng, - is_private: vm.deviceForm.is_private, - hardware_version_override: vm.deviceForm.legacyVersion, - /*jshint camelcase: false */ - user_tags: _.map(vm.deviceForm.tags, 'name').join(',') - }; + angular.module('app.components') + .factory('Marker', ['deviceUtils', 'markerUtils', 'timeUtils', '$state', function(deviceUtils, markerUtils, timeUtils, $state) { + /** + * Marker constructor + * @constructor + * @param {Object} deviceData - Object with data about marker from API + * @property {number} lat - Latitude + * @property {number} lng - Longitude + * @property {string} message - Message inside marker popup + * @property {Object} icon - Object with classname, size and type of marker icon + * @property {string} layer - Map layer that icons belongs to + * @property {boolean} focus - Whether marker popup is opened + * @property {Object} myData - Marker id and labels + */ + function Marker(deviceData) { + let linkStart = '', linkEnd = ''; + const id = deviceData.id; + if ($state.$current.name === 'embbed') { + linkStart = ''; + linkEnd = ''; + } + this.lat = deviceUtils.parseCoordinates(deviceData).lat; + this.lng = deviceUtils.parseCoordinates(deviceData).lng; + // TODO: Bug, pop-up lastreading at doesn't get updated by publication + this.message = ''; - device.createDevice(data) - .then( - function(response) { - device.updateContext().then(function(){ - auth.setCurrentUser('appLoad').then(function(){ - $timeout($state.go('layout.kitEdit', {id:response.id, step:2}), 2000); - }); - }); - }, - function(err) { - vm.errors = err.data.errors; - alert.error('There has been an error during kit set up'); - }); + this.icon = markerUtils.getIcon(deviceData); + this.layer = 'devices'; + this.focus = false; + this.myData = { + id: id, + labels: deviceUtils.parseSystemTags(deviceData), + tags: deviceUtils.parseUserTags(deviceData) + }; } + return Marker; - function getTags() { - tag.getTags() - .then(function(tagsData) { - vm.tags = tagsData; - }); - } + function createTagsTemplate(tagsArr, tagType, clickable) { + if(typeof(clickable) === 'undefined'){ + clickable = false; + } + var clickablTag = ''; + if(clickable){ + clickablTag = 'clickable'; + } - function toProfile(){ - $state.transitionTo('layout.myProfile.kits', $stateParams, - { reload: false, - inherit: false, - notify: true - }); - } + if(!tagType){ + tagType = 'tag'; + } - function backToProfile(){ - // TODO: Refactor Check - toProfile(); + return _.reduce(tagsArr, function(acc, label) { + var element =''; + if(tagType === 'tag'){ + element = ''; + }else{ + element = ''+label+''; + } + return acc.concat(element); + }, ''); } - //TODO: move to utils - function findExposure(nameOrValue) { - var findProp, resultProp; - //if it's a string - if(isNaN(parseInt(nameOrValue))) { - findProp = 'name'; - resultProp = 'value'; - } else { - findProp = 'value'; - resultProp = 'name'; - } + }]); +})(); - var option = _.find(vm.exposure, function(exposureFromList) { - return exposureFromList[findProp] === nameOrValue; - }); - if(option) { - return option[resultProp]; - } +(function () { + 'use strict'; + + angular.module('app.components') + .factory('PreviewDevice', ['Device', function (Device) { + + /** + * Preview Device constructor. + * Used for devices stacked in a list, like in User Profile or Device states + * @extends Device + * @constructor + * @param {Object} object - Object with all the data about the device from the API + */ + function PreviewDevice(object) { + Device.call(this, object); + + this.dropdownOptions = []; + this.dropdownOptions.push({ text: 'EDIT', value: '1', href: 'kits/' + this.id + '/edit', icon: 'fa fa-edit' }); + this.dropdownOptions.push({ text: 'SD CARD UPLOAD', value: '2', href: 'kits/' + this.id + '/upload', icon: 'fa fa-sd-card' }); } - } + PreviewDevice.prototype = Object.create(Device.prototype); + PreviewDevice.prototype.constructor = Device; + return PreviewDevice; + }]); })(); (function() { 'use strict'; - // Taken from this answer on SO: - // https://stackoverflow.com/questions/17893708/angularjs-textarea-bind-to-json-object-shows-object-object - angular.module('app.components').directive('jsonText', function() { - return { - restrict: 'A', - require: 'ngModel', - link: function(scope, element, attr, ngModel){ - function into(input) { - return JSON.parse(input); - } - function out(data) { - return JSON.stringify(data); - } - ngModel.$parsers.push(into); - ngModel.$formatters.push(out); - } - }; - }); - angular.module('app.components') - .controller('EditKitController', EditKitController); + .factory('HasSensorDevice', ['Device', function(Device) { - EditKitController.$inject = ['$scope', '$element', '$location', '$timeout', '$state', - 'animation','auth','device', 'tag', 'alert', 'step', '$stateParams', 'FullDevice']; - function EditKitController($scope, $element, $location, $timeout, $state, animation, - auth, device, tag, alert, step, $stateParams, FullDevice) { + function HasSensorDevice(object) { + Device.call(this, object); - var vm = this; + this.sensors = object.data.sensors; + this.longitude = object.data.location.longitude; + this.latitude = object.data.location.latitude; + } - // WAIT INTERVAL FOR USER FEEDBACK and TRANSITIONS (This will need to change) - var timewait = { - long: 5000, - normal: 2000, - short: 1000 + HasSensorDevice.prototype = Object.create(Device.prototype); + HasSensorDevice.prototype.constructor = Device; + + HasSensorDevice.prototype.sensorsHasData = function() { + var parsedSensors = this.sensors.map(function(sensor) { + return sensor.value; + }); + + return _.some(parsedSensors, function(sensorValue) { + return !!sensorValue; + }); }; - vm.step = step; + return HasSensorDevice; + }]); +})(); - // KEY USER ACTIONS - vm.submitFormAndKit = submitFormAndKit; - vm.backToProfile = backToProfile; - vm.backToDevice = backToDevice; - vm.submitForm = submitForm; - vm.goToStep = goToStep; - vm.nextAction = 'save'; +(function() { + 'use strict'; - // EXPOSURE SELECT - vm.exposure = [ - {name: 'indoor', value: 1}, - {name: 'outdoor', value: 2} - ]; + angular.module('app.components') + .factory('FullDevice', ['Device', 'Sensor', 'deviceUtils', function(Device, Sensor, deviceUtils) { - // FORM INFO - vm.deviceForm = {}; - vm.device = undefined; + /** + * Full Device constructor. + * @constructor + * @extends Device + * @param {Object} object - Object with all the data about the device from the API + * @property {Object} owner - Device owner data + * @property {Array} data - Device sensor's data + * @property {Array} sensors - Device sensors data + * @property {Array} postProcessing - Device postprocessing + */ + function FullDevice(object) { + Device.call(this, object); - $scope.clearSearchTerm = function() { - $scope.searchTerm = ''; + this.owner = deviceUtils.parseOwner(object); + this.postProcessing = object.postprocessing; + this.data = object.data; + this.sensors = object.data.sensors; + } + + FullDevice.prototype = Object.create(Device.prototype); + FullDevice.prototype.constructor = FullDevice; + + FullDevice.prototype.getSensors = function(options) { + var sensors = _(this.data.sensors) + .chain() + .map(function(sensor) { + return new Sensor(sensor); + }).sort(function(a, b) { + /* This is a temporary hack to set always PV panel at the end*/ + if (a.id === 18){ return -1;} + if (b.id === 18){ return 1;} + /* This is a temporary hack to set always the Battery at the end*/ + if (a.id === 17){ return -1;} + if (b.id === 17){ return 1;} + /* This is a temporary hack to set always the Battery at the end*/ + if (a.id === 10){ return -1;} + if (b.id === 10){ return 1;} + /* After the hacks, sort the sensors by id */ + return b.id - a.id; + }) + .tap(function(sensors) { + if(options.type === 'compare') { + sensors.unshift({ + name: 'NONE', + color: 'white', + id: -1 + }); + } + }) + .value(); + return sensors; }; - // The md-select directive eats keydown events for some quick select - // logic. Since we have a search input here, we don't need that logic. - $element.find('input').on('keydown', function(ev) { - ev.stopPropagation(); - }); - $scope.$on('leafletDirectiveMarker.dragend', function(event, args){ - vm.deviceForm.location.lat = args.model.lat; - vm.deviceForm.location.lng = args.model.lng; - }); + return FullDevice; + }]); +})(); - // MAP CONFIGURATION - var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; +(function() { + 'use strict'; - vm.getLocation = getLocation; - vm.markers = {}; - vm.tiles = { - url: 'https://api.mapbox.com/styles/v1/mapbox/streets-v10/tiles/{z}/{x}/{y}?access_token=' + mapBoxToken - }; - vm.defaults = { - scrollWheelZoom: false - }; + angular.module('app.components') + .factory('Device', ['deviceUtils', 'timeUtils', function(deviceUtils, timeUtils) { - initialize(); + /** + * Device constructor. + * @constructor + * @param {Object} object - Object with all the data about the device from the API + * @property {number} id - ID of the device + * @property {string} name - Name of the device + * @property {string} state - State of the device. Ex: Never published + * @property {string} description - Device description + * @property {Array} systemTags - System tags + * @property {Array} userTags - User tags. Ex: '' + * @property {bool} isPrivate - True if private device + * @property {Array} notifications - Notifications for low battery and stopped publishing + * @property {Object} lastReadingAt - last_reading_at: raw, ago, and parsed + * @property {Object} createdAt - created_at: raw, ago, and parsed + * @property {Object} updatedAt - updated_at: raw, ago, and parsed + * @property {Object} location - Location of device. Object with lat, long, elevation, city, country, country_code + * @property {string} locationString - Location of device. Ex: Madrid, Spain; Germany; Paris, France + * @property {Object} hardware - Device hardware field. Contains type, version, info, slug and name + * @property {string} hardwareName - Device hardware name + * @property {bool} isLegacy - True if legacy device + * @property {bool} isSCK - True if SC device + * @property {string} avatar - URL that contains the user avatar + */ + function Device(object) { + // Basic information + this.id = object.id; + this.name = object.name; + this.state = deviceUtils.parseState(object); + this.description = object.description; + this.token = object.device_token; + this.macAddress = object.mac_address; - ///////////////// + // Tags and dates + this.systemTags = deviceUtils.parseSystemTags(object); + this.userTags = deviceUtils.parseUserTags(object); + this.isPrivate = deviceUtils.isPrivate(object); + this.preciseLocation = deviceUtils.preciseLocation(object); + this.enableForwarding = deviceUtils.enableForwarding(object); + this.notifications = deviceUtils.parseNotifications(object); + this.lastReadingAt = timeUtils.parseDate(object.last_reading_at); + this.createdAt = timeUtils.parseDate(object.created_at); + this.updatedAt = timeUtils.parseDate(object.updated_at); - function initialize() { - var deviceID = $stateParams.id; + // Location + this.location = object.location; + this.locationString = deviceUtils.parseLocation(object); - animation.viewLoaded(); - getTags(); + // Hardware + this.hardware = deviceUtils.parseHardware(object); + this.hardwareName = deviceUtils.parseHardwareName(this); + this.isLegacy = deviceUtils.isLegacyVersion(this); + this.isSCK = deviceUtils.isSCKHardware(this); + // this.class = deviceUtils.classify(object); // TODO - Do we need this? - if (!deviceID || deviceID === ''){ - return; - } - device.getDevice(deviceID) - .then(function(deviceData) { - vm.device = new FullDevice(deviceData); - vm.userRole = auth.getCurrentUser().data.role; - vm.deviceForm = { - name: vm.device.name, - exposure: findExposureFromLabels(vm.device.systemTags), - location: { - lat: vm.device.location.latitude, - lng: vm.device.location.longitude, - zoom: 16 - }, - is_private: vm.device.isPrivate, - precise_location: vm.device.preciseLocation, - enable_forwarding: vm.device.enableForwarding, - notify_low_battery: vm.device.notifications.lowBattery, - notify_stopped_publishing: vm.device.notifications.stopPublishing, - tags: vm.device.userTags, - postprocessing: vm.device.postProcessing, - description: vm.device.description, - hardwareName: vm.device.hardware.name - }; - vm.markers = { - main: { - lat: vm.device.location.latitude, - lng: vm.device.location.longitude, - draggable: true - } - }; - - if (vm.device.isLegacy) { - vm.deviceForm.macAddress = vm.device.macAddress; - } - }); + this.avatar = deviceUtils.parseAvatar(); + /*jshint camelcase: false */ } - // Return tags in a comma separated list - function joinSelectedTags(){ - let tmp = [] - $scope.selectedTags.forEach(function(e){ - tmp.push(e.name) - }) - return tmp.join(', '); - } + return Device; + }]); +})(); - function getLocation() { - window.navigator.geolocation.getCurrentPosition(function(position) { - $scope.$apply(function() { - var lat = position.coords.latitude; - var lng = position.coords.longitude; - vm.deviceForm.location.lat = lat; - vm.deviceForm.location.lng = lng; - vm.markers.main.lat = lat; - vm.markers.main.lng = lng; - }); - }); - } +(function() { + 'use strict'; - function submitFormAndKit(){ - submitForm(backToProfile, timewait.normal); - } + angular.module('app.components') + .directive('noDataBackdrop', noDataBackdrop); - function submitForm(next, delayTransition) { - var data = { - name: vm.deviceForm.name, - description: vm.deviceForm.description, - postprocessing_attributes: vm.deviceForm.postprocessing, - exposure: findExposure(vm.deviceForm.exposure), - latitude: vm.deviceForm.location.lat, - longitude: vm.deviceForm.location.lng, - is_private: vm.deviceForm.is_private, - enable_forwarding: vm.deviceForm.enable_forwarding, - precise_location: vm.deviceForm.precise_location, - notify_low_battery: vm.deviceForm.notify_low_battery, - notify_stopped_publishing: vm.deviceForm.notify_stopped_publishing, - mac_address: "", - /*jshint camelcase: false */ - user_tags: joinSelectedTags(), - }; + /** + * Backdrop for chart section when kit has no data + * + */ + noDataBackdrop.$inject = []; - vm.errors={}; + function noDataBackdrop() { + return { + restrict: 'A', + scope: {}, + templateUrl: 'app/core/animation/backdrop/noDataBackdrop.html', + controller: function($scope, $timeout) { + var vm = this; - if(!vm.device.isSCK) { - data.hardware_name_override = vm.deviceForm.hardwareName; - } + vm.deviceWithoutData = false; + vm.scrollToComments = scrollToComments; - // Workaround for the mac_address bypass - // If mac_address is "", we get an error on the request -> we use it for the newKit - // If mac_address is null, no problem -> we use it for the - if ($stateParams.step === "2") { - data.mac_address = vm.deviceForm.macAddress ? vm.deviceForm.macAddress : ""; - } else { - data.mac_address = vm.deviceForm.macAddress ? vm.deviceForm.macAddress : null; - } + $scope.$on('deviceWithoutData', function(ev, data) { - device.updateDevice(vm.device.id, data) - .then( - function() { + $timeout(function() { + vm.device = data.device; + vm.deviceWithoutData = true; - if (next){ - alert.success('Your kit was updated!'); - } + if (data.belongsToUser) { + vm.user = 'owner'; + } else { + vm.user = 'visitor'; + } + }, 0); - device.updateContext().then(function(){ - if (next){ - $timeout(next, delayTransition); - } - }); - }) - .catch(function(err) { - if(err.data.errors) { - vm.errors = err.data.errors; - var message = Object.keys(vm.errors).map(function (key, _) { - return [key, vm.errors[key][0]].join(' '); }).join(''); - alert.error('Oups! Check the input. Something went wrong!'); - throw new Error('[Client:error] ' + message); - } - $timeout(function(){ }, timewait.long); - }); - } + }); - function findExposureFromLabels(labels){ - var label = vm.exposure.filter(function(n) { - return labels.indexOf(n.name) !== -1; - })[0]; - if(label) { - return findExposure(label.name); - } else { - return findExposure(vm.exposure[0].name); + function scrollToComments(){ + location.hash = ''; + location.hash = '#disqus_thread'; } - } + }, + controllerAs: 'vm' + }; + } +})(); - function findExposure(nameOrValue) { - var findProp, resultProp; +(function() { + 'use strict'; - //if it's a string - if(isNaN(parseInt(nameOrValue))) { - findProp = 'name'; - resultProp = 'value'; - } else { - findProp = 'value'; - resultProp = 'name'; - } + angular.module('app.components') + .directive('loadingBackdrop', loadingBackdrop); - var option = _.find(vm.exposure, function(exposureFromList) { - return exposureFromList[findProp] === nameOrValue; - }); - if(option) { - return option[resultProp]; - } else { - return vm.exposure[0][resultProp]; - } - } + /** + * Backdrop for app initialization and between states + * + */ + loadingBackdrop.$inject = []; + function loadingBackdrop() { + return { + templateUrl: 'app/core/animation/backdrop/loadingBackdrop.html', + controller: function($scope) { + var vm = this; + vm.isViewLoading = true; + vm.mapStateLoading = false; - function getTags() { - tag.getTags() - .then(function(tagsData) { - vm.tags = tagsData; + // listen for app loading event + $scope.$on('viewLoading', function() { + vm.isViewLoading = true; }); - } - function backToProfile(){ - $state.transitionTo('layout.myProfile.kits', $stateParams, - { reload: false, - inherit: false, - notify: true - }); - } + $scope.$on('viewLoaded', function() { + vm.isViewLoading = false; + }); - function backToDevice(){ - $state.transitionTo('layout.home.kit', $stateParams, - { reload: false, - inherit: false, - notify: true - }); - } + // listen for map state loading event + $scope.$on('mapStateLoading', function() { + if(vm.isViewLoading) { + return; + } + vm.mapStateLoading = true; + }); - function goToStep(step) { - vm.step = step; - $state.transitionTo('layout.kitEdit', { id:$stateParams.id, step: step} , - { - reload: false, - inherit: false, - notify: false - }); - } + $scope.$on('mapStateLoaded', function() { + vm.mapStateLoading = false; + }); + }, + controllerAs: 'vm' + }; } })(); @@ -1834,787 +1834,769 @@ 'use strict'; angular.module('app.components') - .factory('userUtils', userUtils); + .controller('UserProfileController', UserProfileController); - function userUtils() { - var service = { - isAdmin: isAdmin, - isAuthUser: isAuthUser - }; - return service; + UserProfileController.$inject = ['$scope', '$stateParams', '$location', + 'user', 'auth', 'userUtils', '$timeout', 'animation', + 'NonAuthUser', '$q', 'PreviewDevice']; + function UserProfileController($scope, $stateParams, $location, + user, auth, userUtils, $timeout, animation, + NonAuthUser, $q, PreviewDevice) { - /////////// + var vm = this; + var userID = parseInt($stateParams.id); - function isAdmin(userData) { - return userData.role === 'admin'; - } - function isAuthUser(userID, authUserData) { - return userID === authUserData.id; - } - } -})(); - -(function() { - 'use strict'; - - angular.module('app.components') - .factory('timeUtils', timeUtils); - - function timeUtils() { - var service = { - getSecondsFromDate: getSecondsFromDate, - getMillisFromDate: getMillisFromDate, - getCurrentRange: getCurrentRange, - getToday: getToday, - getHourBefore: getHourBefore, - getSevenDaysAgo: getSevenDaysAgo, - getDateIn: getDateIn, - convertTime: convertTime, - formatDate: formatDate, - isSameDay: isSameDay, - isWithin15min: isWithin15min, - isWithin1Month: isWithin1Month, - isWithin: isWithin, - isDiffMoreThan15min: isDiffMoreThan15min, - parseDate: parseDate - }; - return service; - - //////////// - - function getDateIn(timeMS, format) { - if(!format) { - return timeMS; - } + vm.status = undefined; + vm.user = {}; + vm.devices = []; + vm.filteredDevices = []; + vm.filterDevices = filterDevices; - var result; - if(format === 'ms') { - result = timeMS; - } else if(format === 's') { - result = timeMS / 1000; - } else if(format === 'm') { - result = timeMS / 1000 / 60; - } else if(format === 'h') { - result = timeMS / 1000 / 60 / 60; - } else if(format === 'd') { - result = timeMS / 1000 / 60 / 60 / 24; - } - return result; - } + $scope.$on('loggedIn', function() { + var authUser = auth.getCurrentUser().data; + if( userUtils.isAuthUser(userID, authUser) ) { + $location.path('/profile'); + } + }); - function convertTime(time) { - return moment(time).toISOString(); - } + initialize(); - function formatDate(time) { - return moment(time).format('YYYY-MM-DDTHH:mm:ss'); - } + ////////////////// - function getSecondsFromDate(date) { - return (new Date(date)).getTime(); - } + function initialize() { - function getMillisFromDate(date) { - return (new Date(date)).getTime(); - } + user.getUser(userID) + .then(function(user) { + vm.user = new NonAuthUser(user); - function getCurrentRange(fromDate, toDate) { - return moment(toDate).diff(moment(fromDate), 'days'); - } + if(!vm.user.devices.length) { + return []; + } - function getToday() { - return (new Date()).getTime(); - } + $q.all(vm.devices = vm.user.devices.map(function(data){ + return new PreviewDevice(data); + })) - function getSevenDaysAgo() { - return getSecondsFromDate( getToday() - (7 * 24 * 60 * 60 * 1000) ); - } + }).then(function(error) { + if(error && error.status === 404) { + $location.url('/404'); + } + }); - function getHourBefore(date) { - var now = moment(date); - return now.subtract(1, 'hour').valueOf(); - } + $timeout(function() { + setSidebarMinHeight(); + animation.viewLoaded(); + }, 500); + } - function isSameDay(day1, day2) { - day1 = moment(day1); - day2 = moment(day2); + function filterDevices(status) { + if(status === 'all') { + status = undefined; + } + vm.status = status; + } - if(day1.startOf('day').isSame(day2.startOf('day'))) { - return true; + function setSidebarMinHeight() { + var height = document.body.clientHeight / 4 * 3; + angular.element('.profile_content').css('min-height', height + 'px'); } - return false; } +})(); - function isDiffMoreThan15min(dateToCheckFrom, dateToCheckTo) { - var duration = moment.duration(moment(dateToCheckTo).diff(moment(dateToCheckFrom))); - return duration.as('minutes') > 15; - } +(function() { + 'use strict'; - function isWithin15min(dateToCheck) { - var fifteenMinAgo = moment().subtract(15, 'minutes').valueOf(); - dateToCheck = moment(dateToCheck).valueOf(); + angular.module('app.components') + .controller('UploadController', UploadController); - return dateToCheck > fifteenMinAgo; - } + UploadController.$inject = ['kit', '$state', '$stateParams', 'animation']; + function UploadController(kit, $state, $stateParams, animation) { + var vm = this; - function isWithin1Month(dateToCheck) { - var oneMonthAgo = moment().subtract(1, 'months').valueOf(); - dateToCheck = moment(dateToCheck).valueOf(); + vm.kit = kit; - return dateToCheck > oneMonthAgo; - } + vm.backToProfile = backToProfile; - function isWithin(number, type, dateToCheck) { - var ago = moment().subtract(number, type).valueOf(); - dateToCheck = moment(dateToCheck).valueOf(); + initialize(); - return dateToCheck > ago; + ///////////////// + + function initialize() { + animation.viewLoaded(); } - function parseDate(object){ - var time = object; - return { - raw: time, - parsed: !time ? 'No time' : moment(time).format('MMMM DD, YYYY - HH:mm'), - ago: !time ? 'No time' : moment(time).fromNow() - } + function backToProfile() { + $state.transitionTo('layout.myProfile.kits', $stateParams, + { reload: false, + inherit: false, + notify: true + }); } } })(); -(function() { - 'use strict'; - - angular.module('app.components') - .factory('sensorUtils', sensorUtils); - - sensorUtils.$inject = ['timeUtils']; - function sensorUtils(timeUtils) { - var service = { - getRollup: getRollup, - getSensorName: getSensorName, - getSensorValue: getSensorValue, - getSensorPrevValue: getSensorPrevValue, - getSensorIcon: getSensorIcon, - getSensorArrow: getSensorArrow, - getSensorColor: getSensorColor, - getSensorDescription: getSensorDescription - }; - return service; - - /////////////// +(function(){ +'use strict'; - function getRollup(dateFrom, dateTo) { - // Calculate how many data points we can fit on a users screen - // Smaller screens request less data from the API - var durationInSec = moment(dateTo).diff(moment(dateFrom)) / 1000; - var chartWidth = window.innerWidth / 2; - var rollup = parseInt(durationInSec / chartWidth) + 's'; +function parseDataForPost(csvArray) { + /* + EXPECTED PAYLOAD + { + "data": [{ + "recorded_at": "2016-06-08 10:30:00", + "sensors": [{ + "id": 22, + "value": 21 + }] + }] + } + */ + const ids = csvArray[3]; // save ids from the 4th header + csvArray.splice(0,4); // remove useless headers + return { + data: csvArray.map((data) => { + return { + recorded_at: data.shift(), // get the timestamp from the first column + sensors: data.map((value, index) => { + return { + id: ids[index+1], // get ID of sensor from headers + value: value + }; + }) + .filter((sensor) => sensor.value && sensor.id) // remove empty value or id + }; + }) + }; +} - /* - //var rangeDays = timeUtils.getCurrentRange(dateFrom, dateTo, {format: 'd'}); - var rollup; - if(rangeDays <= 1) { - rollup = '15s'; - } else if(rangeDays <= 7) { - rollup = '1h';//rollup = '15m'; - } else if(rangeDays > 7) { - rollup = '1d'; - } - */ - return rollup; - } - function getSensorName(name) { - var sensorName; - // TODO: Improvement check how we set new names - if( new RegExp('custom circuit', 'i').test(name) ) { - sensorName = name; - } else { - if(new RegExp('noise', 'i').test(name) ) { - sensorName = 'SOUND'; - } else if(new RegExp('light', 'i').test(name) ) { - sensorName = 'LIGHT'; - } else if((new RegExp('nets', 'i').test(name) ) || - (new RegExp('wifi', 'i').test(name))) { - sensorName = 'NETWORKS'; - } else if(new RegExp('co', 'i').test(name) ) { - sensorName = 'CO'; - } else if(new RegExp('no2', 'i').test(name) ) { - sensorName = 'NO2'; - } else if(new RegExp('humidity', 'i').test(name) ) { - sensorName = 'HUMIDITY'; - } else if(new RegExp('temperature', 'i').test(name) ) { - sensorName = 'TEMPERATURE'; - } else if(new RegExp('panel', 'i').test(name) ) { - sensorName = 'SOLAR PANEL'; - } else if(new RegExp('battery', 'i').test(name) ) { - sensorName = 'BATTERY'; - } else if(new RegExp('barometric pressure', 'i').test(name) ) { - sensorName = 'BAROMETRIC PRESSURE'; - } else if(new RegExp('PM 1', 'i').test(name) ) { - sensorName = 'PM 1'; - } else if(new RegExp('PM 2.5', 'i').test(name) ) { - sensorName = 'PM 2.5'; - } else if(new RegExp('PM 10', 'i').test(name) ) { - sensorName = 'PM 10'; - } else { - sensorName = name; +controller.$inject = ['device', 'Papa', '$mdDialog', '$q']; +function controller(device, Papa, $mdDialog, $q) { + var vm = this; + vm.loadingStatus = false; + vm.loadingProgress = 0; + vm.loadingType = 'indeterminate'; + vm.csvFiles = []; + vm.$onInit = function() { + vm.kitLastUpdate = Math.floor(new Date(vm.kit.time).getTime() / 1000); + } + vm.onSelect = function() { + vm.loadingStatus = true; + vm.loadingType = 'indeterminate'; + } + vm.change = function(files, invalidFiles) { + let count = 0; + vm.invalidFiles = invalidFiles; + if (!files) { return; } + vm.loadingStatus = true; + vm.loadingType = 'determinate'; + vm.loadingProgress = 0; + $q.all( + files + .filter((file) => vm._checkDuplicate(file)) + .map((file, index, filteredFiles) => { + vm.csvFiles.push(file); + return vm._analyzeData(file) + .then((result) => { + if (result.errors && result.errors.length > 0) { + file.parseErrors = result.errors; } - } - return sensorName.toUpperCase(); - } + const lastTimestamp = Math.floor((new Date(result.data[result.data.length - 1][0])).getTime() / 1000); + const isNew = vm.kitLastUpdate < lastTimestamp; + file.checked = isNew; + file.progress = null; + file.isNew = isNew; + }) + .then(() => { + count += 1; + vm.loadingProgress = (count)/filteredFiles.length * 100; - function getSensorValue(sensor) { - var value = sensor.value; + }); + }) + ).then(() => { + vm.loadingStatus = false; + }).catch(() => { + vm.loadingStatus = false; + }); + } - if(isNaN(parseInt(value))) { - value = 'NA'; - } else { - value = round(value, 1).toString(); - } + vm.haveSelectedFiles = function() { + return vm.csvFiles && vm.csvFiles.some((file) => file.checked); + }; - return value; - } + vm.haveSelectedNoFiles = function() { + return vm.csvFiles && !vm.csvFiles.some((file) => file.checked); + }; - function round(value, precision) { - var multiplier = Math.pow(10, precision || 0); - return Math.round(value * multiplier) / multiplier; - } + vm.haveSelectedAllFiles = function() { + return vm.csvFiles && vm.csvFiles.every((file) => file.checked); + }; - function getSensorPrevValue(sensor) { - /*jshint camelcase: false */ - var prevValue = sensor.prev_value; - return (prevValue && prevValue.toString() ) || 0; - } + vm.doAction = function() { + switch (vm.action) { + case 'selectAll': + vm.selectAll(true); + break; + case 'deselectAll': + vm.selectAll(false); + break; + case 'upload': + vm.uploadData(); + break; + case 'remove': + vm.csvFiles = vm.csvFiles.filter((file) => !file.checked); + break; + } + vm.action = null; + }; - function getSensorIcon(sensorName) { + vm.selectAll = function(value) { + vm.csvFiles.forEach((file) => { file.checked = value }); + }; - var thisName = getSensorName(sensorName); + vm.removeFile = function(index) { + vm.csvFiles.splice(index, 1); + }; + vm._analyzeData = function(file) { + file.progress = true; + return Papa.parse(file, { + delimiter: ',', + dynamicTyping: true, + worker: false, + skipEmptyLines: true + }).catch((err) => { + file.progress = null; + console('catch',err) + }); + }; - switch(thisName) { - case 'TEMPERATURE': - return './assets/images/temperature_icon_new.svg'; + vm._checkDuplicate = function(file) { + if (vm.csvFiles.some((csvFile) => file.name === csvFile.name)) { + file.$errorMessages = {}; + file.$errorMessages.duplicate = true; + vm.invalidFiles.push(file); + return false; + } else { + return true; + } + }; - case 'HUMIDITY': - return './assets/images/humidity_icon_new.svg'; + vm.showErrorModal = function(csvFile) { + $mdDialog.show({ + hasBackdrop: true, + controller: ['$mdDialog',function($mdDialog) { + this.parseErrors = csvFile.parseErrors; + this.backEndErrors = csvFile.backEndErrors; + this.cancel = function() { $mdDialog.hide(); }; + }], + controllerAs: 'csvFile', + templateUrl: 'app/components/upload/errorModal.html', + clickOutsideToClose: true + }); + } - case 'LIGHT': - return './assets/images/light_icon_new.svg'; - case 'SOUND': - return './assets/images/sound_icon_new.svg'; + vm.uploadData = function() { + vm.loadingStatus = true; + vm.loadingType = 'indeterminate'; + vm.loadingProgress = 0; + let count = 0; - case 'CO': - return './assets/images/co_icon_new.svg'; + $q.all( + vm.csvFiles + .filter((file) => file.checked && !file.success) + .map((file, index, filteredFiles) => { + file.progress = true; + return vm._analyzeData(file) + .then((result) => parseDataForPost(result.data)) // TODO: Improvement remove + // TODO: Improvement with workers + .then((payload) => device.postReadings(vm.kit, payload)) + .then(() => { + if (vm.loadingType === 'indeterminate') { vm.loadingType = 'determinate'; }; + file.success = true; + file.progress = null; + count += 1; + vm.loadingProgress = (count)/filteredFiles.length * 100; + }) + .catch((errors) => { + console.log(errors); + file.detailShowed = true; + file.backEndErrors = errors; + file.progress = null; + }); + }) + ).then(() => { + vm.loadingStatus = false; + }) + .catch(() => { + vm.loadingStatus = false; + }); + } +}; - case 'NO2': - return './assets/images/no2_icon_new.svg'; - case 'NETWORKS': - return './assets/images/networks_icon.svg'; +angular.module('app.components') + .component('scCsvUpload', { + templateUrl: 'app/components/upload/csvUpload.html', + controller: controller, + bindings: { + kit: '<' + }, + controllerAs: 'vm' + }); +})(); - case 'BATTERY': - return './assets/images/battery_icon.svg'; +(function() { + 'use strict'; - case 'SOLAR PANEL': - return './assets/images/solar_panel_icon.svg'; + angular.module('app.components') + .controller('tagsController', tagsController); - case 'BAROMETRIC PRESSURE': - return './assets/images/pressure_icon_new.svg'; + tagsController.$inject = ['tag', '$scope', 'device', '$state', '$q', + 'PreviewDevice', 'animation' + ]; - case 'PM 1': - case 'PM 2.5': - case 'PM 10': - return './assets/images/particle_icon_new.svg'; + function tagsController(tag, $scope, device, $state, $q, PreviewDevice, + animation) { - default: - return './assets/images/unknownsensor_icon.svg'; - } - } + var vm = this; - function getSensorArrow(currentValue, prevValue) { - currentValue = parseInt(currentValue) || 0; - prevValue = parseInt(prevValue) || 0; + vm.selectedTags = tag.getSelectedTags(); + vm.markers = []; + vm.kits = []; + vm.percActive = 0; - if(currentValue > prevValue) { - return 'arrow_up'; - } else if(currentValue < prevValue) { - return 'arrow_down'; - } else { - return 'equal'; - } - } + initialize(); - function getSensorColor(sensorName) { - switch(getSensorName(sensorName)) { - case 'TEMPERATURE': - return '#FF3D4C'; + ///////////////////////////////////////////////////////// - case 'HUMIDITY': - return '#55C4F5'; + function initialize() { + if(vm.selectedTags.length === 0){ + $state.transitionTo('layout.home.kit'); + } - case 'LIGHT': - return '#ffc107'; + if (device.getWorldMarkers()) { + // If the user has already loaded a prev page and has markers in mem or localstorage + updateSelectedTags(); + } else { + // If the user is new we wait the map to load the markers + $scope.$on('mapStateLoaded', function(event, data) { + updateSelectedTags(); + }); + } - case 'SOUND': - return '#0019FF'; + } - case 'CO': - return '#00A103'; + function updateSelectedTags(){ - case 'NO2': - return '#8cc252'; + vm.markers = tag.filterMarkersByTag(device.getWorldMarkers()); - case 'NETWORKS': - return '#681EBD'; + var onlineMarkers = _.filter(vm.markers, isOnline); + if (vm.markers.length === 0) { + vm.percActive = 0; + } else { + vm.percActive = Math.floor(onlineMarkers.length / vm.markers.length * + 100); + } - case 'SOLAR PANEL': - return '#d555ce'; + animation.viewLoaded(); - case 'BATTERY': - return '#ff8601'; + getTaggedDevices() + .then(function(res){ + vm.kits = res; + }); + } - default: - return '#0019FF'; - } - } - function getSensorDescription(sensorID, sensorTypes) { - return _(sensorTypes) - .chain() - .find(function(sensorType) { - return sensorType.id === sensorID; - }) - .value() - .measurement.description; - } + function isOnline(marker) { + return _.includes(marker.myData.labels, 'online'); } -})(); -(function() { - 'use strict'; + function descLastUpdate(o) { + return -new Date(o.last_reading_at).getTime(); + } - angular.module('app.components') - .factory('searchUtils', searchUtils); + function getTaggedDevices() { + var deviceProm = _.map(vm.markers, getMarkerDevice); - searchUtils.$inject = []; - function searchUtils() { - var service = { - parseLocation: parseLocation, - parseName: parseName, - parseIcon: parseIcon, - parseIconType: parseIconType - }; - return service; + return $q.all(deviceProm) + .then(function(devices) { + return _.map(_.sortBy(devices, descLastUpdate), toPreviewDevice); // This sort is temp + }); + } - ///////////////// + function toPreviewDevice(dev) { + return new PreviewDevice(dev); + } - function parseLocation(object) { - var location = ''; + function getMarkerDevice(marker) { + return device.getDevice(marker.myData.id); + } + } - if(!!object.city) { - location += object.city; - } - if(!!object.city && !!object.country) { - location += ', '; - } - if(!!object.country) { - location += object.country; - } +})(); - return location; - } +(function(){ + 'use strict'; + angular.module('app.components') + .directive('tag',tag); - function parseName(object) { - var name = object.type === 'User' ? object.username : object.name; - return name; - } + function tag(){ + return{ + restrict: 'E', + scope:{ + tagName: '=', + openTag: '&' + }, + controller:function($scope, $state){ + $scope.openTag = function(){ + $state.go('layout.home.tags', {tags:[$scope.tagName]}); + }; + }, + template:'{{tagName}}', + link: function(scope, element, attrs){ + element.addClass('tag'); - function parseIcon(object, type) { - switch(type) { - case 'User': - return object.profile_picture; - case 'Device': - return 'assets/images/kit.svg'; - case 'Country': - case 'City': - return 'assets/images/location_icon_normal.svg'; + if(typeof(attrs.clickable) !== 'undefined'){ + element.bind('click', scope.openTag); } } + }; + } +})(); - function parseIconType(type) { - switch(type) { - case 'Device': - return 'div'; - default: - return 'img'; - } - } +(function() { + 'use strict'; + + angular.module('app.components') + .controller('StoreModalController', StoreModalController); + + StoreModalController.$inject = ['$scope', '$mdDialog']; + function StoreModalController($scope, $mdDialog) { + + $scope.cancel = function() { + $mdDialog.hide(); + }; } })(); (function() { 'use strict'; - angular.module('app.components') - .factory('markerUtils', markerUtils); + angular.module('app.components') + .directive('store', store); - markerUtils.$inject = ['deviceUtils', 'MARKER_ICONS']; - function markerUtils(deviceUtils, MARKER_ICONS) { - var service = { - getIcon: getIcon, - getMarkerIcon: getMarkerIcon, + function store() { + return { + scope: { + isLoggedin: '=logged' + }, + restrict: 'A', + controller: 'StoreController', + controllerAs: 'vm', + templateUrl: 'app/components/store/store.html' }; - _.defaults(service, deviceUtils); - return service; + } +})(); - /////////////// +(function() { + 'use strict'; - function getIcon(object) { - var icon; - var labels = deviceUtils.parseSystemTags(object); - var isSCKHardware = deviceUtils.isSCKHardware(object); + angular.module('app.components') + .controller('StoreController', StoreController); - if(hasLabel(labels, 'offline')) { - icon = MARKER_ICONS.markerSmartCitizenOffline; - } else if (isSCKHardware) { - icon = MARKER_ICONS.markerSmartCitizenOnline; - } else { - icon = MARKER_ICONS.markerExperimentalNormal; - } - return icon; - } + StoreController.$inject = ['$scope', '$mdDialog']; + function StoreController($scope, $mdDialog) { - function hasLabel(labels, targetLabel) { - return _.some(labels, function(label) { - return label === targetLabel; - }); - } + $scope.showStore = showStore; - function getMarkerIcon(marker, state) { - var markerType = marker.icon.className; + $scope.$on('showStore', function() { + showStore(); + }); + + //////////////// - if(state === 'active') { - marker.icon = MARKER_ICONS[markerType + 'Active']; - marker.focus = true; - } else if(state === 'inactive') { - var targetClass = markerType.split(' ')[0]; - marker.icon = MARKER_ICONS[targetClass]; - } - return marker; - } + function showStore() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'StoreModalController', + templateUrl: 'app/components/store/storeModal.html', + clickOutsideToClose: true + }); } + + } })(); (function() { 'use strict'; angular.module('app.components') - .factory('mapUtils', mapUtils); + .controller('StaticController', StaticController); - mapUtils.$inject = []; - function mapUtils() { - var service = { - getDefaultFilters: getDefaultFilters, - setDefaultFilters: setDefaultFilters, - canFilterBeRemoved: canFilterBeRemoved - }; - return service; + StaticController.$inject = ['$timeout', 'animation', '$mdDialog', '$location', '$anchorScroll']; - ////////////// + function StaticController($timeout, animation, $mdDialog, $location, $anchorScroll) { + var vm = this; - function getDefaultFilters(filterData, defaultFilters) { - var obj = {}; - if(!filterData.indoor && !filterData.outdoor) { - obj[defaultFilters.exposure] = true; - } - if(!filterData.online && !filterData.offline) { - obj[defaultFilters.status] = true; - } - return obj; - } + vm.showStore = showStore; - function setDefaultFilters(filterData) { - var obj = {}; - if(!filterData.indoor || !filterData.outdoor) { - obj.exposure = filterData.indoor ? 'indoor' : 'outdoor'; - } - if(!filterData.online || !filterData.offline) { - obj.status = filterData.online ? 'online' : 'offline'; - } - return obj; - } + $anchorScroll.yOffset = 80; - function canFilterBeRemoved(filterData, filterName) { - if(filterName === 'indoor' || filterName === 'outdoor') { - return filterData.indoor && filterData.outdoor; - } else if(filterName === 'online' || filterName === 'offline') { - return filterData.online && filterData.offline; + /////////////////////// + + initialize(); + + ////////////////// + + function initialize() { + $timeout(function() { + animation.viewLoaded(); + if($location.hash()){ + $anchorScroll(); } - } + }, 500); + } + + function showStore() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'StoreModalController', + templateUrl: 'app/components/store/storeModal.html', + clickOutsideToClose: true + }); } + } })(); (function() { 'use strict'; + angular.module('app.components') - .config(function ($provide) { - $provide.decorator('$exceptionHandler', ['$delegate', function($delegate) { - return function (exception, cause) { - /*jshint camelcase: false */ - $delegate(exception, cause); - }; - }]); + .controller('SignupModalController', SignupModalController); - }); + SignupModalController.$inject = ['$scope', '$mdDialog', 'user', + 'alert', 'animation']; + function SignupModalController($scope, $mdDialog, user, + alert, animation ) { + var vm = this; + vm.answer = function(signupForm) { + + if (!signupForm.$valid){ + return; + } + + $scope.waitingFromServer = true; + user.createUser(vm.user) + .then(function() { + alert.success('Signup was successful'); + $mdDialog.hide(); + }).catch(function(err) { + alert.error('Signup failed'); + $scope.errors = err.data.errors; + }) + .finally(function() { + $scope.waitingFromServer = false; + }); + }; + $scope.hide = function() { + $mdDialog.hide(); + }; + $scope.cancel = function() { + $mdDialog.cancel(); + }; + + $scope.openLogin = function() { + animation.showLogin(); + $mdDialog.hide(); + }; + } })(); (function() { 'use strict'; - angular.module('app.components') - .factory('deviceUtils', deviceUtils); + angular.module('app.components') + .directive('signup', signup); - deviceUtils.$inject = ['COUNTRY_CODES', 'device']; - function deviceUtils(COUNTRY_CODES, device) { - var service = { - parseLocation: parseLocation, - parseCoordinates: parseCoordinates, - parseSystemTags: parseSystemTags, - parseUserTags: parseUserTags, - classify: classify, - parseNotifications: parseNotifications, - parseOwner: parseOwner, - parseName: parseName, - parseString: parseString, - parseHardware: parseHardware, - parseHardwareInfo: parseHardwareInfo, - parseHardwareName: parseHardwareName, - isPrivate: isPrivate, - preciseLocation: preciseLocation, - enableForwarding: enableForwarding, - isLegacyVersion: isLegacyVersion, - isSCKHardware: isSCKHardware, - parseState: parseState, - parseAvatar: parseAvatar, - belongsToUser: belongsToUser, - parseSensorTime: parseSensorTime + function signup() { + return { + scope: { + show: '=', + }, + restrict: 'A', + controller: 'SignupController', + controllerAs: 'vm', + templateUrl: 'app/components/signup/signup.html' }; + } +})(); - return service; +(function() { + 'use strict'; - /////////////// + angular.module('app.components') + .controller('SignupController', SignupController); - function parseLocation(object) { - var location = ''; - var city = ''; - var country = ''; + SignupController.$inject = ['$scope', '$mdDialog']; + function SignupController($scope, $mdDialog) { + var vm = this; - if (object.location) { - city = object.location.city; - country = object.location.country; - if(!!city) { - location += city; - } - if(!!city && !!location) { - location += ', ' - } - if(!!country) { - location += country; - } - } - return location; - } + vm.showSignup = showSignup; - function parseCoordinates(object) { - if (object.location) { - return { - lat: object.location.latitude, - lng: object.location.longitude - }; - } - // TODO: Bug - what happens if no location? - } + $scope.$on('showSignup', function() { + showSignup(); + }); + //////////////////////// - function parseSystemTags(object) { - /*jshint camelcase: false */ - return object.system_tags; - } - function parseUserTags(object) { - return object.user_tags; + function showSignup() { + $mdDialog.show({ + fullscreen: true, + hasBackdrop: true, + controller: 'SignupModalController', + controllerAs: 'vm', + templateUrl: 'app/components/signup/signupModal.html', + clickOutsideToClose: true + }); } + } +})(); - function parseNotifications(object){ - return { - lowBattery: object.notify.low_battery, - stopPublishing: object.notify.stopped_publishing - } - } +(function() { +'use strict'; - function classify(kitType) { - if(!kitType) { - return ''; - } - return kitType.toLowerCase().split(' ').join('_'); - } - function parseName(object, trim=false) { - if(!object.name) { - return; - } - if (trim) { - return object.name.length <= 41 ? object.name : object.name.slice(0, 35).concat(' ... '); - } - return object.name; - } + angular.module('app.components') + .directive('search', search); - function parseHardware(object) { - if (!object.hardware) { - return; - } + function search() { + return { + scope: true, + restrict: 'E', + templateUrl: 'app/components/search/search.html', + controller: 'SearchController', + controllerAs: 'vm' + }; + } +})(); - return { - name: parseString(object.hardware.name), - type: parseString(object.hardware.type), - description: parseString(object.hardware.description), - version: parseVersionString(object.hardware.version), - slug: object.hardware.slug, - info: parseHardwareInfo(object.hardware.info) - } - } +(function() { + 'use strict'; - function parseString(str) { - if (typeof(str) !== 'string') { return null; } - return str; - } + angular.module('app.components') + .controller('SearchController', SearchController); - function parseVersionString (str) { - if (typeof(str) !== 'string') { return null; } - var x = str.split('.'); - // parse from string or default to 0 if can't parse - var maj = parseInt(x[0]) || 0; - var min = parseInt(x[1]) || 0; - var pat = parseInt(x[2]) || 0; - return { - major: maj, - minor: min, - patch: pat - }; - } + SearchController.$inject = ['$scope', 'search', 'SearchResult', '$location', 'animation', 'SearchResultLocation']; + function SearchController($scope, search, SearchResult, $location, animation, SearchResultLocation) { + var vm = this; - function parseHardwareInfo (object) { - if (!object) { return null; } // null - if (typeof(object) == 'string') { return null; } // FILTERED + vm.searchTextChange = searchTextChange; + vm.selectedItemChange = selectedItemChange; + vm.querySearch = querySearch; - var id = parseString(object.id); - var mac = parseString(object.mac); - var time = Date(object.time); - var esp_bd = parseString(object.esp_bd); - var hw_ver = parseString(object.hw_ver); - var sam_bd = parseString(object.sam_bd); - var esp_ver = parseString(object.esp_ver); - var sam_ver = parseString(object.sam_ver); + /////////////////// - return { - id: id, - mac: mac, - time: time, - esp_bd: esp_bd, - hw_ver: hw_ver, - sam_bd: sam_bd, - esp_ver: esp_ver, - sam_ver: sam_ver - }; + function searchTextChange() { } - function parseHardwareName(object) { - if (object.hasOwnProperty('hardware')) { - if (!object.hardware.name) { - return 'Unknown hardware' - } - return object.hardware.name; - } else { - return 'Unknown hardware' + function selectedItemChange(result) { + if (!result) { return; } + if(result.type === 'User') { + $location.path('/users/' + result.id); + } else if(result.type === 'Device') { + $location.path('/kits/' + result.id); + } else if (result.type === 'City'){ + animation.goToLocation({lat: result.lat, lng: result.lng, type: result.type, layer: result.layer}); } } - function isPrivate(object) { - return object.data_policy.is_private; - } + function querySearch(query) { + if(query.length < 3) { + return []; + } - function preciseLocation(object) { - return object.data_policy.precise_location; - } + return search.globalSearch(query) + .then(function(data) { - function enableForwarding(object) { - return object.data_policy.enable_forwarding ; - } + return data.map(function(object) { - function isLegacyVersion (object) { - if (!object.hardware || !object.hardware.version || object.hardware.version.major > 1) { - return false; - } else { - if (object.hardware.version.major == 1 && object.hardware.version.minor <5 ){ - return true; - } - return false; - } + if(object.type === 'City' || object.type === 'Country') { + return new SearchResultLocation(object); + } else { + return new SearchResult(object); + } + }); + }); } + } +})(); - function isSCKHardware (object){ - if (!object.hardware || !object.hardware.type || object.hardware.type != 'SCK') { - return false; - } else { - return true; - } - } +(function() { + 'use strict'; - function parseOwner(object) { - return { - id: object.owner.id, - username: object.owner.username, - /*jshint camelcase: false */ - devices: object.owner.device_ids, - city: object.owner.location.city, - country: COUNTRY_CODES[object.owner.location.country_code], - url: object.owner.url, - profile_picture: object.owner.profile_picture - }; - } + angular.module('app.components') + .controller('PasswordResetController', PasswordResetController); - function parseState(status) { - var name = parseStateName(status); - var className = classify(name); + PasswordResetController.$inject = ['$mdDialog', '$stateParams', '$timeout', + 'animation', '$location', 'alert', 'auth']; + function PasswordResetController($mdDialog, $stateParams, $timeout, + animation, $location, alert, auth) { + + var vm = this; + vm.showForm = false; + vm.form = {}; + vm.isDifferent = false; + vm.answer = answer; - return { - name: name, - className: className - }; - } + initialize(); + /////////// - function parseStateName(object) { - return object.state.replace('_', ' '); + function initialize() { + $timeout(function() { + animation.viewLoaded(); + }, 500); + getUserData(); } - function parseAvatar() { - return './assets/images/sckit_avatar.jpg'; + function getUserData() { + auth.getResetPassword($stateParams.code) + .then(function() { + vm.showForm = true; + }) + .catch(function() { + alert.error('Wrong url'); + $location.path('/'); + }); } - function parseSensorTime(sensor) { - /*jshint camelcase: false */ - return moment(sensor.recorded_at).format(''); - } + function answer(data) { + vm.waitingFromServer = true; + vm.errors = undefined; - function belongsToUser(devicesArray, deviceID) { - return _.some(devicesArray, function(device) { - return device.id === deviceID; - }); + if(data.newPassword === data.confirmPassword) { + vm.isDifferent = false; + } else { + vm.isDifferent = true; + return; + } + + auth.patchResetPassword($stateParams.code, {password: data.newPassword}) + .then(function() { + alert.success('Your data was updated successfully'); + $location.path('/profile'); + }) + .catch(function(err) { + alert.error('Your data wasn\'t updated'); + vm.errors = err.data.errors; + }) + .finally(function() { + vm.waitingFromServer = false; + }); } } })(); @@ -2623,25 +2605,42 @@ 'use strict'; angular.module('app.components') - .filter('filterLabel', filterLabel); + .controller('PasswordRecoveryModalController', PasswordRecoveryModalController); + PasswordRecoveryModalController.$inject = ['$scope', 'animation', '$mdDialog', 'auth', 'alert']; + function PasswordRecoveryModalController($scope, animation, $mdDialog, auth, alert) { - function filterLabel() { - return function(devices, targetLabel) { - if(targetLabel === undefined) { - return devices; - } - if(devices) { - return _.filter(devices, function(device) { - var containsLabel = device.systemTags.indexOf(targetLabel) !== -1; - if(containsLabel) { - return containsLabel; - } - // This should be fixed or polished in the future - // var containsNewIfTargetIsOnline = targetLabel === 'online' && _.some(kit.labels, function(label) {return label.indexOf('new') !== -1;}); - // return containsNewIfTargetIsOnline; - }); - } + $scope.hide = function() { + $mdDialog.hide(); + }; + $scope.cancel = function() { + $mdDialog.cancel(); + }; + + $scope.recoverPassword = function() { + $scope.waitingFromServer = true; + var data = { + /*jshint camelcase: false */ + email_or_username: $scope.input + }; + + auth.recoverPassword(data) + .then(function() { + alert.success('You were sent an email to recover your password'); + $mdDialog.hide(); + }) + .catch(function(err) { + alert.error('That username doesn\'t exist'); + $scope.errors = err.data; + }) + .finally(function() { + $scope.waitingFromServer = false; + }); + }; + + $scope.openSignup = function() { + animation.showSignup(); + $mdDialog.hide(); }; } })(); @@ -2649,1634 +2648,1199 @@ (function() { 'use strict'; - /** - * Tools links for user profile - * @constant - * @type {Array} - */ - angular.module('app.components') - .constant('PROFILE_TOOLS', [{ - type: 'documentation', - title: 'How to connect your Smart Citizen Kit tutorial', - description: 'Adding a Smart Citizen Kit tutorial', - avatar: '', - href: 'http://docs.smartcitizen.me/#/start/adding-a-smart-citizen-kit' - }, { - type: 'documentation', - title: 'Download the latest Smart Citizen Kit Firmware', - description: 'The latest Arduino firmware for your kit', - avatar: '', - href: 'https://github.com/fablabbcn/Smart-Citizen-Kit/releases/latest' - }, { - type: 'documentation', - title: 'API Documentation', - description: 'Documentation for the new API', - avatar: '', - href: 'http://developer.smartcitizen.me/' - }, { - type: 'community', - title: 'Smart Citizen Forum', - description: 'Join the community discussion. Your feedback is important for us.', - avatar: '', - href:'http://forum.smartcitizen.me/' - }, { - type: 'documentation', - title: 'Smart Citizen Kit hardware details', - description: 'Visit the docs', - avatar: 'https://docs.smartcitizen.me/#/start/hardware' - }, { - type: 'documentation', - title: 'Style Guide', - description: 'Guidelines of the Smart Citizen UI', - avatar: '', - href: '/styleguide' - }, { - type: 'social', - title: 'Like us on Facebook', - description: 'Join the community on Facebook', - avatar: '', - href: 'https://www.facebook.com/smartcitizenBCN' - }, { - type: 'social', - title: 'Follow us on Twitter', - description: 'Follow our news on Twitter', - avatar: '', - href: 'https://twitter.com/SmartCitizenKit' - }]); -})(); + .controller('PasswordRecoveryController', PasswordRecoveryController); -(function() { - 'use strict'; + PasswordRecoveryController.$inject = ['auth', 'alert', '$mdDialog']; + function PasswordRecoveryController(auth, alert, $mdDialog) { + var vm = this; - /** - * Marker icons - * @constant - * @type {Object} - */ + vm.waitingFromServer = false; + vm.errors = undefined; + vm.recoverPassword = recoverPassword; - angular.module('app.components') - .constant('MARKER_ICONS', { - defaultIcon: {}, - markerSmartCitizenNormal: { - type: 'div', - className: 'markerSmartCitizenNormal', - iconSize: [24, 24] - }, - markerExperimentalNormal: { - type: 'div', - className: 'markerExperimentalNormal', - iconSize: [24, 24] - }, - markerSmartCitizenOnline: { - type: 'div', - className: 'markerSmartCitizenOnline', - iconSize: [24, 24] - }, - markerSmartCitizenOnlineActive: { - type: 'div', - className: 'markerSmartCitizenOnline marker_blink', - iconSize: [24, 24] - }, - markerSmartCitizenOffline: { - type: 'div', - className: 'markerSmartCitizenOffline', - iconSize: [24, 24] - }, - markerSmartCitizenOfflineActive: { - type: 'div', - className: 'markerSmartCitizenOffline marker_blink', - iconSize: [24, 24] + /////////////// + + function recoverPassword() { + vm.waitingFromServer = true; + vm.errors = undefined; + + var data = { + username: vm.username + }; + + auth.recoverPassword(data) + .then(function() { + alert.success('You were sent an email to recover your password'); + $mdDialog.hide(); + }) + .catch(function(err) { + vm.errors = err.data.errors; + if(vm.errors) { + alert.error('That email/username doesn\'t exist'); + } + }) + .finally(function() { + vm.waitingFromServer = false; + }); } - }); + } })(); (function() { 'use strict'; - /** - * Dropdown options for user - * @constant - * @type {Array} - */ angular.module('app.components') - .constant('DROPDOWN_OPTIONS_USER', [ - {divider: true, text: 'Hi,', href: './profile'}, - {text: 'My profile', href: './profile'}, - {text: 'Log out', href: './logout'} - ]); -})(); + .controller('MyProfileController', MyProfileController); -(function() { - 'use strict'; + MyProfileController.$inject = ['$scope', '$location', '$q', '$interval', + 'userData', 'AuthUser', 'user', 'auth', 'alert', + 'COUNTRY_CODES', '$timeout', 'file', 'animation', + '$mdDialog', 'PreviewDevice', 'device', 'deviceUtils', + 'userUtils', '$filter', '$state', 'Restangular', '$window']; + function MyProfileController($scope, $location, $q, $interval, + userData, AuthUser, user, auth, alert, + COUNTRY_CODES, $timeout, file, animation, + $mdDialog, PreviewDevice, device, deviceUtils, + userUtils, $filter, $state, Restangular, $window) { - /** - * Dropdown options for community button - * @constant - * @type {Array} - */ + var vm = this; - angular.module('app.components') - .constant('DROPDOWN_OPTIONS_COMMUNITY', [ - {text: 'About', href: '/about'}, - {text: 'Forum', href: 'https://forum.smartcitizen.me/'}, - {text: 'Documentation', href: 'http://docs.smartcitizen.me/'}, - {text: 'API Reference', href: 'http://developer.smartcitizen.me/'}, - {text: 'Github', href: 'https://github.com/fablabbcn/Smart-Citizen-Kit'}, - {text: 'Legal', href: '/policy'} - ]); -})(); + vm.unhighlightIcon = unhighlightIcon; -(function() { - 'use strict'; + //PROFILE TAB + vm.formUser = {}; + vm.getCountries = getCountries; - /** - * Country codes. - * @constant - * @type {Object} - */ - - angular.module('app.components') - .constant('COUNTRY_CODES', { - 'AF': 'Afghanistan', - 'AX': 'Aland Islands', - 'AL': 'Albania', - 'DZ': 'Algeria', - 'AS': 'American Samoa', - 'AD': 'Andorra', - 'AO': 'Angola', - 'AI': 'Anguilla', - 'AQ': 'Antarctica', - 'AG': 'Antigua And Barbuda', - 'AR': 'Argentina', - 'AM': 'Armenia', - 'AW': 'Aruba', - 'AU': 'Australia', - 'AT': 'Austria', - 'AZ': 'Azerbaijan', - 'BS': 'Bahamas', - 'BH': 'Bahrain', - 'BD': 'Bangladesh', - 'BB': 'Barbados', - 'BY': 'Belarus', - 'BE': 'Belgium', - 'BZ': 'Belize', - 'BJ': 'Benin', - 'BM': 'Bermuda', - 'BT': 'Bhutan', - 'BO': 'Bolivia', - 'BA': 'Bosnia And Herzegovina', - 'BW': 'Botswana', - 'BV': 'Bouvet Island', - 'BR': 'Brazil', - 'IO': 'British Indian Ocean Territory', - 'BN': 'Brunei Darussalam', - 'BG': 'Bulgaria', - 'BF': 'Burkina Faso', - 'BI': 'Burundi', - 'KH': 'Cambodia', - 'CM': 'Cameroon', - 'CA': 'Canada', - 'CV': 'Cape Verde', - 'KY': 'Cayman Islands', - 'CF': 'Central African Republic', - 'TD': 'Chad', - 'CL': 'Chile', - 'CN': 'China', - 'CX': 'Christmas Island', - 'CC': 'Cocos (Keeling) Islands', - 'CO': 'Colombia', - 'KM': 'Comoros', - 'CG': 'Congo', - 'CD': 'Congo, Democratic Republic', - 'CK': 'Cook Islands', - 'CR': 'Costa Rica', - 'CI': 'Cote D\'Ivoire', - 'HR': 'Croatia', - 'CU': 'Cuba', - 'CY': 'Cyprus', - 'CZ': 'Czech Republic', - 'DK': 'Denmark', - 'DJ': 'Djibouti', - 'DM': 'Dominica', - 'DO': 'Dominican Republic', - 'EC': 'Ecuador', - 'EG': 'Egypt', - 'SV': 'El Salvador', - 'GQ': 'Equatorial Guinea', - 'ER': 'Eritrea', - 'EE': 'Estonia', - 'ET': 'Ethiopia', - 'FK': 'Falkland Islands (Malvinas)', - 'FO': 'Faroe Islands', - 'FJ': 'Fiji', - 'FI': 'Finland', - 'FR': 'France', - 'GF': 'French Guiana', - 'PF': 'French Polynesia', - 'TF': 'French Southern Territories', - 'GA': 'Gabon', - 'GM': 'Gambia', - 'GE': 'Georgia', - 'DE': 'Germany', - 'GH': 'Ghana', - 'GI': 'Gibraltar', - 'GR': 'Greece', - 'GL': 'Greenland', - 'GD': 'Grenada', - 'GP': 'Guadeloupe', - 'GU': 'Guam', - 'GT': 'Guatemala', - 'GG': 'Guernsey', - 'GN': 'Guinea', - 'GW': 'Guinea-Bissau', - 'GY': 'Guyana', - 'HT': 'Haiti', - 'HM': 'Heard Island & Mcdonald Islands', - 'VA': 'Holy See (Vatican City State)', - 'HN': 'Honduras', - 'HK': 'Hong Kong', - 'HU': 'Hungary', - 'IS': 'Iceland', - 'IN': 'India', - 'ID': 'Indonesia', - 'IR': 'Iran, Islamic Republic Of', - 'IQ': 'Iraq', - 'IE': 'Ireland', - 'IM': 'Isle Of Man', - 'IL': 'Israel', - 'IT': 'Italy', - 'JM': 'Jamaica', - 'JP': 'Japan', - 'JE': 'Jersey', - 'JO': 'Jordan', - 'KZ': 'Kazakhstan', - 'KE': 'Kenya', - 'KI': 'Kiribati', - 'KR': 'Korea', - 'KW': 'Kuwait', - 'KG': 'Kyrgyzstan', - 'LA': 'Lao People\'s Democratic Republic', - 'LV': 'Latvia', - 'LB': 'Lebanon', - 'LS': 'Lesotho', - 'LR': 'Liberia', - 'LY': 'Libyan Arab Jamahiriya', - 'LI': 'Liechtenstein', - 'LT': 'Lithuania', - 'LU': 'Luxembourg', - 'MO': 'Macao', - 'MK': 'Macedonia', - 'MG': 'Madagascar', - 'MW': 'Malawi', - 'MY': 'Malaysia', - 'MV': 'Maldives', - 'ML': 'Mali', - 'MT': 'Malta', - 'MH': 'Marshall Islands', - 'MQ': 'Martinique', - 'MR': 'Mauritania', - 'MU': 'Mauritius', - 'YT': 'Mayotte', - 'MX': 'Mexico', - 'FM': 'Micronesia, Federated States Of', - 'MD': 'Moldova', - 'MC': 'Monaco', - 'MN': 'Mongolia', - 'ME': 'Montenegro', - 'MS': 'Montserrat', - 'MA': 'Morocco', - 'MZ': 'Mozambique', - 'MM': 'Myanmar', - 'NA': 'Namibia', - 'NR': 'Nauru', - 'NP': 'Nepal', - 'NL': 'Netherlands', - 'AN': 'Netherlands Antilles', - 'NC': 'New Caledonia', - 'NZ': 'New Zealand', - 'NI': 'Nicaragua', - 'NE': 'Niger', - 'NG': 'Nigeria', - 'NU': 'Niue', - 'NF': 'Norfolk Island', - 'MP': 'Northern Mariana Islands', - 'NO': 'Norway', - 'OM': 'Oman', - 'PK': 'Pakistan', - 'PW': 'Palau', - 'PS': 'Palestinian Territory, Occupied', - 'PA': 'Panama', - 'PG': 'Papua New Guinea', - 'PY': 'Paraguay', - 'PE': 'Peru', - 'PH': 'Philippines', - 'PN': 'Pitcairn', - 'PL': 'Poland', - 'PT': 'Portugal', - 'PR': 'Puerto Rico', - 'QA': 'Qatar', - 'RE': 'Reunion', - 'RO': 'Romania', - 'RU': 'Russian Federation', - 'RW': 'Rwanda', - 'BL': 'Saint Barthelemy', - 'SH': 'Saint Helena', - 'KN': 'Saint Kitts And Nevis', - 'LC': 'Saint Lucia', - 'MF': 'Saint Martin', - 'PM': 'Saint Pierre And Miquelon', - 'VC': 'Saint Vincent And Grenadines', - 'WS': 'Samoa', - 'SM': 'San Marino', - 'ST': 'Sao Tome And Principe', - 'SA': 'Saudi Arabia', - 'SN': 'Senegal', - 'RS': 'Serbia', - 'SC': 'Seychelles', - 'SL': 'Sierra Leone', - 'SG': 'Singapore', - 'SK': 'Slovakia', - 'SI': 'Slovenia', - 'SB': 'Solomon Islands', - 'SO': 'Somalia', - 'ZA': 'South Africa', - 'GS': 'South Georgia And Sandwich Isl.', - 'ES': 'Spain', - 'LK': 'Sri Lanka', - 'SD': 'Sudan', - 'SR': 'Suriname', - 'SJ': 'Svalbard And Jan Mayen', - 'SZ': 'Swaziland', - 'SE': 'Sweden', - 'CH': 'Switzerland', - 'SY': 'Syrian Arab Republic', - 'TW': 'Taiwan', - 'TJ': 'Tajikistan', - 'TZ': 'Tanzania', - 'TH': 'Thailand', - 'TL': 'Timor-Leste', - 'TG': 'Togo', - 'TK': 'Tokelau', - 'TO': 'Tonga', - 'TT': 'Trinidad And Tobago', - 'TN': 'Tunisia', - 'TR': 'Turkey', - 'TM': 'Turkmenistan', - 'TC': 'Turks And Caicos Islands', - 'TV': 'Tuvalu', - 'UG': 'Uganda', - 'UA': 'Ukraine', - 'AE': 'United Arab Emirates', - 'GB': 'United Kingdom', - 'US': 'United States', - 'UM': 'United States Outlying Islands', - 'UY': 'Uruguay', - 'UZ': 'Uzbekistan', - 'VU': 'Vanuatu', - 'VE': 'Venezuela', - 'VN': 'Viet Nam', - 'VG': 'Virgin Islands, British', - 'VI': 'Virgin Islands, U.S.', - 'WF': 'Wallis And Futuna', - 'EH': 'Western Sahara', - 'YE': 'Yemen', - 'ZM': 'Zambia', - 'ZW': 'Zimbabwe' - }); -})(); + vm.user = userData; + copyUserToForm(vm.formUser, vm.user); + vm.searchText = vm.formUser.country; -(function() { - 'use strict'; + vm.updateUser = updateUser; + vm.removeUser = removeUser; + vm.uploadAvatar = uploadAvatar; - angular.module('app.components') - .factory('user', user); + //THIS IS TEMPORARY. + // Will grow on to a dynamic API KEY management + // with the new /accounts oAuth mgmt methods - user.$inject = ['Restangular']; - function user(Restangular) { - var service = { - createUser: createUser, - getUser: getUser, - updateUser: updateUser - }; - return service; + // The auth controller has not populated the `user` at this point, + // so user.token is undefined + // This controller depends on auth has already been run. + vm.user.token = auth.getToken(); + vm.addNewDevice = addNewDevice; - //////////////////// + //KITS TAB + vm.devices = []; + vm.deviceStatus = undefined; + vm.removeDevice = removeDevice; + vm.downloadData = downloadData; - function createUser(signupData) { - return Restangular.all('users').post(signupData); - } + vm.filteredDevices = []; + vm.dropdownSelected = undefined; - function getUser(id) { - return Restangular.one('users', id).get(); - } + //SIDEBAR + vm.filterDevices = filterDevices; + vm.filterTools = filterTools; - function updateUser(updateData) { - return Restangular.all('me').customPUT(updateData); - } - } -})(); + vm.selectThisTab = selectThisTab; -(function() { - 'use strict'; + $scope.$on('loggedOut', function() { + $location.path('/'); + }); - angular.module('app.components') - .factory('tag', tag); + $scope.$on('devicesContextUpdated', function(){ + var userData = auth.getCurrentUser().data; + if(userData){ + vm.user = userData; + } + initialize(); + }); - tag.$inject = ['Restangular']; - function tag(Restangular) { - var tags = []; - var selectedTags = []; + initialize(); - var service = { - getTags: getTags, - getSelectedTags: getSelectedTags, - setSelectedTags: setSelectedTags, - tagWithName: tagWithName, - filterMarkersByTag: filterMarkersByTag - }; + ////////////////// - return service; + function initialize() { - ///////////////// + startingTab(); + if(!vm.user.devices.length) { + vm.devices = []; + animation.viewLoaded(); + } else { - function getTags() { - return Restangular.all('tags') - .getList({'per_page': 200}) - .then(function(fetchedTags){ - tags = fetchedTags.plain(); - return tags; + vm.devices = vm.user.devices.map(function(data) { + return new PreviewDevice(data); + }) + + $timeout(function() { + mapWithBelongstoUser(vm.devices); + filterDevices(vm.status); + setSidebarMinHeight(); + animation.viewLoaded(); }); - } - function getSelectedTags(){ - return selectedTags; - } - - function setSelectedTags(tags){ - selectedTags = tags; + } } - function tagWithName(name){ - var result = _.where(tags, {name: name}); - if (result && result.length > 0){ - return result[0]; - }else{ - return; + function filterDevices(status) { + if(status === 'all') { + status = undefined; } + vm.deviceStatus = status; + vm.filteredDevices = $filter('filterLabel')(vm.devices, vm.deviceStatus); } - function filterMarkersByTag(tmpMarkers) { - var markers = filterMarkers(tmpMarkers); - return markers; + function filterTools(type) { + if(type === 'all') { + type = undefined; + } + vm.toolType = type; } - function filterMarkers(tmpMarkers) { - if (service.getSelectedTags().length === 0){ - return tmpMarkers; + function updateUser(userData) { + if(userData.country) { + _.each(COUNTRY_CODES, function(value, key) { + if(value === userData.country) { + /*jshint camelcase: false */ + userData.country_code = key; + return; + } + }); + } else { + userData.country_code = null; } - return tmpMarkers.filter(function(marker) { - var tags = marker.myData.tags; - if (tags.length === 0){ - return false; - } - return _.some(tags, function(tag) { - return _.includes(service.getSelectedTags(), tag); + + user.updateUser(userData) + .then(function(data) { + var user = new AuthUser(data); + _.extend(vm.user, user); + auth.updateUser(); + vm.errors = {}; + alert.success('User updated'); + }) + .catch(function(err) { + alert.error('User could not be updated '); + vm.errors = err.data.errors; }); - }); } - } -})(); -(function() { - 'use strict'; + function removeUser() { + var confirm = $mdDialog.confirm() + .title('Delete your account?') + .textContent('Are you sure you want to delete your account?') + .ariaLabel('') + .ok('delete') + .cancel('cancel') + .theme('primary') + .clickOutsideToClose(true); - angular.module('app.components') - .factory('sensor', sensor); + $mdDialog.show(confirm) + .then(function(){ + return Restangular.all('').customDELETE('me') + .then(function(){ + alert.success('Account removed successfully. Redirecting you…'); + $timeout(function(){ + auth.logout(); + $state.transitionTo('landing'); + }, 2000); + }) + .catch(function(){ + alert.error('Error occurred trying to delete your account.'); + }); + }); + } - sensor.$inject = ['Restangular', 'timeUtils', 'sensorUtils']; - function sensor(Restangular, timeUtils, sensorUtils) { - var sensorTypes; - callAPI().then(function(data) { - setTypes(data); - }); + function selectThisTab(iconIndex, uistate){ + /* This looks more like a hack but we need to workout how to properly use md-tab with ui-router */ - var service = { - callAPI: callAPI, - setTypes: setTypes, - getTypes: getTypes, - getSensorsData: getSensorsData - }; - return service; + highlightIcon(iconIndex); - //////////////// + if ($state.current.name.includes('myProfileAdmin')){ + var transitionState = 'layout.myProfileAdmin.' + uistate; + $state.transitionTo(transitionState, {id: userData.id}); + } else { + var transitionState = 'layout.myProfile.' + uistate; + $state.transitionTo(transitionState); + } - function callAPI() { - return Restangular.all('sensors').getList({'per_page': 1000}); } - function setTypes(sensorTypes) { - sensorTypes = sensorTypes; - } + function startingTab() { + /* This looks more like a hack but we need to workout how to properly use md-tab with ui-router */ - function getTypes() { - return sensorTypes; - } + var childState = $state.current.name.split('.').pop(); - function getSensorsData(deviceID, sensorID, dateFrom, dateTo) { - var rollup = sensorUtils.getRollup(dateFrom, dateTo); - dateFrom = timeUtils.convertTime(dateFrom); - dateTo = timeUtils.convertTime(dateTo); + switch(childState) { + case 'user': + vm.startingTab = 1; + break; + default: + vm.startingTab = 0; + break; + } - return Restangular.one('devices', deviceID).customGET('readings', {'from': dateFrom, 'to': dateTo, 'rollup': rollup, 'sensor_id': sensorID, 'all_intervals': true}); } - } -})(); -(function() { - 'use strict'; + function highlightIcon(iconIndex) { - angular.module('app.components') - .factory('search', search); - - search.$inject = ['$http', 'Restangular']; - function search($http, Restangular) { - var service = { - globalSearch: globalSearch - }; + var icons = angular.element('.myProfile_tab_icon'); - return service; + _.each(icons, function(icon) { + unhighlightIcon(icon); + }); - ///////////////////////// + var icon = icons[iconIndex]; - function globalSearch(query) { - return Restangular.all('search').getList({q: query}); + angular.element(icon).find('.stroke_container').css({'stroke': 'white', 'stroke-width': '0.01px'}); + angular.element(icon).find('.fill_container').css('fill', 'white'); } - } -})(); - -(function() { - 'use strict'; - - angular.module('app.components') - .factory('measurement', measurement); - measurement.$inject = ['Restangular']; + function unhighlightIcon(icon) { + icon = angular.element(icon); - function measurement(Restangular) { + icon.find('.stroke_container').css({'stroke': 'none'}); + icon.find('.fill_container').css('fill', '#FF8600'); + } - var service = { - getTypes: getTypes, - getMeasurement: getMeasurement + function setSidebarMinHeight() { + var height = document.body.clientHeight / 4 * 3; + angular.element('.profile_content').css('min-height', height + 'px'); + } - }; - return service; + function getCountries(searchText) { + return _.filter(COUNTRY_CODES, createFilter(searchText)); + } - //////////////// + function createFilter(searchText) { + searchText = searchText.toLowerCase(); + return function(country) { + country = country.toLowerCase(); + return country.indexOf(searchText) !== -1; + }; + } + function uploadAvatar(fileData) { + if(fileData && fileData.length) { - function getTypes() { - return Restangular.all('measurements').getList({'per_page': 1000}); - } + // TODO: Improvement Is there a simpler way to patch the image to the API and use the response? + // Something like: + //Restangular.all('/me').patch(data); + // Instead of doing it manually like here: + var fd = new FormData(); + fd.append('profile_picture', fileData[0]); + Restangular.one('/me') + .withHttpConfig({transformRequest: angular.identity}) + .customPATCH(fd, '', undefined, {'Content-Type': undefined}) + .then(function(resp){ + vm.user.profile_picture = resp.profile_picture; + }) + } + } - function getMeasurement(mesID) { + function copyUserToForm(formData, userData) { + var props = {username: true, email: true, city: true, country: true, country_code: true, url: true, constructor: false}; - return Restangular.one('measurements', mesID).get(); - } - } -})(); -(function() { - 'use strict'; + for(var key in userData) { + if(props[key]) { + formData[key] = userData[key]; + } + } + } - angular.module('app.components') - .factory('geolocation', geolocation); + function mapWithBelongstoUser(devices){ + _.map(devices, addBelongProperty); + } - geolocation.$inject = ['$http', '$window']; - function geolocation($http, $window) { + function addBelongProperty(device){ + device.belongProperty = deviceBelongsToUser(device); + return device; + } - var service = { - grantHTML5Geolocation: grantHTML5Geolocation, - isHTML5GeolocationGranted: isHTML5GeolocationGranted - }; - return service; - /////////////////////////// + function deviceBelongsToUser(device){ + if(!auth.isAuth() || !device || !device.id) { + return false; + } + var deviceID = parseInt(device.id); + var userData = ( auth.getCurrentUser().data ) || + ($window.localStorage.getItem('smartcitizen.data') && + new AuthUser( JSON.parse( + $window.localStorage.getItem('smartcitizen.data') ))); + var belongsToUser = deviceUtils.belongsToUser(userData.devices, deviceID); + var isAdmin = userUtils.isAdmin(userData); - function grantHTML5Geolocation(){ - $window.localStorage.setItem('smartcitizen.geolocation_granted', true); - } + return isAdmin || belongsToUser; + } - function isHTML5GeolocationGranted(){ - return $window.localStorage - .getItem('smartcitizen.geolocation_granted'); - } - } -})(); - -(function() { - 'use strict'; + function downloadData(device){ + $mdDialog.show({ + hasBackdrop: true, + controller: 'DownloadModalController', + controllerAs: 'vm', + templateUrl: 'app/components/download/downloadModal.html', + clickOutsideToClose: true, + locals: {thisDevice:device} + }).then(function(){ + var alert = $mdDialog.alert() + .title('SUCCESS') + .textContent('We are processing your data. Soon you will be notified in your inbox') + .ariaLabel('') + .ok('OK!') + .theme('primary') + .clickOutsideToClose(true); - angular.module('app.components') - .factory('file', file); + $mdDialog.show(alert); + }).catch(function(err){ + if (!err){ + return; + } + var errorAlert = $mdDialog.alert() + .title('ERROR') + .textContent('Uh-oh, something went wrong') + .ariaLabel('') + .ok('D\'oh') + .theme('primary') + .clickOutsideToClose(false); - file.$inject = ['Restangular', 'Upload']; - function file(Restangular, Upload) { - var service = { - getCredentials: getCredentials, - uploadFile: uploadFile, - getImageURL: getImageURL - }; - return service; + $mdDialog.show(errorAlert); + }); + } - /////////////// + function removeDevice(deviceID) { + var confirm = $mdDialog.confirm() + .title('Delete this kit?') + .textContent('Are you sure you want to delete this kit?') + .ariaLabel('') + .ok('DELETE') + .cancel('Cancel') + .theme('primary') + .clickOutsideToClose(true); - function getCredentials(filename) { - var data = { - filename: filename - }; - return Restangular.all('me/avatar').post(data); + $mdDialog + .show(confirm) + .then(function(){ + device + .removeDevice(deviceID) + .then(function(){ + alert.success('Your kit was deleted successfully'); + device.updateContext(); + }) + .catch(function(){ + alert.error('Error trying to delete your kit.'); + }); + }); } - function uploadFile(fileData, key, policy, signature) { - return Upload.upload({ - url: 'https://smartcitizen.s3-eu-west-1.amazonaws.com', - method: 'POST', - data: { - key: key, - policy: policy, - signature: signature, - AWSAccessKeyId: 'AKIAJ753OQI6JPSDCPHA', - acl: 'public-read', - "Content-Type": fileData.type || 'application/octet-stream', - /*jshint camelcase: false */ - success_action_status: 200, - file: fileData - } + $scope.addDeviceSelector = addDeviceSelector; + function addDeviceSelector(){ + $mdDialog.show({ + templateUrl: 'app/components/myProfile/addDeviceSelectorModal.html', + clickOutsideToClose: true, + multiple: true, + controller: DialogController, }); } - function getImageURL(filename, size) { - size = size === undefined ? 's101' : size; + function DialogController($scope, $mdDialog){ + $scope.cancel = function(){ + $mdDialog.cancel(); + }; + } - return 'https://images.smartcitizen.me/' + size + '/' + filename; + function addNewDevice() { + var confirm = $mdDialog.confirm() + .title('Hey! Do you want to add a new kit?') + .textContent('Please, notice this currently supports just the SCK 1.0 and SCK 1.1') + .ariaLabel('') + .ok('Ok') + .cancel('Cancel') + .theme('primary') + .clickOutsideToClose(true); + + $mdDialog + .show(confirm) + .then(function(){ + $state.go('layout.kitAdd'); + }); } + + } })(); (function() { - 'use strict'; + 'use strict'; - angular.module('app.components') - .factory('device', device); + angular.module('app.components') + .controller('MapTagModalController', MapTagModalController); - device.$inject = ['Restangular', '$window', 'timeUtils','$http', 'auth', '$rootScope']; - function device(Restangular, $window, timeUtils, $http, auth, $rootScope) { - var worldMarkers; + MapTagModalController.$inject = ['$mdDialog', 'tag', 'selectedTags']; - initialize(); + function MapTagModalController($mdDialog, tag, selectedTags) { - var service = { - getDevices: getDevices, - getAllDevices: getAllDevices, - getDevice: getDevice, - createDevice: createDevice, - updateDevice: updateDevice, - getWorldMarkers: getWorldMarkers, - setWorldMarkers: setWorldMarkers, - mailReadings: mailReadings, - postReadings: postReadings, - removeDevice: removeDevice, - updateContext: updateContext - }; + var vm = this; - return service; + vm.checks = {}; - ////////////////////////// + vm.answer = answer; + vm.hide = hide; + vm.clear = clear; + vm.cancel = cancel; + vm.tags = []; - function initialize() { - if(areMarkersOld()) { - removeMarkers(); - } - } + init(); - function getDevices(location) { - var parameter = ''; - parameter += location.lat + ',' + location.lng; - return Restangular.all('devices').getList({near: parameter, 'per_page': '100'}); - } + //////////////////////////////////////////////////////// - function getAllDevices(forceReload) { - if (forceReload || auth.isAuth()) { - return getAllDevicesNoCached(); - } else { - return getAllDevicesCached(); - } - } + function init() { + tag.getTags() + .then(function(tags) { + vm.tags = tags; - function getAllDevicesCached() { - return Restangular.all('devices/world_map') - .getList() - .then(function(fetchedDevices){ - return fetchedDevices.plain(); - }); - } + _.forEach(selectedTags, select); - function getAllDevicesNoCached() { - return Restangular.all('devices/fresh_world_map') - .getList() - .then(function(fetchedDevices){ - return fetchedDevices.plain(); }); - } + } - function getDevice(id) { - return Restangular.one('devices', id).get(); - } + function answer() { - function createDevice(data) { - return Restangular.all('devices').post(data); - } + var selectedTags = _(vm.tags) + .filter(isTagSelected) + .value(); + $mdDialog.hide(selectedTags); + } - function updateDevice(id, data) { - return Restangular.one('devices', id).patch(data); - } + function hide() { + answer(); + } - function getWorldMarkers() { - return worldMarkers || ($window.localStorage.getItem('smartcitizen.markers') && JSON.parse($window.localStorage.getItem('smartcitizen.markers') ).data); - } + function clear() { + $mdDialog.hide(null); + } - function setWorldMarkers(data) { - var obj = { - timestamp: new Date(), - data: data - }; - try { - $window.localStorage.setItem('smartcitizen.markers', JSON.stringify(obj) ); - } catch (e) { - console.log("Could not store markers in localstorage. skipping..."); - } - worldMarkers = obj.data; - } + function cancel() { + answer(); + } - function getTimeStamp() { - return ($window.localStorage.getItem('smartcitizen.markers') && - JSON.parse($window.localStorage - .getItem('smartcitizen.markers') ).timestamp); - } + function isTagSelected(tag) { + return vm.checks[tag.name]; + } - function areMarkersOld() { - var markersDate = getTimeStamp(); - return !timeUtils.isWithin(1, 'minutes', markersDate); - } - - function removeMarkers() { - worldMarkers = null; - $window.localStorage.removeItem('smartcitizen.markers'); - } - - function mailReadings(kit) { - return Restangular - .one('devices', kit.id) - .customGET('readings/csv_archive'); - } - - function postReadings(kit, readings) { - return Restangular - .one('devices', kit.id) - .post('readings', readings); - } - - function removeDevice(deviceID){ - return Restangular - .one('devices', deviceID) - .remove().then(function () { - $rootScope.$broadcast('devicesContextUpdated'); - }) - ; - } - - function updateContext (){ - return auth.updateUser().then(function(){ - removeMarkers(); - $rootScope.$broadcast('devicesContextUpdated'); - }); - } - - } -})(); + function select(tag){ + vm.checks[tag] = true; + } + } +})(); (function() { 'use strict'; angular.module('app.components') - .factory('auth', auth); - - auth.$inject = ['$location', '$window', '$state', 'Restangular', - '$rootScope', 'AuthUser', '$timeout', 'alert', '$cookies']; - function auth($location, $window, $state, Restangular, $rootScope, AuthUser, - $timeout, alert, $cookies) { - - var user = {}; + .controller('MapFilterModalController', MapFilterModalController); - //wait until http interceptor is added to Restangular - $timeout(function() { - initialize(); - }, 100); + MapFilterModalController.$inject = ['$mdDialog','selectedFilters', '$timeout']; - var service = { - isAuth: isAuth, - setCurrentUser: setCurrentUser, - getCurrentUser: getCurrentUser, - updateUser: updateUser, - saveToken: saveToken, - getToken: getToken, - login: login, - logout: logout, - recoverPassword: recoverPassword, - getResetPassword: getResetPassword, - patchResetPassword: patchResetPassword, - isAdmin: isAdmin - }; - return service; + function MapFilterModalController($mdDialog, selectedFilters, $timeout) { - ////////////////////////// + var vm = this; - function initialize() { - //console.log('---- AUTH INIT -----'); - setCurrentUser('appLoad'); - } + vm.checks = {}; - //run on app initialization so that we can keep auth across different sessions - // 1. Check if token in cookie exists. Return if it doesn't, user needs to login (and save a token to the cookie) - // 2. Populate user.data with the response from the API. - // 3. Broadcast logged in - function setCurrentUser(time) { - // TODO later: Should we check if token is expired here? - if (getToken()) { - user.token = getToken(); - }else{ - //console.log('token not found in cookie, returning'); - return; - } + vm.answer = answer; + vm.hide = hide; + vm.clear = clear; + vm.cancel = cancel; + vm.toggle = toggle; - return getCurrentUserFromAPI() - .then(function(data) { - // Save user.data also in localStorage. It is beeing used across the app. - // Should it instead just be saved in the user object? Or is it OK to also have it in localStorage? - $window.localStorage.setItem('smartcitizen.data', JSON.stringify(data.plain()) ); + vm.location = ['indoor', 'outdoor']; + vm.status = ['online', 'offline']; + vm.new = ['new']; - var newUser = new AuthUser(data); - //check sensitive information - if(user.data && user.data.role !== newUser.role) { - user.data = newUser; - $location.path('/'); - } - user.data = newUser; + vm.filters = []; - //console.log('-- User populated with data: ', user) - // Broadcast happens 2x, so the user wont think he is not logged in. - // The 2nd broadcast waits 3sec, because f.x. on the /kits/ page, the layout has not loaded when the broadcast is sent - $rootScope.$broadcast('loggedIn'); + init(); - // used for app initialization - if(time && time === 'appLoad') { - //wait until navbar is loaded to emit event - $timeout(function() { - $rootScope.$broadcast('loggedIn', {time: 'appLoad'}); - }, 3000); - } else { - // used for login - //$state.reload(); - $timeout(function() { - alert.success('Login was successful'); - $rootScope.$broadcast('loggedIn', {}); - }, 2000); - } - }); - } + //////////////////////////////////////////////////////// - // Called from device.service.js updateContext(), which is called from multiple /kit/ pages - function updateUser() { - return getCurrentUserFromAPI() - .then(function(data) { - // TODO: Should this update the token or user.data? Then it could instead call setCurrentUser? - $window.localStorage.setItem('smartcitizen.data', JSON.stringify(data.plain()) ); - return getCurrentUser(); - }); - } + function init() { + _.forEach(selectedFilters, select); + } - function getCurrentUser() { - user.token = getToken(); - user.data = $window.localStorage.getItem('smartcitizen.data') && new AuthUser(JSON.parse( $window.localStorage.getItem('smartcitizen.data') )); - return user; - } + function answer() { + vm.filters = vm.filters.concat(vm.location, vm.status, vm.new); + var selectedFilters = _(vm.filters) + .filter(isFilterSelected) + .value(); + $mdDialog.hide(selectedFilters); + } - // Should check if user.token exists - but now checks if the cookies.token exists. - function isAuth() { - // TODO: isAuth() is called from many different services BEFORE auth.init has run. - // That means that the user.token is EMPTY, meaning isAuth will be false - // We can cheat and just check the cookie, but we should NOT. Because auth.init should also check if the cookie is valid / expired - // Ideally it should return !!user.token - //return !!user.token; - return !!getToken(); - } + function hide() { + answer(); + } - // LoginModal calls this after it receives the token from the API, and wants to save it in a cookie. - function saveToken(token) { - //console.log('saving Token to cookie:', token); - $cookies.put('smartcitizen.token', token); - setCurrentUser(); - } + function clear() { + vm.filters = vm.filters.concat(vm.location, vm.status, vm.new); + $mdDialog.hide(vm.filters); + } - function getToken(){ - return $cookies.get('smartcitizen.token'); - } + function cancel() { + answer(); + } - function login(loginData) { - return Restangular.all('sessions').post(loginData); - } + function isFilterSelected(filter) { + return vm.checks[filter]; + } - function logout() { - $cookies.remove('smartcitizen.token'); - } + function toggle(filters) { + $timeout(function() { - function getCurrentUserFromAPI() { - return Restangular.all('').customGET('me'); - } + for (var i = 0; i < filters.length - 1; i++) { + if (vm.checks[filters[i]] === false && vm.checks[filters[i]] === vm.checks[filters[i+1]]) { + for (var n = 0; n < filters.length; n++) { + vm.checks[filters[n]] = true; + } + } + } - function recoverPassword(data) { - return Restangular.all('password_resets').post(data); - } + }); + } - function getResetPassword(code) { - return Restangular.one('password_resets', code).get(); - } - function patchResetPassword(code, data) { - return Restangular.one('password_resets', code).patch(data); - } - function isAdmin(userData) { - return userData.role === 'admin'; - } + function select(filter){ + vm.checks[filter] = true; } + } })(); (function() { 'use strict'; - /** - * Unused directive. Double-check before removing. - * - */ angular.module('app.components') - .directive('slide', slide) - .directive('slideMenu', slideMenu); + .controller('MapController', MapController); - function slideMenu() { - return { - controller: controller, - link: link - }; + MapController.$inject = ['$scope', '$state', '$stateParams', '$timeout', 'device', + '$mdDialog', 'leafletData', 'alert', + 'Marker', 'tag', 'animation', '$q']; + function MapController($scope, $state, $stateParams, $timeout, device, + $mdDialog, leafletData, alert, Marker, tag, animation, $q) { + var vm = this; + var updateType; + var focusedMarkerID; - function link(scope, element) { - scope.element = element; - } + vm.markers = []; - function controller($scope) { - $scope.slidePosition = 0; - $scope.slideSize = 20; + var retinaSuffix = isRetina() ? '512' : '256'; + var retinaLegacySuffix = isRetina() ? '@2x' : ''; - this.getTimesSlided = function() { - return $scope.slideSize; - }; - this.getPosition = function() { - return $scope.slidePosition * $scope.slideSize; - }; - this.decrementPosition = function() { - $scope.slidePosition -= 1; - }; - this.incrementPosition = function() { - $scope.slidePosition += 1; - }; - this.scrollIsValid = function(direction) { - var scrollPosition = $scope.element.scrollLeft(); - console.log('scrollpos', scrollPosition); - if(direction === 'left') { - return scrollPosition > 0 && $scope.slidePosition >= 0; - } else if(direction === 'right') { - return scrollPosition < 300; - } - }; - } - } + var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; - slide.$inject = []; - function slide() { - return { - link: link, - require: '^slide-menu', - restrict: 'A', - scope: { - direction: '@' + vm.layers = { + baselayers: { + osm: { + name: 'OpenStreetMap', + type: 'xyz', + url: 'https://api.mapbox.com/styles/v1/mapbox/streets-v10/tiles/' + retinaSuffix + '/{z}/{x}/{y}?access_token=' + mapBoxToken + }, + legacy: { + name: 'Legacy', + type: 'xyz', + url: 'https://api.tiles.mapbox.com/v4/mapbox.streets-basic/{z}/{x}/{y}'+ retinaLegacySuffix +'.png' + '?access_token=' + mapBoxToken + }, + sat: { + name: 'Satellite', + type: 'xyz', + url: 'https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v10/tiles/' + retinaSuffix + '/{z}/{x}/{y}?access_token=' + mapBoxToken + } + }, + overlays: { + devices: { + name: 'Devices', + type: 'markercluster', + visible: true, + layerOptions: { + showCoverageOnHover: false + } + } } }; - function link(scope, element, attr, slideMenuCtrl) { - //select first sensor container - var sensorsContainer = angular.element('.sensors_container'); - - element.on('click', function() { - - if(slideMenuCtrl.scrollIsValid('left') && attr.direction === 'left') { - slideMenuCtrl.decrementPosition(); - sensorsContainer.scrollLeft(slideMenuCtrl.getPosition()); - console.log(slideMenuCtrl.getPosition()); - } else if(slideMenuCtrl.scrollIsValid('right') && attr.direction === 'right') { - slideMenuCtrl.incrementPosition(); - sensorsContainer.scrollLeft(slideMenuCtrl.getPosition()); - console.log(slideMenuCtrl.getPosition()); - } - }); - } - } -})(); - -(function() { - 'use strict'; + vm.center = { + lat: $stateParams.lat ? parseInt($stateParams.lat, 10) : 13.14950321154457, + lng: $stateParams.lng ? parseInt($stateParams.lng, 10) : -1.58203125, + zoom: $stateParams.zoom ? parseInt($stateParams.zoom, 10) : 2 + }; - angular.module('app.components') - .directive('showPopupInfo', showPopupInfo); + vm.defaults = { + dragging: true, + touchZoom: true, + scrollWheelZoom: true, + doubleClickZoom: true, + minZoom:2, + worldCopyJump: true + }; - /** - * Used to show/hide explanation of sensor value at kit dashboard - * - */ - showPopupInfo.$inject = []; - function showPopupInfo() { - return { - link: link + vm.events = { + map: { + enable: ['dragend', 'zoomend', 'moveend', 'popupopen', 'popupclose', + 'mousedown', 'dblclick', 'click', 'touchstart', 'mouseup'], + logic: 'broadcast' + } }; - ////// + $scope.$on('leafletDirectiveMarker.click', function(event, data) { + var id = undefined; + var currentMarker = vm.markers[data.modelName]; + if(currentMarker) { + id = currentMarker.myData.id; + } - function link(scope, elem) { - elem.on('mouseenter', function() { - angular.element('.sensor_data_description').css('display', 'inline-block'); - }); - elem.on('mouseleave', function() { - angular.element('.sensor_data_description').css('display', 'none'); - }); - } - } -})(); + vm.deviceLoading = true; + vm.center.lat = data.leafletEvent.latlng.lat; + vm.center.lng = data.leafletEvent.latlng.lng; -(function() { - 'use strict'; + if(id === parseInt($state.params.id)) { + $timeout(function() { + vm.deviceLoading = false; + }); + return; + } - angular.module('app.components') - .directive('showPopup', showPopup); + updateType = 'map'; - /** - * Used on kit dashboard to open full sensor description - */ + if ($state.$current.name === 'embbed') { return; } + $state.go('layout.home.kit', {id: id}); - showPopup.$inject = []; - function showPopup() { - return { - link: link - }; + // angular.element('section.map').scope().$broadcast('resizeMapHeight'); + }); - ///// - function link(scope, element) { - element.on('click', function() { - var text = angular.element('.sensor_description_preview').text(); - if(text.length < 140) { - return; + $scope.$on('leafletDirectiveMarker.popupclose', function() { + if(focusedMarkerID) { + var marker = vm.markers[focusedMarkerID]; + if(marker) { + vm.markers[focusedMarkerID].focus = false; } - angular.element('.sensor_description_preview').hide(); - angular.element('.sensor_description_full').show(); - }); - } - } -})(); + } + }); -(function() { - 'use strict'; + vm.readyForDevice = { + device: false, + map: false + }; - angular.module('app.components') - .directive('moveFilters', moveFilters); + $scope.$on('deviceLoaded', function(event, data) { + vm.readyForDevice.device = data; + }); - /** - * Moves map filters when scrolling - * - */ - moveFilters.$inject = ['$window', '$timeout']; - function moveFilters($window, $timeout) { - return { - link: link - }; + $scope.$watch('vm.readyForDevice', function() { + if (vm.readyForDevice.device && vm.readyForDevice.map) { + zoomDeviceAndPopUp(vm.readyForDevice.device); + } + }, true); - function link() { - var chartHeight; - $timeout(function() { - chartHeight = angular.element('.kit_chart').height(); - }, 1000); + $scope.$on('goToLocation', function(event, data) { + goToLocation(data); + }); - /* - angular.element($window).on('scroll', function() { - var windowPosition = document.body.scrollTop; - if(chartHeight > windowPosition) { - elem.css('bottom', 12 + windowPosition + 'px'); - } - }); - */ - } - } -})(); + vm.filters = ['indoor', 'outdoor', 'online', 'offline']; -(function() { - 'use strict'; + vm.openFilterPopup = openFilterPopup; + vm.openTagPopup = openTagPopup; + vm.removeFilter = removeFilter; + vm.removeTag = removeTag; + vm.selectedTags = tag.getSelectedTags(); + vm.selectedFilters = ['indoor', 'outdoor', 'online', 'offline', 'new']; - angular.module('app.components') - .factory('layout', layout); + vm.checkAllFiltersSelected = checkAllFiltersSelected; + initialize(); - function layout() { + ///////////////////// - var kitHeight; + function initialize() { - var service = { - setKit: setKit, - getKit: getKit - }; - return service; + vm.readyForDevice.map = false; - function setKit(height) { - kitHeight = height; - } + $q.all([device.getAllDevices($stateParams.reloadMap)]) + .then(function(data){ - function getKit() { - return kitHeight; - } - } -})(); + data = data[0]; -(function() { - 'use strict'; + vm.markers = _.chain(data) + .map(function(device) { + return new Marker(device); + }) + .filter(function(marker) { + return !!marker.lng && !!marker.lat; + }) + .tap(function(data) { + device.setWorldMarkers(data); + }) + .value(); - angular.module('app.components') - .directive('horizontalScroll', horizontalScroll); - - /** - * Used to highlight and unhighlight buttons on the kit dashboard when scrolling horizontally - * - */ - horizontalScroll.$inject = ['$window', '$timeout']; - function horizontalScroll($window, $timeout) { - return { - link: link, - restrict: 'A' - }; - - /////////////////// + var markersByIndex = _.keyBy(vm.markers, function(marker) { + return marker.myData.id; + }); + if($state.params.id && markersByIndex[parseInt($state.params.id)]){ + focusedMarkerID = markersByIndex[parseInt($state.params.id)] + .myData.id; + vm.readyForDevice.map = true; + } else { + updateMarkers(); + vm.readyForDevice.map = true; + } - function link(scope, element) { + }); + } - element.on('scroll', function() { - // horizontal scroll position - var position = angular.element(this).scrollLeft(); - // real width of element - var scrollWidth = this.scrollWidth; - // visible width of element - var width = angular.element(this).width(); + function zoomDeviceAndPopUp(data){ - // if you cannot scroll, unhighlight both - if(scrollWidth === width) { - angular.element('.button_scroll_left').css('opacity', '0.5'); - angular.element('.button_scroll_right').css('opacity', '0.5'); - } - // if scroll is in the middle, highlight both - if(scrollWidth - width > 2) { - angular.element('.button_scroll_left').css('opacity', '1'); - angular.element('.button_scroll_right').css('opacity', '1'); - } - // if scroll is at the far right, unhighligh right button - if(scrollWidth - width - position <= 2) { - angular.element('.button_scroll_right').css('opacity', '0.5'); + if(updateType === 'map') { + vm.deviceLoading = false; + updateType = undefined; return; + } else { + vm.deviceLoading = true; } - // if scroll is at the far left, unhighligh left button - if(position === 0) { - angular.element('.button_scroll_left').css('opacity', '0.5'); - return; - } - //set opacity back to normal otherwise - angular.element('.button_scroll_left').css('opacity', '1'); - angular.element('.button_scroll_right').css('opacity', '1'); - }); + leafletData.getMarkers() + .then(function(markers) { + var currentMarker = _.find(markers, function(marker) { + return data.id === marker.options.myData.id; + }); - $timeout(function() { - element.trigger('scroll'); - }); + var id = data.id; - angular.element($window).on('resize', function() { - $timeout(function() { - element.trigger('scroll'); - }, 1000); - }); - } - } -})(); + leafletData.getLayers() + .then(function(layers) { + if(currentMarker){ + layers.overlays.devices.zoomToShowLayer(currentMarker, + function() { + var selectedMarker = currentMarker; + if(selectedMarker) { + // Ensures the marker is not just zoomed but the marker is centered to improve UX + // The $timeout can be replaced by an event but tests didn't show good results + $timeout(function() { + vm.center.lat = selectedMarker.options.lat; + vm.center.lng = selectedMarker.options.lng; + selectedMarker.openPopup(); + vm.deviceLoading = false; + }, 1000); + } + }); + } else { + leafletData.getMap().then(function(map){ + map.closePopup(); + }); + } + }); + }); -(function() { - 'use strict'; + } - angular.module('app.components') - .directive('hidePopup', hidePopup); + function checkAllFiltersSelected() { + var allFiltersSelected = _.every(vm.filters, function(filterValue) { + return _.includes(vm.selectedFilters, filterValue); + }); + return allFiltersSelected; + } - /** - * Used on kit dashboard to hide popup with full sensor description - * - */ - - hidePopup.$inject = []; - function hidePopup() { - return { - link: link - }; + function openFilterPopup() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'MapFilterModalController', + controllerAs: 'vm', + templateUrl: 'app/components/map/mapFilterModal.html', + clickOutsideToClose: true, + locals: { + selectedFilters: vm.selectedFilters + } + }) + .then(function(selectedFilters) { + updateType = 'map'; + vm.selectedFilters = selectedFilters; + updateMapFilters(); + }); + } - ///////////// + function openTagPopup() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'MapTagModalController', + controllerAs: 'vm', + templateUrl: 'app/components/map/mapTagModal.html', + //targetEvent: ev, + clickOutsideToClose: true, + locals: { + selectedTags: vm.selectedTags + } + }) + .then(function(selectedTags) { + if (selectedTags && selectedTags.length > 0) { + updateType = 'map'; + tag.setSelectedTags(_.map(selectedTags, 'name')); + vm.selectedTags = tag.getSelectedTags(); + reloadWithTags(); + } else if (selectedTags === null) { + reloadNoTags(); + } + }); + } - function link(scope, elem) { - elem.on('mouseleave', function() { - angular.element('.sensor_description_preview').show(); - angular.element('.sensor_description_full').hide(); + function updateMapFilters(){ + vm.selectedTags = tag.getSelectedTags(); + checkAllFiltersSelected(); + updateMarkers(); + } + + function removeFilter(filterName) { + vm.selectedFilters = _.filter(vm.selectedFilters, function(el){ + return el !== filterName; }); + if(vm.selectedFilters.length === 0){ + vm.selectedFilters = vm.filters; + } + updateMarkers(); } - } -})(); -(function() { - 'use strict'; + function filterMarkersByLabel(tmpMarkers) { + return tmpMarkers.filter(function(marker) { + var labels = marker.myData.labels; + if (labels.length === 0 && vm.selectedFilters.length !== 0){ + return false; + } + return _.every(labels, function(label) { + return _.includes(vm.selectedFilters, label); + }); + }); + } - angular.module('app.components') - .directive('disableScroll', disableScroll); + function updateMarkers() { + $timeout(function() { + $scope.$apply(function() { + var allMarkers = device.getWorldMarkers(); - disableScroll.$inject = ['$timeout']; - function disableScroll($timeout) { - return { - // link: { - // pre: link - // }, - compile: link, - restrict: 'A', - priority: 100000 - }; + var updatedMarkers = allMarkers; + updatedMarkers = tag.filterMarkersByTag(updatedMarkers); + updatedMarkers = filterMarkersByLabel(updatedMarkers); + vm.markers = updatedMarkers; - ////////////////////// + animation.mapStateLoaded(); - function link(elem) { - console.log('i', elem); - // var select = elem.find('md-select'); - // angular.element(select).on('click', function() { - elem.on('click', function() { - console.log('e'); - angular.element(document.body).css('overflow', 'hidden'); - $timeout(function() { - angular.element(document.body).css('overflow', 'initial'); + vm.deviceLoading = false; + + zoomOnMarkers(); }); }); } - } -})(); - -(function() { - 'use strict'; - - angular.module('app.components') - .factory('animation', animation); - - /** - * Used to emit events from rootscope. - * - * This events are then listened by $scope on controllers and directives that care about that particular event - */ - animation.$inject = ['$rootScope']; - function animation($rootScope) { + function getZoomLevel(data) { + // data.layer is an array of strings like ["establishment", "point_of_interest"] + var zoom = 18; - var service = { - blur: blur, - unblur: unblur, - removeNav: removeNav, - addNav: addNav, - showChartSpinner: showChartSpinner, - hideChartSpinner: hideChartSpinner, - deviceLoaded: deviceLoaded, - showPasswordRecovery: showPasswordRecovery, - showLogin: showLogin, - showSignup: showSignup, - showPasswordReset: showPasswordReset, - hideAlert: hideAlert, - viewLoading: viewLoading, - viewLoaded: viewLoaded, - deviceWithoutData: deviceWithoutData, - deviceIsPrivate: deviceIsPrivate, - goToLocation: goToLocation, - mapStateLoading: mapStateLoading, - mapStateLoaded: mapStateLoaded - }; - return service; - - ////////////// + if(data.layer && data.layer[0]) { + switch(data.layer[0]) { + case 'point_of_interest': + zoom = 18; + break; + case 'address': + zoom = 18; + break; + case "establishment": + zoom = 15; + break; + case 'neighbourhood': + zoom = 13; + break; + case 'locality': + zoom = 13; + break; + case 'localadmin': + zoom = 9; + break; + case 'county': + zoom = 9; + break; + case 'region': + zoom = 8; + break; + case 'country': + zoom = 7; + break; + case 'coarse': + zoom = 7; + break; + } + } - function blur() { - $rootScope.$broadcast('blur'); - } - function unblur() { - $rootScope.$broadcast('unblur'); - } - function removeNav() { - $rootScope.$broadcast('removeNav'); - } - function addNav() { - $rootScope.$broadcast('addNav'); - } - function showChartSpinner() { - $rootScope.$broadcast('showChartSpinner'); - } - function hideChartSpinner() { - $rootScope.$broadcast('hideChartSpinner'); - } - function deviceLoaded(data) { - $rootScope.$broadcast('deviceLoaded', data); - } - function showPasswordRecovery() { - $rootScope.$broadcast('showPasswordRecovery'); - } - function showLogin() { - $rootScope.$broadcast('showLogin'); - } - function showSignup() { - $rootScope.$broadcast('showSignup'); - } - function showPasswordReset() { - $rootScope.$broadcast('showPasswordReset'); - } - function hideAlert() { - $rootScope.$broadcast('hideAlert'); - } - function viewLoading() { - $rootScope.$broadcast('viewLoading'); + return zoom; } - function viewLoaded() { - $rootScope.$broadcast('viewLoaded'); + + function isRetina(){ + return ((window.matchMedia && + (window.matchMedia('only screen and (min-resolution: 192dpi), ' + + 'only screen and (min-resolution: 2dppx), only screen and ' + + '(min-resolution: 75.6dpcm)').matches || + window.matchMedia('only screen and (-webkit-min-device-pixel-ra' + + 'tio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only' + + ' screen and (min--moz-device-pixel-ratio: 2), only screen and ' + + '(min-device-pixel-ratio: 2)').matches)) || + (window.devicePixelRatio && window.devicePixelRatio >= 2)) && + /(iPad|iPhone|iPod|Apple)/g.test(navigator.userAgent); } - function deviceWithoutData(data) { - $rootScope.$broadcast('deviceWithoutData', data); + + function goToLocation(data){ + // This ensures the action runs after the event is registered + $timeout(function() { + vm.center.lat = data.lat; + vm.center.lng = data.lng; + vm.center.zoom = getZoomLevel(data); + }); } - function deviceIsPrivate(data) { - $rootScope.$broadcast('deviceIsPrivate', data); + + function removeTag(tagName){ + tag.setSelectedTags(_.filter(vm.selectedTags, function(el){ + return el !== tagName; + })); + + vm.selectedTags = tag.getSelectedTags(); + + if(vm.selectedTags.length === 0){ + reloadNoTags(); + } else { + reloadWithTags(); + } + } - function goToLocation(data) { - $rootScope.$broadcast('goToLocation', data); + + function zoomOnMarkers(){ + $timeout(function() { + if(vm.markers && vm.markers.length > 0) { + leafletData.getMap().then(function(map){ + var bounds = L.latLngBounds(vm.markers); + map.fitBounds(bounds); + }); + } else { + alert.error('No markers found with those filters', 5000); + } + }); } - function mapStateLoading() { - $rootScope.$broadcast('mapStateLoading'); + + function reloadWithTags(){ + $state.transitionTo('layout.home.tags', {tags: vm.selectedTags}, {reload: true}); } - function mapStateLoaded() { - $rootScope.$broadcast('mapStateLoaded'); + + function reloadNoTags(){ + $state.transitionTo('layout.home.kit'); } + } + })(); (function() { 'use strict'; - /** - * TODO: Improvement These directives can be split up each one in a different file - */ + angular.module('app.components') + .controller('LoginModalController', LoginModalController); - angular.module('app.components') - .directive('moveDown', moveDown) - .directive('stick', stick) - .directive('blur', blur) - .directive('focus', focus) - .directive('changeMapHeight', changeMapHeight) - .directive('changeContentMargin', changeContentMargin) - .directive('focusInput', focusInput); + LoginModalController.$inject = ['$scope', '$mdDialog', 'auth', 'animation']; + function LoginModalController($scope, $mdDialog, auth, animation) { + const vm = this; + $scope.answer = function(answer) { + $scope.waitingFromServer = true; + auth.login(answer) + .then(function(data) { + /*jshint camelcase: false */ + var token = data.access_token; + auth.saveToken(token); + $mdDialog.hide(); + }) + .catch(function(err) { + vm.errors = err.data; + }) + .finally(function() { + $scope.waitingFromServer = false; + }); + }; + $scope.hide = function() { + $mdDialog.hide(); + }; + $scope.cancel = function() { + $mdDialog.hide(); + }; - /** - * It moves down kit section to ease the transition after the kit menu is sticked to the top - * - */ - moveDown.$inject = []; - function moveDown() { + $scope.openSignup = function() { + animation.showSignup(); + $mdDialog.hide(); + }; - function link(scope, element) { - scope.$watch('moveDown', function(isTrue) { - if(isTrue) { - element.addClass('move_down'); - } else { - element.removeClass('move_down'); - } + $scope.openPasswordRecovery = function() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'PasswordRecoveryModalController', + templateUrl: 'app/components/passwordRecovery/passwordRecoveryModal.html', + clickOutsideToClose: true }); - } - return { - link: link, - scope: false, - restrict: 'A' + $mdDialog.hide(); }; } +})(); - /** - * It sticks kit menu when kit menu touchs navbar on scrolling - * - */ - stick.$inject = ['$window', '$timeout']; - function stick($window, $timeout) { - function link(scope, element) { - var elementPosition = element[0].offsetTop; - //var elementHeight = element[0].offsetHeight; - var navbarHeight = angular.element('.stickNav').height(); +(function() { + 'use strict'; - $timeout(function() { - elementPosition = element[0].offsetTop; - //var elementHeight = element[0].offsetHeight; - navbarHeight = angular.element('.stickNav').height(); - }, 1000); + angular.module('app.components') + .directive('login', login); + function login() { + return { + scope: { + show: '=' + }, + restrict: 'A', + controller: 'LoginController', + controllerAs: 'vm', + templateUrl: 'app/components/login/login.html' + }; + } +})(); - angular.element($window).on('scroll', function() { - var windowPosition = document.body.scrollTop; +(function() { + 'use strict'; - //sticking menu and moving up/down - if(windowPosition + navbarHeight >= elementPosition) { - element.addClass('stickMenu'); - scope.$apply(function() { - scope.moveDown = true; - }); - } else { - element.removeClass('stickMenu'); - scope.$apply(function() { - scope.moveDown = false; - }); - } - }); - } - - return { - link: link, - scope: false, - restrict: 'A' - }; - } - - /** - * Unused directive. Double-check is not being used before removing it - * - */ + angular.module('app.components') + .controller('LoginController', LoginController); - function blur() { + LoginController.$inject = ['$scope', '$mdDialog']; + function LoginController($scope, $mdDialog) { - function link(scope, element) { + $scope.showLogin = showLogin; - scope.$on('blur', function() { - element.addClass('blur'); - }); + $scope.$on('showLogin', function() { + showLogin(); + }); - scope.$on('unblur', function() { - element.removeClass('blur'); - }); - } + //////////////// - return { - link: link, - scope: false, - restrict: 'A' - }; + function showLogin() { + $mdDialog.show({ + hasBackdrop: true, + fullscreen: true, + controller: 'LoginModalController', + controllerAs: 'vm', + templateUrl: 'app/components/login/loginModal.html', + clickOutsideToClose: true + }); } - /** - * Used to remove nav and unable scrolling when searching - * - */ - focus.$inject = ['animation']; - function focus(animation) { - function link(scope, element) { - element.on('focusin', function() { - animation.removeNav(); - }); - - element.on('focusout', function() { - animation.addNav(); - }); - - var searchInput = element.find('input'); - searchInput.on('blur', function() { - //enable scrolling on body when search input is not active - angular.element(document.body).css('overflow', 'auto'); - }); - - searchInput.on('focus', function() { - angular.element(document.body).css('overflow', 'hidden'); - }); - } + } +})(); - return { - link: link - }; - } +(function() { + 'use strict'; - /** - * Changes map section based on screen size - * - */ - changeMapHeight.$inject = ['$document', 'layout', '$timeout']; - function changeMapHeight($document, layout, $timeout) { - function link(scope, element) { + angular.module('app.components') + .controller('LayoutController', LayoutController); - var screenHeight = $document[0].body.clientHeight; - var navbarHeight = angular.element('.stickNav').height(); + LayoutController.$inject = ['$mdSidenav','$mdDialog', '$location', '$state', '$scope', '$transitions', 'auth', 'animation', '$timeout', 'DROPDOWN_OPTIONS_COMMUNITY', 'DROPDOWN_OPTIONS_USER']; + function LayoutController($mdSidenav, $mdDialog, $location, $state, $scope, $transitions, auth, animation, $timeout, DROPDOWN_OPTIONS_COMMUNITY, DROPDOWN_OPTIONS_USER) { + var vm = this; - // var overviewHeight = angular.element('.kit_overview').height(); - // var menuHeight = angular.element('.kit_menu').height(); - // var chartHeight = angular.element('.kit_chart').height(); + vm.navRightLayout = 'space-around center'; - function resizeMap(){ - $timeout(function() { - var overviewHeight = angular.element('.over_map').height(); + $scope.toggleRight = buildToggler('right'); - var objectsHeight = navbarHeight + overviewHeight; - var objectsHeightPercentage = parseInt((objectsHeight * 100) / screenHeight); - var mapHeightPercentage = 100 - objectsHeightPercentage; + function buildToggler(componentId) { + return function() { + $mdSidenav(componentId).toggle(); + }; + } - element.css('height', mapHeightPercentage + '%'); + // listen for any login event so that the navbar can be updated + $scope.$on('loggedIn', function(ev, options) { + // if(options && options.time === 'appLoad') { + // $scope.$apply(function() { + // vm.isLoggedin = true; + // vm.isShown = true; + // angular.element('.nav_right .wrap-dd-menu').css('display', 'initial'); + // vm.currentUser = auth.getCurrentUser().data; + // vm.dropdownOptions[0].text = 'Hello, ' + vm.currentUser.username; + // vm.navRightLayout = 'end center'; + // }); + // } else { + // vm.isLoggedin = true; + // vm.isShown = true; + // angular.element('.nav_right .wrap-dd-menu').css('display', 'initial'); + // vm.currentUser = auth.getCurrentUser().data; + // vm.dropdownOptions[0].text = 'Hello, ' + vm.currentUser.username; + // vm.navRightLayout = 'end center'; + // } - var aboveTheFoldHeight = screenHeight - overviewHeight; - angular - .element('section[change-content-margin]') - .css('margin-top', aboveTheFoldHeight + 'px'); - }); + vm.isLoggedin = true; + vm.isShown = true; + angular.element('.nav_right .wrap-dd-menu').css('display', 'initial'); + vm.currentUser = auth.getCurrentUser().data; + vm.dropdownOptions[0].text = 'Hi, ' + vm.currentUser.username + '!'; + vm.navRightLayout = 'end center'; + if(!$scope.$$phase) { + $scope.$digest(); } + }); - resizeMap(); - - scope.element = element; + // listen for logout events so that the navbar can be updated + $scope.$on('loggedOut', function() { + vm.isLoggedIn = false; + vm.isShown = true; + angular.element('navbar .wrap-dd-menu').css('display', 'none'); + vm.navRightLayout = 'space-around center'; + }); - scope.$on('resizeMapHeight',function(){ - resizeMap(); - }); - } + vm.isShown = true; + vm.isLoggedin = false; + vm.logout = logout; - return { - link: link, - scope: true, - restrict: 'A' - }; - } + vm.dropdownOptions = DROPDOWN_OPTIONS_USER; + vm.dropdownSelected = undefined; - /** - * Changes margin on kit section based on above-the-fold space left after map section is resize - */ + vm.dropdownOptionsCommunity = DROPDOWN_OPTIONS_COMMUNITY; + vm.dropdownSelectedCommunity = undefined; - changeContentMargin.$inject = ['layout', '$timeout', '$document']; - function changeContentMargin(layout, $timeout, $document) { - function link(scope, element) { - var screenHeight = $document[0].body.clientHeight; + $scope.$on('removeNav', function() { + vm.isShown = false; + }); - var overviewHeight = angular.element('.over_map').height(); + $scope.$on('addNav', function() { + vm.isShown = true; + }); - var aboveTheFoldHeight = screenHeight - overviewHeight; - element.css('margin-top', aboveTheFoldHeight + 'px'); - } + initialize(); - return { - link: link - }; - } + ////////////////// - /** - * Fixes autofocus for inputs that are inside modals - * - */ - focusInput.$inject = ['$timeout']; - function focusInput($timeout) { - function link(scope, elem) { + function initialize() { $timeout(function() { - elem.focus(); - }); + var hash = $location.search(); + if(hash.signup) { + animation.showSignup(); + } else if(hash.login) { + animation.showLogin(); + } else if(hash.passwordRecovery) { + animation.showPasswordRecovery(); + } + }, 1000); + } + + function logout() { + auth.logout(); + vm.isLoggedin = false; } - return { - link: link - }; } })(); @@ -4284,931 +3848,951 @@ 'use strict'; angular.module('app.components') - .directive('activeButton', activeButton); + .controller('LandingController', LandingController); - /** - * Used to highlight and unhighlight buttons on kit menu - * - * It attaches click handlers dynamically - */ + LandingController.$inject = ['$timeout', 'animation', '$mdDialog', '$location', '$anchorScroll']; - activeButton.$inject = ['$timeout', '$window']; - function activeButton($timeout, $window) { - return { - link: link, - restrict: 'A' + function LandingController($timeout, animation, $mdDialog, $location, $anchorScroll) { + var vm = this; - }; + vm.showStore = showStore; + vm.goToHash = goToHash; - //////////////////////////// + /////////////////////// - function link(scope, element) { - var childrens = element.children(); - var container; + initialize(); - $timeout(function() { - var navbar = angular.element('.stickNav'); - var kitMenu = angular.element('.kit_menu'); - var kitOverview = angular.element('.kit_overview'); - var kitDashboard = angular.element('.kit_chart'); - var kitDetails = angular.element('.kit_details'); - var kitOwner = angular.element('.kit_owner'); - var kitComments = angular.element('.kit_comments'); - - container = { - navbar: { - height: navbar.height() - }, - kitMenu: { - height: kitMenu.height() - }, - kitOverview: { - height: kitOverview.height(), - offset: kitOverview.offset().top, - buttonOrder: 0 - }, - kitDashboard: { - height: kitDashboard.height(), - offset: kitDashboard.offset().top, - buttonOrder: 40 - }, - kitDetails: { - height: kitDetails.height(), - offset: kitDetails.offset() ? kitDetails.offset().top : 0, - buttonOrder: 1 - }, - kitOwner: { - height: kitOwner.height(), - offset: kitOwner.offset() ? kitOwner.offset().top : 0, - buttonOrder: 2 - }, - kitComments: { - height: kitComments.height(), - offset: kitComments.offset() ? kitComments.offset().top : 0, - buttonOrder: 3 - } - }; - }, 1000); - - function scrollTo(offset) { - if(!container) { - return; - } - angular.element($window).scrollTop(offset - container.navbar.height - container.kitMenu.height); - } - - function getButton(buttonOrder) { - return childrens[buttonOrder]; - } - - function unHighlightButtons() { - //remove border, fill and stroke of every icon - var activeButton = angular.element('.md-button.button_active'); - if(activeButton.length) { - activeButton.removeClass('button_active'); - - var strokeContainer = activeButton.find('.stroke_container'); - strokeContainer.css('stroke', 'none'); - strokeContainer.css('stroke-width', '1'); + ////////////////// - var fillContainer = strokeContainer.find('.fill_container'); - fillContainer.css('fill', '#FF8600'); - } + function initialize() { + $timeout(function() { + animation.viewLoaded(); + if($location.hash()) { + $anchorScroll(); } + }, 500); + } - function highlightButton(button) { - var clickedButton = angular.element(button); - //add border, fill and stroke to every icon - clickedButton.addClass('button_active'); + function goToHash(hash){ + $location.hash(hash); + $anchorScroll(); + } - var strokeContainer = clickedButton.find('.stroke_container'); - strokeContainer.css('stroke', 'white'); - strokeContainer.css('stroke-width', '0.01px'); + function showStore() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'StoreModalController', + templateUrl: 'app/components/store/storeModal.html', + clickOutsideToClose: true + }); + } + } +})(); - var fillContainer = strokeContainer.find('.fill_container'); - fillContainer.css('fill', 'white'); - } +(function(){ + 'use strict'; + angular.module('app.components') + .directive('kitList',kitList); - //attach event handlers for clicks for every button and scroll to a section when clicked - _.each(childrens, function(button) { - angular.element(button).on('click', function() { - var buttonOrder = angular.element(this).index(); - for(var elem in container) { - if(container[elem].buttonOrder === buttonOrder) { - var offset = container[elem].offset; - scrollTo(offset); - angular.element($window).trigger('scroll'); - } - } - }); - }); + function kitList(){ + return{ + restrict:'E', + scope:{ + devices:'=devices', + actions: '=actions' + }, + controllerAs:'vm', + templateUrl:'app/components/kitList/kitList.html' + }; + } +})(); - var currentSection; +(function() { + 'use strict'; - //on scroll, check if window is on a section - angular.element($window).on('scroll', function() { - if(!container){ return; } + angular.module('app.components') + .controller('HomeController', HomeController); - var windowPosition = document.body.scrollTop; - var appPosition = windowPosition + container.navbar.height + container.kitMenu.height; - var button; - if(currentSection !== 'none' && appPosition <= container.kitOverview.offset) { - button = getButton(container.kitOverview.buttonOrder); - unHighlightButtons(); - currentSection = 'none'; - } else if(currentSection !== 'overview' && appPosition >= container.kitOverview.offset && appPosition <= container.kitOverview.offset + container.kitOverview.height) { - button = getButton(container.kitOverview.buttonOrder); - unHighlightButtons(); - highlightButton(button); - currentSection = 'overview'; - } else if(currentSection !== 'details' && appPosition >= container.kitDetails.offset && appPosition <= container.kitDetails.offset + container.kitDetails.height) { - button = getButton(container.kitDetails.buttonOrder); - unHighlightButtons(); - highlightButton(button); - currentSection = 'details'; - } else if(currentSection !== 'owner' && appPosition >= container.kitOwner.offset && appPosition <= container.kitOwner.offset + container.kitOwner.height) { - button = getButton(container.kitOwner.buttonOrder); - unHighlightButtons(); - highlightButton(button); - currentSection = 'owner'; - } else if(currentSection !== 'comments' && appPosition >= container.kitComments.offset && appPosition <= container.kitComments.offset + container.kitOwner.height) { - button = getButton(container.kitComments.buttonOrder); - unHighlightButtons(); - highlightButton(button); - currentSection = 'comments'; - } - }); - } - } + function HomeController() { + } })(); +(function (){ + 'use strict'; -(function() { - 'use strict'; + angular.module('app.components') + .controller('DownloadModalController', DownloadModalController); - angular.module('app.components') - .controller('UserProfileController', UserProfileController); + DownloadModalController.$inject = ['thisDevice', 'device', '$mdDialog']; - UserProfileController.$inject = ['$scope', '$stateParams', '$location', - 'user', 'auth', 'userUtils', '$timeout', 'animation', - 'NonAuthUser', '$q', 'PreviewDevice']; - function UserProfileController($scope, $stateParams, $location, - user, auth, userUtils, $timeout, animation, - NonAuthUser, $q, PreviewDevice) { + function DownloadModalController(thisDevice, device, $mdDialog) { + var vm = this; - var vm = this; - var userID = parseInt($stateParams.id); + vm.device = thisDevice; + vm.download = download; + vm.cancel = cancel; - vm.status = undefined; - vm.user = {}; - vm.devices = []; - vm.filteredDevices = []; - vm.filterDevices = filterDevices; + //////////////////////////// - $scope.$on('loggedIn', function() { - var authUser = auth.getCurrentUser().data; - if( userUtils.isAuthUser(userID, authUser) ) { - $location.path('/profile'); - } - }); + function download(){ + device.mailReadings(vm.device) + .then(function (){ + $mdDialog.hide(); + }).catch(function(err){ + $mdDialog.cancel(err); + }); + } - initialize(); + function cancel(){ + $mdDialog.cancel(); + } + } - ////////////////// +})(); - function initialize() { +(function(){ +'use strict'; - user.getUser(userID) - .then(function(user) { - vm.user = new NonAuthUser(user); +angular.module('app.components') + .directive('cookiesLaw', cookiesLaw); - if(!vm.user.devices.length) { - return []; - } - $q.all(vm.devices = vm.user.devices.map(function(data){ - return new PreviewDevice(data); - })) +cookiesLaw.$inject = ['$cookies']; - }).then(function(error) { - if(error && error.status === 404) { - $location.url('/404'); - } - }); +function cookiesLaw($cookies) { + return { + template: + '
' + + 'This site uses cookies to offer you a better experience. ' + + ' Accept or' + + ' Learn More. ' + + '
', + controller: function($scope) { - $timeout(function() { - setSidebarMinHeight(); - animation.viewLoaded(); - }, 500); + var init = function(){ + $scope.isCookieValid(); } - function filterDevices(status) { - if(status === 'all') { - status = undefined; - } - vm.status = status; - } + // Helpers to debug + // You can also use `document.cookie` in the browser dev console. + //console.log($cookies.getAll()); - function setSidebarMinHeight() { - var height = document.body.clientHeight / 4 * 3; - angular.element('.profile_content').css('min-height', height + 'px'); + $scope.isCookieValid = function() { + // Use a boolean for the ng-hide, because using a function with ng-hide + // is considered bad practice. The digest cycle will call it multiple + // times, in our case around 240 times. + $scope.isCookieValidBool = ($cookies.get('consent') === 'true') } + + $scope.acceptCookie = function() { + //console.log('Accepting cookie...'); + var today = new Date(); + var expireDate = new Date(today); + expireDate.setMonth(today.getMonth() + 6); + + $cookies.put('consent', true, {'expires' : expireDate.toUTCString()} ); + + // Trigger the check again, after we click + $scope.isCookieValid(); + }; + + init(); + } + }; +} + + })(); (function() { 'use strict'; angular.module('app.components') - .controller('UploadController', UploadController); + .directive('chart', chart); - UploadController.$inject = ['kit', '$state', '$stateParams', 'animation']; - function UploadController(kit, $state, $stateParams, animation) { - var vm = this; + chart.$inject = ['sensor', 'animation', '$timeout', '$window']; + function chart(sensor, animation, $timeout, $window) { + var margin, width, height, svg, xScale, yScale0, yScale1, xAxis, yAxisLeft, yAxisRight, dateFormat, areaMain, valueLineMain, areaCompare, valueLineCompare, focusCompare, focusMain, popup, dataMain, colorMain, yAxisScale, unitMain, popupContainer; - vm.kit = kit; + return { + link: link, + restrict: 'A', + scope: { + chartData: '=' + } + }; - vm.backToProfile = backToProfile; + function link(scope, elem) { - initialize(); + $timeout(function() { + createChart(elem[0]); + }, 0); - ///////////////// + var lastData = {}; - function initialize() { - animation.viewLoaded(); - } + // on window resize, it re-renders the chart to fit into the new window size + angular.element($window).on('resize', function() { + createChart(elem[0]); + updateChartData(lastData.data, {type: lastData.type, container: elem[0], color: lastData.color, unit: lastData.unit}); + }); - function backToProfile() { - $state.transitionTo('layout.myProfile.kits', $stateParams, - { reload: false, - inherit: false, - notify: true - }); - } - } -})(); + scope.$watch('chartData', function(newData) { + if(!newData) { + return; + } -(function(){ -'use strict'; + if(newData !== undefined) { + // if there's data for 2 sensors + if(newData[0] && newData[1]) { + var sensorDataMain = newData[0].data; + // we could get some performance from saving the map in the showKit controller on line 218 and putting that logic in here + var dataMain = sensorDataMain.map(function(dataPoint) { + return { + date: dateFormat(dataPoint.time), + count: dataPoint && dataPoint.count, + value: dataPoint && dataPoint.value + }; + }); + // sort data points by date + dataMain.sort(function(a, b) { + return a.date - b.date; + }); + var sensorDataCompare = newData[1].data; + var dataCompare = sensorDataCompare.map(function(dataPoint) { + return { + date: dateFormat(dataPoint.time), + count: dataPoint && dataPoint.count, + value: dataPoint && dataPoint.value + }; + }); + dataCompare.sort(function(a, b) { + return a.date - b.date; + }); -function parseDataForPost(csvArray) { - /* - EXPECTED PAYLOAD - { - "data": [{ - "recorded_at": "2016-06-08 10:30:00", - "sensors": [{ - "id": 22, - "value": 21 - }] - }] - } - */ - const ids = csvArray[3]; // save ids from the 4th header - csvArray.splice(0,4); // remove useless headers - return { - data: csvArray.map((data) => { - return { - recorded_at: data.shift(), // get the timestamp from the first column - sensors: data.map((value, index) => { - return { - id: ids[index+1], // get ID of sensor from headers - value: value - }; - }) - .filter((sensor) => sensor.value && sensor.id) // remove empty value or id - }; - }) - }; -} + var data = [dataMain, dataCompare]; + var colors = [newData[0].color, newData[1].color]; + var units = [newData[0].unit, newData[1].unit]; + // saves everything in case we need to re-render + lastData = { + data: data, + type: 'both', + color: colors, + unit: units + }; + // call function to update the chart with the new data + updateChartData(data, {type: 'both', container: elem[0], color: colors, unit: units }); + // if only data for the main sensor + } else if(newData[0]) { + var sensorData = newData[0].data; + /*jshint -W004 */ + var data = sensorData.map(function(dataPoint) { + return { + date: dateFormat(dataPoint.time), + count: dataPoint && dataPoint.count, + value: dataPoint && dataPoint.value + }; + }); + data.sort(function(a, b) { + return a.date - b.date; + }); -controller.$inject = ['device', 'Papa', '$mdDialog', '$q']; -function controller(device, Papa, $mdDialog, $q) { - var vm = this; - vm.loadingStatus = false; - vm.loadingProgress = 0; - vm.loadingType = 'indeterminate'; - vm.csvFiles = []; - vm.$onInit = function() { - vm.kitLastUpdate = Math.floor(new Date(vm.kit.time).getTime() / 1000); - } - vm.onSelect = function() { - vm.loadingStatus = true; - vm.loadingType = 'indeterminate'; - } - vm.change = function(files, invalidFiles) { - let count = 0; - vm.invalidFiles = invalidFiles; - if (!files) { return; } - vm.loadingStatus = true; - vm.loadingType = 'determinate'; - vm.loadingProgress = 0; - $q.all( - files - .filter((file) => vm._checkDuplicate(file)) - .map((file, index, filteredFiles) => { - vm.csvFiles.push(file); - return vm._analyzeData(file) - .then((result) => { - if (result.errors && result.errors.length > 0) { - file.parseErrors = result.errors; - } - const lastTimestamp = Math.floor((new Date(result.data[result.data.length - 1][0])).getTime() / 1000); - const isNew = vm.kitLastUpdate < lastTimestamp; - file.checked = isNew; - file.progress = null; - file.isNew = isNew; - }) - .then(() => { - count += 1; - vm.loadingProgress = (count)/filteredFiles.length * 100; + var color = newData[0].color; + var unit = newData[0].unit; + + lastData = { + data: data, + type: 'main', + color: color, + unit: unit + }; + updateChartData(data, {type: 'main', container: elem[0], color: color, unit: unit }); + } + animation.hideChartSpinner(); + } }); - }) - ).then(() => { - vm.loadingStatus = false; - }).catch(() => { - vm.loadingStatus = false; - }); - } + } - vm.haveSelectedFiles = function() { - return vm.csvFiles && vm.csvFiles.some((file) => file.checked); - }; + // creates the container that is re-used across different sensor charts + function createChart(elem) { + d3.select(elem).selectAll('*').remove(); - vm.haveSelectedNoFiles = function() { - return vm.csvFiles && !vm.csvFiles.some((file) => file.checked); - }; + margin = {top: 20, right: 12, bottom: 20, left: 42}; + width = elem.clientWidth - margin.left - margin.right; + height = elem.clientHeight - margin.top - margin.bottom; - vm.haveSelectedAllFiles = function() { - return vm.csvFiles && vm.csvFiles.every((file) => file.checked); - }; + xScale = d3.time.scale().range([0, width]); + xScale.tickFormat("%Y-%m-%d %I:%M:%S"); + yScale0 = d3.scale.linear().range([height, 0]); + yScale1 = d3.scale.linear().range([height, 0]); + yAxisScale = d3.scale.linear().range([height, 0]); - vm.doAction = function() { - switch (vm.action) { - case 'selectAll': - vm.selectAll(true); - break; - case 'deselectAll': - vm.selectAll(false); - break; - case 'upload': - vm.uploadData(); - break; - case 'remove': - vm.csvFiles = vm.csvFiles.filter((file) => !file.checked); - break; - } - vm.action = null; - }; + dateFormat = d3.time.format('%Y-%m-%dT%H:%M:%S').parse;//d3.time.format('%Y-%m-%dT%X.%LZ').parse; //'YYYY-MM-DDTHH:mm:ssZ' - vm.selectAll = function(value) { - vm.csvFiles.forEach((file) => { file.checked = value }); - }; + xAxis = d3.svg.axis() + .scale(xScale) + .orient('bottom') + .ticks(5); - vm.removeFile = function(index) { - vm.csvFiles.splice(index, 1); - }; - vm._analyzeData = function(file) { - file.progress = true; - return Papa.parse(file, { - delimiter: ',', - dynamicTyping: true, - worker: false, - skipEmptyLines: true - }).catch((err) => { - file.progress = null; - console('catch',err) - }); - }; + yAxisLeft = d3.svg.axis() + .scale(yScale0) + .orient('left') + .ticks(5); - vm._checkDuplicate = function(file) { - if (vm.csvFiles.some((csvFile) => file.name === csvFile.name)) { - file.$errorMessages = {}; - file.$errorMessages.duplicate = true; - vm.invalidFiles.push(file); - return false; - } else { - return true; - } - }; + yAxisRight = d3.svg.axis() + .scale(yScale1) + .orient('right') + .ticks(5); - vm.showErrorModal = function(csvFile) { - $mdDialog.show({ - hasBackdrop: true, - controller: ['$mdDialog',function($mdDialog) { - this.parseErrors = csvFile.parseErrors; - this.backEndErrors = csvFile.backEndErrors; - this.cancel = function() { $mdDialog.hide(); }; - }], - controllerAs: 'csvFile', - templateUrl: 'app/components/upload/errorModal.html', - clickOutsideToClose: true - }); - } + areaMain = d3.svg.area() + .defined(function(d) {return d.value != null }) + .interpolate('linear') + .x(function(d) { return xScale(d.date); }) + .y0(height) + .y1(function(d) { return yScale0(d.count); }); + valueLineMain = d3.svg.line() + .defined(function(d) {return d.value != null }) + .interpolate('linear') + .x(function(d) { return xScale(d.date); }) + .y(function(d) { return yScale0(d.count); }); - vm.uploadData = function() { - vm.loadingStatus = true; - vm.loadingType = 'indeterminate'; - vm.loadingProgress = 0; - let count = 0; + areaCompare = d3.svg.area() + .defined(function(d) {return d.value != null }) + .interpolate('linear') + .x(function(d) { return xScale(d.date); }) + .y0(height) + .y1(function(d) { return yScale1(d.count); }); - $q.all( - vm.csvFiles - .filter((file) => file.checked && !file.success) - .map((file, index, filteredFiles) => { - file.progress = true; - return vm._analyzeData(file) - .then((result) => parseDataForPost(result.data)) // TODO: Improvement remove - // TODO: Improvement with workers - .then((payload) => device.postReadings(vm.kit, payload)) - .then(() => { - if (vm.loadingType === 'indeterminate') { vm.loadingType = 'determinate'; }; - file.success = true; - file.progress = null; - count += 1; - vm.loadingProgress = (count)/filteredFiles.length * 100; - }) - .catch((errors) => { - console.log(errors); - file.detailShowed = true; - file.backEndErrors = errors; - file.progress = null; - }); - }) - ).then(() => { - vm.loadingStatus = false; - }) - .catch(() => { - vm.loadingStatus = false; - }); - } -}; + valueLineCompare = d3.svg.line() + .defined(function(d) {return d.value != null }) + .interpolate('linear') + .x(function(d) { return xScale(d.date); }) + .y(function(d) { return yScale1(d.count); }); + svg = d3 + .select(elem) + .append('svg') + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr('transform', 'translate(' + (margin.left - margin.right) + ',' + margin.top + ')'); + } + // calls functions depending on type of chart + function updateChartData(newData, options) { + if(options.type === 'main') { + updateChartMain(newData, options); + } else if(options.type === 'both') { + updateChartCompare(newData, options); + } + } + // function in charge of rendering when there's data for 1 sensor + function updateChartMain(data, options) { + xScale.domain(d3.extent(data, function(d) { return d.date; })); + yScale0.domain([(d3.min(data, function(d) { return d.count; })) * 0.8, (d3.max(data, function(d) { return d.count; })) * 1.2]); -angular.module('app.components') - .component('scCsvUpload', { - templateUrl: 'app/components/upload/csvUpload.html', - controller: controller, - bindings: { - kit: '<' - }, - controllerAs: 'vm' - }); -})(); + svg.selectAll('*').remove(); -(function() { - 'use strict'; + //Add the area path + svg.append('path') + .datum(data) + .attr('class', 'chart_area') + .attr('fill', options.color) + .attr('d', areaMain); - angular.module('app.components') - .controller('tagsController', tagsController); + // Add the valueline path. + svg.append('path') + .attr('class', 'chart_line') + .attr('stroke', options.color) + .attr('d', valueLineMain(data)); - tagsController.$inject = ['tag', '$scope', 'device', '$state', '$q', - 'PreviewDevice', 'animation' - ]; + // Add the X Axis + svg.append('g') + .attr('class', 'axis x') + .attr('transform', 'translate(0,' + height + ')') + .call(xAxis); - function tagsController(tag, $scope, device, $state, $q, PreviewDevice, - animation) { + // Add the Y Axis + svg.append('g') + .attr('class', 'axis y_left') + .call(yAxisLeft); - var vm = this; + // Draw the x Grid lines + svg.append('g') + .attr('class', 'grid') + .attr('transform', 'translate(0,' + height + ')') + .call(xGrid() + .tickSize(-height, 0, 0) + .tickFormat('') + ); - vm.selectedTags = tag.getSelectedTags(); - vm.markers = []; - vm.kits = []; - vm.percActive = 0; + // Draw the y Grid lines + svg.append('g') + .attr('class', 'grid') + .call(yGrid() + .tickSize(-width, 0, 0) + .tickFormat('') + ); - initialize(); + focusMain = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - ///////////////////////////////////////////////////////// + focusMain.append('circle') + .style('stroke', options.color) + .attr('r', 4.5); - function initialize() { - if(vm.selectedTags.length === 0){ - $state.transitionTo('layout.home.kit'); - } + var popupWidth = 84; + var popupHeight = 46; - if (device.getWorldMarkers()) { - // If the user has already loaded a prev page and has markers in mem or localstorage - updateSelectedTags(); - } else { - // If the user is new we wait the map to load the markers - $scope.$on('mapStateLoaded', function(event, data) { - updateSelectedTags(); - }); - } + popup = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - } + popupContainer = popup.append('rect') + .attr('width', popupWidth) + .attr('height', popupHeight) + .attr('transform', function() { + var result = 'translate(-42, 5)'; - function updateSelectedTags(){ + return result; + }) + .style('stroke', 'grey') + .style('stroke-width', '0.5') + .style('fill', 'white'); - vm.markers = tag.filterMarkersByTag(device.getWorldMarkers()); + var text = popup.append('text') + .attr('class', ''); - var onlineMarkers = _.filter(vm.markers, isOnline); - if (vm.markers.length === 0) { - vm.percActive = 0; - } else { - vm.percActive = Math.floor(onlineMarkers.length / vm.markers.length * - 100); - } + var textMain = text.append('tspan') + .attr('class', 'popup_main') + .attr('text-anchor', 'start') + .attr('x', -popupWidth / 2) + .attr('dx', 8) + .attr('y', popupHeight / 2) + .attr('dy', 3); - animation.viewLoaded(); + textMain.append('tspan') + .attr('class', 'popup_value'); - getTaggedDevices() - .then(function(res){ - vm.kits = res; - }); - } - - - function isOnline(marker) { - return _.includes(marker.myData.labels, 'online'); - } + textMain.append('tspan') + .attr('class', 'popup_unit') + .attr('dx', 5); - function descLastUpdate(o) { - return -new Date(o.last_reading_at).getTime(); - } + text.append('tspan') + .attr('class', 'popup_date') + .attr('x', -popupWidth / 2) + .attr('dx', 8) + .attr('y', popupHeight - 2) + .attr('dy', 0) + .attr( 'text-anchor', 'start' ); - function getTaggedDevices() { + svg.append('rect') + .attr('class', 'overlay') + .attr('width', width) + .attr('height', height) + .on('mouseover', function() { + popup.style('display', null); + focusMain.style('display', null); + }) + .on('mouseout', function() { + popup.style('display', 'none'); + focusMain.style('display', 'none'); + }) + .on('mousemove', mousemove); - var deviceProm = _.map(vm.markers, getMarkerDevice); - return $q.all(deviceProm) - .then(function(devices) { - return _.map(_.sortBy(devices, descLastUpdate), toPreviewDevice); // This sort is temp - }); - } - function toPreviewDevice(dev) { - return new PreviewDevice(dev); - } + function mousemove() { + var bisectDate = d3.bisector(function(d) { return d.date; }).left; - function getMarkerDevice(marker) { - return device.getDevice(marker.myData.id); - } - } + var x0 = xScale.invert(d3.mouse(this)[0]); + var i = bisectDate(data, x0, 1); + var d0 = data[i - 1]; + var d1 = data[i]; + var d = d1 && (x0 - d0.date > d1.date - x0) ? d1 : d0; -})(); + focusMain.attr('transform', 'translate(' + xScale(d.date) + ', ' + yScale0(d.count) + ')'); + var popupText = popup.select('text'); + var textMain = popupText.select('.popup_main'); + var valueMain = textMain.select('.popup_value').text(parseValue(d.value)); + var unitMain = textMain.select('.popup_unit').text(options.unit); + var date = popupText.select('.popup_date').text(parseTime(d.date)); -(function(){ - 'use strict'; - angular.module('app.components') - .directive('tag',tag); + var textContainers = [ + textMain, + date + ]; - function tag(){ - return{ - restrict: 'E', - scope:{ - tagName: '=', - openTag: '&' - }, - controller:function($scope, $state){ - $scope.openTag = function(){ - $state.go('layout.home.tags', {tags:[$scope.tagName]}); - }; - }, - template:'{{tagName}}', - link: function(scope, element, attrs){ - element.addClass('tag'); + var popupWidth = resizePopup(popupContainer, textContainers); - if(typeof(attrs.clickable) !== 'undefined'){ - element.bind('click', scope.openTag); + if(xScale(d.date) + 80 + popupWidth > options.container.clientWidth) { + popup.attr('transform', 'translate(' + (xScale(d.date) - 120) + ', ' + (d3.mouse(this)[1] - 20) + ')'); + } else { + popup.attr('transform', 'translate(' + (xScale(d.date) + 80) + ', ' + (d3.mouse(this)[1] - 20) + ')'); + } } } - }; - } -})(); -(function() { - 'use strict'; + // function in charge of rendering when there's data for 2 sensors + function updateChartCompare(data, options) { + xScale.domain(d3.extent(data[0], function(d) { return d.date; })); + yScale0.domain([(d3.min(data[0], function(d) { return d.count; })) * 0.8, (d3.max(data[0], function(d) { return d.count; })) * 1.2]); + yScale1.domain([(d3.min(data[1], function(d) { return d.count; })) * 0.8, (d3.max(data[1], function(d) { return d.count; })) * 1.2]); - angular.module('app.components') - .controller('StoreModalController', StoreModalController); + svg.selectAll('*').remove(); - StoreModalController.$inject = ['$scope', '$mdDialog']; - function StoreModalController($scope, $mdDialog) { + //Add both area paths + svg.append('path') + .datum(data[0]) + .attr('class', 'chart_area') + .attr('fill', options.color[0]) + .attr('d', areaMain); - $scope.cancel = function() { - $mdDialog.hide(); - }; - } -})(); + svg.append('path') + .datum(data[1]) + .attr('class', 'chart_area') + .attr('fill', options.color[1]) + .attr('d', areaCompare); -(function() { - 'use strict'; + // Add both valueline paths. + svg.append('path') + .attr('class', 'chart_line') + .attr('stroke', options.color[0]) + .attr('d', valueLineMain(data[0])); - angular.module('app.components') - .directive('store', store); + svg.append('path') + .attr('class', 'chart_line') + .attr('stroke', options.color[1]) + .attr('d', valueLineCompare(data[1])); - function store() { - return { - scope: { - isLoggedin: '=logged' - }, - restrict: 'A', - controller: 'StoreController', - controllerAs: 'vm', - templateUrl: 'app/components/store/store.html' - }; - } -})(); + // Add the X Axis + svg.append('g') + .attr('class', 'axis x') + .attr('transform', 'translate(0,' + height + ')') + .call(xAxis); -(function() { - 'use strict'; + // Add both Y Axis + svg.append('g') + .attr('class', 'axis y_left') + .call(yAxisLeft); - angular.module('app.components') - .controller('StoreController', StoreController); + svg.append('g') + .attr('class', 'axis y_right') + .attr('transform', 'translate(' + width + ' ,0)') + .call(yAxisRight); - StoreController.$inject = ['$scope', '$mdDialog']; - function StoreController($scope, $mdDialog) { + // Draw the x Grid lines + svg.append('g') + .attr('class', 'grid') + .attr('transform', 'translate(0,' + height + ')') + .call(xGrid() + .tickSize(-height, 0, 0) + .tickFormat('') + ); - $scope.showStore = showStore; + // Draw the y Grid lines + svg.append('g') + .attr('class', 'grid') + .call(yGrid() + .tickSize(-width, 0, 0) + .tickFormat('') + ); - $scope.$on('showStore', function() { - showStore(); - }); - - //////////////// + focusCompare = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - function showStore() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'StoreModalController', - templateUrl: 'app/components/store/storeModal.html', - clickOutsideToClose: true - }); - } + focusMain = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - } -})(); + focusCompare.append('circle') + .style('stroke', options.color[1]) + .attr('r', 4.5); -(function() { - 'use strict'; + focusMain.append('circle') + .style('stroke', options.color[0]) + .attr('r', 4.5); - angular.module('app.components') - .controller('StaticController', StaticController); + var popupWidth = 84; + var popupHeight = 75; - StaticController.$inject = ['$timeout', 'animation', '$mdDialog', '$location', '$anchorScroll']; + popup = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - function StaticController($timeout, animation, $mdDialog, $location, $anchorScroll) { - var vm = this; + popupContainer = popup.append('rect') + .attr('width', popupWidth) + .attr('height', popupHeight) + .style('min-width', '40px') + .attr('transform', function() { + var result = 'translate(-42, 5)'; - vm.showStore = showStore; + return result; + }) + .style('stroke', 'grey') + .style('stroke-width', '0.5') + .style('fill', 'white'); - $anchorScroll.yOffset = 80; + popup.append('rect') + .attr('width', 8) + .attr('height', 2) + .attr('transform', function() { + return 'translate(' + (-popupWidth / 2 + 4).toString() + ', 20)'; + }) + .style('fill', options.color[0]); - /////////////////////// + popup.append('rect') + .attr('width', 8) + .attr('height', 2) + .attr('transform', function() { + return 'translate(' + (-popupWidth / 2 + 4).toString() + ', 45)'; + }) + .style('fill', options.color[1]); - initialize(); + var text = popup.append('text') + .attr('class', ''); - ////////////////// + var textMain = text.append('tspan') + .attr('class', 'popup_main') + .attr('x', -popupHeight / 2 + 7) //position of text + .attr('dx', 8) //margin given to the element, will be applied to both sides thanks to resizePopup function + .attr('y', popupHeight / 3) + .attr('dy', 3); - function initialize() { - $timeout(function() { - animation.viewLoaded(); - if($location.hash()){ - $anchorScroll(); - } - }, 500); - } + textMain.append('tspan') + .attr('class', 'popup_value') + .attr( 'text-anchor', 'start' ); - function showStore() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'StoreModalController', - templateUrl: 'app/components/store/storeModal.html', - clickOutsideToClose: true - }); - } - } -})(); + textMain.append('tspan') + .attr('class', 'popup_unit') + .attr('dx', 5); -(function() { - 'use strict'; + var textCompare = text.append('tspan') + .attr('class', 'popup_compare') + .attr('x', -popupHeight / 2 + 7) //position of text + .attr('dx', 8) //margin given to the element, will be applied to both sides thanks to resizePopup function + .attr('y', popupHeight / 1.5) + .attr('dy', 3); - angular.module('app.components') - .controller('SignupModalController', SignupModalController); + textCompare.append('tspan') + .attr('class', 'popup_value') + .attr( 'text-anchor', 'start' ); - SignupModalController.$inject = ['$scope', '$mdDialog', 'user', - 'alert', 'animation']; - function SignupModalController($scope, $mdDialog, user, - alert, animation ) { - var vm = this; - vm.answer = function(signupForm) { + textCompare.append('tspan') + .attr('class', 'popup_unit') + .attr('dx', 5); - if (!signupForm.$valid){ - return; - } + text.append('tspan') + .attr('class', 'popup_date') + .attr('x', (- popupWidth / 2)) + .attr('dx', 8) + .attr('y', popupHeight - 2) + .attr('dy', 0) + .attr( 'text-anchor', 'start' ); - $scope.waitingFromServer = true; - user.createUser(vm.user) - .then(function() { - alert.success('Signup was successful'); - $mdDialog.hide(); - }).catch(function(err) { - alert.error('Signup failed'); - $scope.errors = err.data.errors; + svg.append('rect') + .attr('class', 'overlay') + .attr('width', width) + .attr('height', height) + .on('mouseover', function() { + focusCompare.style('display', null); + focusMain.style('display', null); + popup.style('display', null); }) - .finally(function() { - $scope.waitingFromServer = false; - }); - }; - $scope.hide = function() { - $mdDialog.hide(); - }; - $scope.cancel = function() { - $mdDialog.cancel(); - }; + .on('mouseout', function() { + focusCompare.style('display', 'none'); + focusMain.style('display', 'none'); + popup.style('display', 'none'); + }) + .on('mousemove', mousemove); - $scope.openLogin = function() { - animation.showLogin(); - $mdDialog.hide(); - }; - } -})(); + function mousemove() { + var bisectDate = d3.bisector(function(d) { return d.date; }).left; -(function() { - 'use strict'; + var x0 = xScale.invert(d3.mouse(this)[0]); + var i = bisectDate(data[1], x0, 1); + var d0 = data[1][i - 1]; + var d1 = data[1][i]; + var d = x0 - d0.date > d1.date - x0 ? d1 : d0; + focusCompare.attr('transform', 'translate(' + xScale(d.date) + ', ' + yScale1(d.count) + ')'); - angular.module('app.components') - .directive('signup', signup); - function signup() { - return { - scope: { - show: '=', - }, - restrict: 'A', - controller: 'SignupController', - controllerAs: 'vm', - templateUrl: 'app/components/signup/signup.html' - }; - } -})(); + var dMain0 = data[0][i - 1]; + var dMain1 = data[0][i]; + var dMain = x0 - dMain0.date > dMain1.date - x0 ? dMain1 : dMain0; + focusMain.attr('transform', 'translate(' + xScale(dMain.date) + ', ' + yScale0(dMain.count) + ')'); -(function() { - 'use strict'; + var popupText = popup.select('text'); + var textMain = popupText.select('.popup_main'); + textMain.select('.popup_value').text(parseValue(dMain.value)); + textMain.select('.popup_unit').text(options.unit[0]); + var textCompare = popupText.select('.popup_compare'); + textCompare.select('.popup_value').text(parseValue(d.value)); + textCompare.select('.popup_unit').text(options.unit[1]); + var date = popupText.select('.popup_date').text(parseTime(d.date)); - angular.module('app.components') - .controller('SignupController', SignupController); + var textContainers = [ + textMain, + textCompare, + date + ]; - SignupController.$inject = ['$scope', '$mdDialog']; - function SignupController($scope, $mdDialog) { - var vm = this; + var popupWidth = resizePopup(popupContainer, textContainers); - vm.showSignup = showSignup; + if(xScale(d.date) + 80 + popupWidth > options.container.clientWidth) { + popup.attr('transform', 'translate(' + (xScale(d.date) - 120) + ', ' + (d3.mouse(this)[1] - 20) + ')'); + } else { + popup.attr('transform', 'translate(' + (xScale(d.date) + 80) + ', ' + (d3.mouse(this)[1] - 20) + ')'); + } + } + } - $scope.$on('showSignup', function() { - showSignup(); - }); - //////////////////////// + function xGrid() { + return d3.svg.axis() + .scale(xScale) + .orient('bottom') + .ticks(5); + } + function yGrid() { + return d3.svg.axis() + .scale(yScale0) + .orient('left') + .ticks(5); + } - function showSignup() { - $mdDialog.show({ - fullscreen: true, - hasBackdrop: true, - controller: 'SignupModalController', - controllerAs: 'vm', - templateUrl: 'app/components/signup/signupModal.html', - clickOutsideToClose: true - }); + function parseValue(value) { + if(value === null) { + return 'No data on the current timespan'; + } else if(value.toString().indexOf('.') !== -1) { + var result = value.toString().split('.'); + return result[0] + '.' + result[1].slice(0, 2); + } else if(value > 99.99) { + return value.toString(); + } else { + return value.toString().slice(0, 2); + } + } + + function parseTime(time) { + return moment(time).format('h:mm a ddd Do MMM YYYY'); + } + + function resizePopup(popupContainer, textContainers) { + if(!textContainers.length) { + return; + } + + var widestElem = textContainers.reduce(function(widestElemSoFar, textContainer) { + var currentTextContainerWidth = getContainerWidth(textContainer); + var prevTextContainerWidth = getContainerWidth(widestElemSoFar); + return prevTextContainerWidth >= currentTextContainerWidth ? widestElemSoFar : textContainer; + }, textContainers[0]); + + var margins = widestElem.attr('dx') * 2; + + popupContainer + .attr('width', getContainerWidth(widestElem) + margins); + + function getContainerWidth(container) { + var node = container.node(); + var width; + if(node.getComputedTextLength) { + width = node.getComputedTextLength(); + } else if(node.getBoundingClientRect) { + width = node.getBoundingClientRect().width; + } else { + width = node.getBBox().width; + } + return width; + } + return getContainerWidth(widestElem) + margins; } } -})(); -(function() { -'use strict'; +})(); +(function(){ + 'use strict'; angular.module('app.components') - .directive('search', search); + .directive('apiKey', apiKey); - function search() { + function apiKey(){ return { - scope: true, - restrict: 'E', - templateUrl: 'app/components/search/search.html', - controller: 'SearchController', - controllerAs: 'vm' + scope: { + apiKey: '=apiKey' + }, + restrict: 'A', + controller: 'ApiKeyController', + controllerAs: 'vm', + templateUrl: 'app/components/apiKey/apiKey.html' }; } })(); -(function() { +(function(){ 'use strict'; angular.module('app.components') - .controller('SearchController', SearchController); + .controller('ApiKeyController', ApiKeyController); - SearchController.$inject = ['$scope', 'search', 'SearchResult', '$location', 'animation', 'SearchResultLocation']; - function SearchController($scope, search, SearchResult, $location, animation, SearchResultLocation) { - var vm = this; + ApiKeyController.$inject = ['alert']; + function ApiKeyController(alert){ + var vm = this; - vm.searchTextChange = searchTextChange; - vm.selectedItemChange = selectedItemChange; - vm.querySearch = querySearch; + vm.copied = copied; + vm.copyFail = copyFail; - /////////////////// + /////////////// - function searchTextChange() { - } + function copied(){ + alert.success('API key copied to your clipboard.'); + } - function selectedItemChange(result) { - if (!result) { return; } - if(result.type === 'User') { - $location.path('/users/' + result.id); - } else if(result.type === 'Device') { - $location.path('/kits/' + result.id); - } else if (result.type === 'City'){ - animation.goToLocation({lat: result.lat, lng: result.lng, type: result.type, layer: result.layer}); - } + function copyFail(err){ + console.log('Copy error: ', err); + alert.error('Oops! An error occurred copying the api key.'); + } + + } +})(); + +(function() { + 'use strict'; + + angular.module('app.components') + .factory('alert', alert); + + alert.$inject = ['$mdToast']; + function alert($mdToast) { + var service = { + success: success, + error: error, + info: { + noData: { + visitor: infoNoDataVisitor, + owner: infoNoDataOwner, + private: infoDataPrivate, + }, + longTime: infoLongTime, + // TODO: Refactor, check why this was removed + // inValid: infoDataInvalid, + generic: info } + }; - function querySearch(query) { - if(query.length < 3) { - return []; - } + return service; - return search.globalSearch(query) - .then(function(data) { + /////////////////// - return data.map(function(object) { + function success(message) { + toast('success', message); + } - if(object.type === 'City' || object.type === 'Country') { - return new SearchResultLocation(object); - } else { - return new SearchResult(object); - } - }); - }); + function error(message) { + toast('error', message); + } + + function infoNoDataVisitor() { + info('Woah! We couldn\'t locate this kit on the map because it hasn\'t published any data. Leave a ' + + 'comment to let its owner know.', + 10000, + { + button: 'Leave comment', + href: 'https://forum.smartcitizen.me/' + }); + } + + function infoNoDataOwner() { + info('Woah! We couldn\'t locate this kit on the map because it hasn\'t published any data.', + 10000); + } + + function infoDataPrivate() { + info('Device not found, or it has been set to private. Leave a ' + + 'comment to let its owner know you\'re interested.', + 10000, + { + button: 'Leave comment', + href: 'https://forum.smartcitizen.me/' + }); + } + + // TODO: Refactor, check why this was removed + // function infoDataInvalid() { + // info('Device not found, or it has been set to private.', + // 10000); + // } + + function infoLongTime() { + info('😅 It looks like this kit hasn\'t posted any data in a long ' + + 'time. Why not leave a comment to let its owner know?', + 10000, + { + button: 'Leave comment', + href: 'https://forum.smartcitizen.me/' + }); + } + + function info(message, delay, options) { + if(options && options.button) { + toast('infoButton', message, options, undefined, delay); + } else { + toast('info', message, options, undefined, delay); } } + + function toast(type, message, options, position, delay) { + position = position === undefined ? 'top': position; + delay = delay === undefined ? 5000 : delay; + + $mdToast.show({ + controller: 'AlertController', + controllerAs: 'vm', + templateUrl: 'app/components/alert/alert' + type + '.html', + hideDelay: delay, + position: position, + locals: { + message: message, + button: options && options.button, + href: options && options.href + } + }); + } + } })(); (function() { 'use strict'; angular.module('app.components') - .controller('PasswordResetController', PasswordResetController); + .controller('AlertController', AlertController); - PasswordResetController.$inject = ['$mdDialog', '$stateParams', '$timeout', - 'animation', '$location', 'alert', 'auth']; - function PasswordResetController($mdDialog, $stateParams, $timeout, - animation, $location, alert, auth) { - + AlertController.$inject = ['$scope', '$mdToast', 'message', 'button', 'href']; + function AlertController($scope, $mdToast, message, button, href) { var vm = this; - vm.showForm = false; - vm.form = {}; - vm.isDifferent = false; - vm.answer = answer; - - initialize(); - /////////// - - function initialize() { - $timeout(function() { - animation.viewLoaded(); - }, 500); - getUserData(); - } - function getUserData() { - auth.getResetPassword($stateParams.code) - .then(function() { - vm.showForm = true; - }) - .catch(function() { - alert.error('Wrong url'); - $location.path('/'); - }); - } + vm.close = close; + vm.message = message; + vm.button = button; + vm.href = href; - function answer(data) { - vm.waitingFromServer = true; - vm.errors = undefined; + // hideAlert will be triggered on state change + $scope.$on('hideAlert', function() { + close(); + }); - if(data.newPassword === data.confirmPassword) { - vm.isDifferent = false; - } else { - vm.isDifferent = true; - return; - } + /////////////////// - auth.patchResetPassword($stateParams.code, {password: data.newPassword}) - .then(function() { - alert.success('Your data was updated successfully'); - $location.path('/profile'); - }) - .catch(function(err) { - alert.error('Your data wasn\'t updated'); - vm.errors = err.data.errors; - }) - .finally(function() { - vm.waitingFromServer = false; - }); + function close() { + $mdToast.hide(); } } })(); @@ -5217,43 +4801,23 @@ angular.module('app.components') 'use strict'; angular.module('app.components') - .controller('PasswordRecoveryModalController', PasswordRecoveryModalController); - - PasswordRecoveryModalController.$inject = ['$scope', 'animation', '$mdDialog', 'auth', 'alert']; - function PasswordRecoveryModalController($scope, animation, $mdDialog, auth, alert) { + .factory('userUtils', userUtils); - $scope.hide = function() { - $mdDialog.hide(); - }; - $scope.cancel = function() { - $mdDialog.cancel(); + function userUtils() { + var service = { + isAdmin: isAdmin, + isAuthUser: isAuthUser }; + return service; - $scope.recoverPassword = function() { - $scope.waitingFromServer = true; - var data = { - /*jshint camelcase: false */ - email_or_username: $scope.input - }; - - auth.recoverPassword(data) - .then(function() { - alert.success('You were sent an email to recover your password'); - $mdDialog.hide(); - }) - .catch(function(err) { - alert.error('That username doesn\'t exist'); - $scope.errors = err.data; - }) - .finally(function() { - $scope.waitingFromServer = false; - }); - }; + /////////// - $scope.openSignup = function() { - animation.showSignup(); - $mdDialog.hide(); - }; + function isAdmin(userData) { + return userData.role === 'admin'; + } + function isAuthUser(userID, authUserData) { + return userID === authUserData.id; + } } })(); @@ -5261,424 +4825,340 @@ angular.module('app.components') 'use strict'; angular.module('app.components') - .controller('PasswordRecoveryController', PasswordRecoveryController); + .factory('timeUtils', timeUtils); - PasswordRecoveryController.$inject = ['auth', 'alert', '$mdDialog']; - function PasswordRecoveryController(auth, alert, $mdDialog) { - var vm = this; - - vm.waitingFromServer = false; - vm.errors = undefined; - vm.recoverPassword = recoverPassword; + function timeUtils() { + var service = { + getSecondsFromDate: getSecondsFromDate, + getMillisFromDate: getMillisFromDate, + getCurrentRange: getCurrentRange, + getToday: getToday, + getHourBefore: getHourBefore, + getSevenDaysAgo: getSevenDaysAgo, + getDateIn: getDateIn, + convertTime: convertTime, + formatDate: formatDate, + isSameDay: isSameDay, + isWithin15min: isWithin15min, + isWithin1Month: isWithin1Month, + isWithin: isWithin, + isDiffMoreThan15min: isDiffMoreThan15min, + parseDate: parseDate + }; + return service; - /////////////// + //////////// - function recoverPassword() { - vm.waitingFromServer = true; - vm.errors = undefined; - - var data = { - username: vm.username - }; + function getDateIn(timeMS, format) { + if(!format) { + return timeMS; + } - auth.recoverPassword(data) - .then(function() { - alert.success('You were sent an email to recover your password'); - $mdDialog.hide(); - }) - .catch(function(err) { - vm.errors = err.data.errors; - if(vm.errors) { - alert.error('That email/username doesn\'t exist'); - } - }) - .finally(function() { - vm.waitingFromServer = false; - }); + var result; + if(format === 'ms') { + result = timeMS; + } else if(format === 's') { + result = timeMS / 1000; + } else if(format === 'm') { + result = timeMS / 1000 / 60; + } else if(format === 'h') { + result = timeMS / 1000 / 60 / 60; + } else if(format === 'd') { + result = timeMS / 1000 / 60 / 60 / 24; } - } -})(); + return result; + } -(function() { - 'use strict'; + function convertTime(time) { + return moment(time).toISOString(); + } - angular.module('app.components') - .controller('MyProfileController', MyProfileController); + function formatDate(time) { + return moment(time).format('YYYY-MM-DDTHH:mm:ss'); + } - MyProfileController.$inject = ['$scope', '$location', '$q', '$interval', - 'userData', 'AuthUser', 'user', 'auth', 'alert', - 'COUNTRY_CODES', '$timeout', 'file', 'animation', - '$mdDialog', 'PreviewDevice', 'device', 'deviceUtils', - 'userUtils', '$filter', '$state', 'Restangular', '$window']; - function MyProfileController($scope, $location, $q, $interval, - userData, AuthUser, user, auth, alert, - COUNTRY_CODES, $timeout, file, animation, - $mdDialog, PreviewDevice, device, deviceUtils, - userUtils, $filter, $state, Restangular, $window) { + function getSecondsFromDate(date) { + return (new Date(date)).getTime(); + } - var vm = this; + function getMillisFromDate(date) { + return (new Date(date)).getTime(); + } - vm.unhighlightIcon = unhighlightIcon; + function getCurrentRange(fromDate, toDate) { + return moment(toDate).diff(moment(fromDate), 'days'); + } - //PROFILE TAB - vm.formUser = {}; - vm.getCountries = getCountries; + function getToday() { + return (new Date()).getTime(); + } - vm.user = userData; - copyUserToForm(vm.formUser, vm.user); - vm.searchText = vm.formUser.country; + function getSevenDaysAgo() { + return getSecondsFromDate( getToday() - (7 * 24 * 60 * 60 * 1000) ); + } - vm.updateUser = updateUser; - vm.removeUser = removeUser; - vm.uploadAvatar = uploadAvatar; + function getHourBefore(date) { + var now = moment(date); + return now.subtract(1, 'hour').valueOf(); + } - //THIS IS TEMPORARY. - // Will grow on to a dynamic API KEY management - // with the new /accounts oAuth mgmt methods + function isSameDay(day1, day2) { + day1 = moment(day1); + day2 = moment(day2); - // The auth controller has not populated the `user` at this point, - // so user.token is undefined - // This controller depends on auth has already been run. - vm.user.token = auth.getToken(); - vm.addNewDevice = addNewDevice; + if(day1.startOf('day').isSame(day2.startOf('day'))) { + return true; + } + return false; + } - //KITS TAB - vm.devices = []; - vm.deviceStatus = undefined; - vm.removeDevice = removeDevice; - vm.downloadData = downloadData; + function isDiffMoreThan15min(dateToCheckFrom, dateToCheckTo) { + var duration = moment.duration(moment(dateToCheckTo).diff(moment(dateToCheckFrom))); + return duration.as('minutes') > 15; + } - vm.filteredDevices = []; - vm.dropdownSelected = undefined; + function isWithin15min(dateToCheck) { + var fifteenMinAgo = moment().subtract(15, 'minutes').valueOf(); + dateToCheck = moment(dateToCheck).valueOf(); - //SIDEBAR - vm.filterDevices = filterDevices; - vm.filterTools = filterTools; + return dateToCheck > fifteenMinAgo; + } - vm.selectThisTab = selectThisTab; + function isWithin1Month(dateToCheck) { + var oneMonthAgo = moment().subtract(1, 'months').valueOf(); + dateToCheck = moment(dateToCheck).valueOf(); - $scope.$on('loggedOut', function() { - $location.path('/'); - }); + return dateToCheck > oneMonthAgo; + } - $scope.$on('devicesContextUpdated', function(){ - var userData = auth.getCurrentUser().data; - if(userData){ - vm.user = userData; - } - initialize(); - }); + function isWithin(number, type, dateToCheck) { + var ago = moment().subtract(number, type).valueOf(); + dateToCheck = moment(dateToCheck).valueOf(); - initialize(); + return dateToCheck > ago; + } - ////////////////// + function parseDate(object){ + var time = object; + return { + raw: time, + parsed: !time ? 'No time' : moment(time).format('MMMM DD, YYYY - HH:mm'), + ago: !time ? 'No time' : moment(time).fromNow() + } + } + } +})(); - function initialize() { +(function() { + 'use strict'; - startingTab(); - if(!vm.user.devices.length) { - vm.devices = []; - animation.viewLoaded(); - } else { + angular.module('app.components') + .factory('sensorUtils', sensorUtils); - vm.devices = vm.user.devices.map(function(data) { - return new PreviewDevice(data); - }) + sensorUtils.$inject = ['timeUtils']; + function sensorUtils(timeUtils) { + var service = { + getRollup: getRollup, + getSensorName: getSensorName, + getSensorValue: getSensorValue, + getSensorPrevValue: getSensorPrevValue, + getSensorIcon: getSensorIcon, + getSensorArrow: getSensorArrow, + getSensorColor: getSensorColor, + getSensorDescription: getSensorDescription + }; + return service; - $timeout(function() { - mapWithBelongstoUser(vm.devices); - filterDevices(vm.status); - setSidebarMinHeight(); - animation.viewLoaded(); - }); + /////////////// - } - } + function getRollup(dateFrom, dateTo) { - function filterDevices(status) { - if(status === 'all') { - status = undefined; - } - vm.deviceStatus = status; - vm.filteredDevices = $filter('filterLabel')(vm.devices, vm.deviceStatus); - } + // Calculate how many data points we can fit on a users screen + // Smaller screens request less data from the API + var durationInSec = moment(dateTo).diff(moment(dateFrom)) / 1000; + var chartWidth = window.innerWidth / 2; - function filterTools(type) { - if(type === 'all') { - type = undefined; + var rollup = parseInt(durationInSec / chartWidth) + 's'; + + /* + //var rangeDays = timeUtils.getCurrentRange(dateFrom, dateTo, {format: 'd'}); + var rollup; + if(rangeDays <= 1) { + rollup = '15s'; + } else if(rangeDays <= 7) { + rollup = '1h';//rollup = '15m'; + } else if(rangeDays > 7) { + rollup = '1d'; } - vm.toolType = type; + */ + return rollup; } - function updateUser(userData) { - if(userData.country) { - _.each(COUNTRY_CODES, function(value, key) { - if(value === userData.country) { - /*jshint camelcase: false */ - userData.country_code = key; - return; - } - }); + function getSensorName(name) { + + var sensorName; + // TODO: Improvement check how we set new names + if( new RegExp('custom circuit', 'i').test(name) ) { + sensorName = name; } else { - userData.country_code = null; + if(new RegExp('noise', 'i').test(name) ) { + sensorName = 'SOUND'; + } else if(new RegExp('light', 'i').test(name) ) { + sensorName = 'LIGHT'; + } else if((new RegExp('nets', 'i').test(name) ) || + (new RegExp('wifi', 'i').test(name))) { + sensorName = 'NETWORKS'; + } else if(new RegExp('co', 'i').test(name) ) { + sensorName = 'CO'; + } else if(new RegExp('no2', 'i').test(name) ) { + sensorName = 'NO2'; + } else if(new RegExp('humidity', 'i').test(name) ) { + sensorName = 'HUMIDITY'; + } else if(new RegExp('temperature', 'i').test(name) ) { + sensorName = 'TEMPERATURE'; + } else if(new RegExp('panel', 'i').test(name) ) { + sensorName = 'SOLAR PANEL'; + } else if(new RegExp('battery', 'i').test(name) ) { + sensorName = 'BATTERY'; + } else if(new RegExp('barometric pressure', 'i').test(name) ) { + sensorName = 'BAROMETRIC PRESSURE'; + } else if(new RegExp('PM 1', 'i').test(name) ) { + sensorName = 'PM 1'; + } else if(new RegExp('PM 2.5', 'i').test(name) ) { + sensorName = 'PM 2.5'; + } else if(new RegExp('PM 10', 'i').test(name) ) { + sensorName = 'PM 10'; + } else { + sensorName = name; + } } - - user.updateUser(userData) - .then(function(data) { - var user = new AuthUser(data); - _.extend(vm.user, user); - auth.updateUser(); - vm.errors = {}; - alert.success('User updated'); - }) - .catch(function(err) { - alert.error('User could not be updated '); - vm.errors = err.data.errors; - }); - } - - function removeUser() { - var confirm = $mdDialog.confirm() - .title('Delete your account?') - .textContent('Are you sure you want to delete your account?') - .ariaLabel('') - .ok('delete') - .cancel('cancel') - .theme('primary') - .clickOutsideToClose(true); - - $mdDialog.show(confirm) - .then(function(){ - return Restangular.all('').customDELETE('me') - .then(function(){ - alert.success('Account removed successfully. Redirecting you…'); - $timeout(function(){ - auth.logout(); - $state.transitionTo('landing'); - }, 2000); - }) - .catch(function(){ - alert.error('Error occurred trying to delete your account.'); - }); - }); + return sensorName.toUpperCase(); } - function selectThisTab(iconIndex, uistate){ - /* This looks more like a hack but we need to workout how to properly use md-tab with ui-router */ - - highlightIcon(iconIndex); + function getSensorValue(sensor) { + var value = sensor.value; - if ($state.current.name.includes('myProfileAdmin')){ - var transitionState = 'layout.myProfileAdmin.' + uistate; - $state.transitionTo(transitionState, {id: userData.id}); + if(isNaN(parseInt(value))) { + value = 'NA'; } else { - var transitionState = 'layout.myProfile.' + uistate; - $state.transitionTo(transitionState); + value = round(value, 1).toString(); } + return value; } - function startingTab() { - /* This looks more like a hack but we need to workout how to properly use md-tab with ui-router */ + function round(value, precision) { + var multiplier = Math.pow(10, precision || 0); + return Math.round(value * multiplier) / multiplier; + } - var childState = $state.current.name.split('.').pop(); + function getSensorPrevValue(sensor) { + /*jshint camelcase: false */ + var prevValue = sensor.prev_value; + return (prevValue && prevValue.toString() ) || 0; + } - switch(childState) { - case 'user': - vm.startingTab = 1; - break; - default: - vm.startingTab = 0; - break; - } + function getSensorIcon(sensorName) { - } + var thisName = getSensorName(sensorName); - function highlightIcon(iconIndex) { + switch(thisName) { + case 'TEMPERATURE': + return './assets/images/temperature_icon_new.svg'; - var icons = angular.element('.myProfile_tab_icon'); + case 'HUMIDITY': + return './assets/images/humidity_icon_new.svg'; - _.each(icons, function(icon) { - unhighlightIcon(icon); - }); + case 'LIGHT': + return './assets/images/light_icon_new.svg'; - var icon = icons[iconIndex]; + case 'SOUND': + return './assets/images/sound_icon_new.svg'; - angular.element(icon).find('.stroke_container').css({'stroke': 'white', 'stroke-width': '0.01px'}); - angular.element(icon).find('.fill_container').css('fill', 'white'); - } + case 'CO': + return './assets/images/co_icon_new.svg'; - function unhighlightIcon(icon) { - icon = angular.element(icon); + case 'NO2': + return './assets/images/no2_icon_new.svg'; - icon.find('.stroke_container').css({'stroke': 'none'}); - icon.find('.fill_container').css('fill', '#FF8600'); - } + case 'NETWORKS': + return './assets/images/networks_icon.svg'; - function setSidebarMinHeight() { - var height = document.body.clientHeight / 4 * 3; - angular.element('.profile_content').css('min-height', height + 'px'); - } + case 'BATTERY': + return './assets/images/battery_icon.svg'; - function getCountries(searchText) { - return _.filter(COUNTRY_CODES, createFilter(searchText)); - } + case 'SOLAR PANEL': + return './assets/images/solar_panel_icon.svg'; - function createFilter(searchText) { - searchText = searchText.toLowerCase(); - return function(country) { - country = country.toLowerCase(); - return country.indexOf(searchText) !== -1; - }; - } + case 'BAROMETRIC PRESSURE': + return './assets/images/pressure_icon_new.svg'; - function uploadAvatar(fileData) { - if(fileData && fileData.length) { + case 'PM 1': + case 'PM 2.5': + case 'PM 10': + return './assets/images/particle_icon_new.svg'; - // TODO: Improvement Is there a simpler way to patch the image to the API and use the response? - // Something like: - //Restangular.all('/me').patch(data); - // Instead of doing it manually like here: - var fd = new FormData(); - fd.append('profile_picture', fileData[0]); - Restangular.one('/me') - .withHttpConfig({transformRequest: angular.identity}) - .customPATCH(fd, '', undefined, {'Content-Type': undefined}) - .then(function(resp){ - vm.user.profile_picture = resp.profile_picture; - }) + default: + return './assets/images/unknownsensor_icon.svg'; } } - function copyUserToForm(formData, userData) { - var props = {username: true, email: true, city: true, country: true, country_code: true, url: true, constructor: false}; + function getSensorArrow(currentValue, prevValue) { + currentValue = parseInt(currentValue) || 0; + prevValue = parseInt(prevValue) || 0; - for(var key in userData) { - if(props[key]) { - formData[key] = userData[key]; - } + if(currentValue > prevValue) { + return 'arrow_up'; + } else if(currentValue < prevValue) { + return 'arrow_down'; + } else { + return 'equal'; } } - function mapWithBelongstoUser(devices){ - _.map(devices, addBelongProperty); - } + function getSensorColor(sensorName) { + switch(getSensorName(sensorName)) { + case 'TEMPERATURE': + return '#FF3D4C'; - function addBelongProperty(device){ - device.belongProperty = deviceBelongsToUser(device); - return device; - } + case 'HUMIDITY': + return '#55C4F5'; + case 'LIGHT': + return '#ffc107'; - function deviceBelongsToUser(device){ - if(!auth.isAuth() || !device || !device.id) { - return false; - } - var deviceID = parseInt(device.id); - var userData = ( auth.getCurrentUser().data ) || - ($window.localStorage.getItem('smartcitizen.data') && - new AuthUser( JSON.parse( - $window.localStorage.getItem('smartcitizen.data') ))); + case 'SOUND': + return '#0019FF'; - var belongsToUser = deviceUtils.belongsToUser(userData.devices, deviceID); - var isAdmin = userUtils.isAdmin(userData); + case 'CO': + return '#00A103'; - return isAdmin || belongsToUser; - } + case 'NO2': + return '#8cc252'; - function downloadData(device){ - $mdDialog.show({ - hasBackdrop: true, - controller: 'DownloadModalController', - controllerAs: 'vm', - templateUrl: 'app/components/download/downloadModal.html', - clickOutsideToClose: true, - locals: {thisDevice:device} - }).then(function(){ - var alert = $mdDialog.alert() - .title('SUCCESS') - .textContent('We are processing your data. Soon you will be notified in your inbox') - .ariaLabel('') - .ok('OK!') - .theme('primary') - .clickOutsideToClose(true); + case 'NETWORKS': + return '#681EBD'; - $mdDialog.show(alert); - }).catch(function(err){ - if (!err){ - return; - } - var errorAlert = $mdDialog.alert() - .title('ERROR') - .textContent('Uh-oh, something went wrong') - .ariaLabel('') - .ok('D\'oh') - .theme('primary') - .clickOutsideToClose(false); - - $mdDialog.show(errorAlert); - }); - } - - function removeDevice(deviceID) { - var confirm = $mdDialog.confirm() - .title('Delete this kit?') - .textContent('Are you sure you want to delete this kit?') - .ariaLabel('') - .ok('DELETE') - .cancel('Cancel') - .theme('primary') - .clickOutsideToClose(true); - - $mdDialog - .show(confirm) - .then(function(){ - device - .removeDevice(deviceID) - .then(function(){ - alert.success('Your kit was deleted successfully'); - device.updateContext(); - }) - .catch(function(){ - alert.error('Error trying to delete your kit.'); - }); - }); - } + case 'SOLAR PANEL': + return '#d555ce'; - $scope.addDeviceSelector = addDeviceSelector; - function addDeviceSelector(){ - $mdDialog.show({ - templateUrl: 'app/components/myProfile/addDeviceSelectorModal.html', - clickOutsideToClose: true, - multiple: true, - controller: DialogController, - }); - } + case 'BATTERY': + return '#ff8601'; - function DialogController($scope, $mdDialog){ - $scope.cancel = function(){ - $mdDialog.cancel(); - }; + default: + return '#0019FF'; + } } - function addNewDevice() { - var confirm = $mdDialog.confirm() - .title('Hey! Do you want to add a new kit?') - .textContent('Please, notice this currently supports just the SCK 1.0 and SCK 1.1') - .ariaLabel('') - .ok('Ok') - .cancel('Cancel') - .theme('primary') - .clickOutsideToClose(true); - - $mdDialog - .show(confirm) - .then(function(){ - $state.go('layout.kitAdd'); - }); + function getSensorDescription(sensorID, sensorTypes) { + return _(sensorTypes) + .chain() + .find(function(sensorType) { + return sensorType.id === sensorID; + }) + .value() + .measurement.description; } - - } })(); @@ -5686,772 +5166,1018 @@ angular.module('app.components') 'use strict'; angular.module('app.components') - .controller('MapTagModalController', MapTagModalController); - - MapTagModalController.$inject = ['$mdDialog', 'tag', 'selectedTags']; + .factory('searchUtils', searchUtils); - function MapTagModalController($mdDialog, tag, selectedTags) { - var vm = this; + searchUtils.$inject = []; + function searchUtils() { + var service = { + parseLocation: parseLocation, + parseName: parseName, + parseIcon: parseIcon, + parseIconType: parseIconType + }; + return service; - vm.checks = {}; + ///////////////// - vm.answer = answer; - vm.hide = hide; - vm.clear = clear; - vm.cancel = cancel; - vm.tags = []; + function parseLocation(object) { + var location = ''; - init(); + if(!!object.city) { + location += object.city; + } + if(!!object.city && !!object.country) { + location += ', '; + } + if(!!object.country) { + location += object.country; + } - //////////////////////////////////////////////////////// + return location; + } - function init() { - tag.getTags() - .then(function(tags) { - vm.tags = tags; + function parseName(object) { + var name = object.type === 'User' ? object.username : object.name; + return name; + } - _.forEach(selectedTags, select); + function parseIcon(object, type) { + switch(type) { + case 'User': + return object.profile_picture; + case 'Device': + return 'assets/images/kit.svg'; + case 'Country': + case 'City': + return 'assets/images/location_icon_normal.svg'; + } + } - }); + function parseIconType(type) { + switch(type) { + case 'Device': + return 'div'; + default: + return 'img'; + } + } } +})(); - function answer() { +(function() { + 'use strict'; - var selectedTags = _(vm.tags) - .filter(isTagSelected) - .value(); - $mdDialog.hide(selectedTags); - } + angular.module('app.components') + .factory('markerUtils', markerUtils); - function hide() { - answer(); - } + markerUtils.$inject = ['deviceUtils', 'MARKER_ICONS']; + function markerUtils(deviceUtils, MARKER_ICONS) { + var service = { + getIcon: getIcon, + getMarkerIcon: getMarkerIcon, + }; + _.defaults(service, deviceUtils); + return service; - function clear() { - $mdDialog.hide(null); - } + /////////////// - function cancel() { - answer(); - } + function getIcon(object) { + var icon; + var labels = deviceUtils.parseSystemTags(object); + var isSCKHardware = deviceUtils.isSCKHardware(object); - function isTagSelected(tag) { - return vm.checks[tag.name]; - } + if(hasLabel(labels, 'offline')) { + icon = MARKER_ICONS.markerSmartCitizenOffline; + } else if (isSCKHardware) { + icon = MARKER_ICONS.markerSmartCitizenOnline; + } else { + icon = MARKER_ICONS.markerExperimentalNormal; + } + return icon; + } - function select(tag){ - vm.checks[tag] = true; + function hasLabel(labels, targetLabel) { + return _.some(labels, function(label) { + return label === targetLabel; + }); + } + + function getMarkerIcon(marker, state) { + var markerType = marker.icon.className; + + if(state === 'active') { + marker.icon = MARKER_ICONS[markerType + 'Active']; + marker.focus = true; + } else if(state === 'inactive') { + var targetClass = markerType.split(' ')[0]; + marker.icon = MARKER_ICONS[targetClass]; + } + return marker; + } } - } })(); (function() { 'use strict'; angular.module('app.components') - .controller('MapFilterModalController', MapFilterModalController); + .factory('mapUtils', mapUtils); - MapFilterModalController.$inject = ['$mdDialog','selectedFilters', '$timeout']; + mapUtils.$inject = []; + function mapUtils() { + var service = { + getDefaultFilters: getDefaultFilters, + setDefaultFilters: setDefaultFilters, + canFilterBeRemoved: canFilterBeRemoved + }; + return service; - function MapFilterModalController($mdDialog, selectedFilters, $timeout) { + ////////////// - var vm = this; + function getDefaultFilters(filterData, defaultFilters) { + var obj = {}; + if(!filterData.indoor && !filterData.outdoor) { + obj[defaultFilters.exposure] = true; + } + if(!filterData.online && !filterData.offline) { + obj[defaultFilters.status] = true; + } + return obj; + } - vm.checks = {}; + function setDefaultFilters(filterData) { + var obj = {}; + if(!filterData.indoor || !filterData.outdoor) { + obj.exposure = filterData.indoor ? 'indoor' : 'outdoor'; + } + if(!filterData.online || !filterData.offline) { + obj.status = filterData.online ? 'online' : 'offline'; + } + return obj; + } - vm.answer = answer; - vm.hide = hide; - vm.clear = clear; - vm.cancel = cancel; - vm.toggle = toggle; - - vm.location = ['indoor', 'outdoor']; - vm.status = ['online', 'offline']; - vm.new = ['new']; - - vm.filters = []; - - init(); - - //////////////////////////////////////////////////////// - - function init() { - _.forEach(selectedFilters, select); - } - - function answer() { - vm.filters = vm.filters.concat(vm.location, vm.status, vm.new); - var selectedFilters = _(vm.filters) - .filter(isFilterSelected) - .value(); - $mdDialog.hide(selectedFilters); - } - - function hide() { - answer(); - } - - function clear() { - vm.filters = vm.filters.concat(vm.location, vm.status, vm.new); - $mdDialog.hide(vm.filters); - } - - function cancel() { - answer(); - } - - function isFilterSelected(filter) { - return vm.checks[filter]; - } - - function toggle(filters) { - $timeout(function() { - - for (var i = 0; i < filters.length - 1; i++) { - if (vm.checks[filters[i]] === false && vm.checks[filters[i]] === vm.checks[filters[i+1]]) { - for (var n = 0; n < filters.length; n++) { - vm.checks[filters[n]] = true; - } - } + function canFilterBeRemoved(filterData, filterName) { + if(filterName === 'indoor' || filterName === 'outdoor') { + return filterData.indoor && filterData.outdoor; + } else if(filterName === 'online' || filterName === 'offline') { + return filterData.online && filterData.offline; } - - }); + } } +})(); - function select(filter){ - vm.checks[filter] = true; - } - } +(function() { + 'use strict'; + angular.module('app.components') + .config(function ($provide) { + $provide.decorator('$exceptionHandler', ['$delegate', function($delegate) { + return function (exception, cause) { + /*jshint camelcase: false */ + $delegate(exception, cause); + }; + }]); + + }); })(); (function() { 'use strict'; angular.module('app.components') - .controller('MapController', MapController); + .factory('deviceUtils', deviceUtils); - MapController.$inject = ['$scope', '$state', '$stateParams', '$timeout', 'device', - '$mdDialog', 'leafletData', 'alert', - 'Marker', 'tag', 'animation', '$q']; - function MapController($scope, $state, $stateParams, $timeout, device, - $mdDialog, leafletData, alert, Marker, tag, animation, $q) { - var vm = this; - var updateType; - var focusedMarkerID; + deviceUtils.$inject = ['COUNTRY_CODES', 'device']; + function deviceUtils(COUNTRY_CODES, device) { + var service = { + parseLocation: parseLocation, + parseCoordinates: parseCoordinates, + parseSystemTags: parseSystemTags, + parseUserTags: parseUserTags, + classify: classify, + parseNotifications: parseNotifications, + parseOwner: parseOwner, + parseName: parseName, + parseString: parseString, + parseHardware: parseHardware, + parseHardwareInfo: parseHardwareInfo, + parseHardwareName: parseHardwareName, + isPrivate: isPrivate, + preciseLocation: preciseLocation, + enableForwarding: enableForwarding, + isLegacyVersion: isLegacyVersion, + isSCKHardware: isSCKHardware, + parseState: parseState, + parseAvatar: parseAvatar, + belongsToUser: belongsToUser, + parseSensorTime: parseSensorTime + }; - vm.markers = []; + return service; - var retinaSuffix = isRetina() ? '512' : '256'; - var retinaLegacySuffix = isRetina() ? '@2x' : ''; + /////////////// - var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; + function parseLocation(object) { + var location = ''; + var city = ''; + var country = ''; - vm.layers = { - baselayers: { - osm: { - name: 'OpenStreetMap', - type: 'xyz', - url: 'https://api.mapbox.com/styles/v1/mapbox/streets-v10/tiles/' + retinaSuffix + '/{z}/{x}/{y}?access_token=' + mapBoxToken - }, - legacy: { - name: 'Legacy', - type: 'xyz', - url: 'https://api.tiles.mapbox.com/v4/mapbox.streets-basic/{z}/{x}/{y}'+ retinaLegacySuffix +'.png' + '?access_token=' + mapBoxToken - }, - sat: { - name: 'Satellite', - type: 'xyz', - url: 'https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v10/tiles/' + retinaSuffix + '/{z}/{x}/{y}?access_token=' + mapBoxToken + if (object.location) { + city = object.location.city; + country = object.location.country; + if(!!city) { + location += city; } - }, - overlays: { - devices: { - name: 'Devices', - type: 'markercluster', - visible: true, - layerOptions: { - showCoverageOnHover: false - } + if(!!city && !!location) { + location += ', ' + } + if(!!country) { + location += country; } } - }; - - vm.center = { - lat: $stateParams.lat ? parseInt($stateParams.lat, 10) : 13.14950321154457, - lng: $stateParams.lng ? parseInt($stateParams.lng, 10) : -1.58203125, - zoom: $stateParams.zoom ? parseInt($stateParams.zoom, 10) : 2 - }; - - vm.defaults = { - dragging: true, - touchZoom: true, - scrollWheelZoom: true, - doubleClickZoom: true, - minZoom:2, - worldCopyJump: true - }; + return location; + } - vm.events = { - map: { - enable: ['dragend', 'zoomend', 'moveend', 'popupopen', 'popupclose', - 'mousedown', 'dblclick', 'click', 'touchstart', 'mouseup'], - logic: 'broadcast' + function parseCoordinates(object) { + if (object.location) { + return { + lat: object.location.latitude, + lng: object.location.longitude + }; } - }; + // TODO: Bug - what happens if no location? + } - $scope.$on('leafletDirectiveMarker.click', function(event, data) { - var id = undefined; - var currentMarker = vm.markers[data.modelName]; + function parseSystemTags(object) { + /*jshint camelcase: false */ + return object.system_tags; + } - if(currentMarker) { - id = currentMarker.myData.id; + function parseUserTags(object) { + return object.user_tags; + } + + function parseNotifications(object){ + return { + lowBattery: object.notify.low_battery, + stopPublishing: object.notify.stopped_publishing } + } - vm.deviceLoading = true; - vm.center.lat = data.leafletEvent.latlng.lat; - vm.center.lng = data.leafletEvent.latlng.lng; + function classify(kitType) { + if(!kitType) { + return ''; + } + return kitType.toLowerCase().split(' ').join('_'); + } - if(id === parseInt($state.params.id)) { - $timeout(function() { - vm.deviceLoading = false; - }); + function parseName(object, trim=false) { + if(!object.name) { return; } - - updateType = 'map'; - - if ($state.$current.name === 'embbed') { return; } - $state.go('layout.home.kit', {id: id}); - - // angular.element('section.map').scope().$broadcast('resizeMapHeight'); - }); - - - $scope.$on('leafletDirectiveMarker.popupclose', function() { - if(focusedMarkerID) { - var marker = vm.markers[focusedMarkerID]; - if(marker) { - vm.markers[focusedMarkerID].focus = false; - } + if (trim) { + return object.name.length <= 41 ? object.name : object.name.slice(0, 35).concat(' ... '); } - }); - - vm.readyForDevice = { - device: false, - map: false - }; - - $scope.$on('deviceLoaded', function(event, data) { - vm.readyForDevice.device = data; - }); + return object.name; + } - $scope.$watch('vm.readyForDevice', function() { - if (vm.readyForDevice.device && vm.readyForDevice.map) { - zoomDeviceAndPopUp(vm.readyForDevice.device); + function parseHardware(object) { + if (!object.hardware) { + return; } - }, true); - $scope.$on('goToLocation', function(event, data) { - goToLocation(data); - }); - - vm.filters = ['indoor', 'outdoor', 'online', 'offline']; - - vm.openFilterPopup = openFilterPopup; - vm.openTagPopup = openTagPopup; - vm.removeFilter = removeFilter; - vm.removeTag = removeTag; - vm.selectedTags = tag.getSelectedTags(); - vm.selectedFilters = ['indoor', 'outdoor', 'online', 'offline', 'new']; - - vm.checkAllFiltersSelected = checkAllFiltersSelected; - - initialize(); + return { + name: parseString(object.hardware.name), + type: parseString(object.hardware.type), + description: parseString(object.hardware.description), + version: parseVersionString(object.hardware.version), + slug: object.hardware.slug, + info: parseHardwareInfo(object.hardware.info) + } + } - ///////////////////// + function parseString(str) { + if (typeof(str) !== 'string') { return null; } + return str; + } - function initialize() { + function parseVersionString (str) { + if (typeof(str) !== 'string') { return null; } + var x = str.split('.'); + // parse from string or default to 0 if can't parse + var maj = parseInt(x[0]) || 0; + var min = parseInt(x[1]) || 0; + var pat = parseInt(x[2]) || 0; + return { + major: maj, + minor: min, + patch: pat + }; + } - vm.readyForDevice.map = false; + function parseHardwareInfo (object) { + if (!object) { return null; } // null + if (typeof(object) == 'string') { return null; } // FILTERED - $q.all([device.getAllDevices($stateParams.reloadMap)]) - .then(function(data){ + var id = parseString(object.id); + var mac = parseString(object.mac); + var time = Date(object.time); + var esp_bd = parseString(object.esp_bd); + var hw_ver = parseString(object.hw_ver); + var sam_bd = parseString(object.sam_bd); + var esp_ver = parseString(object.esp_ver); + var sam_ver = parseString(object.sam_ver); - data = data[0]; + return { + id: id, + mac: mac, + time: time, + esp_bd: esp_bd, + hw_ver: hw_ver, + sam_bd: sam_bd, + esp_ver: esp_ver, + sam_ver: sam_ver + }; + } - vm.markers = _.chain(data) - .map(function(device) { - return new Marker(device); - }) - .filter(function(marker) { - return !!marker.lng && !!marker.lat; - }) - .tap(function(data) { - device.setWorldMarkers(data); - }) - .value(); + function parseHardwareName(object) { + if (object.hasOwnProperty('hardware')) { + if (!object.hardware.name) { + return 'Unknown hardware' + } + return object.hardware.name; + } else { + return 'Unknown hardware' + } + } - var markersByIndex = _.keyBy(vm.markers, function(marker) { - return marker.myData.id; - }); + function isPrivate(object) { + return object.data_policy.is_private; + } - if($state.params.id && markersByIndex[parseInt($state.params.id)]){ - focusedMarkerID = markersByIndex[parseInt($state.params.id)] - .myData.id; - vm.readyForDevice.map = true; - } else { - updateMarkers(); - vm.readyForDevice.map = true; - } + function preciseLocation(object) { + return object.data_policy.precise_location; + } - }); + function enableForwarding(object) { + return object.data_policy.enable_forwarding ; } - function zoomDeviceAndPopUp(data){ + function isLegacyVersion (object) { + if (!object.hardware || !object.hardware.version || object.hardware.version.major > 1) { + return false; + } else { + if (object.hardware.version.major == 1 && object.hardware.version.minor <5 ){ + return true; + } + return false; + } + } - if(updateType === 'map') { - vm.deviceLoading = false; - updateType = undefined; - return; + function isSCKHardware (object){ + if (!object.hardware || !object.hardware.type || object.hardware.type != 'SCK') { + return false; } else { - vm.deviceLoading = true; + return true; } + } - leafletData.getMarkers() - .then(function(markers) { - var currentMarker = _.find(markers, function(marker) { - return data.id === marker.options.myData.id; - }); + function parseOwner(object) { + return { + id: object.owner.id, + username: object.owner.username, + /*jshint camelcase: false */ + devices: object.owner.device_ids, + city: object.owner.location.city, + country: COUNTRY_CODES[object.owner.location.country_code], + url: object.owner.url, + profile_picture: object.owner.profile_picture + }; + } - var id = data.id; + function parseState(status) { + var name = parseStateName(status); + var className = classify(name); - leafletData.getLayers() - .then(function(layers) { - if(currentMarker){ - layers.overlays.devices.zoomToShowLayer(currentMarker, - function() { - var selectedMarker = currentMarker; - if(selectedMarker) { - // Ensures the marker is not just zoomed but the marker is centered to improve UX - // The $timeout can be replaced by an event but tests didn't show good results - $timeout(function() { - vm.center.lat = selectedMarker.options.lat; - vm.center.lng = selectedMarker.options.lng; - selectedMarker.openPopup(); - vm.deviceLoading = false; - }, 1000); - } - }); - } else { - leafletData.getMap().then(function(map){ - map.closePopup(); - }); - } - }); - }); + return { + name: name, + className: className + }; + } + function parseStateName(object) { + return object.state.replace('_', ' '); } - function checkAllFiltersSelected() { - var allFiltersSelected = _.every(vm.filters, function(filterValue) { - return _.includes(vm.selectedFilters, filterValue); - }); - return allFiltersSelected; + function parseAvatar() { + return './assets/images/sckit_avatar.jpg'; } - function openFilterPopup() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'MapFilterModalController', - controllerAs: 'vm', - templateUrl: 'app/components/map/mapFilterModal.html', - clickOutsideToClose: true, - locals: { - selectedFilters: vm.selectedFilters - } - }) - .then(function(selectedFilters) { - updateType = 'map'; - vm.selectedFilters = selectedFilters; - updateMapFilters(); - }); + function parseSensorTime(sensor) { + /*jshint camelcase: false */ + return moment(sensor.recorded_at).format(''); } - function openTagPopup() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'MapTagModalController', - controllerAs: 'vm', - templateUrl: 'app/components/map/mapTagModal.html', - //targetEvent: ev, - clickOutsideToClose: true, - locals: { - selectedTags: vm.selectedTags - } - }) - .then(function(selectedTags) { - if (selectedTags && selectedTags.length > 0) { - updateType = 'map'; - tag.setSelectedTags(_.map(selectedTags, 'name')); - vm.selectedTags = tag.getSelectedTags(); - reloadWithTags(); - } else if (selectedTags === null) { - reloadNoTags(); - } + function belongsToUser(devicesArray, deviceID) { + return _.some(devicesArray, function(device) { + return device.id === deviceID; }); } + } +})(); - function updateMapFilters(){ - vm.selectedTags = tag.getSelectedTags(); - checkAllFiltersSelected(); - updateMarkers(); - } +(function() { + 'use strict'; - function removeFilter(filterName) { - vm.selectedFilters = _.filter(vm.selectedFilters, function(el){ - return el !== filterName; - }); - if(vm.selectedFilters.length === 0){ - vm.selectedFilters = vm.filters; - } - updateMarkers(); - } + angular.module('app.components') + .filter('filterLabel', filterLabel); - function filterMarkersByLabel(tmpMarkers) { - return tmpMarkers.filter(function(marker) { - var labels = marker.myData.labels; - if (labels.length === 0 && vm.selectedFilters.length !== 0){ - return false; - } - return _.every(labels, function(label) { - return _.includes(vm.selectedFilters, label); - }); - }); - } - - function updateMarkers() { - $timeout(function() { - $scope.$apply(function() { - var allMarkers = device.getWorldMarkers(); - - var updatedMarkers = allMarkers; - updatedMarkers = tag.filterMarkersByTag(updatedMarkers); - updatedMarkers = filterMarkersByLabel(updatedMarkers); - vm.markers = updatedMarkers; - - animation.mapStateLoaded(); - - vm.deviceLoading = false; - - zoomOnMarkers(); + function filterLabel() { + return function(devices, targetLabel) { + if(targetLabel === undefined) { + return devices; + } + if(devices) { + return _.filter(devices, function(device) { + var containsLabel = device.systemTags.indexOf(targetLabel) !== -1; + if(containsLabel) { + return containsLabel; + } + // This should be fixed or polished in the future + // var containsNewIfTargetIsOnline = targetLabel === 'online' && _.some(kit.labels, function(label) {return label.indexOf('new') !== -1;}); + // return containsNewIfTargetIsOnline; }); - }); - } - - function getZoomLevel(data) { - // data.layer is an array of strings like ["establishment", "point_of_interest"] - var zoom = 18; - - if(data.layer && data.layer[0]) { - switch(data.layer[0]) { - case 'point_of_interest': - zoom = 18; - break; - case 'address': - zoom = 18; - break; - case "establishment": - zoom = 15; - break; - case 'neighbourhood': - zoom = 13; - break; - case 'locality': - zoom = 13; - break; - case 'localadmin': - zoom = 9; - break; - case 'county': - zoom = 9; - break; - case 'region': - zoom = 8; - break; - case 'country': - zoom = 7; - break; - case 'coarse': - zoom = 7; - break; - } } + }; + } +})(); - return zoom; - } - - function isRetina(){ - return ((window.matchMedia && - (window.matchMedia('only screen and (min-resolution: 192dpi), ' + - 'only screen and (min-resolution: 2dppx), only screen and ' + - '(min-resolution: 75.6dpcm)').matches || - window.matchMedia('only screen and (-webkit-min-device-pixel-ra' + - 'tio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only' + - ' screen and (min--moz-device-pixel-ratio: 2), only screen and ' + - '(min-device-pixel-ratio: 2)').matches)) || - (window.devicePixelRatio && window.devicePixelRatio >= 2)) && - /(iPad|iPhone|iPod|Apple)/g.test(navigator.userAgent); - } +(function() { + 'use strict'; - function goToLocation(data){ - // This ensures the action runs after the event is registered - $timeout(function() { - vm.center.lat = data.lat; - vm.center.lng = data.lng; - vm.center.zoom = getZoomLevel(data); - }); - } + /** + * Tools links for user profile + * @constant + * @type {Array} + */ - function removeTag(tagName){ - tag.setSelectedTags(_.filter(vm.selectedTags, function(el){ - return el !== tagName; - })); + angular.module('app.components') + .constant('PROFILE_TOOLS', [{ + type: 'documentation', + title: 'How to connect your Smart Citizen Kit tutorial', + description: 'Adding a Smart Citizen Kit tutorial', + avatar: '', + href: 'http://docs.smartcitizen.me/#/start/adding-a-smart-citizen-kit' + }, { + type: 'documentation', + title: 'Download the latest Smart Citizen Kit Firmware', + description: 'The latest Arduino firmware for your kit', + avatar: '', + href: 'https://github.com/fablabbcn/Smart-Citizen-Kit/releases/latest' + }, { + type: 'documentation', + title: 'API Documentation', + description: 'Documentation for the new API', + avatar: '', + href: 'http://developer.smartcitizen.me/' + }, { + type: 'community', + title: 'Smart Citizen Forum', + description: 'Join the community discussion. Your feedback is important for us.', + avatar: '', + href:'http://forum.smartcitizen.me/' + }, { + type: 'documentation', + title: 'Smart Citizen Kit hardware details', + description: 'Visit the docs', + avatar: 'https://docs.smartcitizen.me/#/start/hardware' + }, { + type: 'documentation', + title: 'Style Guide', + description: 'Guidelines of the Smart Citizen UI', + avatar: '', + href: '/styleguide' + }, { + type: 'social', + title: 'Like us on Facebook', + description: 'Join the community on Facebook', + avatar: '', + href: 'https://www.facebook.com/smartcitizenBCN' + }, { + type: 'social', + title: 'Follow us on Twitter', + description: 'Follow our news on Twitter', + avatar: '', + href: 'https://twitter.com/SmartCitizenKit' + }]); +})(); - vm.selectedTags = tag.getSelectedTags(); +(function() { + 'use strict'; - if(vm.selectedTags.length === 0){ - reloadNoTags(); - } else { - reloadWithTags(); - } + /** + * Marker icons + * @constant + * @type {Object} + */ + angular.module('app.components') + .constant('MARKER_ICONS', { + defaultIcon: {}, + markerSmartCitizenNormal: { + type: 'div', + className: 'markerSmartCitizenNormal', + iconSize: [24, 24] + }, + markerExperimentalNormal: { + type: 'div', + className: 'markerExperimentalNormal', + iconSize: [24, 24] + }, + markerSmartCitizenOnline: { + type: 'div', + className: 'markerSmartCitizenOnline', + iconSize: [24, 24] + }, + markerSmartCitizenOnlineActive: { + type: 'div', + className: 'markerSmartCitizenOnline marker_blink', + iconSize: [24, 24] + }, + markerSmartCitizenOffline: { + type: 'div', + className: 'markerSmartCitizenOffline', + iconSize: [24, 24] + }, + markerSmartCitizenOfflineActive: { + type: 'div', + className: 'markerSmartCitizenOffline marker_blink', + iconSize: [24, 24] } + }); +})(); - function zoomOnMarkers(){ - $timeout(function() { - if(vm.markers && vm.markers.length > 0) { - leafletData.getMap().then(function(map){ - var bounds = L.latLngBounds(vm.markers); - map.fitBounds(bounds); - }); - } else { - alert.error('No markers found with those filters', 5000); - } - }); - } +(function() { + 'use strict'; - function reloadWithTags(){ - $state.transitionTo('layout.home.tags', {tags: vm.selectedTags}, {reload: true}); - } + /** + * Dropdown options for user + * @constant + * @type {Array} + */ + angular.module('app.components') + .constant('DROPDOWN_OPTIONS_USER', [ + {divider: true, text: 'Hi,', href: './profile'}, + {text: 'My profile', href: './profile'}, + {text: 'Log out', href: './logout'} + ]); +})(); - function reloadNoTags(){ - $state.transitionTo('layout.home.kit'); - } +(function() { + 'use strict'; - } + /** + * Dropdown options for community button + * @constant + * @type {Array} + */ + + angular.module('app.components') + .constant('DROPDOWN_OPTIONS_COMMUNITY', [ + {text: 'About', href: '/about'}, + {text: 'Forum', href: 'https://forum.smartcitizen.me/'}, + {text: 'Documentation', href: 'http://docs.smartcitizen.me/'}, + {text: 'API Reference', href: 'http://developer.smartcitizen.me/'}, + {text: 'Github', href: 'https://github.com/fablabbcn/Smart-Citizen-Kit'}, + {text: 'Legal', href: '/policy'} + ]); +})(); + +(function() { + 'use strict'; + /** + * Country codes. + * @constant + * @type {Object} + */ + + angular.module('app.components') + .constant('COUNTRY_CODES', { + 'AF': 'Afghanistan', + 'AX': 'Aland Islands', + 'AL': 'Albania', + 'DZ': 'Algeria', + 'AS': 'American Samoa', + 'AD': 'Andorra', + 'AO': 'Angola', + 'AI': 'Anguilla', + 'AQ': 'Antarctica', + 'AG': 'Antigua And Barbuda', + 'AR': 'Argentina', + 'AM': 'Armenia', + 'AW': 'Aruba', + 'AU': 'Australia', + 'AT': 'Austria', + 'AZ': 'Azerbaijan', + 'BS': 'Bahamas', + 'BH': 'Bahrain', + 'BD': 'Bangladesh', + 'BB': 'Barbados', + 'BY': 'Belarus', + 'BE': 'Belgium', + 'BZ': 'Belize', + 'BJ': 'Benin', + 'BM': 'Bermuda', + 'BT': 'Bhutan', + 'BO': 'Bolivia', + 'BA': 'Bosnia And Herzegovina', + 'BW': 'Botswana', + 'BV': 'Bouvet Island', + 'BR': 'Brazil', + 'IO': 'British Indian Ocean Territory', + 'BN': 'Brunei Darussalam', + 'BG': 'Bulgaria', + 'BF': 'Burkina Faso', + 'BI': 'Burundi', + 'KH': 'Cambodia', + 'CM': 'Cameroon', + 'CA': 'Canada', + 'CV': 'Cape Verde', + 'KY': 'Cayman Islands', + 'CF': 'Central African Republic', + 'TD': 'Chad', + 'CL': 'Chile', + 'CN': 'China', + 'CX': 'Christmas Island', + 'CC': 'Cocos (Keeling) Islands', + 'CO': 'Colombia', + 'KM': 'Comoros', + 'CG': 'Congo', + 'CD': 'Congo, Democratic Republic', + 'CK': 'Cook Islands', + 'CR': 'Costa Rica', + 'CI': 'Cote D\'Ivoire', + 'HR': 'Croatia', + 'CU': 'Cuba', + 'CY': 'Cyprus', + 'CZ': 'Czech Republic', + 'DK': 'Denmark', + 'DJ': 'Djibouti', + 'DM': 'Dominica', + 'DO': 'Dominican Republic', + 'EC': 'Ecuador', + 'EG': 'Egypt', + 'SV': 'El Salvador', + 'GQ': 'Equatorial Guinea', + 'ER': 'Eritrea', + 'EE': 'Estonia', + 'ET': 'Ethiopia', + 'FK': 'Falkland Islands (Malvinas)', + 'FO': 'Faroe Islands', + 'FJ': 'Fiji', + 'FI': 'Finland', + 'FR': 'France', + 'GF': 'French Guiana', + 'PF': 'French Polynesia', + 'TF': 'French Southern Territories', + 'GA': 'Gabon', + 'GM': 'Gambia', + 'GE': 'Georgia', + 'DE': 'Germany', + 'GH': 'Ghana', + 'GI': 'Gibraltar', + 'GR': 'Greece', + 'GL': 'Greenland', + 'GD': 'Grenada', + 'GP': 'Guadeloupe', + 'GU': 'Guam', + 'GT': 'Guatemala', + 'GG': 'Guernsey', + 'GN': 'Guinea', + 'GW': 'Guinea-Bissau', + 'GY': 'Guyana', + 'HT': 'Haiti', + 'HM': 'Heard Island & Mcdonald Islands', + 'VA': 'Holy See (Vatican City State)', + 'HN': 'Honduras', + 'HK': 'Hong Kong', + 'HU': 'Hungary', + 'IS': 'Iceland', + 'IN': 'India', + 'ID': 'Indonesia', + 'IR': 'Iran, Islamic Republic Of', + 'IQ': 'Iraq', + 'IE': 'Ireland', + 'IM': 'Isle Of Man', + 'IL': 'Israel', + 'IT': 'Italy', + 'JM': 'Jamaica', + 'JP': 'Japan', + 'JE': 'Jersey', + 'JO': 'Jordan', + 'KZ': 'Kazakhstan', + 'KE': 'Kenya', + 'KI': 'Kiribati', + 'KR': 'Korea', + 'KW': 'Kuwait', + 'KG': 'Kyrgyzstan', + 'LA': 'Lao People\'s Democratic Republic', + 'LV': 'Latvia', + 'LB': 'Lebanon', + 'LS': 'Lesotho', + 'LR': 'Liberia', + 'LY': 'Libyan Arab Jamahiriya', + 'LI': 'Liechtenstein', + 'LT': 'Lithuania', + 'LU': 'Luxembourg', + 'MO': 'Macao', + 'MK': 'Macedonia', + 'MG': 'Madagascar', + 'MW': 'Malawi', + 'MY': 'Malaysia', + 'MV': 'Maldives', + 'ML': 'Mali', + 'MT': 'Malta', + 'MH': 'Marshall Islands', + 'MQ': 'Martinique', + 'MR': 'Mauritania', + 'MU': 'Mauritius', + 'YT': 'Mayotte', + 'MX': 'Mexico', + 'FM': 'Micronesia, Federated States Of', + 'MD': 'Moldova', + 'MC': 'Monaco', + 'MN': 'Mongolia', + 'ME': 'Montenegro', + 'MS': 'Montserrat', + 'MA': 'Morocco', + 'MZ': 'Mozambique', + 'MM': 'Myanmar', + 'NA': 'Namibia', + 'NR': 'Nauru', + 'NP': 'Nepal', + 'NL': 'Netherlands', + 'AN': 'Netherlands Antilles', + 'NC': 'New Caledonia', + 'NZ': 'New Zealand', + 'NI': 'Nicaragua', + 'NE': 'Niger', + 'NG': 'Nigeria', + 'NU': 'Niue', + 'NF': 'Norfolk Island', + 'MP': 'Northern Mariana Islands', + 'NO': 'Norway', + 'OM': 'Oman', + 'PK': 'Pakistan', + 'PW': 'Palau', + 'PS': 'Palestinian Territory, Occupied', + 'PA': 'Panama', + 'PG': 'Papua New Guinea', + 'PY': 'Paraguay', + 'PE': 'Peru', + 'PH': 'Philippines', + 'PN': 'Pitcairn', + 'PL': 'Poland', + 'PT': 'Portugal', + 'PR': 'Puerto Rico', + 'QA': 'Qatar', + 'RE': 'Reunion', + 'RO': 'Romania', + 'RU': 'Russian Federation', + 'RW': 'Rwanda', + 'BL': 'Saint Barthelemy', + 'SH': 'Saint Helena', + 'KN': 'Saint Kitts And Nevis', + 'LC': 'Saint Lucia', + 'MF': 'Saint Martin', + 'PM': 'Saint Pierre And Miquelon', + 'VC': 'Saint Vincent And Grenadines', + 'WS': 'Samoa', + 'SM': 'San Marino', + 'ST': 'Sao Tome And Principe', + 'SA': 'Saudi Arabia', + 'SN': 'Senegal', + 'RS': 'Serbia', + 'SC': 'Seychelles', + 'SL': 'Sierra Leone', + 'SG': 'Singapore', + 'SK': 'Slovakia', + 'SI': 'Slovenia', + 'SB': 'Solomon Islands', + 'SO': 'Somalia', + 'ZA': 'South Africa', + 'GS': 'South Georgia And Sandwich Isl.', + 'ES': 'Spain', + 'LK': 'Sri Lanka', + 'SD': 'Sudan', + 'SR': 'Suriname', + 'SJ': 'Svalbard And Jan Mayen', + 'SZ': 'Swaziland', + 'SE': 'Sweden', + 'CH': 'Switzerland', + 'SY': 'Syrian Arab Republic', + 'TW': 'Taiwan', + 'TJ': 'Tajikistan', + 'TZ': 'Tanzania', + 'TH': 'Thailand', + 'TL': 'Timor-Leste', + 'TG': 'Togo', + 'TK': 'Tokelau', + 'TO': 'Tonga', + 'TT': 'Trinidad And Tobago', + 'TN': 'Tunisia', + 'TR': 'Turkey', + 'TM': 'Turkmenistan', + 'TC': 'Turks And Caicos Islands', + 'TV': 'Tuvalu', + 'UG': 'Uganda', + 'UA': 'Ukraine', + 'AE': 'United Arab Emirates', + 'GB': 'United Kingdom', + 'US': 'United States', + 'UM': 'United States Outlying Islands', + 'UY': 'Uruguay', + 'UZ': 'Uzbekistan', + 'VU': 'Vanuatu', + 'VE': 'Venezuela', + 'VN': 'Viet Nam', + 'VG': 'Virgin Islands, British', + 'VI': 'Virgin Islands, U.S.', + 'WF': 'Wallis And Futuna', + 'EH': 'Western Sahara', + 'YE': 'Yemen', + 'ZM': 'Zambia', + 'ZW': 'Zimbabwe' + }); })(); (function() { - 'use strict'; + 'use strict'; - angular.module('app.components') - .controller('LoginModalController', LoginModalController); + angular.module('app.components') + .factory('user', user); - LoginModalController.$inject = ['$scope', '$mdDialog', 'auth', 'animation']; - function LoginModalController($scope, $mdDialog, auth, animation) { - const vm = this; - $scope.answer = function(answer) { - $scope.waitingFromServer = true; - auth.login(answer) - .then(function(data) { - /*jshint camelcase: false */ - var token = data.access_token; - auth.saveToken(token); - $mdDialog.hide(); - }) - .catch(function(err) { - vm.errors = err.data; - }) - .finally(function() { - $scope.waitingFromServer = false; - }); - }; - $scope.hide = function() { - $mdDialog.hide(); - }; - $scope.cancel = function() { - $mdDialog.hide(); + user.$inject = ['Restangular']; + function user(Restangular) { + var service = { + createUser: createUser, + getUser: getUser, + updateUser: updateUser }; + return service; - $scope.openSignup = function() { - animation.showSignup(); - $mdDialog.hide(); - }; + //////////////////// - $scope.openPasswordRecovery = function() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'PasswordRecoveryModalController', - templateUrl: 'app/components/passwordRecovery/passwordRecoveryModal.html', - clickOutsideToClose: true - }); + function createUser(signupData) { + return Restangular.all('users').post(signupData); + } - $mdDialog.hide(); - }; - } + function getUser(id) { + return Restangular.one('users', id).get(); + } + + function updateUser(updateData) { + return Restangular.all('me').customPUT(updateData); + } + } })(); (function() { 'use strict'; - angular.module('app.components') - .directive('login', login); + angular.module('app.components') + .factory('tag', tag); - function login() { - return { - scope: { - show: '=' - }, - restrict: 'A', - controller: 'LoginController', - controllerAs: 'vm', - templateUrl: 'app/components/login/login.html' + tag.$inject = ['Restangular']; + function tag(Restangular) { + var tags = []; + var selectedTags = []; + + var service = { + getTags: getTags, + getSelectedTags: getSelectedTags, + setSelectedTags: setSelectedTags, + tagWithName: tagWithName, + filterMarkersByTag: filterMarkersByTag }; - } -})(); -(function() { - 'use strict'; + return service; - angular.module('app.components') - .controller('LoginController', LoginController); + ///////////////// - LoginController.$inject = ['$scope', '$mdDialog']; - function LoginController($scope, $mdDialog) { + function getTags() { + return Restangular.all('tags') + .getList({'per_page': 200}) + .then(function(fetchedTags){ + tags = fetchedTags.plain(); + return tags; + }); + } - $scope.showLogin = showLogin; + function getSelectedTags(){ + return selectedTags; + } + + function setSelectedTags(tags){ + selectedTags = tags; + } - $scope.$on('showLogin', function() { - showLogin(); - }); + function tagWithName(name){ + var result = _.where(tags, {name: name}); + if (result && result.length > 0){ + return result[0]; + }else{ + return; + } + } - //////////////// + function filterMarkersByTag(tmpMarkers) { + var markers = filterMarkers(tmpMarkers); + return markers; + } - function showLogin() { - $mdDialog.show({ - hasBackdrop: true, - fullscreen: true, - controller: 'LoginModalController', - controllerAs: 'vm', - templateUrl: 'app/components/login/loginModal.html', - clickOutsideToClose: true - }); + function filterMarkers(tmpMarkers) { + if (service.getSelectedTags().length === 0){ + return tmpMarkers; + } + return tmpMarkers.filter(function(marker) { + var tags = marker.myData.tags; + if (tags.length === 0){ + return false; + } + return _.some(tags, function(tag) { + return _.includes(service.getSelectedTags(), tag); + }); + }); + } } - - } })(); (function() { 'use strict'; angular.module('app.components') - .controller('LayoutController', LayoutController); + .factory('sensor', sensor); - LayoutController.$inject = ['$mdSidenav','$mdDialog', '$location', '$state', '$scope', '$transitions', 'auth', 'animation', '$timeout', 'DROPDOWN_OPTIONS_COMMUNITY', 'DROPDOWN_OPTIONS_USER']; - function LayoutController($mdSidenav, $mdDialog, $location, $state, $scope, $transitions, auth, animation, $timeout, DROPDOWN_OPTIONS_COMMUNITY, DROPDOWN_OPTIONS_USER) { - var vm = this; + sensor.$inject = ['Restangular', 'timeUtils', 'sensorUtils']; + function sensor(Restangular, timeUtils, sensorUtils) { + var sensorTypes; + callAPI().then(function(data) { + setTypes(data); + }); - vm.navRightLayout = 'space-around center'; + var service = { + callAPI: callAPI, + setTypes: setTypes, + getTypes: getTypes, + getSensorsData: getSensorsData + }; + return service; - $scope.toggleRight = buildToggler('right'); + //////////////// - function buildToggler(componentId) { - return function() { - $mdSidenav(componentId).toggle(); - }; + function callAPI() { + return Restangular.all('sensors').getList({'per_page': 1000}); } - // listen for any login event so that the navbar can be updated - $scope.$on('loggedIn', function(ev, options) { - // if(options && options.time === 'appLoad') { - // $scope.$apply(function() { - // vm.isLoggedin = true; - // vm.isShown = true; - // angular.element('.nav_right .wrap-dd-menu').css('display', 'initial'); - // vm.currentUser = auth.getCurrentUser().data; - // vm.dropdownOptions[0].text = 'Hello, ' + vm.currentUser.username; - // vm.navRightLayout = 'end center'; - // }); - // } else { - // vm.isLoggedin = true; - // vm.isShown = true; - // angular.element('.nav_right .wrap-dd-menu').css('display', 'initial'); - // vm.currentUser = auth.getCurrentUser().data; - // vm.dropdownOptions[0].text = 'Hello, ' + vm.currentUser.username; - // vm.navRightLayout = 'end center'; - // } - - vm.isLoggedin = true; - vm.isShown = true; - angular.element('.nav_right .wrap-dd-menu').css('display', 'initial'); - vm.currentUser = auth.getCurrentUser().data; - vm.dropdownOptions[0].text = 'Hi, ' + vm.currentUser.username + '!'; - vm.navRightLayout = 'end center'; - if(!$scope.$$phase) { - $scope.$digest(); - } - }); - - // listen for logout events so that the navbar can be updated - $scope.$on('loggedOut', function() { - vm.isLoggedIn = false; - vm.isShown = true; - angular.element('navbar .wrap-dd-menu').css('display', 'none'); - vm.navRightLayout = 'space-around center'; - }); - - - vm.isShown = true; - vm.isLoggedin = false; - vm.logout = logout; + function setTypes(sensorTypes) { + sensorTypes = sensorTypes; + } - vm.dropdownOptions = DROPDOWN_OPTIONS_USER; - vm.dropdownSelected = undefined; + function getTypes() { + return sensorTypes; + } - vm.dropdownOptionsCommunity = DROPDOWN_OPTIONS_COMMUNITY; - vm.dropdownSelectedCommunity = undefined; + function getSensorsData(deviceID, sensorID, dateFrom, dateTo) { + var rollup = sensorUtils.getRollup(dateFrom, dateTo); + dateFrom = timeUtils.convertTime(dateFrom); + dateTo = timeUtils.convertTime(dateTo); - $scope.$on('removeNav', function() { - vm.isShown = false; - }); + return Restangular.one('devices', deviceID).customGET('readings', {'from': dateFrom, 'to': dateTo, 'rollup': rollup, 'sensor_id': sensorID, 'all_intervals': true}); + } + } +})(); - $scope.$on('addNav', function() { - vm.isShown = true; - }); +(function() { + 'use strict'; - initialize(); + angular.module('app.components') + .factory('search', search); + + search.$inject = ['$http', 'Restangular']; + function search($http, Restangular) { + var service = { + globalSearch: globalSearch + }; - ////////////////// + return service; - function initialize() { - $timeout(function() { - var hash = $location.search(); - if(hash.signup) { - animation.showSignup(); - } else if(hash.login) { - animation.showLogin(); - } else if(hash.passwordRecovery) { - animation.showPasswordRecovery(); - } - }, 1000); - } + ///////////////////////// - function logout() { - auth.logout(); - vm.isLoggedin = false; + function globalSearch(query) { + return Restangular.all('search').getList({q: query}); } } })(); @@ -6460,951 +6186,1225 @@ angular.module('app.components') 'use strict'; angular.module('app.components') - .controller('LandingController', LandingController); + .factory('measurement', measurement); - LandingController.$inject = ['$timeout', 'animation', '$mdDialog', '$location', '$anchorScroll']; + measurement.$inject = ['Restangular']; - function LandingController($timeout, animation, $mdDialog, $location, $anchorScroll) { - var vm = this; + function measurement(Restangular) { - vm.showStore = showStore; - vm.goToHash = goToHash; + var service = { + getTypes: getTypes, + getMeasurement: getMeasurement - /////////////////////// + }; + return service; - initialize(); + //////////////// - ////////////////// - function initialize() { - $timeout(function() { - animation.viewLoaded(); - if($location.hash()) { - $anchorScroll(); - } - }, 500); + function getTypes() { + return Restangular.all('measurements').getList({'per_page': 1000}); } - function goToHash(hash){ - $location.hash(hash); - $anchorScroll(); - } + function getMeasurement(mesID) { - function showStore() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'StoreModalController', - templateUrl: 'app/components/store/storeModal.html', - clickOutsideToClose: true - }); + return Restangular.one('measurements', mesID).get(); } } })(); +(function() { + 'use strict'; -(function(){ - 'use strict'; - angular.module('app.components') - .directive('kitList',kitList); + angular.module('app.components') + .factory('geolocation', geolocation); - function kitList(){ - return{ - restrict:'E', - scope:{ - devices:'=devices', - actions: '=actions' - }, - controllerAs:'vm', - templateUrl:'app/components/kitList/kitList.html' - }; - } + geolocation.$inject = ['$http', '$window']; + function geolocation($http, $window) { + + var service = { + grantHTML5Geolocation: grantHTML5Geolocation, + isHTML5GeolocationGranted: isHTML5GeolocationGranted + }; + return service; + + /////////////////////////// + + + function grantHTML5Geolocation(){ + $window.localStorage.setItem('smartcitizen.geolocation_granted', true); + } + + function isHTML5GeolocationGranted(){ + return $window.localStorage + .getItem('smartcitizen.geolocation_granted'); + } + } })(); (function() { - 'use strict'; + 'use strict'; - angular.module('app.components') - .controller('HomeController', HomeController); + angular.module('app.components') + .factory('file', file); - function HomeController() { - } + file.$inject = ['Restangular', 'Upload']; + function file(Restangular, Upload) { + var service = { + getCredentials: getCredentials, + uploadFile: uploadFile, + getImageURL: getImageURL + }; + return service; + + /////////////// + + function getCredentials(filename) { + var data = { + filename: filename + }; + return Restangular.all('me/avatar').post(data); + } + + function uploadFile(fileData, key, policy, signature) { + return Upload.upload({ + url: 'https://smartcitizen.s3-eu-west-1.amazonaws.com', + method: 'POST', + data: { + key: key, + policy: policy, + signature: signature, + AWSAccessKeyId: 'AKIAJ753OQI6JPSDCPHA', + acl: 'public-read', + "Content-Type": fileData.type || 'application/octet-stream', + /*jshint camelcase: false */ + success_action_status: 200, + file: fileData + } + }); + } + + function getImageURL(filename, size) { + size = size === undefined ? 's101' : size; + + return 'https://images.smartcitizen.me/' + size + '/' + filename; + } + } })(); -(function (){ + +(function() { 'use strict'; angular.module('app.components') - .controller('DownloadModalController', DownloadModalController); - - DownloadModalController.$inject = ['thisDevice', 'device', '$mdDialog']; + .factory('device', device); - function DownloadModalController(thisDevice, device, $mdDialog) { - var vm = this; + device.$inject = ['Restangular', '$window', 'timeUtils','$http', 'auth', '$rootScope']; + function device(Restangular, $window, timeUtils, $http, auth, $rootScope) { + var worldMarkers; - vm.device = thisDevice; - vm.download = download; - vm.cancel = cancel; + initialize(); - //////////////////////////// + var service = { + getDevices: getDevices, + getAllDevices: getAllDevices, + getDevice: getDevice, + createDevice: createDevice, + updateDevice: updateDevice, + getWorldMarkers: getWorldMarkers, + setWorldMarkers: setWorldMarkers, + mailReadings: mailReadings, + postReadings: postReadings, + removeDevice: removeDevice, + updateContext: updateContext + }; - function download(){ - device.mailReadings(vm.device) - .then(function (){ - $mdDialog.hide(); - }).catch(function(err){ - $mdDialog.cancel(err); - }); - } + return service; - function cancel(){ - $mdDialog.cancel(); - } - } + ////////////////////////// -})(); + function initialize() { + if(areMarkersOld()) { + removeMarkers(); + } + } -(function(){ -'use strict'; + function getDevices(location) { + var parameter = ''; + parameter += location.lat + ',' + location.lng; + return Restangular.all('devices').getList({near: parameter, 'per_page': '100'}); + } -angular.module('app.components') - .directive('cookiesLaw', cookiesLaw); + function getAllDevices(forceReload) { + if (forceReload || auth.isAuth()) { + return getAllDevicesNoCached(); + } else { + return getAllDevicesCached(); + } + } + function getAllDevicesCached() { + return Restangular.all('devices/world_map') + .getList() + .then(function(fetchedDevices){ + return fetchedDevices.plain(); + }); + } -cookiesLaw.$inject = ['$cookies']; + function getAllDevicesNoCached() { + return Restangular.all('devices/fresh_world_map') + .getList() + .then(function(fetchedDevices){ + return fetchedDevices.plain(); + }); + } -function cookiesLaw($cookies) { - return { - template: - '
' + - 'This site uses cookies to offer you a better experience. ' + - ' Accept or' + - ' Learn More. ' + - '
', - controller: function($scope) { + function getDevice(id) { + return Restangular.one('devices', id).get(); + } - var init = function(){ - $scope.isCookieValid(); + function createDevice(data) { + return Restangular.all('devices').post(data); } - // Helpers to debug - // You can also use `document.cookie` in the browser dev console. - //console.log($cookies.getAll()); + function updateDevice(id, data) { + return Restangular.one('devices', id).patch(data); + } - $scope.isCookieValid = function() { - // Use a boolean for the ng-hide, because using a function with ng-hide - // is considered bad practice. The digest cycle will call it multiple - // times, in our case around 240 times. - $scope.isCookieValidBool = ($cookies.get('consent') === 'true') + function getWorldMarkers() { + return worldMarkers || ($window.localStorage.getItem('smartcitizen.markers') && JSON.parse($window.localStorage.getItem('smartcitizen.markers') ).data); } - $scope.acceptCookie = function() { - //console.log('Accepting cookie...'); - var today = new Date(); - var expireDate = new Date(today); - expireDate.setMonth(today.getMonth() + 6); + function setWorldMarkers(data) { + var obj = { + timestamp: new Date(), + data: data + }; + try { + $window.localStorage.setItem('smartcitizen.markers', JSON.stringify(obj) ); + } catch (e) { + console.log("Could not store markers in localstorage. skipping..."); + } + worldMarkers = obj.data; + } - $cookies.put('consent', true, {'expires' : expireDate.toUTCString()} ); + function getTimeStamp() { + return ($window.localStorage.getItem('smartcitizen.markers') && + JSON.parse($window.localStorage + .getItem('smartcitizen.markers') ).timestamp); + } - // Trigger the check again, after we click - $scope.isCookieValid(); - }; + function areMarkersOld() { + var markersDate = getTimeStamp(); + return !timeUtils.isWithin(1, 'minutes', markersDate); + } - init(); + function removeMarkers() { + worldMarkers = null; + $window.localStorage.removeItem('smartcitizen.markers'); + } - } - }; -} + function mailReadings(kit) { + return Restangular + .one('devices', kit.id) + .customGET('readings/csv_archive'); + } + + function postReadings(kit, readings) { + return Restangular + .one('devices', kit.id) + .post('readings', readings); + } + + function removeDevice(deviceID){ + return Restangular + .one('devices', deviceID) + .remove().then(function () { + $rootScope.$broadcast('devicesContextUpdated'); + }) + ; + } + function updateContext (){ + return auth.updateUser().then(function(){ + removeMarkers(); + $rootScope.$broadcast('devicesContextUpdated'); + }); + } + } })(); (function() { 'use strict'; angular.module('app.components') - .directive('chart', chart); - - chart.$inject = ['sensor', 'animation', '$timeout', '$window']; - function chart(sensor, animation, $timeout, $window) { - var margin, width, height, svg, xScale, yScale0, yScale1, xAxis, yAxisLeft, yAxisRight, dateFormat, areaMain, valueLineMain, areaCompare, valueLineCompare, focusCompare, focusMain, popup, dataMain, colorMain, yAxisScale, unitMain, popupContainer; - - return { - link: link, - restrict: 'A', - scope: { - chartData: '=' - } - }; + .factory('auth', auth); - function link(scope, elem) { + auth.$inject = ['$location', '$window', '$state', 'Restangular', + '$rootScope', 'AuthUser', '$timeout', 'alert', '$cookies']; + function auth($location, $window, $state, Restangular, $rootScope, AuthUser, + $timeout, alert, $cookies) { - $timeout(function() { - createChart(elem[0]); - }, 0); + var user = {}; - var lastData = {}; + //wait until http interceptor is added to Restangular + $timeout(function() { + initialize(); + }, 100); - // on window resize, it re-renders the chart to fit into the new window size - angular.element($window).on('resize', function() { - createChart(elem[0]); - updateChartData(lastData.data, {type: lastData.type, container: elem[0], color: lastData.color, unit: lastData.unit}); - }); + var service = { + isAuth: isAuth, + setCurrentUser: setCurrentUser, + getCurrentUser: getCurrentUser, + updateUser: updateUser, + saveToken: saveToken, + getToken: getToken, + login: login, + logout: logout, + recoverPassword: recoverPassword, + getResetPassword: getResetPassword, + patchResetPassword: patchResetPassword, + isAdmin: isAdmin + }; + return service; - scope.$watch('chartData', function(newData) { - if(!newData) { - return; - } + ////////////////////////// - if(newData !== undefined) { - // if there's data for 2 sensors - if(newData[0] && newData[1]) { - var sensorDataMain = newData[0].data; - // we could get some performance from saving the map in the showKit controller on line 218 and putting that logic in here - var dataMain = sensorDataMain.map(function(dataPoint) { - return { - date: dateFormat(dataPoint.time), - count: dataPoint && dataPoint.count, - value: dataPoint && dataPoint.value - }; - }); - // sort data points by date - dataMain.sort(function(a, b) { - return a.date - b.date; - }); + function initialize() { + //console.log('---- AUTH INIT -----'); + setCurrentUser('appLoad'); + } - var sensorDataCompare = newData[1].data; - var dataCompare = sensorDataCompare.map(function(dataPoint) { - return { - date: dateFormat(dataPoint.time), - count: dataPoint && dataPoint.count, - value: dataPoint && dataPoint.value - }; - }); + //run on app initialization so that we can keep auth across different sessions + // 1. Check if token in cookie exists. Return if it doesn't, user needs to login (and save a token to the cookie) + // 2. Populate user.data with the response from the API. + // 3. Broadcast logged in + function setCurrentUser(time) { + // TODO later: Should we check if token is expired here? + if (getToken()) { + user.token = getToken(); + }else{ + //console.log('token not found in cookie, returning'); + return; + } - dataCompare.sort(function(a, b) { - return a.date - b.date; - }); + return getCurrentUserFromAPI() + .then(function(data) { + // Save user.data also in localStorage. It is beeing used across the app. + // Should it instead just be saved in the user object? Or is it OK to also have it in localStorage? + $window.localStorage.setItem('smartcitizen.data', JSON.stringify(data.plain()) ); - var data = [dataMain, dataCompare]; - var colors = [newData[0].color, newData[1].color]; - var units = [newData[0].unit, newData[1].unit]; - // saves everything in case we need to re-render - lastData = { - data: data, - type: 'both', - color: colors, - unit: units - }; - // call function to update the chart with the new data - updateChartData(data, {type: 'both', container: elem[0], color: colors, unit: units }); - // if only data for the main sensor - } else if(newData[0]) { + var newUser = new AuthUser(data); + //check sensitive information + if(user.data && user.data.role !== newUser.role) { + user.data = newUser; + $location.path('/'); + } + user.data = newUser; - var sensorData = newData[0].data; - /*jshint -W004 */ - var data = sensorData.map(function(dataPoint) { - return { - date: dateFormat(dataPoint.time), - count: dataPoint && dataPoint.count, - value: dataPoint && dataPoint.value - }; - }); + //console.log('-- User populated with data: ', user) + // Broadcast happens 2x, so the user wont think he is not logged in. + // The 2nd broadcast waits 3sec, because f.x. on the /kits/ page, the layout has not loaded when the broadcast is sent + $rootScope.$broadcast('loggedIn'); - data.sort(function(a, b) { - return a.date - b.date; - }); + // used for app initialization + if(time && time === 'appLoad') { + //wait until navbar is loaded to emit event + $timeout(function() { + $rootScope.$broadcast('loggedIn', {time: 'appLoad'}); + }, 3000); + } else { + // used for login + //$state.reload(); + $timeout(function() { + alert.success('Login was successful'); + $rootScope.$broadcast('loggedIn', {}); + }, 2000); + } + }); + } - var color = newData[0].color; - var unit = newData[0].unit; + // Called from device.service.js updateContext(), which is called from multiple /kit/ pages + function updateUser() { + return getCurrentUserFromAPI() + .then(function(data) { + // TODO: Should this update the token or user.data? Then it could instead call setCurrentUser? + $window.localStorage.setItem('smartcitizen.data', JSON.stringify(data.plain()) ); + return getCurrentUser(); + }); + } - lastData = { - data: data, - type: 'main', - color: color, - unit: unit - }; + function getCurrentUser() { + user.token = getToken(); + user.data = $window.localStorage.getItem('smartcitizen.data') && new AuthUser(JSON.parse( $window.localStorage.getItem('smartcitizen.data') )); + return user; + } - updateChartData(data, {type: 'main', container: elem[0], color: color, unit: unit }); - } - animation.hideChartSpinner(); - } - }); + // Should check if user.token exists - but now checks if the cookies.token exists. + function isAuth() { + // TODO: isAuth() is called from many different services BEFORE auth.init has run. + // That means that the user.token is EMPTY, meaning isAuth will be false + // We can cheat and just check the cookie, but we should NOT. Because auth.init should also check if the cookie is valid / expired + // Ideally it should return !!user.token + //return !!user.token; + return !!getToken(); } - // creates the container that is re-used across different sensor charts - function createChart(elem) { - d3.select(elem).selectAll('*').remove(); + // LoginModal calls this after it receives the token from the API, and wants to save it in a cookie. + function saveToken(token) { + //console.log('saving Token to cookie:', token); + $cookies.put('smartcitizen.token', token); + setCurrentUser(); + } - margin = {top: 20, right: 12, bottom: 20, left: 42}; - width = elem.clientWidth - margin.left - margin.right; - height = elem.clientHeight - margin.top - margin.bottom; + function getToken(){ + return $cookies.get('smartcitizen.token'); + } - xScale = d3.time.scale().range([0, width]); - xScale.tickFormat("%Y-%m-%d %I:%M:%S"); - yScale0 = d3.scale.linear().range([height, 0]); - yScale1 = d3.scale.linear().range([height, 0]); - yAxisScale = d3.scale.linear().range([height, 0]); + function login(loginData) { + return Restangular.all('sessions').post(loginData); + } - dateFormat = d3.time.format('%Y-%m-%dT%H:%M:%S').parse;//d3.time.format('%Y-%m-%dT%X.%LZ').parse; //'YYYY-MM-DDTHH:mm:ssZ' + function logout() { + $cookies.remove('smartcitizen.token'); + } - xAxis = d3.svg.axis() - .scale(xScale) - .orient('bottom') - .ticks(5); + function getCurrentUserFromAPI() { + return Restangular.all('').customGET('me'); + } - yAxisLeft = d3.svg.axis() - .scale(yScale0) - .orient('left') - .ticks(5); + function recoverPassword(data) { + return Restangular.all('password_resets').post(data); + } - yAxisRight = d3.svg.axis() - .scale(yScale1) - .orient('right') - .ticks(5); + function getResetPassword(code) { + return Restangular.one('password_resets', code).get(); + } + function patchResetPassword(code, data) { + return Restangular.one('password_resets', code).patch(data); + } + function isAdmin(userData) { + return userData.role === 'admin'; + } + } +})(); - areaMain = d3.svg.area() - .defined(function(d) {return d.value != null }) - .interpolate('linear') - .x(function(d) { return xScale(d.date); }) - .y0(height) - .y1(function(d) { return yScale0(d.count); }); +(function() { + 'use strict'; - valueLineMain = d3.svg.line() - .defined(function(d) {return d.value != null }) - .interpolate('linear') - .x(function(d) { return xScale(d.date); }) - .y(function(d) { return yScale0(d.count); }); + /** + * Unused directive. Double-check before removing. + * + */ + angular.module('app.components') + .directive('slide', slide) + .directive('slideMenu', slideMenu); - areaCompare = d3.svg.area() - .defined(function(d) {return d.value != null }) - .interpolate('linear') - .x(function(d) { return xScale(d.date); }) - .y0(height) - .y1(function(d) { return yScale1(d.count); }); + function slideMenu() { + return { + controller: controller, + link: link + }; - valueLineCompare = d3.svg.line() - .defined(function(d) {return d.value != null }) - .interpolate('linear') - .x(function(d) { return xScale(d.date); }) - .y(function(d) { return yScale1(d.count); }); + function link(scope, element) { + scope.element = element; + } - svg = d3 - .select(elem) - .append('svg') - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', 'translate(' + (margin.left - margin.right) + ',' + margin.top + ')'); + function controller($scope) { + $scope.slidePosition = 0; + $scope.slideSize = 20; + + this.getTimesSlided = function() { + return $scope.slideSize; + }; + this.getPosition = function() { + return $scope.slidePosition * $scope.slideSize; + }; + this.decrementPosition = function() { + $scope.slidePosition -= 1; + }; + this.incrementPosition = function() { + $scope.slidePosition += 1; + }; + this.scrollIsValid = function(direction) { + var scrollPosition = $scope.element.scrollLeft(); + console.log('scrollpos', scrollPosition); + if(direction === 'left') { + return scrollPosition > 0 && $scope.slidePosition >= 0; + } else if(direction === 'right') { + return scrollPosition < 300; + } + }; } - // calls functions depending on type of chart - function updateChartData(newData, options) { - if(options.type === 'main') { - updateChartMain(newData, options); - } else if(options.type === 'both') { - updateChartCompare(newData, options); + } + + slide.$inject = []; + function slide() { + return { + link: link, + require: '^slide-menu', + restrict: 'A', + scope: { + direction: '@' } - } - // function in charge of rendering when there's data for 1 sensor - function updateChartMain(data, options) { - xScale.domain(d3.extent(data, function(d) { return d.date; })); - yScale0.domain([(d3.min(data, function(d) { return d.count; })) * 0.8, (d3.max(data, function(d) { return d.count; })) * 1.2]); + }; - svg.selectAll('*').remove(); + function link(scope, element, attr, slideMenuCtrl) { + //select first sensor container + var sensorsContainer = angular.element('.sensors_container'); - //Add the area path - svg.append('path') - .datum(data) - .attr('class', 'chart_area') - .attr('fill', options.color) - .attr('d', areaMain); + element.on('click', function() { - // Add the valueline path. - svg.append('path') - .attr('class', 'chart_line') - .attr('stroke', options.color) - .attr('d', valueLineMain(data)); + if(slideMenuCtrl.scrollIsValid('left') && attr.direction === 'left') { + slideMenuCtrl.decrementPosition(); + sensorsContainer.scrollLeft(slideMenuCtrl.getPosition()); + console.log(slideMenuCtrl.getPosition()); + } else if(slideMenuCtrl.scrollIsValid('right') && attr.direction === 'right') { + slideMenuCtrl.incrementPosition(); + sensorsContainer.scrollLeft(slideMenuCtrl.getPosition()); + console.log(slideMenuCtrl.getPosition()); + } + }); + } + } +})(); - // Add the X Axis - svg.append('g') - .attr('class', 'axis x') - .attr('transform', 'translate(0,' + height + ')') - .call(xAxis); +(function() { + 'use strict'; - // Add the Y Axis - svg.append('g') - .attr('class', 'axis y_left') - .call(yAxisLeft); + angular.module('app.components') + .directive('showPopupInfo', showPopupInfo); - // Draw the x Grid lines - svg.append('g') - .attr('class', 'grid') - .attr('transform', 'translate(0,' + height + ')') - .call(xGrid() - .tickSize(-height, 0, 0) - .tickFormat('') - ); + /** + * Used to show/hide explanation of sensor value at kit dashboard + * + */ + showPopupInfo.$inject = []; + function showPopupInfo() { + return { + link: link + }; - // Draw the y Grid lines - svg.append('g') - .attr('class', 'grid') - .call(yGrid() - .tickSize(-width, 0, 0) - .tickFormat('') - ); + ////// - focusMain = svg.append('g') - .attr('class', 'focus') - .style('display', 'none'); - focusMain.append('circle') - .style('stroke', options.color) - .attr('r', 4.5); + function link(scope, elem) { + elem.on('mouseenter', function() { + angular.element('.sensor_data_description').css('display', 'inline-block'); + }); + elem.on('mouseleave', function() { + angular.element('.sensor_data_description').css('display', 'none'); + }); + } + } +})(); - var popupWidth = 84; - var popupHeight = 46; +(function() { + 'use strict'; - popup = svg.append('g') - .attr('class', 'focus') - .style('display', 'none'); + angular.module('app.components') + .directive('showPopup', showPopup); - popupContainer = popup.append('rect') - .attr('width', popupWidth) - .attr('height', popupHeight) - .attr('transform', function() { - var result = 'translate(-42, 5)'; + /** + * Used on kit dashboard to open full sensor description + */ - return result; - }) - .style('stroke', 'grey') - .style('stroke-width', '0.5') - .style('fill', 'white'); + showPopup.$inject = []; + function showPopup() { + return { + link: link + }; - var text = popup.append('text') - .attr('class', ''); + ///// - var textMain = text.append('tspan') - .attr('class', 'popup_main') - .attr('text-anchor', 'start') - .attr('x', -popupWidth / 2) - .attr('dx', 8) - .attr('y', popupHeight / 2) - .attr('dy', 3); + function link(scope, element) { + element.on('click', function() { + var text = angular.element('.sensor_description_preview').text(); + if(text.length < 140) { + return; + } + angular.element('.sensor_description_preview').hide(); + angular.element('.sensor_description_full').show(); + }); + } + } +})(); - textMain.append('tspan') - .attr('class', 'popup_value'); +(function() { + 'use strict'; - textMain.append('tspan') - .attr('class', 'popup_unit') - .attr('dx', 5); + angular.module('app.components') + .directive('moveFilters', moveFilters); - text.append('tspan') - .attr('class', 'popup_date') - .attr('x', -popupWidth / 2) - .attr('dx', 8) - .attr('y', popupHeight - 2) - .attr('dy', 0) - .attr( 'text-anchor', 'start' ); + /** + * Moves map filters when scrolling + * + */ + moveFilters.$inject = ['$window', '$timeout']; + function moveFilters($window, $timeout) { + return { + link: link + }; - svg.append('rect') - .attr('class', 'overlay') - .attr('width', width) - .attr('height', height) - .on('mouseover', function() { - popup.style('display', null); - focusMain.style('display', null); - }) - .on('mouseout', function() { - popup.style('display', 'none'); - focusMain.style('display', 'none'); - }) - .on('mousemove', mousemove); + function link() { + var chartHeight; + $timeout(function() { + chartHeight = angular.element('.kit_chart').height(); + }, 1000); + /* + angular.element($window).on('scroll', function() { + var windowPosition = document.body.scrollTop; + if(chartHeight > windowPosition) { + elem.css('bottom', 12 + windowPosition + 'px'); + } + }); + */ + } + } +})(); +(function() { + 'use strict'; - function mousemove() { - var bisectDate = d3.bisector(function(d) { return d.date; }).left; + angular.module('app.components') + .factory('layout', layout); - var x0 = xScale.invert(d3.mouse(this)[0]); - var i = bisectDate(data, x0, 1); - var d0 = data[i - 1]; - var d1 = data[i]; - var d = d1 && (x0 - d0.date > d1.date - x0) ? d1 : d0; - focusMain.attr('transform', 'translate(' + xScale(d.date) + ', ' + yScale0(d.count) + ')'); - var popupText = popup.select('text'); - var textMain = popupText.select('.popup_main'); - var valueMain = textMain.select('.popup_value').text(parseValue(d.value)); - var unitMain = textMain.select('.popup_unit').text(options.unit); - var date = popupText.select('.popup_date').text(parseTime(d.date)); + function layout() { - var textContainers = [ - textMain, - date - ]; + var kitHeight; - var popupWidth = resizePopup(popupContainer, textContainers); + var service = { + setKit: setKit, + getKit: getKit + }; + return service; - if(xScale(d.date) + 80 + popupWidth > options.container.clientWidth) { - popup.attr('transform', 'translate(' + (xScale(d.date) - 120) + ', ' + (d3.mouse(this)[1] - 20) + ')'); - } else { - popup.attr('transform', 'translate(' + (xScale(d.date) + 80) + ', ' + (d3.mouse(this)[1] - 20) + ')'); - } - } + function setKit(height) { + kitHeight = height; } - // function in charge of rendering when there's data for 2 sensors - function updateChartCompare(data, options) { - xScale.domain(d3.extent(data[0], function(d) { return d.date; })); - yScale0.domain([(d3.min(data[0], function(d) { return d.count; })) * 0.8, (d3.max(data[0], function(d) { return d.count; })) * 1.2]); - yScale1.domain([(d3.min(data[1], function(d) { return d.count; })) * 0.8, (d3.max(data[1], function(d) { return d.count; })) * 1.2]); + function getKit() { + return kitHeight; + } + } +})(); - svg.selectAll('*').remove(); +(function() { + 'use strict'; - //Add both area paths - svg.append('path') - .datum(data[0]) - .attr('class', 'chart_area') - .attr('fill', options.color[0]) - .attr('d', areaMain); + angular.module('app.components') + .directive('horizontalScroll', horizontalScroll); - svg.append('path') - .datum(data[1]) - .attr('class', 'chart_area') - .attr('fill', options.color[1]) - .attr('d', areaCompare); + /** + * Used to highlight and unhighlight buttons on the kit dashboard when scrolling horizontally + * + */ + horizontalScroll.$inject = ['$window', '$timeout']; + function horizontalScroll($window, $timeout) { + return { + link: link, + restrict: 'A' + }; - // Add both valueline paths. - svg.append('path') - .attr('class', 'chart_line') - .attr('stroke', options.color[0]) - .attr('d', valueLineMain(data[0])); + /////////////////// - svg.append('path') - .attr('class', 'chart_line') - .attr('stroke', options.color[1]) - .attr('d', valueLineCompare(data[1])); - // Add the X Axis - svg.append('g') - .attr('class', 'axis x') - .attr('transform', 'translate(0,' + height + ')') - .call(xAxis); + function link(scope, element) { - // Add both Y Axis - svg.append('g') - .attr('class', 'axis y_left') - .call(yAxisLeft); + element.on('scroll', function() { + // horizontal scroll position + var position = angular.element(this).scrollLeft(); + // real width of element + var scrollWidth = this.scrollWidth; + // visible width of element + var width = angular.element(this).width(); - svg.append('g') - .attr('class', 'axis y_right') - .attr('transform', 'translate(' + width + ' ,0)') - .call(yAxisRight); + // if you cannot scroll, unhighlight both + if(scrollWidth === width) { + angular.element('.button_scroll_left').css('opacity', '0.5'); + angular.element('.button_scroll_right').css('opacity', '0.5'); + } + // if scroll is in the middle, highlight both + if(scrollWidth - width > 2) { + angular.element('.button_scroll_left').css('opacity', '1'); + angular.element('.button_scroll_right').css('opacity', '1'); + } + // if scroll is at the far right, unhighligh right button + if(scrollWidth - width - position <= 2) { + angular.element('.button_scroll_right').css('opacity', '0.5'); + return; + } + // if scroll is at the far left, unhighligh left button + if(position === 0) { + angular.element('.button_scroll_left').css('opacity', '0.5'); + return; + } - // Draw the x Grid lines - svg.append('g') - .attr('class', 'grid') - .attr('transform', 'translate(0,' + height + ')') - .call(xGrid() - .tickSize(-height, 0, 0) - .tickFormat('') - ); + //set opacity back to normal otherwise + angular.element('.button_scroll_left').css('opacity', '1'); + angular.element('.button_scroll_right').css('opacity', '1'); + }); - // Draw the y Grid lines - svg.append('g') - .attr('class', 'grid') - .call(yGrid() - .tickSize(-width, 0, 0) - .tickFormat('') - ); + $timeout(function() { + element.trigger('scroll'); + }); - focusCompare = svg.append('g') - .attr('class', 'focus') - .style('display', 'none'); + angular.element($window).on('resize', function() { + $timeout(function() { + element.trigger('scroll'); + }, 1000); + }); + } + } +})(); + +(function() { + 'use strict'; + + angular.module('app.components') + .directive('hidePopup', hidePopup); - focusMain = svg.append('g') - .attr('class', 'focus') - .style('display', 'none'); + /** + * Used on kit dashboard to hide popup with full sensor description + * + */ + + hidePopup.$inject = []; + function hidePopup() { + return { + link: link + }; - focusCompare.append('circle') - .style('stroke', options.color[1]) - .attr('r', 4.5); + ///////////// - focusMain.append('circle') - .style('stroke', options.color[0]) - .attr('r', 4.5); + function link(scope, elem) { + elem.on('mouseleave', function() { + angular.element('.sensor_description_preview').show(); + angular.element('.sensor_description_full').hide(); + }); + } + } +})(); - var popupWidth = 84; - var popupHeight = 75; +(function() { + 'use strict'; - popup = svg.append('g') - .attr('class', 'focus') - .style('display', 'none'); + angular.module('app.components') + .directive('disableScroll', disableScroll); - popupContainer = popup.append('rect') - .attr('width', popupWidth) - .attr('height', popupHeight) - .style('min-width', '40px') - .attr('transform', function() { - var result = 'translate(-42, 5)'; + disableScroll.$inject = ['$timeout']; + function disableScroll($timeout) { + return { + // link: { + // pre: link + // }, + compile: link, + restrict: 'A', + priority: 100000 + }; - return result; - }) - .style('stroke', 'grey') - .style('stroke-width', '0.5') - .style('fill', 'white'); - popup.append('rect') - .attr('width', 8) - .attr('height', 2) - .attr('transform', function() { - return 'translate(' + (-popupWidth / 2 + 4).toString() + ', 20)'; - }) - .style('fill', options.color[0]); + ////////////////////// - popup.append('rect') - .attr('width', 8) - .attr('height', 2) - .attr('transform', function() { - return 'translate(' + (-popupWidth / 2 + 4).toString() + ', 45)'; - }) - .style('fill', options.color[1]); + function link(elem) { + console.log('i', elem); + // var select = elem.find('md-select'); + // angular.element(select).on('click', function() { + elem.on('click', function() { + console.log('e'); + angular.element(document.body).css('overflow', 'hidden'); + $timeout(function() { + angular.element(document.body).css('overflow', 'initial'); + }); + }); + } + } +})(); - var text = popup.append('text') - .attr('class', ''); +(function() { + 'use strict'; - var textMain = text.append('tspan') - .attr('class', 'popup_main') - .attr('x', -popupHeight / 2 + 7) //position of text - .attr('dx', 8) //margin given to the element, will be applied to both sides thanks to resizePopup function - .attr('y', popupHeight / 3) - .attr('dy', 3); + angular.module('app.components') + .factory('animation', animation); - textMain.append('tspan') - .attr('class', 'popup_value') - .attr( 'text-anchor', 'start' ); + /** + * Used to emit events from rootscope. + * + * This events are then listened by $scope on controllers and directives that care about that particular event + */ - textMain.append('tspan') - .attr('class', 'popup_unit') - .attr('dx', 5); + animation.$inject = ['$rootScope']; + function animation($rootScope) { - var textCompare = text.append('tspan') - .attr('class', 'popup_compare') - .attr('x', -popupHeight / 2 + 7) //position of text - .attr('dx', 8) //margin given to the element, will be applied to both sides thanks to resizePopup function - .attr('y', popupHeight / 1.5) - .attr('dy', 3); + var service = { + blur: blur, + unblur: unblur, + removeNav: removeNav, + addNav: addNav, + showChartSpinner: showChartSpinner, + hideChartSpinner: hideChartSpinner, + deviceLoaded: deviceLoaded, + showPasswordRecovery: showPasswordRecovery, + showLogin: showLogin, + showSignup: showSignup, + showPasswordReset: showPasswordReset, + hideAlert: hideAlert, + viewLoading: viewLoading, + viewLoaded: viewLoaded, + deviceWithoutData: deviceWithoutData, + deviceIsPrivate: deviceIsPrivate, + goToLocation: goToLocation, + mapStateLoading: mapStateLoading, + mapStateLoaded: mapStateLoaded + }; + return service; - textCompare.append('tspan') - .attr('class', 'popup_value') - .attr( 'text-anchor', 'start' ); + ////////////// - textCompare.append('tspan') - .attr('class', 'popup_unit') - .attr('dx', 5); + function blur() { + $rootScope.$broadcast('blur'); + } + function unblur() { + $rootScope.$broadcast('unblur'); + } + function removeNav() { + $rootScope.$broadcast('removeNav'); + } + function addNav() { + $rootScope.$broadcast('addNav'); + } + function showChartSpinner() { + $rootScope.$broadcast('showChartSpinner'); + } + function hideChartSpinner() { + $rootScope.$broadcast('hideChartSpinner'); + } + function deviceLoaded(data) { + $rootScope.$broadcast('deviceLoaded', data); + } + function showPasswordRecovery() { + $rootScope.$broadcast('showPasswordRecovery'); + } + function showLogin() { + $rootScope.$broadcast('showLogin'); + } + function showSignup() { + $rootScope.$broadcast('showSignup'); + } + function showPasswordReset() { + $rootScope.$broadcast('showPasswordReset'); + } + function hideAlert() { + $rootScope.$broadcast('hideAlert'); + } + function viewLoading() { + $rootScope.$broadcast('viewLoading'); + } + function viewLoaded() { + $rootScope.$broadcast('viewLoaded'); + } + function deviceWithoutData(data) { + $rootScope.$broadcast('deviceWithoutData', data); + } + function deviceIsPrivate(data) { + $rootScope.$broadcast('deviceIsPrivate', data); + } + function goToLocation(data) { + $rootScope.$broadcast('goToLocation', data); + } + function mapStateLoading() { + $rootScope.$broadcast('mapStateLoading'); + } + function mapStateLoaded() { + $rootScope.$broadcast('mapStateLoaded'); + } + } +})(); - text.append('tspan') - .attr('class', 'popup_date') - .attr('x', (- popupWidth / 2)) - .attr('dx', 8) - .attr('y', popupHeight - 2) - .attr('dy', 0) - .attr( 'text-anchor', 'start' ); +(function() { + 'use strict'; - svg.append('rect') - .attr('class', 'overlay') - .attr('width', width) - .attr('height', height) - .on('mouseover', function() { - focusCompare.style('display', null); - focusMain.style('display', null); - popup.style('display', null); - }) - .on('mouseout', function() { - focusCompare.style('display', 'none'); - focusMain.style('display', 'none'); - popup.style('display', 'none'); - }) - .on('mousemove', mousemove); + /** + * TODO: Improvement These directives can be split up each one in a different file + */ - function mousemove() { - var bisectDate = d3.bisector(function(d) { return d.date; }).left; + angular.module('app.components') + .directive('moveDown', moveDown) + .directive('stick', stick) + .directive('blur', blur) + .directive('focus', focus) + .directive('changeMapHeight', changeMapHeight) + .directive('changeContentMargin', changeContentMargin) + .directive('focusInput', focusInput); - var x0 = xScale.invert(d3.mouse(this)[0]); - var i = bisectDate(data[1], x0, 1); - var d0 = data[1][i - 1]; - var d1 = data[1][i]; - var d = x0 - d0.date > d1.date - x0 ? d1 : d0; - focusCompare.attr('transform', 'translate(' + xScale(d.date) + ', ' + yScale1(d.count) + ')'); + /** + * It moves down kit section to ease the transition after the kit menu is sticked to the top + * + */ + moveDown.$inject = []; + function moveDown() { + function link(scope, element) { + scope.$watch('moveDown', function(isTrue) { + if(isTrue) { + element.addClass('move_down'); + } else { + element.removeClass('move_down'); + } + }); + } - var dMain0 = data[0][i - 1]; - var dMain1 = data[0][i]; - var dMain = x0 - dMain0.date > dMain1.date - x0 ? dMain1 : dMain0; - focusMain.attr('transform', 'translate(' + xScale(dMain.date) + ', ' + yScale0(dMain.count) + ')'); + return { + link: link, + scope: false, + restrict: 'A' + }; + } - var popupText = popup.select('text'); - var textMain = popupText.select('.popup_main'); - textMain.select('.popup_value').text(parseValue(dMain.value)); - textMain.select('.popup_unit').text(options.unit[0]); - var textCompare = popupText.select('.popup_compare'); - textCompare.select('.popup_value').text(parseValue(d.value)); - textCompare.select('.popup_unit').text(options.unit[1]); - var date = popupText.select('.popup_date').text(parseTime(d.date)); + /** + * It sticks kit menu when kit menu touchs navbar on scrolling + * + */ + stick.$inject = ['$window', '$timeout']; + function stick($window, $timeout) { + function link(scope, element) { + var elementPosition = element[0].offsetTop; + //var elementHeight = element[0].offsetHeight; + var navbarHeight = angular.element('.stickNav').height(); - var textContainers = [ - textMain, - textCompare, - date - ]; + $timeout(function() { + elementPosition = element[0].offsetTop; + //var elementHeight = element[0].offsetHeight; + navbarHeight = angular.element('.stickNav').height(); + }, 1000); - var popupWidth = resizePopup(popupContainer, textContainers); - if(xScale(d.date) + 80 + popupWidth > options.container.clientWidth) { - popup.attr('transform', 'translate(' + (xScale(d.date) - 120) + ', ' + (d3.mouse(this)[1] - 20) + ')'); + angular.element($window).on('scroll', function() { + var windowPosition = document.body.scrollTop; + + //sticking menu and moving up/down + if(windowPosition + navbarHeight >= elementPosition) { + element.addClass('stickMenu'); + scope.$apply(function() { + scope.moveDown = true; + }); } else { - popup.attr('transform', 'translate(' + (xScale(d.date) + 80) + ', ' + (d3.mouse(this)[1] - 20) + ')'); + element.removeClass('stickMenu'); + scope.$apply(function() { + scope.moveDown = false; + }); } - } + }); } - function xGrid() { - return d3.svg.axis() - .scale(xScale) - .orient('bottom') - .ticks(5); - } + return { + link: link, + scope: false, + restrict: 'A' + }; + } - function yGrid() { - return d3.svg.axis() - .scale(yScale0) - .orient('left') - .ticks(5); - } + /** + * Unused directive. Double-check is not being used before removing it + * + */ - function parseValue(value) { - if(value === null) { - return 'No data on the current timespan'; - } else if(value.toString().indexOf('.') !== -1) { - var result = value.toString().split('.'); - return result[0] + '.' + result[1].slice(0, 2); - } else if(value > 99.99) { - return value.toString(); - } else { - return value.toString().slice(0, 2); - } - } + function blur() { - function parseTime(time) { - return moment(time).format('h:mm a ddd Do MMM YYYY'); + function link(scope, element) { + + scope.$on('blur', function() { + element.addClass('blur'); + }); + + scope.$on('unblur', function() { + element.removeClass('blur'); + }); } - function resizePopup(popupContainer, textContainers) { - if(!textContainers.length) { - return; - } + return { + link: link, + scope: false, + restrict: 'A' + }; + } - var widestElem = textContainers.reduce(function(widestElemSoFar, textContainer) { - var currentTextContainerWidth = getContainerWidth(textContainer); - var prevTextContainerWidth = getContainerWidth(widestElemSoFar); - return prevTextContainerWidth >= currentTextContainerWidth ? widestElemSoFar : textContainer; - }, textContainers[0]); + /** + * Used to remove nav and unable scrolling when searching + * + */ + focus.$inject = ['animation']; + function focus(animation) { + function link(scope, element) { + element.on('focusin', function() { + animation.removeNav(); + }); - var margins = widestElem.attr('dx') * 2; + element.on('focusout', function() { + animation.addNav(); + }); - popupContainer - .attr('width', getContainerWidth(widestElem) + margins); + var searchInput = element.find('input'); + searchInput.on('blur', function() { + //enable scrolling on body when search input is not active + angular.element(document.body).css('overflow', 'auto'); + }); - function getContainerWidth(container) { - var node = container.node(); - var width; - if(node.getComputedTextLength) { - width = node.getComputedTextLength(); - } else if(node.getBoundingClientRect) { - width = node.getBoundingClientRect().width; - } else { - width = node.getBBox().width; - } - return width; - } - return getContainerWidth(widestElem) + margins; + searchInput.on('focus', function() { + angular.element(document.body).css('overflow', 'hidden'); + }); } + + return { + link: link + }; } -})(); + /** + * Changes map section based on screen size + * + */ + changeMapHeight.$inject = ['$document', 'layout', '$timeout']; + function changeMapHeight($document, layout, $timeout) { + function link(scope, element) { -(function(){ - 'use strict'; + var screenHeight = $document[0].body.clientHeight; + var navbarHeight = angular.element('.stickNav').height(); - angular.module('app.components') - .directive('apiKey', apiKey); + // var overviewHeight = angular.element('.kit_overview').height(); + // var menuHeight = angular.element('.kit_menu').height(); + // var chartHeight = angular.element('.kit_chart').height(); - function apiKey(){ - return { - scope: { - apiKey: '=apiKey' - }, - restrict: 'A', - controller: 'ApiKeyController', - controllerAs: 'vm', - templateUrl: 'app/components/apiKey/apiKey.html' - }; - } -})(); + function resizeMap(){ + $timeout(function() { + var overviewHeight = angular.element('.over_map').height(); -(function(){ - 'use strict'; + var objectsHeight = navbarHeight + overviewHeight; + var objectsHeightPercentage = parseInt((objectsHeight * 100) / screenHeight); + var mapHeightPercentage = 100 - objectsHeightPercentage; - angular.module('app.components') - .controller('ApiKeyController', ApiKeyController); + element.css('height', mapHeightPercentage + '%'); - ApiKeyController.$inject = ['alert']; - function ApiKeyController(alert){ - var vm = this; + var aboveTheFoldHeight = screenHeight - overviewHeight; + angular + .element('section[change-content-margin]') + .css('margin-top', aboveTheFoldHeight + 'px'); + }); + } - vm.copied = copied; - vm.copyFail = copyFail; + resizeMap(); - /////////////// + scope.element = element; - function copied(){ - alert.success('API key copied to your clipboard.'); + scope.$on('resizeMapHeight',function(){ + resizeMap(); + }); + + } + + return { + link: link, + scope: true, + restrict: 'A' + }; } - function copyFail(err){ - console.log('Copy error: ', err); - alert.error('Oops! An error occurred copying the api key.'); + /** + * Changes margin on kit section based on above-the-fold space left after map section is resize + */ + + changeContentMargin.$inject = ['layout', '$timeout', '$document']; + function changeContentMargin(layout, $timeout, $document) { + function link(scope, element) { + var screenHeight = $document[0].body.clientHeight; + + var overviewHeight = angular.element('.over_map').height(); + + var aboveTheFoldHeight = screenHeight - overviewHeight; + element.css('margin-top', aboveTheFoldHeight + 'px'); + } + + return { + link: link + }; } - } + /** + * Fixes autofocus for inputs that are inside modals + * + */ + focusInput.$inject = ['$timeout']; + function focusInput($timeout) { + function link(scope, elem) { + $timeout(function() { + elem.focus(); + }); + } + return { + link: link + }; + } })(); (function() { 'use strict'; angular.module('app.components') - .factory('alert', alert); - - alert.$inject = ['$mdToast']; - function alert($mdToast) { - var service = { - success: success, - error: error, - info: { - noData: { - visitor: infoNoDataVisitor, - owner: infoNoDataOwner, - private: infoDataPrivate, - }, - longTime: infoLongTime, - // TODO: Refactor, check why this was removed - // inValid: infoDataInvalid, - generic: info - } - }; + .directive('activeButton', activeButton); - return service; + /** + * Used to highlight and unhighlight buttons on kit menu + * + * It attaches click handlers dynamically + */ - /////////////////// + activeButton.$inject = ['$timeout', '$window']; + function activeButton($timeout, $window) { + return { + link: link, + restrict: 'A' - function success(message) { - toast('success', message); - } + }; - function error(message) { - toast('error', message); - } + //////////////////////////// - function infoNoDataVisitor() { - info('Woah! We couldn\'t locate this kit on the map because it hasn\'t published any data. Leave a ' + - 'comment to let its owner know.', - 10000, - { - button: 'Leave comment', - href: 'https://forum.smartcitizen.me/' - }); - } + function link(scope, element) { + var childrens = element.children(); + var container; - function infoNoDataOwner() { - info('Woah! We couldn\'t locate this kit on the map because it hasn\'t published any data.', - 10000); - } + $timeout(function() { + var navbar = angular.element('.stickNav'); + var kitMenu = angular.element('.kit_menu'); + var kitOverview = angular.element('.kit_overview'); + var kitDashboard = angular.element('.kit_chart'); + var kitDetails = angular.element('.kit_details'); + var kitOwner = angular.element('.kit_owner'); + var kitComments = angular.element('.kit_comments'); - function infoDataPrivate() { - info('Device not found, or it has been set to private. Leave a ' + - 'comment to let its owner know you\'re interested.', - 10000, - { - button: 'Leave comment', - href: 'https://forum.smartcitizen.me/' - }); - } + container = { + navbar: { + height: navbar.height() + }, + kitMenu: { + height: kitMenu.height() + }, + kitOverview: { + height: kitOverview.height(), + offset: kitOverview.offset().top, + buttonOrder: 0 + }, + kitDashboard: { + height: kitDashboard.height(), + offset: kitDashboard.offset().top, + buttonOrder: 40 + }, + kitDetails: { + height: kitDetails.height(), + offset: kitDetails.offset() ? kitDetails.offset().top : 0, + buttonOrder: 1 + }, + kitOwner: { + height: kitOwner.height(), + offset: kitOwner.offset() ? kitOwner.offset().top : 0, + buttonOrder: 2 + }, + kitComments: { + height: kitComments.height(), + offset: kitComments.offset() ? kitComments.offset().top : 0, + buttonOrder: 3 + } + }; + }, 1000); - // TODO: Refactor, check why this was removed - // function infoDataInvalid() { - // info('Device not found, or it has been set to private.', - // 10000); - // } + function scrollTo(offset) { + if(!container) { + return; + } + angular.element($window).scrollTop(offset - container.navbar.height - container.kitMenu.height); + } - function infoLongTime() { - info('😅 It looks like this kit hasn\'t posted any data in a long ' + - 'time. Why not leave a comment to let its owner know?', - 10000, - { - button: 'Leave comment', - href: 'https://forum.smartcitizen.me/' - }); - } + function getButton(buttonOrder) { + return childrens[buttonOrder]; + } - function info(message, delay, options) { - if(options && options.button) { - toast('infoButton', message, options, undefined, delay); - } else { - toast('info', message, options, undefined, delay); - } - } + function unHighlightButtons() { + //remove border, fill and stroke of every icon + var activeButton = angular.element('.md-button.button_active'); + if(activeButton.length) { + activeButton.removeClass('button_active'); - function toast(type, message, options, position, delay) { - position = position === undefined ? 'top': position; - delay = delay === undefined ? 5000 : delay; + var strokeContainer = activeButton.find('.stroke_container'); + strokeContainer.css('stroke', 'none'); + strokeContainer.css('stroke-width', '1'); - $mdToast.show({ - controller: 'AlertController', - controllerAs: 'vm', - templateUrl: 'app/components/alert/alert' + type + '.html', - hideDelay: delay, - position: position, - locals: { - message: message, - button: options && options.button, - href: options && options.href + var fillContainer = strokeContainer.find('.fill_container'); + fillContainer.css('fill', '#FF8600'); + } } - }); - } - } -})(); -(function() { - 'use strict'; + function highlightButton(button) { + var clickedButton = angular.element(button); + //add border, fill and stroke to every icon + clickedButton.addClass('button_active'); - angular.module('app.components') - .controller('AlertController', AlertController); + var strokeContainer = clickedButton.find('.stroke_container'); + strokeContainer.css('stroke', 'white'); + strokeContainer.css('stroke-width', '0.01px'); - AlertController.$inject = ['$scope', '$mdToast', 'message', 'button', 'href']; - function AlertController($scope, $mdToast, message, button, href) { - var vm = this; + var fillContainer = strokeContainer.find('.fill_container'); + fillContainer.css('fill', 'white'); + } - vm.close = close; - vm.message = message; - vm.button = button; - vm.href = href; + //attach event handlers for clicks for every button and scroll to a section when clicked + _.each(childrens, function(button) { + angular.element(button).on('click', function() { + var buttonOrder = angular.element(this).index(); + for(var elem in container) { + if(container[elem].buttonOrder === buttonOrder) { + var offset = container[elem].offset; + scrollTo(offset); + angular.element($window).trigger('scroll'); + } + } + }); + }); - // hideAlert will be triggered on state change - $scope.$on('hideAlert', function() { - close(); - }); + var currentSection; - /////////////////// + //on scroll, check if window is on a section + angular.element($window).on('scroll', function() { + if(!container){ return; } - function close() { - $mdToast.hide(); + var windowPosition = document.body.scrollTop; + var appPosition = windowPosition + container.navbar.height + container.kitMenu.height; + var button; + if(currentSection !== 'none' && appPosition <= container.kitOverview.offset) { + button = getButton(container.kitOverview.buttonOrder); + unHighlightButtons(); + currentSection = 'none'; + } else if(currentSection !== 'overview' && appPosition >= container.kitOverview.offset && appPosition <= container.kitOverview.offset + container.kitOverview.height) { + button = getButton(container.kitOverview.buttonOrder); + unHighlightButtons(); + highlightButton(button); + currentSection = 'overview'; + } else if(currentSection !== 'details' && appPosition >= container.kitDetails.offset && appPosition <= container.kitDetails.offset + container.kitDetails.height) { + button = getButton(container.kitDetails.buttonOrder); + unHighlightButtons(); + highlightButton(button); + currentSection = 'details'; + } else if(currentSection !== 'owner' && appPosition >= container.kitOwner.offset && appPosition <= container.kitOwner.offset + container.kitOwner.height) { + button = getButton(container.kitOwner.buttonOrder); + unHighlightButtons(); + highlightButton(button); + currentSection = 'owner'; + } else if(currentSection !== 'comments' && appPosition >= container.kitComments.offset && appPosition <= container.kitComments.offset + container.kitOwner.height) { + button = getButton(container.kitComments.buttonOrder); + unHighlightButtons(); + highlightButton(button); + currentSection = 'comments'; + } + }); } } })();