diff --git a/404.html b/404.html index 7ea2d1d9..ea862d68 100644 --- a/404.html +++ b/404.html @@ -127,7 +127,7 @@
Your browser is out of date!
- + diff --git a/index.html b/index.html index 7ea2d1d9..ea862d68 100644 --- a/index.html +++ b/index.html @@ -127,7 +127,7 @@
Your browser is out of date!
- + diff --git a/scripts/app-cee135d840.js b/scripts/app-d41f061e2e.js similarity index 100% rename from scripts/app-cee135d840.js rename to scripts/app-d41f061e2e.js index b47f2879..a76eb51e 100644 --- a/scripts/app-cee135d840.js +++ b/scripts/app-d41f061e2e.js @@ -9,1842 +9,1818 @@ '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; - }]); -})(); + 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() { - 'use strict'; + vm.device = newDevice; + setOwnerSampleDevices(); - 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 = ''; + if (vm.device.state.name === 'has published') { + /* Device has data */ + setDeviceOnMap(); + setChartTimeRange(); + deviceAnnouncements(); - this.icon = markerUtils.getIcon(deviceData); - this.layer = 'devices'; - this.focus = false; - this.myData = { - id: id, - labels: deviceUtils.parseSystemTags(deviceData), - tags: deviceUtils.parseUserTags(deviceData) - }; + /*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); } - return Marker; + } - function createTagsTemplate(tagsArr, tagType, clickable) { - if(typeof(clickable) === 'undefined'){ - clickable = false; + function killSensorsLoading(error){ + if(error) { + if(error.status === 404) { + $state.go('layout.404'); } - var clickablTag = ''; - if(clickable){ - clickablTag = 'clickable'; + else if (error.justPublished) { + $state.transitionTo($state.current, {reloadMap: true, id: vm.deviceID}, { + reload: true, inherit: false, notify: true + }); } - - if(!tagType){ - tagType = 'tag'; + else if (error.noSensorData) { + deviceHasNoData(); + } + else if (error.status === 403){ + deviceIsPrivate(); } - - return _.reduce(tagsArr, function(acc, label) { - var element =''; - if(tagType === 'tag'){ - element = ''; - }else{ - element = ''+label+''; - } - return acc.concat(element); - }, ''); } + } - }]); -})(); + 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!'); + } + } -(function () { - 'use strict'; + 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(); + } + } - angular.module('app.components') - .factory('PreviewDevice', ['Device', function (Device) { + function deviceIsPrivate() { + alert.info.noData.private(); + } - /** - * 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); + 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; + }); + } - 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' }); + function setChartTimeRange() { + if(vm.allowUpdateChart) { + /* Init the chart range to default if doesn't exist of the user hasn't interacted */ + picker = initializePicker(); } - PreviewDevice.prototype = Object.create(Device.prototype); - PreviewDevice.prototype.constructor = Device; - return PreviewDevice; - }]); -})(); + } -(function() { - 'use strict'; + function setDeviceOnMap() { + animation.deviceLoaded({lat: vm.device.latitude, lng: vm.device.longitude, + id: vm.device.id}); + } - angular.module('app.components') - .factory('HasSensorDevice', ['Device', function(Device) { + function setSensors(sensorsRes){ - function HasSensorDevice(object) { - Device.call(this, object); + var mainSensors = sensorsRes[0]; + var compareSensors = sensorsRes[1]; - this.sensors = object.data.sensors; - this.longitude = object.data.location.longitude; - this.latitude = object.data.location.latitude; + vm.battery = _.find(mainSensors, {name: 'battery'}); + vm.sensors = mainSensors.reverse(); + vm.sensors.forEach(checkRaw); + vm.sensors.forEach(getHardwareName); + + setSensorSideChart(); + + if (!vm.selectedSensor) { + vm.chartSensors = vm.sensors; + vm.sensorsToCompare = compareSensors; + vm.selectedSensor = (vm.sensors && vm.sensors[0]) ? vm.sensors[0].id : undefined; } - HasSensorDevice.prototype = Object.create(Device.prototype); - HasSensorDevice.prototype.constructor = Device; + animation.mapStateLoaded(); + } - HasSensorDevice.prototype.sensorsHasData = function() { - var parsedSensors = this.sensors.map(function(sensor) { - return sensor.value; - }); + function checkRaw(value){ + vm.hasRaw |= (value.tags.indexOf('raw') !== -1); + } - return _.some(parsedSensors, function(sensorValue) { - return !!sensorValue; + 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); + } }); - }; - - return HasSensorDevice; - }]); -})(); + } + } -(function() { - 'use strict'; + 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); - angular.module('app.components') - .factory('FullDevice', ['Device', 'Sensor', 'deviceUtils', function(Device, Sensor, deviceUtils) { + $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.'); + }); + }); + } - /** - * 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); + function showSensorOnChart(sensorID) { + vm.selectedSensor = sensorID; + } - this.owner = deviceUtils.parseOwner(object); - this.postProcessing = object.postprocessing; - this.data = object.data; - this.sensors = object.data.sensors; - } + function slide(direction) { + var slideContainer = angular.element('.sensors_container'); + var scrollPosition = slideContainer.scrollLeft(); + var width = slideContainer.width(); + var slideStep = width/2; - FullDevice.prototype = Object.create(Device.prototype); - FullDevice.prototype.constructor = FullDevice; + 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}); + } + } - 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; - }; + function getSensorsToCompare() { + return vm.sensors ? vm.sensors.filter(function(sensor) { + return sensor.id !== vm.selectedSensor; + }) : []; + } - return FullDevice; - }]); -})(); + function changeChart(sensorsID, options) { + if(!sensorsID[0]) { + return; + } -(function() { - 'use strict'; + if(!options) { + options = {}; + } + options.from = options && options.from || picker.getValuePickerFrom(); + options.to = options && options.to || picker.getValuePickerTo(); - angular.module('app.components') - .factory('Device', ['deviceUtils', 'timeUtils', function(deviceUtils, timeUtils) { + //show spinner + vm.loadingChart = true; + //grab chart data and save it - /** - * 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; + // 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; + }); + } - // Tags and dates - this.systemTags = deviceUtils.parseSystemTags(object); - this.userTags = deviceUtils.parseUserTags(object); - this.isPrivate = deviceUtils.isPrivate(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 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]); - // Location - this.location = object.location; - this.locationString = deviceUtils.parseLocation(object); + compareSensor = { + data: parsedDataCompare, + color: vm.selectedSensorToCompareData.color, + unit: vm.selectedSensorToCompareData.unit + }; + } + var newChartData = [mainSensor, compareSensor]; + return newChartData; + } - // 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? + 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 + }; + }); + } - this.avatar = deviceUtils.parseAvatar(); - /*jshint camelcase: false */ + function setSensor(options) { + var sensorID = options.value; + if(sensorID === undefined) { + return; + } + if(options.type === 'main') { + mainSensorID = sensorID; + } else if(options.type === 'compare') { + compareSensorID = sensorID; } + } - return Device; - }]); -})(); + 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 = []; + var valueTo, valueFrom; + //grab current date range + var currentRange = getCurrentRange(); - 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) { - - $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); + + $mdDialog.show(errorAlert); + }); } - function deviceAnnouncements(){ - if(!timeUtils.isWithin(1, 'months', vm.device.lastReadingAt.raw)) { - //TODO: Cosmetic Update the message - alert.info.longTime(); + function getMainSensors(deviceData) { + if(!deviceData) { + return undefined; } - /* 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!'); + return deviceData.getSensors({type: 'main'}); + } + function getCompareSensors(deviceData) { + if(!vm.device) { + return undefined; } + deviceData.getSensors({type: 'compare'}); } - - 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 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 deviceIsPrivate() { - alert.info.noData.private(); - } - - 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 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 setChartTimeRange() { - if(vm.allowUpdateChart) { - /* Init the chart range to default if doesn't exist of the user hasn't interacted */ - picker = initializePicker(); + function timeOptSelected(){ + vm.allowUpdateChart = false; + if (vm.dropDownSelection){ + setFromLast(vm.dropDownSelection); } } + function resetTimeOpts(){ + vm.allowUpdateChart = false; + vm.dropDownSelection = undefined; + } - function setDeviceOnMap() { - animation.deviceLoaded({lat: vm.device.latitude, lng: vm.device.longitude, - id: vm.device.id}); + function showStore() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'StoreModalController', + templateUrl: 'app/components/store/storeModal.html', + clickOutsideToClose: true + }); } + } +})(); - function setSensors(sensorsRes){ +(function() { + 'use strict'; - var mainSensors = sensorsRes[0]; - var compareSensors = sensorsRes[1]; + angular.module('app.components') + .controller('NewKitController', NewKitController); - vm.battery = _.find(mainSensors, {name: 'battery'}); - vm.sensors = mainSensors.reverse(); - vm.sensors.forEach(checkRaw); - vm.sensors.forEach(getHardwareName); + 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; - setSensorSideChart(); + vm.step = 1; + vm.submitStepOne = submitStepOne; + vm.backToProfile = backToProfile; - if (!vm.selectedSensor) { - vm.chartSensors = vm.sensors; - vm.sensorsToCompare = compareSensors; - vm.selectedSensor = (vm.sensors && vm.sensors[0]) ? vm.sensors[0].id : undefined; - } + // FORM INFO + vm.deviceForm = { + name: undefined, + exposure: undefined, + location: { + lat: undefined, + lng: undefined, + zoom: 16 + }, + is_private: false, + legacyVersion: '1.1', + tags: [] + }; - animation.mapStateLoaded(); - } + // EXPOSURE SELECT + vm.exposure = [ + {name: 'indoor', value: 1}, + {name: 'outdoor', value: 2} + ]; - function checkRaw(value){ - vm.hasRaw |= (value.tags.indexOf('raw') !== -1); - } + // VERSION SELECT + vm.version = [ + {name: 'Smart Citizen Kit 1.0', value: '1.0'}, + {name: 'Smart Citizen Kit 1.1', value: '1.1'} + ]; - 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); - } - }); - } - } + $scope.$on('leafletDirectiveMarker.dragend', function(event, args){ + vm.deviceForm.location.lat = args.model.lat; + vm.deviceForm.location.lng = args.model.lng; + }); - 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); + // TAGS SELECT + vm.tags = []; + $scope.$watch('vm.tag', function(newVal, oldVal) { + if(!newVal) { + return; + } + // remove selected tag from select element + vm.tag = undefined; - $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.'); - }); + 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(); - if(!options) { - options = {}; + ////////////// + + function initialize() { + animation.viewLoaded(); + getTags(); + vm.userRole = auth.getCurrentUser().data.role; } - 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 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; + }); + }); + } - // 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 removeTag(tagID) { + vm.deviceForm.tags = _.filter(vm.deviceForm.tags, function(tag) { + return tag.id !== tagID; }); - } + } - 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, + 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, + 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() { - return to; - } + if (next){ + alert.success('Your kit was updated!'); + } - function updateChart() { - var sensors = [mainSensorID, compareSensorID]; - sensors = sensors.filter(function(sensor) { - return sensor; - }); - changeChart(sensors, { - from: range.from, - to: range.to - }); + 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 setFromRange(from) { - range.from = from; - from_picker.set('select', getFromRange()); - updateChart(); + 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 setToRange(to) { - range.to = to; - to_picker.set('select', getToRange()); - updateChart(); - } + function findExposure(nameOrValue) { + var findProp, resultProp; - function getFromRange() { - return moment(range.from).toDate(); + //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 getToRange() { - return moment(range.to).toDate(); + function getTags() { + tag.getTags() + .then(function(tagsData) { + vm.tags = tagsData; + }); } - function setRange(from, to) { - range.from = from; - range.to = to; - from_picker.set('select', getFromRange()); - to_picker.set('select', getToRange()); - updateChart(); + function backToProfile(){ + $state.transitionTo('layout.myProfile.kits', $stateParams, + { reload: false, + inherit: false, + notify: true + }); } - 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 backToDevice(){ + $state.transitionTo('layout.home.kit', $stateParams, + { reload: false, + inherit: false, + notify: true + }); } - // 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 goToStep(step) { + vm.step = step; + $state.transitionTo('layout.kitEdit', { id:$stateParams.id, step: step} , + { + reload: false, + inherit: false, + notify: false + }); + } } +})(); - 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() { + 'use strict'; - geolocation.grantHTML5Geolocation(); + angular.module('app.components') + .factory('User', ['COUNTRY_CODES', function(COUNTRY_CODES) { - var location = { - lat:position.coords.latitude, - lng:position.coords.longitude - }; - device.getDevices(location) - .then(function(data){ - data = data.plain(); + /** + * 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 + */ - _(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 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]; } - } - - 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); + return User; + }]); - $mdDialog.show(errorAlert); - }); - } +})(); - 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); - }); - }) - ); - } +(function() { + 'use strict'; - 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()]); - } + angular.module('app.components') + .factory('NonAuthUser', ['User', function(User) { - function timeOptSelected(){ - vm.allowUpdateChart = false; - if (vm.dropDownSelection){ - setFromLast(vm.dropDownSelection); + function NonAuthUser(userData) { + User.call(this, userData); } - } - function resetTimeOpts(){ - vm.allowUpdateChart = false; - vm.dropDownSelection = undefined; - } + NonAuthUser.prototype = Object.create(User.prototype); + NonAuthUser.prototype.constructor = User; - function showStore() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'StoreModalController', - templateUrl: 'app/components/store/storeModal.html', - clickOutsideToClose: true - }); - } - } + return NonAuthUser; + }]); })(); (function() { 'use strict'; angular.module('app.components') - .controller('NewKitController', NewKitController); - - 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; - - vm.step = 1; - vm.submitStepOne = submitStepOne; - vm.backToProfile = backToProfile; - - // FORM INFO - vm.deviceForm = { - name: undefined, - exposure: undefined, - location: { - lat: undefined, - lng: undefined, - zoom: 16 - }, - is_private: false, - legacyVersion: '1.1', - tags: [] - }; + .factory('AuthUser', ['User', function(User) { - // EXPOSURE SELECT - vm.exposure = [ - {name: 'indoor', value: 1}, - {name: 'outdoor', value: 2} - ]; + /** + * 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 + */ - // VERSION SELECT - vm.version = [ - {name: 'Smart Citizen Kit 1.0', value: '1.0'}, - {name: 'Smart Citizen Kit 1.1', value: '1.1'} - ]; + function AuthUser(userData) { + User.call(this, userData); - $scope.$on('leafletDirectiveMarker.dragend', function(event, args){ - vm.deviceForm.location.lat = args.model.lat; - vm.deviceForm.location.lng = args.model.lng; - }); + 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; - // TAGS SELECT - vm.tags = []; - $scope.$watch('vm.tag', function(newVal, oldVal) { - if(!newVal) { - return; - } - // remove selected tag from select element - vm.tag = undefined; + return AuthUser; + }]); +})(); - var alreadyPushed = _.some(vm.deviceForm.tags, function(tag) { - return tag.id === newVal; - }); - if(alreadyPushed) { - return; - } +(function() { + 'use strict'; - var tag = _.find(vm.tags, function(tag) { - return tag.id === newVal; - }); - vm.deviceForm.tags.push(tag); - }); - vm.removeTag = removeTag; + angular.module('app.components') + .factory('Sensor', ['sensorUtils', 'timeUtils', function(sensorUtils, timeUtils) { - // MAP CONFIGURATION - var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; + /** + * 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.getLocation = getLocation; - vm.markers = { - main: { - lat: undefined, - lng: undefined, - draggable: true + 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; + + // 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; } - }; - vm.tiles = { - url: 'https://api.mapbox.com/styles/v1/mapbox/streets-v10/tiles/{z}/{x}/{y}?access_token=' + mapBoxToken - }; - vm.defaults = { - scrollWheelZoom: false - }; - vm.macAddress = undefined; + // Get sensor tags + this.tags = sensorData.tags; + } - initialize(); + return Sensor; + }]); +})(); +(function() { + 'use strict'; - ////////////// + angular.module('app.components') + .factory('SearchResultLocation', ['SearchResult', function(SearchResult) { - function initialize() { - animation.viewLoaded(); - getTags(); - vm.userRole = auth.getCurrentUser().data.role; - } + /** + * 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); - 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; - }); - }); + this.lat = object.latitude; + this.lng = object.longitude; + this.layer = object.layer; } + return SearchResultLocation; + }]); - function removeTag(tagID) { - vm.deviceForm.tags = _.filter(vm.deviceForm.tags, function(tag) { - return tag.id !== tagID; - }); - } +})(); - 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(',') - }; +(function() { + 'use strict'; - 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'); - }); - } + angular.module('app.components') + .factory('SearchResult', ['searchUtils', function(searchUtils) { - function getTags() { - tag.getTags() - .then(function(tagsData) { - vm.tags = tagsData; - }); + /** + * 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 toProfile(){ - $state.transitionTo('layout.myProfile.kits', $stateParams, - { reload: false, - inherit: false, - notify: true - }); - } +(function() { + 'use strict'; - function backToProfile(){ - // TODO: Refactor Check - toProfile(); + 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 = ''; + + 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; - //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 createTagsTemplate(tagsArr, tagType, clickable) { + if(typeof(clickable) === 'undefined'){ + clickable = false; + } + var clickablTag = ''; + if(clickable){ + clickablTag = 'clickable'; } - var option = _.find(vm.exposure, function(exposureFromList) { - return exposureFromList[findProp] === nameOrValue; - }); - if(option) { - return option[resultProp]; + if(!tagType){ + tagType = 'tag'; } + + return _.reduce(tagsArr, function(acc, label) { + var element =''; + if(tagType === 'tag'){ + element = ''; + }else{ + element = ''+label+''; + } + return acc.concat(element); + }, ''); } - } + + }]); })(); -(function() { +(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); - - 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) { - - var vm = this; + .factory('PreviewDevice', ['Device', function (Device) { - // WAIT INTERVAL FOR USER FEEDBACK and TRANSITIONS (This will need to change) - var timewait = { - long: 5000, - normal: 2000, - short: 1000 - }; + /** + * 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); - vm.step = step; + 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; + }]); +})(); - // 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('HasSensorDevice', ['Device', function(Device) { - // FORM INFO - vm.deviceForm = {}; - vm.device = undefined; + function HasSensorDevice(object) { + Device.call(this, object); - $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(); - }); + this.sensors = object.data.sensors; + this.longitude = object.data.location.longitude; + this.latitude = object.data.location.latitude; + } - $scope.$on('leafletDirectiveMarker.dragend', function(event, args){ - vm.deviceForm.location.lat = args.model.lat; - vm.deviceForm.location.lng = args.model.lng; - }); + HasSensorDevice.prototype = Object.create(Device.prototype); + HasSensorDevice.prototype.constructor = Device; - // MAP CONFIGURATION - var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; + HasSensorDevice.prototype.sensorsHasData = function() { + var parsedSensors = this.sensors.map(function(sensor) { + return sensor.value; + }); - 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 + return _.some(parsedSensors, function(sensorValue) { + return !!sensorValue; + }); }; - initialize(); - - ///////////////// - - function initialize() { - var deviceID = $stateParams.id; + return HasSensorDevice; + }]); +})(); - animation.viewLoaded(); - getTags(); +(function() { + 'use strict'; - 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, - 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 - } - }; + angular.module('app.components') + .factory('FullDevice', ['Device', 'Sensor', 'deviceUtils', function(Device, Sensor, deviceUtils) { - if (vm.device.isLegacy) { - vm.deviceForm.macAddress = vm.device.macAddress; - } - }); - } + /** + * 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); - // Return tags in a comma separated list - function joinSelectedTags(){ - let tmp = [] - $scope.selectedTags.forEach(function(e){ - tmp.push(e.name) - }) - return tmp.join(', '); + this.owner = deviceUtils.parseOwner(object); + this.postProcessing = object.postprocessing; + this.data = object.data; + this.sensors = object.data.sensors; } - 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; - }); - }); - } + FullDevice.prototype = Object.create(Device.prototype); + FullDevice.prototype.constructor = FullDevice; - function submitFormAndKit(){ - submitForm(backToProfile, timewait.normal); - } + 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; + }; - 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, - 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 FullDevice; + }]); +})(); - vm.errors={}; +(function() { + 'use strict'; - if(!vm.device.isSCK) { - data.hardware_name_override = vm.deviceForm.hardwareName; - } + angular.module('app.components') + .factory('Device', ['deviceUtils', 'timeUtils', function(deviceUtils, timeUtils) { - // 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; - } + /** + * 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; - device.updateDevice(vm.device.id, data) - .then( - function() { + // Tags and dates + this.systemTags = deviceUtils.parseSystemTags(object); + this.userTags = deviceUtils.parseUserTags(object); + this.isPrivate = deviceUtils.isPrivate(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); - if (next){ - alert.success('Your kit was updated!'); - } + // Location + this.location = object.location; + this.locationString = deviceUtils.parseLocation(object); - 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); - }); - } + // 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? - 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); - } + this.avatar = deviceUtils.parseAvatar(); + /*jshint camelcase: false */ } - function findExposure(nameOrValue) { - var findProp, resultProp; + return Device; + }]); +})(); - //if it's a string - if(isNaN(parseInt(nameOrValue))) { - findProp = 'name'; - resultProp = 'value'; - } else { - findProp = 'value'; - resultProp = 'name'; - } +(function() { + 'use strict'; - var option = _.find(vm.exposure, function(exposureFromList) { - return exposureFromList[findProp] === nameOrValue; - }); - if(option) { - return option[resultProp]; - } else { - return vm.exposure[0][resultProp]; - } - } + angular.module('app.components') + .directive('noDataBackdrop', noDataBackdrop); - function getTags() { - tag.getTags() - .then(function(tagsData) { - vm.tags = tagsData; - }); - } + /** + * Backdrop for chart section when kit has no data + * + */ + noDataBackdrop.$inject = []; - function backToProfile(){ - $state.transitionTo('layout.myProfile.kits', $stateParams, - { reload: false, - inherit: false, - notify: true - }); - } + function noDataBackdrop() { + return { + restrict: 'A', + scope: {}, + templateUrl: 'app/core/animation/backdrop/noDataBackdrop.html', + controller: function($scope, $timeout) { + var vm = this; - function backToDevice(){ - $state.transitionTo('layout.home.kit', $stateParams, - { reload: false, - inherit: false, - notify: true - }); - } + vm.deviceWithoutData = false; + vm.scrollToComments = scrollToComments; + + $scope.$on('deviceWithoutData', function(ev, data) { + + $timeout(function() { + vm.device = data.device; + vm.deviceWithoutData = true; + + if (data.belongsToUser) { + vm.user = 'owner'; + } else { + vm.user = 'visitor'; + } + }, 0); - function goToStep(step) { - vm.step = step; - $state.transitionTo('layout.kitEdit', { id:$stateParams.id, step: step} , - { - reload: false, - inherit: false, - notify: false }); - } - } + + function scrollToComments(){ + location.hash = ''; + location.hash = '#disqus_thread'; + } + }, + controllerAs: 'vm' + }; + } })(); (function() { 'use strict'; angular.module('app.components') - .factory('userUtils', userUtils); + .directive('loadingBackdrop', loadingBackdrop); - function userUtils() { - var service = { - isAdmin: isAdmin, - isAuthUser: isAuthUser - }; - return service; + /** + * 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; - /////////// + // listen for app loading event + $scope.$on('viewLoading', function() { + vm.isViewLoading = true; + }); - function isAdmin(userData) { - return userData.role === 'admin'; - } - function isAuthUser(userID, authUserData) { - return userID === authUserData.id; - } + $scope.$on('viewLoaded', function() { + vm.isViewLoading = false; + }); + + // listen for map state loading event + $scope.$on('mapStateLoading', function() { + if(vm.isViewLoading) { + return; + } + vm.mapStateLoading = true; + }); + + $scope.$on('mapStateLoaded', function() { + vm.mapStateLoading = false; + }); + }, + controllerAs: 'vm' + }; } })(); @@ -1852,754 +1828,814 @@ '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; - - //////////// + .controller('UserProfileController', UserProfileController); - function getDateIn(timeMS, format) { - if(!format) { - return timeMS; - } + 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 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; - } + var vm = this; + var userID = parseInt($stateParams.id); - function convertTime(time) { - return moment(time).toISOString(); - } + vm.status = undefined; + vm.user = {}; + vm.devices = []; + vm.filteredDevices = []; + vm.filterDevices = filterDevices; - function formatDate(time) { - return moment(time).format('YYYY-MM-DDTHH:mm:ss'); - } + $scope.$on('loggedIn', function() { + var authUser = auth.getCurrentUser().data; + if( userUtils.isAuthUser(userID, authUser) ) { + $location.path('/profile'); + } + }); - function getSecondsFromDate(date) { - return (new Date(date)).getTime(); - } + initialize(); - function getMillisFromDate(date) { - return (new Date(date)).getTime(); - } + ////////////////// - function getCurrentRange(fromDate, toDate) { - return moment(toDate).diff(moment(fromDate), 'days'); - } + function initialize() { - function getToday() { - return (new Date()).getTime(); - } + user.getUser(userID) + .then(function(user) { + vm.user = new NonAuthUser(user); - function getSevenDaysAgo() { - return getSecondsFromDate( getToday() - (7 * 24 * 60 * 60 * 1000) ); - } + if(!vm.user.devices.length) { + return []; + } - function getHourBefore(date) { - var now = moment(date); - return now.subtract(1, 'hour').valueOf(); - } + $q.all(vm.devices = vm.user.devices.map(function(data){ + return new PreviewDevice(data); + })) - function isSameDay(day1, day2) { - day1 = moment(day1); - day2 = moment(day2); + }).then(function(error) { + if(error && error.status === 404) { + $location.url('/404'); + } + }); - if(day1.startOf('day').isSame(day2.startOf('day'))) { - return true; + $timeout(function() { + setSidebarMinHeight(); + animation.viewLoaded(); + }, 500); } - return false; - } - function isDiffMoreThan15min(dateToCheckFrom, dateToCheckTo) { - var duration = moment.duration(moment(dateToCheckTo).diff(moment(dateToCheckFrom))); - return duration.as('minutes') > 15; + function filterDevices(status) { + if(status === 'all') { + status = undefined; + } + vm.status = status; + } + + function setSidebarMinHeight() { + var height = document.body.clientHeight / 4 * 3; + angular.element('.profile_content').css('min-height', height + 'px'); + } } +})(); - function isWithin15min(dateToCheck) { - var fifteenMinAgo = moment().subtract(15, 'minutes').valueOf(); - dateToCheck = moment(dateToCheck).valueOf(); +(function() { + 'use strict'; - return dateToCheck > fifteenMinAgo; - } + angular.module('app.components') + .controller('UploadController', UploadController); - function isWithin1Month(dateToCheck) { - var oneMonthAgo = moment().subtract(1, 'months').valueOf(); - dateToCheck = moment(dateToCheck).valueOf(); + UploadController.$inject = ['kit', '$state', '$stateParams', 'animation']; + function UploadController(kit, $state, $stateParams, animation) { + var vm = this; - return dateToCheck > oneMonthAgo; - } + vm.kit = kit; - function isWithin(number, type, dateToCheck) { - var ago = moment().subtract(number, type).valueOf(); - dateToCheck = moment(dateToCheck).valueOf(); + vm.backToProfile = backToProfile; - return dateToCheck > ago; + initialize(); + + ///////////////// + + 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'; +(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 + +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 }; - return service; + }) + }; +} - /////////////// - 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; +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 rollup = parseInt(durationInSec / chartWidth) + 's'; + }); + }) + ).then(() => { + vm.loadingStatus = false; + }).catch(() => { + vm.loadingStatus = false; + }); + } - /* - //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; - } + vm.haveSelectedFiles = function() { + return vm.csvFiles && vm.csvFiles.some((file) => file.checked); + }; - function getSensorName(name) { + vm.haveSelectedNoFiles = function() { + return vm.csvFiles && !vm.csvFiles.some((file) => file.checked); + }; - 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; - } - } - return sensorName.toUpperCase(); - } + vm.haveSelectedAllFiles = function() { + return vm.csvFiles && vm.csvFiles.every((file) => file.checked); + }; - function getSensorValue(sensor) { - var value = sensor.value; + 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; + }; - if(isNaN(parseInt(value))) { - value = 'NA'; - } else { - value = round(value, 1).toString(); - } + vm.selectAll = function(value) { + vm.csvFiles.forEach((file) => { file.checked = value }); + }; - return value; - } + 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) + }); + }; - function round(value, precision) { - var multiplier = Math.pow(10, precision || 0); - return Math.round(value * multiplier) / multiplier; - } + 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; + } + }; - function getSensorPrevValue(sensor) { - /*jshint camelcase: false */ - var prevValue = sensor.prev_value; - return (prevValue && prevValue.toString() ) || 0; - } + 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 + }); + } - function getSensorIcon(sensorName) { - var thisName = getSensorName(sensorName); + vm.uploadData = function() { + vm.loadingStatus = true; + vm.loadingType = 'indeterminate'; + vm.loadingProgress = 0; + let count = 0; - switch(thisName) { - case 'TEMPERATURE': - return './assets/images/temperature_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 'HUMIDITY': - return './assets/images/humidity_icon_new.svg'; - case 'LIGHT': - return './assets/images/light_icon_new.svg'; +angular.module('app.components') + .component('scCsvUpload', { + templateUrl: 'app/components/upload/csvUpload.html', + controller: controller, + bindings: { + kit: '<' + }, + controllerAs: 'vm' + }); +})(); - case 'SOUND': - return './assets/images/sound_icon_new.svg'; +(function() { + 'use strict'; - case 'CO': - return './assets/images/co_icon_new.svg'; + angular.module('app.components') + .controller('tagsController', tagsController); - case 'NO2': - return './assets/images/no2_icon_new.svg'; + tagsController.$inject = ['tag', '$scope', 'device', '$state', '$q', + 'PreviewDevice', 'animation' + ]; - case 'NETWORKS': - return './assets/images/networks_icon.svg'; + function tagsController(tag, $scope, device, $state, $q, PreviewDevice, + animation) { - case 'BATTERY': - return './assets/images/battery_icon.svg'; + var vm = this; - case 'SOLAR PANEL': - return './assets/images/solar_panel_icon.svg'; + vm.selectedTags = tag.getSelectedTags(); + vm.markers = []; + vm.kits = []; + vm.percActive = 0; - case 'BAROMETRIC PRESSURE': - return './assets/images/pressure_icon_new.svg'; + initialize(); - case 'PM 1': - case 'PM 2.5': - case 'PM 10': - return './assets/images/particle_icon_new.svg'; + ///////////////////////////////////////////////////////// - default: - return './assets/images/unknownsensor_icon.svg'; - } + function initialize() { + if(vm.selectedTags.length === 0){ + $state.transitionTo('layout.home.kit'); } - function getSensorArrow(currentValue, prevValue) { - currentValue = parseInt(currentValue) || 0; - prevValue = parseInt(prevValue) || 0; - - if(currentValue > prevValue) { - return 'arrow_up'; - } else if(currentValue < prevValue) { - return 'arrow_down'; - } else { - return 'equal'; - } + 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(); + }); } - function getSensorColor(sensorName) { - switch(getSensorName(sensorName)) { - case 'TEMPERATURE': - return '#FF3D4C'; + } - case 'HUMIDITY': - return '#55C4F5'; + function updateSelectedTags(){ - case 'LIGHT': - return '#ffc107'; + vm.markers = tag.filterMarkersByTag(device.getWorldMarkers()); - case 'SOUND': - return '#0019FF'; + 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 'CO': - return '#00A103'; + animation.viewLoaded(); - case 'NO2': - return '#8cc252'; + getTaggedDevices() + .then(function(res){ + vm.kits = res; + }); + } - case 'NETWORKS': - return '#681EBD'; - case 'SOLAR PANEL': - return '#d555ce'; + function isOnline(marker) { + return _.includes(marker.myData.labels, 'online'); + } - case 'BATTERY': - return '#ff8601'; + function descLastUpdate(o) { + return -new Date(o.last_reading_at).getTime(); + } - default: - return '#0019FF'; - } - } + function getTaggedDevices() { - function getSensorDescription(sensorID, sensorTypes) { - return _(sensorTypes) - .chain() - .find(function(sensorType) { - return sensorType.id === sensorID; - }) - .value() - .measurement.description; - } + 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 getMarkerDevice(marker) { + return device.getDevice(marker.myData.id); } + } + +})(); + +(function(){ + 'use strict'; + angular.module('app.components') + .directive('tag',tag); + + 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'); + + if(typeof(attrs.clickable) !== 'undefined'){ + element.bind('click', scope.openTag); + } + } + }; + } })(); (function() { 'use strict'; angular.module('app.components') - .factory('searchUtils', searchUtils); + .controller('StoreModalController', StoreModalController); + StoreModalController.$inject = ['$scope', '$mdDialog']; + function StoreModalController($scope, $mdDialog) { - searchUtils.$inject = []; - function searchUtils() { - var service = { - parseLocation: parseLocation, - parseName: parseName, - parseIcon: parseIcon, - parseIconType: parseIconType + $scope.cancel = function() { + $mdDialog.hide(); }; - return service; + } +})(); - ///////////////// +(function() { + 'use strict'; - function parseLocation(object) { - var location = ''; + angular.module('app.components') + .directive('store', store); - if(!!object.city) { - location += object.city; - } - if(!!object.city && !!object.country) { - location += ', '; - } - if(!!object.country) { - location += object.country; - } + function store() { + return { + scope: { + isLoggedin: '=logged' + }, + restrict: 'A', + controller: 'StoreController', + controllerAs: 'vm', + templateUrl: 'app/components/store/store.html' + }; + } +})(); - return location; - } +(function() { + 'use strict'; - function parseName(object) { - var name = object.type === 'User' ? object.username : object.name; - return name; - } + angular.module('app.components') + .controller('StoreController', StoreController); - 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'; - } - } + StoreController.$inject = ['$scope', '$mdDialog']; + function StoreController($scope, $mdDialog) { - function parseIconType(type) { - switch(type) { - case 'Device': - return 'div'; - default: - return 'img'; - } - } + $scope.showStore = showStore; + + $scope.$on('showStore', function() { + showStore(); + }); + + //////////////// + + function showStore() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'StoreModalController', + templateUrl: 'app/components/store/storeModal.html', + clickOutsideToClose: true + }); } + + } })(); (function() { 'use strict'; angular.module('app.components') - .factory('markerUtils', markerUtils); + .controller('StaticController', StaticController); - markerUtils.$inject = ['deviceUtils', 'MARKER_ICONS']; - function markerUtils(deviceUtils, MARKER_ICONS) { - var service = { - getIcon: getIcon, - getMarkerIcon: getMarkerIcon, - }; - _.defaults(service, deviceUtils); - return service; + StaticController.$inject = ['$timeout', 'animation', '$mdDialog', '$location', '$anchorScroll']; - /////////////// + function StaticController($timeout, animation, $mdDialog, $location, $anchorScroll) { + var vm = this; - function getIcon(object) { - var icon; - var labels = deviceUtils.parseSystemTags(object); - var isSCKHardware = deviceUtils.isSCKHardware(object); + vm.showStore = showStore; - if(hasLabel(labels, 'offline')) { - icon = MARKER_ICONS.markerSmartCitizenOffline; - } else if (isSCKHardware) { - icon = MARKER_ICONS.markerSmartCitizenOnline; - } else { - icon = MARKER_ICONS.markerExperimentalNormal; - } - return icon; - } + $anchorScroll.yOffset = 80; - function hasLabel(labels, targetLabel) { - return _.some(labels, function(label) { - return label === targetLabel; - }); - } + /////////////////////// - function getMarkerIcon(marker, state) { - var markerType = marker.icon.className; + initialize(); - 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]; + ////////////////// + + function initialize() { + $timeout(function() { + animation.viewLoaded(); + if($location.hash()){ + $anchorScroll(); } - return marker; - } + }, 500); + } + + 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); - - mapUtils.$inject = []; - function mapUtils() { - var service = { - getDefaultFilters: getDefaultFilters, - setDefaultFilters: setDefaultFilters, - canFilterBeRemoved: canFilterBeRemoved - }; - return service; + .controller('SignupModalController', SignupModalController); - ////////////// - - 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; - } + SignupModalController.$inject = ['$scope', '$mdDialog', 'user', + 'alert', 'animation']; + function SignupModalController($scope, $mdDialog, user, + alert, animation ) { + var vm = this; + vm.answer = function(signupForm) { - 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'; + if (!signupForm.$valid){ + return; } - return obj; - } - 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; - } - } + $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') - .config(function ($provide) { - $provide.decorator('$exceptionHandler', ['$delegate', function($delegate) { - return function (exception, cause) { - /*jshint camelcase: false */ - $delegate(exception, cause); - }; - }]); - }); + angular.module('app.components') + .directive('signup', signup); + + function signup() { + return { + scope: { + show: '=', + }, + restrict: 'A', + controller: 'SignupController', + controllerAs: 'vm', + templateUrl: 'app/components/signup/signup.html' + }; + } })(); (function() { 'use strict'; angular.module('app.components') - .factory('deviceUtils', deviceUtils); + .controller('SignupController', SignupController); - 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, - isLegacyVersion: isLegacyVersion, - isSCKHardware: isSCKHardware, - parseState: parseState, - parseAvatar: parseAvatar, - belongsToUser: belongsToUser, - parseSensorTime: parseSensorTime - }; + SignupController.$inject = ['$scope', '$mdDialog']; + function SignupController($scope, $mdDialog) { + var vm = this; - return service; + vm.showSignup = showSignup; - /////////////// + $scope.$on('showSignup', function() { + showSignup(); + }); + //////////////////////// - function parseLocation(object) { - var location = ''; - var city = ''; - var country = ''; - 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; + function showSignup() { + $mdDialog.show({ + fullscreen: true, + hasBackdrop: true, + controller: 'SignupModalController', + controllerAs: 'vm', + templateUrl: 'app/components/signup/signupModal.html', + clickOutsideToClose: true + }); } + } +})(); - function parseCoordinates(object) { - if (object.location) { - return { - lat: object.location.latitude, - lng: object.location.longitude - }; - } - // TODO: Bug - what happens if no location? - } +(function() { +'use strict'; - function parseSystemTags(object) { - /*jshint camelcase: false */ - return object.system_tags; - } - function parseUserTags(object) { - return object.user_tags; - } + angular.module('app.components') + .directive('search', search); - function parseNotifications(object){ - return { - lowBattery: object.notify.low_battery, - stopPublishing: object.notify.stopped_publishing - } - } + function search() { + return { + scope: true, + restrict: 'E', + templateUrl: 'app/components/search/search.html', + controller: 'SearchController', + controllerAs: 'vm' + }; + } +})(); - function classify(kitType) { - if(!kitType) { - return ''; - } - return kitType.toLowerCase().split(' ').join('_'); +(function() { + 'use strict'; + + angular.module('app.components') + .controller('SearchController', SearchController); + + SearchController.$inject = ['$scope', 'search', 'SearchResult', '$location', 'animation', 'SearchResultLocation']; + function SearchController($scope, search, SearchResult, $location, animation, SearchResultLocation) { + var vm = this; + + vm.searchTextChange = searchTextChange; + vm.selectedItemChange = selectedItemChange; + vm.querySearch = querySearch; + + /////////////////// + + function searchTextChange() { } - function parseName(object, trim=false) { - if(!object.name) { - return; - } - if (trim) { - return object.name.length <= 41 ? object.name : object.name.slice(0, 35).concat(' ... '); + 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}); } - return object.name; } - function parseHardware(object) { - if (!object.hardware) { - return; + function querySearch(query) { + if(query.length < 3) { + return []; } - 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) - } - } + return search.globalSearch(query) + .then(function(data) { - function parseString(str) { - if (typeof(str) !== 'string') { return null; } - return str; + return data.map(function(object) { + + if(object.type === 'City' || object.type === 'Country') { + return new SearchResultLocation(object); + } else { + return new SearchResult(object); + } + }); + }); } + } +})(); - 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 - }; - } +(function() { + 'use strict'; - function parseHardwareInfo (object) { - if (!object) { return null; } // null - if (typeof(object) == 'string') { return null; } // FILTERED + angular.module('app.components') + .controller('PasswordResetController', PasswordResetController); - 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); + 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 { - 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 - }; - } + initialize(); + /////////// - function parseHardwareName(object) { - if (object.hasOwnProperty('hardware')) { - if (!object.hardware.name) { - return 'Unknown hardware' - } - return object.hardware.name; - } else { - return 'Unknown hardware' - } + function initialize() { + $timeout(function() { + animation.viewLoaded(); + }, 500); + getUserData(); } - function isPrivate(object) { - return object.is_private; + function getUserData() { + auth.getResetPassword($stateParams.code) + .then(function() { + vm.showForm = true; + }) + .catch(function() { + alert.error('Wrong url'); + $location.path('/'); + }); } - 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; - } - } + function answer(data) { + vm.waitingFromServer = true; + vm.errors = undefined; - function isSCKHardware (object){ - if (!object.hardware || !object.hardware.type || object.hardware.type != 'SCK') { - return false; + if(data.newPassword === data.confirmPassword) { + vm.isDifferent = false; } else { - return true; + vm.isDifferent = true; + return; } - } - 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 - }; + 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 parseState(status) { - var name = parseStateName(status); - var className = classify(name); +(function() { + 'use strict'; - return { - name: name, - className: className - }; - } + angular.module('app.components') + .controller('PasswordRecoveryModalController', PasswordRecoveryModalController); - function parseStateName(object) { - return object.state.replace('_', ' '); - } + PasswordRecoveryModalController.$inject = ['$scope', 'animation', '$mdDialog', 'auth', 'alert']; + function PasswordRecoveryModalController($scope, animation, $mdDialog, auth, alert) { - function parseAvatar() { - return './assets/images/sckit_avatar.jpg'; - } + $scope.hide = function() { + $mdDialog.hide(); + }; + $scope.cancel = function() { + $mdDialog.cancel(); + }; - function parseSensorTime(sensor) { - /*jshint camelcase: false */ - return moment(sensor.recorded_at).format(''); - } + $scope.recoverPassword = function() { + $scope.waitingFromServer = true; + var data = { + /*jshint camelcase: false */ + email_or_username: $scope.input + }; - function belongsToUser(devicesArray, deviceID) { - return _.some(devicesArray, function(device) { - return device.id === deviceID; - }); - } + 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(); + }; } })(); @@ -2607,1310 +2643,566 @@ 'use strict'; angular.module('app.components') - .filter('filterLabel', filterLabel); + .controller('PasswordRecoveryController', PasswordRecoveryController); + PasswordRecoveryController.$inject = ['auth', 'alert', '$mdDialog']; + function PasswordRecoveryController(auth, alert, $mdDialog) { + var vm = this; - 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; - }); - } - }; - } + vm.waitingFromServer = false; + vm.errors = undefined; + vm.recoverPassword = recoverPassword; + + /////////////// + + 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'; - /** - * 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('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) { - /** - * Marker icons - * @constant - * @type {Object} - */ + var vm = this; - 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] + vm.unhighlightIcon = unhighlightIcon; + + //PROFILE TAB + vm.formUser = {}; + vm.getCountries = getCountries; + + vm.user = userData; + copyUserToForm(vm.formUser, vm.user); + vm.searchText = vm.formUser.country; + + vm.updateUser = updateUser; + vm.removeUser = removeUser; + vm.uploadAvatar = uploadAvatar; + + //THIS IS TEMPORARY. + // Will grow on to a dynamic API KEY management + // with the new /accounts oAuth mgmt methods + + // 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; + + vm.filteredDevices = []; + vm.dropdownSelected = undefined; + + //SIDEBAR + vm.filterDevices = filterDevices; + vm.filterTools = filterTools; + + vm.selectThisTab = selectThisTab; + + $scope.$on('loggedOut', function() { + $location.path('/'); + }); + + $scope.$on('devicesContextUpdated', function(){ + var userData = auth.getCurrentUser().data; + if(userData){ + vm.user = userData; + } + initialize(); + }); + + initialize(); + + ////////////////// + + function initialize() { + + startingTab(); + if(!vm.user.devices.length) { + vm.devices = []; + animation.viewLoaded(); + } else { + + vm.devices = vm.user.devices.map(function(data) { + return new PreviewDevice(data); + }) + + $timeout(function() { + mapWithBelongstoUser(vm.devices); + filterDevices(vm.status); + setSidebarMinHeight(); + animation.viewLoaded(); + }); + + } } - }); -})(); -(function() { - 'use strict'; + function filterDevices(status) { + if(status === 'all') { + status = undefined; + } + vm.deviceStatus = status; + vm.filteredDevices = $filter('filterLabel')(vm.devices, vm.deviceStatus); + } - /** - * 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 filterTools(type) { + if(type === 'all') { + type = undefined; + } + vm.toolType = type; + } -(function() { - 'use strict'; + 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; + } - /** - * Dropdown options for community button - * @constant - * @type {Array} - */ + 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; + }); + } - 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 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); -(function() { - 'use strict'; + $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.'); + }); + }); + } - /** - * 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'; - - angular.module('app.components') - .factory('user', user); - - user.$inject = ['Restangular']; - function user(Restangular) { - var service = { - createUser: createUser, - getUser: getUser, - updateUser: updateUser - }; - return service; - - //////////////////// - - function createUser(signupData) { - return Restangular.all('users').post(signupData); - } - - 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') - .factory('tag', tag); - - tag.$inject = ['Restangular']; - function tag(Restangular) { - var tags = []; - var selectedTags = []; - - var service = { - getTags: getTags, - getSelectedTags: getSelectedTags, - setSelectedTags: setSelectedTags, - tagWithName: tagWithName, - filterMarkersByTag: filterMarkersByTag - }; - - return service; - - ///////////////// - - function getTags() { - return Restangular.all('tags') - .getList({'per_page': 200}) - .then(function(fetchedTags){ - tags = fetchedTags.plain(); - return tags; - }); - } - - 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 filterMarkersByTag(tmpMarkers) { - var markers = filterMarkers(tmpMarkers); - return markers; - } - - 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') - .factory('sensor', sensor); - - sensor.$inject = ['Restangular', 'timeUtils', 'sensorUtils']; - function sensor(Restangular, timeUtils, sensorUtils) { - var sensorTypes; - callAPI().then(function(data) { - setTypes(data); - }); - - var service = { - callAPI: callAPI, - setTypes: setTypes, - getTypes: getTypes, - getSensorsData: getSensorsData - }; - return service; - - //////////////// - - function callAPI() { - return Restangular.all('sensors').getList({'per_page': 1000}); - } - - function setTypes(sensorTypes) { - sensorTypes = sensorTypes; - } - - function getTypes() { - return sensorTypes; - } - - function getSensorsData(deviceID, sensorID, dateFrom, dateTo) { - var rollup = sensorUtils.getRollup(dateFrom, dateTo); - dateFrom = timeUtils.convertTime(dateFrom); - dateTo = timeUtils.convertTime(dateTo); - - return Restangular.one('devices', deviceID).customGET('readings', {'from': dateFrom, 'to': dateTo, 'rollup': rollup, 'sensor_id': sensorID, 'all_intervals': true}); - } - } -})(); - -(function() { - 'use strict'; - - angular.module('app.components') - .factory('search', search); - - search.$inject = ['$http', 'Restangular']; - function search($http, Restangular) { - var service = { - globalSearch: globalSearch - }; - - return service; - - ///////////////////////// - - function globalSearch(query) { - return Restangular.all('search').getList({q: query}); - } - } -})(); - -(function() { - 'use strict'; - - // Deprecated module. Currently not in use within the app. - - angular.module('app.components') - .factory('push', push); - - function push() { - var socket; - - init(); - - var service = { - devices: devices, - device: device - }; - - function init(){ - socket = io.connect('wss://ws.smartcitizen'); - } - - function devices(then){ - socket.on('data-received', then); - } - - function device(id, scope){ - devices(function(data){ - if(id === data.id) { - scope.$emit('published', data); - } - }); - } - - return service; - } - -})(); - -(function() { - 'use strict'; - - angular.module('app.components') - .factory('measurement', measurement); - - measurement.$inject = ['Restangular']; - - function measurement(Restangular) { - - var service = { - getTypes: getTypes, - getMeasurement: getMeasurement - - }; - return service; - - //////////////// - - - function getTypes() { - return Restangular.all('measurements').getList({'per_page': 1000}); - } - - function getMeasurement(mesID) { - - return Restangular.one('measurements', mesID).get(); - } - } -})(); -(function() { - 'use strict'; - - angular.module('app.components') - .factory('geolocation', geolocation); - - 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'; - - angular.module('app.components') - .factory('file', file); - - 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 selectThisTab(iconIndex, uistate){ + /* This looks more like a hack but we need to workout how to properly use md-tab with ui-router */ - 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 - } - }); - } + highlightIcon(iconIndex); - function getImageURL(filename, size) { - size = size === undefined ? 's101' : size; + 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); + } - return 'https://images.smartcitizen.me/' + size + '/' + filename; } - } -})(); - -(function() { - 'use strict'; - angular.module('app.components') - .factory('device', device); - - device.$inject = ['Restangular', '$window', 'timeUtils','$http', 'auth', '$rootScope']; - function device(Restangular, $window, timeUtils, $http, auth, $rootScope) { - var worldMarkers; - - 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 - }; - - return service; + function startingTab() { + /* This looks more like a hack but we need to workout how to properly use md-tab with ui-router */ - ////////////////////////// + var childState = $state.current.name.split('.').pop(); - function initialize() { - if(areMarkersOld()) { - removeMarkers(); + switch(childState) { + case 'user': + vm.startingTab = 1; + break; + default: + vm.startingTab = 0; + break; } - } - 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 highlightIcon(iconIndex) { - function getAllDevicesCached() { - return Restangular.all('devices/world_map') - .getList() - .then(function(fetchedDevices){ - return fetchedDevices.plain(); - }); - } + var icons = angular.element('.myProfile_tab_icon'); - function getAllDevicesNoCached() { - return Restangular.all('devices/fresh_world_map') - .getList() - .then(function(fetchedDevices){ - return fetchedDevices.plain(); + _.each(icons, function(icon) { + unhighlightIcon(icon); }); - } - function getDevice(id) { - return Restangular.one('devices', id).get(); - } + var icon = icons[iconIndex]; - function createDevice(data) { - return Restangular.all('devices').post(data); + angular.element(icon).find('.stroke_container').css({'stroke': 'white', 'stroke-width': '0.01px'}); + angular.element(icon).find('.fill_container').css('fill', 'white'); } - function updateDevice(id, data) { - return Restangular.one('devices', id).patch(data); - } + function unhighlightIcon(icon) { + icon = angular.element(icon); - function getWorldMarkers() { - return worldMarkers || ($window.localStorage.getItem('smartcitizen.markers') && JSON.parse($window.localStorage.getItem('smartcitizen.markers') ).data); + icon.find('.stroke_container').css({'stroke': 'none'}); + icon.find('.fill_container').css('fill', '#FF8600'); } - 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 setSidebarMinHeight() { + var height = document.body.clientHeight / 4 * 3; + angular.element('.profile_content').css('min-height', height + 'px'); } - function getTimeStamp() { - return ($window.localStorage.getItem('smartcitizen.markers') && - JSON.parse($window.localStorage - .getItem('smartcitizen.markers') ).timestamp); + function getCountries(searchText) { + return _.filter(COUNTRY_CODES, createFilter(searchText)); } - function areMarkersOld() { - var markersDate = getTimeStamp(); - return !timeUtils.isWithin(1, 'minutes', markersDate); + function createFilter(searchText) { + searchText = searchText.toLowerCase(); + return function(country) { + country = country.toLowerCase(); + return country.indexOf(searchText) !== -1; + }; } - function removeMarkers() { - worldMarkers = null; - $window.localStorage.removeItem('smartcitizen.markers'); - } + function uploadAvatar(fileData) { + if(fileData && fileData.length) { - function mailReadings(kit) { - return Restangular - .one('devices', kit.id) - .customGET('readings/csv_archive'); + // 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 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 copyUserToForm(formData, userData) { + var props = {username: true, email: true, city: true, country: true, country_code: true, url: true, constructor: false}; - function updateContext (){ - return auth.updateUser().then(function(){ - removeMarkers(); - $rootScope.$broadcast('devicesContextUpdated'); - }); + for(var key in userData) { + if(props[key]) { + formData[key] = userData[key]; + } + } } - } -})(); - -(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 = {}; - - //wait until http interceptor is added to Restangular - $timeout(function() { - initialize(); - }, 100); - - 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 initialize() { - //console.log('---- AUTH INIT -----'); - setCurrentUser('appLoad'); + function mapWithBelongstoUser(devices){ + _.map(devices, addBelongProperty); } - //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; - } + function addBelongProperty(device){ + device.belongProperty = deviceBelongsToUser(device); + return device; + } - 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 newUser = new AuthUser(data); - //check sensitive information - if(user.data && user.data.role !== newUser.role) { - user.data = newUser; - $location.path('/'); - } - user.data = newUser; + 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') ))); - //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'); + var belongsToUser = deviceUtils.belongsToUser(userData.devices, deviceID); + var isAdmin = userUtils.isAdmin(userData); - // 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); - } - }); + return isAdmin || belongsToUser; } - // 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 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); - function getCurrentUser() { - user.token = getToken(); - user.data = $window.localStorage.getItem('smartcitizen.data') && new AuthUser(JSON.parse( $window.localStorage.getItem('smartcitizen.data') )); - return user; - } + $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); - // 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(); + $mdDialog.show(errorAlert); + }); } - // 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 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 getToken(){ - return $cookies.get('smartcitizen.token'); + $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 login(loginData) { - return Restangular.all('sessions').post(loginData); + $scope.addDeviceSelector = addDeviceSelector; + function addDeviceSelector(){ + $mdDialog.show({ + templateUrl: 'app/components/myProfile/addDeviceSelectorModal.html', + clickOutsideToClose: true, + multiple: true, + controller: DialogController, + }); } - function logout() { - $cookies.remove('smartcitizen.token'); + function DialogController($scope, $mdDialog){ + $scope.cancel = function(){ + $mdDialog.cancel(); + }; } - function getCurrentUserFromAPI() { - return Restangular.all('').customGET('me'); - } + 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); - function recoverPassword(data) { - return Restangular.all('password_resets').post(data); + $mdDialog + .show(confirm) + .then(function(){ + $state.go('layout.kitAdd'); + }); } - 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() { 'use strict'; - /** - * Unused directive. Double-check before removing. - * - */ angular.module('app.components') - .directive('slide', slide) - .directive('slideMenu', slideMenu); - - function slideMenu() { - return { - controller: controller, - link: link - }; - - function link(scope, element) { - scope.element = element; - } - - 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; - } - }; - } - } + .controller('MapTagModalController', MapTagModalController); - slide.$inject = []; - function slide() { - return { - link: link, - require: '^slide-menu', - restrict: 'A', - scope: { - direction: '@' - } - }; + MapTagModalController.$inject = ['$mdDialog', 'tag', 'selectedTags']; - function link(scope, element, attr, slideMenuCtrl) { - //select first sensor container - var sensorsContainer = angular.element('.sensors_container'); + function MapTagModalController($mdDialog, tag, selectedTags) { - element.on('click', function() { + var vm = this; - 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()); - } - }); - } - } -})(); + vm.checks = {}; -(function() { - 'use strict'; + vm.answer = answer; + vm.hide = hide; + vm.clear = clear; + vm.cancel = cancel; + vm.tags = []; - angular.module('app.components') - .directive('showPopupInfo', showPopupInfo); + init(); - /** - * Used to show/hide explanation of sensor value at kit dashboard - * - */ - showPopupInfo.$inject = []; - function showPopupInfo() { - return { - link: link - }; + //////////////////////////////////////////////////////// - ////// + function init() { + tag.getTags() + .then(function(tags) { + vm.tags = tags; + _.forEach(selectedTags, select); - 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'); }); - } } -})(); -(function() { - 'use strict'; + function answer() { - angular.module('app.components') - .directive('showPopup', showPopup); + var selectedTags = _(vm.tags) + .filter(isTagSelected) + .value(); + $mdDialog.hide(selectedTags); + } - /** - * Used on kit dashboard to open full sensor description - */ + function hide() { + answer(); + } - showPopup.$inject = []; - function showPopup() { - return { - link: link - }; + function clear() { + $mdDialog.hide(null); + } - ///// + function cancel() { + answer(); + } - 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(); - }); - } + function isTagSelected(tag) { + return vm.checks[tag.name]; + } + + function select(tag){ + vm.checks[tag] = true; } + } })(); (function() { 'use strict'; angular.module('app.components') - .directive('moveFilters', moveFilters); - - /** - * Moves map filters when scrolling - * - */ - moveFilters.$inject = ['$window', '$timeout']; - function moveFilters($window, $timeout) { - return { - link: link - }; + .controller('MapFilterModalController', MapFilterModalController); - function link() { - var chartHeight; - $timeout(function() { - chartHeight = angular.element('.kit_chart').height(); - }, 1000); + MapFilterModalController.$inject = ['$mdDialog','selectedFilters', '$timeout']; - /* - angular.element($window).on('scroll', function() { - var windowPosition = document.body.scrollTop; - if(chartHeight > windowPosition) { - elem.css('bottom', 12 + windowPosition + 'px'); - } - }); - */ - } - } -})(); + function MapFilterModalController($mdDialog, selectedFilters, $timeout) { -(function() { - 'use strict'; + var vm = this; - angular.module('app.components') - .factory('layout', layout); + vm.checks = {}; + vm.answer = answer; + vm.hide = hide; + vm.clear = clear; + vm.cancel = cancel; + vm.toggle = toggle; - function layout() { + vm.location = ['indoor', 'outdoor']; + vm.status = ['online', 'offline']; + vm.new = ['new']; - var kitHeight; + vm.filters = []; - var service = { - setKit: setKit, - getKit: getKit - }; - return service; + init(); - function setKit(height) { - kitHeight = height; - } + //////////////////////////////////////////////////////// - function getKit() { - return kitHeight; - } + function init() { + _.forEach(selectedFilters, select); } -})(); - -(function() { - 'use strict'; - angular.module('app.components') - .directive('horizontalScroll', horizontalScroll); + function answer() { + vm.filters = vm.filters.concat(vm.location, vm.status, vm.new); + var selectedFilters = _(vm.filters) + .filter(isFilterSelected) + .value(); + $mdDialog.hide(selectedFilters); + } - /** - * 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' - }; + function hide() { + answer(); + } - /////////////////// + function clear() { + vm.filters = vm.filters.concat(vm.location, vm.status, vm.new); + $mdDialog.hide(vm.filters); + } + function cancel() { + answer(); + } - function link(scope, element) { + function isFilterSelected(filter) { + return vm.checks[filter]; + } - 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 toggle(filters) { + $timeout(function() { - // 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; + 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; + } + } } - // 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'); - }); - $timeout(function() { - element.trigger('scroll'); }); + } - angular.element($window).on('resize', function() { - $timeout(function() { - element.trigger('scroll'); - }, 1000); - }); + function select(filter){ + vm.checks[filter] = true; } } })(); @@ -3919,386 +3211,477 @@ 'use strict'; angular.module('app.components') - .directive('hidePopup', hidePopup); + .controller('MapController', MapController); - /** - * Used on kit dashboard to hide popup with full sensor description - * - */ - - hidePopup.$inject = []; - function hidePopup() { - return { - 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; + + vm.markers = []; + + var retinaSuffix = isRetina() ? '512' : '256'; + var retinaLegacySuffix = isRetina() ? '@2x' : ''; + + var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; + + 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 + } + } + } }; - ///////////// + 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 + }; - function link(scope, elem) { - elem.on('mouseleave', function() { - angular.element('.sensor_description_preview').show(); - angular.element('.sensor_description_full').hide(); - }); - } - } -})(); + vm.defaults = { + dragging: true, + touchZoom: true, + scrollWheelZoom: true, + doubleClickZoom: true, + minZoom:2, + worldCopyJump: true + }; + + 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; + } + + vm.deviceLoading = true; + vm.center.lat = data.leafletEvent.latlng.lat; + vm.center.lng = data.leafletEvent.latlng.lng; + + if(id === parseInt($state.params.id)) { + $timeout(function() { + vm.deviceLoading = false; + }); + return; + } -(function() { - 'use strict'; + updateType = 'map'; - angular.module('app.components') - .directive('disableScroll', disableScroll); + if ($state.$current.name === 'embbed') { return; } + $state.go('layout.home.kit', {id: id}); - disableScroll.$inject = ['$timeout']; - function disableScroll($timeout) { - return { - // link: { - // pre: link - // }, - compile: link, - restrict: 'A', - priority: 100000 + // 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; + } + } + }); + + vm.readyForDevice = { + device: false, + map: false }; + $scope.$on('deviceLoaded', function(event, data) { + vm.readyForDevice.device = data; + }); - ////////////////////// + $scope.$watch('vm.readyForDevice', function() { + if (vm.readyForDevice.device && vm.readyForDevice.map) { + zoomDeviceAndPopUp(vm.readyForDevice.device); + } + }, true); - 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'); - }); - }); - } - } -})(); + $scope.$on('goToLocation', function(event, data) { + goToLocation(data); + }); -(function() { - 'use strict'; + vm.filters = ['indoor', 'outdoor', 'online', 'offline']; - angular.module('app.components') - .factory('animation', animation); + vm.openFilterPopup = openFilterPopup; + vm.openTagPopup = openTagPopup; + vm.removeFilter = removeFilter; + vm.removeTag = removeTag; + vm.selectedTags = tag.getSelectedTags(); + vm.selectedFilters = ['indoor', 'outdoor', 'online', 'offline', 'new']; - /** - * Used to emit events from rootscope. - * - * This events are then listened by $scope on controllers and directives that care about that particular event - */ + vm.checkAllFiltersSelected = checkAllFiltersSelected; - animation.$inject = ['$rootScope']; - function animation($rootScope) { + initialize(); - 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; + ///////////////////// - ////////////// + function initialize() { - 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'); - } - } -})(); + vm.readyForDevice.map = false; -(function() { - 'use strict'; + $q.all([device.getAllDevices($stateParams.reloadMap)]) + .then(function(data){ - /** - * TODO: Improvement These directives can be split up each one in a different file - */ + data = data[0]; - 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); + 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(); - /** - * It moves down kit section to ease the transition after the kit menu is sticked to the top - * - */ - moveDown.$inject = []; - function moveDown() { + var markersByIndex = _.keyBy(vm.markers, function(marker) { + return marker.myData.id; + }); - function link(scope, element) { - scope.$watch('moveDown', function(isTrue) { - if(isTrue) { - element.addClass('move_down'); - } else { - element.removeClass('move_down'); - } - }); + 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; + } + + }); } - return { - link: link, - scope: false, - restrict: 'A' - }; - } + function zoomDeviceAndPopUp(data){ - /** - * 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(); + if(updateType === 'map') { + vm.deviceLoading = false; + updateType = undefined; + return; + } else { + vm.deviceLoading = true; + } - $timeout(function() { - elementPosition = element[0].offsetTop; - //var elementHeight = element[0].offsetHeight; - navbarHeight = angular.element('.stickNav').height(); - }, 1000); + leafletData.getMarkers() + .then(function(markers) { + var currentMarker = _.find(markers, function(marker) { + return data.id === marker.options.myData.id; + }); + + var id = data.id; + + 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(); + }); + } + }); + }); + } - angular.element($window).on('scroll', function() { - var windowPosition = document.body.scrollTop; + function checkAllFiltersSelected() { + var allFiltersSelected = _.every(vm.filters, function(filterValue) { + return _.includes(vm.selectedFilters, filterValue); + }); + return allFiltersSelected; + } - //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; - }); + 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(); }); } - return { - link: link, - scope: false, - restrict: 'A' - }; - } - - /** - * Unused directive. Double-check is not being used before removing it - * - */ - - function blur() { + 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, element) { + function updateMapFilters(){ + vm.selectedTags = tag.getSelectedTags(); + checkAllFiltersSelected(); + updateMarkers(); + } - scope.$on('blur', function() { - element.addClass('blur'); + function removeFilter(filterName) { + vm.selectedFilters = _.filter(vm.selectedFilters, function(el){ + return el !== filterName; }); + if(vm.selectedFilters.length === 0){ + vm.selectedFilters = vm.filters; + } + updateMarkers(); + } - scope.$on('unblur', function() { - element.removeClass('blur'); + 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); + }); }); } - return { - link: link, - scope: false, - restrict: 'A' - }; - } + function updateMarkers() { + $timeout(function() { + $scope.$apply(function() { + var allMarkers = device.getWorldMarkers(); - /** - * 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 updatedMarkers = allMarkers; - element.on('focusout', function() { - animation.addNav(); - }); + updatedMarkers = tag.filterMarkersByTag(updatedMarkers); + updatedMarkers = filterMarkersByLabel(updatedMarkers); + vm.markers = updatedMarkers; - 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'); - }); + animation.mapStateLoaded(); - searchInput.on('focus', function() { - angular.element(document.body).css('overflow', 'hidden'); + vm.deviceLoading = false; + + zoomOnMarkers(); + }); }); } - return { - link: link - }; - } + function getZoomLevel(data) { + // data.layer is an array of strings like ["establishment", "point_of_interest"] + var zoom = 18; - /** - * Changes map section based on screen size - * - */ - changeMapHeight.$inject = ['$document', 'layout', '$timeout']; - function changeMapHeight($document, layout, $timeout) { - function link(scope, element) { + 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; + } + } - var screenHeight = $document[0].body.clientHeight; - var navbarHeight = angular.element('.stickNav').height(); + return zoom; + } - // var overviewHeight = angular.element('.kit_overview').height(); - // var menuHeight = angular.element('.kit_menu').height(); - // var chartHeight = angular.element('.kit_chart').height(); + 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 resizeMap(){ - $timeout(function() { - var overviewHeight = angular.element('.over_map').height(); + 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); + }); + } - var objectsHeight = navbarHeight + overviewHeight; - var objectsHeightPercentage = parseInt((objectsHeight * 100) / screenHeight); - var mapHeightPercentage = 100 - objectsHeightPercentage; + function removeTag(tagName){ + tag.setSelectedTags(_.filter(vm.selectedTags, function(el){ + return el !== tagName; + })); - element.css('height', mapHeightPercentage + '%'); + vm.selectedTags = tag.getSelectedTags(); - var aboveTheFoldHeight = screenHeight - overviewHeight; - angular - .element('section[change-content-margin]') - .css('margin-top', aboveTheFoldHeight + 'px'); - }); + if(vm.selectedTags.length === 0){ + reloadNoTags(); + } else { + reloadWithTags(); } - resizeMap(); - - scope.element = element; + } - scope.$on('resizeMapHeight',function(){ - resizeMap(); + 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 reloadWithTags(){ + $state.transitionTo('layout.home.tags', {tags: vm.selectedTags}, {reload: true}); + } + function reloadNoTags(){ + $state.transitionTo('layout.home.kit'); } - return { - link: link, - scope: true, - restrict: 'A' - }; } - /** - * 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; +(function() { + 'use strict'; - var overviewHeight = angular.element('.over_map').height(); + angular.module('app.components') + .controller('LoginModalController', LoginModalController); - var aboveTheFoldHeight = screenHeight - overviewHeight; - element.css('margin-top', aboveTheFoldHeight + 'px'); - } + 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(); + }; - return { - link: link + $scope.openSignup = function() { + animation.showSignup(); + $mdDialog.hide(); }; - } - /** - * Fixes autofocus for inputs that are inside modals - * - */ - focusInput.$inject = ['$timeout']; - function focusInput($timeout) { - function link(scope, elem) { - $timeout(function() { - elem.focus(); + $scope.openPasswordRecovery = function() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'PasswordRecoveryModalController', + templateUrl: 'app/components/passwordRecovery/passwordRecoveryModal.html', + clickOutsideToClose: true }); - } - return { - link: link + + $mdDialog.hide(); }; } })(); @@ -4306,162 +3689,19 @@ (function() { 'use strict'; - angular.module('app.components') - .directive('activeButton', activeButton); - - /** - * Used to highlight and unhighlight buttons on kit menu - * - * It attaches click handlers dynamically - */ + angular.module('app.components') + .directive('login', login); - activeButton.$inject = ['$timeout', '$window']; - function activeButton($timeout, $window) { + function login() { return { - link: link, - restrict: 'A' - + scope: { + show: '=' + }, + restrict: 'A', + controller: 'LoginController', + controllerAs: 'vm', + templateUrl: 'app/components/login/login.html' }; - - //////////////////////////// - - function link(scope, element) { - var childrens = element.children(); - var container; - - $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 highlightButton(button) { - var clickedButton = angular.element(button); - //add border, fill and stroke to every icon - clickedButton.addClass('button_active'); - - var strokeContainer = clickedButton.find('.stroke_container'); - strokeContainer.css('stroke', 'white'); - strokeContainer.css('stroke-width', '0.01px'); - - var fillContainer = strokeContainer.find('.fill_container'); - fillContainer.css('fill', 'white'); - } - - //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'); - } - } - }); - }); - - var currentSection; - - //on scroll, check if window is on a section - angular.element($window).on('scroll', function() { - if(!container){ return; } - - 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'; - } - }); - } } })(); @@ -4469,71 +3709,131 @@ 'use strict'; angular.module('app.components') - .controller('UserProfileController', UserProfileController); + .controller('LoginController', LoginController); - 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) { + LoginController.$inject = ['$scope', '$mdDialog']; + function LoginController($scope, $mdDialog) { + + $scope.showLogin = showLogin; + + $scope.$on('showLogin', function() { + showLogin(); + }); + + //////////////// + + function showLogin() { + $mdDialog.show({ + hasBackdrop: true, + fullscreen: true, + controller: 'LoginModalController', + controllerAs: 'vm', + templateUrl: 'app/components/login/loginModal.html', + clickOutsideToClose: true + }); + } + + } +})(); +(function() { + 'use strict'; + + angular.module('app.components') + .controller('LayoutController', LayoutController); + + 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 userID = parseInt($stateParams.id); - vm.status = undefined; - vm.user = {}; - vm.devices = []; - vm.filteredDevices = []; - vm.filterDevices = filterDevices; + vm.navRightLayout = 'space-around center'; - $scope.$on('loggedIn', function() { - var authUser = auth.getCurrentUser().data; - if( userUtils.isAuthUser(userID, authUser) ) { - $location.path('/profile'); + $scope.toggleRight = buildToggler('right'); + + function buildToggler(componentId) { + return function() { + $mdSidenav(componentId).toggle(); + }; + } + + // 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(); } }); - initialize(); + // 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'; + }); - ////////////////// - function initialize() { + vm.isShown = true; + vm.isLoggedin = false; + vm.logout = logout; - user.getUser(userID) - .then(function(user) { - vm.user = new NonAuthUser(user); + vm.dropdownOptions = DROPDOWN_OPTIONS_USER; + vm.dropdownSelected = undefined; - if(!vm.user.devices.length) { - return []; - } + vm.dropdownOptionsCommunity = DROPDOWN_OPTIONS_COMMUNITY; + vm.dropdownSelectedCommunity = undefined; - $q.all(vm.devices = vm.user.devices.map(function(data){ - return new PreviewDevice(data); - })) + $scope.$on('removeNav', function() { + vm.isShown = false; + }); - }).then(function(error) { - if(error && error.status === 404) { - $location.url('/404'); - } - }); + $scope.$on('addNav', function() { + vm.isShown = true; + }); - $timeout(function() { - setSidebarMinHeight(); - animation.viewLoaded(); - }, 500); - } + initialize(); - function filterDevices(status) { - if(status === 'all') { - status = undefined; - } - vm.status = status; + ////////////////// + + 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 setSidebarMinHeight() { - var height = document.body.clientHeight / 4 * 3; - angular.element('.profile_content').css('min-height', height + 'px'); + function logout() { + auth.logout(); + vm.isLoggedin = false; } } })(); @@ -4542,1807 +3842,2260 @@ 'use strict'; angular.module('app.components') - .controller('UploadController', UploadController); + .controller('LandingController', LandingController); - UploadController.$inject = ['kit', '$state', '$stateParams', 'animation']; - function UploadController(kit, $state, $stateParams, animation) { + LandingController.$inject = ['$timeout', 'animation', '$mdDialog', '$location', '$anchorScroll']; + + function LandingController($timeout, animation, $mdDialog, $location, $anchorScroll) { var vm = this; - vm.kit = kit; + vm.showStore = showStore; + vm.goToHash = goToHash; - vm.backToProfile = backToProfile; + /////////////////////// initialize(); - ///////////////// + ////////////////// function initialize() { - animation.viewLoaded(); + $timeout(function() { + animation.viewLoaded(); + if($location.hash()) { + $anchorScroll(); + } + }, 500); } - function backToProfile() { - $state.transitionTo('layout.myProfile.kits', $stateParams, - { reload: false, - inherit: false, - notify: true + function goToHash(hash){ + $location.hash(hash); + $anchorScroll(); + } + + function showStore() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'StoreModalController', + templateUrl: 'app/components/store/storeModal.html', + clickOutsideToClose: true }); } } })(); (function(){ -'use strict'; + 'use strict'; + angular.module('app.components') + .directive('kitList',kitList); + function kitList(){ + return{ + restrict:'E', + scope:{ + devices:'=devices', + actions: '=actions' + }, + controllerAs:'vm', + templateUrl:'app/components/kitList/kitList.html' + }; + } +})(); +(function() { + 'use strict'; -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 - }; - }) - }; -} + angular.module('app.components') + .controller('HomeController', HomeController); + + function HomeController() { + } +})(); +(function (){ + 'use strict'; + + angular.module('app.components') + .controller('DownloadModalController', DownloadModalController); + + DownloadModalController.$inject = ['thisDevice', 'device', '$mdDialog']; + + function DownloadModalController(thisDevice, device, $mdDialog) { + var vm = this; + + vm.device = thisDevice; + vm.download = download; + vm.cancel = cancel; + //////////////////////////// + function download(){ + device.mailReadings(vm.device) + .then(function (){ + $mdDialog.hide(); + }).catch(function(err){ + $mdDialog.cancel(err); + }); + } -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; + function cancel(){ + $mdDialog.cancel(); + } + } - }); - }) - ).then(() => { - vm.loadingStatus = false; - }).catch(() => { - vm.loadingStatus = false; - }); - } +})(); - vm.haveSelectedFiles = function() { - return vm.csvFiles && vm.csvFiles.some((file) => file.checked); - }; +(function(){ +'use strict'; - vm.haveSelectedNoFiles = function() { - return vm.csvFiles && !vm.csvFiles.some((file) => file.checked); - }; +angular.module('app.components') + .directive('cookiesLaw', cookiesLaw); - vm.haveSelectedAllFiles = function() { - return vm.csvFiles && vm.csvFiles.every((file) => file.checked); - }; - 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; - }; +cookiesLaw.$inject = ['$cookies']; - vm.selectAll = function(value) { - vm.csvFiles.forEach((file) => { file.checked = value }); - }; +function cookiesLaw($cookies) { + return { + template: + '
' + + 'This site uses cookies to offer you a better experience. ' + + ' Accept or' + + ' Learn More. ' + + '
', + controller: function($scope) { - 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) - }); - }; + var init = function(){ + $scope.isCookieValid(); + } - 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; - } - }; + // Helpers to debug + // You can also use `document.cookie` in the browser dev console. + //console.log($cookies.getAll()); - 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 - }); - } + $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); - vm.uploadData = function() { - vm.loadingStatus = true; - vm.loadingType = 'indeterminate'; - vm.loadingProgress = 0; - let count = 0; + $cookies.put('consent', true, {'expires' : expireDate.toUTCString()} ); - $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; - }); - } -}; + // Trigger the check again, after we click + $scope.isCookieValid(); + }; + + init(); + + } + }; +} -angular.module('app.components') - .component('scCsvUpload', { - templateUrl: 'app/components/upload/csvUpload.html', - controller: controller, - bindings: { - kit: '<' - }, - controllerAs: 'vm' - }); })(); (function() { 'use strict'; angular.module('app.components') - .controller('tagsController', tagsController); + .directive('chart', chart); - tagsController.$inject = ['tag', '$scope', 'device', '$state', '$q', - 'PreviewDevice', 'animation' - ]; + 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; - function tagsController(tag, $scope, device, $state, $q, PreviewDevice, - animation) { + return { + link: link, + restrict: 'A', + scope: { + chartData: '=' + } + }; - var vm = this; + function link(scope, elem) { - vm.selectedTags = tag.getSelectedTags(); - vm.markers = []; - vm.kits = []; - vm.percActive = 0; + $timeout(function() { + createChart(elem[0]); + }, 0); - initialize(); + var lastData = {}; + + // 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}); + }); + + 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() { - if(vm.selectedTags.length === 0){ - $state.transitionTo('layout.home.kit'); - } + var sensorDataCompare = newData[1].data; + var dataCompare = sensorDataCompare.map(function(dataPoint) { + return { + date: dateFormat(dataPoint.time), + count: dataPoint && dataPoint.count, + value: dataPoint && dataPoint.value + }; + }); - 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(); - }); - } + dataCompare.sort(function(a, b) { + return a.date - b.date; + }); - } + 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]) { - function updateSelectedTags(){ + 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 + }; + }); - vm.markers = tag.filterMarkersByTag(device.getWorldMarkers()); + data.sort(function(a, b) { + return a.date - b.date; + }); - 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 color = newData[0].color; + var unit = newData[0].unit; - animation.viewLoaded(); + lastData = { + data: data, + type: 'main', + color: color, + unit: unit + }; - getTaggedDevices() - .then(function(res){ - vm.kits = res; + updateChartData(data, {type: 'main', container: elem[0], color: color, unit: unit }); + } + animation.hideChartSpinner(); + } }); - } + } + // creates the container that is re-used across different sensor charts + function createChart(elem) { + d3.select(elem).selectAll('*').remove(); - function isOnline(marker) { - return _.includes(marker.myData.labels, 'online'); - } + margin = {top: 20, right: 12, bottom: 20, left: 42}; + width = elem.clientWidth - margin.left - margin.right; + height = elem.clientHeight - margin.top - margin.bottom; - function descLastUpdate(o) { - return -new Date(o.last_reading_at).getTime(); - } + 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 getTaggedDevices() { + 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' - var deviceProm = _.map(vm.markers, getMarkerDevice); + xAxis = d3.svg.axis() + .scale(xScale) + .orient('bottom') + .ticks(5); - return $q.all(deviceProm) - .then(function(devices) { - return _.map(_.sortBy(devices, descLastUpdate), toPreviewDevice); // This sort is temp - }); - } + yAxisLeft = d3.svg.axis() + .scale(yScale0) + .orient('left') + .ticks(5); - function toPreviewDevice(dev) { - return new PreviewDevice(dev); - } + yAxisRight = d3.svg.axis() + .scale(yScale1) + .orient('right') + .ticks(5); - function getMarkerDevice(marker) { - return device.getDevice(marker.myData.id); - } - } + 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); }); -(function(){ - 'use strict'; - angular.module('app.components') - .directive('tag',tag); + 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 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'); + 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); }); - if(typeof(attrs.clickable) !== 'undefined'){ - element.bind('click', scope.openTag); + 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]); -(function() { - 'use strict'; + svg.selectAll('*').remove(); - angular.module('app.components') - .controller('StoreModalController', StoreModalController); + //Add the area path + svg.append('path') + .datum(data) + .attr('class', 'chart_area') + .attr('fill', options.color) + .attr('d', areaMain); - StoreModalController.$inject = ['$scope', '$mdDialog']; - function StoreModalController($scope, $mdDialog) { + // Add the valueline path. + svg.append('path') + .attr('class', 'chart_line') + .attr('stroke', options.color) + .attr('d', valueLineMain(data)); - $scope.cancel = function() { - $mdDialog.hide(); - }; - } -})(); + // 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); + + // Draw the x Grid lines + svg.append('g') + .attr('class', 'grid') + .attr('transform', 'translate(0,' + height + ')') + .call(xGrid() + .tickSize(-height, 0, 0) + .tickFormat('') + ); + + // Draw the y Grid lines + svg.append('g') + .attr('class', 'grid') + .call(yGrid() + .tickSize(-width, 0, 0) + .tickFormat('') + ); - angular.module('app.components') - .directive('store', store); + focusMain = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - function store() { - return { - scope: { - isLoggedin: '=logged' - }, - restrict: 'A', - controller: 'StoreController', - controllerAs: 'vm', - templateUrl: 'app/components/store/store.html' - }; - } -})(); + focusMain.append('circle') + .style('stroke', options.color) + .attr('r', 4.5); -(function() { - 'use strict'; + var popupWidth = 84; + var popupHeight = 46; - angular.module('app.components') - .controller('StoreController', StoreController); + popup = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - StoreController.$inject = ['$scope', '$mdDialog']; - function StoreController($scope, $mdDialog) { + popupContainer = popup.append('rect') + .attr('width', popupWidth) + .attr('height', popupHeight) + .attr('transform', function() { + var result = 'translate(-42, 5)'; - $scope.showStore = showStore; + return result; + }) + .style('stroke', 'grey') + .style('stroke-width', '0.5') + .style('fill', 'white'); - $scope.$on('showStore', function() { - showStore(); - }); - - //////////////// + var text = popup.append('text') + .attr('class', ''); - function showStore() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'StoreModalController', - templateUrl: 'app/components/store/storeModal.html', - clickOutsideToClose: true - }); - } + 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); - } -})(); + textMain.append('tspan') + .attr('class', 'popup_value'); -(function() { - 'use strict'; + textMain.append('tspan') + .attr('class', 'popup_unit') + .attr('dx', 5); - angular.module('app.components') - .controller('StaticController', StaticController); + 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' ); - StaticController.$inject = ['$timeout', 'animation', '$mdDialog', '$location', '$anchorScroll']; + 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 StaticController($timeout, animation, $mdDialog, $location, $anchorScroll) { - var vm = this; - vm.showStore = showStore; - $anchorScroll.yOffset = 80; + function mousemove() { + var bisectDate = d3.bisector(function(d) { return d.date; }).left; - /////////////////////// + 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; - initialize(); + 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)); - ////////////////// + var textContainers = [ + textMain, + date + ]; - function initialize() { - $timeout(function() { - animation.viewLoaded(); - if($location.hash()){ - $anchorScroll(); + 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) + ')'); + } else { + popup.attr('transform', 'translate(' + (xScale(d.date) + 80) + ', ' + (d3.mouse(this)[1] - 20) + ')'); + } } - }, 500); - } + } - function showStore() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'StoreModalController', - templateUrl: 'app/components/store/storeModal.html', - clickOutsideToClose: true - }); - } - } -})(); + // 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() { - 'use strict'; + svg.selectAll('*').remove(); - angular.module('app.components') - .controller('SignupModalController', SignupModalController); + //Add both area paths + svg.append('path') + .datum(data[0]) + .attr('class', 'chart_area') + .attr('fill', options.color[0]) + .attr('d', areaMain); - SignupModalController.$inject = ['$scope', '$mdDialog', 'user', - 'alert', 'animation']; - function SignupModalController($scope, $mdDialog, user, - alert, animation ) { - var vm = this; - vm.answer = function(signupForm) { + svg.append('path') + .datum(data[1]) + .attr('class', 'chart_area') + .attr('fill', options.color[1]) + .attr('d', areaCompare); - if (!signupForm.$valid){ - return; - } + // Add both valueline paths. + svg.append('path') + .attr('class', 'chart_line') + .attr('stroke', options.color[0]) + .attr('d', valueLineMain(data[0])); - $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(); - }; + svg.append('path') + .attr('class', 'chart_line') + .attr('stroke', options.color[1]) + .attr('d', valueLineCompare(data[1])); - $scope.openLogin = function() { - animation.showLogin(); - $mdDialog.hide(); - }; - } -})(); + // 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') - .directive('signup', signup); + svg.append('g') + .attr('class', 'axis y_right') + .attr('transform', 'translate(' + width + ' ,0)') + .call(yAxisRight); - function signup() { - return { - scope: { - show: '=', - }, - restrict: 'A', - controller: 'SignupController', - controllerAs: 'vm', - templateUrl: 'app/components/signup/signup.html' - }; - } -})(); + // Draw the x Grid lines + svg.append('g') + .attr('class', 'grid') + .attr('transform', 'translate(0,' + height + ')') + .call(xGrid() + .tickSize(-height, 0, 0) + .tickFormat('') + ); + + // Draw the y Grid lines + svg.append('g') + .attr('class', 'grid') + .call(yGrid() + .tickSize(-width, 0, 0) + .tickFormat('') + ); + + focusCompare = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); -(function() { - 'use strict'; + focusMain = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - angular.module('app.components') - .controller('SignupController', SignupController); + focusCompare.append('circle') + .style('stroke', options.color[1]) + .attr('r', 4.5); - SignupController.$inject = ['$scope', '$mdDialog']; - function SignupController($scope, $mdDialog) { - var vm = this; + focusMain.append('circle') + .style('stroke', options.color[0]) + .attr('r', 4.5); - vm.showSignup = showSignup; + var popupWidth = 84; + var popupHeight = 75; - $scope.$on('showSignup', function() { - showSignup(); - }); - //////////////////////// + popup = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); + popupContainer = popup.append('rect') + .attr('width', popupWidth) + .attr('height', popupHeight) + .style('min-width', '40px') + .attr('transform', function() { + var result = 'translate(-42, 5)'; - function showSignup() { - $mdDialog.show({ - fullscreen: true, - hasBackdrop: true, - controller: 'SignupModalController', - controllerAs: 'vm', - templateUrl: 'app/components/signup/signupModal.html', - clickOutsideToClose: true - }); - } - } -})(); + return result; + }) + .style('stroke', 'grey') + .style('stroke-width', '0.5') + .style('fill', 'white'); -(function() { -'use strict'; + 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]); - angular.module('app.components') - .directive('search', search); + var text = popup.append('text') + .attr('class', ''); - function search() { - return { - scope: true, - restrict: 'E', - templateUrl: 'app/components/search/search.html', - controller: 'SearchController', - controllerAs: 'vm' - }; - } -})(); + 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() { - 'use strict'; + textMain.append('tspan') + .attr('class', 'popup_value') + .attr( 'text-anchor', 'start' ); - angular.module('app.components') - .controller('SearchController', SearchController); + textMain.append('tspan') + .attr('class', 'popup_unit') + .attr('dx', 5); - SearchController.$inject = ['$scope', 'search', 'SearchResult', '$location', 'animation', 'SearchResultLocation']; - function SearchController($scope, search, SearchResult, $location, animation, SearchResultLocation) { - var vm = this; + 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); - vm.searchTextChange = searchTextChange; - vm.selectedItemChange = selectedItemChange; - vm.querySearch = querySearch; + textCompare.append('tspan') + .attr('class', 'popup_value') + .attr( 'text-anchor', 'start' ); - /////////////////// + textCompare.append('tspan') + .attr('class', 'popup_unit') + .attr('dx', 5); - function searchTextChange() { - } + 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 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}); - } - } + 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); - function querySearch(query) { - if(query.length < 3) { - return []; - } + function mousemove() { + var bisectDate = d3.bisector(function(d) { return d.date; }).left; - return search.globalSearch(query) - .then(function(data) { + 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) + ')'); - return data.map(function(object) { - if(object.type === 'City' || object.type === 'Country') { - return new SearchResultLocation(object); - } else { - return new SearchResult(object); - } - }); - }); - } - } -})(); + 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('PasswordResetController', PasswordResetController); + var textContainers = [ + textMain, + textCompare, + date + ]; - 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; + var popupWidth = resizePopup(popupContainer, textContainers); - initialize(); - /////////// + 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 initialize() { - $timeout(function() { - animation.viewLoaded(); - }, 500); - getUserData(); + function xGrid() { + return d3.svg.axis() + .scale(xScale) + .orient('bottom') + .ticks(5); } - function getUserData() { - auth.getResetPassword($stateParams.code) - .then(function() { - vm.showForm = true; - }) - .catch(function() { - alert.error('Wrong url'); - $location.path('/'); - }); + function yGrid() { + return d3.svg.axis() + .scale(yScale0) + .orient('left') + .ticks(5); + } + + 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 answer(data) { - vm.waitingFromServer = true; - vm.errors = undefined; - - if(data.newPassword === data.confirmPassword) { - vm.isDifferent = false; - } else { - vm.isDifferent = true; + function resizePopup(popupContainer, textContainers) { + if(!textContainers.length) { 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; - }); + 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() { +(function(){ 'use strict'; angular.module('app.components') - .controller('PasswordRecoveryModalController', PasswordRecoveryModalController); + .directive('apiKey', apiKey); - PasswordRecoveryModalController.$inject = ['$scope', 'animation', '$mdDialog', 'auth', 'alert']; - function PasswordRecoveryModalController($scope, animation, $mdDialog, auth, alert) { + function apiKey(){ + return { + scope: { + apiKey: '=apiKey' + }, + restrict: 'A', + controller: 'ApiKeyController', + controllerAs: 'vm', + templateUrl: 'app/components/apiKey/apiKey.html' + }; + } +})(); - $scope.hide = function() { - $mdDialog.hide(); - }; - $scope.cancel = function() { - $mdDialog.cancel(); - }; +(function(){ + 'use strict'; - $scope.recoverPassword = function() { - $scope.waitingFromServer = true; - var data = { - /*jshint camelcase: false */ - email_or_username: $scope.input - }; + angular.module('app.components') + .controller('ApiKeyController', ApiKeyController); - 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; - }); - }; + ApiKeyController.$inject = ['alert']; + function ApiKeyController(alert){ + var vm = this; - $scope.openSignup = function() { - animation.showSignup(); - $mdDialog.hide(); - }; + vm.copied = copied; + vm.copyFail = copyFail; + + /////////////// + + function copied(){ + alert.success('API key copied to your clipboard.'); + } + + 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') - .controller('PasswordRecoveryController', PasswordRecoveryController); + .factory('alert', alert); - PasswordRecoveryController.$inject = ['auth', 'alert', '$mdDialog']; - function PasswordRecoveryController(auth, alert, $mdDialog) { - var vm = this; + 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 + } + }; - vm.waitingFromServer = false; - vm.errors = undefined; - vm.recoverPassword = recoverPassword; + return service; - /////////////// + /////////////////// - function recoverPassword() { - vm.waitingFromServer = true; - vm.errors = undefined; - - var data = { - username: vm.username - }; + function success(message) { + toast('success', message); + } - 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 error(message) { + toast('error', message); + } -(function() { - 'use strict'; + 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/' + }); + } - angular.module('app.components') - .controller('MyProfileController', MyProfileController); + function infoNoDataOwner() { + info('Woah! We couldn\'t locate this kit on the map because it hasn\'t published any data.', + 10000); + } - 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 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/' + }); + } - var vm = this; + // TODO: Refactor, check why this was removed + // function infoDataInvalid() { + // info('Device not found, or it has been set to private.', + // 10000); + // } - vm.unhighlightIcon = unhighlightIcon; + 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/' + }); + } - //PROFILE TAB - vm.formUser = {}; - vm.getCountries = getCountries; + 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 + } + }); + } + } +})(); - 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') + .controller('AlertController', AlertController); - //THIS IS TEMPORARY. - // Will grow on to a dynamic API KEY management - // with the new /accounts oAuth mgmt methods + AlertController.$inject = ['$scope', '$mdToast', 'message', 'button', 'href']; + function AlertController($scope, $mdToast, message, button, href) { + var vm = this; - // 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; + vm.close = close; + vm.message = message; + vm.button = button; + vm.href = href; - //KITS TAB - vm.devices = []; - vm.deviceStatus = undefined; - vm.removeDevice = removeDevice; - vm.downloadData = downloadData; + // hideAlert will be triggered on state change + $scope.$on('hideAlert', function() { + close(); + }); - vm.filteredDevices = []; - vm.dropdownSelected = undefined; + /////////////////// - //SIDEBAR - vm.filterDevices = filterDevices; - vm.filterTools = filterTools; + function close() { + $mdToast.hide(); + } + } +})(); - vm.selectThisTab = selectThisTab; +(function() { + 'use strict'; - $scope.$on('loggedOut', function() { - $location.path('/'); - }); + angular.module('app.components') + .factory('userUtils', userUtils); - $scope.$on('devicesContextUpdated', function(){ - var userData = auth.getCurrentUser().data; - if(userData){ - vm.user = userData; - } - initialize(); - }); + function userUtils() { + var service = { + isAdmin: isAdmin, + isAuthUser: isAuthUser + }; + return service; - initialize(); + /////////// - ////////////////// + function isAdmin(userData) { + return userData.role === 'admin'; + } + function isAuthUser(userID, authUserData) { + return userID === authUserData.id; + } + } +})(); - function initialize() { +(function() { + 'use strict'; - startingTab(); - if(!vm.user.devices.length) { - vm.devices = []; - animation.viewLoaded(); - } else { + angular.module('app.components') + .factory('timeUtils', timeUtils); - vm.devices = vm.user.devices.map(function(data) { - return new PreviewDevice(data); - }) + 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; - $timeout(function() { - mapWithBelongstoUser(vm.devices); - filterDevices(vm.status); - setSidebarMinHeight(); - animation.viewLoaded(); - }); + //////////// - } + function getDateIn(timeMS, format) { + if(!format) { + return timeMS; } - function filterDevices(status) { - if(status === 'all') { - status = undefined; - } - vm.deviceStatus = status; - vm.filteredDevices = $filter('filterLabel')(vm.devices, vm.deviceStatus); + 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 filterTools(type) { - if(type === 'all') { - type = undefined; - } - vm.toolType = type; - } + function convertTime(time) { + return moment(time).toISOString(); + } - 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; - } + function formatDate(time) { + return moment(time).format('YYYY-MM-DDTHH:mm:ss'); + } - 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 getSecondsFromDate(date) { + return (new Date(date)).getTime(); + } - 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); + function getMillisFromDate(date) { + return (new Date(date)).getTime(); + } - $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.'); - }); - }); - } + function getCurrentRange(fromDate, toDate) { + return moment(toDate).diff(moment(fromDate), 'days'); + } - function selectThisTab(iconIndex, uistate){ - /* This looks more like a hack but we need to workout how to properly use md-tab with ui-router */ + function getToday() { + return (new Date()).getTime(); + } - highlightIcon(iconIndex); + function getSevenDaysAgo() { + return getSecondsFromDate( getToday() - (7 * 24 * 60 * 60 * 1000) ); + } - 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 getHourBefore(date) { + var now = moment(date); + return now.subtract(1, 'hour').valueOf(); + } - } + function isSameDay(day1, day2) { + day1 = moment(day1); + day2 = moment(day2); - function startingTab() { - /* This looks more like a hack but we need to workout how to properly use md-tab with ui-router */ + if(day1.startOf('day').isSame(day2.startOf('day'))) { + return true; + } + return false; + } - var childState = $state.current.name.split('.').pop(); + function isDiffMoreThan15min(dateToCheckFrom, dateToCheckTo) { + var duration = moment.duration(moment(dateToCheckTo).diff(moment(dateToCheckFrom))); + return duration.as('minutes') > 15; + } - switch(childState) { - case 'user': - vm.startingTab = 1; - break; - default: - vm.startingTab = 0; - break; - } + function isWithin15min(dateToCheck) { + var fifteenMinAgo = moment().subtract(15, 'minutes').valueOf(); + dateToCheck = moment(dateToCheck).valueOf(); - } + return dateToCheck > fifteenMinAgo; + } - function highlightIcon(iconIndex) { + function isWithin1Month(dateToCheck) { + var oneMonthAgo = moment().subtract(1, 'months').valueOf(); + dateToCheck = moment(dateToCheck).valueOf(); - var icons = angular.element('.myProfile_tab_icon'); + return dateToCheck > oneMonthAgo; + } - _.each(icons, function(icon) { - unhighlightIcon(icon); - }); + function isWithin(number, type, dateToCheck) { + var ago = moment().subtract(number, type).valueOf(); + dateToCheck = moment(dateToCheck).valueOf(); - var icon = icons[iconIndex]; + return dateToCheck > ago; + } - angular.element(icon).find('.stroke_container').css({'stroke': 'white', 'stroke-width': '0.01px'}); - angular.element(icon).find('.fill_container').css('fill', 'white'); + 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 unhighlightIcon(icon) { - icon = angular.element(icon); +(function() { + 'use strict'; - icon.find('.stroke_container').css({'stroke': 'none'}); - icon.find('.fill_container').css('fill', '#FF8600'); - } + angular.module('app.components') + .factory('sensorUtils', sensorUtils); - function setSidebarMinHeight() { - var height = document.body.clientHeight / 4 * 3; - angular.element('.profile_content').css('min-height', height + 'px'); - } + 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 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 getRollup(dateFrom, dateTo) { - function uploadAvatar(fileData) { - if(fileData && fileData.length) { + // 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; - // 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; - }) + 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'; } + */ + return rollup; } - function copyUserToForm(formData, userData) { - var props = {username: true, email: true, city: true, country: true, country_code: true, url: true, constructor: false}; + function getSensorName(name) { - for(var key in userData) { - if(props[key]) { - formData[key] = userData[key]; + 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; } } + return sensorName.toUpperCase(); } - function mapWithBelongstoUser(devices){ - _.map(devices, addBelongProperty); - } - - function addBelongProperty(device){ - device.belongProperty = deviceBelongsToUser(device); - return device; - } - + function getSensorValue(sensor) { + var value = sensor.value; - function deviceBelongsToUser(device){ - if(!auth.isAuth() || !device || !device.id) { - return false; + if(isNaN(parseInt(value))) { + value = 'NA'; + } else { + value = round(value, 1).toString(); } - 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); - return isAdmin || belongsToUser; + return value; } - 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); - - $mdDialog.show(errorAlert); - }); + function round(value, precision) { + var multiplier = Math.pow(10, precision || 0); + return Math.round(value * multiplier) / multiplier; } - 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.'); - }); - }); + function getSensorPrevValue(sensor) { + /*jshint camelcase: false */ + var prevValue = sensor.prev_value; + return (prevValue && prevValue.toString() ) || 0; } - $scope.addDeviceSelector = addDeviceSelector; - function addDeviceSelector(){ - $mdDialog.show({ - templateUrl: 'app/components/myProfile/addDeviceSelectorModal.html', - clickOutsideToClose: true, - multiple: true, - controller: DialogController, - }); - } + function getSensorIcon(sensorName) { - function DialogController($scope, $mdDialog){ - $scope.cancel = function(){ - $mdDialog.cancel(); - }; - } + var thisName = getSensorName(sensorName); - 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); + switch(thisName) { + case 'TEMPERATURE': + return './assets/images/temperature_icon_new.svg'; - $mdDialog - .show(confirm) - .then(function(){ - $state.go('layout.kitAdd'); - }); - } + case 'HUMIDITY': + return './assets/images/humidity_icon_new.svg'; + case 'LIGHT': + return './assets/images/light_icon_new.svg'; - } -})(); + case 'SOUND': + return './assets/images/sound_icon_new.svg'; -(function() { - 'use strict'; + case 'CO': + return './assets/images/co_icon_new.svg'; - angular.module('app.components') - .controller('MapTagModalController', MapTagModalController); + case 'NO2': + return './assets/images/no2_icon_new.svg'; - MapTagModalController.$inject = ['$mdDialog', 'tag', 'selectedTags']; + case 'NETWORKS': + return './assets/images/networks_icon.svg'; - function MapTagModalController($mdDialog, tag, selectedTags) { + case 'BATTERY': + return './assets/images/battery_icon.svg'; - var vm = this; + case 'SOLAR PANEL': + return './assets/images/solar_panel_icon.svg'; - vm.checks = {}; + case 'BAROMETRIC PRESSURE': + return './assets/images/pressure_icon_new.svg'; - vm.answer = answer; - vm.hide = hide; - vm.clear = clear; - vm.cancel = cancel; - vm.tags = []; + case 'PM 1': + case 'PM 2.5': + case 'PM 10': + return './assets/images/particle_icon_new.svg'; - init(); + default: + return './assets/images/unknownsensor_icon.svg'; + } + } - //////////////////////////////////////////////////////// + function getSensorArrow(currentValue, prevValue) { + currentValue = parseInt(currentValue) || 0; + prevValue = parseInt(prevValue) || 0; - function init() { - tag.getTags() - .then(function(tags) { - vm.tags = tags; + if(currentValue > prevValue) { + return 'arrow_up'; + } else if(currentValue < prevValue) { + return 'arrow_down'; + } else { + return 'equal'; + } + } - _.forEach(selectedTags, select); + function getSensorColor(sensorName) { + switch(getSensorName(sensorName)) { + case 'TEMPERATURE': + return '#FF3D4C'; - }); - } + case 'HUMIDITY': + return '#55C4F5'; - function answer() { + case 'LIGHT': + return '#ffc107'; - var selectedTags = _(vm.tags) - .filter(isTagSelected) - .value(); - $mdDialog.hide(selectedTags); - } + case 'SOUND': + return '#0019FF'; - function hide() { - answer(); - } + case 'CO': + return '#00A103'; - function clear() { - $mdDialog.hide(null); - } + case 'NO2': + return '#8cc252'; - function cancel() { - answer(); - } + case 'NETWORKS': + return '#681EBD'; - function isTagSelected(tag) { - return vm.checks[tag.name]; - } + case 'SOLAR PANEL': + return '#d555ce'; - function select(tag){ - vm.checks[tag] = true; + case 'BATTERY': + return '#ff8601'; + + default: + return '#0019FF'; + } + } + + function getSensorDescription(sensorID, sensorTypes) { + return _(sensorTypes) + .chain() + .find(function(sensorType) { + return sensorType.id === sensorID; + }) + .value() + .measurement.description; + } } - } })(); (function() { 'use strict'; angular.module('app.components') - .controller('MapFilterModalController', MapFilterModalController); - - MapFilterModalController.$inject = ['$mdDialog','selectedFilters', '$timeout']; - - function MapFilterModalController($mdDialog, selectedFilters, $timeout) { + .factory('searchUtils', searchUtils); - var vm = this; - vm.checks = {}; + searchUtils.$inject = []; + function searchUtils() { + var service = { + parseLocation: parseLocation, + parseName: parseName, + parseIcon: parseIcon, + parseIconType: parseIconType + }; + return service; - 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']; + function parseLocation(object) { + var location = ''; - vm.filters = []; + if(!!object.city) { + location += object.city; + } + if(!!object.city && !!object.country) { + location += ', '; + } + if(!!object.country) { + location += object.country; + } - init(); + return location; + } - //////////////////////////////////////////////////////// + function parseName(object) { + var name = object.type === 'User' ? object.username : object.name; + return name; + } - function init() { - _.forEach(selectedFilters, 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 answer() { - vm.filters = vm.filters.concat(vm.location, vm.status, vm.new); - var selectedFilters = _(vm.filters) - .filter(isFilterSelected) - .value(); - $mdDialog.hide(selectedFilters); + function parseIconType(type) { + switch(type) { + case 'Device': + return 'div'; + default: + return 'img'; + } + } } +})(); - function hide() { - answer(); - } +(function() { + 'use strict'; - function clear() { - vm.filters = vm.filters.concat(vm.location, vm.status, vm.new); - $mdDialog.hide(vm.filters); - } + angular.module('app.components') + .factory('markerUtils', markerUtils); - function cancel() { - answer(); - } + markerUtils.$inject = ['deviceUtils', 'MARKER_ICONS']; + function markerUtils(deviceUtils, MARKER_ICONS) { + var service = { + getIcon: getIcon, + getMarkerIcon: getMarkerIcon, + }; + _.defaults(service, deviceUtils); + return service; - function isFilterSelected(filter) { - return vm.checks[filter]; - } + /////////////// - function toggle(filters) { - $timeout(function() { + function getIcon(object) { + var icon; + var labels = deviceUtils.parseSystemTags(object); + var isSCKHardware = deviceUtils.isSCKHardware(object); - 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; - } - } + if(hasLabel(labels, 'offline')) { + icon = MARKER_ICONS.markerSmartCitizenOffline; + } else if (isSCKHardware) { + icon = MARKER_ICONS.markerSmartCitizenOnline; + } else { + icon = MARKER_ICONS.markerExperimentalNormal; } + return icon; + } - }); - } + function hasLabel(labels, targetLabel) { + return _.some(labels, function(label) { + return label === targetLabel; + }); + } + + function getMarkerIcon(marker, state) { + var markerType = marker.icon.className; - function select(filter){ - vm.checks[filter] = true; + 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('MapController', MapController); - - 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; + .factory('mapUtils', mapUtils); - vm.markers = []; + mapUtils.$inject = []; + function mapUtils() { + var service = { + getDefaultFilters: getDefaultFilters, + setDefaultFilters: setDefaultFilters, + canFilterBeRemoved: canFilterBeRemoved + }; + return service; - var retinaSuffix = isRetina() ? '512' : '256'; - var retinaLegacySuffix = isRetina() ? '@2x' : ''; + ////////////// - var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; + 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.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 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'; } - }; - - 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 obj; + } - vm.events = { - map: { - enable: ['dragend', 'zoomend', 'moveend', 'popupopen', 'popupclose', - 'mousedown', 'dblclick', 'click', 'touchstart', 'mouseup'], - logic: 'broadcast' + 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; } - }; + } + } +})(); - $scope.$on('leafletDirectiveMarker.click', function(event, data) { - var id = undefined; - var currentMarker = vm.markers[data.modelName]; +(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); + }; + }]); - if(currentMarker) { - id = currentMarker.myData.id; - } + }); +})(); - 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') + .factory('deviceUtils', deviceUtils); - updateType = 'map'; + 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, + isLegacyVersion: isLegacyVersion, + isSCKHardware: isSCKHardware, + parseState: parseState, + parseAvatar: parseAvatar, + belongsToUser: belongsToUser, + parseSensorTime: parseSensorTime + }; - if ($state.$current.name === 'embbed') { return; } - $state.go('layout.home.kit', {id: id}); + return service; - // angular.element('section.map').scope().$broadcast('resizeMapHeight'); - }); + /////////////// + function parseLocation(object) { + var location = ''; + var city = ''; + var country = ''; - $scope.$on('leafletDirectiveMarker.popupclose', function() { - if(focusedMarkerID) { - var marker = vm.markers[focusedMarkerID]; - if(marker) { - vm.markers[focusedMarkerID].focus = false; + if (object.location) { + city = object.location.city; + country = object.location.country; + if(!!city) { + location += city; + } + if(!!city && !!location) { + location += ', ' + } + if(!!country) { + location += country; } } - }); - - vm.readyForDevice = { - device: false, - map: false - }; - - $scope.$on('deviceLoaded', function(event, data) { - vm.readyForDevice.device = data; - }); + return location; + } - $scope.$watch('vm.readyForDevice', function() { - if (vm.readyForDevice.device && vm.readyForDevice.map) { - zoomDeviceAndPopUp(vm.readyForDevice.device); + function parseCoordinates(object) { + if (object.location) { + return { + lat: object.location.latitude, + lng: object.location.longitude + }; } - }, true); + // TODO: Bug - what happens if no location? + } - $scope.$on('goToLocation', function(event, data) { - goToLocation(data); - }); + function parseSystemTags(object) { + /*jshint camelcase: false */ + return object.system_tags; + } - vm.filters = ['indoor', 'outdoor', 'online', 'offline']; + function parseUserTags(object) { + return object.user_tags; + } - vm.openFilterPopup = openFilterPopup; - vm.openTagPopup = openTagPopup; - vm.removeFilter = removeFilter; - vm.removeTag = removeTag; - vm.selectedTags = tag.getSelectedTags(); - vm.selectedFilters = ['indoor', 'outdoor', 'online', 'offline', 'new']; + function parseNotifications(object){ + return { + lowBattery: object.notify.low_battery, + stopPublishing: object.notify.stopped_publishing + } + } - vm.checkAllFiltersSelected = checkAllFiltersSelected; + function classify(kitType) { + if(!kitType) { + return ''; + } + return kitType.toLowerCase().split(' ').join('_'); + } - initialize(); + 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; + } - ///////////////////// + function parseHardware(object) { + if (!object.hardware) { + return; + } - function 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) + } + } - vm.readyForDevice.map = false; + function parseString(str) { + if (typeof(str) !== 'string') { return null; } + return str; + } - $q.all([device.getAllDevices($stateParams.reloadMap)]) - .then(function(data){ + 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 + }; + } - data = data[0]; + function parseHardwareInfo (object) { + if (!object) { return null; } // null + if (typeof(object) == 'string') { return null; } // FILTERED - 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(); + 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); - var markersByIndex = _.keyBy(vm.markers, function(marker) { - return marker.myData.id; - }); + 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 + }; + } - 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 parseHardwareName(object) { + if (object.hasOwnProperty('hardware')) { + if (!object.hardware.name) { + return 'Unknown hardware' + } + return object.hardware.name; + } else { + return 'Unknown hardware' + } + } - }); + function isPrivate(object) { + return object.is_private; } - 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 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 updateMarkers() { - $timeout(function() { - $scope.$apply(function() { - var allMarkers = device.getWorldMarkers(); +(function() { + 'use strict'; - var updatedMarkers = allMarkers; + /** + * Tools links for user profile + * @constant + * @type {Array} + */ - updatedMarkers = tag.filterMarkersByTag(updatedMarkers); - updatedMarkers = filterMarkersByLabel(updatedMarkers); - vm.markers = updatedMarkers; + 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' + }]); +})(); - animation.mapStateLoaded(); +(function() { + 'use strict'; - vm.deviceLoading = false; + /** + * Marker icons + * @constant + * @type {Object} + */ - zoomOnMarkers(); - }); - }); + 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 getZoomLevel(data) { - // data.layer is an array of strings like ["establishment", "point_of_interest"] - var zoom = 18; +(function() { + 'use strict'; - 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; - } - } + /** + * 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'} + ]); +})(); - return zoom; - } +(function() { + 'use strict'; - 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); - } + /** + * Dropdown options for community button + * @constant + * @type {Array} + */ - 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); - }); - } + 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 removeTag(tagName){ - tag.setSelectedTags(_.filter(vm.selectedTags, function(el){ - return el !== tagName; - })); +(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' + }); +})(); - vm.selectedTags = tag.getSelectedTags(); +(function() { + 'use strict'; - if(vm.selectedTags.length === 0){ - reloadNoTags(); - } else { - reloadWithTags(); - } + angular.module('app.components') + .factory('user', user); - } + user.$inject = ['Restangular']; + function user(Restangular) { + var service = { + createUser: createUser, + getUser: getUser, + updateUser: updateUser + }; + return service; - 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 reloadWithTags(){ - $state.transitionTo('layout.home.tags', {tags: vm.selectedTags}, {reload: true}); + function createUser(signupData) { + return Restangular.all('users').post(signupData); } - function reloadNoTags(){ - $state.transitionTo('layout.home.kit'); + 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') - .controller('LoginModalController', LoginModalController); + .factory('tag', tag); - 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(); - }; + tag.$inject = ['Restangular']; + function tag(Restangular) { + var tags = []; + var selectedTags = []; - $scope.openSignup = function() { - animation.showSignup(); - $mdDialog.hide(); + var service = { + getTags: getTags, + getSelectedTags: getSelectedTags, + setSelectedTags: setSelectedTags, + tagWithName: tagWithName, + filterMarkersByTag: filterMarkersByTag }; - $scope.openPasswordRecovery = function() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'PasswordRecoveryModalController', - templateUrl: 'app/components/passwordRecovery/passwordRecoveryModal.html', - clickOutsideToClose: true - }); + return service; - $mdDialog.hide(); - }; - } -})(); + ///////////////// -(function() { - 'use strict'; + function getTags() { + return Restangular.all('tags') + .getList({'per_page': 200}) + .then(function(fetchedTags){ + tags = fetchedTags.plain(); + return tags; + }); + } - angular.module('app.components') - .directive('login', login); + function getSelectedTags(){ + return selectedTags; + } + + function setSelectedTags(tags){ + selectedTags = tags; + } - function login() { - return { - scope: { - show: '=' - }, - restrict: 'A', - controller: 'LoginController', - controllerAs: 'vm', - templateUrl: 'app/components/login/login.html' - }; + 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 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); + }); + }); + } } })(); @@ -6350,1084 +6103,1331 @@ angular.module('app.components') 'use strict'; angular.module('app.components') - .controller('LoginController', LoginController); + .factory('sensor', sensor); - LoginController.$inject = ['$scope', '$mdDialog']; - function LoginController($scope, $mdDialog) { + sensor.$inject = ['Restangular', 'timeUtils', 'sensorUtils']; + function sensor(Restangular, timeUtils, sensorUtils) { + var sensorTypes; + callAPI().then(function(data) { + setTypes(data); + }); - $scope.showLogin = showLogin; + var service = { + callAPI: callAPI, + setTypes: setTypes, + getTypes: getTypes, + getSensorsData: getSensorsData + }; + return service; - $scope.$on('showLogin', function() { - showLogin(); - }); + //////////////// - //////////////// + function callAPI() { + return Restangular.all('sensors').getList({'per_page': 1000}); + } - function showLogin() { - $mdDialog.show({ - hasBackdrop: true, - fullscreen: true, - controller: 'LoginModalController', - controllerAs: 'vm', - templateUrl: 'app/components/login/loginModal.html', - clickOutsideToClose: true - }); - } + function setTypes(sensorTypes) { + sensorTypes = sensorTypes; + } - } + function getTypes() { + return sensorTypes; + } + + function getSensorsData(deviceID, sensorID, dateFrom, dateTo) { + var rollup = sensorUtils.getRollup(dateFrom, dateTo); + dateFrom = timeUtils.convertTime(dateFrom); + dateTo = timeUtils.convertTime(dateTo); + + return Restangular.one('devices', deviceID).customGET('readings', {'from': dateFrom, 'to': dateTo, 'rollup': rollup, 'sensor_id': sensorID, 'all_intervals': true}); + } + } })(); -(function() { +(function() { 'use strict'; angular.module('app.components') - .controller('LayoutController', LayoutController); - - 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; + .factory('search', search); + + search.$inject = ['$http', 'Restangular']; + function search($http, Restangular) { + var service = { + globalSearch: globalSearch + }; - vm.navRightLayout = 'space-around center'; + return service; - $scope.toggleRight = buildToggler('right'); + ///////////////////////// - function buildToggler(componentId) { - return function() { - $mdSidenav(componentId).toggle(); - }; + function globalSearch(query) { + return Restangular.all('search').getList({q: query}); } + } +})(); - // 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'; - }); - +(function() { + 'use strict'; - vm.isShown = true; - vm.isLoggedin = false; - vm.logout = logout; + // Deprecated module. Currently not in use within the app. - vm.dropdownOptions = DROPDOWN_OPTIONS_USER; - vm.dropdownSelected = undefined; + angular.module('app.components') + .factory('push', push); - vm.dropdownOptionsCommunity = DROPDOWN_OPTIONS_COMMUNITY; - vm.dropdownSelectedCommunity = undefined; + function push() { + var socket; - $scope.$on('removeNav', function() { - vm.isShown = false; - }); + init(); - $scope.$on('addNav', function() { - vm.isShown = true; - }); + var service = { + devices: devices, + device: device + }; - initialize(); + function init(){ + socket = io.connect('wss://ws.smartcitizen'); + } - ////////////////// + function devices(then){ + socket.on('data-received', then); + } - 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(); + function device(id, scope){ + devices(function(data){ + if(id === data.id) { + scope.$emit('published', data); } - }, 1000); + }); } - function logout() { - auth.logout(); - vm.isLoggedin = false; - } - } + return service; + } + })(); (function() { '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); + .factory('device', device); - DownloadModalController.$inject = ['thisDevice', 'device', '$mdDialog']; + device.$inject = ['Restangular', '$window', 'timeUtils','$http', 'auth', '$rootScope']; + function device(Restangular, $window, timeUtils, $http, auth, $rootScope) { + var worldMarkers; - function DownloadModalController(thisDevice, device, $mdDialog) { - var vm = this; + initialize(); - vm.device = thisDevice; - vm.download = download; - vm.cancel = cancel; + var service = { + getDevices: getDevices, + getAllDevices: getAllDevices, + getDevice: getDevice, + createDevice: createDevice, + updateDevice: updateDevice, + getWorldMarkers: getWorldMarkers, + setWorldMarkers: setWorldMarkers, + mailReadings: mailReadings, + postReadings: postReadings, + removeDevice: removeDevice, + updateContext: updateContext + }; - //////////////////////////// + return service; - function download(){ - device.mailReadings(vm.device) - .then(function (){ - $mdDialog.hide(); - }).catch(function(err){ - $mdDialog.cancel(err); - }); - } + ////////////////////////// - function cancel(){ - $mdDialog.cancel(); - } - } + function initialize() { + if(areMarkersOld()) { + removeMarkers(); + } + } -})(); + function getDevices(location) { + var parameter = ''; + parameter += location.lat + ',' + location.lng; + return Restangular.all('devices').getList({near: parameter, 'per_page': '100'}); + } -(function(){ -'use strict'; + function getAllDevices(forceReload) { + if (forceReload || auth.isAuth()) { + return getAllDevicesNoCached(); + } else { + return getAllDevicesCached(); + } + } -angular.module('app.components') - .directive('cookiesLaw', cookiesLaw); + function getAllDevicesCached() { + return Restangular.all('devices/world_map') + .getList() + .then(function(fetchedDevices){ + return fetchedDevices.plain(); + }); + } + function getAllDevicesNoCached() { + return Restangular.all('devices/fresh_world_map') + .getList() + .then(function(fetchedDevices){ + return fetchedDevices.plain(); + }); + } -cookiesLaw.$inject = ['$cookies']; + function getDevice(id) { + return Restangular.one('devices', id).get(); + } -function cookiesLaw($cookies) { - return { - template: - '
' + - 'This site uses cookies to offer you a better experience. ' + - ' Accept or' + - ' Learn More. ' + - '
', - controller: function($scope) { + function createDevice(data) { + return Restangular.all('devices').post(data); + } - var init = function(){ - $scope.isCookieValid(); + function updateDevice(id, data) { + return Restangular.one('devices', id).patch(data); } - // Helpers to debug - // You can also use `document.cookie` in the browser dev console. - //console.log($cookies.getAll()); + function getWorldMarkers() { + return worldMarkers || ($window.localStorage.getItem('smartcitizen.markers') && JSON.parse($window.localStorage.getItem('smartcitizen.markers') ).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 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; } - $scope.acceptCookie = function() { - //console.log('Accepting cookie...'); - var today = new Date(); - var expireDate = new Date(today); - expireDate.setMonth(today.getMonth() + 6); + function getTimeStamp() { + return ($window.localStorage.getItem('smartcitizen.markers') && + JSON.parse($window.localStorage + .getItem('smartcitizen.markers') ).timestamp); + } - $cookies.put('consent', true, {'expires' : expireDate.toUTCString()} ); + function areMarkersOld() { + var markersDate = getTimeStamp(); + return !timeUtils.isWithin(1, 'minutes', markersDate); + } - // Trigger the check again, after we click - $scope.isCookieValid(); - }; + function removeMarkers() { + worldMarkers = null; + $window.localStorage.removeItem('smartcitizen.markers'); + } - init(); + 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; + .factory('auth', auth); - return { - link: link, - restrict: 'A', - scope: { - chartData: '=' - } - }; + auth.$inject = ['$location', '$window', '$state', 'Restangular', + '$rootScope', 'AuthUser', '$timeout', 'alert', '$cookies']; + function auth($location, $window, $state, Restangular, $rootScope, AuthUser, + $timeout, alert, $cookies) { - function link(scope, elem) { + var user = {}; - $timeout(function() { - createChart(elem[0]); - }, 0); + //wait until http interceptor is added to Restangular + $timeout(function() { + initialize(); + }, 100); - var lastData = {}; + 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; - // 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}); - }); + ////////////////////////// - scope.$watch('chartData', function(newData) { - if(!newData) { - return; - } + function initialize() { + //console.log('---- AUTH INIT -----'); + setCurrentUser('appLoad'); + } - 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; - }); + //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; + } - var sensorDataCompare = newData[1].data; - var dataCompare = sensorDataCompare.map(function(dataPoint) { - return { - date: dateFormat(dataPoint.time), - count: dataPoint && dataPoint.count, - value: dataPoint && dataPoint.value - }; - }); + 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()) ); - dataCompare.sort(function(a, b) { - return a.date - b.date; - }); + 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 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]) { + //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'); - 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 - }; - }); + // 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); + } + }); + } - data.sort(function(a, b) { - return a.date - b.date; - }); + // 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(); + }); + } - var color = newData[0].color; - var unit = newData[0].unit; + function getCurrentUser() { + user.token = getToken(); + user.data = $window.localStorage.getItem('smartcitizen.data') && new AuthUser(JSON.parse( $window.localStorage.getItem('smartcitizen.data') )); + return user; + } - lastData = { - data: data, - type: 'main', - color: color, - unit: unit - }; + // 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(); + } - updateChartData(data, {type: 'main', container: elem[0], color: color, unit: unit }); - } - animation.hideChartSpinner(); - } - }); + // 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(); } - // creates the container that is re-used across different sensor charts - function createChart(elem) { - d3.select(elem).selectAll('*').remove(); + function getToken(){ + return $cookies.get('smartcitizen.token'); + } - margin = {top: 20, right: 12, bottom: 20, left: 42}; - width = elem.clientWidth - margin.left - margin.right; - height = elem.clientHeight - margin.top - margin.bottom; + function login(loginData) { + return Restangular.all('sessions').post(loginData); + } - 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 logout() { + $cookies.remove('smartcitizen.token'); + } - 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 getCurrentUserFromAPI() { + return Restangular.all('').customGET('me'); + } - xAxis = d3.svg.axis() - .scale(xScale) - .orient('bottom') - .ticks(5); + function recoverPassword(data) { + return Restangular.all('password_resets').post(data); + } - yAxisLeft = d3.svg.axis() - .scale(yScale0) - .orient('left') - .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'; + } + } +})(); - yAxisRight = d3.svg.axis() - .scale(yScale1) - .orient('right') - .ticks(5); +(function() { + 'use strict'; - 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); }); + /** + * Unused directive. Double-check before removing. + * + */ + angular.module('app.components') + .directive('slide', slide) + .directive('slideMenu', slideMenu); - 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); }); + function slideMenu() { + return { + controller: controller, + link: link + }; - 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 link(scope, element) { + scope.element = element; + } - 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 controller($scope) { + $scope.slidePosition = 0; + $scope.slideSize = 20; - 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 + ')'); + 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); + + /** + * Used on kit dashboard to open full sensor description + */ - popupContainer = popup.append('rect') - .attr('width', popupWidth) - .attr('height', popupHeight) - .attr('transform', function() { - var result = 'translate(-42, 5)'; + showPopup.$inject = []; + function showPopup() { + return { + link: link + }; - return result; - }) - .style('stroke', 'grey') - .style('stroke-width', '0.5') - .style('fill', 'white'); + ///// - var text = popup.append('text') - .attr('class', ''); + 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(); + }); + } + } +})(); - 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() { + 'use strict'; - textMain.append('tspan') - .attr('class', 'popup_value'); + angular.module('app.components') + .directive('moveFilters', moveFilters); - textMain.append('tspan') - .attr('class', 'popup_unit') - .attr('dx', 5); + /** + * Moves map filters when scrolling + * + */ + moveFilters.$inject = ['$window', '$timeout']; + function moveFilters($window, $timeout) { + return { + link: link + }; - 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 link() { + var chartHeight; + $timeout(function() { + chartHeight = angular.element('.kit_chart').height(); + }, 1000); - 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); + /* + angular.element($window).on('scroll', function() { + var windowPosition = document.body.scrollTop; + if(chartHeight > windowPosition) { + elem.css('bottom', 12 + windowPosition + 'px'); + } + }); + */ + } + } +})(); +(function() { + 'use strict'; + angular.module('app.components') + .factory('layout', layout); - function mousemove() { - var bisectDate = d3.bisector(function(d) { return d.date; }).left; - 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; + function layout() { - 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)); + var kitHeight; - var textContainers = [ - textMain, - date - ]; + var service = { + setKit: setKit, + getKit: getKit + }; + return service; - var popupWidth = resizePopup(popupContainer, textContainers); + function setKit(height) { + kitHeight = height; + } - 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 getKit() { + return kitHeight; } + } +})(); - // 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() { + 'use strict'; - svg.selectAll('*').remove(); + angular.module('app.components') + .directive('horizontalScroll', horizontalScroll); - //Add both area paths - svg.append('path') - .datum(data[0]) - .attr('class', 'chart_area') - .attr('fill', options.color[0]) - .attr('d', areaMain); + /** + * 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' + }; - svg.append('path') - .datum(data[1]) - .attr('class', 'chart_area') - .attr('fill', options.color[1]) - .attr('d', areaCompare); + /////////////////// - // 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])); + function link(scope, element) { - // Add the X Axis - svg.append('g') - .attr('class', 'axis x') - .attr('transform', 'translate(0,' + height + ')') - .call(xAxis); + 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(); - // Add both Y Axis - svg.append('g') - .attr('class', 'axis y_left') - .call(yAxisLeft); + // 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; + } - svg.append('g') - .attr('class', 'axis y_right') - .attr('transform', 'translate(' + width + ' ,0)') - .call(yAxisRight); + //set opacity back to normal otherwise + angular.element('.button_scroll_left').css('opacity', '1'); + angular.element('.button_scroll_right').css('opacity', '1'); + }); - // Draw the x Grid lines - svg.append('g') - .attr('class', 'grid') - .attr('transform', 'translate(0,' + height + ')') - .call(xGrid() - .tickSize(-height, 0, 0) - .tickFormat('') - ); + $timeout(function() { + element.trigger('scroll'); + }); - // Draw the y Grid lines - svg.append('g') - .attr('class', 'grid') - .call(yGrid() - .tickSize(-width, 0, 0) - .tickFormat('') - ); + angular.element($window).on('resize', function() { + $timeout(function() { + element.trigger('scroll'); + }, 1000); + }); + } + } +})(); - focusCompare = svg.append('g') - .attr('class', 'focus') - .style('display', 'none'); +(function() { + 'use strict'; + + angular.module('app.components') + .directive('hidePopup', hidePopup); + + /** + * Used on kit dashboard to hide popup with full sensor description + * + */ + + hidePopup.$inject = []; + function hidePopup() { + return { + link: link + }; - focusMain = svg.append('g') - .attr('class', 'focus') - .style('display', 'none'); + ///////////// - focusCompare.append('circle') - .style('stroke', options.color[1]) - .attr('r', 4.5); + function link(scope, elem) { + elem.on('mouseleave', function() { + angular.element('.sensor_description_preview').show(); + angular.element('.sensor_description_full').hide(); + }); + } + } +})(); - focusMain.append('circle') - .style('stroke', options.color[0]) - .attr('r', 4.5); +(function() { + 'use strict'; - var popupWidth = 84; - var popupHeight = 75; + angular.module('app.components') + .directive('disableScroll', disableScroll); - popup = svg.append('g') - .attr('class', 'focus') - .style('display', 'none'); + disableScroll.$inject = ['$timeout']; + function disableScroll($timeout) { + return { + // link: { + // pre: link + // }, + compile: link, + restrict: 'A', + priority: 100000 + }; - popupContainer = popup.append('rect') - .attr('width', popupWidth) - .attr('height', popupHeight) - .style('min-width', '40px') - .attr('transform', function() { - var result = 'translate(-42, 5)'; - 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]); + 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'); + }); + }); + } + } +})(); - 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() { + 'use strict'; - var text = popup.append('text') - .attr('class', ''); + angular.module('app.components') + .factory('animation', animation); - 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); + /** + * 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_value') - .attr( 'text-anchor', 'start' ); + animation.$inject = ['$rootScope']; + function animation($rootScope) { - textMain.append('tspan') - .attr('class', 'popup_unit') - .attr('dx', 5); + 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; - 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); + ////////////// - textCompare.append('tspan') - .attr('class', 'popup_value') - .attr( 'text-anchor', 'start' ); + 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'); + } + } +})(); - textCompare.append('tspan') - .attr('class', 'popup_unit') - .attr('dx', 5); +(function() { + 'use strict'; - 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' ); + /** + * TODO: Improvement These directives can be split up each one in a different file + */ - 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); + 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); - function mousemove() { - var bisectDate = d3.bisector(function(d) { return d.date; }).left; + /** + * It moves down kit section to ease the transition after the kit menu is sticked to the top + * + */ + moveDown.$inject = []; + function moveDown() { - 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) + ')'); + function link(scope, element) { + scope.$watch('moveDown', function(isTrue) { + if(isTrue) { + element.addClass('move_down'); + } else { + element.removeClass('move_down'); + } + }); + } + return { + link: link, + scope: false, + restrict: 'A' + }; + } - 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) + ')'); + /** + * 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 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)); + $timeout(function() { + elementPosition = element[0].offsetTop; + //var elementHeight = element[0].offsetHeight; + navbarHeight = angular.element('.stickNav').height(); + }, 1000); - var textContainers = [ - textMain, - textCompare, - date - ]; - var popupWidth = resizePopup(popupContainer, textContainers); + angular.element($window).on('scroll', function() { + var windowPosition = document.body.scrollTop; - if(xScale(d.date) + 80 + popupWidth > options.container.clientWidth) { - popup.attr('transform', 'translate(' + (xScale(d.date) - 120) + ', ' + (d3.mouse(this)[1] - 20) + ')'); + //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'; + } + }); } } })(); @@ -8062,7 +8062,7 @@ $templateCache.put('app/components/upload/errorModal.html','
CSV File UploadBack to Kit
'); $templateCache.put('app/components/userProfile/userProfile.html','

{{ vm.user.username || \'No data\' }}

{{ vm.user.city }} , {{ vm.user.country }} No data

FILTER KITS BY

ALLONLINEOFFLINE
{{ vm.filteredDevices.length || 0 }} kits filtering by {{ vm.status.toUpperCase() || \'ALL\' }}
There are not kits yet
'); $templateCache.put('app/components/kit/editKit/editKit.html','

Edit your kit

Finalise your setup

2
Set up
Backto ProfileSave

Basic information

Want to change your kit\'s name? Or perhaps say something nice about it in the description?
Don\'t forget about the exposure!
Name {{ error }}
{{ exposure.name }}

Legacy devices

Seems like you have a {{vm.device.hardware.name}}. Use this field to input your MAC address. You can find the MAC address using the onboard kit\'s shell. More information in the docs
MAC address {{ error }}

Kit location

You can adjust the location by dragging the marker on the map.
Get your location

Open data

Sometimes, your devices might be collecting sensitive personal data (i.e. your exact location or by GPS using in your bike).
Check the box in case you want to prevent others from accesssing your data.

Manage how others can access your data:

Notifications

Manage your notifications

Get emails when the following events occur:

Kit tags

Kits can be grouped by tags. Choose from the available tags or submit a tag request on the Forum.
{{item.name}}

Postprocessing info

Follow the instructions here to generate a valid JSON containing the postprocessing information for your device. This is an advanced feature and it\'s not required for standard Smart Citizen Kits!

Last updated: {{vm.deviceForm.postprocessing.updated_at}}
Latest postprocessing: {{vm.deviceForm.postprocessing.latest_postprocessing}}

Setup your kit

In order to have your kit connected to the Smart Citizen platform, we need a few step involving the connection of your kit to your computer. If this is your first time, maybe you will like to follow the Startup guide.
Smartcitizen Kit

MAC address

Use this field to input your MAC address. You can find the MAC address using the onboard kit\'s shell.
MAC address {{ error }}
Waiting for your kit\'s dataWe are waiting for your kit to connect on-line, this can take a few minutesCheck the process on the report window and contact support@smartcitizen.me if you have any problem.Ready! Go and visit your kit on-line
'); -$templateCache.put('app/components/kit/showKit/showKit.html','

Device not found, or it has been set to private. You can ask in the forum for more information.

{{ vm.device.name }}
{{ vm.battery.value }} {{ vm.battery.unit }}
NOT CONNECTED
Last data received:{{ vm.device.lastReadingAt.parsed }}
ChartKit DetailUser info

No kit selected \uD83D\uDC46

Browse the map and click on any kit to see its data.

Click to see more sensors
{{ sensor.value }}
{{ sensor.unit }}

{{ sensor.measurement.name }}

Click to see more sensors

We can also take you to your nearest online kit by letting us know your location.

Locate me

{{ vm.selectedSensorData.value }} {{ vm.selectedSensorData.unit }}
{{ sensor.measurement.name }}
This is the latest value received
{{ vm.sensorNames[vm.selectedSensor] }}
{{ vm.selectedSensorData.fullDescription }}More info
Compare with{{ sensor.measurement.name }}
{{ opt }}
Move chart to the leftMove chart to the right

{{ vm.device.name }}

Private

{{ vm.device.locationString || \'No location\' }}

{{ vm.device.hardwareName }}

{{ vm.device.state.name }}

{{ system_tag }}

Manage your kit

EDITDownload CSVSD CARD UPLOADDELETE

We empower communities to better understand their environment

Smart Citizen is a project by Fab Lab Barcelona to offer an alternative to the centralised data production and management systems used by the large corporations that constitute the driving force behind the smart city concept. The project empowers citizens and communities to gather information on their environment and make it available to the public, using open source hardware and software design.

{{ vm.device.owner.username }}

{{ vm.device.owner.city }} , {{ vm.device.owner.country }} No location

{{ vm.device.owner.url || \'No URL\'}}

Other kits owned by {{ vm.device.owner.username }}

VIEW ALL KITS BY {{ vm.device.owner.username }}
'); $templateCache.put('app/components/kit/newKit/newKit.html','
Add your kit
Backto ProfileNext

Basic information

Want to change your kit\'s name? Or perhaps say something nice about it in the description?
Don\'t forget about the exposure!
Name {{ error }}
{{ exposure.name }}
{{ version.name }}

Open data

Sometimes, your devices might be collecting sensitive personal data (i.e. your exact location or by GPS using in your bike).
Check the box in case you want to prevent others from accesssing your data.

Manage how others can access your data:

Kit location

Please, let us locate you, later you can adjust the location by dragging the marker on the map.
Get your location

Kit tags

Kits can be grouped by tags. Choose from the available tags or submit a tag request on the Forum.
{{tag.name}}
'); +$templateCache.put('app/components/kit/showKit/showKit.html','

Device not found, or it has been set to private. You can ask in the forum for more information.

{{ vm.device.name }}
{{ vm.battery.value }} {{ vm.battery.unit }}
NOT CONNECTED
Last data received:{{ vm.device.lastReadingAt.parsed }}
ChartKit DetailUser info

No kit selected \uD83D\uDC46

Browse the map and click on any kit to see its data.

Click to see more sensors
{{ sensor.value }}
{{ sensor.unit }}

{{ sensor.measurement.name }}

Click to see more sensors

We can also take you to your nearest online kit by letting us know your location.

Locate me

{{ vm.selectedSensorData.value }} {{ vm.selectedSensorData.unit }}
{{ sensor.measurement.name }}
This is the latest value received
{{ vm.sensorNames[vm.selectedSensor] }}
{{ vm.selectedSensorData.fullDescription }}More info
Compare with{{ sensor.measurement.name }}
{{ opt }}
Move chart to the leftMove chart to the right

{{ vm.device.name }}

Private

{{ vm.device.locationString || \'No location\' }}

{{ vm.device.hardwareName }}

{{ vm.device.state.name }}

{{ system_tag }}

Manage your kit

EDITDownload CSVSD CARD UPLOADDELETE

We empower communities to better understand their environment

Smart Citizen is a project by Fab Lab Barcelona to offer an alternative to the centralised data production and management systems used by the large corporations that constitute the driving force behind the smart city concept. The project empowers citizens and communities to gather information on their environment and make it available to the public, using open source hardware and software design.

{{ vm.device.owner.username }}

{{ vm.device.owner.city }} , {{ vm.device.owner.country }} No location

{{ vm.device.owner.url || \'No URL\'}}

Other kits owned by {{ vm.device.owner.username }}

VIEW ALL KITS BY {{ vm.device.owner.username }}
'); $templateCache.put('app/core/animation/backdrop/loadingBackdrop.html',''); $templateCache.put('app/core/animation/backdrop/noDataBackdrop.html','

This kit hasn\u2019t still said a word \uD83D\uDC76

Your kit has still not posted any data \uD83D\uDD27\uD83D\uDD29\uD83D\uDD28

');}]); \ No newline at end of file