diff --git a/.env.sample b/.env.sample new file mode 100644 index 00000000..eb95c1bc --- /dev/null +++ b/.env.sample @@ -0,0 +1,4 @@ +OPENSENSEMAP_API_URL=https://localhost:8000 +OPENSENSEMAP_STYLE_URL= +OPENSENSEMAP_ACCESS_TOKEN= +OPENSENSEMAP_CMS_URL= \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 0bd46820..5137bd7b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ Gruntfile.js node_modules/ +app/bower_components diff --git a/.eslintrc.js b/.eslintrc.js index a77196c6..17e37626 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,16 +1,14 @@ module.exports = { env: { - browser: true + browser: true, }, globals: { - "angular": 1, - "L": 1, - "moment": 1 + angular: 1, + L: 1, + moment: 1, + Promise: 1, }, - extends: [ - '@sensebox/eslint-config-sensebox', - 'plugin:angular/johnpapa' - ], + extends: ["@sensebox/eslint-config-sensebox", "plugin:angular/johnpapa"], rules: { "angular/file-name": 0, "no-var": 0, @@ -18,8 +16,9 @@ module.exports = { "func-style": 0, "angular/function-type": 0, "wrap-iife": 0, - "strict": ['error', 'function'], + strict: ["error", "function"], "no-console": 1, - "no-warning-comments": 1 - } + "no-warning-comments": 1, + // "comma-dangle": ["error"] + }, }; diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..b629cf04 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,133 @@ +name: Build + +on: + push: + branches: + - master + pull_request: + branches: + - master + release: + branches: + - master + types: + - published + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/${{ secrets.GHCR_IMAGE_NAME }} + +jobs: + lint: + name: Lint code using eslint + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + + - name: Install Node.js 10 + uses: actions/setup-node@v3 + with: + node-version: 10 + + - name: Cache node modules + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: | + ~/.npm + ~/.cache/bower + app/bower_components + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Install project dependencies + run: | + npm ci + + - name: Run eslint + run: | + npm run lint + + build: + name: Build frontend + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + + - name: Install Node.js 10 + uses: actions/setup-node@v3 + with: + node-version: 10 + + - name: Cache node modules + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: | + ~/.npm + ~/.cache/bower + app/bower_components + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Install project dependencies + run: | + npm ci && npx bower@1.8.8 install + + - name: Prepare build environment variables + id: branch_info + run: | + branch_name=${GITHUB_REF##*/} + short_sha=${GITHUB_SHA:0:7} + echo "##[set-output name=branch_name;]${branch_name}" + echo "##[set-output name=short_sha;]${short_sha}" + + if [ "$branch_name" = "master" || ${{ github.event_name == 'release' }} ]; then + echo "OPENSENSEMAP_API_URL=https://api.opensensemap.org" >> $GITHUB_ENV + echo "OPENSENSEMAP_CMS_URL=${{ secrets.OPENSENSEMAP_CMS_URL }}" >> $GITHUB_ENV + echo "OPENSENSEMAP_STYLE_URL=${{ secrets.OPENSENSEMAP_STYLE_URL }}" >> $GITHUB_ENV + echo "OPENSENSEMAP_ACCESS_TOKEN=${{ secrets.OPENSENSEMAP_ACCESS_TOKEN }}" >> $GITHUB_ENV + else + echo "OPENSENSEMAP_API_URL=https://api.testing.opensensemap.org" >> $GITHUB_ENV + echo "OPENSENSEMAP_CMS_URL=${{ secrets.OPENSENSEMAP_CMS_URL }}" >> $GITHUB_ENV + echo "OPENSENSEMAP_STYLE_URL=${{ secrets.OPENSENSEMAP_STYLE_URL }}" >> $GITHUB_ENV + echo "OPENSENSEMAP_ACCESS_TOKEN=${{ secrets.OPENSENSEMAP_ACCESS_TOKEN_TESTING }}" >> $GITHUB_ENV + fi + + # - name: Update version in package.json + # if: ${{ github.event_name == 'release' && github.event.action == 'published' }} + # run: | + # npm version ${{ github.event.release.tag_name }} --no-git-tag-version --no-commit-hooks + + - name: Build + run: | + npx grunt build --target=build + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Github Container Registry (ghcr) + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GHCR_TOKEN }} + + - name: Build container image and push to Docker Hub (not for PRs) + uses: docker/build-push-action@v3 + with: + push: ${{ github.event_name == 'push' || ( github.event_name == 'release' && github.event.action == 'published' ) }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.branch_info.outputs.branch_name }},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.branch_info.outputs.branch_name }}-${{ steps.branch_info.outputs.short_sha }} + context: ./ + cache-to: type=local,dest=/tmp/.buildx-cache,mode=max + provenance: false # https://github.com/docker/buildx/issues/1533 diff --git a/.github/workflows/registry-purge.yaml b/.github/workflows/registry-purge.yaml new file mode 100644 index 00000000..07edfa49 --- /dev/null +++ b/.github/workflows/registry-purge.yaml @@ -0,0 +1,18 @@ +name: Purge untagged images + +# https://docs.github.com/en/actions/reference/events-that-trigger-workflows#registry_package +# Run cleanup job if a new package was published or updated +on: + registry_package: + +jobs: + purge_untagged_images: + runs-on: ubuntu-latest + steps: + - name: clean packages + uses: vlaurin/action-ghcr-prune@v0.3.0 + with: + token: ${{ secrets.GHCR_TOKEN}} + organization: ${{ github.repository_owner}} + container: ${{ secrets.GHCR_IMAGE_NAME }} + untagged: true \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d1d6fdfb..00000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -sudo: required -services: - - docker - -language: node_js -node_js: - - '6' - -cache: - directories: - - node_modules - - "$HOME/.cache/bower" - - "$HOME/.npm" - -before_install: - - npm install -g grunt-cli bower - - sudo apt-get -y install build-essential -addons: - apt: - update: true - -script: - # Set dev URLs - # By default env variables contain production urls - - echo "Lets check what env variables we want to use" - - echo "We have travis tag - $TRAVIS_TAG and travis branch - $TRAVIS_BRANCH" - - if [[ -z "$TRAVIS_TAG" && "$TRAVIS_BRANCH" != "master" ]]; then - echo "Lets use the dev envs"; - export OPENSENSEMAP_API_URL=$OPENSENSEMAP_API_URL_DEV; - export OPENSENSEMAP_MAPTILES_URL=$OPENSENSEMAP_MAPTILES_URL_DEV; - fi - - npm run build - - docker build -t $IMAGE_NAME:$TRAVIS_BRANCH-temp . - -after_success: - - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" - - - docker pull $IMAGE_NAME:$TRAVIS_BRANCH - - docker tag $IMAGE_NAME:$TRAVIS_BRANCH $IMAGE_NAME:$TRAVIS_BRANCH-rollback - - docker push $IMAGE_NAME:$TRAVIS_BRANCH-rollback - - - docker tag $IMAGE_NAME:$TRAVIS_BRANCH-temp $IMAGE_NAME:$TRAVIS_BRANCH - - docker push $IMAGE_NAME:$TRAVIS_BRANCH diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9a4f8c..ce4ebe29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,94 @@ -# Change Log +# Changelog All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [3.5.0](https://github.com/sensebox/openSenseMap/compare/v3.3.0...v3.5.0) (2023-10-30) + + +### Features + +* **account:** dashboard boxes pagination ([#475](https://github.com/sensebox/openSenseMap/issues/475)) ([b032ba5](https://github.com/sensebox/openSenseMap/commit/b032ba514f1b5bba61e125c9caa9215a00bbe03e)) + + +### Bug Fixes + +* **ci:** set `provenance: false` on docker/build-push-action ([#472](https://github.com/sensebox/openSenseMap/issues/472)) ([78d330b](https://github.com/sensebox/openSenseMap/commit/78d330ba81c89ac8f2be4b012dc0b2e004168626)) +* **ci:** specify docker platform ([542d0a9](https://github.com/sensebox/openSenseMap/commit/542d0a9d28071b916541951bb012eebdb62c21c0)) +* **ci:** specify docker platform ([#471](https://github.com/sensebox/openSenseMap/issues/471)) ([be733d8](https://github.com/sensebox/openSenseMap/commit/be733d88af7fc1a04ae084883b24e9e0f1e85ea0)) +* **edit:** validate MQTT Integration url ([#477](https://github.com/sensebox/openSenseMap/issues/477)) ([6bf2b59](https://github.com/sensebox/openSenseMap/commit/6bf2b59c0e12c80a76f505536537ec06cd07c061)) + +## [3.4.0](https://github.com/sensebox/openSenseMap/compare/v3.2.0...v3.4.0) (2023-07-06) + + +### Features + +* **account:** dashboard boxes pagination ([#475](https://github.com/sensebox/openSenseMap/issues/475)) ([b032ba5](https://github.com/sensebox/openSenseMap/commit/b032ba514f1b5bba61e125c9caa9215a00bbe03e)) +* **registration:** show sensors regardless of the selected hardware ([#444](https://github.com/sensebox/openSenseMap/issues/444)) ([681ac68](https://github.com/sensebox/openSenseMap/commit/681ac68ffa65f3cdbe4d4fd6b78b12bbce69a87d)), closes [#374](https://github.com/sensebox/openSenseMap/issues/374) + + +### Bug Fixes + +* **ci:** set `provenance: false` on docker/build-push-action ([#472](https://github.com/sensebox/openSenseMap/issues/472)) ([78d330b](https://github.com/sensebox/openSenseMap/commit/78d330ba81c89ac8f2be4b012dc0b2e004168626)) +* **ci:** specify docker platform ([542d0a9](https://github.com/sensebox/openSenseMap/commit/542d0a9d28071b916541951bb012eebdb62c21c0)) +* **ci:** specify docker platform ([#471](https://github.com/sensebox/openSenseMap/issues/471)) ([be733d8](https://github.com/sensebox/openSenseMap/commit/be733d88af7fc1a04ae084883b24e9e0f1e85ea0)) + +### [3.3.5](https://github.com/sensebox/openSenseMap/compare/v3.3.4...v3.3.5) (2023-03-30) + + +### Bug Fixes + +* **ci:** set `provenance: false` on docker/build-push-action ([#472](https://github.com/sensebox/openSenseMap/issues/472)) ([78d330b](https://github.com/sensebox/openSenseMap/commit/78d330ba81c89ac8f2be4b012dc0b2e004168626)) + +### [3.3.4](https://github.com/sensebox/openSenseMap/compare/v3.3.3...v3.3.4) (2023-03-30) + + +### Bug Fixes + +* **ci:** specify docker platform ([542d0a9](https://github.com/sensebox/openSenseMap/commit/542d0a9d28071b916541951bb012eebdb62c21c0)) +* **ci:** specify docker platform ([#471](https://github.com/sensebox/openSenseMap/issues/471)) ([be733d8](https://github.com/sensebox/openSenseMap/commit/be733d88af7fc1a04ae084883b24e9e0f1e85ea0)) + +### [3.3.3](https://github.com/sensebox/openSenseMap/compare/v3.3.2...v3.3.3) (2023-03-30) + +### [3.3.2](https://github.com/sensebox/openSenseMap/compare/v3.3.1...v3.3.2) (2023-03-30) + +## [3.3.0](https://github.com/sensebox/openSenseMap/compare/v3.2.0...v3.3.0) (2023-01-13) + + +### Features + +* **registration:** show sensors regardless of the selected hardware ([#444](https://github.com/sensebox/openSenseMap/issues/444)) ([681ac68](https://github.com/sensebox/openSenseMap/commit/681ac68ffa65f3cdbe4d4fd6b78b12bbce69a87d)), closes [#374](https://github.com/sensebox/openSenseMap/issues/374) + +## [3.2.0](https://github.com/sensebox/openSenseMap/compare/v3.1.3...v3.2.0) (2022-09-06) + + +### Features + +* **device:** add transfer capabilties ([#446](https://github.com/sensebox/openSenseMap/issues/446)) ([da4bc8d](https://github.com/sensebox/openSenseMap/commit/da4bc8d6307be977b72aef743d2d2d3027194215)) + + +### Bug Fixes + +* **chart:** render chart with one datapoint ([#452](https://github.com/sensebox/openSenseMap/issues/452)) ([05cd796](https://github.com/sensebox/openSenseMap/commit/05cd7969ab0533954e77b251f9f3aa73b6fa0937)) + +### [3.1.3](https://github.com/sensebox/openSenseMap/compare/v3.1.2...v3.1.3) (2022-04-08) + + +### Bug Fixes + +* **edit:** fix saving general device info ([06cc5fb](https://github.com/sensebox/openSenseMap/commit/06cc5fbef06626141a77f646aca4cdbddcd46552)) + +### [3.1.2](https://github.com/sensebox/openSenseMap/compare/v3.1.0...v3.1.2) (2022-04-08) + + +### Bug Fixes + +* **edit:** concat grouptags ([#445](https://github.com/sensebox/openSenseMap/issues/445)) ([36427f5](https://github.com/sensebox/openSenseMap/commit/36427f5f0282f0cb44f8132a4a0741a6212e15af)) + + +## [2.3.3](https://github.com/sensebox/openSenseMap/compare/v2.3.2...v2.3.3) (2020-10-29) + + + ## [2.3.2](https://github.com/sensebox/openSenseMap/compare/v2.3.1...v2.3.2) (2020-10-08) diff --git a/Gruntfile.js b/Gruntfile.js index de6bca7a..174a5958 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -11,6 +11,7 @@ module.exports = function (grunt) { var modRewrite = require('connect-modrewrite'); var serveStatic = require('serve-static'); + var { uniqueNamesGenerator, adjectives, animals } = require('unique-names-generator'); // Load grunt tasks automatically require('load-grunt-tasks')(grunt); @@ -20,13 +21,12 @@ module.exports = function (grunt) { // Define the configuration for all the tasks grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), + pkg: grunt.file.readJSON("package.json"), env: { dev: { - OPENSENSEMAP_API_URL: 'https://api.testing.opensensemap.org', - OPENSENSEMAP_MAPTILES_URL: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - } + src: ".env", + }, }, replace: { @@ -35,161 +35,217 @@ module.exports = function (grunt) { usePrefix: false, patterns: [ { - match: 'controls', - replacement: 'controls="map.controls"' - } - ] + match: "controls", + replacement: 'controls="map.controls"', + }, + ], }, files: [ - { expand: true, flatten: true, src: ['<%= yeoman.dist %>/views/explore2.map.html'], dest: '<%= yeoman.dist %>/views/' } - ] + { + expand: true, + flatten: true, + src: ["<%= yeoman.dist %>/views/explore2.map.html"], + dest: "<%= yeoman.dist %>/views/", + }, + ], }, urls: { options: { patterns: [ { - match: 'OPENSENSEMAP_API_URL', - replacement: '<%= OPENSENSEMAP_API_URL %>' + match: "OPENSENSEMAP_API_URL", + replacement: "<%= OPENSENSEMAP_API_URL %>", }, { - match: 'OPENSENSEMAP_MAPTILES_URL', - replacement: '<%= OPENSENSEMAP_MAPTILES_URL %>' + match: "OPENSENSEMAP_CMS_URL", + replacement: "<%= OPENSENSEMAP_CMS_URL %>", }, { - match: 'VERSION', - replacement: '<%= pkg.version %>' + match: "OPENSENSEMAP_STYLE_URL", + replacement: "<%= OPENSENSEMAP_STYLE_URL %>", }, { - match: 'REVISION', - replacement: '<%= gitinfo.local.branch.current.shortSHA %>' - } - ] + match: "OPENSENSEMAP_ACCESS_TOKEN", + replacement: "<%= OPENSENSEMAP_ACCESS_TOKEN %>", + }, + { + match: "VERSION", + replacement: "<%= pkg.version %>", + }, + { + match: "NAME", + replacement: "<%= OPENSENSEMAP_RELEASE_NAME %>", + }, + { + match: "REVISION", + replacement: "<%= gitinfo.local.branch.current.shortSHA %>", + }, + ], }, files: [ - { expand: true, flatten: true, src: ['<%= yeoman.dist %>/scripts/*.scripts.js'], dest: '<%= yeoman.dist %>/scripts/' } - ] + { + expand: true, + flatten: true, + src: ["<%= yeoman.dist %>/scripts/*.scripts.js"], + dest: "<%= yeoman.dist %>/scripts/", + }, + ], }, devapi: { options: { patterns: [ { - match: 'OPENSENSEMAP_API_URL', - replacement: '<%= OPENSENSEMAP_API_URL %>' - } - ] + match: "OPENSENSEMAP_API_URL", + replacement: "<%= OPENSENSEMAP_API_URL %>", + }, + { + match: "OPENSENSEMAP_CMS_URL", + replacement: "<%= OPENSENSEMAP_CMS_URL %>", + }, + ], }, files: [ - { expand: true, flatten: true, src: ['.tmp/scripts/constants.js'], dest: '.tmp/scripts' }, - { expand: true, flatten: true, src: ['.tmp/scripts/services/opensensemapapi.js'], dest: '.tmp/scripts/services' } - ] + { + expand: true, + flatten: true, + src: [".tmp/scripts/constants.js"], + dest: ".tmp/scripts", + }, + { + expand: true, + flatten: true, + src: [".tmp/scripts/services/opensensemapapi.js"], + dest: ".tmp/scripts/services", + }, + ], }, devmaps: { options: { patterns: [ { - match: 'OPENSENSEMAP_MAPTILES_URL', - replacement: '<%= OPENSENSEMAP_MAPTILES_URL %>' - } - ] + match: "OPENSENSEMAP_STYLE_URL", + replacement: "<%= OPENSENSEMAP_STYLE_URL %>", + }, + { + match: "OPENSENSEMAP_ACCESS_TOKEN", + replacement: "<%= OPENSENSEMAP_ACCESS_TOKEN %>", + }, + ], }, files: [ - { expand: true, flatten: true, src: ['.tmp/components/leaflet.directive.js'], dest: '.tmp/components' } - ] + { + expand: true, + flatten: true, + src: [".tmp/components/leaflet.directive.js"], + dest: ".tmp/components", + }, + ], }, version: { options: { patterns: [ { - match: 'VERSION', - replacement: '<%= pkg.version %>' + match: "VERSION", + replacement: "<%= pkg.version %>", + }, + { + match: "REVISION", + replacement: "<%= gitinfo.local.branch.current.shortSHA %>", }, { - match: 'REVISION', - replacement: '<%= gitinfo.local.branch.current.shortSHA %>' - } - ] + match: "NAME", + replacement: "<%= OPENSENSEMAP_RELEASE_NAME %>", + }, + ], }, files: [ - { expand: true, flatten: true, src: ['.tmp/scripts/constants.js'], dest: '.tmp/scripts' } - ] - } + { + expand: true, + flatten: true, + src: [".tmp/scripts/constants.js"], + dest: ".tmp/scripts", + }, + ], + }, }, // Project settings yeoman: { // configurable paths - app: require('./bower.json').appPath || 'app', - dist: 'dist' + app: require("./bower.json").appPath || "app", + dist: "dist", }, // Watches files for changes and runs tasks based on the changed files watch: { bower: { - files: ['bower.json'], - tasks: ['bowerInstall'] + files: ["bower.json"], + tasks: ["bowerInstall"], }, js: { files: [ - '<%= yeoman.app %>/scripts/{,*/}*.js', - '<%= yeoman.app %>/components/{,*/}*.js' + "<%= yeoman.app %>/scripts/{,*/}*.js", + "<%= yeoman.app %>/components/{,*/}*.js", ], - tasks: ['newer:eslint'], + tasks: ["newer:eslint"], options: { - livereload: true - } + livereload: true, + }, }, //jsTest: { // files: ['test/spec/{,*/}*.js'], // tasks: ['newer:jshint:test', 'karma'] //}, styles: { - files: ['<%= yeoman.app %>/styles/{,*/}*.css'], - tasks: ['newer:copy:styles', 'autoprefixer'] + files: ["<%= yeoman.app %>/styles/{,*/}*.css"], + tasks: ["newer:copy:styles", "autoprefixer"], }, gruntfile: { - files: ['Gruntfile.js'] + files: ["Gruntfile.js"], }, html: { - files: ['<%= yeoman.app %>/index.html'], - tasks: ['languages'], + files: ["<%= yeoman.app %>/index.html"], + tasks: ["languages"], }, livereload: { options: { - livereload: '<%= connect.options.livereload %>' + livereload: "<%= connect.options.livereload %>", }, files: [ - '<%= yeoman.app %>/{,*/}*.html', - '.tmp/styles/{,*/}*.css', - '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' - ] + "<%= yeoman.app %>/{,*/}*.html", + ".tmp/styles/{,*/}*.css", + "<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}", + ], }, devs: { files: [ - '<%= yeoman.app %>/scripts/controllers/map.js', - '<%= yeoman.app %>/scripts/controllers/register.js', + "<%= yeoman.app %>/scripts/controllers/map.js", + "<%= yeoman.app %>/scripts/controllers/register.js", // '<%= yeoman.app %>/scripts/services/opensenseboxapi.js', - '<%= yeoman.app %>/scripts/services/opensensemapapi.js', - '<%= yeoman.app %>/scripts/services/map.js', - '<%= yeoman.app %>/components/leaflet.directive.js', - '<%= yeoman.app %>/components/charts.directive.js', - '<%= yeoman.app %>/components/calendar.directive.js', - '<%= yeoman.app %>/components/badge.directive.js', - '<%= yeoman.app %>/components/loader.directive.js', - '<%= yeoman.app %>/components/clipboard.directive.js' + "<%= yeoman.app %>/scripts/services/opensensemapapi.js", + "<%= yeoman.app %>/scripts/services/map.js", + "<%= yeoman.app %>/components/leaflet.directive.js", + "<%= yeoman.app %>/components/charts.directive.js", + "<%= yeoman.app %>/components/calendar.directive.js", + "<%= yeoman.app %>/components/badge.directive.js", + "<%= yeoman.app %>/components/loader.directive.js", + "<%= yeoman.app %>/components/clipboard.directive.js", + "<%= yeoman.app %>/components/announcement.directive.js", + "<%= yeoman.app %>/components/geocoder.directive.js", ], tasks: [ // 'newer:copy:api', - 'loadconst', - 'newer:copy:apinew', - 'newer:copy:maps', - 'replace:devapi', - 'newer:copy:components', - 'replace:devmaps' + "loadconst", + "newer:copy:apinew", + "newer:copy:maps", + "replace:devapi", + "newer:copy:components", + "replace:devmaps", ], options: { - livereload: true - } - } + livereload: true, + }, + }, }, // The actual grunt server settings @@ -197,86 +253,83 @@ module.exports = function (grunt) { options: { port: 9000, // Change this to '0.0.0.0' to access the server from outside. - hostname: '0.0.0.0', + hostname: "0.0.0.0", livereload: 35729, - base: 'app' + base: "app", }, livereload: { options: { open: true, - base: [ - '.tmp', - '<%= yeoman.app %>' - ], + base: [".tmp", "<%= yeoman.app %>"], middleware: function (connect, options) { var middlewares = []; - middlewares.push(modRewrite(['!\\.html|\\.js|\\.css|\\.svg|\\.jp(e?)g|\\.png|\\.woff2|\\.gif|\\.ttf$ /index.html'])); + middlewares.push( + modRewrite([ + "!\\.html|\\.js|\\.css|\\.svg|\\.jp(e?)g|\\.png|\\.woff2|\\.gif|\\.ttf$ /index.html", + ]) + ); options.base.forEach(function (base) { middlewares.push(serveStatic(base)); }); return middlewares; - } - } + }, + }, }, test: { options: { port: 9001, - base: [ - '.tmp', - 'test', - '<%= yeoman.app %>' - ] - } + base: [".tmp", "test", "<%= yeoman.app %>"], + }, }, dist: { options: { - base: '<%= yeoman.dist %>' - } - } + base: "<%= yeoman.dist %>", + }, + }, }, // Make sure code styles are up to par and there are no obvious mistakes eslint: { - target: ['<%= yeoman.app %>/scripts/{,*/}*.js'] + target: ["<%= yeoman.app %>/scripts/{,*/}*.js"], }, // Empties folders to start fresh clean: { dist: { - files: [{ - dot: true, - src: [ - '.tmp/', - '<%= yeoman.dist %>/*', - '!<%= yeoman.dist %>/.git*' - ] - }] + files: [ + { + dot: true, + src: [".tmp/", "<%= yeoman.dist %>/*", "!<%= yeoman.dist %>/.git*"], + }, + ], }, - server: '.tmp/' + server: ".tmp/", }, // Add vendor prefixed styles autoprefixer: { options: { - browsers: ['last 1 version'] + browsers: ["last 1 version"], }, dist: { - files: [{ - expand: true, - cwd: '.tmp/styles/', - src: '{,*/}*.css', - dest: '.tmp/styles/' - }] - } + files: [ + { + expand: true, + cwd: ".tmp/styles/", + src: "{,*/}*.css", + dest: ".tmp/styles/", + }, + ], + }, }, // Automatically inject Bower components into the app bowerInstall: { app: { - src: ['<%= yeoman.app %>/index.html'], - ignorePath: '<%= yeoman.app %>/' - } + src: ["<%= yeoman.app %>/index.html"], + ignorePath: "<%= yeoman.app %>/", + }, }, // Renames files for browser caching purposes @@ -284,72 +337,76 @@ module.exports = function (grunt) { dist: { files: { src: [ - '<%= yeoman.dist %>/scripts/{,*/}*.js', - '<%= yeoman.dist %>/scripts/{,*/}*.js.map', - '<%= yeoman.dist %>/styles/{,*/}*.css', - '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', - '!<%= yeoman.dist %>/images/placeholder.png', - '<%= yeoman.dist %>/styles/fonts/*' - ] - } - } + "<%= yeoman.dist %>/scripts/{,*/}*.js", + "<%= yeoman.dist %>/scripts/{,*/}*.js.map", + "<%= yeoman.dist %>/styles/{,*/}*.css", + "<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}", + "!<%= yeoman.dist %>/images/placeholder.png", + "<%= yeoman.dist %>/styles/fonts/*", + ], + }, + }, }, // Reads HTML for usemin blocks to enable smart builds that automatically // concat, minify and revision files. Creates configurations in memory so // additional tasks can operate on them useminPrepare: { - html: '<%= yeoman.app %>/index.html', + html: "<%= yeoman.app %>/index.html", options: { - dest: '<%= yeoman.dist %>', + dest: "<%= yeoman.dist %>", flow: { html: { steps: { - js: ['concat'], - css: ['cssmin'] + js: ["concat"], + css: ["cssmin"], }, - post: {} - } - } - } + post: {}, + }, + }, + }, }, // Performs rewrites based on rev and the useminPrepare configuration usemin: { - html: ['<%= yeoman.dist %>/{,*/}*.html'], - css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], + html: ["<%= yeoman.dist %>/{,*/}*.html"], + css: ["<%= yeoman.dist %>/styles/{,*/}*.css"], options: { - assetsDirs: ['<%= yeoman.dist %>'], + assetsDirs: ["<%= yeoman.dist %>"], blockReplacements: { js: function (block) { console.log(block.dest); return ` - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + en: { + description: 'Matomo is a simple, self-hosted analytics service.' + }, + de: { + description: 'Matomo ist ein einfacher, selbstgehosteter Analytics-Service.' + }, + }, + purposes: ['analytics'], + cookies: [ + [/^_pk_.*$/, '/', 'opensensemap.org'], + [/^_pk_.*$/, '/', 'localhost'], + 'piwik_ignore', + ], + required: false, + optOut: false, + onlyOnce: true, + }, + { + name: 'openSenseMap', + default: true, + purposes: ['functional'], + cookies: [ + 'osem', + 'faq' + ], + required: true, + optOut: false, + onlyOnce: true, + }, + ], + }; + + - - - - - - + + + + + \ No newline at end of file diff --git a/app/scripts/app.js b/app/scripts/app.js index 599a36b5..239b52bf 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -20,425 +20,509 @@ 'tmh.dynamicLocale', 'rzModule', 'mgo-angular-wizard', - 'ismobile' + 'ismobile', + 'ngCookies', + 'ng-showdown', ]) - .config(['$stateProvider', '$httpProvider', '$urlRouterProvider', '$locationProvider', '$compileProvider', '$logProvider', 'tmhDynamicLocaleProvider', function ($stateProvider, $httpProvider, $urlRouterProvider, $locationProvider, $compileProvider, $logProvider, tmhDynamicLocaleProvider) { - $compileProvider.debugInfoEnabled(false); - $logProvider.debugEnabled(false); + .config([ + '$stateProvider', + '$httpProvider', + '$urlRouterProvider', + '$locationProvider', + '$compileProvider', + '$logProvider', + 'tmhDynamicLocaleProvider', + '$showdownProvider', + function ( + $stateProvider, + $httpProvider, + $urlRouterProvider, + $locationProvider, + $compileProvider, + $logProvider, + tmhDynamicLocaleProvider, + $showdownProvider + ) { + $compileProvider.debugInfoEnabled(false); + $logProvider.debugEnabled(false); - $locationProvider.html5Mode(true); + $locationProvider.html5Mode(true); - $httpProvider.interceptors.push('AuthenticationInterceptor'); - $httpProvider.interceptors.push('TemplateCacheInterceptor'); + $httpProvider.interceptors.push('AuthenticationInterceptor'); + $httpProvider.interceptors.push('TemplateCacheInterceptor'); - $urlRouterProvider.otherwise('/'); + $urlRouterProvider.otherwise('/'); - tmhDynamicLocaleProvider.localeLocationPattern('translations/angular/angular-locale_{{locale}}.js'); + tmhDynamicLocaleProvider.localeLocationPattern( + 'translations/angular/angular-locale_{{locale}}.js' + ); - $stateProvider - .state('explore', { - url: '/', - abstract: true, - templateUrl: 'views/explore2.html' - }) - .state('explore.map', { - url: '', - controller: 'MapController', - controllerAs: 'map', - templateUrl: 'views/explore2.map.html', - resolve: { /* @ngInject */ - boxes: function (BoxesService) { - // FIXME: when loading the page starting with explore.map.filter, - // boxes are fetched twice (once with minimal, once full) due to - // ui-router resolve inheritance. I found no workaround, because - // $state.current.name is not initialized here yet. - return BoxesService.getBoxesMinimal(); - } - } - }) - .state('explore.map.sidebar', { - url: 'explore', - resolve: { /* @ngInject */ - Sidebar: function ($rootScope) { - var vm = this; - vm.title = ''; - vm.translationId = ''; - vm.actions = []; + $showdownProvider.setOption('openLinksInNewWindow', true); - return { - getTitle: function () { - return vm.title; - }, - setTitle: function (title) { - vm.title = title; - $rootScope.$broadcast('sidebar:titleChanged', {}); - }, - setTranslationId: function (id) { - vm.translationId = id; - }, - getTranslationId: function () { - return vm.translationId; - }, - getActions: function () { - return vm.actions; - }, - addAction: function (action) { - vm.actions.push(action); - }, - removeActions: function () { - vm.actions = []; - } - }; - } - }, - views: { - 'sidebar': { - controller: 'SidebarController', - controllerAs: 'sidebar', - templateUrl: 'views/sidebar.html', - } - } - }) - .state('explore.map.sidebar.error', { - url: '^/error', - views: { - 'sidebarContent': { - controller: 'SidebarErrorController', - controllerAs: 'error', - templateUrl: 'views/sidebar.error.html' - } - } - }) - .state('explore.map.sidebar.boxdetails', { - url: '/:id', // no leading / because it is a child of the 'explore' state - views: { - 'sidebarContent': { - controller: 'SidebarBoxDetailsController', - controllerAs: 'details', - templateUrl: 'views/explore2.sidebar.box.html', - } - } - }) - .state('explore.map.sidebar.filter', { - url: '^/filter', - views: { - 'sidebarContent': { - controller: 'SidebarFilterController', - controllerAs: 'filter', - templateUrl: 'views/explore2.sidebar.filter.html' - } - }, - resolve: { /* @ngInject */ - boxes: function (BoxesService) { - return BoxesService.getBoxesFullMetadata(); - }, - } - }) - .state('explore.map.sidebar.download', { - url: '^/download', - views: { - 'sidebarContent': { - controller: 'SidebarDownloadController', - controllerAs: 'download', - templateUrl: 'views/explore2.sidebar.download.html' - } - }, - resolve: { /* @ngInject */ - boxes: function (BoxesService) { - return BoxesService.getBoxesFullMetadata(); - }, - } - }) - .state('explore.map.sidebar.interpolation', { - url: '^/interpolation', - views: { - 'sidebarContent': { - controller: 'InterpolationController', - controllerAs: 'interpolation', - templateUrl: 'views/explore2.sidebar.interpolation.html' - } - } - }) - .state('account', { - url: '/account', - abstract: true, - templateUrl: 'views/account.html', - data: { - requiresLogin: true - } - }) - .state('account.dataupload', { - url: '/:id/dataupload', - views: { - 'account': { - controller: 'DataUploadController', - controllerAs: 'dataupload', - templateUrl: 'views/account.box.dataupload.html' - } - }, - params: { - box: {} - } - }) - .state('account.edit', { - url: '/:id/edit', - params: { - box: { } - }, - resolve: { /* @ngInject */ - boxData: function ($state, $stateParams, AccountService) { - // Resolve boxData if called directly by URL - // $stateParams.box is set if called from account dashboard - if (angular.equals({}, $stateParams.box)) { - return AccountService.getUsersBoxes() - .then(function (response) { - for (var i = response.length - 1; i >= 0; i--) { - if (response[i]._id === $stateParams.id) { - return response[i]; - } - } - throw new Error('box not found'); - }) - .catch(function () { - $state.go('account.dashboard'); - }); - } - - return $stateParams.box; + $stateProvider + .state('explore', { + url: '/', + abstract: true, + templateUrl: 'views/explore2.html', + }) + .state('explore.map', { + url: '', + controller: 'MapController', + controllerAs: 'map', + templateUrl: 'views/explore2.map.html', + resolve: { + /* @ngInject */ + boxes: function (BoxesService) { + // eslint-disable-next-line no-warning-comments + // FIXME: when loading the page starting with explore.map.filter, + // boxes are fetched twice (once with minimal, once full) due to + // ui-router resolve inheritance. I found no workaround, because + // $state.current.name is not initialized here yet. + return BoxesService.getBoxesMinimal(); + }, }, - notifications: function ($translate, boxData) { - var vm = this; - vm.alerts = []; + }) + .state('explore.map.sidebar', { + url: 'explore', + resolve: { + /* @ngInject */ + Sidebar: function ($rootScope) { + var vm = this; + vm.title = ''; + vm.translationId = ''; + vm.actions = []; - return { - getAlerts: function () { - return vm.alerts; - }, - addAlert: function (type, messageId, sensorId) { - vm.alerts.pop(); - $translate(messageId, { boxId: boxData._id, sensorId: sensorId }).then(function (translation) { - vm.alerts.push({ type: type, msg: translation }); - }); - }, - closeAlert: function (index) { - vm.alerts.splice(index, 1); + return { + getTitle: function () { + return vm.title; + }, + setTitle: function (title) { + vm.title = title; + $rootScope.$broadcast('sidebar:titleChanged', {}); + }, + setTranslationId: function (id) { + vm.translationId = id; + }, + getTranslationId: function () { + return vm.translationId; + }, + getActions: function () { + return vm.actions; + }, + addAction: function (action) { + vm.actions.push(action); + }, + removeActions: function () { + vm.actions = []; + }, + }; + }, + }, + views: { + sidebar: { + controller: 'SidebarController', + controllerAs: 'sidebar', + templateUrl: 'views/sidebar.html', + }, + }, + }) + .state('explore.map.sidebar.error', { + url: '^/error', + views: { + sidebarContent: { + controller: 'SidebarErrorController', + controllerAs: 'error', + templateUrl: 'views/sidebar.error.html', + }, + }, + }) + .state('explore.map.sidebar.boxdetails', { + url: '/:id', // no leading / because it is a child of the 'explore' state + views: { + sidebarContent: { + controller: 'SidebarBoxDetailsController', + controllerAs: 'details', + templateUrl: 'views/explore2.sidebar.box.html', + }, + }, + }) + .state('explore.map.sidebar.filter', { + url: '^/filter', + views: { + sidebarContent: { + controller: 'SidebarFilterController', + controllerAs: 'filter', + templateUrl: 'views/explore2.sidebar.filter.html', + }, + }, + resolve: { + /* @ngInject */ + boxes: function (BoxesService) { + return BoxesService.getBoxesFullMetadata(); + }, + }, + }) + .state('explore.map.sidebar.download', { + url: '^/download', + views: { + sidebarContent: { + controller: 'SidebarDownloadController', + controllerAs: 'download', + templateUrl: 'views/explore2.sidebar.download.html', + }, + }, + resolve: { + /* @ngInject */ + boxes: function (BoxesService) { + return BoxesService.getBoxesFullMetadata(); + }, + }, + }) + .state('explore.map.sidebar.interpolation', { + url: '^/interpolation', + views: { + sidebarContent: { + controller: 'InterpolationController', + controllerAs: 'interpolation', + templateUrl: 'views/explore2.sidebar.interpolation.html', + }, + }, + }) + .state('account', { + url: '/account', + abstract: true, + templateUrl: 'views/account.html', + data: { + requiresLogin: true, + }, + }) + .state('account.dataupload', { + url: '/:id/dataupload', + views: { + account: { + controller: 'DataUploadController', + controllerAs: 'dataupload', + templateUrl: 'views/account.box.dataupload.html', + }, + }, + params: { + box: {}, + }, + }) + .state('account.edit', { + url: '/:id/edit', + params: { + box: {}, + }, + resolve: { + /* @ngInject */ + boxData: function ($state, $stateParams, AccountService) { + // Resolve boxData if called directly by URL + // $stateParams.box is set if called from account dashboard + if (angular.equals({}, $stateParams.box)) { + return AccountService.getUsersBoxes() + .then(function (response) { + for (var i = response.length - 1; i >= 0; i--) { + if (response[i]._id === $stateParams.id) { + return response[i]; + } + } + throw new Error('box not found'); + }) + .catch(function () { + $state.go('account.dashboard'); + }); } - }; - } - }, - views: { - 'account': { - controller: 'EditBoxController', - controllerAs: 'edit', - templateUrl: 'views/account.box.edit.html' - } - } - }) - .state('account.edit.general', { - url: '/general', - views: { - 'edit': { - controller: 'EditBoxGeneralController', - controllerAs: 'general', - templateUrl: 'views/account.box.edit.general.html' - } - } - }) - .state('account.edit.sensors', { - url: '/sensors', - views: { - 'edit': { - controller: 'EditBoxSensorsController', - controllerAs: 'sensors', - templateUrl: 'views/account.box.edit.sensors.html' - } - } - }) - .state('account.edit.security', { - url: '/security', - views: { - 'edit': { - controller: 'EditBoxSecurityController', - controllerAs: 'security', - templateUrl: 'views/account.box.edit.security.html' - } - } - }) - .state('account.edit.extensions', { - url: '/extensions', - views: { - 'edit': { - controller: 'EditBoxExtensionsController', - controllerAs: 'extensions', - templateUrl: 'views/account.box.edit.extensions.html' - } - } - }) - .state('account.edit.location', { - url: '/location', - views: { - 'edit': { - controller: 'EditBoxLocationController', - controllerAs: 'location', - templateUrl: 'views/account.box.edit.location.html' - } - } - }) - .state('account.edit.script', { - url: '/script', - views: { - 'edit': { - controller: 'EditBoxScriptController', - controllerAs: 'script', - templateUrl: 'views/account.box.edit.script.html' - } - } - }) - .state('account.edit.mqtt', { - url: '/mqtt', - views: { - 'edit': { - controller: 'EditBoxMqttController', - controllerAs: 'mqtt', - templateUrl: 'views/account.box.edit.mqtt.html' - } - } - }) - .state('account.edit.ttn', { - url: '/ttn', - views: { - 'edit': { - controller: 'EditBoxTtnController', - controllerAs: 'ttn', - templateUrl: 'views/account.box.edit.ttn.html' - } - } - }) - .state('account.dashboard', { - url: '', - views: { - 'account': { - controller: 'AccountDashboardController', - controllerAs: 'dashboard', - templateUrl: 'views/account.dashboard.html' - } - } - }) - .state('account.settings', { - url: '/settings', - views: { - 'account': { - controller: 'AccountSettingsController', - controllerAs: 'settings', - templateUrl: 'views/account.settings.html' - } - } - }) - .state('account.changepassword', { - url: '/settings/changepassword', - views: { - 'account': { - controller: 'AccountSettingsChangePasswordController', - controllerAs: 'changepassword', - templateUrl: 'views/account.settings.changepassword.html' - } - } - }) - .state('account.reset', { - url: '/password-reset?token', - views: { - 'account': { - controller: 'PasswordResetController', - controllerAs: 'reset', - templateUrl: 'views/account.reset.html' - } - }, - data: { - requiresLogin: false - } - }) - .state('account.confirm', { - url: '/confirm-email?email&token', - views: { - 'account': { - controller: 'ConfirmEmailController', - controllerAs: 'confirm', - templateUrl: 'views/account.confirm.html' - } - }, - data: { - requiresLogin: false - } - }) - .state('account.register', { - url: '/register', - views: { - 'account': { - controller: 'RegisterController', - controllerAs: 'register', - templateUrl: 'views/account.box.register.html' - } - } - }) - .state('register', { - url: '/register', - templateUrl: 'views/registration.html', - controller: 'SignupLoginController', - controllerAs: 'account' - }) - .state('info', { - url: '/info', - abstract: true, - templateUrl: 'views/info.html', - controller: 'InfoController', - controllerAs: 'info' - }) - .state('info.about', { - url: '^/about', - views: { - 'info': { - templateUrl: 'views/info.about.html' - } - } - }) - .state('info.privacy', { - url: '^/privacy', - views: { - 'info': { - templateUrl: 'views/info.privacy.html' - } - } - }) - .state('info.imprint', { - url: '^/imprint', - views: { - 'info': { - templateUrl: 'views/info.imprint.html' - } - } + + return $stateParams.box; + }, + notifications: function ($translate, boxData) { + var vm = this; + vm.alerts = []; + + return { + getAlerts: function () { + return vm.alerts; + }, + addAlert: function (type, messageId, sensorId) { + vm.alerts.pop(); + $translate(messageId, { + boxId: boxData._id, + sensorId: sensorId, + }).then(function (translation) { + vm.alerts.push({ type: type, msg: translation }); + }); + }, + closeAlert: function (index) { + vm.alerts.splice(index, 1); + }, + }; + }, + }, + views: { + account: { + controller: 'EditBoxController', + controllerAs: 'edit', + templateUrl: 'views/account.box.edit.html', + }, + }, + }) + .state('account.edit.general', { + url: '/general', + views: { + edit: { + controller: 'EditBoxGeneralController', + controllerAs: 'general', + templateUrl: 'views/account.box.edit.general.html', + }, + }, + }) + .state('account.edit.sensors', { + url: '/sensors', + views: { + edit: { + controller: 'EditBoxSensorsController', + controllerAs: 'sensors', + templateUrl: 'views/account.box.edit.sensors.html', + }, + }, + }) + .state('account.edit.security', { + url: '/security', + views: { + edit: { + controller: 'EditBoxSecurityController', + controllerAs: 'security', + templateUrl: 'views/account.box.edit.security.html', + }, + }, + }) + .state('account.edit.extensions', { + url: '/extensions', + views: { + edit: { + controller: 'EditBoxExtensionsController', + controllerAs: 'extensions', + templateUrl: 'views/account.box.edit.extensions.html', + }, + }, + }) + .state('account.edit.location', { + url: '/location', + views: { + edit: { + controller: 'EditBoxLocationController', + controllerAs: 'location', + templateUrl: 'views/account.box.edit.location.html', + }, + }, + }) + .state('account.edit.script', { + url: '/script', + views: { + edit: { + controller: 'EditBoxScriptController', + controllerAs: 'script', + templateUrl: 'views/account.box.edit.script.html', + }, + }, + }) + .state('account.edit.mqtt', { + url: '/mqtt', + views: { + edit: { + controller: 'EditBoxMqttController', + controllerAs: 'mqtt', + templateUrl: 'views/account.box.edit.mqtt.html', + }, + }, + }) + .state('account.edit.ttn', { + url: '/ttn', + views: { + edit: { + controller: 'EditBoxTtnController', + controllerAs: 'ttn', + templateUrl: 'views/account.box.edit.ttn.html', + }, + }, + }) + .state('account.edit.transfer', { + url: '/transfer', + views: { + edit: { + controller: 'EditBoxTransferController', + controllerAs: 'transfer', + templateUrl: 'views/account.box.edit.transfer.html', + }, + }, + }) + .state('account.dashboard', { + url: '', + views: { + account: { + controller: 'AccountDashboardController', + controllerAs: 'dashboard', + templateUrl: 'views/account.dashboard.html', + }, + }, + }) + .state('account.settings', { + url: '/settings', + views: { + account: { + controller: 'AccountSettingsController', + controllerAs: 'settings', + templateUrl: 'views/account.settings.html', + }, + }, + }) + .state('account.changepassword', { + url: '/settings/changepassword', + views: { + account: { + controller: 'AccountSettingsChangePasswordController', + controllerAs: 'changepassword', + templateUrl: 'views/account.settings.changepassword.html', + }, + }, + }) + .state('account.reset', { + url: '/password-reset?token', + views: { + account: { + controller: 'PasswordResetController', + controllerAs: 'reset', + templateUrl: 'views/account.reset.html', + }, + }, + data: { + requiresLogin: false, + }, + }) + .state('account.confirm', { + url: '/confirm-email?email&token', + views: { + account: { + controller: 'ConfirmEmailController', + controllerAs: 'confirm', + templateUrl: 'views/account.confirm.html', + }, + }, + data: { + requiresLogin: false, + }, + }) + .state('account.register', { + url: '/register', + views: { + account: { + controller: 'RegisterController', + controllerAs: 'register', + templateUrl: 'views/account.box.register.html', + }, + }, + }) + .state('register', { + url: '/register', + templateUrl: 'views/registration.html', + controller: 'SignupLoginController', + controllerAs: 'account', + }) + .state('info', { + url: '/info', + abstract: true, + templateUrl: 'views/info.html', + controller: 'InfoController', + controllerAs: 'info', + }) + .state('info.about', { + url: '^/about', + views: { + info: { + templateUrl: 'views/info.about.html', + }, + }, + }) + .state('info.faq', { + url: '^/faq', + views: { + info: { + templateUrl: 'views/info.faq.html', + controller: 'HelpController', + controllerAs: 'help', + }, + }, + }) + .state('info.privacy', { + url: '^/privacy', + views: { + info: { + templateUrl: 'views/info.privacy.html', + }, + }, + }) + .state('info.imprint', { + url: '^/imprint', + views: { + info: { + templateUrl: 'views/info.imprint.html', + }, + }, + }) + .state('donate', { + url: '/donate', + abstract: true, + templateUrl: 'views/donors.html', + controller: 'DonorsController', + controllerAs: 'donors', + }) + .state('donate.about', { + url: '^/donate', + views: { + donors: { + templateUrl: 'views/donors.html', + }, + }, + }); + }, + ]) + .config([ + '$translateProvider', + function ($translateProvider) { + $translateProvider.useStaticFilesLoader({ + prefix: 'https://unpkg.com/@sensebox/opensensemap-i18n@4.3.0/dist/', + suffix: '.json', }); - }]) - .config(['$translateProvider', function ($translateProvider) { - $translateProvider.useStaticFilesLoader({ - prefix: '../translations/', - suffix: '.json' - }); - $translateProvider.use('de_DE'); - $translateProvider.fallbackLanguage('en_US'); - $translateProvider.preferredLanguage('de_DE'); - $translateProvider.determinePreferredLanguage(); - $translateProvider.useSanitizeValueStrategy('escaped'); - }]) + $translateProvider.use('de_DE'); + $translateProvider.fallbackLanguage('en_US'); + $translateProvider.preferredLanguage('de_DE'); + $translateProvider.determinePreferredLanguage(); + $translateProvider.useSanitizeValueStrategy('escaped'); + }, + ]) - .run(['LanguageService', function (LanguageService) { - LanguageService.initialize(); - }]) + .run([ + 'LanguageService', + function (LanguageService) { + LanguageService.initialize(); + }, + ]) + .run([ + 'DonationModalService', + function (DonationModalService) { + DonationModalService.onStartup(); + }, + ]) - .filter('unsafe', ['$sce', function ($sce) { - return function (val) { - return $sce.trustAsHtml(val); - }; - }]) + .filter('unsafe', [ + '$sce', + function ($sce) { + return function (val) { + return $sce.trustAsHtml(val); + }; + }, + ]) .factory('FilterActiveService', function () { return { active: false }; diff --git a/app/scripts/constants.js b/app/scripts/constants.js index c414f828..50d21666 100644 --- a/app/scripts/constants.js +++ b/app/scripts/constants.js @@ -7,7 +7,8 @@ .constant('app', { VERSION: '@@VERSION', REVISION: '@@REVISION', - NAME: 'Zahngold Bernd', - API_URL: '@@OPENSENSEMAP_API_URL' + NAME: '@@NAME', + API_URL: '@@OPENSENSEMAP_API_URL', + CMS_URL: '@@OPENSENSEMAP_CMS_URL' }); })(); diff --git a/app/scripts/controllers/account.box.dataupload.js b/app/scripts/controllers/account.box.dataupload.js index 41d0afc4..cc65b760 100644 --- a/app/scripts/controllers/account.box.dataupload.js +++ b/app/scripts/controllers/account.box.dataupload.js @@ -27,6 +27,8 @@ vm.submitData = submitData; vm.onFileSelect = onFileSelect; + var mimeTypes = ['text/csv', 'application/vnd.ms-excel', 'application/json']; + activate(); //// @@ -43,13 +45,24 @@ function onFileSelect (event, $flow, file) { event.preventDefault(); vm.error = ''; - if (Object.keys(vm.dataTypes).indexOf(file.file.type) === -1) { + if (mimeTypes.indexOf(file.file.type) === -1) { vm.error = { code: 'FORMAT' }; return; } - vm.dataFormat = file.file.type; + switch (file.file.type) { + case 'text/csv': + case 'application/vnd.ms-excel': + vm.dataFormat = 'text/csv'; + break; + case 'application/json': + vm.dataFormat = 'application/json'; + break; + default: + vm.dataFormat = file.file.type; + break; + } vm.fileReader.readAsText(file.file); } @@ -57,9 +70,8 @@ function submitData () { vm.success = false; vm.error = ''; - OpenSenseMapAPI - .postMeasurements($state.params.id, vm.measurementData, vm.dataFormat) + .postMeasurements($state.params.id, vm.measurementData, vm.dataFormat, $state.params.box.access_token) .then(function () { vm.success = true; }) diff --git a/app/scripts/controllers/account.box.edit.general.js b/app/scripts/controllers/account.box.edit.general.js index c5d775ce..c2c0770b 100644 --- a/app/scripts/controllers/account.box.edit.general.js +++ b/app/scripts/controllers/account.box.edit.general.js @@ -21,16 +21,23 @@ //// function activate () { + // Concat grouptags + boxData.grouptag = boxData.grouptag.join(','); + angular.copy(boxData, vm.editingMarker); } function save () { + var grouptags = []; + if (vm.editingMarker.grouptag.length > 0) { + grouptags = vm.editingMarker.grouptag.split(','); + } var imgsrc = angular.element($document[0].getElementById('flowUploadImage')).attr('src'); var data = { name: vm.editingMarker.name, description: vm.editingMarker.description, weblink: vm.editingMarker.weblink, - grouptag: vm.editingMarker.grouptag, + grouptag: grouptags, exposure: vm.editingMarker.exposure, image: imgsrc }; diff --git a/app/scripts/controllers/account.box.edit.mqtt.js b/app/scripts/controllers/account.box.edit.mqtt.js index d86c459c..d6c383e9 100644 --- a/app/scripts/controllers/account.box.edit.mqtt.js +++ b/app/scripts/controllers/account.box.edit.mqtt.js @@ -16,11 +16,12 @@ topic: '', messageFormat: '', decodeOptions: '', - connectionOptions: '' + connectionOptions: '', }; vm.save = save; - + // Inspired by https://regex101.com/r/mIA28O/1 + vm.urlRegEx = /^((mqtt|ws)s?:\/\/)(([^:\s]+):([^:\s]+)@)?([a-zA-Z0-9.-]+)(:\d{1,5})?(\/[^?#\s]*)?$/; activate(); //// diff --git a/app/scripts/controllers/account.box.edit.script.js b/app/scripts/controllers/account.box.edit.script.js index 4f885844..ad7a243b 100644 --- a/app/scripts/controllers/account.box.edit.script.js +++ b/app/scripts/controllers/account.box.edit.script.js @@ -10,7 +10,8 @@ function EditBoxScriptController (boxData, AccountService) { var vm = this; vm.box = boxData; - + vm.display_enabled = false; + vm.enable_debug = false; vm.serialPort = 'Serial1'; vm.boxScript = ''; vm.showConfiguration = false; @@ -57,6 +58,10 @@ vm.showTTNConfiguration = true; } + if (boxData.model === 'homeV2Ethernet') { + vm.showConfiguration = true; + } + if (boxData.sensorsArray.filter(function (s) { return s.sensorType === 'SMT50'; }).length !== 0) { @@ -94,7 +99,9 @@ password: vm.wifi.password, devEUI: vm.ttn.devEUI, appEUI: vm.ttn.appEUI, - appKey: vm.ttn.appKey + appKey: vm.ttn.appKey, + display_enabled: vm.display_enabled, + enable_debug: vm.enable_debug }) .then(function (response) { vm.boxScript = response; diff --git a/app/scripts/controllers/account.box.edit.security.js b/app/scripts/controllers/account.box.edit.security.js index 540cbcde..f156ba00 100644 --- a/app/scripts/controllers/account.box.edit.security.js +++ b/app/scripts/controllers/account.box.edit.security.js @@ -1,77 +1,75 @@ (function () { - 'use strict'; - - angular - .module('openSenseMapApp') - .controller('EditBoxSecurityController', EditBoxSecurityController); - - EditBoxSecurityController.$inject = ['$document', 'notifications', 'boxData', 'AccountService', 'Box']; - - function EditBoxSecurityController ($document, notifications, boxData, AccountService, Box) { - var vm = this; - - vm.access_token = ""; - vm.useAuth = false; - vm.refreshAccessToken = false; - vm.boxData = boxData; + 'use strict'; - vm.revealPassword = revealPassword; - vm.toggleModal = toggleModal; - vm.generateNewToken = generateNewToken; - vm.save = save; + angular + .module('openSenseMapApp') + .controller('EditBoxSecurityController', EditBoxSecurityController); - vm.modalVisible = false; - - activate(); - - //// - - function activate () { - vm.access_token = boxData.access_token; - vm.useAuth = boxData.useAuth; - } + EditBoxSecurityController.$inject = ['$document', 'notifications', 'boxData', 'AccountService', 'Box']; - function revealPassword() { - var passField = document.getElementById('password-field') - if(passField.type === 'password'){ - passField.type = "text"; - } else { - passField.type = "password"; - } - } + function EditBoxSecurityController ($document, notifications, boxData, AccountService, Box) { + var vm = this; - function toggleModal() { - this.modalVisible = !this.modalVisible - } + vm.access_token = ''; + vm.useAuth = false; + vm.refreshAccessToken = false; + vm.boxData = boxData; - function generateNewToken(){ - this.modalVisible = false; - - return AccountService.updateBox(boxData._id, { generate_access_token: true }) - .then(function (response) { - angular.copy(new Box(response.data), boxData); - // vm.access_token = boxData.access_token; - vm.boxData = boxData; - notifications.addAlert('info', 'NOTIFICATION_BOX_UPDATE_SUCCESS'); - }) - .catch(function () { - notifications.addAlert('danger', 'NOTIFICATION_BOX_UPDATE_FAILED'); - }); + vm.revealPassword = revealPassword; + vm.toggleModal = toggleModal; + vm.generateNewToken = generateNewToken; + vm.save = save; + vm.modalVisible = false; + + activate(); + + //// + + function activate () { + vm.access_token = boxData.access_token; + vm.useAuth = boxData.useAuth; + } + + function revealPassword () { + var passField = $document[0].getElementById('password-field'); + if (passField.type === 'password') { + passField.type = 'text'; + } else { + passField.type = 'password'; } - - function save () { - console.log(vm.useAuth) - return AccountService.updateBox(boxData._id, { useAuth: vm.useAuth === true ? true : 'false', refreshAccessToken: vm.refreshAccessToken }) - .then(function (response) { - angular.copy(new Box(response.data), boxData); - vm.boxData = boxData; - notifications.addAlert('info', 'NOTIFICATION_BOX_UPDATE_SUCCESS'); - }) - .catch(function () { - notifications.addAlert('danger', 'NOTIFICATION_BOX_UPDATE_FAILED'); - }); - } } + + function toggleModal () { + vm.modalVisible = !vm.modalVisible; + } + + function generateNewToken () { + vm.modalVisible = false; + + return AccountService.updateBox(boxData._id, { generate_access_token: true }) + .then(function (response) { + angular.copy(new Box(response.data), boxData); + // vm.access_token = boxData.access_token; + vm.boxData = boxData; + notifications.addAlert('info', 'NOTIFICATION_BOX_UPDATE_SUCCESS'); + }) + .catch(function () { + notifications.addAlert('danger', 'NOTIFICATION_BOX_UPDATE_FAILED'); + }); + + } + + function save () { + return AccountService.updateBox(boxData._id, { useAuth: vm.useAuth === true ? true : 'false', refreshAccessToken: vm.refreshAccessToken }) + .then(function (response) { + angular.copy(new Box(response.data), boxData); + vm.boxData = boxData; + notifications.addAlert('info', 'NOTIFICATION_BOX_UPDATE_SUCCESS'); + }) + .catch(function () { + notifications.addAlert('danger', 'NOTIFICATION_BOX_UPDATE_FAILED'); + }); + } + } })(); - \ No newline at end of file diff --git a/app/scripts/controllers/account.box.edit.sensors.js b/app/scripts/controllers/account.box.edit.sensors.js index eaa36adf..60dc5542 100644 --- a/app/scripts/controllers/account.box.edit.sensors.js +++ b/app/scripts/controllers/account.box.edit.sensors.js @@ -5,9 +5,9 @@ .module('openSenseMapApp') .controller('EditBoxSensorsController', EditBoxSensorsController); - EditBoxSensorsController.$inject = ['boxData', 'notifications', 'SensorIcons', 'AccountService', 'OpenSenseMapAPI']; + EditBoxSensorsController.$inject = ['boxData', 'notifications', 'SensorIcons', 'AccountService']; - function EditBoxSensorsController (boxData, notifications, SensorIcons, AccountService, OpenSenseMapAPI) { + function EditBoxSensorsController (boxData, notifications, SensorIcons, AccountService) { var vm = this; vm.sensors = []; vm.icons = []; @@ -218,10 +218,10 @@ setDeleteMethod(vm.deleteOptions.method); return AccountService.deleteMeasurement(boxData._id, sensor._id, vm.deleteOptions.params) - .then(function (response) { + .then(function () { notifications.addAlert('info', 'NOTIFICATION_SUCCESSFULLY_DELETED', sensor._id); }) - .catch(function (error) { + .catch(function () { notifications.addAlert('danger', 'NOTIFICATION_NO_MATCHING_MEASUREMENTS'); }); } diff --git a/app/scripts/controllers/account.box.edit.transfer.js b/app/scripts/controllers/account.box.edit.transfer.js new file mode 100644 index 00000000..dbdc63d2 --- /dev/null +++ b/app/scripts/controllers/account.box.edit.transfer.js @@ -0,0 +1,79 @@ +(function () { + 'use strict'; + + angular + .module('openSenseMapApp') + .controller('EditBoxTransferController', EditBoxTransferController); + + EditBoxTransferController.$inject = ['boxData', 'moment', 'AccountService']; + + function EditBoxTransferController (boxData, moment, AccountService) { + var vm = this; + vm.device = {}; + vm.deviceName = ''; + vm.data = {}; + vm.tokenExists = false; + vm.expiration = '1'; + + vm.transferDevice = transferDevice; + vm.revokeToken = revokeToken; + + activate(); + + //// + + function activate () { + if (angular.isDefined(boxData)) { + angular.copy(boxData, vm.device); + + return AccountService.getTransferToken(vm.device._id) + .then(function (response) { + if (response.data) { + angular.copy(response.data, vm.data); + vm.tokenExists = true; + } + }) + .catch(function (error) { + console.log(error); + }); + } + } + + function transferDevice () { + + var payload = { + boxId: vm.device._id, + date: moment.utc().add(vm.expiration, 'd') + .toDate() + }; + + return AccountService.transferDevice(payload) + .then(function (response) { + angular.copy(response.data, vm.data); + vm.tokenExists = true; + }) + .catch(function (error) { + console.log(error); + }); + } + + function revokeToken () { + var payload = { + boxId: vm.device._id, + token: vm.data.token + }; + + return AccountService.revokeToken(payload) + .then(function (response) { + if (response.status === 204) { + angular.copy({}, vm.data); + vm.tokenExists = false; + } + }) + .catch(function (error) { + console.log(error); + }); + } + + } +})(); diff --git a/app/scripts/controllers/account.box.edit.ttn.js b/app/scripts/controllers/account.box.edit.ttn.js index 95f30979..dd2698e6 100644 --- a/app/scripts/controllers/account.box.edit.ttn.js +++ b/app/scripts/controllers/account.box.edit.ttn.js @@ -5,10 +5,11 @@ .module('openSenseMapApp') .controller('EditBoxTtnController', EditBoxTtnController); - EditBoxTtnController.$inject = ['$scope', 'boxData', 'notifications', 'AccountService']; + EditBoxTtnController.$inject = ['$scope', 'boxData', 'notifications', 'AccountService', 'Box']; - function EditBoxTtnController ($scope, boxData, notifications, AccountService) { + function EditBoxTtnController ($scope, boxData, notifications, AccountService, Box) { var vm = this; + vm.editingBox = {}; vm.validTTNconfig = true; vm.settings = { profile: 'sensebox/home', @@ -18,9 +19,11 @@ }; vm.save = save; + vm.reloadCayenne = createCayenneEncoding; vm.decodeOptions = []; vm.cayenneLppDecodingChanged = cayenneLppDecodingChanged; + vm.cayenneDecodingOptions = []; // temp variable for Cayenne activate(); @@ -31,21 +34,35 @@ angular.copy(boxData.integrations.ttn, vm.settings); vm.settings.decodeOptions = JSON.stringify(vm.settings.decodeOptions); - vm.decodeOptions = JSON.parse(vm.settings.decodeOptions) + vm.decodeOptions = JSON.parse(vm.settings.decodeOptions); + if (vm.settings.profile === 'cayenne-lpp') { + vm.cayenneDecodingOptions = JSON.parse(vm.settings.decodeOptions); + } } + + angular.copy(new Box(boxData), vm.editingBox); } - function cayenneLppDecodingChanged(sensor, index) { - vm.decodeOptions[index].sensor_title = sensor.sensor_title - vm.decodeOptions[index].sensor_type = sensor.sensor_type - vm.settings.decodeOptions = JSON.stringify(vm.decodeOptions) + function cayenneLppDecodingChanged (sensor, index) { + vm.cayenneDecodingOptions[index].sensor_title = sensor.sensor_title; + vm.cayenneDecodingOptions[index].sensor_type = sensor.sensor_type; + + // Update original decodeOptions and not just temp + vm.settings.decodeOptions = JSON.stringify(vm.cayenneDecodingOptions); } function save () { var req = { ttn: angular.copy(vm.settings) }; - req.ttn.decodeOptions = JSON.parse(req.ttn.decodeOptions); - return AccountService.updateBox(boxData._id, req) + // Copy temp Cayenne encoding to finally save it + if (vm.settings.profile === 'cayenne-lpp') { + req.ttn.decodeOptions = vm.cayenneDecodingOptions; + } else { + req.ttn.decodeOptions = JSON.parse(req.ttn.decodeOptions); + } + + + return AccountService.updateBox(vm.editingBox._id, req) .then(function (response) { angular.copy(response.data, boxData); notifications.addAlert('info', 'NOTIFICATION_BOX_UPDATE_SUCCESS'); @@ -55,8 +72,46 @@ }); } + /** + * Create basic and initial Cayenne encoding + */ + function createCayenneEncoding () { + vm.cayenneDecodingOptions = vm.editingBox.sensorsArray.map(function (sensor) { + var decoderGuess = 'analog_in'; + + var tempSubstr = ['temp']; + var humiSubstr = ['humi', 'feucht']; + var pressSubstr = ['press', 'druck']; + var illuSubstr = ['hell', 'illu', 'uv', 'beleuch']; + if (new RegExp(tempSubstr.join('|')).test(sensor.title.toLowerCase())) { + decoderGuess = 'temperature'; + } else if (new RegExp(humiSubstr.join('|')).test(sensor.title.toLowerCase())) { + decoderGuess = 'relative_humidity'; + } else if (new RegExp(pressSubstr.join('|')).test(sensor.title.toLowerCase())) { + decoderGuess = 'barometric_pressure'; + } else if (new RegExp(illuSubstr.join('|')).test(sensor.title.toLowerCase())) { + decoderGuess = 'luminosity'; + } + + return Object.assign({ + sensor_title: sensor.title, + sensor_type: sensor.sensorType, + decoder: decoderGuess, + channel: 1 + }, vm.decodeOptions[sensor.id]); + }); + } + //// + // Watch for a profile change + $scope.$watch('ttn.settings.profile', function (newValue) { + // Changed to Cayenne -> create Cayenne encoding + if (newValue === 'cayenne-lpp' && vm.cayenneDecodingOptions.length === 0) { + createCayenneEncoding(); + } + }); + // check if valid json for ttn decodeOptions $scope.$watch('ttn.settings.decodeOptions', function (newValue) { if (!newValue.length) { diff --git a/app/scripts/controllers/account.dashboard.js b/app/scripts/controllers/account.dashboard.js index 0be21c9d..000737c8 100644 --- a/app/scripts/controllers/account.dashboard.js +++ b/app/scripts/controllers/account.dashboard.js @@ -13,8 +13,18 @@ var localStorageOrderByKey = 'osem.dashboard.orderBy'; vm.boxes = []; + vm.boxes_count = 0; vm.listStyle = 'tiles'; vm.orderByProperty = 'createdAt'; + vm.page = 0; + vm.claimToken = ''; + vm.claimPattern = /^[a-z0-9]*$/; + vm.errorMessage = ''; + + vm.claimDevice = claimDevice; + vm.closeAlert = closeAlert; + vm.nextPage = nextPage; + vm.previousPage = previousPage; activate(); @@ -41,12 +51,49 @@ function getUsersBoxes () { vm.boxes = []; - return AccountService.getUsersBoxes() - .then(function (boxes) { - vm.boxes = boxes; + return AccountService.getUsersBoxes(vm.page) + .then(function (response) { + vm.boxes = response.data.boxes; + vm.boxes_count = response.data.boxes_count; + }); + } + + function claimDevice () { + var payload = { + token: vm.claimToken + }; + + + + return AccountService.claimDevice(payload) + .then(function () { + + // Clear token field + vm.claimToken = ''; + + return getUsersBoxes() + .then(function () { + // console.log('refreshed boxes'); + }); + }) + .catch(function (error) { + console.log(error); + vm.errorMessage = error.message; }); } + function closeAlert () { + vm.errorMessage = ''; + } + + function nextPage () { + vm.page = vm.page + 1; + } + + function previousPage () { + vm.page = vm.page - 1; + } + $scope.$watch('dashboard.listStyle', function (value) { LocalStorageService.setValue(localStorageKey, value); }); @@ -54,5 +101,9 @@ $scope.$watch('dashboard.orderByProperty', function (value) { LocalStorageService.setValue(localStorageOrderByKey, value); }); + + $scope.$watch('dashboard.page', function () { + return getUsersBoxes(); + }); } })(); diff --git a/app/scripts/controllers/donation.js b/app/scripts/controllers/donation.js new file mode 100644 index 00000000..920e1849 --- /dev/null +++ b/app/scripts/controllers/donation.js @@ -0,0 +1,20 @@ +(function () { + 'use strict'; + + angular + .module('openSenseMapApp') + .controller('DonationController', DonationController); + + DonationController.$inject = ['LanguageService', '$sce', 'DonationModalService']; + + function DonationController (LanguageService, $sce, DonationModalService) { + var vm = this; + + vm.onCheck = onCheck; + vm.dontshowagain = DonationModalService.hasCookie(); + + function onCheck () { + DonationModalService.setDontShowAgain(vm.dontshowagain); + } + } +})(); diff --git a/app/scripts/controllers/donors.js b/app/scripts/controllers/donors.js new file mode 100644 index 00000000..a3d9d535 --- /dev/null +++ b/app/scripts/controllers/donors.js @@ -0,0 +1,23 @@ +(function () { + 'use strict'; + + angular + .module('openSenseMapApp') + .controller('DonorsController', DonorsController); + + DonorsController.$inject = ['$state', 'app']; + function DonorsController ($state, app) { + var vm = this; + vm.app = app; + + + + activate(); + + //////////////// + + function activate () { + } + + } +})(); diff --git a/app/scripts/controllers/header.js b/app/scripts/controllers/header.js index a136ff72..bcf753d7 100644 --- a/app/scripts/controllers/header.js +++ b/app/scripts/controllers/header.js @@ -5,14 +5,15 @@ .module('openSenseMapApp') .controller('HeaderController', HeaderController); - HeaderController.$inject = ['$rootScope', '$state', '$http', '$document', 'ngDialog', 'OpenSenseMapData', 'OpenSenseMapAPI', 'FilterActiveService', 'AccountService', 'LanguageService', 'osemMapData', 'LocalStorageService']; + HeaderController.$inject = ['$rootScope', '$scope', '$state', '$http', '$document', 'ngDialog', 'OpenSenseMapData', 'OpenSenseMapAPI', 'FilterActiveService', 'AccountService', 'LanguageService', 'osemMapData', 'LocalStorageService', 'DonationModalService']; - function HeaderController ($rootScope, $state, $http, $document, ngDialog, OpenSenseMapData, OpenSenseMapAPI, FilterActiveService, AccountService, LanguageService, osemMapData, LocalStorageService) { + function HeaderController ($rootScope, $scope, $state, $http, $document, ngDialog, OpenSenseMapData, OpenSenseMapAPI, FilterActiveService, AccountService, LanguageService, osemMapData, LocalStorageService, DonationModalService) { var vm = this; vm.key = 'de'; vm.searchString = ''; vm.showClearSearch = false; vm.isNavCollapsed = true; + vm.showAnnouncment = true; vm.filterActive = FilterActiveService; vm.counts = { boxes: '', @@ -32,6 +33,7 @@ vm.searchStringChanged = searchStringChanged; vm.clearSearch = clearSearch; vm.open = open; + vm.openHelp = openHelp; vm.getLocations = getLocations; vm.selectBox = selectBox; vm.changeLang = changeLang; @@ -109,6 +111,11 @@ vm.isNavCollapsed = true; } + function openHelp () { + DonationModalService.open(); + vm.isNavCollapsed = true; + } + function isAuthed () { if (AccountService.isAuthed) { if (AccountService.isAuthed()) { @@ -198,5 +205,9 @@ vm.key = data.data.user.language.split('_')[0]; LanguageService.change(data.data.user.language); }); + + $rootScope.$on('osemAnnouncementClosed', function () { + vm.showAnnouncment = false; + }); } })(); diff --git a/app/scripts/controllers/help.js b/app/scripts/controllers/help.js new file mode 100644 index 00000000..9e7c2a87 --- /dev/null +++ b/app/scripts/controllers/help.js @@ -0,0 +1,15 @@ +(function () { + 'use strict'; + + angular + .module('openSenseMapApp') + .controller('HelpController', HelpController); + + HelpController.$inject = ['LanguageService', '$sce']; + + function HelpController (LanguageService, $sce) { + var vm = this; + var language = LanguageService.getLanguage() === 'de' ? '' : 'en.'; + vm.iframeSrc = $sce.trustAsResourceUrl('https://' + language + 'docs.sensebox.de/opensensemap-faq/'); + } +})(); diff --git a/app/scripts/controllers/info.js b/app/scripts/controllers/info.js index c60c2121..85a8dcd2 100644 --- a/app/scripts/controllers/info.js +++ b/app/scripts/controllers/info.js @@ -11,6 +11,7 @@ vm.app = app; vm.tabActive = { about: true, + faq: false, imprint: false, privacy: false }; @@ -33,6 +34,9 @@ case 'about': vm.tabActive.about = true; break; + case 'faq': + vm.tabActive.faq = true; + break; case 'imprint': vm.tabActive.imprint = true; break; diff --git a/app/scripts/controllers/register.js b/app/scripts/controllers/register.js index 24b32abd..186c25ef 100644 --- a/app/scripts/controllers/register.js +++ b/app/scripts/controllers/register.js @@ -35,23 +35,27 @@ light: false, pollution: false, bme680: false, + co2: false, + dps310: false, + sps30: false }, serialPort: 'Serial1', soilDigitalPort: 'A', soundMeterPort: 'B', windSpeedPort: 'C', - bmePhenomenon: 'tempHumiPress' + bmePhenomenon: 'tempHumiPress', }; - + vm.display_enabled = false; + vm.enable_debug = false; vm.wifi = { ssid: '', - pasword: '' + pasword: '', }; vm.ttn = { devEUI: '', appEUI: '', - appKey: '' + appKey: '', }; // vm.radioModel = null; @@ -61,7 +65,7 @@ vm.tag = ''; vm.geolocationError = { error: false, - message: '' + message: '', }; vm.privacy = false; vm.completed = false; @@ -70,29 +74,29 @@ vm.newSenseBox = { name: '', exposure: '', - location: [] + location: [], }; vm.modelSelected = { id: '', - name: false + name: false, }; vm.extensions = { feinstaub: { - id: '' + id: '', }, soilMoisture: { id: '', - port: 'A' + port: 'A', }, soundLevelMeter: { id: '', - port: 'B' + port: 'B', }, windSpeed: { id: '', - port: 'C' - } + port: 'C', + }, }; vm.mqtt = { @@ -101,7 +105,7 @@ topic: '', messageFormat: '', decodeOptions: '', - connectionOptions: '' + connectionOptions: '', }; vm.ttnEnabled = false; vm.validTTNconfig = true; @@ -111,7 +115,7 @@ app_id: '', dev_id: '', decodeOptions: '[]', - cayenneLppDecoding: [] + cayenneLppDecoding: [], }; vm.open = { sensebox: false, @@ -120,7 +124,7 @@ mqtt: false, ttn: false, hackair: false, - edu: false + edu: false, }; vm.markers = {}; @@ -130,8 +134,9 @@ vm.editing = {}; vm.isCustom = {}; vm.events = { - autolocation: true + autolocation: true, }; + vm.showSameAddressWarning = false; vm.enterEvent = enterEvent; vm.add = add; @@ -150,6 +155,7 @@ vm.addSensorTemplate = addSensorTemplate; vm.generateScript = generateScript; vm.compile = compile; + vm.selectBox = selectBox; activate(); @@ -198,7 +204,8 @@ password: vm.wifi.password, devEUI: vm.ttn.devEUI, appEUI: vm.ttn.appEUI, - appKey: vm.ttn.appKey + appKey: vm.ttn.appKey, + display_enabled: vm.display_enabled, }) .then(function (response) { vm.boxScript = response; @@ -211,7 +218,7 @@ return AccountService.compileSketch({ board: 'sensebox-mcu', - sketch: vm.boxScript + sketch: vm.boxScript, }) .then(function () {}) .catch(function () {}) @@ -233,9 +240,8 @@ } function setStepTitle () { - vm.stepTitle = WizardHandler.wizard( - 'RegistrationWizard' - ).currentStepTitle(); + vm.stepTitle = + WizardHandler.wizard('RegistrationWizard').currentStepTitle(); } function stepBack () { @@ -272,12 +278,32 @@ } } + function selectBox ($item) { + centerLatLng($item.boundingbox); + } + + // centers a latlng (marker) on the map while reserving space for the sidebar + function centerLatLng (latlng) { + osemMapData.getMap('map_register').then(function (map) { + map.fitBounds( + [ + [latlng[0], latlng[2]], + [latlng[1], latlng[3]], + ], + { + paddingTopLeft: [0, 0], + animate: false, + zoom: 18, + } + ); + }); + } + function enterEvent (keyEvent) { var stepNumber = -1; if (keyEvent.which === 13) { - stepNumber = WizardHandler.wizard( - 'RegistrationWizard' - ).currentStepNumber(); + stepNumber = + WizardHandler.wizard('RegistrationWizard').currentStepNumber(); } switch (stepNumber) { case 2: @@ -306,15 +332,15 @@ icon: icon, title: title, unit: unit, - sensorType: sensorType + sensorType: sensorType, }; vm.sensors.push(sensor); vm.sensorSetup = JSON.stringify(vm.sensors); edit(sensor.id); vm.sensorIncomplete = false; - if(vm.ttn.profile === 'cayenne-lpp') { - updateCayenneDecoding() + if (vm.ttn.profile === 'cayenne-lpp') { + updateCayenneDecoding(); } } @@ -332,8 +358,8 @@ vm.sensorSetup = JSON.stringify($scope.sensors); } - if(vm.ttn.profile === 'cayenne-lpp') { - updateCayenneDecoding() + if (vm.ttn.profile === 'cayenne-lpp') { + updateCayenneDecoding(); } } @@ -379,7 +405,7 @@ vm.registering = true; if (vm.tag !== '') { - vm.newSenseBox.grouptag = vm.tag; + vm.newSenseBox.grouptag = vm.tag.split(','); } if (vm.modelSelected.id === 'custom' || vm.modelSelected.id === 'edu') { @@ -410,6 +436,15 @@ case 'bme680': vm.newSenseBox.sensorTemplates.push('bme680'); break; + case 'co2': + vm.newSenseBox.sensorTemplates.push('scd30'); + break; + case 'dps310': + vm.newSenseBox.sensorTemplates.push('dps310'); + break; + case 'sps30': + vm.newSenseBox.sensorTemplates.push('sps30'); + break; } } } @@ -454,7 +489,7 @@ $translate('REGISTRATION_SUCCESS').then(function (msg) { var alert = { type: 'success', - msg: msg + msg: msg, }; vm.alerts.push(alert); vm.regSuccess = true; @@ -468,7 +503,7 @@ $translate('REGISTRATION_FAIL').then(function (msg) { var alert = { type: 'danger', - msg: msg + msg: msg, }; vm.alerts.push(alert); }); @@ -476,10 +511,10 @@ }); } - function cayenneLppDecodingChanged(sensor) { - vm.ttn.cayenneLppDecoding[sensor.id].sensor_title = sensor.title - vm.ttn.cayenneLppDecoding[sensor.id].sensor_type = sensor.sensorType - vm.ttn.decodeOptions = JSON.stringify(vm.ttn.cayenneLppDecoding) + function cayenneLppDecodingChanged (sensor) { + vm.ttn.cayenneLppDecoding[sensor.id].sensor_title = sensor.title; + vm.ttn.cayenneLppDecoding[sensor.id].sensor_type = sensor.sensorType; + vm.ttn.decodeOptions = JSON.stringify(vm.ttn.cayenneLppDecoding); } function senseBoxSetupValid () { @@ -497,12 +532,13 @@ return validTTN && validMQTT; } - function addSensorTemplate (template) { + function generateSensorTemplate (templateName) { var icon = ''; var title = ''; var unit = ''; var sensorType = ''; - switch (template) { + + switch (templateName) { case 'HDC1080_TEMPERATURE': icon = 'osem-thermometer'; title = 'Temperatur'; @@ -599,8 +635,110 @@ unit = 'm/s'; sensorType = 'WINDSPEED'; break; + case 'scd30_co2': + icon = 'osem-co2'; + title = 'CO₂'; + unit = 'ppm'; + sensorType = 'SCD30'; + break; + case 'DPS310_AIRPRESSURE': + icon = 'osem-barometer'; + title = 'Luftdruck'; + unit = 'hPa'; + sensorType = 'DPS310'; + break; + case 'DPS310_TEMPERATURE': + icon = 'osem-thermometer'; + title = 'Temperatur'; + unit = '°C'; + sensorType = 'DPS310'; + break; + case 'SPS30_PM1': + icon = 'osem-cloud'; + title = 'PM1'; + unit = 'µg/m³'; + sensorType = 'SPS30'; + break; + case 'SPS30_PM25': + icon = 'osem-cloud'; + title = 'PM25'; + unit = 'µg/m³'; + sensorType = 'SPS30'; + break; + case 'SPS30_PM4': + icon = 'osem-cloud'; + title = 'PM4'; + unit = 'µg/m³'; + sensorType = 'SPS30'; + break; + case 'SPS30_PM10': + icon = 'osem-cloud'; + title = 'PM10'; + unit = 'µg/m³'; + sensorType = 'SPS30'; + break; } - add(icon, title, unit, sensorType); + + return { + icon: icon, + title: title, + unit: unit, + sensorType: sensorType, + }; + } + + function removeSensorTemplate (template) { + if (template === '' || template === null || template === undefined) { + return; + } + + // Remove specific sensor template + for (var index = 0; index < vm.sensors.length; index++) { + var element = vm.sensors[index]; + if ( + element.sensorType === template.sensorType && + element.title === template.title + ) { + vm.sensors.splice(index, 1); + } + } + + // Rewrite sensor ids + var tempSensors = vm.sensors.map(function (sensor, idx) { + sensor.id = idx; + + return sensor; + }); + + angular.copy(tempSensors, vm.sensors); + } + + function addSensorTemplate (templateName) { + var template = generateSensorTemplate(templateName); + add(template.icon, template.title, template.unit, template.sensorType); + } + + function generateMarkerIcon () { + var icon = 'circle'; + var color = 'red'; + + if ( + vm.newSenseBox.exposure === 'indoor' || + vm.newSenseBox.exposure === 'outdoor' + ) { + icon = 'cube'; + color = 'green'; + } else if (vm.newSenseBox.exposure === 'mobile') { + icon = 'rocket'; + color = 'blue'; + } + + return L.AwesomeMarkers.icon({ + type: 'awesomeMarker', + prefix: 'fa', + icon: icon, + markerColor: color, + }); } //// @@ -612,18 +750,19 @@ layerName: 'registration', latLng: [ parseFloat(args.latlng.lat.toFixed(6)), - parseFloat(args.latlng.lng.toFixed(6)) + parseFloat(args.latlng.lng.toFixed(6)), ], lat: parseFloat(args.latlng.lat.toFixed(6)), lng: parseFloat(args.latlng.lng.toFixed(6)), - draggable: true - } + draggable: true, + icon: generateMarkerIcon(), + }, }; } else { vm.markers = angular.copy(vm.markers); vm.markers.box.latLng = [ parseFloat(args.latlng.lat.toFixed(6)), - parseFloat(args.latlng.lng.toFixed(6)) + parseFloat(args.latlng.lng.toFixed(6)), ]; vm.markers.box.lat = parseFloat(args.latlng.lat.toFixed(6)); vm.markers.box.lng = parseFloat(args.latlng.lng.toFixed(6)); @@ -635,7 +774,7 @@ vm.markers = angular.copy(vm.markers); vm.markers.box.latLng = [ parseFloat(args.target._latlng.lat.toFixed(6)), - parseFloat(args.target._latlng.lng.toFixed(6)) + parseFloat(args.target._latlng.lng.toFixed(6)), ]; vm.markers.box.lat = parseFloat(args.target._latlng.lat.toFixed(6)); vm.markers.box.lng = parseFloat(args.target._latlng.lng.toFixed(6)); @@ -650,12 +789,13 @@ layerName: 'registration', latLng: [ parseFloat(args.latlng.lat.toFixed(6)), - parseFloat(args.latlng.lng.toFixed(6)) + parseFloat(args.latlng.lng.toFixed(6)), ], lat: parseFloat(args.latlng.lat.toFixed(6)), lng: parseFloat(args.latlng.lng.toFixed(6)), - draggable: true - } + draggable: true, + icon: generateMarkerIcon(), + }, }; if (args.latlng.altitude) { vm.markers.box.height = parseFloat(args.latlng.altitude.toFixed(2)); @@ -664,7 +804,7 @@ vm.markers = angular.copy(vm.markers); vm.markers.box.latLng = [ parseFloat(args.latlng.lat.toFixed(6)), - parseFloat(args.latlng.lng.toFixed(6)) + parseFloat(args.latlng.lng.toFixed(6)), ]; vm.markers.box.lat = parseFloat(args.latlng.lat.toFixed(6)); vm.markers.box.lng = parseFloat(args.latlng.lng.toFixed(6)); @@ -743,6 +883,35 @@ vm.sensorSetup = ''; vm.extensions.feinstaub.id = ''; vm.newModel.connection = null; + vm.newModel.sensors = { + temp: false, + pressure: false, + light: false, + pollution: false, + bme680: false, + co2: false, + }; + vm.modelSelected.name = 'Custom'; + + vm.invalidHardware = false; + + return; + } + + if (newValue === 'edu') { + vm.sensorSetup = ''; + vm.sensors = []; + vm.extensions.feinstaub.id = ''; + vm.newModel.connection = null; + vm.newModel.sensors = { + temp: false, + pressure: false, + light: false, + pollution: false, + bme680: false, + co2: false, + }; + vm.modelSelected.name = 'senseBox Edu'; vm.invalidHardware = false; @@ -752,6 +921,121 @@ vm.modelSelected.name = false; }); + // Watch selected sensors if model is homev2 + $scope.$watch('register.newModel.sensors', function (newValue, oldValue) { + // Add sensor templates + if (newValue.temp && oldValue.temp === false) { + addSensorTemplate('HDC1080_TEMPERATURE'); + addSensorTemplate('HDC1080_HUMIDITY'); + } else if (newValue.pressure && oldValue.pressure === false) { + addSensorTemplate('BMP280_PRESSURE'); + } else if (newValue.light && oldValue.light === false) { + addSensorTemplate('VEML6070'); + addSensorTemplate('TSL45315'); + } else if (newValue.bme680 && oldValue.bme680 === false) { + addSensorTemplate('BME680_TEMPERATURE'); + addSensorTemplate('BME680_HUMIDITY'); + addSensorTemplate('BME680_PRESSURE'); + addSensorTemplate('BME680_VOC'); + } else if (newValue.co2 && oldValue.co2 === false) { + addSensorTemplate('scd30_co2'); + } else if (newValue.dps310 && oldValue.dps310 === false) { + addSensorTemplate('DPS310_AIRPRESSURE'); + addSensorTemplate('DPS310_TEMPERATURE'); + } else if (newValue.sps30 && oldValue.sps30 === false) { + addSensorTemplate('SPS30_PM1'); + addSensorTemplate('SPS30_PM25'); + addSensorTemplate('SPS30_PM4'); + addSensorTemplate('SPS30_PM10'); + } + + // Remove sensor templates + if (oldValue.temp && newValue.temp === false) { + removeSensorTemplate(generateSensorTemplate('HDC1080_TEMPERATURE')); + removeSensorTemplate(generateSensorTemplate('HDC1080_HUMIDITY')); + } else if (oldValue.pressure && newValue.pressure === false) { + removeSensorTemplate(generateSensorTemplate('BMP280_PRESSURE')); + } else if (oldValue.light && newValue.light === false) { + removeSensorTemplate(generateSensorTemplate('VEML6070')); + removeSensorTemplate(generateSensorTemplate('TSL45315')); + } else if (oldValue.bme680 && newValue.bme680 === '') { + removeSensorTemplate(generateSensorTemplate('BME680_TEMPERATURE')); + removeSensorTemplate(generateSensorTemplate('BME680_HUMIDITY')); + removeSensorTemplate(generateSensorTemplate('BME680_PRESSURE')); + removeSensorTemplate(generateSensorTemplate('BME680_VOC')); + } else if (oldValue.co2 && newValue.co2 === '') { + removeSensorTemplate(generateSensorTemplate('scd30_co2')); + } else if (oldValue.dps310 && newValue.dps310 === '') { + removeSensorTemplate(generateSensorTemplate('DPS310_AIRPRESSURE')); + removeSensorTemplate(generateSensorTemplate('DPS310_TEMPERATURE')); + } else if (oldValue.sps30 && newValue.sps30 === '') { + removeSensorTemplate(generateSensorTemplate('SPS30_PM1')); + removeSensorTemplate(generateSensorTemplate('SPS30_P25')); + removeSensorTemplate(generateSensorTemplate('SPS30_PM4')); + removeSensorTemplate(generateSensorTemplate('SPS30_PM10')); + } + + // Check on change for sensors with same address + if ([vm.newModel.sensors.bme680, vm.newModel.sensors.dps310, vm.newModel.sensors.pressure].filter(Boolean).length >= 2) { + vm.showSameAddressWarning = true; + } else { + vm.showSameAddressWarning = false; + } + }, true); + + // Watch extensions because they also add sensors + $scope.$watch( + 'register.extensions', + function (newValue, oldValue) { + // Add sensor template + if (newValue.feinstaub.id !== '') { + addSensorTemplate('PM25'); + addSensorTemplate('PM10'); + } else if (newValue.soilMoisture.id !== '') { + addSensorTemplate('smt50_soilmoisture'); + addSensorTemplate('smt50_soiltemperature'); + } else if (newValue.soundLevelMeter.id !== '') { + addSensorTemplate('soundlevelmeter'); + } else if (newValue.windSpeed.id !== '') { + addSensorTemplate('windspeed'); + } + + // Remove sensor template + if (newValue.feinstaub.id === '' && oldValue.feinstaub.id !== '') { + removeSensorTemplate(generateSensorTemplate('PM25')); + removeSensorTemplate(generateSensorTemplate('PM10')); + } else if ( + newValue.soilMoisture.id === '' && + oldValue.soilMoisture.id !== '' + ) { + removeSensorTemplate(generateSensorTemplate('smt50_soilmoisture')); + removeSensorTemplate(generateSensorTemplate('smt50_soiltemperature')); + } else if ( + newValue.soundLevelMeter.id === '' && + oldValue.soundLevelMeter.id !== '' + ) { + removeSensorTemplate(generateSensorTemplate('soundlevelmeter')); + } else if ( + newValue.windSpeed.id === '' && + oldValue.windSpeed.id !== '' + ) { + removeSensorTemplate(generateSensorTemplate('windspeed')); + } + }, + true + ); + + // Watch added sensors if selected model is custom or edu + $scope.$watch( + 'register.sensors', + function () { + if (vm.ttn.profile === 'cayenne-lpp') { + updateCayenneDecoding(); + } + }, + true + ); + $scope.$watch('register.newModel.connection', function (newValue) { if (newValue === 'Lora') { vm.ttnEnabled = true; @@ -764,39 +1048,54 @@ $scope.$watch('register.ttn.profile', function (newValue) { if (newValue === 'cayenne-lpp') { - updateCayenneDecoding() + updateCayenneDecoding(); } else { vm.ttn.cayenneLppDecoding = []; - vm.ttn.decodeOptions = JSON.stringify(vm.ttn.cayenneLppDecoding) + vm.ttn.decodeOptions = JSON.stringify(vm.ttn.cayenneLppDecoding); } }); - function updateCayenneDecoding() { - vm.ttn.cayenneLppDecoding = vm.sensors.map(function(sensor) { + function updateCayenneDecoding () { + var tempCayenneLppDecoding = vm.sensors.map(function (sensor) { var decoderGuess = 'analog_in'; - var tempSubstr = ['temp'] - var humiSubstr = ['humi', 'feucht'] - var pressSubstr = ['press', 'druck'] - var illuSubstr = ['hell', 'illu', 'uv', 'beleuch'] - if(new RegExp(tempSubstr.join('|')).test(sensor.title.toLowerCase())) { - decoderGuess = 'temperature' - } else if(new RegExp(humiSubstr.join('|')).test(sensor.title.toLowerCase())) { - decoderGuess = 'relative_humidity' - } else if(new RegExp(pressSubstr.join('|')).test(sensor.title.toLowerCase())) { - decoderGuess = 'barometric_pressure' - } else if(new RegExp(illuSubstr.join('|')).test(sensor.title.toLowerCase())) { - decoderGuess = 'luminosity' - } - + var tempSubstr = ['temp']; + var humiSubstr = ['humi', 'feucht']; + var pressSubstr = ['press', 'druck']; + var illuSubstr = ['hell', 'illu', 'uv', 'beleuch']; + + // Title could be undefined in manual configuration after adding a sensor + if (sensor.title) { + if ( + new RegExp(tempSubstr.join('|')).test(sensor.title.toLowerCase()) + ) { + decoderGuess = 'temperature'; + } else if ( + new RegExp(humiSubstr.join('|')).test(sensor.title.toLowerCase()) + ) { + decoderGuess = 'relative_humidity'; + } else if ( + new RegExp(pressSubstr.join('|')).test(sensor.title.toLowerCase()) + ) { + decoderGuess = 'barometric_pressure'; + } else if ( + new RegExp(illuSubstr.join('|')).test(sensor.title.toLowerCase()) + ) { + decoderGuess = 'luminosity'; + } + } + return Object.assign({ sensor_title: sensor.title, sensor_type: sensor.sensorType, decoder: decoderGuess, - channel: 1 - }, vm.ttn.cayenneLppDecoding[sensor.id]) + channel: 1, + }); }); - vm.ttn.decodeOptions = JSON.stringify(vm.ttn.cayenneLppDecoding) + + // Copy and apply changes to scope variables + angular.copy(tempCayenneLppDecoding, vm.ttn.cayenneLppDecoding); + vm.ttn.decodeOptions = JSON.stringify(vm.ttn.cayenneLppDecoding); } // check if valid json for ttn decodeOptions @@ -836,93 +1135,75 @@ } }); - $scope.$watchCollection('register.open', function ( - newAccordion, - oldAccordion - ) { - if ( - vm.tag === 'edu' && - newAccordion && - oldAccordion && - oldAccordion.edu === true && - newAccordion.edu === false - ) { - vm.tag = ''; - } - if (newAccordion) { - if (newAccordion.custom === true) { - vm.modelSelected.id = 'custom'; - } else if (newAccordion.edu === true) { - vm.modelSelected.id = 'edu'; - if (vm.tag === '') { - vm.tag = 'edu'; - } + $scope.$watchCollection( + 'register.open', + function (newAccordion, oldAccordion) { + if ( + vm.tag === 'edu' && + newAccordion && + oldAccordion && + oldAccordion.edu === true && + newAccordion.edu === false + ) { + vm.tag = ''; } - } - }); - - $scope.$watchCollection('register.editMarkerInput.box', function ( - newValue, - oldValue - ) { - if ( - newValue && - newValue.lat && - newValue.lng && - !angular.equals(newValue, oldValue) - ) { - if (Object.keys(vm.markers).length === 0) { - vm.markers = { - box: { - latLng: [ - parseFloat(newValue.lat.toFixed(6)), - parseFloat(newValue.lng.toFixed(6)) - ], - lat: parseFloat(newValue.lat.toFixed(6)), - lng: parseFloat(newValue.lng.toFixed(6)), - height: newValue.height, - draggable: true + if (newAccordion) { + if (newAccordion.custom === true) { + vm.modelSelected.id = 'custom'; + } else if (newAccordion.edu === true) { + vm.modelSelected.id = 'edu'; + if (vm.tag === '') { + vm.tag = 'edu'; } - }; - } else { - vm.markers = angular.copy(vm.markers); - (vm.markers.box.latLng = [ - parseFloat(newValue.lat.toFixed(6)), - parseFloat(newValue.lng.toFixed(6)) - ]), - (vm.markers.box.lat = parseFloat(newValue.lat.toFixed(6))); - vm.markers.box.lng = parseFloat(newValue.lng.toFixed(6)); - vm.markers.box.height = newValue.height; + } } - vm.editMarkerInput = angular.copy(vm.markers); - } - }); - - $scope.$watchCollection('register.newSenseBox.exposure', function ( - newValue - ) { - if (newValue === '') { - return; } - var icon = ''; - var color = ''; + ); - if (newValue === 'indoor' || newValue === 'outdoor') { - icon = 'cube'; - color = 'green'; + $scope.$watchCollection( + 'register.editMarkerInput.box', + function (newValue, oldValue) { + if ( + newValue && + newValue.lat && + newValue.lng && + !angular.equals(newValue, oldValue) + ) { + if (Object.keys(vm.markers).length === 0) { + vm.markers = { + box: { + layerName: 'registration', + latLng: [ + parseFloat(newValue.lat.toFixed(6)), + parseFloat(newValue.lng.toFixed(6)), + ], + lat: parseFloat(newValue.lat.toFixed(6)), + lng: parseFloat(newValue.lng.toFixed(6)), + height: newValue.height, + draggable: true, + icon: generateMarkerIcon(), + }, + }; + } else { + vm.markers = angular.copy(vm.markers); + (vm.markers.box.latLng = [ + parseFloat(newValue.lat.toFixed(6)), + parseFloat(newValue.lng.toFixed(6)), + ]), + (vm.markers.box.lat = parseFloat(newValue.lat.toFixed(6))); + vm.markers.box.lng = parseFloat(newValue.lng.toFixed(6)); + vm.markers.box.height = newValue.height; + } + vm.editMarkerInput = angular.copy(vm.markers); + } } + ); - if (newValue === 'mobile') { - icon = 'rocket'; - color = 'blue'; + $scope.$watchCollection('register.newSenseBox.exposure', function () { + if (vm.markers.box) { + vm.markers = angular.copy(vm.markers); + vm.markers.box.icon = generateMarkerIcon(); } - vm.markers = angular.copy(vm.markers); - vm.markers.box.icon = L.AwesomeMarkers.icon({ - type: 'awesomeMarker', - prefix: 'fa', - icon: icon, - markerColor: color - }); }); } })(); diff --git a/app/scripts/controllers/sidebar.error.js b/app/scripts/controllers/sidebar.error.js index a0c899b9..24b02be5 100644 --- a/app/scripts/controllers/sidebar.error.js +++ b/app/scripts/controllers/sidebar.error.js @@ -8,8 +8,6 @@ SidebarErrorController.$inject = []; function SidebarErrorController () { - var vm = this; - activate(); //////////////// diff --git a/app/scripts/controllers/sidebar.js b/app/scripts/controllers/sidebar.js index 849adce3..36a83d05 100644 --- a/app/scripts/controllers/sidebar.js +++ b/app/scripts/controllers/sidebar.js @@ -21,7 +21,7 @@ function activate () { vm.maximizedStyle = { 'bottom': '0px', - 'top': '100px' + 'top': '150px' }; invalidateSize(); @@ -35,6 +35,10 @@ function invalidateSize () { var elem = $window.document.querySelector('#sidebar-title'); + var announcementContainer = $window.document.querySelector( + '#announcement-container' + ); + var announcementDisplay = $window.getComputedStyle(announcementContainer).display; var leafletBottomContainer = $window.document.querySelector('.leaflet-bottom.leaflet-left'); if (elem === null) { return; @@ -60,6 +64,11 @@ 'bottom': vm.maximizedStyle.bottom, 'top': vm.maximizedStyle.top }; + + if (announcementDisplay === 'none') { + vm.style['top'] = '100px'; + } + if (isMobile.phone && leafletBottomContainer !== null) { leafletBottomContainer.setAttribute('style', 'bottom: 0px'); } @@ -73,5 +82,16 @@ invalidateSize(); }); }); + + $scope.$on('osemAnnouncementClosed', function () { + vm.maximizedStyle = { + 'bottom': '0px', + 'top': '100px' + }; + + $timeout(function () { + invalidateSize(); + }); + }); } })(); diff --git a/app/scripts/models/box.js b/app/scripts/models/box.js index caa4b36e..158935be 100644 --- a/app/scripts/models/box.js +++ b/app/scripts/models/box.js @@ -86,12 +86,14 @@ destObj.markerOptions = OpenSenseMapData.makeMarkerOptions(srcObj); for (var key in srcObj) { if (key === 'sensors') { - destObj['sensorsArray'] = srcObj.sensors; + // destObj['sensorsArray'] = srcObj.sensors; + destObj['sensorsArray'] = []; destObj['sensors'] = {}; for (var index in srcObj.sensors) { if (index) { var sensor = new Sensor(srcObj.sensors[index]); destObj.sensors[sensor._id] = sensor; + destObj.sensorsArray.push(sensor); } } } else { diff --git a/app/scripts/services/account.js b/app/scripts/services/account.js index 89fa3496..10c9f8c2 100644 --- a/app/scripts/services/account.js +++ b/app/scripts/services/account.js @@ -27,7 +27,11 @@ deleteMeasurement: deleteMeasurement, postNewBox: postNewBox, deleteAccount: deleteAccount, - compileSketch: compileSketch + compileSketch: compileSketch, + transferDevice: transferDevice, + revokeToken: revokeToken, + getTransferToken: getTransferToken, + claimDevice: claimDevice }; return service; @@ -130,15 +134,22 @@ } } - function getUsersBoxes () { - return $http.get(app.API_URL + '/users/me/boxes', { auth: true }) + function getUsersBoxes (page) { + + page = (page === undefined) ? 0 : page; + + return $http.get(app.API_URL + '/users/me/boxes' + '?page=' + page, { auth: true }) .then(getUsersBoxesComplete) .catch(getUsersBoxesFailed); function getUsersBoxesComplete (response) { - return response.data.data.boxes.map(function (b) { + // Map response to Box model and replace it in response + const boxes = response.data.data.boxes.map(function (b) { return new Box(b); }); + response.data.data.boxes = boxes; + + return response.data; } function getUsersBoxesFailed (error) { @@ -246,5 +257,51 @@ }) .catch(failed); } + + function getTransferToken (boxId) { + return $http + .get(app.API_URL + '/boxes/transfer/' + boxId, { + auth: true, + }) + .then(function (response) { + return response.data; + }) + .catch(failed); + } + + function transferDevice (data) { + return $http.post(app.API_URL + '/boxes/transfer', data, { + auth: true, + }) + .then(function (response) { + return response.data; + }) + .catch(failed); + } + + function revokeToken (data) { + return $http + .delete(app.API_URL + '/boxes/transfer', { + auth: true, + data: data, + headers: { + 'Content-type': 'application/json;charset=utf-8', + }, + }) + .then(function (response) { + return response; + }) + .catch(failed); + } + + function claimDevice (data) { + return $http.post(app.API_URL + '/boxes/claim', data, { + auth: true + }) + .then(function (response) { + return response.data; + }) + .catch(failed); + } } })(); diff --git a/app/scripts/services/authenticationInterceptor.js b/app/scripts/services/authenticationInterceptor.js index 7217505a..a5301f5f 100644 --- a/app/scripts/services/authenticationInterceptor.js +++ b/app/scripts/services/authenticationInterceptor.js @@ -31,8 +31,6 @@ inFlightAuthRequest.then(function (r) { inFlightAuthRequest = null; if (r.data.token && r.data.refreshToken) { - console.log('Token succesfully refreshed', r.data); - AuthenticationService.saveToken(r.data.token); AuthenticationService.saveRefreshToken(r.data.refreshToken); AuthenticationService.saveUser(JSON.stringify(r.data.data.user)); @@ -56,6 +54,7 @@ }); return deferred.promise; + case 400: case 404: return $q.reject(response); default: diff --git a/app/scripts/services/directus.js b/app/scripts/services/directus.js new file mode 100644 index 00000000..2f80876c --- /dev/null +++ b/app/scripts/services/directus.js @@ -0,0 +1,34 @@ +(function () { + 'use strict'; + + angular + .module('app.services') + .factory('DirectusService', DirectusService); + + DirectusService.$inject = ['$http', '$q', 'app']; + + function DirectusService ($http, $q, app) { + var service = { + getAnnouncement: getAnnouncement + }; + + return service; + + //// + + function success (response) { + return response.data; + } + + function failed (error) { + return $q.reject(error.data); + } + + function getAnnouncement () { + return $http + .get(app.CMS_URL + '/items/announcement') + .then(success) + .catch(failed); + } + } +})(); diff --git a/app/scripts/services/donationmodal.js b/app/scripts/services/donationmodal.js new file mode 100644 index 00000000..c27af052 --- /dev/null +++ b/app/scripts/services/donationmodal.js @@ -0,0 +1,57 @@ +(function () { + 'use strict'; + + angular + .module('app.services') + .factory('DonationModalService', DonationModalService); + + DonationModalService.$inject = ['ngDialog', '$cookies']; + + //COOKIE FUNCTIONALITY DISABLED FOR NOW (add again later?) + var cookieId = 'dontShowDonationAgain'; + + function DonationModalService (ngDialog, $cookies) { + var vm = this; + vm.hasCookie = hasCookie; + vm.onStartup = onStartup; + vm.open = open; + vm.setDontShowAgain = setDontShowAgain; + + return vm; + + function open () { + ngDialog.open({ + template: '../../views/donation.html', + className: 'ngdialog-theme-default', + appendClassName: 'donation-modal', + showClose: true, + closeByDocument: true, + controller: 'DonationController', + controllerAs: 'donation' + }); + } + + function setDontShowAgain (value) { + if (value) { + var currentDate = new Date(); + currentDate = new Date(currentDate.setDate(currentDate.getDate() + 2 * 7)); + $cookies.put(cookieId, 'true', { secure: true, expires: currentDate }); + // $cookies.put(cookieId, 'true', { secure: true }); + } else { + $cookies.remove(cookieId); + } + } + + function hasCookie () { + var cookie = $cookies.get(cookieId); + + return Boolean(cookie); + } + + function onStartup () { + if (!vm.hasCookie()) { + vm.open(); + } + } + } +})(); diff --git a/app/scripts/services/helpmodal.js b/app/scripts/services/helpmodal.js new file mode 100644 index 00000000..7a8bc1cf --- /dev/null +++ b/app/scripts/services/helpmodal.js @@ -0,0 +1,55 @@ +(function () { + 'use strict'; + + angular + .module('app.services') + .factory('HelpModalService', HelpModalService); + + HelpModalService.$inject = ['ngDialog' /*, '$cookies' */]; + + //COOKIE FUNCTIONALITY DISABLED FOR NOW (add again later?) + // var cookieId = 'dontShowHelpAgain'; + + function HelpModalService (ngDialog /*, $cookies */) { + var vm = this; + // vm.hasCookie = hasCookie; + vm.onStartup = onStartup; + vm.open = open; + // vm.setDontShowAgain = setDontShowAgain; + + return vm; + + function open () { + ngDialog.open({ + template: '../../views/help.html', + className: 'ngdialog-theme-default', + appendClassName: 'help-modal', + showClose: true, + closeByDocument: true, + controller: 'HelpController', + controllerAs: 'help' + }); + } + + // function setDontShowAgain (value) { + // if (value) { + // $cookies.put(cookieId, 'true', { secure: true }); + // } else { + // $cookies.remove(cookieId); + // } + // } + + // function hasCookie () { + // var cookie = $cookies.get(cookieId); + + // return Boolean(cookie); + // } + + function onStartup () { + // if (!vm.hasCookie()) { + // vm.open(); + // } + vm.open(); + } + } +})(); diff --git a/app/scripts/services/opensensemapapi.js b/app/scripts/services/opensensemapapi.js index ef105932..90b4736b 100644 --- a/app/scripts/services/opensensemapapi.js +++ b/app/scripts/services/opensensemapapi.js @@ -92,12 +92,12 @@ .catch(failed); } - function postMeasurements (boxId, measurements, format) { + function postMeasurements (boxId, measurements, format, access_token) { var url = getUrl() + '/boxes/' + boxId + '/data'; return $http .post(url, measurements, { - headers: { 'content-type': format } + headers: { 'content-type': format, 'Authorization': access_token } }) .then(success) .catch(failed); diff --git a/app/scripts/services/sensoricons.js b/app/scripts/services/sensoricons.js index f205cada..98bd37af 100644 --- a/app/scripts/services/sensoricons.js +++ b/app/scripts/services/sensoricons.js @@ -30,7 +30,8 @@ { name: 'osem-microphone' }, { name: 'osem-wifi' }, { name: 'osem-battery' }, - { name: 'osem-radioactive' } + { name: 'osem-radioactive' }, + { name: 'osem-co2' } ]; return icons; diff --git a/app/styles/main.css b/app/styles/main.css index d56134c2..8f211b34 100644 --- a/app/styles/main.css +++ b/app/styles/main.css @@ -22,14 +22,47 @@ a:focus { text-decoration: none; } +.navbar-fixed-top-announcment { + top: 50px; +} + .monospace { font-family: Courier New, Courier, monospace; } +section { + padding: 20px; +} + +.announcement-container { + width: 100%; +} + +.announcement-content { + display: flex; + position: fixed; + top: 0; + min-height: 50px; + width: 100%; + z-index: 1003; +} + +.announcement-content p { + flex-grow: 1; + margin: 0; + text-align: center; +} + .center { margin-left: auto; margin-right: auto; } +.centerimg { + display: block; + margin-left: auto; + margin-right: auto; + width: 50%; +} .navbar-form { border-top: 0px; @@ -115,11 +148,11 @@ a:focus { .content-container { position: absolute; - top: 0; + top: 40px; left: 0; bottom: 0; width: 100%; - height: 100vh; + /* height: 100vh; */ padding-left: 15px; padding-right: 15px; } @@ -158,7 +191,7 @@ a:focus { } #map_main .leaflet-top { - top: 125px; + top: 175px; } #app_info_button { @@ -582,6 +615,14 @@ div.box { } } @media (max-width: 767px) { + .announcement-content { + min-height: 70px; + height: 70px; + width: 100%; + } + .navbar-fixed-top-announcment { + top: 70px; + } .languages-select.visible-xs { display:inline-block !important; } @@ -782,7 +823,7 @@ li.menuitem.filteractive > a .fa-exclamation-triangle { } } -@media (max-width: 1100px) { +@media (max-width: 1299px) { .navbar-header { float: none; } @@ -797,7 +838,7 @@ li.menuitem.filteractive > a .fa-exclamation-triangle { box-shadow: inset 0 1px 0 rgba(255,255,255,0.1); } .navbar-fixed-top { - top: 0; + /* top: 50px; */ border-width: 0 0 1px; } .navbar-collapse.collapse { @@ -1787,4 +1828,148 @@ nav > a.active { .break { display: block !important; -} \ No newline at end of file +} + +.ngdialog.help-modal .ngdialog-content { + width: 800px; + /* height: 100%; */ +} + +.help-wrapper { + display: flex; + flex-direction: column; + height: 100%; + background: url('data:image/svg+xml;charset=utf-8,⏳') 0px 0px no-repeat; +} + +.external-help { + flex: 1; + border: none; + width: 100%; + height: 100%; + margin-top: 10px; +} + +.help-footer { + display: flex; + justify-content: flex-end; + align-items: center; + margin-top: 10px; +} + +.help-footer > input, .help-footer > label { + padding: 0 !important; + margin: 0 5px 0 0 !important; +} + +.help-footer > button { + margin-right: 5px; +} + +.donation-wrapper.inline-donation { + + display: flex; + flex-direction: column; + align-items: center; + padding-bottom: 4em; +} + +.inner-wrapper-donation { + max-width: 800px; +} + +.inline-donation { + font-size: 1.2em; +} + +.donation-text { + color: black; +} + +.donation-footer { + display: flex; + flex-direction: column; +} + +.donation-footer div { + align-self: flex-end; +} + +.donation-button { + background-color: #4EAF47 !important; + color: white !important; + border-radius: 1; +} + +.donation-button:hover { + background-color: #4EAF47; +} + +.donor-list { + list-style: none; + text-align: center; + font-size: 1.2em; + padding-bottom: 3em; + padding-left: 0; +} + +.faq-wrapper { + height: 1900px; +} +@media (max-width: 767px) { + .faq-wrapper { + height: 2800px; + } +} + +.figure img { + max-width: 100%; + max-height: 100%; +} + +.small { + font-size: 0.8em; +} + +.small #dont-show-again { + position: relative; + top: 4px; + margin: 3px; +} + +.ngdialog.ngdialog-theme-default .ngdialog-content { + width: 600px; +} + +.hello_weigo{ + color:green; +} + +/* Create three equal columns that floats next to each other */ +.column { + float: left; + width: 33.33%; + padding: 10px; + height: 200px; /* Should be removed. Only for demonstration */ +} + +/* Clear floats after the columns */ +.row:after { + content: ""; + display: table; + clear: both; +} + +/* Responsive layout - makes the three columns stack on top of each other instead of next to each other */ +@media screen and (max-width: 600px) { + .column { + width: 100%; + } +} +.vertical-center { + margin: 0; + position: absolute; + top: 50%; + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} diff --git a/app/styles/osemicons.css b/app/styles/osemicons.css index a4c80e17..42720a8d 100755 --- a/app/styles/osemicons.css +++ b/app/styles/osemicons.css @@ -1,10 +1,10 @@ @font-face { font-family: 'opensensemap'; - src: url('../fonts/opensensemap.eot?9c7p1f'); - src: url('../fonts/opensensemap.eot?9c7p1f#iefix') format('embedded-opentype'), - url('../fonts/opensensemap.ttf?9c7p1f') format('truetype'), - url('../fonts/opensensemap.woff?9c7p1f') format('woff'), - url('../fonts/opensensemap.svg?9c7p1f#opensensemap') format('svg'); + src: url('../fonts/opensensemap.eot?d8dt7p'); + src: url('../fonts/opensensemap.eot?d8dt7p#iefix') format('embedded-opentype'), + url('../fonts/opensensemap.ttf?d8dt7p') format('truetype'), + url('../fonts/opensensemap.woff?d8dt7p') format('woff'), + url('../fonts/opensensemap.svg?d8dt7p#opensensemap') format('svg'); font-weight: normal; font-style: normal; } @@ -159,3 +159,6 @@ .osem-battery:before { content: "\f242"; } +.osem-co2:before { + content: "\e900"; +} diff --git a/app/views/account.box.dataupload.html b/app/views/account.box.dataupload.html index 453d4b50..55e37b43 100644 --- a/app/views/account.box.dataupload.html +++ b/app/views/account.box.dataupload.html @@ -29,7 +29,7 @@

