diff --git a/404.html b/404.html index 507fbe37..a2c54337 100644 --- a/404.html +++ b/404.html @@ -125,7 +125,7 @@
Your browser is out of date!
- + diff --git a/index.html b/index.html index 507fbe37..a2c54337 100644 --- a/index.html +++ b/index.html @@ -125,7 +125,7 @@
Your browser is out of date!
- + diff --git a/scripts/app-97a966a2a9.js b/scripts/app-db4bbbfa3b.js similarity index 99% rename from scripts/app-97a966a2a9.js rename to scripts/app-db4bbbfa3b.js index 9df0a7ca..043197c7 100644 --- a/scripts/app-97a966a2a9.js +++ b/scripts/app-db4bbbfa3b.js @@ -9,2430 +9,2305 @@ 'use strict'; angular.module('app.components') - .controller('KitController', KitController); + .factory('User', ['COUNTRY_CODES', function(COUNTRY_CODES) { - 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) { + /** + * 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 + */ - var vm = this; - var sensorsData = []; + 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 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 = {}; +})(); - var focused = true; +(function() { + 'use strict'; - // event listener on change of value of main sensor selector - $scope.$watch('vm.selectedSensor', function(newVal) { + angular.module('app.components') + .factory('NonAuthUser', ['User', function(User) { - // Prevents undisered calls if selected sensor is not yet defined - if (!newVal) { - return; + function NonAuthUser(userData) { + User.call(this, userData); } + NonAuthUser.prototype = Object.create(User.prototype); + NonAuthUser.prototype.constructor = User; - vm.selectedSensorToCompare = undefined; - vm.selectedSensorToCompareData = {}; - vm.chartDataCompare = []; - compareSensorID = undefined; + return NonAuthUser; + }]); +})(); - setSensorSideChart(); +(function() { + 'use strict'; - vm.sensorsToCompare = getSensorsToCompare(); + angular.module('app.components') + .factory('AuthUser', ['User', function(User) { - $timeout(function() { - // TODO: Improvement, change how we set the colors - colorSensorCompareName(); - setSensor({type: 'main', value: newVal}); + /** + * 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 + */ - if (picker){ - changeChart([mainSensorID]); - } - }, 100); + function AuthUser(userData) { + User.call(this, userData); - }); + 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; - // 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); - } - }); + return AuthUser; + }]); +})(); - $timeout(function() { - colorSensorCompareName(); - setSensor({type: 'compare', value: newVal}); +(function() { + 'use strict'; - if(oldVal === undefined && newVal === undefined) { - return; + 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) { + + 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; } - changeChart([compareSensorID]); - }, 100); - }); + // Get sensor tags + this.tags = sensorData.tags; + } - $scope.$on('hideChartSpinner', function() { - vm.loadingChart = false; - }); + return Sensor; + }]); +})(); +(function() { + 'use strict'; - $scope.$on('$destroy', function() { - focused = false; - $timeout.cancel(vm.updateTimeout); - }); + angular.module('app.components') + .factory('SearchResultLocation', ['SearchResult', function(SearchResult) { - $scope.$on('$viewContentLoaded', function(event){ - initialize(); - }); + /** + * 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 initialize() { - animation.viewLoaded(); - updatePeriodically(); - } + this.lat = object.latitude; + this.lng = object.longitude; + this.layer = object.layer; + } + return SearchResultLocation; + }]); - function pollAndUpdate(){ - vm.updateTimeout = $timeout(function() { - updatePeriodically(); - }, vm.updateInterval); - } +})(); - function updatePeriodically(){ - getAndUpdateDevice().then(function(){ - pollAndUpdate(); - }); - } +(function() { + 'use strict'; - 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; + angular.module('app.components') + .factory('SearchResult', ['searchUtils', function(searchUtils) { - 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(); - } - } + /** + * 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; + }]); +})(); - vm.device = newDevice; - setOwnerSampleDevices(); +(function() { + 'use strict'; - if (vm.device.state.name === 'has published') { - /* Device has data */ - setDeviceOnMap(); - setChartTimeRange(); - deviceAnnouncements(); + 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 = ''; - /*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); + this.icon = markerUtils.getIcon(deviceData); + this.layer = 'devices'; + this.focus = false; + this.myData = { + id: id, + labels: deviceUtils.parseSystemTags(deviceData), + tags: deviceUtils.parseUserTags(deviceData) + }; } - } + return Marker; - function 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(); + function createTagsTemplate(tagsArr, tagType, clickable) { + if(typeof(clickable) === 'undefined'){ + clickable = false; } - else if (error.status === 403){ - deviceIsPrivate(); + var clickablTag = ''; + if(clickable){ + clickablTag = 'clickable'; } - } - } - 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!'); - } - } + if(!tagType){ + tagType = 'tag'; + } - 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(); + return _.reduce(tagsArr, function(acc, label) { + var element =''; + if(tagType === 'tag'){ + element = ''; + }else{ + element = ''+label+''; + } + return acc.concat(element); + }, ''); } - } - 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 () { + 'use strict'; - function setChartTimeRange() { - if(vm.allowUpdateChart) { - /* Init the chart range to default if doesn't exist of the user hasn't interacted */ - picker = initializePicker(); - } - } + angular.module('app.components') + .factory('PreviewDevice', ['Device', function (Device) { - function setDeviceOnMap() { - animation.deviceLoaded({lat: vm.device.latitude, lng: vm.device.longitude, - id: vm.device.id}); - } + /** + * 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 setSensors(sensorsRes){ + 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; + }]); +})(); - var mainSensors = sensorsRes[0]; - var compareSensors = sensorsRes[1]; +(function() { + 'use strict'; - vm.battery = _.find(mainSensors, {name: 'battery'}); - vm.sensors = mainSensors.reverse(); - vm.sensors.forEach(checkRaw); - vm.sensors.forEach(getHardwareName); + angular.module('app.components') + .factory('HasSensorDevice', ['Device', function(Device) { - setSensorSideChart(); + function HasSensorDevice(object) { + Device.call(this, object); - if (!vm.selectedSensor) { - vm.chartSensors = vm.sensors; - vm.sensorsToCompare = compareSensors; - vm.selectedSensor = (vm.sensors && vm.sensors[0]) ? vm.sensors[0].id : undefined; + this.sensors = object.data.sensors; + this.longitude = object.data.location.longitude; + this.latitude = object.data.location.latitude; } - animation.mapStateLoaded(); - } + HasSensorDevice.prototype = Object.create(Device.prototype); + HasSensorDevice.prototype.constructor = Device; - function checkRaw(value){ - vm.hasRaw |= (value.tags.indexOf('raw') !== -1); - } + HasSensorDevice.prototype.sensorsHasData = function() { + var parsedSensors = this.sensors.map(function(sensor) { + return sensor.value; + }); - 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 _.some(parsedSensors, function(sensorValue) { + return !!sensorValue; }); - } - } + }; - 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); + return HasSensorDevice; + }]); +})(); - $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.'); - }); - }); - } +(function() { + 'use strict'; - function showSensorOnChart(sensorID) { - vm.selectedSensor = sensorID; - } + angular.module('app.components') + .factory('FullDevice', ['Device', 'Sensor', 'deviceUtils', function(Device, Sensor, deviceUtils) { - function slide(direction) { - var slideContainer = angular.element('.sensors_container'); - var scrollPosition = slideContainer.scrollLeft(); - var width = slideContainer.width(); - var slideStep = width/2; + /** + * 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); - if(direction === 'left') { - slideContainer.animate({'scrollLeft': scrollPosition + slideStep}, - {duration: 250, queue:false}); - } else if(direction === 'right') { - slideContainer.animate({'scrollLeft': scrollPosition - slideStep}, - {duration: 250, queue:false}); + this.owner = deviceUtils.parseOwner(object); + this.postProcessing = object.postprocessing; + this.data = object.data; + this.sensors = object.data.sensors; } - } - function getSensorsToCompare() { - return vm.sensors ? vm.sensors.filter(function(sensor) { - return sensor.id !== vm.selectedSensor; - }) : []; - } + FullDevice.prototype = Object.create(Device.prototype); + FullDevice.prototype.constructor = FullDevice; - function changeChart(sensorsID, options) { - if(!sensorsID[0]) { - return; - } + 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; + }; - if(!options) { - options = {}; - } - options.from = options && options.from || picker.getValuePickerFrom(); - options.to = options && options.to || picker.getValuePickerTo(); + return FullDevice; + }]); +})(); - //show spinner - vm.loadingChart = true; - //grab chart data and save it +(function() { + 'use strict'; - // 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; - }); - } + angular.module('app.components') + .factory('Device', ['deviceUtils', 'timeUtils', function(deviceUtils, timeUtils) { - 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]); + /** + * 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; - compareSensor = { - data: parsedDataCompare, - color: vm.selectedSensorToCompareData.color, - unit: vm.selectedSensorToCompareData.unit - }; - } - var newChartData = [mainSensor, compareSensor]; - return newChartData; - } + // Tags and dates + this.systemTags = deviceUtils.parseSystemTags(object); + this.userTags = deviceUtils.parseUserTags(object); + this.isPrivate = deviceUtils.isPrivate(object); + this.preciseLocation = deviceUtils.preciseLocation(object); + this.enableForwarding = deviceUtils.enableForwarding(object); + this.notifications = deviceUtils.parseNotifications(object); + this.lastReadingAt = timeUtils.parseDate(object.last_reading_at); + this.createdAt = timeUtils.parseDate(object.created_at); + this.updatedAt = timeUtils.parseDate(object.updated_at); - function 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 - }; - }); - } + // Location + this.location = object.location; + this.locationString = deviceUtils.parseLocation(object); - function setSensor(options) { - var sensorID = options.value; - if(sensorID === undefined) { - return; - } - if(options.type === 'main') { - mainSensorID = sensorID; - } else if(options.type === 'compare') { - compareSensorID = sensorID; + // 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? + + this.avatar = deviceUtils.parseAvatar(); + /*jshint camelcase: false */ } - } - 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'); - } + return Device; + }]); +})(); - function getCurrentRange() { - var to = moment(picker.getValuePickerTo()); - var from = moment(picker.getValuePickerFrom()); - return to.diff(from)/1000; - } +(function() { + 'use strict'; - function moveChart(direction) { + angular.module('app.components') + .directive('noDataBackdrop', noDataBackdrop); - var valueTo, valueFrom; - //grab current date range - var currentRange = getCurrentRange(); + /** + * Backdrop for chart section when kit has no data + * + */ + noDataBackdrop.$inject = []; - /*jshint camelcase: false*/ - var from_picker = angular.element('#picker_from').pickadate('picker'); - var to_picker = angular.element('#picker_to').pickadate('picker'); + function noDataBackdrop() { + return { + restrict: 'A', + scope: {}, + templateUrl: 'app/core/animation/backdrop/noDataBackdrop.html', + controller: function($scope, $timeout) { + var vm = this; - if(direction === 'left') { - //set both from and to pickers to prev range - valueTo = moment(picker.getValuePickerFrom()); - valueFrom = moment(picker.getValuePickerFrom()).subtract(currentRange, 'seconds'); + vm.deviceWithoutData = false; + vm.scrollToComments = scrollToComments; - picker.setValuePickers([valueFrom.toDate(), valueTo.toDate()]); + $scope.$on('deviceWithoutData', function(ev, data) { - } else if(direction === 'right') { - var today = timeUtils.getToday(); - var currentValueTo = picker.getValuePickerTo(); - if( timeUtils.isSameDay(today, timeUtils.getMillisFromDate(currentValueTo)) ) { - return; + $timeout(function() { + vm.device = data.device; + vm.deviceWithoutData = true; + + if (data.belongsToUser) { + vm.user = 'owner'; + } else { + vm.user = 'visitor'; + } + }, 0); + + }); + + function scrollToComments(){ + location.hash = ''; + location.hash = '#disqus_thread'; } + }, + controllerAs: 'vm' + }; + } +})(); - valueFrom = moment(picker.getValuePickerTo()); - valueTo = moment(picker.getValuePickerTo()).add(currentRange, 'seconds'); +(function() { + 'use strict'; - picker.setValuePickers([valueFrom.toDate(), valueTo.toDate()]); + angular.module('app.components') + .directive('loadingBackdrop', loadingBackdrop); - } - resetTimeOpts(); - } + /** + * 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; - //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'); + // listen for app loading event + $scope.$on('viewLoading', function() { + vm.isViewLoading = true; + }); - var to_$input = angular.element('#picker_to').pickadate({ - onOpen: function(){ - vm.resetTimeOpts(); - }, - onClose: function(){ - angular.element(document.activeElement).blur(); + $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; + }); }, - container: 'body', - klass: { - holder: 'picker__holder picker_container' - } - }); + controllerAs: 'vm' + }; + } +})(); - var to_picker = to_$input.pickadate('picker'); +(function() { + 'use strict'; - 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') ); - } + angular.module('app.components') + .controller('KitController', KitController); - from_picker.on('close', function(event) { - setFromRange(getCalculatedFrom(from_picker.get('value'))); - }); + 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) { - to_picker.on('close', function(event) { - setToRange(getCalculatedTo(to_picker.get('value'))); - }); + var vm = this; + var sensorsData = []; - 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 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 = {}; - to_picker.on('set', function(event) { - if(event.select) { - from_picker.set('max', getToRange()); - } else if( 'clear' in event) { - from_picker.set('max', false); - } - }); + var focused = true; - //set to-picker max to today - to_picker.set('max', getLatestUpdated()); + // event listener on change of value of main sensor selector + $scope.$watch('vm.selectedSensor', function(newVal) { - function getSevenDaysAgoFromLatestUpdate() { - var lastTime = moment(vm.device.lastReadingAt.raw); - return lastTime.subtract(7, 'days').valueOf(); + // Prevents undisered calls if selected sensor is not yet defined + if (!newVal) { + return; } - function getLatestUpdated() { - return moment(vm.device.lastReadingAt.raw).toDate(); - } + vm.selectedSensorToCompare = undefined; + vm.selectedSensorToCompareData = {}; + vm.chartDataCompare = []; + compareSensorID = undefined; - function getCalculatedFrom(pickerTimeFrom) { - var from, - pickerTime; + setSensorSideChart(); - pickerTime = moment(pickerTimeFrom, 'D MMMM, YYYY'); - from = pickerTime.startOf('day'); + vm.sensorsToCompare = getSensorsToCompare(); - return from; - } + $timeout(function() { + // TODO: Improvement, change how we set the colors + colorSensorCompareName(); + setSensor({type: 'main', value: newVal}); - function getCalculatedTo(pickerTimeTo) { - var to, - pickerTime; + if (picker){ + changeChart([mainSensorID]); + } + }, 100); - pickerTime = moment(pickerTimeTo, 'D MMMM, YYYY'); + }); - 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') - }); - } + // 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); + } + }); - return to; - } + $timeout(function() { + colorSensorCompareName(); + setSensor({type: 'compare', value: newVal}); - function updateChart() { - var sensors = [mainSensorID, compareSensorID]; - sensors = sensors.filter(function(sensor) { - return sensor; - }); - changeChart(sensors, { - from: range.from, - to: range.to - }); - } + if(oldVal === undefined && newVal === undefined) { + return; + } + changeChart([compareSensorID]); + }, 100); - function setFromRange(from) { - range.from = from; - from_picker.set('select', getFromRange()); - updateChart(); - } + }); - function setToRange(to) { - range.to = to; - to_picker.set('select', getToRange()); - updateChart(); - } + $scope.$on('hideChartSpinner', function() { + vm.loadingChart = false; + }); - function getFromRange() { - return moment(range.from).toDate(); - } + $scope.$on('$destroy', function() { + focused = false; + $timeout.cancel(vm.updateTimeout); + }); - function getToRange() { - return moment(range.to).toDate(); - } + $scope.$on('$viewContentLoaded', function(event){ + initialize(); + }); - function setRange(from, to) { - range.from = from; - range.to = to; - from_picker.set('select', getFromRange()); - to_picker.set('select', getToRange()); - updateChart(); - } + function initialize() { + animation.viewLoaded(); + updatePeriodically(); + } - 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 pollAndUpdate(){ + vm.updateTimeout = $timeout(function() { + updatePeriodically(); + }, vm.updateInterval); + } - // 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 updatePeriodically(){ + getAndUpdateDevice().then(function(){ + pollAndUpdate(); + }); } - 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 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; - geolocation.grantHTML5Geolocation(); + 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(); + } + } - var location = { - lat:position.coords.latitude, - lng:position.coords.longitude - }; - device.getDevices(location) - .then(function(data){ - data = data.plain(); + vm.device = newDevice; + setOwnerSampleDevices(); - _(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(); - }); - }); + if (vm.device.state.name === 'has published') { + /* Device has data */ + setDeviceOnMap(); + setChartTimeRange(); + deviceAnnouncements(); + + /*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); } } - 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; + function killSensorsLoading(error){ + if(error) { + if(error.status === 404) { + $state.go('layout.404'); } - var errorAlert = $mdDialog.alert() - .title('ERROR') - .textContent('Uh-oh, something went wrong') - .ariaLabel('') - .ok('D\'oh') - .theme('primary') - .clickOutsideToClose(false); - - $mdDialog.show(errorAlert); - }); + 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 getMainSensors(deviceData) { - if(!deviceData) { - return undefined; + function deviceAnnouncements(){ + if(!timeUtils.isWithin(1, 'months', vm.device.lastReadingAt.raw)) { + //TODO: Cosmetic Update the message + alert.info.longTime(); } - return deviceData.getSensors({type: 'main'}); - } - function getCompareSensors(deviceData) { - if(!vm.device) { - 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!'); } - deviceData.getSensors({type: 'compare'}); } - function getOwnerDevices(deviceData, sampling) { - if(!deviceData) { - return undefined; + + 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(); } - 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 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 deviceIsPrivate() { + alert.info.noData.private(); } - function timeOptSelected(){ - vm.allowUpdateChart = false; - if (vm.dropDownSelection){ - setFromLast(vm.dropDownSelection); - } - } - function resetTimeOpts(){ - vm.allowUpdateChart = false; - vm.dropDownSelection = undefined; + 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 showStore() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'StoreModalController', - templateUrl: 'app/components/store/storeModal.html', - clickOutsideToClose: true - }); + function setChartTimeRange() { + if(vm.allowUpdateChart) { + /* Init the chart range to default if doesn't exist of the user hasn't interacted */ + picker = initializePicker(); + } } - } -})(); - -(function() { - 'use strict'; - angular.module('app.components') - .controller('NewKitController', NewKitController); + function setDeviceOnMap() { + animation.deviceLoaded({lat: vm.device.latitude, lng: vm.device.longitude, + id: vm.device.id}); + } - 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; + function setSensors(sensorsRes){ - vm.step = 1; - vm.submitStepOne = submitStepOne; - vm.backToProfile = backToProfile; + var mainSensors = sensorsRes[0]; + var compareSensors = sensorsRes[1]; - // FORM INFO - vm.deviceForm = { - name: undefined, - exposure: undefined, - location: { - lat: undefined, - lng: undefined, - zoom: 16 - }, - is_private: false, - legacyVersion: '1.1', - tags: [] - }; + vm.battery = _.find(mainSensors, {name: 'battery'}); + vm.sensors = mainSensors.reverse(); + vm.sensors.forEach(checkRaw); + vm.sensors.forEach(getHardwareName); - // EXPOSURE SELECT - vm.exposure = [ - {name: 'indoor', value: 1}, - {name: 'outdoor', value: 2} - ]; + setSensorSideChart(); - // VERSION SELECT - vm.version = [ - {name: 'Smart Citizen Kit 1.0', value: '1.0'}, - {name: 'Smart Citizen Kit 1.1', value: '1.1'} - ]; + if (!vm.selectedSensor) { + vm.chartSensors = vm.sensors; + vm.sensorsToCompare = compareSensors; + vm.selectedSensor = (vm.sensors && vm.sensors[0]) ? vm.sensors[0].id : undefined; + } - $scope.$on('leafletDirectiveMarker.dragend', function(event, args){ - vm.deviceForm.location.lat = args.model.lat; - vm.deviceForm.location.lng = args.model.lng; - }); + animation.mapStateLoaded(); + } - // TAGS SELECT - vm.tags = []; - $scope.$watch('vm.tag', function(newVal, oldVal) { - if(!newVal) { - return; - } - // remove selected tag from select element - vm.tag = undefined; + function checkRaw(value){ + vm.hasRaw |= (value.tags.indexOf('raw') !== -1); + } - var alreadyPushed = _.some(vm.deviceForm.tags, function(tag) { - return tag.id === newVal; + 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); + } }); - if(alreadyPushed) { - return; - } + } + } - var tag = _.find(vm.tags, function(tag) { - return tag.id === newVal; - }); - vm.deviceForm.tags.push(tag); - }); - vm.removeTag = removeTag; + 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); - // MAP CONFIGURATION - var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; + $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.'); + }); + }); + } - 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 showSensorOnChart(sensorID) { + vm.selectedSensor = sensorID; + } - vm.macAddress = undefined; + function slide(direction) { + var slideContainer = angular.element('.sensors_container'); + var scrollPosition = slideContainer.scrollLeft(); + var width = slideContainer.width(); + var slideStep = width/2; - initialize(); + 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}); + } + } - ////////////// + function getSensorsToCompare() { + return vm.sensors ? vm.sensors.filter(function(sensor) { + return sensor.id !== vm.selectedSensor; + }) : []; + } - function initialize() { - animation.viewLoaded(); - getTags(); - vm.userRole = auth.getCurrentUser().data.role; + function changeChart(sensorsID, options) { + if(!sensorsID[0]) { + return; } - 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; - }); - }); + if(!options) { + options = {}; } + options.from = options && options.from || picker.getValuePickerFrom(); + options.to = options && options.to || picker.getValuePickerTo(); - function removeTag(tagID) { - vm.deviceForm.tags = _.filter(vm.deviceForm.tags, function(tag) { - return tag.id !== tagID; + //show spinner + vm.loadingChart = true; + //grab chart data and save it + + // 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 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 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]); - 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'); - }); + compareSensor = { + data: parsedDataCompare, + color: vm.selectedSensorToCompareData.color, + unit: vm.selectedSensorToCompareData.unit + }; } + var newChartData = [mainSensor, compareSensor]; + return newChartData; + } - function getTags() { - tag.getTags() - .then(function(tagsData) { - vm.tags = tagsData; - }); + 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 + }; + }); + } - function toProfile(){ - $state.transitionTo('layout.myProfile.kits', $stateParams, - { reload: false, - inherit: false, - notify: true - }); + function setSensor(options) { + var sensorID = options.value; + if(sensorID === undefined) { + return; } - - function backToProfile(){ - // TODO: Refactor Check - toProfile(); + if(options.type === 'main') { + mainSensorID = sensorID; + } else if(options.type === 'compare') { + compareSensorID = sensorID; } + } - //TODO: move to utils - function findExposure(nameOrValue) { - var findProp, resultProp; - //if it's a string - if(isNaN(parseInt(nameOrValue))) { - findProp = 'name'; - resultProp = 'value'; - } else { - findProp = 'value'; - resultProp = 'name'; - } - - var option = _.find(vm.exposure, function(exposureFromList) { - return exposureFromList[findProp] === nameOrValue; - }); - if(option) { - return option[resultProp]; - } - } + function 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; + } - // 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); - } - }; - }); + function moveChart(direction) { - angular.module('app.components') - .controller('EditKitController', EditKitController); + var valueTo, valueFrom; + //grab current date range + var currentRange = getCurrentRange(); - 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) { + /*jshint camelcase: false*/ + var from_picker = angular.element('#picker_from').pickadate('picker'); + var to_picker = angular.element('#picker_to').pickadate('picker'); - var vm = this; + if(direction === 'left') { + //set both from and to pickers to prev range + valueTo = moment(picker.getValuePickerFrom()); + valueFrom = moment(picker.getValuePickerFrom()).subtract(currentRange, 'seconds'); - // WAIT INTERVAL FOR USER FEEDBACK and TRANSITIONS (This will need to change) - var timewait = { - long: 5000, - normal: 2000, - short: 1000 - }; + picker.setValuePickers([valueFrom.toDate(), valueTo.toDate()]); - vm.step = step; + } else if(direction === 'right') { + var today = timeUtils.getToday(); + var currentValueTo = picker.getValuePickerTo(); + if( timeUtils.isSameDay(today, timeUtils.getMillisFromDate(currentValueTo)) ) { + return; + } - // KEY USER ACTIONS - vm.submitFormAndKit = submitFormAndKit; - vm.backToProfile = backToProfile; - vm.backToDevice = backToDevice; - vm.submitForm = submitForm; - vm.goToStep = goToStep; - vm.nextAction = 'save'; + valueFrom = moment(picker.getValuePickerTo()); + valueTo = moment(picker.getValuePickerTo()).add(currentRange, 'seconds'); - // EXPOSURE SELECT - vm.exposure = [ - {name: 'indoor', value: 1}, - {name: 'outdoor', value: 2} - ]; + picker.setValuePickers([valueFrom.toDate(), valueTo.toDate()]); - // FORM INFO - vm.deviceForm = {}; - vm.device = undefined; + } + resetTimeOpts(); + } - $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(); + //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('leafletDirectiveMarker.dragend', function(event, args){ - vm.deviceForm.location.lat = args.model.lat; - vm.deviceForm.location.lng = args.model.lng; + 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' + } }); - // MAP CONFIGURATION - var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; + var to_picker = to_$input.pickadate('picker'); - 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 - }; + 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') ); + } - initialize(); + from_picker.on('close', function(event) { + setFromRange(getCalculatedFrom(from_picker.get('value'))); + }); - ///////////////// + to_picker.on('close', function(event) { + setToRange(getCalculatedTo(to_picker.get('value'))); + }); - function initialize() { - var deviceID = $stateParams.id; + from_picker.on('set', function(event) { + if(event.select) { + to_picker.set('min', getFromRange()); + } else if( 'clear' in event) { + to_picker.set('min', false); + } + }); - animation.viewLoaded(); - getTags(); - - if (!deviceID || deviceID === ''){ - return; + to_picker.on('set', function(event) { + if(event.select) { + from_picker.set('max', getToRange()); + } else if( 'clear' in event) { + from_picker.set('max', false); } - device.getDevice(deviceID) - .then(function(deviceData) { - vm.device = new FullDevice(deviceData); - vm.userRole = auth.getCurrentUser().data.role; - vm.deviceForm = { - name: vm.device.name, - exposure: findExposureFromLabels(vm.device.systemTags), - location: { - lat: vm.device.location.latitude, - lng: vm.device.location.longitude, - zoom: 16 - }, - is_private: vm.device.isPrivate, - precise_location: vm.device.preciseLocation, - enable_forwarding: vm.device.enableForwarding, - notify_low_battery: vm.device.notifications.lowBattery, - notify_stopped_publishing: vm.device.notifications.stopPublishing, - tags: vm.device.userTags, - postprocessing: vm.device.postProcessing, - description: vm.device.description, - hardwareName: vm.device.hardware.name - }; - vm.markers = { - main: { - lat: vm.device.location.latitude, - lng: vm.device.location.longitude, - draggable: true - } - }; - - if (vm.device.isLegacy) { - vm.deviceForm.macAddress = vm.device.macAddress; - } - }); - } + }); - // Return tags in a comma separated list - function joinSelectedTags(){ - let tmp = [] - $scope.selectedTags.forEach(function(e){ - tmp.push(e.name) - }) - return tmp.join(', '); - } + //set to-picker max to today + to_picker.set('max', getLatestUpdated()); - 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 getSevenDaysAgoFromLatestUpdate() { + var lastTime = moment(vm.device.lastReadingAt.raw); + return lastTime.subtract(7, 'days').valueOf(); } - function submitFormAndKit(){ - submitForm(backToProfile, timewait.normal); + function getLatestUpdated() { + return moment(vm.device.lastReadingAt.raw).toDate(); } - function submitForm(next, delayTransition) { - var data = { - name: vm.deviceForm.name, - description: vm.deviceForm.description, - postprocessing_attributes: vm.deviceForm.postprocessing, - exposure: findExposure(vm.deviceForm.exposure), - latitude: vm.deviceForm.location.lat, - longitude: vm.deviceForm.location.lng, - is_private: vm.deviceForm.is_private, - enable_forwarding: vm.deviceForm.enable_forwarding, - precise_location: vm.deviceForm.precise_location, - notify_low_battery: vm.deviceForm.notify_low_battery, - notify_stopped_publishing: vm.deviceForm.notify_stopped_publishing, - mac_address: "", - /*jshint camelcase: false */ - user_tags: joinSelectedTags(), - }; - - vm.errors={}; + function getCalculatedFrom(pickerTimeFrom) { + var from, + pickerTime; - if(!vm.device.isSCK) { - data.hardware_name_override = vm.deviceForm.hardwareName; - } + pickerTime = moment(pickerTimeFrom, 'D MMMM, YYYY'); + from = pickerTime.startOf('day'); - // 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; - } + return from; + } - device.updateDevice(vm.device.id, data) - .then( - function() { + function getCalculatedTo(pickerTimeTo) { + var to, + pickerTime; - if (next){ - alert.success('Your kit was updated!'); - } + pickerTime = moment(pickerTimeTo, 'D MMMM, YYYY'); - 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); + 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') }); - } + } - 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); - } + return to; } - function findExposure(nameOrValue) { - var findProp, resultProp; - - //if it's a string - if(isNaN(parseInt(nameOrValue))) { - findProp = 'name'; - resultProp = 'value'; - } else { - findProp = 'value'; - resultProp = 'name'; - } - - var option = _.find(vm.exposure, function(exposureFromList) { - return exposureFromList[findProp] === nameOrValue; + function updateChart() { + var sensors = [mainSensorID, compareSensorID]; + sensors = sensors.filter(function(sensor) { + return sensor; + }); + changeChart(sensors, { + from: range.from, + to: range.to }); - if(option) { - return option[resultProp]; - } else { - return vm.exposure[0][resultProp]; - } } - function getTags() { - tag.getTags() - .then(function(tagsData) { - vm.tags = tagsData; - }); + function setFromRange(from) { + range.from = from; + from_picker.set('select', getFromRange()); + updateChart(); } - function backToProfile(){ - $state.transitionTo('layout.myProfile.kits', $stateParams, - { reload: false, - inherit: false, - notify: true - }); + function setToRange(to) { + range.to = to; + to_picker.set('select', getToRange()); + updateChart(); } - function backToDevice(){ - $state.transitionTo('layout.home.kit', $stateParams, - { reload: false, - inherit: false, - notify: true - }); + function getFromRange() { + return moment(range.from).toDate(); } - function goToStep(step) { - vm.step = step; - $state.transitionTo('layout.kitEdit', { id:$stateParams.id, step: step} , - { - reload: false, - inherit: false, - notify: false - }); + function getToRange() { + return moment(range.to).toDate(); } - } -})(); - -(function() { - 'use strict'; - - angular.module('app.components') - .factory('User', ['COUNTRY_CODES', function(COUNTRY_CODES) { - - /** - * User constructor - * @param {Object} userData - User data sent from API - * @property {number} id - User ID - * @property {string} username - Username - * @property {string} profile_picture - Avatar URL of user - * @property {Array} devices - Kits that belongs to this user - * @property {string} url - URL - * @property {string} city - User city - * @property {string} country - User country - */ - function 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 setRange(from, to) { + range.from = from; + range.to = to; + from_picker.set('select', getFromRange()); + to_picker.set('select', getToRange()); + updateChart(); } - return User; - }]); -})(); - -(function() { - 'use strict'; - - angular.module('app.components') - .factory('NonAuthUser', ['User', function(User) { - - function NonAuthUser(userData) { - User.call(this, userData); + 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()); + } } - NonAuthUser.prototype = Object.create(User.prototype); - NonAuthUser.prototype.constructor = User; - - return NonAuthUser; - }]); -})(); -(function() { - 'use strict'; + // 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); + } + }; + } - angular.module('app.components') - .factory('AuthUser', ['User', function(User) { + 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; + } - /** - * 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 - */ + geolocation.grantHTML5Geolocation(); - function AuthUser(userData) { - User.call(this, userData); + var location = { + lat:position.coords.latitude, + lng:position.coords.longitude + }; + device.getDevices(location) + .then(function(data){ + data = data.plain(); - this.email = userData.email; - this.role = userData.role; - /*jshint camelcase: false */ - this.key = userData.legacy_api_key; + _(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(); + }); + }); } - AuthUser.prototype = Object.create(User.prototype); - AuthUser.prototype.constructor = User; - - return AuthUser; - }]); -})(); + } -(function() { - 'use strict'; + function downloadData(device){ + $mdDialog.show({ + hasBackdrop: true, + controller: 'DownloadModalController', + controllerAs: 'vm', + templateUrl: 'app/components/download/downloadModal.html', + clickOutsideToClose: true, + locals: {thisDevice: device} + }).then(function(){ + var alert = $mdDialog.alert() + .title('SUCCESS') + .textContent('We are processing your data. Soon you will be notified in your inbox') + .ariaLabel('') + .ok('OK!') + .theme('primary') + .clickOutsideToClose(true); - angular.module('app.components') - .factory('Sensor', ['sensorUtils', 'timeUtils', function(sensorUtils, timeUtils) { + $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); - /** - * 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) { + $mdDialog.show(errorAlert); + }); + } - 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; + 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); + }); + }) + ); + } - // 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; - } + 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()]); + } - // Get sensor tags - this.tags = sensorData.tags; + function timeOptSelected(){ + vm.allowUpdateChart = false; + if (vm.dropDownSelection){ + setFromLast(vm.dropDownSelection); } + } + function resetTimeOpts(){ + vm.allowUpdateChart = false; + vm.dropDownSelection = undefined; + } - return Sensor; - }]); + function showStore() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'StoreModalController', + templateUrl: 'app/components/store/storeModal.html', + clickOutsideToClose: true + }); + } + } })(); + (function() { 'use strict'; angular.module('app.components') - .factory('SearchResultLocation', ['SearchResult', function(SearchResult) { + .controller('NewKitController', NewKitController); - /** - * 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); - - this.lat = object.latitude; - this.lng = object.longitude; - this.layer = object.layer; - } - return SearchResultLocation; - }]); - -})(); - -(function() { - 'use strict'; + 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; - angular.module('app.components') - .factory('SearchResult', ['searchUtils', function(searchUtils) { + vm.step = 1; + vm.submitStepOne = submitStepOne; + vm.backToProfile = backToProfile; - /** - * 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; - }]); -})(); + // FORM INFO + vm.deviceForm = { + name: undefined, + exposure: undefined, + location: { + lat: undefined, + lng: undefined, + zoom: 16 + }, + is_private: false, + legacyVersion: '1.1', + tags: [] + }; -(function() { - 'use strict'; + // EXPOSURE SELECT + vm.exposure = [ + {name: 'indoor', value: 1}, + {name: 'outdoor', value: 2} + ]; - 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 = ''; + // VERSION SELECT + vm.version = [ + {name: 'Smart Citizen Kit 1.0', value: '1.0'}, + {name: 'Smart Citizen Kit 1.1', value: '1.1'} + ]; - 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; + $scope.$on('leafletDirectiveMarker.dragend', function(event, args){ + vm.deviceForm.location.lat = args.model.lat; + vm.deviceForm.location.lng = args.model.lng; + }); - function createTagsTemplate(tagsArr, tagType, clickable) { - if(typeof(clickable) === 'undefined'){ - clickable = false; - } - var clickablTag = ''; - if(clickable){ - clickablTag = 'clickable'; + // TAGS SELECT + vm.tags = []; + $scope.$watch('vm.tag', function(newVal, oldVal) { + if(!newVal) { + return; } + // remove selected tag from select element + vm.tag = undefined; - if(!tagType){ - tagType = 'tag'; + var alreadyPushed = _.some(vm.deviceForm.tags, function(tag) { + return tag.id === newVal; + }); + if(alreadyPushed) { + return; } - return _.reduce(tagsArr, function(acc, label) { - var element =''; - if(tagType === 'tag'){ - element = ''; - }else{ - element = ''+label+''; - } - return acc.concat(element); - }, ''); - } - - }]); -})(); - -(function () { - 'use strict'; - - angular.module('app.components') - .factory('PreviewDevice', ['Device', function (Device) { + var tag = _.find(vm.tags, function(tag) { + return tag.id === newVal; + }); + vm.deviceForm.tags.push(tag); + }); + vm.removeTag = removeTag; - /** - * 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); + // MAP CONFIGURATION + var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; - 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; - }]); -})(); + 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() { - 'use strict'; + vm.macAddress = undefined; - angular.module('app.components') - .factory('HasSensorDevice', ['Device', function(Device) { + initialize(); - function HasSensorDevice(object) { - Device.call(this, object); + ////////////// - this.sensors = object.data.sensors; - this.longitude = object.data.location.longitude; - this.latitude = object.data.location.latitude; + function initialize() { + animation.viewLoaded(); + getTags(); + vm.userRole = auth.getCurrentUser().data.role; } - HasSensorDevice.prototype = Object.create(Device.prototype); - HasSensorDevice.prototype.constructor = Device; - - HasSensorDevice.prototype.sensorsHasData = function() { - var parsedSensors = this.sensors.map(function(sensor) { - return sensor.value; + 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; + }); }); + } - return _.some(parsedSensors, function(sensorValue) { - return !!sensorValue; + function removeTag(tagID) { + vm.deviceForm.tags = _.filter(vm.deviceForm.tags, function(tag) { + return tag.id !== tagID; }); - }; - - return HasSensorDevice; - }]); -})(); - -(function() { - 'use strict'; + } - angular.module('app.components') - .factory('FullDevice', ['Device', 'Sensor', 'deviceUtils', function(Device, Sensor, deviceUtils) { + 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(',') + }; - /** - * 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); + device.createDevice(data) + .then( + function(response) { + device.updateContext().then(function(){ + auth.setCurrentUser('appLoad').then(function(){ + $timeout($state.go('layout.kitEdit', {id:response.id, step:2}), 2000); + }); + }); + }, + function(err) { + vm.errors = err.data.errors; + alert.error('There has been an error during kit set up'); + }); + } - this.owner = deviceUtils.parseOwner(object); - this.postProcessing = object.postprocessing; - this.data = object.data; - this.sensors = object.data.sensors; + function getTags() { + tag.getTags() + .then(function(tagsData) { + vm.tags = tagsData; + }); } - FullDevice.prototype = Object.create(Device.prototype); - FullDevice.prototype.constructor = FullDevice; + function toProfile(){ + $state.transitionTo('layout.myProfile.kits', $stateParams, + { reload: false, + inherit: false, + notify: true + }); + } - 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 backToProfile(){ + // TODO: Refactor Check + toProfile(); + } - return FullDevice; - }]); + //TODO: move to utils + function findExposure(nameOrValue) { + var findProp, resultProp; + //if it's a string + if(isNaN(parseInt(nameOrValue))) { + findProp = 'name'; + resultProp = 'value'; + } else { + findProp = 'value'; + resultProp = 'name'; + } + + var option = _.find(vm.exposure, function(exposureFromList) { + return exposureFromList[findProp] === nameOrValue; + }); + if(option) { + return option[resultProp]; + } + } + } })(); (function() { 'use strict'; - angular.module('app.components') - .factory('Device', ['deviceUtils', 'timeUtils', function(deviceUtils, timeUtils) { - - /** - * 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; + // 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); + } + }; + }); - // Tags and dates - this.systemTags = deviceUtils.parseSystemTags(object); - this.userTags = deviceUtils.parseUserTags(object); - this.isPrivate = deviceUtils.isPrivate(object); - this.preciseLocation = deviceUtils.preciseLocation(object); - this.enableForwarding = deviceUtils.enableForwarding(object); - this.notifications = deviceUtils.parseNotifications(object); - this.lastReadingAt = timeUtils.parseDate(object.last_reading_at); - this.createdAt = timeUtils.parseDate(object.created_at); - this.updatedAt = timeUtils.parseDate(object.updated_at); + angular.module('app.components') + .controller('EditKitController', EditKitController); - // Location - this.location = object.location; - this.locationString = deviceUtils.parseLocation(object); + 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) { - // 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? + var vm = this; - this.avatar = deviceUtils.parseAvatar(); - /*jshint camelcase: false */ - } + // WAIT INTERVAL FOR USER FEEDBACK and TRANSITIONS (This will need to change) + var timewait = { + long: 5000, + normal: 2000, + short: 1000 + }; - return Device; - }]); -})(); + vm.step = step; -(function() { - 'use strict'; + // KEY USER ACTIONS + vm.submitFormAndKit = submitFormAndKit; + vm.backToProfile = backToProfile; + vm.backToDevice = backToDevice; + vm.submitForm = submitForm; + vm.goToStep = goToStep; + vm.nextAction = 'save'; - angular.module('app.components') - .directive('noDataBackdrop', noDataBackdrop); + // EXPOSURE SELECT + vm.exposure = [ + {name: 'indoor', value: 1}, + {name: 'outdoor', value: 2} + ]; - /** - * Backdrop for chart section when kit has no data - * - */ - noDataBackdrop.$inject = []; + // FORM INFO + vm.deviceForm = {}; + vm.device = undefined; - function noDataBackdrop() { - return { - restrict: 'A', - scope: {}, - templateUrl: 'app/core/animation/backdrop/noDataBackdrop.html', - controller: function($scope, $timeout) { - var vm = this; + $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(); + }); - vm.deviceWithoutData = false; - vm.scrollToComments = scrollToComments; + $scope.$on('leafletDirectiveMarker.dragend', function(event, args){ + vm.deviceForm.location.lat = args.model.lat; + vm.deviceForm.location.lng = args.model.lng; + }); - $scope.$on('deviceWithoutData', function(ev, data) { + // MAP CONFIGURATION + var mapBoxToken = 'pk.eyJ1IjoidG9tYXNkaWV6IiwiYSI6ImRTd01HSGsifQ.loQdtLNQ8GJkJl2LUzzxVg'; - $timeout(function() { - vm.device = data.device; - vm.deviceWithoutData = true; - - if (data.belongsToUser) { - vm.user = 'owner'; - } else { - vm.user = 'visitor'; - } - }, 0); - - }); - - function scrollToComments(){ - location.hash = ''; - location.hash = '#disqus_thread'; - } - }, - controllerAs: 'vm' - }; - } -})(); + 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 + }; -(function() { - 'use strict'; + initialize(); - 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; + function initialize() { + var deviceID = $stateParams.id; - // listen for app loading event - $scope.$on('viewLoading', function() { - vm.isViewLoading = true; - }); + animation.viewLoaded(); + getTags(); - $scope.$on('viewLoaded', function() { - vm.isViewLoading = false; - }); + if (!deviceID || deviceID === ''){ + return; + } + device.getDevice(deviceID) + .then(function(deviceData) { + vm.device = new FullDevice(deviceData); + vm.userRole = auth.getCurrentUser().data.role; + vm.deviceForm = { + name: vm.device.name, + exposure: findExposureFromLabels(vm.device.systemTags), + location: { + lat: vm.device.location.latitude, + lng: vm.device.location.longitude, + zoom: 16 + }, + is_private: vm.device.isPrivate, + precise_location: vm.device.preciseLocation, + enable_forwarding: vm.device.enableForwarding, + notify_low_battery: vm.device.notifications.lowBattery, + notify_stopped_publishing: vm.device.notifications.stopPublishing, + tags: vm.device.userTags, + postprocessing: vm.device.postProcessing, + description: vm.device.description, + hardwareName: vm.device.hardware.name + }; + vm.markers = { + main: { + lat: vm.device.location.latitude, + lng: vm.device.location.longitude, + draggable: true + } + }; - // listen for map state loading event - $scope.$on('mapStateLoading', function() { - if(vm.isViewLoading) { - return; + if (vm.device.isLegacy) { + vm.deviceForm.macAddress = vm.device.macAddress; } - vm.mapStateLoading = true; - }); - - $scope.$on('mapStateLoaded', function() { - vm.mapStateLoading = false; }); - }, - controllerAs: 'vm' - }; - } -})(); + } -(function() { - 'use strict'; + // Return tags in a comma separated list + function joinSelectedTags(){ + let tmp = [] + $scope.selectedTags.forEach(function(e){ + tmp.push(e.name) + }) + return tmp.join(', '); + } - angular.module('app.components') - .controller('UserProfileController', UserProfileController); + 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; + }); + }); + } - UserProfileController.$inject = ['$scope', '$stateParams', '$location', - 'user', 'auth', 'userUtils', '$timeout', 'animation', - 'NonAuthUser', '$q', 'PreviewDevice']; - function UserProfileController($scope, $stateParams, $location, - user, auth, userUtils, $timeout, animation, - NonAuthUser, $q, PreviewDevice) { + function submitFormAndKit(){ + submitForm(backToProfile, timewait.normal); + } - var vm = this; - var userID = parseInt($stateParams.id); + function submitForm(next, delayTransition) { + var data = { + name: vm.deviceForm.name, + description: vm.deviceForm.description, + postprocessing_attributes: vm.deviceForm.postprocessing, + exposure: findExposure(vm.deviceForm.exposure), + latitude: vm.deviceForm.location.lat, + longitude: vm.deviceForm.location.lng, + is_private: vm.deviceForm.is_private, + enable_forwarding: vm.deviceForm.enable_forwarding, + precise_location: vm.deviceForm.precise_location, + notify_low_battery: vm.deviceForm.notify_low_battery, + notify_stopped_publishing: vm.deviceForm.notify_stopped_publishing, + mac_address: "", + /*jshint camelcase: false */ + user_tags: joinSelectedTags(), + }; - vm.status = undefined; - vm.user = {}; - vm.devices = []; - vm.filteredDevices = []; - vm.filterDevices = filterDevices; + vm.errors={}; - $scope.$on('loggedIn', function() { - var authUser = auth.getCurrentUser().data; - if( userUtils.isAuthUser(userID, authUser) ) { - $location.path('/profile'); + if(!vm.device.isSCK) { + data.hardware_name_override = vm.deviceForm.hardwareName; } - }); - - initialize(); - - ////////////////// - - function initialize() { - user.getUser(userID) - .then(function(user) { - vm.user = new NonAuthUser(user); - - if(!vm.user.devices.length) { - return []; - } + // 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; + } - $q.all(vm.devices = vm.user.devices.map(function(data){ - return new PreviewDevice(data); - })) + device.updateDevice(vm.device.id, data) + .then( + function() { - }).then(function(error) { - if(error && error.status === 404) { - $location.url('/404'); - } - }); + if (next){ + alert.success('Your kit was updated!'); + } - $timeout(function() { - setSidebarMinHeight(); - animation.viewLoaded(); - }, 500); + 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 filterDevices(status) { - if(status === 'all') { - status = undefined; + 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); } - vm.status = status; } - function setSidebarMinHeight() { - var height = document.body.clientHeight / 4 * 3; - angular.element('.profile_content').css('min-height', height + 'px'); - } - } -})(); + function findExposure(nameOrValue) { + var findProp, resultProp; -(function() { - 'use strict'; + //if it's a string + if(isNaN(parseInt(nameOrValue))) { + findProp = 'name'; + resultProp = 'value'; + } else { + findProp = 'value'; + resultProp = 'name'; + } - angular.module('app.components') - .controller('UploadController', UploadController); + var option = _.find(vm.exposure, function(exposureFromList) { + return exposureFromList[findProp] === nameOrValue; + }); + if(option) { + return option[resultProp]; + } else { + return vm.exposure[0][resultProp]; + } + } - UploadController.$inject = ['kit', '$state', '$stateParams', 'animation']; - function UploadController(kit, $state, $stateParams, animation) { - var vm = this; + function getTags() { + tag.getTags() + .then(function(tagsData) { + vm.tags = tagsData; + }); + } - vm.kit = kit; + function backToProfile(){ + $state.transitionTo('layout.myProfile.kits', $stateParams, + { reload: false, + inherit: false, + notify: true + }); + } - vm.backToProfile = backToProfile; + function backToDevice(){ + $state.transitionTo('layout.home.kit', $stateParams, + { reload: false, + inherit: false, + notify: true + }); + } - initialize(); + function goToStep(step) { + vm.step = step; + $state.transitionTo('layout.kitEdit', { id:$stateParams.id, step: step} , + { + reload: false, + inherit: false, + notify: false + }); + } + } +})(); - ///////////////// +(function() { + 'use strict'; - function initialize() { - animation.viewLoaded(); - } + angular.module('app.components') + .factory('userUtils', userUtils); - function backToProfile() { - $state.transitionTo('layout.myProfile.kits', $stateParams, - { reload: false, - inherit: false, - notify: true - }); + function userUtils() { + var service = { + isAdmin: isAdmin, + isAuthUser: isAuthUser + }; + return service; + + /////////// + + function isAdmin(userData) { + return userData.role === 'admin'; + } + function isAuthUser(userID, authUserData) { + return userID === authUserData.id; + } } - } })(); -(function(){ -'use strict'; +(function() { + 'use strict'; + angular.module('app.components') + .factory('timeUtils', timeUtils); + function timeUtils() { + var service = { + getSecondsFromDate: getSecondsFromDate, + getMillisFromDate: getMillisFromDate, + getCurrentRange: getCurrentRange, + getToday: getToday, + getHourBefore: getHourBefore, + getSevenDaysAgo: getSevenDaysAgo, + getDateIn: getDateIn, + convertTime: convertTime, + formatDate: formatDate, + isSameDay: isSameDay, + isWithin15min: isWithin15min, + isWithin1Month: isWithin1Month, + isWithin: isWithin, + isDiffMoreThan15min: isDiffMoreThan15min, + parseDate: parseDate + }; + return service; -function 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 - }; - }) - }; -} + //////////// + function getDateIn(timeMS, format) { + if(!format) { + return timeMS; + } + 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; + } -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 convertTime(time) { + return moment(time).toISOString(); + } - }); - }) - ).then(() => { - vm.loadingStatus = false; - }).catch(() => { - vm.loadingStatus = false; - }); - } + function formatDate(time) { + return moment(time).format('YYYY-MM-DDTHH:mm:ss'); + } - vm.haveSelectedFiles = function() { - return vm.csvFiles && vm.csvFiles.some((file) => file.checked); - }; + function getSecondsFromDate(date) { + return (new Date(date)).getTime(); + } - vm.haveSelectedNoFiles = function() { - return vm.csvFiles && !vm.csvFiles.some((file) => file.checked); - }; + function getMillisFromDate(date) { + return (new Date(date)).getTime(); + } - vm.haveSelectedAllFiles = function() { - return vm.csvFiles && vm.csvFiles.every((file) => file.checked); - }; + function getCurrentRange(fromDate, toDate) { + return moment(toDate).diff(moment(fromDate), 'days'); + } - 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; + function getToday() { + return (new Date()).getTime(); } - vm.action = null; - }; - vm.selectAll = function(value) { - vm.csvFiles.forEach((file) => { file.checked = value }); - }; + function getSevenDaysAgo() { + return getSecondsFromDate( getToday() - (7 * 24 * 60 * 60 * 1000) ); + } - 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 getHourBefore(date) { + var now = moment(date); + return now.subtract(1, 'hour').valueOf(); + } - vm._checkDuplicate = function(file) { - if (vm.csvFiles.some((csvFile) => file.name === csvFile.name)) { - file.$errorMessages = {}; - file.$errorMessages.duplicate = true; - vm.invalidFiles.push(file); + function isSameDay(day1, day2) { + day1 = moment(day1); + day2 = moment(day2); + + if(day1.startOf('day').isSame(day2.startOf('day'))) { + return true; + } return false; - } else { - return true; } - }; - 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 isDiffMoreThan15min(dateToCheckFrom, dateToCheckTo) { + var duration = moment.duration(moment(dateToCheckTo).diff(moment(dateToCheckFrom))); + return duration.as('minutes') > 15; + } + function isWithin15min(dateToCheck) { + var fifteenMinAgo = moment().subtract(15, 'minutes').valueOf(); + dateToCheck = moment(dateToCheck).valueOf(); - vm.uploadData = function() { - vm.loadingStatus = true; - vm.loadingType = 'indeterminate'; - vm.loadingProgress = 0; - let count = 0; + return dateToCheck > fifteenMinAgo; + } - $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; - }); - } -}; + function isWithin1Month(dateToCheck) { + var oneMonthAgo = moment().subtract(1, 'months').valueOf(); + dateToCheck = moment(dateToCheck).valueOf(); + return dateToCheck > oneMonthAgo; + } -angular.module('app.components') - .component('scCsvUpload', { - templateUrl: 'app/components/upload/csvUpload.html', - controller: controller, - bindings: { - kit: '<' - }, - controllerAs: 'vm' - }); + function isWithin(number, type, dateToCheck) { + var ago = moment().subtract(number, type).valueOf(); + dateToCheck = moment(dateToCheck).valueOf(); + + return dateToCheck > ago; + } + + function parseDate(object){ + var time = object; + return { + raw: time, + parsed: !time ? 'No time' : moment(time).format('MMMM DD, YYYY - HH:mm'), + ago: !time ? 'No time' : moment(time).fromNow() + } + } + } })(); (function() { 'use strict'; angular.module('app.components') - .controller('tagsController', tagsController); - - tagsController.$inject = ['tag', '$scope', 'device', '$state', '$q', - 'PreviewDevice', 'animation' - ]; + .factory('sensorUtils', sensorUtils); - function tagsController(tag, $scope, device, $state, $q, PreviewDevice, - animation) { + 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; - var vm = this; + /////////////// - vm.selectedTags = tag.getSelectedTags(); - vm.markers = []; - vm.kits = []; - vm.percActive = 0; + function getRollup(dateFrom, dateTo) { - initialize(); + // Calculate how many data points we can fit on a users screen + // Smaller screens request less data from the API + var durationInSec = moment(dateTo).diff(moment(dateFrom)) / 1000; + var chartWidth = window.innerWidth / 2; - ///////////////////////////////////////////////////////// + var rollup = parseInt(durationInSec / chartWidth) + 's'; - function initialize() { - if(vm.selectedTags.length === 0){ - $state.transitionTo('layout.home.kit'); + /* + //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; } - 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 getSensorName(name) { + + var sensorName; + // TODO: Improvement check how we set new names + if( new RegExp('custom circuit', 'i').test(name) ) { + sensorName = name; + } else { + if(new RegExp('noise', 'i').test(name) ) { + sensorName = 'SOUND'; + } else if(new RegExp('light', 'i').test(name) ) { + sensorName = 'LIGHT'; + } else if((new RegExp('nets', 'i').test(name) ) || + (new RegExp('wifi', 'i').test(name))) { + sensorName = 'NETWORKS'; + } else if(new RegExp('co', 'i').test(name) ) { + sensorName = 'CO'; + } else if(new RegExp('no2', 'i').test(name) ) { + sensorName = 'NO2'; + } else if(new RegExp('humidity', 'i').test(name) ) { + sensorName = 'HUMIDITY'; + } else if(new RegExp('temperature', 'i').test(name) ) { + sensorName = 'TEMPERATURE'; + } else if(new RegExp('panel', 'i').test(name) ) { + sensorName = 'SOLAR PANEL'; + } else if(new RegExp('battery', 'i').test(name) ) { + sensorName = 'BATTERY'; + } else if(new RegExp('barometric pressure', 'i').test(name) ) { + sensorName = 'BAROMETRIC PRESSURE'; + } else if(new RegExp('PM 1', 'i').test(name) ) { + sensorName = 'PM 1'; + } else if(new RegExp('PM 2.5', 'i').test(name) ) { + sensorName = 'PM 2.5'; + } else if(new RegExp('PM 10', 'i').test(name) ) { + sensorName = 'PM 10'; + } else { + sensorName = name; + } + } + return sensorName.toUpperCase(); } - } + function getSensorValue(sensor) { + var value = sensor.value; - function updateSelectedTags(){ + if(isNaN(parseInt(value))) { + value = 'NA'; + } else { + value = round(value, 1).toString(); + } - vm.markers = tag.filterMarkersByTag(device.getWorldMarkers()); + return value; + } - 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); + function round(value, precision) { + var multiplier = Math.pow(10, precision || 0); + return Math.round(value * multiplier) / multiplier; } - animation.viewLoaded(); + function getSensorPrevValue(sensor) { + /*jshint camelcase: false */ + var prevValue = sensor.prev_value; + return (prevValue && prevValue.toString() ) || 0; + } - getTaggedDevices() - .then(function(res){ - vm.kits = res; - }); - } + function getSensorIcon(sensorName) { + var thisName = getSensorName(sensorName); - function isOnline(marker) { - return _.includes(marker.myData.labels, 'online'); - } + switch(thisName) { + case 'TEMPERATURE': + return './assets/images/temperature_icon_new.svg'; - function descLastUpdate(o) { - return -new Date(o.last_reading_at).getTime(); - } + case 'HUMIDITY': + return './assets/images/humidity_icon_new.svg'; - function getTaggedDevices() { + case 'LIGHT': + return './assets/images/light_icon_new.svg'; - var deviceProm = _.map(vm.markers, getMarkerDevice); + case 'SOUND': + return './assets/images/sound_icon_new.svg'; - return $q.all(deviceProm) - .then(function(devices) { - return _.map(_.sortBy(devices, descLastUpdate), toPreviewDevice); // This sort is temp - }); - } + case 'CO': + return './assets/images/co_icon_new.svg'; - function toPreviewDevice(dev) { - return new PreviewDevice(dev); - } + case 'NO2': + return './assets/images/no2_icon_new.svg'; - function getMarkerDevice(marker) { - return device.getDevice(marker.myData.id); - } - } + case 'NETWORKS': + return './assets/images/networks_icon.svg'; -})(); + case 'BATTERY': + return './assets/images/battery_icon.svg'; -(function(){ - 'use strict'; - angular.module('app.components') - .directive('tag',tag); + case 'SOLAR PANEL': + return './assets/images/solar_panel_icon.svg'; - 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'); + case 'BAROMETRIC PRESSURE': + return './assets/images/pressure_icon_new.svg'; - if(typeof(attrs.clickable) !== 'undefined'){ - element.bind('click', scope.openTag); + 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() { - 'use strict'; + function getSensorArrow(currentValue, prevValue) { + currentValue = parseInt(currentValue) || 0; + prevValue = parseInt(prevValue) || 0; - angular.module('app.components') - .controller('StoreModalController', StoreModalController); + if(currentValue > prevValue) { + return 'arrow_up'; + } else if(currentValue < prevValue) { + return 'arrow_down'; + } else { + return 'equal'; + } + } - StoreModalController.$inject = ['$scope', '$mdDialog']; - function StoreModalController($scope, $mdDialog) { + function getSensorColor(sensorName) { + switch(getSensorName(sensorName)) { + case 'TEMPERATURE': + return '#FF3D4C'; - $scope.cancel = function() { - $mdDialog.hide(); - }; - } -})(); + case 'HUMIDITY': + return '#55C4F5'; -(function() { - 'use strict'; + case 'LIGHT': + return '#ffc107'; - angular.module('app.components') - .directive('store', store); + case 'SOUND': + return '#0019FF'; - function store() { - return { - scope: { - isLoggedin: '=logged' - }, - restrict: 'A', - controller: 'StoreController', - controllerAs: 'vm', - templateUrl: 'app/components/store/store.html' - }; - } -})(); + case 'CO': + return '#00A103'; -(function() { - 'use strict'; + case 'NO2': + return '#8cc252'; - angular.module('app.components') - .controller('StoreController', StoreController); + case 'NETWORKS': + return '#681EBD'; - StoreController.$inject = ['$scope', '$mdDialog']; - function StoreController($scope, $mdDialog) { + case 'SOLAR PANEL': + return '#d555ce'; - $scope.showStore = showStore; + case 'BATTERY': + return '#ff8601'; - $scope.$on('showStore', function() { - showStore(); - }); - - //////////////// + default: + return '#0019FF'; + } + } - function showStore() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'StoreModalController', - templateUrl: 'app/components/store/storeModal.html', - clickOutsideToClose: true - }); + 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('StaticController', StaticController); + .factory('searchUtils', searchUtils); - StaticController.$inject = ['$timeout', 'animation', '$mdDialog', '$location', '$anchorScroll']; - function StaticController($timeout, animation, $mdDialog, $location, $anchorScroll) { - var vm = this; + searchUtils.$inject = []; + function searchUtils() { + var service = { + parseLocation: parseLocation, + parseName: parseName, + parseIcon: parseIcon, + parseIconType: parseIconType + }; + return service; - vm.showStore = showStore; + ///////////////// - $anchorScroll.yOffset = 80; + function parseLocation(object) { + var location = ''; - /////////////////////// + if(!!object.city) { + location += object.city; + } + if(!!object.city && !!object.country) { + location += ', '; + } + if(!!object.country) { + location += object.country; + } - initialize(); + return location; + } - ////////////////// + function parseName(object) { + var name = object.type === 'User' ? object.username : object.name; + return name; + } - function initialize() { - $timeout(function() { - animation.viewLoaded(); - if($location.hash()){ - $anchorScroll(); + 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'; } - }, 500); - } + } - function showStore() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'StoreModalController', - templateUrl: 'app/components/store/storeModal.html', - clickOutsideToClose: true - }); + function parseIconType(type) { + switch(type) { + case 'Device': + return 'div'; + default: + return 'img'; + } + } } - } })(); (function() { 'use strict'; angular.module('app.components') - .controller('SignupModalController', SignupModalController); + .factory('markerUtils', markerUtils); - SignupModalController.$inject = ['$scope', '$mdDialog', 'user', - 'alert', 'animation']; - function SignupModalController($scope, $mdDialog, user, - alert, animation ) { - var vm = this; - vm.answer = function(signupForm) { + markerUtils.$inject = ['deviceUtils', 'MARKER_ICONS']; + function markerUtils(deviceUtils, MARKER_ICONS) { + var service = { + getIcon: getIcon, + getMarkerIcon: getMarkerIcon, + }; + _.defaults(service, deviceUtils); + return service; - if (!signupForm.$valid){ - return; + /////////////// + + function getIcon(object) { + var icon; + var labels = deviceUtils.parseSystemTags(object); + var isSCKHardware = deviceUtils.isSCKHardware(object); + + if(hasLabel(labels, 'offline')) { + icon = MARKER_ICONS.markerSmartCitizenOffline; + } else if (isSCKHardware) { + icon = MARKER_ICONS.markerSmartCitizenOnline; + } else { + icon = MARKER_ICONS.markerExperimentalNormal; } + return icon; + } - $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'; + function hasLabel(labels, targetLabel) { + return _.some(labels, function(label) { + return label === targetLabel; + }); + } - angular.module('app.components') - .directive('signup', signup); + function getMarkerIcon(marker, state) { + var markerType = marker.icon.className; - function signup() { - return { - scope: { - show: '=', - }, - restrict: 'A', - controller: 'SignupController', - controllerAs: 'vm', - templateUrl: 'app/components/signup/signup.html' - }; + 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; + } } })(); @@ -2440,208 +2315,307 @@ angular.module('app.components') 'use strict'; angular.module('app.components') - .controller('SignupController', SignupController); + .factory('mapUtils', mapUtils); - SignupController.$inject = ['$scope', '$mdDialog']; - function SignupController($scope, $mdDialog) { - var vm = this; + mapUtils.$inject = []; + function mapUtils() { + var service = { + getDefaultFilters: getDefaultFilters, + setDefaultFilters: setDefaultFilters, + canFilterBeRemoved: canFilterBeRemoved + }; + return service; - vm.showSignup = showSignup; + ////////////// - $scope.$on('showSignup', function() { - showSignup(); - }); - //////////////////////// + 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; + } + function setDefaultFilters(filterData) { + var obj = {}; + if(!filterData.indoor || !filterData.outdoor) { + obj.exposure = filterData.indoor ? 'indoor' : 'outdoor'; + } + if(!filterData.online || !filterData.offline) { + obj.status = filterData.online ? 'online' : 'offline'; + } + return obj; + } - function showSignup() { - $mdDialog.show({ - fullscreen: true, - hasBackdrop: true, - controller: 'SignupModalController', - controllerAs: 'vm', - templateUrl: 'app/components/signup/signupModal.html', - clickOutsideToClose: true - }); + function canFilterBeRemoved(filterData, filterName) { + if(filterName === 'indoor' || filterName === 'outdoor') { + return filterData.indoor && filterData.outdoor; + } else if(filterName === 'online' || filterName === 'offline') { + return filterData.online && filterData.offline; + } } } })(); (function() { -'use strict'; - - + 'use strict'; angular.module('app.components') - .directive('search', search); + .config(function ($provide) { + $provide.decorator('$exceptionHandler', ['$delegate', function($delegate) { + return function (exception, cause) { + /*jshint camelcase: false */ + $delegate(exception, cause); + }; + }]); - function search() { - return { - scope: true, - restrict: 'E', - templateUrl: 'app/components/search/search.html', - controller: 'SearchController', - controllerAs: 'vm' - }; - } + }); })(); (function() { 'use strict'; angular.module('app.components') - .controller('SearchController', SearchController); + .factory('deviceUtils', deviceUtils); - SearchController.$inject = ['$scope', 'search', 'SearchResult', '$location', 'animation', 'SearchResultLocation']; - function SearchController($scope, search, SearchResult, $location, animation, SearchResultLocation) { - var vm = this; + deviceUtils.$inject = ['COUNTRY_CODES', 'device']; + function deviceUtils(COUNTRY_CODES, device) { + var service = { + parseLocation: parseLocation, + parseCoordinates: parseCoordinates, + parseSystemTags: parseSystemTags, + parseUserTags: parseUserTags, + classify: classify, + parseNotifications: parseNotifications, + parseOwner: parseOwner, + parseName: parseName, + parseString: parseString, + parseHardware: parseHardware, + parseHardwareInfo: parseHardwareInfo, + parseHardwareName: parseHardwareName, + isPrivate: isPrivate, + preciseLocation: preciseLocation, + enableForwarding: enableForwarding, + isLegacyVersion: isLegacyVersion, + isSCKHardware: isSCKHardware, + parseState: parseState, + parseAvatar: parseAvatar, + belongsToUser: belongsToUser, + parseSensorTime: parseSensorTime + }; - vm.searchTextChange = searchTextChange; - vm.selectedItemChange = selectedItemChange; - vm.querySearch = querySearch; + return service; - /////////////////// + /////////////// - function searchTextChange() { - } + function parseLocation(object) { + var location = ''; + var city = ''; + var country = ''; - 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}); + 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 querySearch(query) { - if(query.length < 3) { - return []; + function parseCoordinates(object) { + if (object.location) { + return { + lat: object.location.latitude, + lng: object.location.longitude + }; } + // TODO: Bug - what happens if no location? + } - return search.globalSearch(query) - .then(function(data) { + function parseSystemTags(object) { + /*jshint camelcase: false */ + return object.system_tags; + } - return data.map(function(object) { + function parseUserTags(object) { + return object.user_tags; + } - if(object.type === 'City' || object.type === 'Country') { - return new SearchResultLocation(object); - } else { - return new SearchResult(object); - } - }); - }); + function parseNotifications(object){ + return { + lowBattery: object.notify.low_battery, + stopPublishing: object.notify.stopped_publishing + } } - } -})(); -(function() { - 'use strict'; + function classify(kitType) { + if(!kitType) { + return ''; + } + return kitType.toLowerCase().split(' ').join('_'); + } - angular.module('app.components') - .controller('PasswordResetController', PasswordResetController); + 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; + } - 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; + function parseHardware(object) { + if (!object.hardware) { + return; + } - initialize(); - /////////// + return { + name: parseString(object.hardware.name), + type: parseString(object.hardware.type), + description: parseString(object.hardware.description), + version: parseVersionString(object.hardware.version), + slug: object.hardware.slug, + info: parseHardwareInfo(object.hardware.info) + } + } - function initialize() { - $timeout(function() { - animation.viewLoaded(); - }, 500); - getUserData(); + function parseString(str) { + if (typeof(str) !== 'string') { return null; } + return str; } - function getUserData() { - auth.getResetPassword($stateParams.code) - .then(function() { - vm.showForm = true; - }) - .catch(function() { - alert.error('Wrong url'); - $location.path('/'); - }); + 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 answer(data) { - vm.waitingFromServer = true; - vm.errors = undefined; + function parseHardwareInfo (object) { + if (!object) { return null; } // null + if (typeof(object) == 'string') { return null; } // FILTERED - if(data.newPassword === data.confirmPassword) { - vm.isDifferent = false; + var id = parseString(object.id); + var mac = parseString(object.mac); + var time = Date(object.time); + var esp_bd = parseString(object.esp_bd); + var hw_ver = parseString(object.hw_ver); + var sam_bd = parseString(object.sam_bd); + var esp_ver = parseString(object.esp_ver); + var sam_ver = parseString(object.sam_ver); + + return { + id: id, + mac: mac, + time: time, + esp_bd: esp_bd, + hw_ver: hw_ver, + sam_bd: sam_bd, + esp_ver: esp_ver, + sam_ver: sam_ver + }; + } + + function parseHardwareName(object) { + if (object.hasOwnProperty('hardware')) { + if (!object.hardware.name) { + return 'Unknown hardware' + } + return object.hardware.name; } else { - vm.isDifferent = true; - return; + return 'Unknown hardware' } + } - 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 isPrivate(object) { + return object.data_policy.is_private; } - } -})(); -(function() { - 'use strict'; + function preciseLocation(object) { + return object.data_policy.precise_location; + } - angular.module('app.components') - .controller('PasswordRecoveryModalController', PasswordRecoveryModalController); + function enableForwarding(object) { + return object.data_policy.enable_forwarding ; + } - PasswordRecoveryModalController.$inject = ['$scope', 'animation', '$mdDialog', 'auth', 'alert']; - function PasswordRecoveryModalController($scope, animation, $mdDialog, auth, alert) { + 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; + } + } - $scope.hide = function() { - $mdDialog.hide(); - }; - $scope.cancel = function() { - $mdDialog.cancel(); - }; + function isSCKHardware (object){ + if (!object.hardware || !object.hardware.type || object.hardware.type != 'SCK') { + return false; + } else { + return true; + } + } - $scope.recoverPassword = function() { - $scope.waitingFromServer = true; - var data = { + function parseOwner(object) { + return { + id: object.owner.id, + username: object.owner.username, /*jshint camelcase: false */ - email_or_username: $scope.input + 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.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; - }); - }; + function parseState(status) { + var name = parseStateName(status); + var className = classify(name); - $scope.openSignup = function() { - animation.showSignup(); - $mdDialog.hide(); - }; + return { + name: name, + className: className + }; + } + + function parseStateName(object) { + return object.state.replace('_', ' '); + } + + function parseAvatar() { + return './assets/images/sckit_avatar.jpg'; + } + + function parseSensorTime(sensor) { + /*jshint camelcase: false */ + return moment(sensor.recorded_at).format(''); + } + + function belongsToUser(devicesArray, deviceID) { + return _.some(devicesArray, function(device) { + return device.id === deviceID; + }); + } } })(); @@ -2649,1065 +2623,1111 @@ angular.module('app.components') 'use strict'; angular.module('app.components') - .controller('PasswordRecoveryController', PasswordRecoveryController); - - PasswordRecoveryController.$inject = ['auth', 'alert', '$mdDialog']; - function PasswordRecoveryController(auth, alert, $mdDialog) { - var vm = this; - - vm.waitingFromServer = false; - vm.errors = undefined; - vm.recoverPassword = recoverPassword; - - /////////////// + .filter('filterLabel', filterLabel); - 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 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() { 'use strict'; - angular.module('app.components') - .controller('MyProfileController', MyProfileController); + /** + * Tools links for user profile + * @constant + * @type {Array} + */ - 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) { - - var vm = this; - - 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 { + angular.module('app.components') + .constant('PROFILE_TOOLS', [{ + type: 'documentation', + title: 'How to connect your Smart Citizen Kit tutorial', + description: 'Adding a Smart Citizen Kit tutorial', + avatar: '', + href: 'http://docs.smartcitizen.me/#/start/adding-a-smart-citizen-kit' + }, { + type: 'documentation', + title: 'Download the latest Smart Citizen Kit Firmware', + description: 'The latest Arduino firmware for your kit', + avatar: '', + href: 'https://github.com/fablabbcn/Smart-Citizen-Kit/releases/latest' + }, { + type: 'documentation', + title: 'API Documentation', + description: 'Documentation for the new API', + avatar: '', + href: 'http://developer.smartcitizen.me/' + }, { + type: 'community', + title: 'Smart Citizen Forum', + description: 'Join the community discussion. Your feedback is important for us.', + avatar: '', + href:'http://forum.smartcitizen.me/' + }, { + type: 'documentation', + title: 'Smart Citizen Kit hardware details', + description: 'Visit the docs', + avatar: 'https://docs.smartcitizen.me/#/start/hardware' + }, { + type: 'documentation', + title: 'Style Guide', + description: 'Guidelines of the Smart Citizen UI', + avatar: '', + href: '/styleguide' + }, { + type: 'social', + title: 'Like us on Facebook', + description: 'Join the community on Facebook', + avatar: '', + href: 'https://www.facebook.com/smartcitizenBCN' + }, { + type: 'social', + title: 'Follow us on Twitter', + description: 'Follow our news on Twitter', + avatar: '', + href: 'https://twitter.com/SmartCitizenKit' + }]); +})(); - vm.devices = vm.user.devices.map(function(data) { - return new PreviewDevice(data); - }) +(function() { + 'use strict'; - $timeout(function() { - mapWithBelongstoUser(vm.devices); - filterDevices(vm.status); - setSidebarMinHeight(); - animation.viewLoaded(); - }); + /** + * Marker icons + * @constant + * @type {Object} + */ - } + angular.module('app.components') + .constant('MARKER_ICONS', { + defaultIcon: {}, + markerSmartCitizenNormal: { + type: 'div', + className: 'markerSmartCitizenNormal', + iconSize: [24, 24] + }, + markerExperimentalNormal: { + type: 'div', + className: 'markerExperimentalNormal', + iconSize: [24, 24] + }, + markerSmartCitizenOnline: { + type: 'div', + className: 'markerSmartCitizenOnline', + iconSize: [24, 24] + }, + markerSmartCitizenOnlineActive: { + type: 'div', + className: 'markerSmartCitizenOnline marker_blink', + iconSize: [24, 24] + }, + markerSmartCitizenOffline: { + type: 'div', + className: 'markerSmartCitizenOffline', + iconSize: [24, 24] + }, + markerSmartCitizenOfflineActive: { + type: 'div', + className: 'markerSmartCitizenOffline marker_blink', + iconSize: [24, 24] } + }); +})(); - function filterDevices(status) { - if(status === 'all') { - status = undefined; - } - vm.deviceStatus = status; - vm.filteredDevices = $filter('filterLabel')(vm.devices, vm.deviceStatus); - } +(function() { + 'use strict'; - function filterTools(type) { - if(type === 'all') { - type = undefined; - } - vm.toolType = type; - } + /** + * 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 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() { + 'use strict'; - 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; - }); - } + /** + * Dropdown options for community button + * @constant + * @type {Array} + */ - function removeUser() { - var confirm = $mdDialog.confirm() - .title('Delete your account?') - .textContent('Are you sure you want to delete your account?') - .ariaLabel('') - .ok('delete') - .cancel('cancel') - .theme('primary') - .clickOutsideToClose(true); - - $mdDialog.show(confirm) - .then(function(){ - return Restangular.all('').customDELETE('me') - .then(function(){ - alert.success('Account removed successfully. Redirecting you…'); - $timeout(function(){ - auth.logout(); - $state.transitionTo('landing'); - }, 2000); - }) - .catch(function(){ - alert.error('Error occurred trying to delete your account.'); - }); - }); - } - - function selectThisTab(iconIndex, uistate){ - /* This looks more like a hack but we need to workout how to properly use md-tab with ui-router */ - - highlightIcon(iconIndex); - - 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 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(); - - switch(childState) { - case 'user': - vm.startingTab = 1; - break; - default: - vm.startingTab = 0; - break; - } - - } - - function highlightIcon(iconIndex) { - - var icons = angular.element('.myProfile_tab_icon'); - - _.each(icons, function(icon) { - unhighlightIcon(icon); - }); - - var icon = icons[iconIndex]; - - angular.element(icon).find('.stroke_container').css({'stroke': 'white', 'stroke-width': '0.01px'}); - angular.element(icon).find('.fill_container').css('fill', 'white'); - } - - function unhighlightIcon(icon) { - icon = angular.element(icon); - - icon.find('.stroke_container').css({'stroke': 'none'}); - icon.find('.fill_container').css('fill', '#FF8600'); - } - - function setSidebarMinHeight() { - var height = document.body.clientHeight / 4 * 3; - angular.element('.profile_content').css('min-height', height + 'px'); - } - - function getCountries(searchText) { - return _.filter(COUNTRY_CODES, createFilter(searchText)); - } - - function createFilter(searchText) { - searchText = searchText.toLowerCase(); - return function(country) { - country = country.toLowerCase(); - return country.indexOf(searchText) !== -1; - }; - } - - function uploadAvatar(fileData) { - if(fileData && fileData.length) { - - // 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 copyUserToForm(formData, userData) { - var props = {username: true, email: true, city: true, country: true, country_code: true, url: true, constructor: false}; - - for(var key in userData) { - if(props[key]) { - formData[key] = userData[key]; - } - } - } - - function mapWithBelongstoUser(devices){ - _.map(devices, addBelongProperty); - } - - function addBelongProperty(device){ - device.belongProperty = deviceBelongsToUser(device); - return device; - } - - - function deviceBelongsToUser(device){ - if(!auth.isAuth() || !device || !device.id) { - return false; - } - var deviceID = parseInt(device.id); - var userData = ( auth.getCurrentUser().data ) || - ($window.localStorage.getItem('smartcitizen.data') && - new AuthUser( JSON.parse( - $window.localStorage.getItem('smartcitizen.data') ))); - - var belongsToUser = deviceUtils.belongsToUser(userData.devices, deviceID); - var isAdmin = userUtils.isAdmin(userData); - - return isAdmin || belongsToUser; - } - - 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 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.'); - }); - }); - } - - $scope.addDeviceSelector = addDeviceSelector; - function addDeviceSelector(){ - $mdDialog.show({ - templateUrl: 'app/components/myProfile/addDeviceSelectorModal.html', - clickOutsideToClose: true, - multiple: true, - controller: DialogController, - }); - } - - function DialogController($scope, $mdDialog){ - $scope.cancel = function(){ - $mdDialog.cancel(); - }; - } - - function addNewDevice() { - var confirm = $mdDialog.confirm() - .title('Hey! Do you want to add a new kit?') - .textContent('Please, notice this currently supports just the SCK 1.0 and SCK 1.1') - .ariaLabel('') - .ok('Ok') - .cancel('Cancel') - .theme('primary') - .clickOutsideToClose(true); - - $mdDialog - .show(confirm) - .then(function(){ - $state.go('layout.kitAdd'); - }); - } - - - } -})(); + angular.module('app.components') + .constant('DROPDOWN_OPTIONS_COMMUNITY', [ + {text: 'About', href: '/about'}, + {text: 'Forum', href: 'https://forum.smartcitizen.me/'}, + {text: 'Documentation', href: 'http://docs.smartcitizen.me/'}, + {text: 'API Reference', href: 'http://developer.smartcitizen.me/'}, + {text: 'Github', href: 'https://github.com/fablabbcn/Smart-Citizen-Kit'}, + {text: 'Legal', href: '/policy'} + ]); +})(); (function() { 'use strict'; + /** + * Country codes. + * @constant + * @type {Object} + */ + angular.module('app.components') - .controller('MapTagModalController', MapTagModalController); - - MapTagModalController.$inject = ['$mdDialog', 'tag', 'selectedTags']; - - function MapTagModalController($mdDialog, tag, selectedTags) { - - var vm = this; - - vm.checks = {}; - - vm.answer = answer; - vm.hide = hide; - vm.clear = clear; - vm.cancel = cancel; - vm.tags = []; - - init(); - - //////////////////////////////////////////////////////// + .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 init() { - tag.getTags() - .then(function(tags) { - vm.tags = tags; +(function() { + 'use strict'; - _.forEach(selectedTags, select); + angular.module('app.components') + .factory('user', user); - }); - } + user.$inject = ['Restangular']; + function user(Restangular) { + var service = { + createUser: createUser, + getUser: getUser, + updateUser: updateUser + }; + return service; - function answer() { + //////////////////// - var selectedTags = _(vm.tags) - .filter(isTagSelected) - .value(); - $mdDialog.hide(selectedTags); - } + function createUser(signupData) { + return Restangular.all('users').post(signupData); + } - function hide() { - answer(); - } + function getUser(id) { + return Restangular.one('users', id).get(); + } - function clear() { - $mdDialog.hide(null); - } + function updateUser(updateData) { + return Restangular.all('me').customPUT(updateData); + } + } +})(); - function cancel() { - answer(); - } +(function() { + 'use strict'; - function isTagSelected(tag) { - return vm.checks[tag.name]; - } + angular.module('app.components') + .factory('tag', tag); - function select(tag){ - vm.checks[tag] = true; + 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') - .controller('MapFilterModalController', MapFilterModalController); + .factory('sensor', sensor); - MapFilterModalController.$inject = ['$mdDialog','selectedFilters', '$timeout']; + sensor.$inject = ['Restangular', 'timeUtils', 'sensorUtils']; + function sensor(Restangular, timeUtils, sensorUtils) { + var sensorTypes; + callAPI().then(function(data) { + setTypes(data); + }); - function MapFilterModalController($mdDialog, selectedFilters, $timeout) { + var service = { + callAPI: callAPI, + setTypes: setTypes, + getTypes: getTypes, + getSensorsData: getSensorsData + }; + return service; - var vm = this; + //////////////// - vm.checks = {}; + function callAPI() { + return Restangular.all('sensors').getList({'per_page': 1000}); + } - vm.answer = answer; - vm.hide = hide; - vm.clear = clear; - vm.cancel = cancel; - vm.toggle = toggle; + function setTypes(sensorTypes) { + sensorTypes = sensorTypes; + } - vm.location = ['indoor', 'outdoor']; - vm.status = ['online', 'offline']; - vm.new = ['new']; + function getTypes() { + return sensorTypes; + } - vm.filters = []; + function getSensorsData(deviceID, sensorID, dateFrom, dateTo) { + var rollup = sensorUtils.getRollup(dateFrom, dateTo); + dateFrom = timeUtils.convertTime(dateFrom); + dateTo = timeUtils.convertTime(dateTo); - init(); + return Restangular.one('devices', deviceID).customGET('readings', {'from': dateFrom, 'to': dateTo, 'rollup': rollup, 'sensor_id': sensorID, 'all_intervals': true}); + } + } +})(); - //////////////////////////////////////////////////////// +(function() { + 'use strict'; - function init() { - _.forEach(selectedFilters, select); - } + angular.module('app.components') + .factory('search', search); + + search.$inject = ['$http', 'Restangular']; + function search($http, Restangular) { + var service = { + globalSearch: globalSearch + }; - function answer() { - vm.filters = vm.filters.concat(vm.location, vm.status, vm.new); - var selectedFilters = _(vm.filters) - .filter(isFilterSelected) - .value(); - $mdDialog.hide(selectedFilters); - } + return service; - function hide() { - answer(); - } + ///////////////////////// - function clear() { - vm.filters = vm.filters.concat(vm.location, vm.status, vm.new); - $mdDialog.hide(vm.filters); + function globalSearch(query) { + return Restangular.all('search').getList({q: query}); + } } +})(); - function cancel() { - answer(); - } +(function() { + 'use strict'; - function isFilterSelected(filter) { - return vm.checks[filter]; - } + angular.module('app.components') + .factory('measurement', measurement); - function toggle(filters) { - $timeout(function() { + measurement.$inject = ['Restangular']; - for (var i = 0; i < filters.length - 1; i++) { - if (vm.checks[filters[i]] === false && vm.checks[filters[i]] === vm.checks[filters[i+1]]) { - for (var n = 0; n < filters.length; n++) { - vm.checks[filters[n]] = true; - } - } - } + function measurement(Restangular) { - }); + var service = { + getTypes: getTypes, + getMeasurement: getMeasurement + + }; + return service; + + //////////////// + + + function getTypes() { + return Restangular.all('measurements').getList({'per_page': 1000}); } - function select(filter){ - vm.checks[filter] = true; + function getMeasurement(mesID) { + + return Restangular.one('measurements', mesID).get(); } } })(); - (function() { - 'use strict'; + 'use strict'; - angular.module('app.components') - .controller('MapController', MapController); + angular.module('app.components') + .factory('geolocation', geolocation); - 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; + geolocation.$inject = ['$http', '$window']; + function geolocation($http, $window) { - vm.markers = []; + var service = { + grantHTML5Geolocation: grantHTML5Geolocation, + isHTML5GeolocationGranted: isHTML5GeolocationGranted + }; + return service; - 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 - }; - - 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; - } + function grantHTML5Geolocation(){ + $window.localStorage.setItem('smartcitizen.geolocation_granted', true); + } - vm.deviceLoading = true; - vm.center.lat = data.leafletEvent.latlng.lat; - vm.center.lng = data.leafletEvent.latlng.lng; + function isHTML5GeolocationGranted(){ + return $window.localStorage + .getItem('smartcitizen.geolocation_granted'); + } + } +})(); - if(id === parseInt($state.params.id)) { - $timeout(function() { - vm.deviceLoading = false; - }); - return; - } +(function() { + 'use strict'; - updateType = 'map'; + angular.module('app.components') + .factory('file', file); - if ($state.$current.name === 'embbed') { return; } - $state.go('layout.home.kit', {id: id}); + file.$inject = ['Restangular', 'Upload']; + function file(Restangular, Upload) { + var service = { + getCredentials: getCredentials, + uploadFile: uploadFile, + getImageURL: getImageURL + }; + return service; - // angular.element('section.map').scope().$broadcast('resizeMapHeight'); - }); + /////////////// + function getCredentials(filename) { + var data = { + filename: filename + }; + return Restangular.all('me/avatar').post(data); + } - $scope.$on('leafletDirectiveMarker.popupclose', function() { - if(focusedMarkerID) { - var marker = vm.markers[focusedMarkerID]; - if(marker) { - vm.markers[focusedMarkerID].focus = false; + 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 } - } - }); + }); + } - vm.readyForDevice = { - device: false, - map: false - }; + function getImageURL(filename, size) { + size = size === undefined ? 's101' : size; - $scope.$on('deviceLoaded', function(event, data) { - vm.readyForDevice.device = data; - }); + return 'https://images.smartcitizen.me/' + size + '/' + filename; + } + } +})(); - $scope.$watch('vm.readyForDevice', function() { - if (vm.readyForDevice.device && vm.readyForDevice.map) { - zoomDeviceAndPopUp(vm.readyForDevice.device); - } - }, true); +(function() { + 'use strict'; - $scope.$on('goToLocation', function(event, data) { - goToLocation(data); - }); + angular.module('app.components') + .factory('device', device); - vm.filters = ['indoor', 'outdoor', 'online', 'offline']; + device.$inject = ['Restangular', '$window', 'timeUtils','$http', 'auth', '$rootScope']; + function device(Restangular, $window, timeUtils, $http, auth, $rootScope) { + var worldMarkers; - vm.openFilterPopup = openFilterPopup; - vm.openTagPopup = openTagPopup; - vm.removeFilter = removeFilter; - vm.removeTag = removeTag; - vm.selectedTags = tag.getSelectedTags(); - vm.selectedFilters = ['indoor', 'outdoor', 'online', 'offline', 'new']; + initialize(); - vm.checkAllFiltersSelected = checkAllFiltersSelected; + var service = { + getDevices: getDevices, + getAllDevices: getAllDevices, + getDevice: getDevice, + createDevice: createDevice, + updateDevice: updateDevice, + getWorldMarkers: getWorldMarkers, + setWorldMarkers: setWorldMarkers, + mailReadings: mailReadings, + postReadings: postReadings, + removeDevice: removeDevice, + updateContext: updateContext + }; - initialize(); + return service; - ///////////////////// + ////////////////////////// function initialize() { + if(areMarkersOld()) { + removeMarkers(); + } + } - vm.readyForDevice.map = false; + function getDevices(location) { + var parameter = ''; + parameter += location.lat + ',' + location.lng; + return Restangular.all('devices').getList({near: parameter, 'per_page': '100'}); + } - $q.all([device.getAllDevices($stateParams.reloadMap)]) - .then(function(data){ + function getAllDevices(forceReload) { + if (forceReload || auth.isAuth()) { + return getAllDevicesNoCached(); + } else { + return getAllDevicesCached(); + } + } - data = data[0]; + function getAllDevicesCached() { + return Restangular.all('devices/world_map') + .getList() + .then(function(fetchedDevices){ + return fetchedDevices.plain(); + }); + } - vm.markers = _.chain(data) - .map(function(device) { - return new Marker(device); - }) - .filter(function(marker) { - return !!marker.lng && !!marker.lat; - }) - .tap(function(data) { - device.setWorldMarkers(data); - }) - .value(); + function getAllDevicesNoCached() { + return Restangular.all('devices/fresh_world_map') + .getList() + .then(function(fetchedDevices){ + return fetchedDevices.plain(); + }); + } - var markersByIndex = _.keyBy(vm.markers, function(marker) { - return marker.myData.id; - }); + function getDevice(id) { + return Restangular.one('devices', id).get(); + } - 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 createDevice(data) { + return Restangular.all('devices').post(data); + } - }); + function updateDevice(id, data) { + return Restangular.one('devices', id).patch(data); } - function zoomDeviceAndPopUp(data){ + function getWorldMarkers() { + return worldMarkers || ($window.localStorage.getItem('smartcitizen.markers') && JSON.parse($window.localStorage.getItem('smartcitizen.markers') ).data); + } - if(updateType === 'map') { - vm.deviceLoading = false; - updateType = undefined; - return; - } else { - vm.deviceLoading = 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..."); } - - 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(); - }); - } - }); - }); - + worldMarkers = obj.data; } - function checkAllFiltersSelected() { - var allFiltersSelected = _.every(vm.filters, function(filterValue) { - return _.includes(vm.selectedFilters, filterValue); - }); - return allFiltersSelected; + function getTimeStamp() { + return ($window.localStorage.getItem('smartcitizen.markers') && + JSON.parse($window.localStorage + .getItem('smartcitizen.markers') ).timestamp); } - 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 areMarkersOld() { + var markersDate = getTimeStamp(); + return !timeUtils.isWithin(1, 'minutes', markersDate); } - 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 removeMarkers() { + worldMarkers = null; + $window.localStorage.removeItem('smartcitizen.markers'); } - function updateMapFilters(){ - vm.selectedTags = tag.getSelectedTags(); - checkAllFiltersSelected(); - updateMarkers(); + function mailReadings(kit) { + return Restangular + .one('devices', kit.id) + .customGET('readings/csv_archive'); } - function removeFilter(filterName) { - vm.selectedFilters = _.filter(vm.selectedFilters, function(el){ - return el !== filterName; - }); - if(vm.selectedFilters.length === 0){ - vm.selectedFilters = vm.filters; - } - updateMarkers(); - } + function postReadings(kit, readings) { + return Restangular + .one('devices', kit.id) + .post('readings', readings); + } - 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 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 updateMarkers() { - $timeout(function() { - $scope.$apply(function() { - var allMarkers = device.getWorldMarkers(); + } +})(); - var updatedMarkers = allMarkers; +(function() { + 'use strict'; - updatedMarkers = tag.filterMarkersByTag(updatedMarkers); - updatedMarkers = filterMarkersByLabel(updatedMarkers); - vm.markers = updatedMarkers; + angular.module('app.components') + .factory('auth', auth); - animation.mapStateLoaded(); + auth.$inject = ['$location', '$window', '$state', 'Restangular', + '$rootScope', 'AuthUser', '$timeout', 'alert', '$cookies']; + function auth($location, $window, $state, Restangular, $rootScope, AuthUser, + $timeout, alert, $cookies) { - vm.deviceLoading = false; + var user = {}; - zoomOnMarkers(); - }); - }); - } + //wait until http interceptor is added to Restangular + $timeout(function() { + initialize(); + }, 100); - function getZoomLevel(data) { - // data.layer is an array of strings like ["establishment", "point_of_interest"] - var zoom = 18; + 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; - if(data.layer && data.layer[0]) { - switch(data.layer[0]) { - case 'point_of_interest': - zoom = 18; - break; - case 'address': - zoom = 18; - break; - case "establishment": - zoom = 15; - break; - case 'neighbourhood': - zoom = 13; - break; - case 'locality': - zoom = 13; - break; - case 'localadmin': - zoom = 9; - break; - case 'county': - zoom = 9; - break; - case 'region': - zoom = 8; - break; - case 'country': - zoom = 7; - break; - case 'coarse': - zoom = 7; - break; - } - } + ////////////////////////// - return zoom; + function initialize() { + //console.log('---- AUTH INIT -----'); + setCurrentUser('appLoad'); } - 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); - } + //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 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); - }); + 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; + + //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'); + + // 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); + } + }); } - function removeTag(tagName){ - tag.setSelectedTags(_.filter(vm.selectedTags, function(el){ - return el !== tagName; - })); + // 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(); + }); + } - vm.selectedTags = tag.getSelectedTags(); + function getCurrentUser() { + user.token = getToken(); + user.data = $window.localStorage.getItem('smartcitizen.data') && new AuthUser(JSON.parse( $window.localStorage.getItem('smartcitizen.data') )); + return user; + } - if(vm.selectedTags.length === 0){ - reloadNoTags(); - } else { - reloadWithTags(); - } + // 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(); + } + // 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 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 getToken(){ + return $cookies.get('smartcitizen.token'); } - function reloadWithTags(){ - $state.transitionTo('layout.home.tags', {tags: vm.selectedTags}, {reload: true}); + function login(loginData) { + return Restangular.all('sessions').post(loginData); } - function reloadNoTags(){ - $state.transitionTo('layout.home.kit'); + function logout() { + $cookies.remove('smartcitizen.token'); } - } + function getCurrentUserFromAPI() { + return Restangular.all('').customGET('me'); + } + + function recoverPassword(data) { + return Restangular.all('password_resets').post(data); + } + function getResetPassword(code) { + return Restangular.one('password_resets', code).get(); + } + function patchResetPassword(code, data) { + return Restangular.one('password_resets', code).patch(data); + } + function isAdmin(userData) { + return userData.role === 'admin'; + } + } })(); (function() { 'use strict'; + /** + * Unused directive. Double-check before removing. + * + */ angular.module('app.components') - .controller('LoginModalController', LoginModalController); + .directive('slide', slide) + .directive('slideMenu', slideMenu); - 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(); + function slideMenu() { + return { + controller: controller, + link: link }; - $scope.openSignup = function() { - animation.showSignup(); - $mdDialog.hide(); - }; + function link(scope, element) { + scope.element = element; + } - $scope.openPasswordRecovery = function() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'PasswordRecoveryModalController', - templateUrl: 'app/components/passwordRecovery/passwordRecoveryModal.html', - clickOutsideToClose: true - }); + function controller($scope) { + $scope.slidePosition = 0; + $scope.slideSize = 20; - $mdDialog.hide(); + 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; + } + }; + } + } + + slide.$inject = []; + function slide() { + return { + link: link, + require: '^slide-menu', + restrict: 'A', + scope: { + direction: '@' + } }; + + function link(scope, element, attr, slideMenuCtrl) { + //select first sensor container + var sensorsContainer = angular.element('.sensors_container'); + + element.on('click', function() { + + if(slideMenuCtrl.scrollIsValid('left') && attr.direction === 'left') { + slideMenuCtrl.decrementPosition(); + sensorsContainer.scrollLeft(slideMenuCtrl.getPosition()); + console.log(slideMenuCtrl.getPosition()); + } else if(slideMenuCtrl.scrollIsValid('right') && attr.direction === 'right') { + slideMenuCtrl.incrementPosition(); + sensorsContainer.scrollLeft(slideMenuCtrl.getPosition()); + console.log(slideMenuCtrl.getPosition()); + } + }); + } } })(); (function() { 'use strict'; - angular.module('app.components') - .directive('login', login); + angular.module('app.components') + .directive('showPopupInfo', showPopupInfo); - function login() { + /** + * Used to show/hide explanation of sensor value at kit dashboard + * + */ + showPopupInfo.$inject = []; + function showPopupInfo() { return { - scope: { - show: '=' - }, - restrict: 'A', - controller: 'LoginController', - controllerAs: 'vm', - templateUrl: 'app/components/login/login.html' + link: link }; + + ////// + + + 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'); + }); + } } })(); @@ -3715,131 +3735,90 @@ angular.module('app.components') 'use strict'; angular.module('app.components') - .controller('LoginController', LoginController); + .directive('showPopup', showPopup); - LoginController.$inject = ['$scope', '$mdDialog']; - function LoginController($scope, $mdDialog) { + /** + * Used on kit dashboard to open full sensor description + */ - $scope.showLogin = showLogin; - - $scope.$on('showLogin', function() { - showLogin(); - }); + showPopup.$inject = []; + function showPopup() { + return { + link: link + }; - //////////////// + ///// - function showLogin() { - $mdDialog.show({ - hasBackdrop: true, - fullscreen: true, - controller: 'LoginModalController', - controllerAs: 'vm', - templateUrl: 'app/components/login/loginModal.html', - clickOutsideToClose: true - }); + 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() { '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; + .directive('moveFilters', moveFilters); - vm.navRightLayout = 'space-around center'; + /** + * Moves map filters when scrolling + * + */ + moveFilters.$inject = ['$window', '$timeout']; + function moveFilters($window, $timeout) { + return { + link: link + }; - $scope.toggleRight = buildToggler('right'); + function link() { + var chartHeight; + $timeout(function() { + chartHeight = angular.element('.kit_chart').height(); + }, 1000); - function buildToggler(componentId) { - return function() { - $mdSidenav(componentId).toggle(); - }; + /* + angular.element($window).on('scroll', function() { + var windowPosition = document.body.scrollTop; + if(chartHeight > windowPosition) { + elem.css('bottom', 12 + windowPosition + 'px'); + } + }); + */ } + } +})(); - // listen for any login event so that the navbar can be updated - $scope.$on('loggedIn', function(ev, options) { - // if(options && options.time === 'appLoad') { - // $scope.$apply(function() { - // vm.isLoggedin = true; - // vm.isShown = true; - // angular.element('.nav_right .wrap-dd-menu').css('display', 'initial'); - // vm.currentUser = auth.getCurrentUser().data; - // vm.dropdownOptions[0].text = 'Hello, ' + vm.currentUser.username; - // vm.navRightLayout = 'end center'; - // }); - // } else { - // vm.isLoggedin = true; - // vm.isShown = true; - // angular.element('.nav_right .wrap-dd-menu').css('display', 'initial'); - // vm.currentUser = auth.getCurrentUser().data; - // vm.dropdownOptions[0].text = 'Hello, ' + vm.currentUser.username; - // vm.navRightLayout = 'end center'; - // } - - vm.isLoggedin = true; - vm.isShown = true; - angular.element('.nav_right .wrap-dd-menu').css('display', 'initial'); - vm.currentUser = auth.getCurrentUser().data; - vm.dropdownOptions[0].text = 'Hi, ' + vm.currentUser.username + '!'; - vm.navRightLayout = 'end center'; - if(!$scope.$$phase) { - $scope.$digest(); - } - }); - - // listen for logout events so that the navbar can be updated - $scope.$on('loggedOut', function() { - vm.isLoggedIn = false; - vm.isShown = true; - angular.element('navbar .wrap-dd-menu').css('display', 'none'); - vm.navRightLayout = 'space-around center'; - }); - - - vm.isShown = true; - vm.isLoggedin = false; - vm.logout = logout; - - vm.dropdownOptions = DROPDOWN_OPTIONS_USER; - vm.dropdownSelected = undefined; +(function() { + 'use strict'; - vm.dropdownOptionsCommunity = DROPDOWN_OPTIONS_COMMUNITY; - vm.dropdownSelectedCommunity = undefined; + angular.module('app.components') + .factory('layout', layout); - $scope.$on('removeNav', function() { - vm.isShown = false; - }); - $scope.$on('addNav', function() { - vm.isShown = true; - }); + function layout() { - initialize(); + var kitHeight; - ////////////////// + var service = { + setKit: setKit, + getKit: getKit + }; + return service; - function initialize() { - $timeout(function() { - var hash = $location.search(); - if(hash.signup) { - animation.showSignup(); - } else if(hash.login) { - animation.showLogin(); - } else if(hash.passwordRecovery) { - animation.showPasswordRecovery(); - } - }, 1000); + function setKit(height) { + kitHeight = height; } - function logout() { - auth.logout(); - vm.isLoggedin = false; + function getKit() { + return kitHeight; } } })(); @@ -3848,1103 +3827,1160 @@ angular.module('app.components') 'use strict'; angular.module('app.components') - .controller('LandingController', LandingController); - - LandingController.$inject = ['$timeout', 'animation', '$mdDialog', '$location', '$anchorScroll']; + .directive('horizontalScroll', horizontalScroll); - function LandingController($timeout, animation, $mdDialog, $location, $anchorScroll) { - var vm = this; + /** + * 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' + }; - vm.showStore = showStore; - vm.goToHash = goToHash; + /////////////////// - /////////////////////// - initialize(); + function link(scope, element) { - ////////////////// + element.on('scroll', function() { + // horizontal scroll position + var position = angular.element(this).scrollLeft(); + // real width of element + var scrollWidth = this.scrollWidth; + // visible width of element + var width = angular.element(this).width(); - function initialize() { - $timeout(function() { - animation.viewLoaded(); - if($location.hash()) { - $anchorScroll(); + // 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'); } - }, 500); - } - - function goToHash(hash){ - $location.hash(hash); - $anchorScroll(); - } + // 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; + } - function showStore() { - $mdDialog.show({ - hasBackdrop: true, - controller: 'StoreModalController', - templateUrl: 'app/components/store/storeModal.html', - clickOutsideToClose: true + //set opacity back to normal otherwise + angular.element('.button_scroll_left').css('opacity', '1'); + angular.element('.button_scroll_right').css('opacity', '1'); }); - } - } -})(); -(function(){ - 'use strict'; - angular.module('app.components') - .directive('kitList',kitList); + $timeout(function() { + element.trigger('scroll'); + }); - function kitList(){ - return{ - restrict:'E', - scope:{ - devices:'=devices', - actions: '=actions' - }, - controllerAs:'vm', - templateUrl:'app/components/kitList/kitList.html' - }; + angular.element($window).on('resize', function() { + $timeout(function() { + element.trigger('scroll'); + }, 1000); + }); + } } })(); (function() { - 'use strict'; + 'use strict'; - angular.module('app.components') - .controller('HomeController', HomeController); + angular.module('app.components') + .directive('hidePopup', hidePopup); - 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); - }); - } + /** + * Used on kit dashboard to hide popup with full sensor description + * + */ + + hidePopup.$inject = []; + function hidePopup() { + return { + link: link + }; - function cancel(){ - $mdDialog.cancel(); - } - } + ///////////// + function link(scope, elem) { + elem.on('mouseleave', function() { + angular.element('.sensor_description_preview').show(); + angular.element('.sensor_description_full').hide(); + }); + } + } })(); -(function(){ -'use strict'; +(function() { + 'use strict'; -angular.module('app.components') - .directive('cookiesLaw', cookiesLaw); + angular.module('app.components') + .directive('disableScroll', disableScroll); + disableScroll.$inject = ['$timeout']; + function disableScroll($timeout) { + return { + // link: { + // pre: link + // }, + compile: link, + restrict: 'A', + priority: 100000 + }; -cookiesLaw.$inject = ['$cookies']; -function cookiesLaw($cookies) { - return { - template: - '
' + - 'This site uses cookies to offer you a better experience. ' + - ' Accept or' + - ' Learn More. ' + - '
', - controller: function($scope) { + ////////////////////// - var init = function(){ - $scope.isCookieValid(); + 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'); + }); + }); } + } +})(); - // Helpers to debug - // You can also use `document.cookie` in the browser dev console. - //console.log($cookies.getAll()); +(function() { + 'use strict'; - $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') - } + angular.module('app.components') + .factory('animation', animation); - $scope.acceptCookie = function() { - //console.log('Accepting cookie...'); - var today = new Date(); - var expireDate = new Date(today); - expireDate.setMonth(today.getMonth() + 6); + /** + * Used to emit events from rootscope. + * + * This events are then listened by $scope on controllers and directives that care about that particular event + */ - $cookies.put('consent', true, {'expires' : expireDate.toUTCString()} ); + animation.$inject = ['$rootScope']; + function animation($rootScope) { - // Trigger the check again, after we click - $scope.isCookieValid(); - }; + 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; - init(); + ////////////// + 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'); + } } - }; -} - - })(); (function() { 'use strict'; - angular.module('app.components') - .directive('chart', chart); + /** + * TODO: Improvement These directives can be split up each one in a different file + */ - 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; + 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); + + /** + * It moves down kit section to ease the transition after the kit menu is sticked to the top + * + */ + moveDown.$inject = []; + function moveDown() { + + function link(scope, element) { + scope.$watch('moveDown', function(isTrue) { + if(isTrue) { + element.addClass('move_down'); + } else { + element.removeClass('move_down'); + } + }); + } return { link: link, - restrict: 'A', - scope: { - chartData: '=' - } + scope: false, + restrict: 'A' }; + } - function link(scope, elem) { - - $timeout(function() { - createChart(elem[0]); - }, 0); - - 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}); - }); + /** + * 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(); - scope.$watch('chartData', function(newData) { - if(!newData) { - return; - } + $timeout(function() { + elementPosition = element[0].offsetTop; + //var elementHeight = element[0].offsetHeight; + navbarHeight = angular.element('.stickNav').height(); + }, 1000); - if(newData !== undefined) { - // if there's data for 2 sensors - if(newData[0] && newData[1]) { - var sensorDataMain = newData[0].data; - // we could get some performance from saving the map in the showKit controller on line 218 and putting that logic in here - var dataMain = sensorDataMain.map(function(dataPoint) { - return { - date: dateFormat(dataPoint.time), - count: dataPoint && dataPoint.count, - value: dataPoint && dataPoint.value - }; - }); - // sort data points by date - dataMain.sort(function(a, b) { - return a.date - b.date; - }); - var sensorDataCompare = newData[1].data; - var dataCompare = sensorDataCompare.map(function(dataPoint) { - return { - date: dateFormat(dataPoint.time), - count: dataPoint && dataPoint.count, - value: dataPoint && dataPoint.value - }; - }); + angular.element($window).on('scroll', function() { + var windowPosition = document.body.scrollTop; - dataCompare.sort(function(a, b) { - return a.date - b.date; - }); + //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; + }); + } + }); + } - 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]) { + return { + link: link, + scope: false, + restrict: 'A' + }; + } - 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 - }; - }); + /** + * Unused directive. Double-check is not being used before removing it + * + */ - data.sort(function(a, b) { - return a.date - b.date; - }); + function blur() { - var color = newData[0].color; - var unit = newData[0].unit; + function link(scope, element) { - lastData = { - data: data, - type: 'main', - color: color, - unit: unit - }; + scope.$on('blur', function() { + element.addClass('blur'); + }); - updateChartData(data, {type: 'main', container: elem[0], color: color, unit: unit }); - } - animation.hideChartSpinner(); - } + scope.$on('unblur', function() { + element.removeClass('blur'); }); } - // creates the container that is re-used across different sensor charts - function createChart(elem) { - d3.select(elem).selectAll('*').remove(); + return { + link: link, + scope: false, + restrict: 'A' + }; + } - margin = {top: 20, right: 12, bottom: 20, left: 42}; - width = elem.clientWidth - margin.left - margin.right; - height = elem.clientHeight - margin.top - margin.bottom; + /** + * 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(); + }); - 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]); + element.on('focusout', function() { + animation.addNav(); + }); - 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 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'); + }); - xAxis = d3.svg.axis() - .scale(xScale) - .orient('bottom') - .ticks(5); + searchInput.on('focus', function() { + angular.element(document.body).css('overflow', 'hidden'); + }); + } - yAxisLeft = d3.svg.axis() - .scale(yScale0) - .orient('left') - .ticks(5); + return { + link: link + }; + } - yAxisRight = d3.svg.axis() - .scale(yScale1) - .orient('right') - .ticks(5); + /** + * Changes map section based on screen size + * + */ + changeMapHeight.$inject = ['$document', 'layout', '$timeout']; + function changeMapHeight($document, layout, $timeout) { + function link(scope, element) { - 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); }); + var screenHeight = $document[0].body.clientHeight; + var navbarHeight = angular.element('.stickNav').height(); - 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); }); + // var overviewHeight = angular.element('.kit_overview').height(); + // var menuHeight = angular.element('.kit_menu').height(); + // var chartHeight = angular.element('.kit_chart').height(); - 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 resizeMap(){ + $timeout(function() { + var overviewHeight = angular.element('.over_map').height(); - 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); }); + var objectsHeight = navbarHeight + overviewHeight; + var objectsHeightPercentage = parseInt((objectsHeight * 100) / screenHeight); + var mapHeightPercentage = 100 - objectsHeightPercentage; - 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); + element.css('height', mapHeightPercentage + '%'); + + var aboveTheFoldHeight = screenHeight - overviewHeight; + angular + .element('section[change-content-margin]') + .css('margin-top', aboveTheFoldHeight + 'px'); + }); } - } - // 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(); + resizeMap(); - //Add the area path - svg.append('path') - .datum(data) - .attr('class', 'chart_area') - .attr('fill', options.color) - .attr('d', areaMain); + scope.element = element; - // Add the valueline path. - svg.append('path') - .attr('class', 'chart_line') - .attr('stroke', options.color) - .attr('d', valueLineMain(data)); - - // Add the X Axis - svg.append('g') - .attr('class', 'axis x') - .attr('transform', 'translate(0,' + height + ')') - .call(xAxis); - - // Add the Y Axis - svg.append('g') - .attr('class', 'axis y_left') - .call(yAxisLeft); + scope.$on('resizeMapHeight',function(){ + resizeMap(); + }); - // 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('') - ); + return { + link: link, + scope: true, + restrict: 'A' + }; + } - focusMain = svg.append('g') - .attr('class', 'focus') - .style('display', 'none'); + /** + * Changes margin on kit section based on above-the-fold space left after map section is resize + */ - focusMain.append('circle') - .style('stroke', options.color) - .attr('r', 4.5); + changeContentMargin.$inject = ['layout', '$timeout', '$document']; + function changeContentMargin(layout, $timeout, $document) { + function link(scope, element) { + var screenHeight = $document[0].body.clientHeight; - var popupWidth = 84; - var popupHeight = 46; + var overviewHeight = angular.element('.over_map').height(); - popup = svg.append('g') - .attr('class', 'focus') - .style('display', 'none'); + var aboveTheFoldHeight = screenHeight - overviewHeight; + element.css('margin-top', aboveTheFoldHeight + 'px'); + } - popupContainer = popup.append('rect') - .attr('width', popupWidth) - .attr('height', popupHeight) - .attr('transform', function() { - var result = 'translate(-42, 5)'; + return { + link: link + }; + } - return result; - }) - .style('stroke', 'grey') - .style('stroke-width', '0.5') - .style('fill', 'white'); + /** + * 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 + }; + } +})(); - var text = popup.append('text') - .attr('class', ''); +(function() { + 'use strict'; - 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); + angular.module('app.components') + .directive('activeButton', activeButton); - textMain.append('tspan') - .attr('class', 'popup_value'); + /** + * Used to highlight and unhighlight buttons on kit menu + * + * It attaches click handlers dynamically + */ - textMain.append('tspan') - .attr('class', 'popup_unit') - .attr('dx', 5); + activeButton.$inject = ['$timeout', '$window']; + function activeButton($timeout, $window) { + return { + link: link, + restrict: 'A' - 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' ); + }; - svg.append('rect') - .attr('class', 'overlay') - .attr('width', width) - .attr('height', height) - .on('mouseover', function() { - popup.style('display', null); - focusMain.style('display', null); - }) - .on('mouseout', function() { - popup.style('display', 'none'); - focusMain.style('display', 'none'); - }) - .on('mousemove', mousemove); + //////////////////////////// + function link(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'); - function mousemove() { - var bisectDate = d3.bisector(function(d) { return d.date; }).left; + 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); - 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 scrollTo(offset) { + if(!container) { + return; + } + angular.element($window).scrollTop(offset - container.navbar.height - container.kitMenu.height); + } - focusMain.attr('transform', 'translate(' + xScale(d.date) + ', ' + yScale0(d.count) + ')'); - var popupText = popup.select('text'); - var textMain = popupText.select('.popup_main'); - var valueMain = textMain.select('.popup_value').text(parseValue(d.value)); - var unitMain = textMain.select('.popup_unit').text(options.unit); - var date = popupText.select('.popup_date').text(parseTime(d.date)); + function getButton(buttonOrder) { + return childrens[buttonOrder]; + } - var textContainers = [ - textMain, - date - ]; + 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 popupWidth = resizePopup(popupContainer, textContainers); + var strokeContainer = activeButton.find('.stroke_container'); + strokeContainer.css('stroke', 'none'); + strokeContainer.css('stroke-width', '1'); - 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) + ')'); + var fillContainer = strokeContainer.find('.fill_container'); + fillContainer.css('fill', '#FF8600'); } } - } - - // 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]); - svg.selectAll('*').remove(); + function highlightButton(button) { + var clickedButton = angular.element(button); + //add border, fill and stroke to every icon + clickedButton.addClass('button_active'); - //Add both area paths - svg.append('path') - .datum(data[0]) - .attr('class', 'chart_area') - .attr('fill', options.color[0]) - .attr('d', areaMain); + var strokeContainer = clickedButton.find('.stroke_container'); + strokeContainer.css('stroke', 'white'); + strokeContainer.css('stroke-width', '0.01px'); - svg.append('path') - .datum(data[1]) - .attr('class', 'chart_area') - .attr('fill', options.color[1]) - .attr('d', areaCompare); + var fillContainer = strokeContainer.find('.fill_container'); + fillContainer.css('fill', 'white'); + } - // 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])); + //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'); + } + } + }); + }); - // Add the X Axis - svg.append('g') - .attr('class', 'axis x') - .attr('transform', 'translate(0,' + height + ')') - .call(xAxis); + var currentSection; - // Add both Y Axis - svg.append('g') - .attr('class', 'axis y_left') - .call(yAxisLeft); + //on scroll, check if window is on a section + angular.element($window).on('scroll', function() { + if(!container){ return; } - svg.append('g') - .attr('class', 'axis y_right') - .attr('transform', 'translate(' + width + ' ,0)') - .call(yAxisRight); + 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'; + } + }); + } + } +})(); - // Draw the x Grid lines - svg.append('g') - .attr('class', 'grid') - .attr('transform', 'translate(0,' + height + ')') - .call(xGrid() - .tickSize(-height, 0, 0) - .tickFormat('') - ); +(function() { + 'use strict'; - // Draw the y Grid lines - svg.append('g') - .attr('class', 'grid') - .call(yGrid() - .tickSize(-width, 0, 0) - .tickFormat('') - ); + angular.module('app.components') + .controller('UserProfileController', UserProfileController); - focusCompare = svg.append('g') - .attr('class', 'focus') - .style('display', 'none'); + 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) { - focusMain = svg.append('g') - .attr('class', 'focus') - .style('display', 'none'); + var vm = this; + var userID = parseInt($stateParams.id); - focusCompare.append('circle') - .style('stroke', options.color[1]) - .attr('r', 4.5); + vm.status = undefined; + vm.user = {}; + vm.devices = []; + vm.filteredDevices = []; + vm.filterDevices = filterDevices; - focusMain.append('circle') - .style('stroke', options.color[0]) - .attr('r', 4.5); + $scope.$on('loggedIn', function() { + var authUser = auth.getCurrentUser().data; + if( userUtils.isAuthUser(userID, authUser) ) { + $location.path('/profile'); + } + }); - var popupWidth = 84; - var popupHeight = 75; + initialize(); - 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 initialize() { - return result; - }) - .style('stroke', 'grey') - .style('stroke-width', '0.5') - .style('fill', 'white'); + user.getUser(userID) + .then(function(user) { + vm.user = new NonAuthUser(user); - popup.append('rect') - .attr('width', 8) - .attr('height', 2) - .attr('transform', function() { - return 'translate(' + (-popupWidth / 2 + 4).toString() + ', 20)'; - }) - .style('fill', options.color[0]); + if(!vm.user.devices.length) { + return []; + } - popup.append('rect') - .attr('width', 8) - .attr('height', 2) - .attr('transform', function() { - return 'translate(' + (-popupWidth / 2 + 4).toString() + ', 45)'; - }) - .style('fill', options.color[1]); + $q.all(vm.devices = vm.user.devices.map(function(data){ + return new PreviewDevice(data); + })) - var text = popup.append('text') - .attr('class', ''); + }).then(function(error) { + if(error && error.status === 404) { + $location.url('/404'); + } + }); - 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); + $timeout(function() { + setSidebarMinHeight(); + animation.viewLoaded(); + }, 500); + } - textMain.append('tspan') - .attr('class', 'popup_value') - .attr( 'text-anchor', 'start' ); + function filterDevices(status) { + if(status === 'all') { + status = undefined; + } + vm.status = status; + } - textMain.append('tspan') - .attr('class', 'popup_unit') - .attr('dx', 5); + function setSidebarMinHeight() { + var height = document.body.clientHeight / 4 * 3; + angular.element('.profile_content').css('min-height', height + 'px'); + } + } +})(); - 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); +(function() { + 'use strict'; - textCompare.append('tspan') - .attr('class', 'popup_value') - .attr( 'text-anchor', 'start' ); + angular.module('app.components') + .controller('UploadController', UploadController); - textCompare.append('tspan') - .attr('class', 'popup_unit') - .attr('dx', 5); + UploadController.$inject = ['kit', '$state', '$stateParams', 'animation']; + function UploadController(kit, $state, $stateParams, animation) { + var vm = this; - 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' ); + vm.kit = kit; - 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); + vm.backToProfile = backToProfile; - function mousemove() { - var bisectDate = d3.bisector(function(d) { return d.date; }).left; + initialize(); - 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 initialize() { + animation.viewLoaded(); + } - 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) + ')'); - - 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)); + function backToProfile() { + $state.transitionTo('layout.myProfile.kits', $stateParams, + { reload: false, + inherit: false, + notify: true + }); + } + } +})(); - var textContainers = [ - textMain, - textCompare, - date - ]; +(function(){ +'use strict'; - 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) + ')'); - } - } - } - function xGrid() { - return d3.svg.axis() - .scale(xScale) - .orient('bottom') - .ticks(5); - } +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 + }; + }) + }; +} - 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'); - } +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 resizePopup(popupContainer, textContainers) { - if(!textContainers.length) { - return; - } + }); + }) + ).then(() => { + vm.loadingStatus = false; + }).catch(() => { + vm.loadingStatus = false; + }); + } - var widestElem = textContainers.reduce(function(widestElemSoFar, textContainer) { - var currentTextContainerWidth = getContainerWidth(textContainer); - var prevTextContainerWidth = getContainerWidth(widestElemSoFar); - return prevTextContainerWidth >= currentTextContainerWidth ? widestElemSoFar : textContainer; - }, textContainers[0]); + vm.haveSelectedFiles = function() { + return vm.csvFiles && vm.csvFiles.some((file) => file.checked); + }; - var margins = widestElem.attr('dx') * 2; + vm.haveSelectedNoFiles = function() { + return vm.csvFiles && !vm.csvFiles.some((file) => file.checked); + }; - popupContainer - .attr('width', getContainerWidth(widestElem) + margins); + vm.haveSelectedAllFiles = function() { + return vm.csvFiles && vm.csvFiles.every((file) => file.checked); + }; - 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; - } + 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; + }; -})(); + vm.selectAll = function(value) { + vm.csvFiles.forEach((file) => { file.checked = value }); + }; -(function(){ - 'use strict'; + 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) + }); + }; - angular.module('app.components') - .directive('apiKey', apiKey); + 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 apiKey(){ - return { - scope: { - apiKey: '=apiKey' - }, - restrict: 'A', - controller: 'ApiKeyController', - controllerAs: 'vm', - templateUrl: 'app/components/apiKey/apiKey.html' - }; + 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(){ - 'use strict'; - angular.module('app.components') - .controller('ApiKeyController', ApiKeyController); + vm.uploadData = function() { + vm.loadingStatus = true; + vm.loadingType = 'indeterminate'; + vm.loadingProgress = 0; + let count = 0; - ApiKeyController.$inject = ['alert']; - function ApiKeyController(alert){ - var vm = this; - - vm.copied = copied; - vm.copyFail = copyFail; - - /////////////// - - function copied(){ - alert.success('API key copied to your clipboard.'); - } + $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; + }); + } +}; - function copyFail(err){ - console.log('Copy error: ', err); - alert.error('Oops! An error occurred copying the api key.'); - } - } +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') - .factory('alert', alert); + .controller('tagsController', tagsController); - 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 - } - }; + tagsController.$inject = ['tag', '$scope', 'device', '$state', '$q', + 'PreviewDevice', 'animation' + ]; - return service; + function tagsController(tag, $scope, device, $state, $q, PreviewDevice, + animation) { - /////////////////// + var vm = this; - function success(message) { - toast('success', message); - } + vm.selectedTags = tag.getSelectedTags(); + vm.markers = []; + vm.kits = []; + vm.percActive = 0; - function error(message) { - toast('error', message); - } + initialize(); - function infoNoDataVisitor() { - info('Woah! We couldn\'t locate this kit on the map because it hasn\'t published any data. Leave a ' + - 'comment to let its owner know.', - 10000, - { - button: 'Leave comment', - href: 'https://forum.smartcitizen.me/' - }); - } + ///////////////////////////////////////////////////////// - function infoNoDataOwner() { - info('Woah! We couldn\'t locate this kit on the map because it hasn\'t published any data.', - 10000); - } + function initialize() { + if(vm.selectedTags.length === 0){ + $state.transitionTo('layout.home.kit'); + } - 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/' + 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(); }); + } + } - // TODO: Refactor, check why this was removed - // function infoDataInvalid() { - // info('Device not found, or it has been set to private.', - // 10000); - // } + function updateSelectedTags(){ - 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/' - }); - } + vm.markers = tag.filterMarkersByTag(device.getWorldMarkers()); - function info(message, delay, options) { - if(options && options.button) { - toast('infoButton', message, options, undefined, delay); + var onlineMarkers = _.filter(vm.markers, isOnline); + if (vm.markers.length === 0) { + vm.percActive = 0; } else { - toast('info', message, options, undefined, delay); + vm.percActive = Math.floor(onlineMarkers.length / vm.markers.length * + 100); } - } - function toast(type, message, options, position, delay) { - position = position === undefined ? 'top': position; - delay = delay === undefined ? 5000 : delay; + animation.viewLoaded(); - $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 - } - }); + getTaggedDevices() + .then(function(res){ + vm.kits = res; + }); } - } -})(); -(function() { - 'use strict'; - angular.module('app.components') - .controller('AlertController', AlertController); + function isOnline(marker) { + return _.includes(marker.myData.labels, 'online'); + } - AlertController.$inject = ['$scope', '$mdToast', 'message', 'button', 'href']; - function AlertController($scope, $mdToast, message, button, href) { - var vm = this; + function descLastUpdate(o) { + return -new Date(o.last_reading_at).getTime(); + } - vm.close = close; - vm.message = message; - vm.button = button; - vm.href = href; + function getTaggedDevices() { - // hideAlert will be triggered on state change - $scope.$on('hideAlert', function() { - close(); - }); + var deviceProm = _.map(vm.markers, getMarkerDevice); - /////////////////// + return $q.all(deviceProm) + .then(function(devices) { + return _.map(_.sortBy(devices, descLastUpdate), toPreviewDevice); // This sort is temp + }); + } - function close() { - $mdToast.hide(); - } + function toPreviewDevice(dev) { + return new PreviewDevice(dev); + } + + function getMarkerDevice(marker) { + return device.getDevice(marker.myData.id); } + } + })(); -(function() { +(function(){ 'use strict'; - angular.module('app.components') - .factory('userUtils', userUtils); - - function userUtils() { - var service = { - isAdmin: isAdmin, - isAuthUser: isAuthUser - }; - return service; + .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'); - function isAdmin(userData) { - return userData.role === 'admin'; - } - function isAuthUser(userID, authUserData) { - return userID === authUserData.id; + if(typeof(attrs.clickable) !== 'undefined'){ + element.bind('click', scope.openTag); + } } - } + }; + } })(); (function() { 'use strict'; angular.module('app.components') - .factory('timeUtils', timeUtils); + .controller('StoreModalController', StoreModalController); - 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; + StoreModalController.$inject = ['$scope', '$mdDialog']; + function StoreModalController($scope, $mdDialog) { - //////////// + $scope.cancel = function() { + $mdDialog.hide(); + }; + } +})(); - function getDateIn(timeMS, format) { - if(!format) { - return timeMS; - } +(function() { + 'use strict'; - 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; - } + angular.module('app.components') + .directive('store', store); - function convertTime(time) { - return moment(time).toISOString(); + function store() { + return { + scope: { + isLoggedin: '=logged' + }, + restrict: 'A', + controller: 'StoreController', + controllerAs: 'vm', + templateUrl: 'app/components/store/store.html' + }; } +})(); - function formatDate(time) { - return moment(time).format('YYYY-MM-DDTHH:mm:ss'); - } +(function() { + 'use strict'; - function getSecondsFromDate(date) { - return (new Date(date)).getTime(); - } + angular.module('app.components') + .controller('StoreController', StoreController); - function getMillisFromDate(date) { - return (new Date(date)).getTime(); - } + StoreController.$inject = ['$scope', '$mdDialog']; + function StoreController($scope, $mdDialog) { - function getCurrentRange(fromDate, toDate) { - return moment(toDate).diff(moment(fromDate), 'days'); - } + $scope.showStore = showStore; - function getToday() { - return (new Date()).getTime(); - } + $scope.$on('showStore', function() { + showStore(); + }); + + //////////////// - function getSevenDaysAgo() { - return getSecondsFromDate( getToday() - (7 * 24 * 60 * 60 * 1000) ); + function showStore() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'StoreModalController', + templateUrl: 'app/components/store/storeModal.html', + clickOutsideToClose: true + }); } - function getHourBefore(date) { - var now = moment(date); - return now.subtract(1, 'hour').valueOf(); - } + } +})(); - function isSameDay(day1, day2) { - day1 = moment(day1); - day2 = moment(day2); +(function() { + 'use strict'; - if(day1.startOf('day').isSame(day2.startOf('day'))) { - return true; - } - return false; - } + angular.module('app.components') + .controller('StaticController', StaticController); - function isDiffMoreThan15min(dateToCheckFrom, dateToCheckTo) { - var duration = moment.duration(moment(dateToCheckTo).diff(moment(dateToCheckFrom))); - return duration.as('minutes') > 15; - } + StaticController.$inject = ['$timeout', 'animation', '$mdDialog', '$location', '$anchorScroll']; - function isWithin15min(dateToCheck) { - var fifteenMinAgo = moment().subtract(15, 'minutes').valueOf(); - dateToCheck = moment(dateToCheck).valueOf(); + function StaticController($timeout, animation, $mdDialog, $location, $anchorScroll) { + var vm = this; - return dateToCheck > fifteenMinAgo; - } + vm.showStore = showStore; - function isWithin1Month(dateToCheck) { - var oneMonthAgo = moment().subtract(1, 'months').valueOf(); - dateToCheck = moment(dateToCheck).valueOf(); + $anchorScroll.yOffset = 80; - return dateToCheck > oneMonthAgo; - } + /////////////////////// - function isWithin(number, type, dateToCheck) { - var ago = moment().subtract(number, type).valueOf(); - dateToCheck = moment(dateToCheck).valueOf(); + initialize(); - return dateToCheck > ago; + ////////////////// + + function initialize() { + $timeout(function() { + animation.viewLoaded(); + if($location.hash()){ + $anchorScroll(); + } + }, 500); } - 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 showStore() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'StoreModalController', + templateUrl: 'app/components/store/storeModal.html', + clickOutsideToClose: true + }); } } })(); @@ -4953,211 +4989,226 @@ function cookiesLaw($cookies) { 'use strict'; angular.module('app.components') - .factory('sensorUtils', sensorUtils); + .controller('SignupModalController', SignupModalController); - sensorUtils.$inject = ['timeUtils']; - function sensorUtils(timeUtils) { - var service = { - getRollup: getRollup, - getSensorName: getSensorName, - getSensorValue: getSensorValue, - getSensorPrevValue: getSensorPrevValue, - getSensorIcon: getSensorIcon, - getSensorArrow: getSensorArrow, - getSensorColor: getSensorColor, - getSensorDescription: getSensorDescription + SignupModalController.$inject = ['$scope', '$mdDialog', 'user', + 'alert', 'animation']; + function SignupModalController($scope, $mdDialog, user, + alert, animation ) { + var vm = this; + vm.answer = function(signupForm) { + + if (!signupForm.$valid){ + return; + } + + $scope.waitingFromServer = true; + user.createUser(vm.user) + .then(function() { + alert.success('Signup was successful'); + $mdDialog.hide(); + }).catch(function(err) { + alert.error('Signup failed'); + $scope.errors = err.data.errors; + }) + .finally(function() { + $scope.waitingFromServer = false; + }); + }; + $scope.hide = function() { + $mdDialog.hide(); + }; + $scope.cancel = function() { + $mdDialog.cancel(); }; - return service; - /////////////// + $scope.openLogin = function() { + animation.showLogin(); + $mdDialog.hide(); + }; + } +})(); - function getRollup(dateFrom, dateTo) { +(function() { + 'use strict'; - // 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; + angular.module('app.components') + .directive('signup', signup); - var rollup = parseInt(durationInSec / chartWidth) + 's'; + function signup() { + return { + scope: { + show: '=', + }, + restrict: 'A', + controller: 'SignupController', + controllerAs: 'vm', + templateUrl: 'app/components/signup/signup.html' + }; + } +})(); - /* - //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() { + 'use strict'; - function getSensorName(name) { + angular.module('app.components') + .controller('SignupController', SignupController); - 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 getSensorValue(sensor) { - var value = sensor.value; + SignupController.$inject = ['$scope', '$mdDialog']; + function SignupController($scope, $mdDialog) { + var vm = this; - if(isNaN(parseInt(value))) { - value = 'NA'; - } else { - value = round(value, 1).toString(); - } + vm.showSignup = showSignup; - return value; - } + $scope.$on('showSignup', function() { + showSignup(); + }); + //////////////////////// - function round(value, precision) { - var multiplier = Math.pow(10, precision || 0); - return Math.round(value * multiplier) / multiplier; - } - function getSensorPrevValue(sensor) { - /*jshint camelcase: false */ - var prevValue = sensor.prev_value; - return (prevValue && prevValue.toString() ) || 0; + function showSignup() { + $mdDialog.show({ + fullscreen: true, + hasBackdrop: true, + controller: 'SignupModalController', + controllerAs: 'vm', + templateUrl: 'app/components/signup/signupModal.html', + clickOutsideToClose: true + }); } + } +})(); - function getSensorIcon(sensorName) { - - var thisName = getSensorName(sensorName); - - switch(thisName) { - case 'TEMPERATURE': - return './assets/images/temperature_icon_new.svg'; - - case 'HUMIDITY': - return './assets/images/humidity_icon_new.svg'; +(function() { +'use strict'; - case 'LIGHT': - return './assets/images/light_icon_new.svg'; - case 'SOUND': - return './assets/images/sound_icon_new.svg'; + angular.module('app.components') + .directive('search', search); - case 'CO': - return './assets/images/co_icon_new.svg'; + function search() { + return { + scope: true, + restrict: 'E', + templateUrl: 'app/components/search/search.html', + controller: 'SearchController', + controllerAs: 'vm' + }; + } +})(); - case 'NO2': - return './assets/images/no2_icon_new.svg'; +(function() { + 'use strict'; - case 'NETWORKS': - return './assets/images/networks_icon.svg'; + angular.module('app.components') + .controller('SearchController', SearchController); - case 'BATTERY': - return './assets/images/battery_icon.svg'; + SearchController.$inject = ['$scope', 'search', 'SearchResult', '$location', 'animation', 'SearchResultLocation']; + function SearchController($scope, search, SearchResult, $location, animation, SearchResultLocation) { + var vm = this; - case 'SOLAR PANEL': - return './assets/images/solar_panel_icon.svg'; + vm.searchTextChange = searchTextChange; + vm.selectedItemChange = selectedItemChange; + vm.querySearch = querySearch; - case 'BAROMETRIC PRESSURE': - return './assets/images/pressure_icon_new.svg'; + /////////////////// - case 'PM 1': - case 'PM 2.5': - case 'PM 10': - return './assets/images/particle_icon_new.svg'; + function searchTextChange() { + } - default: - return './assets/images/unknownsensor_icon.svg'; + function selectedItemChange(result) { + if (!result) { return; } + if(result.type === 'User') { + $location.path('/users/' + result.id); + } else if(result.type === 'Device') { + $location.path('/kits/' + result.id); + } else if (result.type === 'City'){ + animation.goToLocation({lat: result.lat, lng: result.lng, type: result.type, layer: result.layer}); } } - function 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'; + function querySearch(query) { + if(query.length < 3) { + return []; } - } - function getSensorColor(sensorName) { - switch(getSensorName(sensorName)) { - case 'TEMPERATURE': - return '#FF3D4C'; + return search.globalSearch(query) + .then(function(data) { - case 'HUMIDITY': - return '#55C4F5'; + return data.map(function(object) { - case 'LIGHT': - return '#ffc107'; + if(object.type === 'City' || object.type === 'Country') { + return new SearchResultLocation(object); + } else { + return new SearchResult(object); + } + }); + }); + } + } +})(); - case 'SOUND': - return '#0019FF'; +(function() { + 'use strict'; - case 'CO': - return '#00A103'; + angular.module('app.components') + .controller('PasswordResetController', PasswordResetController); - case 'NO2': - return '#8cc252'; + 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; - case 'NETWORKS': - return '#681EBD'; + initialize(); + /////////// - case 'SOLAR PANEL': - return '#d555ce'; + function initialize() { + $timeout(function() { + animation.viewLoaded(); + }, 500); + getUserData(); + } - case 'BATTERY': - return '#ff8601'; + function getUserData() { + auth.getResetPassword($stateParams.code) + .then(function() { + vm.showForm = true; + }) + .catch(function() { + alert.error('Wrong url'); + $location.path('/'); + }); + } - default: - return '#0019FF'; + function answer(data) { + vm.waitingFromServer = true; + vm.errors = undefined; + + if(data.newPassword === data.confirmPassword) { + vm.isDifferent = false; + } else { + vm.isDifferent = true; + return; } - } - function getSensorDescription(sensorID, sensorTypes) { - return _(sensorTypes) - .chain() - .find(function(sensorType) { - return sensorType.id === sensorID; + auth.patchResetPassword($stateParams.code, {password: data.newPassword}) + .then(function() { + alert.success('Your data was updated successfully'); + $location.path('/profile'); }) - .value() - .measurement.description; + .catch(function(err) { + alert.error('Your data wasn\'t updated'); + vm.errors = err.data.errors; + }) + .finally(function() { + vm.waitingFromServer = false; + }); } } })(); @@ -5166,62 +5217,43 @@ function cookiesLaw($cookies) { 'use strict'; angular.module('app.components') - .factory('searchUtils', searchUtils); + .controller('PasswordRecoveryModalController', PasswordRecoveryModalController); + PasswordRecoveryModalController.$inject = ['$scope', 'animation', '$mdDialog', 'auth', 'alert']; + function PasswordRecoveryModalController($scope, animation, $mdDialog, auth, alert) { - searchUtils.$inject = []; - function searchUtils() { - var service = { - parseLocation: parseLocation, - parseName: parseName, - parseIcon: parseIcon, - parseIconType: parseIconType + $scope.hide = function() { + $mdDialog.hide(); + }; + $scope.cancel = function() { + $mdDialog.cancel(); }; - return service; - - ///////////////// - - function parseLocation(object) { - var location = ''; - - if(!!object.city) { - location += object.city; - } - if(!!object.city && !!object.country) { - location += ', '; - } - if(!!object.country) { - location += object.country; - } - - return location; - } - function parseName(object) { - var name = object.type === 'User' ? object.username : object.name; - return name; - } + $scope.recoverPassword = function() { + $scope.waitingFromServer = true; + var data = { + /*jshint camelcase: false */ + email_or_username: $scope.input + }; - 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'; - } - } + 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; + }); + }; - function parseIconType(type) { - switch(type) { - case 'Device': - return 'div'; - default: - return 'img'; - } - } + $scope.openSignup = function() { + animation.showSignup(); + $mdDialog.hide(); + }; } })(); @@ -5229,360 +5261,424 @@ function cookiesLaw($cookies) { 'use strict'; angular.module('app.components') - .factory('markerUtils', markerUtils); - - markerUtils.$inject = ['deviceUtils', 'MARKER_ICONS']; - function markerUtils(deviceUtils, MARKER_ICONS) { - var service = { - getIcon: getIcon, - getMarkerIcon: getMarkerIcon, - }; - _.defaults(service, deviceUtils); - return service; - - /////////////// + .controller('PasswordRecoveryController', PasswordRecoveryController); - function getIcon(object) { - var icon; - var labels = deviceUtils.parseSystemTags(object); - var isSCKHardware = deviceUtils.isSCKHardware(object); + PasswordRecoveryController.$inject = ['auth', 'alert', '$mdDialog']; + function PasswordRecoveryController(auth, alert, $mdDialog) { + var vm = this; - if(hasLabel(labels, 'offline')) { - icon = MARKER_ICONS.markerSmartCitizenOffline; - } else if (isSCKHardware) { - icon = MARKER_ICONS.markerSmartCitizenOnline; - } else { - icon = MARKER_ICONS.markerExperimentalNormal; - } - return icon; - } + vm.waitingFromServer = false; + vm.errors = undefined; + vm.recoverPassword = recoverPassword; - function hasLabel(labels, targetLabel) { - return _.some(labels, function(label) { - return label === targetLabel; - }); - } + /////////////// - function getMarkerIcon(marker, state) { - var markerType = marker.icon.className; + function recoverPassword() { + vm.waitingFromServer = true; + vm.errors = undefined; + + var data = { + username: vm.username + }; - 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; + 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'; angular.module('app.components') - .factory('mapUtils', mapUtils); + .controller('MyProfileController', MyProfileController); - mapUtils.$inject = []; - function mapUtils() { - var service = { - getDefaultFilters: getDefaultFilters, - setDefaultFilters: setDefaultFilters, - canFilterBeRemoved: canFilterBeRemoved - }; - return service; + 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) { - ////////////// + var vm = this; - function getDefaultFilters(filterData, defaultFilters) { - var obj = {}; - if(!filterData.indoor && !filterData.outdoor) { - obj[defaultFilters.exposure] = true; - } - if(!filterData.online && !filterData.offline) { - obj[defaultFilters.status] = true; - } - return obj; - } + vm.unhighlightIcon = unhighlightIcon; - function setDefaultFilters(filterData) { - var obj = {}; - if(!filterData.indoor || !filterData.outdoor) { - obj.exposure = filterData.indoor ? 'indoor' : 'outdoor'; - } - if(!filterData.online || !filterData.offline) { - obj.status = filterData.online ? 'online' : 'offline'; - } - return obj; - } + //PROFILE TAB + vm.formUser = {}; + vm.getCountries = getCountries; - 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; - } - } - } -})(); + vm.user = userData; + copyUserToForm(vm.formUser, vm.user); + vm.searchText = vm.formUser.country; -(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); - }; - }]); + 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 -(function() { - 'use strict'; + // 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; - angular.module('app.components') - .factory('deviceUtils', deviceUtils); + //KITS TAB + vm.devices = []; + vm.deviceStatus = undefined; + vm.removeDevice = removeDevice; + vm.downloadData = downloadData; - deviceUtils.$inject = ['COUNTRY_CODES', 'device']; - function deviceUtils(COUNTRY_CODES, device) { - var service = { - parseLocation: parseLocation, - parseCoordinates: parseCoordinates, - parseSystemTags: parseSystemTags, - parseUserTags: parseUserTags, - classify: classify, - parseNotifications: parseNotifications, - parseOwner: parseOwner, - parseName: parseName, - parseString: parseString, - parseHardware: parseHardware, - parseHardwareInfo: parseHardwareInfo, - parseHardwareName: parseHardwareName, - isPrivate: isPrivate, - preciseLocation: preciseLocation, - enableForwarding: enableForwarding, - isLegacyVersion: isLegacyVersion, - isSCKHardware: isSCKHardware, - parseState: parseState, - parseAvatar: parseAvatar, - belongsToUser: belongsToUser, - parseSensorTime: parseSensorTime - }; + vm.filteredDevices = []; + vm.dropdownSelected = undefined; - return service; + //SIDEBAR + vm.filterDevices = filterDevices; + vm.filterTools = filterTools; - /////////////// + vm.selectThisTab = selectThisTab; - function parseLocation(object) { - var location = ''; - var city = ''; - var country = ''; + $scope.$on('loggedOut', function() { + $location.path('/'); + }); - if (object.location) { - city = object.location.city; - country = object.location.country; - if(!!city) { - location += city; - } - if(!!city && !!location) { - location += ', ' - } - if(!!country) { - location += country; - } + $scope.$on('devicesContextUpdated', function(){ + var userData = auth.getCurrentUser().data; + if(userData){ + vm.user = userData; } - return location; - } + initialize(); + }); - function parseCoordinates(object) { - if (object.location) { - return { - lat: object.location.latitude, - lng: object.location.longitude - }; - } - // TODO: Bug - what happens if no location? - } + initialize(); - function parseSystemTags(object) { - /*jshint camelcase: false */ - return object.system_tags; - } + ////////////////// - function parseUserTags(object) { - return object.user_tags; - } + 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 parseNotifications(object){ - return { - lowBattery: object.notify.low_battery, - stopPublishing: object.notify.stopped_publishing } } - function classify(kitType) { - if(!kitType) { - return ''; + function filterDevices(status) { + if(status === 'all') { + status = undefined; } - return kitType.toLowerCase().split(' ').join('_'); + vm.deviceStatus = status; + vm.filteredDevices = $filter('filterLabel')(vm.devices, vm.deviceStatus); } - 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 filterTools(type) { + if(type === 'all') { + type = undefined; } - return object.name; + vm.toolType = type; } - function parseHardware(object) { - if (!object.hardware) { - return; + function updateUser(userData) { + if(userData.country) { + _.each(COUNTRY_CODES, function(value, key) { + if(value === userData.country) { + /*jshint camelcase: false */ + userData.country_code = key; + return; + } + }); + } else { + userData.country_code = null; } - return { - 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) - } + 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 parseString(str) { - if (typeof(str) !== 'string') { return null; } - return str; - } + 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 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 - }; + $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 parseHardwareInfo (object) { - if (!object) { return null; } // null - if (typeof(object) == 'string') { return null; } // FILTERED - - 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); + function selectThisTab(iconIndex, uistate){ + /* This looks more like a hack but we need to workout how to properly use md-tab with ui-router */ - 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 - }; - } + highlightIcon(iconIndex); - function parseHardwareName(object) { - if (object.hasOwnProperty('hardware')) { - if (!object.hardware.name) { - return 'Unknown hardware' - } - return object.hardware.name; + if ($state.current.name.includes('myProfileAdmin')){ + var transitionState = 'layout.myProfileAdmin.' + uistate; + $state.transitionTo(transitionState, {id: userData.id}); } else { - return 'Unknown hardware' + var transitionState = 'layout.myProfile.' + uistate; + $state.transitionTo(transitionState); } - } - function isPrivate(object) { - return object.data_policy.is_private; } - function preciseLocation(object) { - return object.data_policy.precise_location; - } + function startingTab() { + /* This looks more like a hack but we need to workout how to properly use md-tab with ui-router */ - function enableForwarding(object) { - return object.data_policy.enable_forwarding ; - } + var childState = $state.current.name.split('.').pop(); - 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; + switch(childState) { + case 'user': + vm.startingTab = 1; + break; + default: + vm.startingTab = 0; + break; } - } - function isSCKHardware (object){ - if (!object.hardware || !object.hardware.type || object.hardware.type != 'SCK') { - return false; - } else { - return true; - } } - 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 - }; + function highlightIcon(iconIndex) { + + var icons = angular.element('.myProfile_tab_icon'); + + _.each(icons, function(icon) { + unhighlightIcon(icon); + }); + + var icon = icons[iconIndex]; + + angular.element(icon).find('.stroke_container').css({'stroke': 'white', 'stroke-width': '0.01px'}); + angular.element(icon).find('.fill_container').css('fill', 'white'); } - function parseState(status) { - var name = parseStateName(status); - var className = classify(name); + function unhighlightIcon(icon) { + icon = angular.element(icon); - return { - name: name, - className: className + icon.find('.stroke_container').css({'stroke': 'none'}); + icon.find('.fill_container').css('fill', '#FF8600'); + } + + function setSidebarMinHeight() { + var height = document.body.clientHeight / 4 * 3; + angular.element('.profile_content').css('min-height', height + 'px'); + } + + 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 parseStateName(object) { - return object.state.replace('_', ' '); + function uploadAvatar(fileData) { + if(fileData && fileData.length) { + + // 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 parseAvatar() { - return './assets/images/sckit_avatar.jpg'; + function copyUserToForm(formData, userData) { + var props = {username: true, email: true, city: true, country: true, country_code: true, url: true, constructor: false}; + + for(var key in userData) { + if(props[key]) { + formData[key] = userData[key]; + } + } } - function parseSensorTime(sensor) { - /*jshint camelcase: false */ - return moment(sensor.recorded_at).format(''); + function mapWithBelongstoUser(devices){ + _.map(devices, addBelongProperty); } - function belongsToUser(devicesArray, deviceID) { - return _.some(devicesArray, function(device) { - return device.id === deviceID; + function addBelongProperty(device){ + device.belongProperty = deviceBelongsToUser(device); + return device; + } + + + function deviceBelongsToUser(device){ + if(!auth.isAuth() || !device || !device.id) { + return false; + } + var deviceID = parseInt(device.id); + var userData = ( auth.getCurrentUser().data ) || + ($window.localStorage.getItem('smartcitizen.data') && + new AuthUser( JSON.parse( + $window.localStorage.getItem('smartcitizen.data') ))); + + var belongsToUser = deviceUtils.belongsToUser(userData.devices, deviceID); + var isAdmin = userUtils.isAdmin(userData); + + return isAdmin || belongsToUser; + } + + 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 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.'); + }); + }); + } + + $scope.addDeviceSelector = addDeviceSelector; + function addDeviceSelector(){ + $mdDialog.show({ + templateUrl: 'app/components/myProfile/addDeviceSelectorModal.html', + clickOutsideToClose: true, + multiple: true, + controller: DialogController, + }); + } + + function DialogController($scope, $mdDialog){ + $scope.cancel = function(){ + $mdDialog.cancel(); + }; + } + + function addNewDevice() { + var confirm = $mdDialog.confirm() + .title('Hey! Do you want to add a new kit?') + .textContent('Please, notice this currently supports just the SCK 1.0 and SCK 1.1') + .ariaLabel('') + .ok('Ok') + .cancel('Cancel') + .theme('primary') + .clickOutsideToClose(true); + + $mdDialog + .show(confirm) + .then(function(){ + $state.go('layout.kitAdd'); + }); + } + + } })(); @@ -5590,1821 +5686,1725 @@ function cookiesLaw($cookies) { 'use strict'; angular.module('app.components') - .filter('filterLabel', filterLabel); + .controller('MapTagModalController', MapTagModalController); + MapTagModalController.$inject = ['$mdDialog', 'tag', 'selectedTags']; - 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 MapTagModalController($mdDialog, tag, selectedTags) { + + var vm = this; + + vm.checks = {}; + + vm.answer = answer; + vm.hide = hide; + vm.clear = clear; + vm.cancel = cancel; + vm.tags = []; + + init(); + + //////////////////////////////////////////////////////// + + function init() { + tag.getTags() + .then(function(tags) { + vm.tags = tags; + + _.forEach(selectedTags, select); + + }); } -})(); -(function() { - 'use strict'; + function answer() { - /** - * Tools links for user profile - * @constant - * @type {Array} - */ + var selectedTags = _(vm.tags) + .filter(isTagSelected) + .value(); + $mdDialog.hide(selectedTags); + } - 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' - }]); -})(); + function hide() { + answer(); + } -(function() { - 'use strict'; + function clear() { + $mdDialog.hide(null); + } - /** - * Marker icons - * @constant - * @type {Object} - */ + function cancel() { + answer(); + } - 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 isTagSelected(tag) { + return vm.checks[tag.name]; + } + + function select(tag){ + vm.checks[tag] = true; + } + } })(); (function() { 'use strict'; - /** - * Dropdown options for user - * @constant - * @type {Array} - */ angular.module('app.components') - .constant('DROPDOWN_OPTIONS_USER', [ - {divider: true, text: 'Hi,', href: './profile'}, - {text: 'My profile', href: './profile'}, - {text: 'Log out', href: './logout'} - ]); -})(); + .controller('MapFilterModalController', MapFilterModalController); -(function() { - 'use strict'; + MapFilterModalController.$inject = ['$mdDialog','selectedFilters', '$timeout']; - /** - * Dropdown options for community button - * @constant - * @type {Array} - */ + function MapFilterModalController($mdDialog, selectedFilters, $timeout) { - 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'} - ]); + var vm = this; + + vm.checks = {}; + + vm.answer = answer; + vm.hide = hide; + vm.clear = clear; + vm.cancel = cancel; + vm.toggle = toggle; + + vm.location = ['indoor', 'outdoor']; + vm.status = ['online', 'offline']; + vm.new = ['new']; + + vm.filters = []; + + init(); + + //////////////////////////////////////////////////////// + + function init() { + _.forEach(selectedFilters, select); + } + + function answer() { + vm.filters = vm.filters.concat(vm.location, vm.status, vm.new); + var selectedFilters = _(vm.filters) + .filter(isFilterSelected) + .value(); + $mdDialog.hide(selectedFilters); + } + + function hide() { + answer(); + } + + function clear() { + vm.filters = vm.filters.concat(vm.location, vm.status, vm.new); + $mdDialog.hide(vm.filters); + } + + function cancel() { + answer(); + } + + function isFilterSelected(filter) { + return vm.checks[filter]; + } + + function toggle(filters) { + $timeout(function() { + + for (var i = 0; i < filters.length - 1; i++) { + if (vm.checks[filters[i]] === false && vm.checks[filters[i]] === vm.checks[filters[i+1]]) { + for (var n = 0; n < filters.length; n++) { + vm.checks[filters[n]] = true; + } + } + } + + }); + } + + function select(filter){ + vm.checks[filter] = true; + } + } })(); (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' - }); -})(); + .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; + + 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 + }; + + 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; + } + + updateType = 'map'; + + if ($state.$current.name === 'embbed') { return; } + $state.go('layout.home.kit', {id: id}); + + // angular.element('section.map').scope().$broadcast('resizeMapHeight'); + }); + + + $scope.$on('leafletDirectiveMarker.popupclose', function() { + if(focusedMarkerID) { + var marker = vm.markers[focusedMarkerID]; + if(marker) { + vm.markers[focusedMarkerID].focus = false; + } + } + }); + + 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); + + $scope.$on('goToLocation', function(event, data) { + goToLocation(data); + }); + + vm.filters = ['indoor', 'outdoor', 'online', 'offline']; + + vm.openFilterPopup = openFilterPopup; + vm.openTagPopup = openTagPopup; + vm.removeFilter = removeFilter; + vm.removeTag = removeTag; + vm.selectedTags = tag.getSelectedTags(); + vm.selectedFilters = ['indoor', 'outdoor', 'online', 'offline', 'new']; + + vm.checkAllFiltersSelected = checkAllFiltersSelected; -(function() { - 'use strict'; + initialize(); - angular.module('app.components') - .factory('user', user); + ///////////////////// - user.$inject = ['Restangular']; - function user(Restangular) { - var service = { - createUser: createUser, - getUser: getUser, - updateUser: updateUser - }; - return service; + function initialize() { - //////////////////// + vm.readyForDevice.map = false; - function createUser(signupData) { - return Restangular.all('users').post(signupData); + $q.all([device.getAllDevices($stateParams.reloadMap)]) + .then(function(data){ + + data = data[0]; + + 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 markersByIndex = _.keyBy(vm.markers, function(marker) { + return marker.myData.id; + }); + + if($state.params.id && markersByIndex[parseInt($state.params.id)]){ + focusedMarkerID = markersByIndex[parseInt($state.params.id)] + .myData.id; + vm.readyForDevice.map = true; + } else { + updateMarkers(); + vm.readyForDevice.map = true; + } + + }); } - function getUser(id) { - return Restangular.one('users', id).get(); + function zoomDeviceAndPopUp(data){ + + if(updateType === 'map') { + vm.deviceLoading = false; + updateType = undefined; + return; + } else { + vm.deviceLoading = true; + } + + 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(); + }); + } + }); + }); + } - function updateUser(updateData) { - return Restangular.all('me').customPUT(updateData); + function checkAllFiltersSelected() { + var allFiltersSelected = _.every(vm.filters, function(filterValue) { + return _.includes(vm.selectedFilters, filterValue); + }); + return allFiltersSelected; } - } -})(); -(function() { - 'use strict'; + 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(); + }); + } - angular.module('app.components') - .factory('tag', tag); + 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(); + } + }); + } - tag.$inject = ['Restangular']; - function tag(Restangular) { - var tags = []; - var selectedTags = []; + function updateMapFilters(){ + vm.selectedTags = tag.getSelectedTags(); + checkAllFiltersSelected(); + updateMarkers(); + } - var service = { - getTags: getTags, - getSelectedTags: getSelectedTags, - setSelectedTags: setSelectedTags, - tagWithName: tagWithName, - filterMarkersByTag: filterMarkersByTag - }; + function removeFilter(filterName) { + vm.selectedFilters = _.filter(vm.selectedFilters, function(el){ + return el !== filterName; + }); + if(vm.selectedFilters.length === 0){ + vm.selectedFilters = vm.filters; + } + updateMarkers(); + } - return service; + function filterMarkersByLabel(tmpMarkers) { + return tmpMarkers.filter(function(marker) { + var labels = marker.myData.labels; + if (labels.length === 0 && vm.selectedFilters.length !== 0){ + return false; + } + return _.every(labels, function(label) { + return _.includes(vm.selectedFilters, label); + }); + }); + } - ///////////////// + function updateMarkers() { + $timeout(function() { + $scope.$apply(function() { + var allMarkers = device.getWorldMarkers(); - function getTags() { - return Restangular.all('tags') - .getList({'per_page': 200}) - .then(function(fetchedTags){ - tags = fetchedTags.plain(); - return tags; + var updatedMarkers = allMarkers; + + updatedMarkers = tag.filterMarkersByTag(updatedMarkers); + updatedMarkers = filterMarkersByLabel(updatedMarkers); + vm.markers = updatedMarkers; + + animation.mapStateLoaded(); + + vm.deviceLoading = false; + + zoomOnMarkers(); }); + }); } - function getSelectedTags(){ - return selectedTags; - } - - function setSelectedTags(tags){ - selectedTags = tags; - } + function getZoomLevel(data) { + // data.layer is an array of strings like ["establishment", "point_of_interest"] + var zoom = 18; - function tagWithName(name){ - var result = _.where(tags, {name: name}); - if (result && result.length > 0){ - return result[0]; - }else{ - return; + if(data.layer && data.layer[0]) { + switch(data.layer[0]) { + case 'point_of_interest': + zoom = 18; + break; + case 'address': + zoom = 18; + break; + case "establishment": + zoom = 15; + break; + case 'neighbourhood': + zoom = 13; + break; + case 'locality': + zoom = 13; + break; + case 'localadmin': + zoom = 9; + break; + case 'county': + zoom = 9; + break; + case 'region': + zoom = 8; + break; + case 'country': + zoom = 7; + break; + case 'coarse': + zoom = 7; + break; + } } + + return zoom; } - function filterMarkersByTag(tmpMarkers) { - var markers = filterMarkers(tmpMarkers); - return markers; + 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 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 goToLocation(data){ + // This ensures the action runs after the event is registered + $timeout(function() { + vm.center.lat = data.lat; + vm.center.lng = data.lng; + vm.center.zoom = getZoomLevel(data); }); } - } -})(); - -(function() { - '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); - }); + function removeTag(tagName){ + tag.setSelectedTags(_.filter(vm.selectedTags, function(el){ + return el !== tagName; + })); - var service = { - callAPI: callAPI, - setTypes: setTypes, - getTypes: getTypes, - getSensorsData: getSensorsData - }; - return service; + vm.selectedTags = tag.getSelectedTags(); - //////////////// + if(vm.selectedTags.length === 0){ + reloadNoTags(); + } else { + reloadWithTags(); + } - function callAPI() { - return Restangular.all('sensors').getList({'per_page': 1000}); } - function setTypes(sensorTypes) { - sensorTypes = sensorTypes; + 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 getTypes() { - return sensorTypes; + function reloadWithTags(){ + $state.transitionTo('layout.home.tags', {tags: vm.selectedTags}, {reload: true}); } - 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 reloadNoTags(){ + $state.transitionTo('layout.home.kit'); } - } -})(); -(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'; 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) { + .controller('LoginModalController', LoginModalController); - var service = { - grantHTML5Geolocation: grantHTML5Geolocation, - isHTML5GeolocationGranted: isHTML5GeolocationGranted + 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 service; - - /////////////////////////// + $scope.openSignup = function() { + animation.showSignup(); + $mdDialog.hide(); + }; - function grantHTML5Geolocation(){ - $window.localStorage.setItem('smartcitizen.geolocation_granted', true); - } + $scope.openPasswordRecovery = function() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'PasswordRecoveryModalController', + templateUrl: 'app/components/passwordRecovery/passwordRecoveryModal.html', + clickOutsideToClose: true + }); - function isHTML5GeolocationGranted(){ - return $window.localStorage - .getItem('smartcitizen.geolocation_granted'); - } - } + $mdDialog.hide(); + }; + } })(); (function() { 'use strict'; - angular.module('app.components') - .factory('file', file); + angular.module('app.components') + .directive('login', login); - file.$inject = ['Restangular', 'Upload']; - function file(Restangular, Upload) { - var service = { - getCredentials: getCredentials, - uploadFile: uploadFile, - getImageURL: getImageURL + function login() { + return { + scope: { + show: '=' + }, + restrict: 'A', + controller: 'LoginController', + controllerAs: 'vm', + templateUrl: 'app/components/login/login.html' }; - 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() { - 'use strict'; - - angular.module('app.components') - .factory('device', device); + 'use strict'; - device.$inject = ['Restangular', '$window', 'timeUtils','$http', 'auth', '$rootScope']; - function device(Restangular, $window, timeUtils, $http, auth, $rootScope) { - var worldMarkers; + angular.module('app.components') + .controller('LoginController', LoginController); - initialize(); + LoginController.$inject = ['$scope', '$mdDialog']; + function LoginController($scope, $mdDialog) { - var service = { - getDevices: getDevices, - getAllDevices: getAllDevices, - getDevice: getDevice, - createDevice: createDevice, - updateDevice: updateDevice, - getWorldMarkers: getWorldMarkers, - setWorldMarkers: setWorldMarkers, - mailReadings: mailReadings, - postReadings: postReadings, - removeDevice: removeDevice, - updateContext: updateContext - }; + $scope.showLogin = showLogin; - return service; + $scope.$on('showLogin', function() { + showLogin(); + }); - ////////////////////////// + //////////////// - function initialize() { - if(areMarkersOld()) { - removeMarkers(); - } - } + function showLogin() { + $mdDialog.show({ + hasBackdrop: true, + fullscreen: true, + controller: 'LoginModalController', + controllerAs: 'vm', + templateUrl: 'app/components/login/loginModal.html', + clickOutsideToClose: true + }); + } - 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() { + 'use strict'; - function getAllDevicesCached() { - return Restangular.all('devices/world_map') - .getList() - .then(function(fetchedDevices){ - return fetchedDevices.plain(); - }); - } + angular.module('app.components') + .controller('LayoutController', LayoutController); - function getAllDevicesNoCached() { - return Restangular.all('devices/fresh_world_map') - .getList() - .then(function(fetchedDevices){ - return fetchedDevices.plain(); - }); - } + 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; - function getDevice(id) { - return Restangular.one('devices', id).get(); - } + vm.navRightLayout = 'space-around center'; - function createDevice(data) { - return Restangular.all('devices').post(data); - } + $scope.toggleRight = buildToggler('right'); - function updateDevice(id, data) { - return Restangular.one('devices', id).patch(data); + function buildToggler(componentId) { + return function() { + $mdSidenav(componentId).toggle(); + }; } - function getWorldMarkers() { - return worldMarkers || ($window.localStorage.getItem('smartcitizen.markers') && JSON.parse($window.localStorage.getItem('smartcitizen.markers') ).data); - } + // 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'; + // } - 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..."); + 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(); } - worldMarkers = obj.data; - } + }); - function getTimeStamp() { - return ($window.localStorage.getItem('smartcitizen.markers') && - JSON.parse($window.localStorage - .getItem('smartcitizen.markers') ).timestamp); - } + // 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 areMarkersOld() { - var markersDate = getTimeStamp(); - return !timeUtils.isWithin(1, 'minutes', markersDate); - } - function removeMarkers() { - worldMarkers = null; - $window.localStorage.removeItem('smartcitizen.markers'); - } + vm.isShown = true; + vm.isLoggedin = false; + vm.logout = logout; - function mailReadings(kit) { - return Restangular - .one('devices', kit.id) - .customGET('readings/csv_archive'); - } + vm.dropdownOptions = DROPDOWN_OPTIONS_USER; + vm.dropdownSelected = undefined; - function postReadings(kit, readings) { - return Restangular - .one('devices', kit.id) - .post('readings', readings); - } + vm.dropdownOptionsCommunity = DROPDOWN_OPTIONS_COMMUNITY; + vm.dropdownSelectedCommunity = undefined; - function removeDevice(deviceID){ - return Restangular - .one('devices', deviceID) - .remove().then(function () { - $rootScope.$broadcast('devicesContextUpdated'); - }) - ; - } + $scope.$on('removeNav', function() { + vm.isShown = false; + }); - function updateContext (){ - return auth.updateUser().then(function(){ - removeMarkers(); - $rootScope.$broadcast('devicesContextUpdated'); - }); + $scope.$on('addNav', function() { + vm.isShown = true; + }); + + initialize(); + + ////////////////// + + function initialize() { + $timeout(function() { + var hash = $location.search(); + if(hash.signup) { + animation.showSignup(); + } else if(hash.login) { + animation.showLogin(); + } else if(hash.passwordRecovery) { + animation.showPasswordRecovery(); + } + }, 1000); } - } + function logout() { + auth.logout(); + vm.isLoggedin = false; + } + } })(); (function() { '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) { + .controller('LandingController', LandingController); - var user = {}; + LandingController.$inject = ['$timeout', 'animation', '$mdDialog', '$location', '$anchorScroll']; - //wait until http interceptor is added to Restangular - $timeout(function() { - initialize(); - }, 100); + function LandingController($timeout, animation, $mdDialog, $location, $anchorScroll) { + var vm = this; - 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; + vm.showStore = showStore; + vm.goToHash = goToHash; - ////////////////////////// + /////////////////////// - function initialize() { - //console.log('---- AUTH INIT -----'); - setCurrentUser('appLoad'); - } + initialize(); - //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 initialize() { + $timeout(function() { + animation.viewLoaded(); + if($location.hash()) { + $anchorScroll(); } + }, 500); + } - 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()) ); + function goToHash(hash){ + $location.hash(hash); + $anchorScroll(); + } - 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 showStore() { + $mdDialog.show({ + hasBackdrop: true, + controller: 'StoreModalController', + templateUrl: 'app/components/store/storeModal.html', + clickOutsideToClose: true + }); + } + } +})(); - //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'); +(function(){ + 'use strict'; + angular.module('app.components') + .directive('kitList',kitList); - // 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); - } - }); - } + function kitList(){ + return{ + restrict:'E', + scope:{ + devices:'=devices', + actions: '=actions' + }, + controllerAs:'vm', + templateUrl:'app/components/kitList/kitList.html' + }; + } +})(); - // 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() { + 'use strict'; - function getCurrentUser() { - user.token = getToken(); - user.data = $window.localStorage.getItem('smartcitizen.data') && new AuthUser(JSON.parse( $window.localStorage.getItem('smartcitizen.data') )); - return user; - } + angular.module('app.components') + .controller('HomeController', HomeController); - // Should check if user.token exists - but now checks if the cookies.token exists. - function isAuth() { - // TODO: isAuth() is called from many different services BEFORE auth.init has run. - // That means that the user.token is EMPTY, meaning isAuth will be false - // We can cheat and just check the cookie, but we should NOT. Because auth.init should also check if the cookie is valid / expired - // Ideally it should return !!user.token - //return !!user.token; - return !!getToken(); - } + function HomeController() { + } +})(); +(function (){ + 'use strict'; - // 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(); - } + angular.module('app.components') + .controller('DownloadModalController', DownloadModalController); - function getToken(){ - return $cookies.get('smartcitizen.token'); - } + DownloadModalController.$inject = ['thisDevice', 'device', '$mdDialog']; - function login(loginData) { - return Restangular.all('sessions').post(loginData); - } + function DownloadModalController(thisDevice, device, $mdDialog) { + var vm = this; - function logout() { - $cookies.remove('smartcitizen.token'); - } + vm.device = thisDevice; + vm.download = download; + vm.cancel = cancel; - function getCurrentUserFromAPI() { - return Restangular.all('').customGET('me'); - } + //////////////////////////// - function recoverPassword(data) { - return Restangular.all('password_resets').post(data); - } + function download(){ + device.mailReadings(vm.device) + .then(function (){ + $mdDialog.hide(); + }).catch(function(err){ + $mdDialog.cancel(err); + }); + } + + function cancel(){ + $mdDialog.cancel(); + } + } - 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'; +(function(){ +'use strict'; - /** - * Unused directive. Double-check before removing. - * - */ - angular.module('app.components') - .directive('slide', slide) - .directive('slideMenu', slideMenu); +angular.module('app.components') + .directive('cookiesLaw', cookiesLaw); - function slideMenu() { - return { - controller: controller, - link: link - }; - function link(scope, element) { - scope.element = element; +cookiesLaw.$inject = ['$cookies']; + +function cookiesLaw($cookies) { + return { + template: + '
' + + 'This site uses cookies to offer you a better experience. ' + + ' Accept or' + + ' Learn More. ' + + '
', + controller: function($scope) { + + var init = function(){ + $scope.isCookieValid(); } - function controller($scope) { - $scope.slidePosition = 0; - $scope.slideSize = 20; + // Helpers to debug + // You can also use `document.cookie` in the browser dev console. + //console.log($cookies.getAll()); - 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; - } - }; + $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') } - } - slide.$inject = []; - function slide() { - return { - link: link, - require: '^slide-menu', - restrict: 'A', - scope: { - direction: '@' - } - }; + $scope.acceptCookie = function() { + //console.log('Accepting cookie...'); + var today = new Date(); + var expireDate = new Date(today); + expireDate.setMonth(today.getMonth() + 6); - function link(scope, element, attr, slideMenuCtrl) { - //select first sensor container - var sensorsContainer = angular.element('.sensors_container'); + $cookies.put('consent', true, {'expires' : expireDate.toUTCString()} ); + + // Trigger the check again, after we click + $scope.isCookieValid(); + }; - element.on('click', function() { + init(); - if(slideMenuCtrl.scrollIsValid('left') && attr.direction === 'left') { - slideMenuCtrl.decrementPosition(); - sensorsContainer.scrollLeft(slideMenuCtrl.getPosition()); - console.log(slideMenuCtrl.getPosition()); - } else if(slideMenuCtrl.scrollIsValid('right') && attr.direction === 'right') { - slideMenuCtrl.incrementPosition(); - sensorsContainer.scrollLeft(slideMenuCtrl.getPosition()); - console.log(slideMenuCtrl.getPosition()); - } - }); - } } + }; +} + + })(); (function() { 'use strict'; angular.module('app.components') - .directive('showPopupInfo', showPopupInfo); + .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; - /** - * Used to show/hide explanation of sensor value at kit dashboard - * - */ - showPopupInfo.$inject = []; - function showPopupInfo() { return { - link: link + link: link, + restrict: 'A', + scope: { + chartData: '=' + } }; - ////// + function link(scope, elem) { + $timeout(function() { + createChart(elem[0]); + }, 0); - 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 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}); }); - } - } -})(); -(function() { - 'use strict'; + scope.$watch('chartData', function(newData) { + if(!newData) { + return; + } - angular.module('app.components') - .directive('showPopup', showPopup); + 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; + }); - /** - * Used on kit dashboard to open full sensor description - */ + var sensorDataCompare = newData[1].data; + var dataCompare = sensorDataCompare.map(function(dataPoint) { + return { + date: dateFormat(dataPoint.time), + count: dataPoint && dataPoint.count, + value: dataPoint && dataPoint.value + }; + }); - showPopup.$inject = []; - function showPopup() { - return { - link: link - }; + 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 link(scope, element) { - element.on('click', function() { - var text = angular.element('.sensor_description_preview').text(); - if(text.length < 140) { - return; + var sensorData = newData[0].data; + /*jshint -W004 */ + var data = sensorData.map(function(dataPoint) { + return { + date: dateFormat(dataPoint.time), + count: dataPoint && dataPoint.count, + value: dataPoint && dataPoint.value + }; + }); + + data.sort(function(a, b) { + return a.date - b.date; + }); + + var color = newData[0].color; + var unit = newData[0].unit; + + lastData = { + data: data, + type: 'main', + color: color, + unit: unit + }; + + updateChartData(data, {type: 'main', container: elem[0], color: color, unit: unit }); + } + animation.hideChartSpinner(); } - angular.element('.sensor_description_preview').hide(); - angular.element('.sensor_description_full').show(); }); } - } -})(); -(function() { - 'use strict'; + // creates the container that is re-used across different sensor charts + function createChart(elem) { + d3.select(elem).selectAll('*').remove(); - angular.module('app.components') - .directive('moveFilters', moveFilters); + margin = {top: 20, right: 12, bottom: 20, left: 42}; + width = elem.clientWidth - margin.left - margin.right; + height = elem.clientHeight - margin.top - margin.bottom; - /** - * Moves map filters when scrolling - * - */ - moveFilters.$inject = ['$window', '$timeout']; - function moveFilters($window, $timeout) { - return { - link: link - }; + 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 link() { - var chartHeight; - $timeout(function() { - chartHeight = angular.element('.kit_chart').height(); - }, 1000); + 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' - /* - angular.element($window).on('scroll', function() { - var windowPosition = document.body.scrollTop; - if(chartHeight > windowPosition) { - elem.css('bottom', 12 + windowPosition + 'px'); - } - }); - */ + xAxis = d3.svg.axis() + .scale(xScale) + .orient('bottom') + .ticks(5); + + yAxisLeft = d3.svg.axis() + .scale(yScale0) + .orient('left') + .ticks(5); + + yAxisRight = d3.svg.axis() + .scale(yScale1) + .orient('right') + .ticks(5); + + 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); }); + + 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); }); + + valueLineCompare = d3.svg.line() + .defined(function(d) {return d.value != null }) + .interpolate('linear') + .x(function(d) { return xScale(d.date); }) + .y(function(d) { return yScale1(d.count); }); + + svg = d3 + .select(elem) + .append('svg') + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr('transform', 'translate(' + (margin.left - margin.right) + ',' + margin.top + ')'); } - } -})(); + // calls functions depending on type of chart + function updateChartData(newData, options) { + if(options.type === 'main') { + updateChartMain(newData, options); + } else if(options.type === 'both') { + updateChartCompare(newData, options); + } + } + // function in charge of rendering when there's data for 1 sensor + function updateChartMain(data, options) { + xScale.domain(d3.extent(data, function(d) { return d.date; })); + yScale0.domain([(d3.min(data, function(d) { return d.count; })) * 0.8, (d3.max(data, function(d) { return d.count; })) * 1.2]); -(function() { - 'use strict'; + svg.selectAll('*').remove(); + + //Add the area path + svg.append('path') + .datum(data) + .attr('class', 'chart_area') + .attr('fill', options.color) + .attr('d', areaMain); - angular.module('app.components') - .factory('layout', layout); + // Add the valueline path. + svg.append('path') + .attr('class', 'chart_line') + .attr('stroke', options.color) + .attr('d', valueLineMain(data)); + // Add the X Axis + svg.append('g') + .attr('class', 'axis x') + .attr('transform', 'translate(0,' + height + ')') + .call(xAxis); - function layout() { + // Add the Y Axis + svg.append('g') + .attr('class', 'axis y_left') + .call(yAxisLeft); - var kitHeight; + // Draw the x Grid lines + svg.append('g') + .attr('class', 'grid') + .attr('transform', 'translate(0,' + height + ')') + .call(xGrid() + .tickSize(-height, 0, 0) + .tickFormat('') + ); - var service = { - setKit: setKit, - getKit: getKit - }; - return service; + // Draw the y Grid lines + svg.append('g') + .attr('class', 'grid') + .call(yGrid() + .tickSize(-width, 0, 0) + .tickFormat('') + ); - function setKit(height) { - kitHeight = height; - } + focusMain = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - function getKit() { - return kitHeight; - } - } -})(); + focusMain.append('circle') + .style('stroke', options.color) + .attr('r', 4.5); -(function() { - 'use strict'; + var popupWidth = 84; + var popupHeight = 46; - angular.module('app.components') - .directive('horizontalScroll', horizontalScroll); + popup = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - /** - * 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' - }; + popupContainer = popup.append('rect') + .attr('width', popupWidth) + .attr('height', popupHeight) + .attr('transform', function() { + var result = 'translate(-42, 5)'; - /////////////////// + return result; + }) + .style('stroke', 'grey') + .style('stroke-width', '0.5') + .style('fill', 'white'); + var text = popup.append('text') + .attr('class', ''); - function link(scope, element) { + 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); - 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(); + textMain.append('tspan') + .attr('class', 'popup_value'); - // 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; - } + textMain.append('tspan') + .attr('class', 'popup_unit') + .attr('dx', 5); - //set opacity back to normal otherwise - angular.element('.button_scroll_left').css('opacity', '1'); - angular.element('.button_scroll_right').css('opacity', '1'); - }); + 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' ); - $timeout(function() { - element.trigger('scroll'); - }); + 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('resize', function() { - $timeout(function() { - element.trigger('scroll'); - }, 1000); - }); - } - } -})(); -(function() { - 'use strict'; - angular.module('app.components') - .directive('hidePopup', hidePopup); + function mousemove() { + var bisectDate = d3.bisector(function(d) { return d.date; }).left; - /** - * Used on kit dashboard to hide popup with full sensor description - * - */ - - hidePopup.$inject = []; - function hidePopup() { - return { - link: link - }; + var x0 = xScale.invert(d3.mouse(this)[0]); + var i = bisectDate(data, x0, 1); + var d0 = data[i - 1]; + var d1 = data[i]; + var d = d1 && (x0 - d0.date > d1.date - x0) ? d1 : d0; - ///////////// + focusMain.attr('transform', 'translate(' + xScale(d.date) + ', ' + yScale0(d.count) + ')'); + var popupText = popup.select('text'); + var textMain = popupText.select('.popup_main'); + var valueMain = textMain.select('.popup_value').text(parseValue(d.value)); + var unitMain = textMain.select('.popup_unit').text(options.unit); + var date = popupText.select('.popup_date').text(parseTime(d.date)); - function link(scope, elem) { - elem.on('mouseleave', function() { - angular.element('.sensor_description_preview').show(); - angular.element('.sensor_description_full').hide(); - }); - } - } -})(); + var textContainers = [ + textMain, + date + ]; -(function() { - 'use strict'; + var popupWidth = resizePopup(popupContainer, textContainers); - angular.module('app.components') - .directive('disableScroll', disableScroll); + 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) + ')'); + } + } + } - disableScroll.$inject = ['$timeout']; - function disableScroll($timeout) { - return { - // link: { - // pre: link - // }, - compile: link, - restrict: 'A', - priority: 100000 - }; + // 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]); + svg.selectAll('*').remove(); - ////////////////////// + //Add both area paths + svg.append('path') + .datum(data[0]) + .attr('class', 'chart_area') + .attr('fill', options.color[0]) + .attr('d', areaMain); - 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'); - }); - }); - } - } -})(); + svg.append('path') + .datum(data[1]) + .attr('class', 'chart_area') + .attr('fill', options.color[1]) + .attr('d', areaCompare); -(function() { - 'use strict'; + // Add both valueline paths. + svg.append('path') + .attr('class', 'chart_line') + .attr('stroke', options.color[0]) + .attr('d', valueLineMain(data[0])); - angular.module('app.components') - .factory('animation', animation); + svg.append('path') + .attr('class', 'chart_line') + .attr('stroke', options.color[1]) + .attr('d', valueLineCompare(data[1])); - /** - * Used to emit events from rootscope. - * - * This events are then listened by $scope on controllers and directives that care about that particular event - */ + // Add the X Axis + svg.append('g') + .attr('class', 'axis x') + .attr('transform', 'translate(0,' + height + ')') + .call(xAxis); - animation.$inject = ['$rootScope']; - function animation($rootScope) { + // Add both Y Axis + svg.append('g') + .attr('class', 'axis y_left') + .call(yAxisLeft); - 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; + svg.append('g') + .attr('class', 'axis y_right') + .attr('transform', 'translate(' + width + ' ,0)') + .call(yAxisRight); - ////////////// + // Draw the x Grid lines + svg.append('g') + .attr('class', 'grid') + .attr('transform', 'translate(0,' + height + ')') + .call(xGrid() + .tickSize(-height, 0, 0) + .tickFormat('') + ); - 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'); - } - } -})(); + // Draw the y Grid lines + svg.append('g') + .attr('class', 'grid') + .call(yGrid() + .tickSize(-width, 0, 0) + .tickFormat('') + ); -(function() { - 'use strict'; + focusCompare = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - /** - * TODO: Improvement These directives can be split up each one in a different file - */ + focusMain = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - 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); + focusCompare.append('circle') + .style('stroke', options.color[1]) + .attr('r', 4.5); - /** - * It moves down kit section to ease the transition after the kit menu is sticked to the top - * - */ - moveDown.$inject = []; - function moveDown() { + focusMain.append('circle') + .style('stroke', options.color[0]) + .attr('r', 4.5); - function link(scope, element) { - scope.$watch('moveDown', function(isTrue) { - if(isTrue) { - element.addClass('move_down'); - } else { - element.removeClass('move_down'); - } - }); - } + var popupWidth = 84; + var popupHeight = 75; - return { - link: link, - scope: false, - restrict: 'A' - }; - } + popup = svg.append('g') + .attr('class', 'focus') + .style('display', 'none'); - /** - * 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(); + popupContainer = popup.append('rect') + .attr('width', popupWidth) + .attr('height', popupHeight) + .style('min-width', '40px') + .attr('transform', function() { + var result = 'translate(-42, 5)'; - $timeout(function() { - elementPosition = element[0].offsetTop; - //var elementHeight = element[0].offsetHeight; - navbarHeight = angular.element('.stickNav').height(); - }, 1000); + 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]); - angular.element($window).on('scroll', function() { - var windowPosition = document.body.scrollTop; + popup.append('rect') + .attr('width', 8) + .attr('height', 2) + .attr('transform', function() { + return 'translate(' + (-popupWidth / 2 + 4).toString() + ', 45)'; + }) + .style('fill', options.color[1]); - //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; - }); - } - }); - } + var text = popup.append('text') + .attr('class', ''); - return { - link: link, - scope: false, - restrict: 'A' - }; - } + 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); + + textMain.append('tspan') + .attr('class', 'popup_value') + .attr( 'text-anchor', 'start' ); - /** - * Unused directive. Double-check is not being used before removing it - * - */ + textMain.append('tspan') + .attr('class', 'popup_unit') + .attr('dx', 5); - function blur() { + 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); - function link(scope, element) { + textCompare.append('tspan') + .attr('class', 'popup_value') + .attr( 'text-anchor', 'start' ); - scope.$on('blur', function() { - element.addClass('blur'); - }); + textCompare.append('tspan') + .attr('class', 'popup_unit') + .attr('dx', 5); - scope.$on('unblur', function() { - element.removeClass('blur'); - }); - } + 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' ); - return { - link: link, - scope: false, - restrict: 'A' - }; - } + 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); - /** - * 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(); - }); + function mousemove() { + var bisectDate = d3.bisector(function(d) { return d.date; }).left; - element.on('focusout', function() { - animation.addNav(); - }); + 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) + ')'); - var searchInput = element.find('input'); - searchInput.on('blur', function() { - //enable scrolling on body when search input is not active - angular.element(document.body).css('overflow', 'auto'); - }); - searchInput.on('focus', function() { - angular.element(document.body).css('overflow', 'hidden'); - }); - } + var dMain0 = data[0][i - 1]; + var dMain1 = data[0][i]; + var dMain = x0 - dMain0.date > dMain1.date - x0 ? dMain1 : dMain0; + focusMain.attr('transform', 'translate(' + xScale(dMain.date) + ', ' + yScale0(dMain.count) + ')'); - return { - link: link - }; - } + 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)); - /** - * Changes map section based on screen size - * - */ - changeMapHeight.$inject = ['$document', 'layout', '$timeout']; - function changeMapHeight($document, layout, $timeout) { - function link(scope, element) { + var textContainers = [ + textMain, + textCompare, + date + ]; - var screenHeight = $document[0].body.clientHeight; - var navbarHeight = angular.element('.stickNav').height(); + var popupWidth = resizePopup(popupContainer, textContainers); - // var overviewHeight = angular.element('.kit_overview').height(); - // var menuHeight = angular.element('.kit_menu').height(); - // var chartHeight = angular.element('.kit_chart').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 resizeMap(){ - $timeout(function() { - var overviewHeight = angular.element('.over_map').height(); + function xGrid() { + return d3.svg.axis() + .scale(xScale) + .orient('bottom') + .ticks(5); + } - var objectsHeight = navbarHeight + overviewHeight; - var objectsHeightPercentage = parseInt((objectsHeight * 100) / screenHeight); - var mapHeightPercentage = 100 - objectsHeightPercentage; + function yGrid() { + return d3.svg.axis() + .scale(yScale0) + .orient('left') + .ticks(5); + } - element.css('height', mapHeightPercentage + '%'); + 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); + } + } - var aboveTheFoldHeight = screenHeight - overviewHeight; - angular - .element('section[change-content-margin]') - .css('margin-top', aboveTheFoldHeight + 'px'); - }); + function parseTime(time) { + return moment(time).format('h:mm a ddd Do MMM YYYY'); + } + + function resizePopup(popupContainer, textContainers) { + if(!textContainers.length) { + return; } - resizeMap(); + var widestElem = textContainers.reduce(function(widestElemSoFar, textContainer) { + var currentTextContainerWidth = getContainerWidth(textContainer); + var prevTextContainerWidth = getContainerWidth(widestElemSoFar); + return prevTextContainerWidth >= currentTextContainerWidth ? widestElemSoFar : textContainer; + }, textContainers[0]); - scope.element = element; + var margins = widestElem.attr('dx') * 2; - scope.$on('resizeMapHeight',function(){ - resizeMap(); - }); + 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; } - - 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') + .directive('apiKey', apiKey); - var aboveTheFoldHeight = screenHeight - overviewHeight; - element.css('margin-top', aboveTheFoldHeight + 'px'); - } + function apiKey(){ + return { + scope: { + apiKey: '=apiKey' + }, + restrict: 'A', + controller: 'ApiKeyController', + controllerAs: 'vm', + templateUrl: 'app/components/apiKey/apiKey.html' + }; + } +})(); - return { - link: link - }; - } +(function(){ + 'use strict'; - /** - * 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 - }; + angular.module('app.components') + .controller('ApiKeyController', ApiKeyController); + + ApiKeyController.$inject = ['alert']; + function ApiKeyController(alert){ + var vm = this; + + 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') - .directive('activeButton', activeButton); + .factory('alert', alert); - /** - * Used to highlight and unhighlight buttons on kit menu - * - * It attaches click handlers dynamically - */ + 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 + } + }; - activeButton.$inject = ['$timeout', '$window']; - function activeButton($timeout, $window) { - return { - link: link, - restrict: 'A' + return service; - }; + /////////////////// - //////////////////////////// + function success(message) { + toast('success', message); + } - function link(scope, element) { - var childrens = element.children(); - var container; + function error(message) { + toast('error', message); + } - $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 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/' + }); + } - 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 infoNoDataOwner() { + info('Woah! We couldn\'t locate this kit on the map because it hasn\'t published any data.', + 10000); + } - function scrollTo(offset) { - if(!container) { - return; - } - angular.element($window).scrollTop(offset - container.navbar.height - container.kitMenu.height); - } + 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/' + }); + } - function getButton(buttonOrder) { - return childrens[buttonOrder]; - } + // TODO: Refactor, check why this was removed + // function infoDataInvalid() { + // info('Device not found, or it has been set to private.', + // 10000); + // } - 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 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/' + }); + } - var strokeContainer = activeButton.find('.stroke_container'); - strokeContainer.css('stroke', 'none'); - strokeContainer.css('stroke-width', '1'); + function info(message, delay, options) { + if(options && options.button) { + toast('infoButton', message, options, undefined, delay); + } else { + toast('info', message, options, undefined, delay); + } + } - var fillContainer = strokeContainer.find('.fill_container'); - fillContainer.css('fill', '#FF8600'); - } + function toast(type, message, options, position, delay) { + position = position === undefined ? 'top': position; + delay = delay === undefined ? 5000 : delay; + + $mdToast.show({ + controller: 'AlertController', + controllerAs: 'vm', + templateUrl: 'app/components/alert/alert' + type + '.html', + hideDelay: delay, + position: position, + locals: { + message: message, + button: options && options.button, + href: options && options.href } + }); + } + } +})(); - function highlightButton(button) { - var clickedButton = angular.element(button); - //add border, fill and stroke to every icon - clickedButton.addClass('button_active'); +(function() { + 'use strict'; - var strokeContainer = clickedButton.find('.stroke_container'); - strokeContainer.css('stroke', 'white'); - strokeContainer.css('stroke-width', '0.01px'); + angular.module('app.components') + .controller('AlertController', AlertController); - var fillContainer = strokeContainer.find('.fill_container'); - fillContainer.css('fill', 'white'); - } + AlertController.$inject = ['$scope', '$mdToast', 'message', 'button', 'href']; + function AlertController($scope, $mdToast, message, button, href) { + var vm = this; - //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'); - } - } - }); - }); + vm.close = close; + vm.message = message; + vm.button = button; + vm.href = href; - var currentSection; + // hideAlert will be triggered on state change + $scope.$on('hideAlert', function() { + close(); + }); - //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'; - } - }); + function close() { + $mdToast.hide(); } } })(); @@ -8006,7 +8006,7 @@ $templateCache.put('app/components/download/downloadModal.html','

Except where otherwise noted, content on this site by Smart Citizen\xAE is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. Check the Terms of use and Privacy Policy.

'); $templateCache.put('app/components/home/template.html','
'); $templateCache.put('app/components/kitList/kitList.html','
No kits

{{ device.name || \'No name\' }}

{{ device.locationString || \'No location\' }}{{ device.hardwareName || \'Unknown Kit\'}}

{{ device.state.name }}

{{ system_tag }}
View on map{{item.text}}Download CSVREMOVE
'); -$templateCache.put('app/components/landing/landing.html','

SMART CITIZEN

OPEN TOOLS FOR ENVIRONMENTAL MONITORING

LEARN MORE

EMPOWERING COMMUNITIES FOR ACTION

We are a collective of passionate people who believe that data is critical to inform civic participation. We develop free and open-source tools for environmental monitoring and methodologies for community-driven action.

BUILDING ON OUR HISTORY

The Smart Citizen project provides tools for collecting, understanding, and sharing environmental data. Since 2012, we have developed various iterations of the Smart Citizen Kit, allowing anyone to contribute and take part in data-driven action.

GET YOUR KIT

TOOLS FOR EVERY COMMUNITY

Community icon

LOCAL COMMUNITIES

Launch a crowd-sensing campaign in your city. Create local maps of noise and air quality. Raise awareness and find solutions for issues that matter to your community.

Community icon

RESEARCH INSTITUTIONS

Use Smart Citizen as a tool for environmental data collection and analysis. Trigger informed action that brings communities together, raising awareness of environmental issues.

Community icon

EDUCATORS

Find ways to bring Smart Citizen tools into the classroom. Explore our freely accessible tools and methodologies for educators, giving environmental awareness a hands-on toolkit.

AN OPEN PROJECT

Learn, build and contribute. The project builds on open source technologies such as Arduino to enable citizens and communities to gather information on their environment and make it available to the public via the Smart Citizen platform.

Check out our documentation and learn how to contribute to the project by joining the open source development community.

GO TO THE DOCS

GET YOUR KIT

New kit icon

SMART CITIZEN KIT

We\'re excited to release our newest version of the Smart Citizen Kit! Smart Citizen 2.3 is an upgrade of the kit that you already know and love, and it will be available soon through SEEED Studio. The latest version includes UV readings, an improved PM sensor and lots of new features!

Stay tuned by subscribing to our newsletter.

SUBSCRIBE
Contact icon

PROJECTS AND CUSTOMIZATIONS

Looking for something big? Do you want to use the kit as part of a research project? While all of our hardware is open source, we work directly with research organizations and projects to make open hardware that anyone can use.

Get in touch for customized projects.

CONTACT US

SUBSCRIBE TO GET THE LATEST NEWS

API icon

DEVELOPER
READY

Use our powerful API to build amazing things using data.

USE THE API
Github icon

WE\u2019RE
OPEN SOURCE

Fork and contribute to the project or download designs.

VISIT OUR REPOS
Forum icon

JOIN THE
FORUM

A place to share ideas with the community or find support.

GET INVOLVED
'); +$templateCache.put('app/components/landing/landing.html','

SMART CITIZEN

OPEN TOOLS FOR ENVIRONMENTAL MONITORING

LEARN MORE

EMPOWERING COMMUNITIES FOR ACTION

We are a collective of passionate people who believe that data is critical to inform civic participation. We develop free and open-source tools for environmental monitoring and methodologies for community-driven action.

BUILDING ON OUR HISTORY

The Smart Citizen project provides tools for collecting, understanding, and sharing environmental data. Since 2012, we have developed various iterations of the Smart Citizen Kit, allowing anyone to contribute and take part in data-driven action.

GET YOUR KIT

TOOLS FOR EVERY COMMUNITY

Community icon

LOCAL COMMUNITIES

Launch a crowd-sensing campaign in your city. Create local maps of noise and air quality. Raise awareness and find solutions for issues that matter to your community.

Community icon

RESEARCH INSTITUTIONS

Use Smart Citizen as a tool for environmental data collection and analysis. Trigger informed action that brings communities together, raising awareness of environmental issues.

Community icon

EDUCATORS

Find ways to bring Smart Citizen tools into the classroom. Explore our freely accessible tools and methodologies for educators, giving environmental awareness a hands-on toolkit.

AN OPEN PROJECT

Learn, build and contribute. The project builds on open source technologies such as Arduino to enable citizens and communities to gather information on their environment and make it available to the public via the Smart Citizen platform.

Check out our documentation and learn how to contribute to the project by joining the open source development community.

GO TO THE DOCS

GET YOUR KIT

New kit icon

SMART CITIZEN KIT

We\'re excited to release our newest version of the Smart Citizen Kit! Smart Citizen 2.3 is an upgrade of the kit that you already know and love, and it will be available soon through SEEED Studio. The latest version includes UV readings, an improved PM sensor and lots of new features!

Stay tuned by subscribing to our newsletter.

SUBSCRIBE
Contact icon

PROJECTS AND CUSTOMIZATIONS

Looking for something big? Do you want to use the kit as part of a research project? While all of our hardware is open source, we work directly with research organizations and projects to make open hardware that anyone can use.

Get in touch for customized projects.

CONTACT US

SUBSCRIBE TO GET THE LATEST NEWS

API icon

DEVELOPER
READY

Use our powerful API to build amazing things using data.

USE THE API
Github icon

WE\u2019RE
OPEN SOURCE

Fork and contribute to the project or download designs.

VISIT OUR REPOS
Forum icon

JOIN THE
FORUM

A place to share ideas with the community or find support.

GET INVOLVED
'); $templateCache.put('app/components/landing/static.html','

Title

Heading 2

Heading 3

Heading 4

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam a porta quam. Phasellus tincidunt facilisis blandit. Aenean tempor diam quis turpis vestibulum, ac semper turpis mollis. Sed ac ultricies est. Vivamus efficitur orci efficitur turpis commodo dignissim. Aliquam sagittis risus in semper ullamcorper. Sed enim diam, tempus eget lorem sit amet, luctus porta enim. Nam aliquam mollis massa quis euismod. In commodo laoreet mattis. Nunc auctor, massa ut sollicitudin imperdiet, mauris magna tristique metus, quis lobortis ex ex id augue. In hac habitasse platea dictumst. Sed sagittis iaculis eros non sollicitudin. Sed congue, urna ut aliquet ornare, nisi tellus euismod nisi, a ullamcorper augue arcu sit amet ante. Mauris condimentum ex ante, vitae accumsan sapien vulputate in. In tempor ligula ut scelerisque feugiat. Morbi quam nisi, blandit quis malesuada sit amet, gravida ut urna.

buttonbutton

Heading 2

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam a porta quam. Phasellus tincidunt facilisis blandit. Aenean tempor diam quis turpis vestibulum, ac semper turpis mollis. Sed ac ultricies est. Vivamus efficitur orci efficitur turpis commodo dignissim. Aliquam sagittis risus in semper ullamcorper. Sed enim diam, tempus eget lorem sit amet, luctus porta enim. Nam aliquam mollis massa quis euismod. In commodo laoreet mattis. Nunc auctor, massa ut sollicitudin imperdiet, mauris magna tristique metus, quis lobortis ex ex id augue. In hac habitasse platea dictumst. Sed sagittis iaculis eros non sollicitudin. Sed congue, urna ut aliquet ornare, nisi tellus euismod nisi, a ullamcorper augue arcu sit amet ante. Mauris condimentum ex ante, vitae accumsan sapien vulputate in. In tempor ligula ut scelerisque feugiat. Morbi quam nisi, blandit quis malesuada sit amet, gravida ut urna.

Small section

Single line comment.

'); $templateCache.put('app/components/layout/layout.html','
ProfileLog outMapGet your kit{{item.text}}
'); $templateCache.put('app/components/login/login.html','Log In'); @@ -8037,8 +8037,8 @@ $templateCache.put('app/components/upload/csvUpload.html','

Upl $templateCache.put('app/components/upload/errorModal.html','

Errors

{{error.message}} (at row: {{error.row}}){{csvFile.backEndErrors.statusText || csvFile.backEndErrors}} {{csvFile.backEndErrors.status}} : {{ csvFile.backEndErrors.data.message || csvFile.backEndErrors.data.errors }}
'); $templateCache.put('app/components/upload/upload.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/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

'); $templateCache.put('app/components/kit/editKit/editKit.html','

Edit your kit

Finalise your setup

2
Set up
BackSave

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. You can also choose to blurr the location, or enable MQTT forwarding.

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/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 +$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 }}
');}]); \ No newline at end of file