{{'DATAUPLOAD_TITLE'|translate}}
- + {{'DATAUPLOAD_SELECTFILE'|translate}}
diff --git a/app/views/account.box.edit.html b/app/views/account.box.edit.html index f415b8c6..e7b6c022 100644 --- a/app/views/account.box.edit.html +++ b/app/views/account.box.edit.html @@ -19,6 +19,7 @@
  • {{'EDIT_SCRIPT'|translate}}
  • {{'EDIT_MQTTOPTIONS'|translate}}
  • {{'EDIT_TTN'|translate}}
  • +
  • {{'EDIT_TRANSFER'|translate}}
  • diff --git a/app/views/account.box.edit.mqtt.html b/app/views/account.box.edit.mqtt.html index 8d09d144..7dc02ccf 100644 --- a/app/views/account.box.edit.mqtt.html +++ b/app/views/account.box.edit.mqtt.html @@ -22,7 +22,15 @@

    {{'EDIT_MQTTOPTIONS'|translate}}
    - +
    diff --git a/app/views/account.box.edit.script.html b/app/views/account.box.edit.script.html index 6d7be7fc..8611ef62 100644 --- a/app/views/account.box.edit.script.html +++ b/app/views/account.box.edit.script.html @@ -3,10 +3,33 @@

    {{'EDIT_SCRIPT'|translate}}

    {{'CONFIGURATION'|translate}}

    -
    + +
    +
    +
    + + +
    + {{'ENABLE_DEBUG' | translate}} +
    +
    +
    +
    +
    + + +
    + + {{'DISPLAY_ENABLED' | translate}} +
    +
    {{'EDIT_SECURITY' | translate}}<
    -
    {{'LUFTDATEN_AUTH_HEADER'}}
    -

    {{'LUFTDATEN_AUTH_TEXT'}}

    +
    {{'LUFTDATEN_AUTH_HEADER' | translate}}
    +

    {{'LUFTDATEN_AUTH_TEXT' | translate}}

    -
    {{'HOME_AUTH_HEADER'}}
    -

    {{'HOME_AUTH_TEXT'}}

    +
    {{'HOME_AUTH_HEADER' | translate}}
    +

    {{'HOME_AUTH_TEXT' | translate}}

    -
    {{'CUSTOM_AUTH_HEADER'}}
    -

    {{'CUSTOM_AUTH_TEXT'}}

    +
    {{'CUSTOM_AUTH_HEADER' | translate}}
    +

    {{'CUSTOM_AUTH_TEXT' | translate}}

    @@ -57,7 +57,7 @@
    {{'CUSTOM_AUTH_HEADER'}}

    - {{'GENERATE_NEW_TOKEN_WARNING' | translate}} +

    @@ -73,7 +73,7 @@
    @@ -400,7 +422,7 @@

    {{'SENSORS' | translate}}

    - {{'SOIL_MOISTURE' | translate}} + {{'SOIL_MOISTURE' | translate}} (SMT50)
    {{'SENSORS' | translate}}

    +
    +
    +
    +
    + + +
    + + CO2 (SCD30) +
    +
    +
    +
    +
    +
    +
    + + +
    + + {{'SENSOR_PM_SPS'| translate }} +
    +
    +
    @@ -622,30 +678,28 @@

    {{'TEMPERATURE' | translate}}

    -
    -

    {{'ILLUMINATION' | translate}}

    -

    (TSL45315)

    +

    {{'AIRPRESSURE' | translate}}

    +

    (DPS310)

    + ng-click="register.addSensorTemplate('DPS310_AIRPRESSURE')">{{'ADD_SENSOR'|translate}}

    -
    -

    {{'UV' | translate}}

    -

    (VEML6070)

    +

    {{'TEMPERATURE' | translate}}

    +

    (DPS310)

    + ng-click="register.addSensorTemplate('DPS310_TEMPERATURE')">{{'ADD_SENSOR'|translate}}

    @@ -653,14 +707,14 @@

    {{'UV' | translate}}

    -

    {{'PARTICULLARMATTER' | translate}} PM2.5

    -

    (SDS011)

    +

    {{'ILLUMINATION' | translate}}

    +

    (TSL45315)

    + ng-click="register.addSensorTemplate('TSL45315')">{{'ADD_SENSOR'|translate}}

    @@ -668,14 +722,14 @@

    {{'PARTICULLARMATTER' | translate}} PM2.5

    -

    {{'PARTICULLARMATTER' | translate}} PM10

    -

    (SDS011)

    +

    {{'UV' | translate}}

    +

    (VEML6070)

    + ng-click="register.addSensorTemplate('VEML6070')">{{'ADD_SENSOR'|translate}}

    @@ -740,6 +794,36 @@

    VOC

    +
    +
    + +
    +

    {{'PARTICULLARMATTER' | translate}} PM2.5

    +

    (SDS011)

    +

    + +

    +
    +
    +
    +
    +
    + +
    +

    {{'PARTICULLARMATTER' | translate}} PM10

    +

    (SDS011)

    +

    + +

    +
    +
    +
    Bodentemperatur style="vertical-align: middle; height: 90px; width: 90px;" alt="">

    {{'SOUND_LEVEL'|translate}}

    +

     

    @@ -789,6 +874,7 @@

    {{'SOUND_LEVEL'|translate}}

    style="vertical-align: middle; height: 90px; width: 90px;" alt="">

    {{'WIND_SPEED'|translate}}

    +

     

    @@ -796,6 +882,81 @@

    {{'WIND_SPEED'|translate}}

    +
    +
    + +
    +

    CO2

    +

    (SCD30)

    +

    + +

    +
    +
    +
    +
    +
    + +
    +

    Feinstaub PM1

    +

    (SPS30)

    +

    + +

    +
    +
    +
    +
    +
    + +
    +

    Feinstaub PM25

    +

    (SPS30)

    +

    + +

    +
    +
    +
    +
    +
    + +
    +

    Feinstaub PM4

    +

    (SPS30)

    +

    + +

    +
    +
    +
    +
    +
    + +
    +

    Feinstaub PM10

    +

    (SPS30)

    +

    + +

    +
    +
    +


    @@ -888,9 +1049,16 @@

    {{'WIND_SPEED'|translate}}

    - luftdaten.info + + + +
    @@ -982,6 +1150,20 @@

    {{'WIND_SPEED'|translate}}

    {{'LD_INFO_PMS1003_WITHOUT' | translate}}
    +
    + + +
    +
    + + +
    @@ -1013,6 +1195,7 @@

    {{'WIND_SPEED'|translate}}


    +

    @@ -1264,7 +1447,7 @@

    {{ 'STEP2_SUB4' | translate }}

    - - + + @@ -1432,9 +1615,24 @@

    {{'SENSORS' | translate }} & IDs

    Arduino Code

    - @@ -1331,13 +1514,13 @@

    senseBox

    - + - + @@ -1351,7 +1534,7 @@

    senseBox

    {{register.newModel.connection}}
    senseBox {{'EDIT_EXTENSIONS'|translate}}{{'EDIT_EXTENSIONS'|translate}} {{'EXTENSIONS_FINE_PARTICULATE_MATTER'|translate}}
    senseBox NameName {{register.newSenseBox.name}}
    -
    +

    {{'SENSORS' | translate}}

      @@ -1408,8 +1591,8 @@

      senseBox

    senseBox ID {{register.newSenseBox.id}}
    senseBox access token
    {{'ACCESS_TOKEN'|translate}} {{register.newSenseBox.access_token}}