Compare commits

...

10 Commits

Author SHA1 Message Date
Norbert Schmidt
12fb3f27b4 Update README.md 2023-02-27 13:08:58 +01:00
Norbert Schmidt
5cdeb98493 Update README.md 2023-02-27 13:08:26 +01:00
Norbert Schmidt
c2c9ac6bb6 Update Readme 2023-02-27 13:06:25 +01:00
Norbert Schmidt
f4481d175e Updated env 2023-02-22 10:40:24 +01:00
Norbert Schmidt
f90c564a3d More plantnet bugfixes. But working now 2023-02-22 09:08:38 +01:00
Norbert Schmidt
b1c26baea0 Update README, added example and fixed issue with plantnet 2023-02-22 07:20:16 +01:00
Norbert Schmidt
0d99d94642 Update README.md 2023-02-07 15:39:40 +01:00
Norbert Schmidt
ac973b7288 Fixed auth on iOS, improved iSPEX and PlantNet Plugins 2023-02-07 15:35:40 +01:00
Norbert Schmidt
96734e4f60 Update config auth 2023-01-16 11:38:16 +01:00
Norbert Schmidt
ef5d8e15d4 Updated parse lib in mini secchi submission 2023-01-13 08:59:13 +01:00
77 changed files with 2212 additions and 1663 deletions

View File

@@ -1,14 +0,0 @@
package io.ionic.starter;
import com.getcapacitor.BridgeActivity;
import com.codetrixstudio.capacitor.GoogleAuth.GoogleAuth;
import android.os.Bundle; // required for onCreate parameter
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
registerPlugin(GoogleAuth.class);
}
}

View File

@@ -29,6 +29,50 @@ This readme acts as a reference for the mobis software and / or documentation.
## Audience
Scientists, (app) developers, project partners, people who are lookikng for low cost sensor integration solutions.
## Documentation
More in depth documentation is available on ZENODO: https://zenodo.org/record/7615472#.Y-JidC8w2gQ
## Installation and setup
### Prerequisites
- Node.js
- Ionic / capactior
- Git
- Xcode (for iOS)
- Android Studio (for Android)
A Parse server (for example the one provided by Pocket Science, DIY or Back4App) is required for the app to work.
A Firebase account for login and push notifications is required.
### Installation
Rename the provided .app. constant example to app.copnstnt and fill in the required fields.
1. Clone the repository
2. Install the dependencies
npm install
3. Add these entries to Android and ios
### Android manifest
android.permission.INTERNET
android.permission.ACCESS_COARSE_LOCATION
android.permission.ACCESS_FINE_LOCATION
android.permission.BLUETOOTH
android.permission.BLUETOOTH_SCAN
android.permission.BLUETOOTH_ADVERTISE
android.permission.CAMERA
android.permission.VIBRATE
android.permission.WRITE_EXTERNAL_STORAGE
### IOS capability
Add capabilities: Push notifications and Apple logon
Needed permissions (add to info.plist): camera, bluetooth, GPS
## Generic
Provided functionalities in R 1.0:
@@ -42,9 +86,12 @@ Push notifications
# Plugin description
### General description
We created some sensor plugins for example. Sensors can be native (camera, GPS) or external (iSPEX and Canair.io)
## CANAIR.IO
### Description
Canair.io (see website) is a bluetooth peripheral to get PM2.5 and CO2 (depending on model) out. Connection is made via bluetooth.
## MINI SECCHI
### Description of the mobis Mini Secchi Plugin

View File

@@ -8,7 +8,7 @@
"plugins": {
"GoogleAuth": {
"scopes": ["profile", "email"],
"clientId": "778522712103-tqkc468qermr7hvbqsmm2p2ueglcmjsi.apps.googleusercontent.com",
"clientId": "your key here.apps.googleusercontent.com",
"forceCodeForRefreshToken" : true
}
}

View File

@@ -9,7 +9,7 @@ const config: CapacitorConfig = {
// eslint-disable-next-line @typescript-eslint/naming-convention
GoogleAuth: {
scopes: ['profile', 'email'],
androidClientId: '778522712103-tqkc468qermr7hvbqsmm2p2ueglcmjsi.apps.googleusercontent.com',
androidClientId: '778522712103-put key here.apps.googleusercontent.com',
forceCodeForRefreshToken: true,
}
}

View File

@@ -1,37 +0,0 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
SELENIUM_PROMISE_MANAGER: false,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: StacktraceOption.PRETTY
}
}));
}
};

View File

@@ -1,14 +0,0 @@
import { AppPage } from './app.po';
describe('new App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should be blank', () => {
page.navigateTo();
expect(page.getParagraphText()).toContain('Start with Ionic UI Components');
});
});

View File

@@ -1,11 +0,0 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.deepCss('app-root ion-content')).getText();
}
}

View File

@@ -1,12 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2018",
"types": [
"jasmine",
"node"
]
}
}

View File

@@ -1,13 +1,13 @@
<ion-app>
<ion-menu side="start" contentId="main">
<ion-menu side="end" contentId="main">
<ion-menu-toggle>
<ion-header color="primary">
<ion-toolbar>
<ion-item lines="none">
<ion-label routerLink="home">M O B I S</ion-label>
<ion-title routerLink="home" class="ion-no-padding">M O B I S</ion-title>
<!-- <ion-label routerLink="home">M O B I S</ion-label> -->
<ion-buttons>
<ion-button type="icon-only" (click)="closeMenu()"><ion-icon name="close-outline" color="dark"></ion-icon></ion-button>
<ion-icon name="close-outline" (click)="closeMenu()" size="large" slot="start"></ion-icon>
</ion-buttons>
</ion-item>
@@ -15,25 +15,25 @@
</ion-header>
</ion-menu-toggle>
<ion-content>
<ion-list>
<ion-list lines="none" class="ion-no-padding">
<ion-list-header>
<ion-label>{{ "menu.account" | translate }}</ion-label>
</ion-list-header>
<ion-menu-toggle>
<ion-item button routerLink="signup" [disabled]="user">
<ion-item button routerLink="/signup" [disabled]="user">
<ion-icon name="person-add-outline"></ion-icon>
<ion-label>{{ "menu.signup" | translate }}</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle>
<ion-item button routerLink="login" [disabled]="user">
<ion-item button routerLink="/login" [disabled]="user">
<ion-icon name="log-in-outline"></ion-icon>
<ion-label>{{ "menu.signin" | translate }}</ion-label>
</ion-item>
</ion-menu-toggle>
<ion-menu-toggle>
<ion-item button routerLink="profile" [disabled]="!user">
<ion-item button routerLink="/profile" [disabled]="!user">
<ion-icon name="person-circle-outline"></ion-icon>
<ion-label>{{ "menu.profile" | translate }}</ion-label>
</ion-item>
@@ -48,7 +48,7 @@
<ion-label>{{ "menu.settings" | translate }}</ion-label>
</ion-list-header>
<ion-menu-toggle>
<ion-item button routerLink="options">
<ion-item button routerLink="/options">
<ion-icon name="settings-outline"></ion-icon>
<ion-label>{{ "menu.options" | translate }}</ion-label>
</ion-item>

View File

@@ -0,0 +1,36 @@
export const ENV = {
production: false,
parseAppId: 'PARSE APP HERE',
parseServerUrl: 'PARSE URL HERE',
parseJSKey: 'parseJSKey here',
fileKey: 'parsefileKey here',
plantnetKey: 'plantnetKey here'
}
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
firebase: {
projectId: 'mobisapp-prod',
appId: 'firebase app id',
storageBucket: 'xx-prod.appspot.com',
//locationId: 'europe-west',
apiKey: 'xxx-no',
authDomain: 'xxxm',
messagingSenderId: 'xxx',
},
production: true
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

View File

@@ -4,12 +4,10 @@ import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { environment } from '../environments/environment';
import { provideAuth, getAuth } from '@angular/fire/auth';
import { getApp, initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { environment } from './app.constant';
import { provideFirestore, getFirestore } from '@angular/fire/firestore';
import { provideStorage, getStorage } from '@angular/fire/storage';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient, HttpClientModule } from '@angular/common/http';
@@ -20,6 +18,14 @@ import { IonicStorageModule } from '@ionic/storage-angular';
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/languages/', '.json');
}
//imports for IOS fix
import {
getAuth,
indexedDBLocalPersistence,
initializeAuth,
provideAuth,
} from '@angular/fire/auth';
import { Capacitor } from '@capacitor/core';
@NgModule({
declarations: [AppComponent],
@@ -31,7 +37,15 @@ export function createTranslateLoader(http: HttpClient) {
IonicStorageModule.forRoot(),
TranslateModule.forRoot({loader: {provide: TranslateLoader, useFactory: (createTranslateLoader), deps: [HttpClient]}}),
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => getAuth()),
provideAuth(() => {
if (Capacitor.isNativePlatform()) {
return initializeAuth(getApp(), {
persistence: indexedDBLocalPersistence,
});
} else {
return getAuth();
}
}),
provideFirestore(() => getFirestore()),
provideStorage(() => getStorage()),
],

View File

@@ -1,7 +1,8 @@
<ion-header class="ion-no-border">
<ion-toolbar color="primary">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="home" icon="chevron-back-outline"></ion-back-button>
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">{{ "loginPage.title" | translate}}</ion-title>
<ion-buttons slot="end">

View File

@@ -82,7 +82,7 @@ export class LoginPage implements OnInit {
await loading.present();
this.authService
.loginWithGoogle()
.then(() => this.router.navigate(['/profile']))
.then(() => this.router.navigate(['/home']))
.catch((e) => console.log(e.message));
await loading.dismiss();
}
@@ -96,7 +96,7 @@ export class LoginPage implements OnInit {
await loading.present();
this.authService
.loginWithApple()
.then(() => this.router.navigate(['/profile']))
.then(() => this.router.navigate(['/home']))
.catch((e) => console.log(e.message));
await loading.dismiss();
}
@@ -110,7 +110,7 @@ export class LoginPage implements OnInit {
await loading.present();
this.authService
.anonymousLogin()
.then(() => this.router.navigate(['/profile']))
.then(() => this.router.navigate(['/home']))
.catch((e) => console.log(e.message));
await loading.dismiss();
}

View File

@@ -1,7 +1,7 @@
<ion-header class="ion-no-border">
<ion-toolbar color="primary">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="home" icon="chevron-back-outline"></ion-back-button>
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">{{ "profilePage.title" | translate }}</ion-title>
<ion-buttons slot="end">
@@ -17,6 +17,7 @@
<ion-grid>
<ion-row>
<ion-col>
<div class="preview">
<ion-avatar (click)="changeImage()">
<img
@@ -31,6 +32,12 @@
</ng-template>
</ion-avatar>
</div>
</ion-col>
<ion-col>
<ion-button *ngIf="this.photoURL !== null" (click)="deleteAvatar()">Delete Avatar</ion-button>
<ion-button (click)="getWeather()" size="small">Get Weather</ion-button>
</ion-col>
</ion-row>
</ion-grid>
<ion-grid *ngIf="!isAnonymous && user !== null">
@@ -100,11 +107,14 @@
>{{ "profilePage.anonymous login text" | translate }}
</ion-card-content>
</ion-card>
<ion-button (click)="logout()">{{ "profilePage.logout-button" | translate }}</ion-button>
<ion-button *ngIf="this.user !== null" (click)="deleteAccount()" color="danger"><ion-icon name="alert-circle-outline"></ion-icon>{{ "profilePage.delete-profile-button" | translate }}</ion-button>
</ion-content>
<ion-content *ngIf="user === null">
<ion-card>
<ion-card-header>{{ "profilePage.logged out" | translate }}</ion-card-header>
<ion-card-content>Please <a routerLink="['/login']">login</a> to view your profile</ion-card-content>
<ion-card-content>Please <a routerLink="/login">login</a> to view your profile</ion-card-content>
</ion-card>
</ion-content>

View File

@@ -34,7 +34,7 @@ ion-avatar {
.preview {
//margin-top: 50px;
display: flex;
justify-content: center;
justify-content: left;
}
.fallback {

View File

@@ -6,6 +6,7 @@ import { user, Auth } from '@angular/fire/auth';
import { AvatarService } from '../../services/avatar.service';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import { TranslateService } from '@ngx-translate/core';
import { WeatherService } from 'src/app/services/weather.service';
@Component({
selector: 'app-profile',
@@ -30,24 +31,15 @@ export class ProfilePage implements OnInit {
private loadingController: LoadingController,
private alertController: AlertController,
private afAuth: Auth,
private translateService: TranslateService
private translateService: TranslateService,
private weatherService: WeatherService
) {
// not the nicest way, sort of duplicate to have a URL in the profile variable and also check later the user variable
this.avatarService.getUserProfile().subscribe((data) => {
if (data) {
this.profile = data;
if (this.profile.imageUrl !== null) {
this.photoURL = this.profile.imageUrl;
}
}
//console.log('data' , data);
});
user(this.afAuth).subscribe((response) => {
//fill the user to verify if someone is logged in
this.user = response;
if (response !== null) {
console.log(response);
//console.log(response);
this.displayName = response.displayName;
this.email = response.email;
this.photoURL = response.photoURL;
@@ -64,6 +56,12 @@ export class ProfilePage implements OnInit {
}
ngOnInit(): void {}
deleteAvatar(){
this.avatarService.removeImage(); //remove image from storage
this.photoURL = null; //remove image from profile page
this.authService.updatePhotoURL(''); //remove profile url from profile
}
editDisplayNameField() {
this.editDisplayName = true;
}
@@ -94,6 +92,16 @@ export class ProfilePage implements OnInit {
await this.authService.logout();
this.router.navigateByUrl('/home', { replaceUrl: true });
}
//delete user in firebase project, not actual google profile
deleteAccount(){
//todo: show pop up with question if user really wants to delete his account if clicked yes, delete account
this.authService.deleteAccount();
this.router.navigateByUrl('/home', { replaceUrl: true });
}
getWeather() {
this.weatherService.showCurrentWeather();
}
async changeImage() {
const image = await Camera.getPhoto({

View File

@@ -1,8 +1,8 @@
<ion-header>
<ion-toolbar color="primary">
<ion-toolbar color="black">
<ion-title>{{ "resetpwPage.title" | translate }}</ion-title>
<ion-buttons slot="start">
<ion-back-button defaultHref="/"></ion-back-button>
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

View File

@@ -1,7 +1,7 @@
<ion-header class="ion-no-border">
<ion-toolbar color="primary">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="home" icon="chevron-back-outline"></ion-back-button>
<ion-back-button defaultHref="home" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">{{ "signupPage.title" | translate }}</ion-title>
<ion-buttons slot="end">

View File

View File

View File

@@ -1,12 +1,26 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomePage } from './home.page';
import {redirectUnauthorizedTo, redirectLoggedInTo, canActivate } from '@angular/fire/auth-guard';
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['login']);
const redirectLoggedInToProfile = () => redirectLoggedInTo(['profile']);
const routes: Routes = [
{
path: '',
component: HomePage,
}
},
{ path: 'login',
loadChildren: () => import('../auth/login/login.module').then( m => m.LoginPageModule)
},
{
path: 'profile',
loadChildren: () => import('../auth/profile/profile.module').then( m => m.ProfilePageModule),
...canActivate(redirectUnauthorizedToLogin)
},
];
@NgModule({

View File

@@ -7,7 +7,6 @@ import { HomePage } from './home.page';
import { HomePageRoutingModule } from './home-routing.module';
import { TranslateModule } from '@ngx-translate/core';
@NgModule({
imports: [
CommonModule,

View File

@@ -1,56 +1,84 @@
<ion-header>
<ion-toolbar color="primary">
<ion-title>M O B I S</ion-title>
<ion-header>
<ion-toolbar color="black">
<ion-title mode="ios">M O B I S</ion-title>
<ion-buttons slot="start">
<ion-menu-button slot="start"></ion-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-buttons slot="end">
<ion-menu-button slot="end"></ion-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-card>
<ion-card-header>{{ "measure.welcome-to-mobis" | translate }}</ion-card-header>
<ion-card-content class="ion-no-padding">
<ion-list lines="none" class="ion-no-padding">
<ion-list-header>{{ "menu.account" | translate }}</ion-list-header>
<ion-item button routerLink="/login" [disabled]="user">
<ion-icon name="person-add-outline"></ion-icon>
<ion-label>{{ "menu.login-register" | translate }}</ion-label>
</ion-item>
<ion-item button routerLink="/profile" [disabled]="!user">
<ion-icon name="person-circle-outline"></ion-icon>
<ion-label>{{ "menu.profile" | translate }}</ion-label>
</ion-item>
<ion-item button (click)="logout()" [disabled]="!user">
<ion-icon name="log-out-outline"></ion-icon>
<ion-label>{{ "menu.signout" | translate }}</ion-label>
</ion-item>
</ion-list>
</ion-card-content>
</ion-card>
<ion-card>
<ion-card-header>{{ "measure.measure" | translate }}</ion-card-header>
<ion-card-content class="ion-no-padding">
<ion-list lines="none" class="ion-no-padding">
<ion-list-header>
{{ "measure.environment" | translate }}
</ion-list-header>
<ion-item button routerLink="/envpic" [disabled]="!user">
<ion-label>{{ "measure.envpic" | translate }}</ion-label>
</ion-item>
<ion-item button routerLink="/ispex" [disabled]="!user">
<ion-label>{{ "measure.ispex2-observation" | translate }}</ion-label>
</ion-item>
<ion-list-header>
{{ "measure.water-quality" | translate }}
</ion-list-header>
<ion-item button routerLink="/minisecchi" [disabled]="!user">
<ion-label> {{ "measure.mini-secchi-observation" | translate }}</ion-label>
</ion-item>
<ion-content class="ion-padding">
<ion-card>
<ion-card-title>{{ "measure.welcome-to-mobis" | translate }}</ion-card-title>
<ion-card-header>{{ "measure.measure" | translate }}</ion-card-header>
<ion-card-content>
<ion-list>
<ion-list-header>
{{ "measure.water-quality" | translate }}
</ion-list-header>
<ion-button shape="round" expand="block" routerLink="/minisecchi" strong="true" routerDirection="forward" >
{{ "measure.new-mini-secchi-observation" | translate }} </ion-button>
<ion-button shape="round" expand="block" routerLink="/ispex" routerDirection="forward" strong="true" routerDirection="forward" >
{{ "measure.new-ispex2-observation" | translate }} </ion-button>
<ion-list-header>
{{ "measure.air-quality" | translate }}
</ion-list-header>
<ion-button shape="round" expand="block" routerLink="/canairiopm25" routerDirection="forward" strong="true" routerDirection="forward" >
{{ "measure.new-canairiopm25-observation" | translate }}
</ion-button>
<ion-button shape="round" expand="block" routerLink="/canairioco2" routerDirection="forward" strong="true" routerDirection="forward" >
{{ "measure.new-canairioco2-observation" | translate }}
</ion-button>
<ion-list-header>
{{ "measure.biodiversity" | translate }}
</ion-list-header>
<ion-button shape="round" expand="block" routerLink="/plantnet" routerDirection="forward" strong="true" routerDirection="forward" >
{{ "measure.new-plantnet-observation" | translate }}
</ion-button>
</ion-list>
</ion-card-content>
</ion-card>
<ion-list-header>
{{ "measure.air-quality" | translate }}
</ion-list-header>
<ion-item button routerLink="/canairiopm25" [disabled]="!user">
<ion-label> {{ "measure.canairiopm2.5-observation" | translate }}</ion-label>
</ion-item>
<ion-item button routerLink="/canairioco2" [disabled]="!user">
<ion-label>{{ "measure.canairioco2-observation" | translate }}</ion-label>
</ion-item>
<ion-list-header>
{{ "measure.biodiversity" | translate }}
</ion-list-header>
<ion-item button routerLink="/plantnet" [disabled]="!user">
<ion-label> {{ "measure.plantnet-observation" | translate }}</ion-label>
</ion-item>
</ion-list>
</ion-card-content>
</ion-card>
</ion-content>

View File

@@ -1,128 +1,7 @@
#container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
#container strong {
font-size: 20px;
line-height: 26px;
}
#container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
#container a {
text-decoration: none;
}
ion-avatar {
width: 128px;
height: 128px;
}
.preview {
margin-top: 50px;
display: flex;
justify-content: center;
}
.fallback {
width: 128px;
height: 128px;
border-radius: 50%;
background: #bfbfbf;
display: flex;
justify-content: center;
align-items: center;
font-weight: 500;
}
ion-label.title{
ion-title {
font-size: 1.5em;
font-weight: bold;
color: white;
text-transform: uppercase;
font-family: Open Sans, Helvetica, sans-serif;
}
ion-col{
text-align: start;
vertical-align: middle;
}
ion-col ion-button{
vertical-align: baseline;
}
ion-icon {
pointer-events: none;
}
ion-card{
padding: 0px;
margin: 2px;
ion-card-header{
font-weight: bold;
}
ion-card-content{
font-size: 12px;
}
}
ion-header ion-img{
width:50px;
}
ion-img{
// display: block;
margin-left: auto;
margin-right: auto;
width: 80%;
}
ion-col {
display: flex;
justify-content: center;
padding: 0px;
}
ion-footer{
padding-top: 2px;
color: black;
display: flex;
align-items:center;
justify-content: center;
border-top: 5px, solid;
border-color: #8c8c8c;
background-color: rgb(255, 255, 255);
}
.icon-verified {
display: inline-block;
font-size: 20px;
color: #1a7900;
vertical-align: middle;
}
.icon-not-verified {
display: inline-block;
font-size: 20px;
color: #790000;
vertical-align: middle;
}
.text{
display: inline-block;
vertical-align: middle;
}

View File

@@ -2,7 +2,10 @@ import { Component, OnInit } from '@angular/core';
import { MenuController } from '@ionic/angular';
import { TranslateService } from '@ngx-translate/core';
import { Geolocation, PositionOptions } from '@capacitor/geolocation';
import { AuthService } from '../services/auth.service';
import { Auth, user } from '@angular/fire/auth';
import { Storage } from '@ionic/storage-angular';
import { Router } from '@angular/router';
@Component({
selector: 'app-home',
@@ -11,11 +14,23 @@ import { Geolocation, PositionOptions } from '@capacitor/geolocation';
})
export class HomePage implements OnInit{
user = null;
language= '';
constructor(
private menuController: MenuController, private translateService: TranslateService
private menuController: MenuController,
private translateService: TranslateService,
private authService: AuthService,
private afAuth: Auth,
private storage: Storage,
private router: Router
) {
user(this.afAuth).subscribe((response) => {
//fill the user to verify if someone is logged in
this.user = response;
});
@@ -35,7 +50,23 @@ export class HomePage implements OnInit{
console.log('Current location: ', location);
this.storage.create();
this.translateService.setDefaultLang('en'); //set default language English
this.language = await this.storage.get('language');
if (this.language === null){
console.log('no value for language in storage, trying getting from browser');
this.language = this.translateService.getBrowserLang();
}
this.translateService.use(this.language);
}
async logout() {
await this.authService.logout();
this.router.navigateByUrl('/home', { replaceUrl: true });
}
openMenu(){

View File

@@ -1,82 +1,56 @@
<ion-header>
<ion-toolbar>
<ion-title>New Canair.io CO2 observation</ion-title>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title>CANAIR.IO CO2</ion-title>
</ion-toolbar>
</ion-header>
</ion-toolbar>
<ion-content>
<ion-item>
<ion-label> Canairio output </ion-label>
</ion-item>
</ion-header>
<ion-item>
<ion-label primary>CO2: {{CO2}} ({{CO2Interpret}}) </ion-label>
</ion-item>
<ion-content>
<ion-item>
<ion-label>
Canairio output
</ion-label>
</ion-item>
<ion-item>
<ion-label primary>CO2: {{CO2}} ({{CO2Interpret}}) </ion-label>
</ion-item>
<ion-item>
<ion-item>
<ion-label primary>Temperature: {{TEMP}} C</ion-label>
</ion-item>
<ion-item>
</ion-item>
<ion-item>
<ion-label primary>Humidity: {{HUMID }} % </ion-label>
</ion-item>
</ion-item>
<ion-item>
<ion-item>
<ion-label primary>PAX: {{PAX}} </ion-label>
</ion-item>
</ion-item>
<ion-item>
<ion-item>
<ion-label primary>Lat: {{latitude}} </ion-label>
</ion-item>
</ion-item>
<ion-item>
<ion-item>
<ion-label primary>Lon: {{longitude}} </ion-label>
</ion-item>
<ion-item>
</ion-item>
<ion-item>
<ion-label primary>Alt: {{altitude}} </ion-label>
</ion-item>
</ion-item>
<ion-item>
<ion-button (click)="startLogging()" [disabled]="isLogging"> Start logging</ion-button>
<ion-button (click)="stopLogging()" [disabled]="!isLogging"> Stop logging</ion-button>
</ion-item>
<ion-item>
<ion-label>Log Interval:</ion-label>
<ion-input>{{logInterval}}</ion-input>
</ion-item>
<ion-item>
<ion-button (click) ="startLogging()">Start logging</ion-button>
<ion-button (click) ="stopLogging()"> Stop logging</ion-button>
</ion-item>
<ion-item>
<ion-input>Log Interval:{{logInterval}}</ion-input>
</ion-item>
<ion-item>
<ion-label>Log status:{{lblLogstatus}}</ion-label>
</ion-item>
</ion-content>
<ion-item>
<ion-label>Log status:{{lblLogstatus}}</ion-label>
</ion-item>
</ion-content>

View File

@@ -1,23 +1,30 @@
/* eslint-disable @typescript-eslint/member-ordering */
/* eslint-disable @typescript-eslint/naming-convention */
import { Component, OnInit } from '@angular/core';
import { ENV } from '../../app.constant';
import { Geolocation} from '@capacitor/geolocation';
import { Guid } from "guid-typescript";
import {interval, Subscription} from 'rxjs';
import { Geolocation } from '@capacitor/geolocation';
import { Guid } from 'guid-typescript';
import { interval, Subscription } from 'rxjs';
import { Storage } from '@ionic/storage-angular';
import * as Parse from 'parse';
import { BackgroundMode } from '@awesome-cordova-plugins/background-mode/ngx';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '../../services/auth.service';
import { Auth, user } from '@angular/fire/auth';
// Setup Bluetooth LE
// Import the wrapper class directly
import { BleClient, numbersToDataView, numberToUUID, dataViewToText } from '@capacitor-community/bluetooth-le';
import {
BleClient,
numbersToDataView,
numberToUUID,
dataViewToText,
} from '@capacitor-community/bluetooth-le';
const PM25_SERVICE = 'C8D1D262-861F-4082-947E-F383A259AAF3';
const PM25_SERVICE_LCASE= 'c8d1d262-861f-4082-947e-f383a259aaf3';
const PM25_SERVICE_LCASE = 'c8d1d262-861f-4082-947e-f383a259aaf3';
const PM25_SERVICE_CHARACTERISTIC = 'B0F332A8-A5AA-4F3F-BB43-F99E7791AE01';
/* other services "B0F332A8-A5AA-4F3F-BB43-F99E7791AE02",
@@ -30,216 +37,219 @@ const PM25_SERVICE_CHARACTERISTIC = 'B0F332A8-A5AA-4F3F-BB43-F99E7791AE01';
styleUrls: ['./canairioco2.page.scss'],
})
export class Canairioco2Page implements OnInit {
public txtco2: string;
public datetime_ux: string;
public datetime: Date;
public latitude: number;
public longitude: number;
public altitude: number;
private parseAppId: string = ENV.parseAppId;
private parseServerUrl: string = ENV.parseServerUrl;
private parseJSKey: string=ENV.parseJSKey;
public result: string;
public output_json: string;
public rec_uid: string;
public session_uid: string;
user = null;
language = '';
public deviceId:string;
public intervalID:NodeJS.Timeout;
public logInterval:number;
public lblLogstatus: string;
public allData: any;
public CO2: string;
public TEMP: string;
public HUMID: string;
public PAX: string;
public CO2Interpret:string;
public txtco2: string;
public datetime_ux: string;
public datetime: Date;
public latitude: number;
public longitude: number;
public altitude: number;
private parseAppId: string = ENV.parseAppId;
private parseServerUrl: string = ENV.parseServerUrl;
private parseJSKey: string = ENV.parseJSKey;
public result: string;
public output_json: string;
public rec_uid: string;
public session_uid: string;
public ParseServerKey:string;
public ParseServerURL:string;
public ParseServerAppID:string;
public deviceId: string;
public intervalID: NodeJS.Timeout;
public logInterval: number;
public lblLogstatus: string;
public allData: any;
public CO2: string;
public TEMP: string;
public HUMID: string;
public PAX: string;
public CO2Interpret: string;
public ParseServerKey: string;
public ParseServerURL: string;
public ParseServerAppID: string;
constructor(private backgroundMode: BackgroundMode) { }
// Initialize Parse SDK and connect to Parse Server
ngOnInit() {
this.parseInitialize();
this.getLocation();
this.connect();
this.logInterval=5000;
this.lblLogstatus="Not logging";
this.readandSave();
this.parseAppId=ENV.parseAppId;
this.parseServerUrl=ENV.parseServerUrl;
this.parseJSKey=ENV.parseJSKey;
isLogging = false;
constructor(
private backgroundMode: BackgroundMode,
private translateService: TranslateService,
private authService: AuthService,
private afAuth: Auth,
private storage: Storage
) {
user(this.afAuth).subscribe((response) => {
//fill the user to verify if someone is logged in
this.user = response;
console.log(this.user.uid);
});
}
// i am not sure if this is needed, because CO2 is not moving
// Initialize Parse SDK and connect to Parse Server
async startLogging() {
ngOnInit() {
this.parseInitialize();
this.getLocation();
this.connect();
this.logInterval = 5000;
this.lblLogstatus = 'Not logging';
this.backgroundMode.enable();
this.parseAppId = ENV.parseAppId;
this.parseServerUrl = ENV.parseServerUrl;
this.parseJSKey = ENV.parseJSKey;
this.lblLogstatus="Logging..";
// set session uid
this.session_uid = Guid.raw(); // make it a string
var str_counter;
str_counter=0;
this.readandSave();
}
// i am not sure if this is needed, because CO2 is not moving
this.intervalID = setInterval( () => {
async startLogging() {
this.backgroundMode.enable();
this.lblLogstatus = 'Logging..';
// set session uid
this.session_uid = Guid.raw(); // make it a string
var str_counter;
str_counter = 0;
this.lblLogstatus="Logging: " + str_counter.toString() ;
this.intervalID = setInterval(() => {
this.lblLogstatus = 'Logging: ' + str_counter.toString();
this.isLogging = true;
str_counter++;
str_counter++;
this.getLocation();
this.readandSave();
}, this.logInterval);
}
this.getLocation();
this.readandSave();
async stopLogging() {
clearInterval(this.intervalID);
this.lblLogstatus = 'Not logging';
this.isLogging = false;
},this.logInterval);
this.backgroundMode.disable();
}
async readandSave() {
const result = await BleClient.read(
this.deviceId,
PM25_SERVICE,
PM25_SERVICE_CHARACTERISTIC
);
console.log('canair.io result array', dataViewToText(result));
this.allData = JSON.parse(dataViewToText(result)); // parse json data and pass json string
}
this.CO2 = this.allData['CO2'];
// Indoor CO2 levels
if (Number(this.CO2) > 0 && Number(this.CO2) < 700)
this.CO2Interpret = 'Excellent';
if (Number(this.CO2) >= 700 && Number(this.CO2) < 800)
this.CO2Interpret = 'Good';
if (Number(this.CO2) >= 800 && Number(this.CO2) < 1000)
this.CO2Interpret = 'Fair';
if (Number(this.CO2) >= 1000 && Number(this.CO2) < 1500)
this.CO2Interpret = 'Mediocre';
if (Number(this.CO2) >= 1500) this.CO2Interpret = 'Bad';
async stopLogging() {
clearInterval(this.intervalID);
this.lblLogstatus="Not logging";
this.backgroundMode.disable();
}
async readandSave() {
const result = await BleClient.read(this.deviceId, PM25_SERVICE, PM25_SERVICE_CHARACTERISTIC);
console.log('canair.io result array', dataViewToText(result));
this.allData = JSON.parse(dataViewToText(result)); // parse json data and pass json string
this.CO2= this.allData['CO2'];
// Indoor CO2 levels
if (Number(this.CO2)>0 && Number(this.CO2)<700 ) this.CO2Interpret= ("Excellent");
if (Number(this.CO2)>=700 && Number(this.CO2)<800 )this.CO2Interpret= ("Good");
if (Number(this.CO2)>=800 && Number(this.CO2)<1000 )this.CO2Interpret= ("Fair");
if (Number(this.CO2)>=1000 && Number(this.CO2)<1500 )this.CO2Interpret= ("Mediocre");
if (Number(this.CO2)>=1500 ) this.CO2Interpret= ("Bad");
this.TEMP= this.allData['tmp'];
this.HUMID= this.allData['hum'];
this.PAX= this.allData['PAX'];
this.CO2= this.allData['CO2'];
// lets assume we have
if (Number(this.CO2)>0) {
this.TEMP= this.allData['CO2T'];
this.HUMID= this.allData['CO2H'];
}
// this.txtco2= this.allData['CO2']; // this is the CO2 value
let d = new Date();
this.datetime=d;
var unixTimeStamp = Math.floor(d.getTime() / 1000);
this.datetime_ux=unixTimeStamp.toString();
this.output_json=dataViewToText(result);
this.rec_uid = Guid.raw(); // make it a string
var Comment = Parse.Object.extend('canairico2_raw_data');
var canairio_store = new Comment();
// set initial data record
canairio_store.set('session_UID',this.session_uid);
canairio_store.set('record_UID',this.rec_uid);
canairio_store.set('device_UID',this.deviceId);
canairio_store.set('output_json',dataViewToText(result));
canairio_store.set('TEMP',this.TEMP);
canairio_store.set('HUMID',this.HUMID);
canairio_store.set('CO2',this.CO2);
canairio_store.set('PAX',this.PAX);
canairio_store.set('latitude',this.latitude);
canairio_store.set('longitude',this.longitude);
canairio_store.set('altitude',this.altitude);
canairio_store.set('datetime_ux',this.datetime_ux);
await canairio_store.save();
}
// get location and save to class variables
async getLocation() {
const position = await Geolocation.getCurrentPosition({enableHighAccuracy: true});
this.latitude = position.coords.latitude;
console.log (position.coords.latitude);
this.longitude = position.coords.longitude;
this.altitude = position.coords.altitude;
return position.coords;
}
// connect to parse server and initialize
private parseInitialize() {
Parse.initialize(this.parseAppId, this.parseJSKey);
(Parse as any).serverURL = this.ParseServerURL; // use your server url
this.TEMP = this.allData['tmp'];
this.HUMID = this.allData['hum'];
this.PAX = this.allData['PAX'];
this.CO2 = this.allData['CO2'];
// lets assume we have
if (Number(this.CO2) > 0) {
this.TEMP = this.allData['CO2T'];
this.HUMID = this.allData['CO2H'];
}
// this.txtco2= this.allData['CO2']; // this is the CO2 value
async connect(){
const d = new Date();
this.datetime = d;
const unixTimeStamp = Math.floor(d.getTime() / 1000);
this.datetime_ux = unixTimeStamp.toString();
this.output_json = dataViewToText(result);
this.rec_uid = Guid.raw(); // make it a string\
try {
await BleClient.initialize();
const device = await BleClient.requestDevice({
namePrefix: 'CanAirIO',
optionalServices : [PM25_SERVICE_LCASE]
});
// The parse part
// create parse class
const canairio_data_store = Parse.Object.extend('canairio_raw_data');
// console.log('device', device);
// create new instance of parse class
const canairio_store = new canairio_data_store();
await BleClient.connect(device.deviceId);
console.log('connected to device', device);
// set value for parse clas
this.deviceId=device.deviceId;
this.readandSave();
// set initial data record
canairio_store.set('session_UID', this.session_uid);
canairio_store.set('record_UID', this.rec_uid);
canairio_store.set('device_UID', this.deviceId);
canairio_store.set('output_json', dataViewToText(result));
canairio_store.set('TEMP', this.TEMP);
canairio_store.set('HUMID', this.HUMID);
canairio_store.set('CO2', this.CO2);
canairio_store.set('PAX', this.PAX);
canairio_store.set('latitude', this.latitude);
canairio_store.set('longitude', this.longitude);
canairio_store.set('altitude', this.altitude);
setTimeout(async () => {
await BleClient.stopLEScan();
console.log('stopped scanning');
}, 5000);
} catch (error) {
console.error(error);
canairio_store.set('datetime_ux', this.datetime_ux);
canairio_store.set('user_uid', this.user.uid);
canairio_store.set('name', this.user.displayName);
canairio_store.set('email', this.user.email);
//log the parse save to console
canairio_store.save().then(
(saveResult: any) => {
console.log(saveResult);
},
(error: any) => {
console.log(error);
}
);
}
// get location and save to class variables
async getLocation() {
const position = await Geolocation.getCurrentPosition({
enableHighAccuracy: true,
});
this.latitude = position.coords.latitude;
console.log(position.coords.latitude);
this.longitude = position.coords.longitude;
this.altitude = position.coords.altitude;
return position.coords;
}
}
// connect to parse server and initialize
private parseInitialize() {
Parse.initialize(ENV.parseAppId, ENV.parseJSKey);
(Parse as any).serverURL = ENV.parseServerUrl; // use your server url
}
async connect() {
try {
await BleClient.initialize();
const device = await BleClient.requestDevice({
namePrefix: 'CanAirIO',
optionalServices: [PM25_SERVICE_LCASE],
});
// console.log('device', device);
await BleClient.connect(device.deviceId);
console.log('connected to device', device);
this.deviceId = device.deviceId;
this.readandSave();
setTimeout(async () => {
await BleClient.stopLEScan();
console.log('stopped scanning');
}, 5000);
} catch (error) {
console.error(error);
}
}
}

View File

@@ -1,85 +1,59 @@
<ion-header>
<ion-toolbar>
<ion-title>New Canair.io PM2.5 observation</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title>CANAIR.IO PM2.5</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
<ion-label>
Canairio output
</ion-label>
<ion-item>
<ion-label> Canairio output </ion-label>
</ion-item>
<ion-item>
<ion-label primary>{{txtpm25}} </ion-label>
</ion-item>
<ion-item>
<ion-label primary>{{txtpm25}} </ion-label>
</ion-item>
<ion-item>
<ion-label primary>PM 2.5: {{PM25}} ({{PM25Interpret}}) </ion-label>
</ion-item>
<ion-item>
<ion-label primary>PM 2.5: {{PM25}} ({{PM25Interpret}}) </ion-label>
<ion-item>
<ion-label primary>Temperature: {{TEMP}} C</ion-label>
</ion-item>
<ion-item>
<ion-label primary>Humidity: {{HUMID }} % </ion-label>
</ion-item>
</ion-item>
<ion-item>
<ion-label primary>PAX: {{PAX}} </ion-label>
</ion-item>
<ion-item>
<ion-label primary>Lat: {{latitude}} </ion-label>
</ion-item>
<ion-item>
<ion-label primary>Lon: {{longitude}} </ion-label>
</ion-item>
<ion-item>
<ion-label primary>Temperature: {{TEMP}} C</ion-label>
<ion-item>
<ion-label primary>Alt: {{altitude}} </ion-label>
</ion-item>
</ion-item>
<ion-item>
<ion-label primary>Humidity: {{HUMID }} % </ion-label>
</ion-item>
<ion-item>
<ion-label primary>PAX: {{PAX}} </ion-label>
</ion-item>
<ion-item>
<ion-label primary>Lat: {{latitude}} </ion-label>
</ion-item>
<ion-item>
<ion-label primary>Lon: {{longitude}} </ion-label>
</ion-item>
<ion-item>
<ion-label primary>Alt: {{altitude}} </ion-label>
</ion-item>
<ion-item>
<ion-button (click) ="startLogging()">Start logging</ion-button>
<ion-button (click) ="stopLogging()"> Stop logging</ion-button>
</ion-item>
<ion-item>
<ion-input>Log Interval:{{logInterval}}</ion-input>
</ion-item>
<ion-item>
<ion-label>Log status:{{lblLogstatus}}</ion-label>
</ion-item>
<ion-item>
<ion-button (click)="startLogging()" [disabled]="isLogging">Start logging</ion-button>
<ion-button (click)="stopLogging()" [disabled]="!isLogging"> Stop logging</ion-button>
</ion-item>
<ion-item>
<ion-label>Log Interval:</ion-label>
<ion-input>{{logInterval}}</ion-input>
</ion-item>
<ion-item>
<ion-label>Log status:{{lblLogstatus}}</ion-label>
</ion-item>
</ion-content>

View File

@@ -1,23 +1,27 @@
/* eslint-disable @typescript-eslint/member-ordering */
/* eslint-disable @typescript-eslint/naming-convention */
import { Component, OnInit } from '@angular/core';
import { ENV } from '../../app.constant';
import { Geolocation} from '@capacitor/geolocation';
import { Guid } from "guid-typescript";
import {interval, Subscription} from 'rxjs';
import { Geolocation } from '@capacitor/geolocation';
import { Guid } from 'guid-typescript';
import { interval, Subscription } from 'rxjs';
import { Storage } from '@ionic/storage-angular';
import * as Parse from 'parse';
import { BackgroundMode } from '@awesome-cordova-plugins/background-mode/ngx';
import { AuthService } from '../../services/auth.service';
import { Auth, user } from '@angular/fire/auth';
// Import the wrapper class directly
import { BleClient, numbersToDataView, numberToUUID, dataViewToText } from '@capacitor-community/bluetooth-le';
import {
BleClient,
numbersToDataView,
numberToUUID,
dataViewToText,
} from '@capacitor-community/bluetooth-le';
const PM25_SERVICE = 'C8D1D262-861F-4082-947E-F383A259AAF3';
const PM25_SERVICE_LCASE= 'c8d1d262-861f-4082-947e-f383a259aaf3';
const PM25_SERVICE_LCASE = 'c8d1d262-861f-4082-947e-f383a259aaf3';
const PM25_SERVICE_CHARACTERISTIC = 'B0F332A8-A5AA-4F3F-BB43-F99E7791AE01';
/* other services "B0F332A8-A5AA-4F3F-BB43-F99E7791AE02",
@@ -30,222 +34,217 @@ const PM25_SERVICE_CHARACTERISTIC = 'B0F332A8-A5AA-4F3F-BB43-F99E7791AE01';
styleUrls: ['./canairiopm25.page.scss'],
})
export class Canairiopm25Page implements OnInit {
public txtpm25: string;
public datetime_ux: string;
public datetime: Date;
public latitude: number;
public longitude: number;
public altitude: number;
private parseAppId: string = ENV.parseAppId;
private parseServerUrl: string = ENV.parseServerUrl;
private parseJSKey: string=ENV.parseJSKey;
public result: string;
public output_json: string;
public rec_uid: string;
public session_uid: string;
image = '';
user = null;
public txtpm25: string;
public datetime_ux: string;
public datetime: Date;
public latitude: number;
public longitude: number;
public altitude: number;
private parseAppId: string = ENV.parseAppId;
private parseServerUrl: string = ENV.parseServerUrl;
private parseJSKey: string = ENV.parseJSKey;
public result: string;
public output_json: string;
public rec_uid: string;
public session_uid: string;
public deviceId:string;
public intervalID:NodeJS.Timeout;
public logInterval:number;
public lblLogstatus: string;
public allData: any;
public PM25: string;
public CO2: string;
public TEMP: string;
public HUMID: string;
public PAX: string;
public PM25Interpret:string;
public ParseServerKey:string;
public ParseServerURL:string;
public ParseServerAppID:string;
constructor(private backgroundMode: BackgroundMode) { }
ngOnInit() {
this.parseInitialize();
this.getLocation();
this.connect();
this.logInterval=5000;
this.lblLogstatus="Not logging";
this.readandSave();
this.parseAppId=ENV.parseAppId;
this.parseServerUrl=ENV.parseServerUrl;
this.parseJSKey=ENV.parseJSKey;
public deviceId: string;
public intervalID: NodeJS.Timeout;
public logInterval: number;
public lblLogstatus: string;
public allData: any;
public PM25: string;
public CO2: string;
public TEMP: string;
public HUMID: string;
public PAX: string;
public PM25Interpret: string;
public ParseServerKey: string;
public ParseServerURL: string;
public ParseServerAppID: string;
isLogging = false;
constructor(
private backgroundMode: BackgroundMode,
private authService: AuthService,
private afAuth: Auth
) {
user(this.afAuth).subscribe((response) => {
//fill the user to verify if someone is logged in
this.user = response;
console.log(this.user.uid);
});
}
ngOnInit() {
this.parseInitialize();
this.getLocation();
this.connect();
this.logInterval = 5000;
this.lblLogstatus = 'Not logging';
this.readandSave();
this.parseAppId = ENV.parseAppId;
this.parseServerUrl = ENV.parseServerUrl;
this.parseJSKey = ENV.parseJSKey;
}
async startLogging() {
this.backgroundMode.enable();
this.lblLogstatus = 'Logging..';
this.isLogging = true;
// set session uid
this.session_uid = Guid.raw(); // make it a string
var str_counter;
str_counter = 0;
this.intervalID = setInterval(() => {
this.lblLogstatus = 'Logging: ' + str_counter.toString();
str_counter++;
this.getLocation();
this.readandSave();
}, this.logInterval);
}
async stopLogging() {
clearInterval(this.intervalID);
this.lblLogstatus = 'Not logging';
this.isLogging = false;
this.backgroundMode.disable();
}
async readandSave() {
const result = await BleClient.read(
this.deviceId,
PM25_SERVICE,
PM25_SERVICE_CHARACTERISTIC
);
console.log('canair.io result array', dataViewToText(result));
this.allData = JSON.parse(dataViewToText(result)); // parse json data and pass json string
this.PM25 = this.allData['P25'];
//outdoor
if (Number(this.PM25) > 0 && Number(this.PM25) < 25)
{this.PM25Interpret = 'Good';}
if (Number(this.PM25) > 24 && Number(this.PM25) < 51)
{this.PM25Interpret = 'Fair';}
if (Number(this.PM25) > 49 && Number(this.PM25) < 101)
{this.PM25Interpret = 'Poor';}
if (Number(this.PM25) > 100 && Number(this.PM25) < 300)
{this.PM25Interpret = 'Very Poor';}
if (Number(this.PM25) > 300) {this.PM25Interpret = 'Extremely Poor';}
this.TEMP = this.allData['tmp'];
this.HUMID = this.allData['hum'];
this.PAX = this.allData['PAX'];
this.CO2 = this.allData['CO2'];
// lets assume we have
if (Number(this.CO2) > 0) {
this.TEMP = this.allData['CO2T'];
this.HUMID = this.allData['CO2H'];
}
const d = new Date();
this.datetime = d;
const unixTimeStamp = Math.floor(d.getTime() / 1000);
this.datetime_ux = unixTimeStamp.toString();
this.output_json = dataViewToText(result);
this.txtpm25 = this.output_json;
this.rec_uid = Guid.raw(); // make it a string
// The parse part
// create parse class
const canairio_data_store = Parse.Object.extend('canairio_raw_data');
async startLogging() {
// create new instance of parse class
this.backgroundMode.enable();
this.lblLogstatus="Logging..";
// set session uid
this.session_uid = Guid.raw(); // make it a string
var str_counter;
str_counter=0;
const canairio_store = new canairio_data_store();
this.intervalID = setInterval( () => {
// set value for parse clas
this.lblLogstatus="Logging: " + str_counter.toString() ;
str_counter++;
this.getLocation();
this.readandSave();
},this.logInterval);
}
async stopLogging() {
clearInterval(this.intervalID);
this.lblLogstatus="Not logging";
this.backgroundMode.disable();
}
async readandSave() {
const result = await BleClient.read(this.deviceId, PM25_SERVICE, PM25_SERVICE_CHARACTERISTIC);
console.log('canair.io result array', dataViewToText(result));
this.allData = JSON.parse(dataViewToText(result)); // parse json data and pass json string
this.PM25= this.allData['P25'];
//outdoor
if (Number(this.PM25)>0 && Number(this.PM25)<25 ) this.PM25Interpret= ("Good");
if (Number(this.PM25)>24 && Number(this.PM25)<51 )this.PM25Interpret= ("Fair");
if (Number(this.PM25)>49 && Number(this.PM25)<101 )this.PM25Interpret= ("Poor");
if (Number(this.PM25)>100 && Number(this.PM25)<300 )this.PM25Interpret= ("Very Poor");
if (Number(this.PM25)>300 ) this.PM25Interpret= ("Extremely Poor");
this.TEMP= this.allData['tmp'];
this.HUMID= this.allData['hum'];
this.PAX= this.allData['PAX'];
this.CO2= this.allData['CO2'];
// lets assume we have
if (Number(this.CO2)>0) {
this.TEMP= this.allData['CO2T'];
this.HUMID= this.allData['CO2H'];
}
let d = new Date();
this.datetime=d;
var unixTimeStamp = Math.floor(d.getTime() / 1000);
this.datetime_ux=unixTimeStamp.toString();
this.output_json=dataViewToText(result);
this.txtpm25=this.output_json;
this.rec_uid = Guid.raw(); // make it a string
var Comment = Parse.Object.extend('canairio_raw_data');
var canairio_store = new Comment();
// set initial data record
canairio_store.set('session_UID',this.session_uid);
canairio_store.set('record_UID',this.rec_uid);
canairio_store.set('device_UID',this.deviceId);
canairio_store.set('output_json',dataViewToText(result));
canairio_store.set('PM25',this.PM25);
canairio_store.set('TEMP',this.TEMP);
canairio_store.set('HUMID',this.HUMID);
canairio_store.set('CO2',this.CO2);
canairio_store.set('PAX',this.PAX);
canairio_store.set('latitude',this.latitude);
canairio_store.set('longitude',this.longitude);
canairio_store.set('altitude',this.altitude);
canairio_store.set('datetime_ux',this.datetime_ux);
await canairio_store.save();
canairio_store.set('user_uid', this.user.uid);
canairio_store.set('name', this.user.displayName);
canairio_store.set('email', this.user.email);
//log the parse save to console
canairio_store.save().then((saveResult: any) => {
console.log(saveResult);
}
}, (error: any) => {
console.log(error);
});
}
async getLocation() {
const position = await Geolocation.getCurrentPosition({enableHighAccuracy: true});
this.latitude = position.coords.latitude;
console.log (position.coords.latitude);
this.longitude = position.coords.longitude;
this.altitude = position.coords.altitude;
return position.coords;
}
private parseInitialize() {
Parse.initialize(this.parseAppId, this.parseJSKey);
(Parse as any).serverURL = this.ParseServerURL; // use your server url
}
async connect(){
try {
await BleClient.initialize();
const device = await BleClient.requestDevice({
namePrefix: 'CanAirIO',
optionalServices : [PM25_SERVICE_LCASE]
async getLocation() {
const position = await Geolocation.getCurrentPosition({
enableHighAccuracy: true,
});
// console.log('device', device);
await BleClient.connect(device.deviceId);
console.log('connected to device', device);
this.deviceId=device.deviceId;
this.readandSave();
setTimeout(async () => {
await BleClient.stopLEScan();
console.log('stopped scanning');
}, 5000);
} catch (error) {
console.error(error);
this.latitude = position.coords.latitude;
console.log(position.coords.latitude);
this.longitude = position.coords.longitude;
this.altitude = position.coords.altitude;
return position.coords;
}
}
private parseInitialize() {
Parse.initialize(ENV.parseAppId, ENV.parseJSKey);
(Parse as any).serverURL = ENV.parseServerUrl; // use your server url
}
async connect() {
try {
await BleClient.initialize();
const device = await BleClient.requestDevice({
namePrefix: 'CanAirIO',
optionalServices: [PM25_SERVICE_LCASE],
});
// console.log('device', device);
await BleClient.connect(device.deviceId);
console.log('connected to device', device);
this.deviceId = device.deviceId;
this.readandSave();
setTimeout(async () => {
await BleClient.stopLEScan();
console.log('stopped scanning');
}, 5000);
} catch (error) {
console.error(error);
}
}
}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ClassifyPage } from './classify.page';
const routes: Routes = [
{
path: '',
component: ClassifyPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ClassifyPageRoutingModule {}

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { ClassifyPageRoutingModule } from './classify-routing.module';
import { ClassifyPage } from './classify.page';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ClassifyPageRoutingModule
],
declarations: [ClassifyPage]
})
export class ClassifyPageModule {}

View File

@@ -0,0 +1,55 @@
<ion-header>
<ion-toolbar>
<ion-title>Classify your spectrum</ion-title>
<ion-buttons slot="start">
<ion-back-button defaultHref="home" icon="chevron-back-outline"></ion-back-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
<ion-label>Choose a class</ion-label>
<ion-select>
<ion-select-option value="gray">
Gray card
</ion-select-option>
<ion-select-option value="sky">
Sky
</ion-select-option>
<ion-select-option value="water">
Water
</ion-select-option>
<ion-select-option value="lamp">
Lamp
</ion-select-option>
<ion-select-option value="other">
Other
</ion-select-option>
</ion-select>
</ion-item>
</ion-content>

View File

@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { ClassifyPage } from './classify.page';
describe('ClassifyPage', () => {
let component: ClassifyPage;
let fixture: ComponentFixture<ClassifyPage>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ClassifyPage ],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(ClassifyPage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-classify',
templateUrl: './classify.page.html',
styleUrls: ['./classify.page.scss'],
})
export class ClassifyPage implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@@ -7,6 +7,10 @@ const routes: Routes = [
{
path: '',
component: IspexPage
},
{
path: 'classify',
loadChildren: () => import('./classify/classify.module').then( m => m.ClassifyPageModule)
}
];

View File

@@ -1,24 +1,24 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { IspexPageRoutingModule } from './ispex-routing.module';
import { PreviewPageModule } from "./preview/preview.module"
import { Vibration } from '@ionic-native/vibration/ngx';
import { TranslateModule } from '@ngx-translate/core';
import { IspexPage } from './ispex.page';
import { PreviewPage } from './preview/preview.page';
import { PreviewPageModule } from "./preview/preview.module"
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
IspexPageRoutingModule
IspexPageRoutingModule,
TranslateModule,
PreviewPageModule
],
declarations: [IspexPage],
providers: [Vibration]
})
export class IspexPageModule {}

View File

@@ -1,64 +1,32 @@
<ion-header>
<ion-toolbar color="primary">
<ion-title>New iSPEX observation</ion-title>
<ion-header [translucent]="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button slot="start"></ion-menu-button>
<ion-back-button></ion-back-button>
<ion-back-button defaultHref="home" icon="chevron-back-outline"></ion-back-button>
</ion-buttons>
<ion-title>
Snap a spectrum
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="false">
<ion-card>
Connect your iSPEX unit to your phone and snap a spectrum.
</ion-card>
<ion-content [fullscreen]="true">
<div *ngIf="image">
<img #image id="img" [src]="image" alt="" srcset="">
<img [src]="image" alt="" srcset="">
</div>
<div *ngIf="uploadSuccess">
<ion-button expand="block" routerLink="classify" routerDirection="forward">
Lets classify this spectrum!
</ion-button>
</div>
<ion-button (click)="openCamera()" color="primary" expand="block" fill="solid" size="default">
Attach the iSPEX unit and open camera
Snap a spectrum
</ion-button>
<ion-card>
<ion-card-title>Mount iSPEX2 unit on phone</ion-card-title>
<ion-card-subtitle></ion-card-subtitle>
<ion-card-content>1. If you have a smartphone
cover or case, remove it.<br>
2. Take your smartphone in
one hand and the iSPEX2
add-on in the other. If this
is not possible, use a table
or similar surface to lay the
smartphone on.<br>
<ion-img src="assets/images/ispex-mount1.png"></ion-img>
3. The iSPEX2 add-on will be mounted on the back side of the phone and the clip will be on the front screen.
Slide your smartphone into the smartphone clip. You may need to use your other
hand to slightly open the clip, especially for thicker
smartphones.<br>
<ion-img src="assets/images/ispex-mount3.png"></ion-img>
4. Slide the iSPEX2
add-on over your
smartphone until the
bars on the top and
left sides fit snugly.<br>
<ion-img src="assets/images/ispex-mount2.png"></ion-img>
5. Firmly press the suction cup on the
back of the smartphone until it is
clearly attached.<br>
<ion-img src="assets/images/ispex-mount4.png"></ion-img>
6. Gently move the smartphone back
and forth to ensure the add-on is
firmly attached.
If the add-on appears to rotate or slide off, start again. </ion-card-content>
</ion-card>
<canvas id="canvasOutput"></canvas>
</ion-content>

View File

@@ -1,75 +1,100 @@
import { Component, OnInit, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
/* eslint-disable @typescript-eslint/naming-convention */
import * as Parse from 'parse';
import { ENV } from '../../app.constant';
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { ModalController } from '@ionic/angular';
import { PreviewPage } from './preview/preview.page';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '../../services/auth.service';
import { Auth, user } from '@angular/fire/auth';
import { Storage } from '@ionic/storage-angular';
import { Router } from '@angular/router';
declare var cv: any;
Parse.initialize(ENV.parseAppId, ENV.parseJSKey);
(Parse as any).serverURL = ENV.parseServerUrl; // use your server url
let uploadSuccess = false;
@Component({
selector: 'app-ispex',
templateUrl: './ispex.page.html',
styleUrls: ['./ispex.page.scss'],
})
export class IspexPage implements OnInit {
uploadSuccess = false; // Add this line
image = '';
user = null;
language = '';
export class IspexPage implements AfterViewInit {
@ViewChild('imageCanvas', { static: false }) canvasEl : ElementRef;
@ViewChild('image', { static: false }) imageEl : ElementRef;
image = null;
constructor(private modal: ModalController) {}
ngAfterViewInit() {
// this._CANVAS = this.canvasEl.nativeElement;
// this._IMAGE = this.imageEl.nativeElement;
console.log ("ngAfterViewInit");
constructor(
private modal: ModalController,
private translateService: TranslateService,
private authService: AuthService,
private afAuth: Auth,
private storage: Storage,
private router: Router
) {
user(this.afAuth).subscribe((response) => {
//fill the user to verify if someone is logged in
this.user = response;
console.log(this.user.uid);
});
}
ngOnInit() {}
async openCamera() {
const modal = await this.modal.create({
component: PreviewPage,
cssClass: 'fullscreen',
animated: true
animated: true,
});
modal.onDidDismiss().then((data) => {
if (data !== null) {
this.image = data.data;
if (data !== null) {
this.image = data.data;
this.uploadSuccess = true;
// create parse class
let imgElement = document.getElementById('img');
let src = cv.imread(imgElement);
let dst = new cv.Mat();
let dsize = new cv.Size(src.rows, src.cols);
let center = new cv.Point(src.cols / 2, src.rows / 2);
let M = cv.getRotationMatrix2D(center, -90, 1);
cv.warpAffine(src, dst, M, dsize, cv.INTER_LINEAR, cv.BORDER_CONSTANT, new cv.Scalar());
cv.imshow('canvasOutput', dst);
src.delete(); dst.delete(); M.delete();
const myspex_data_store = Parse.Object.extend('myspex_data');
// create new instance of parse class
cv.imshow('canvasOutput', dst);
src.delete(); dst.delete();
const myspex_data = new myspex_data_store();
// set value for parse clas
const file = new Parse.File('image.jpg', { base64: this.image });
file.save().then(
(file) => {
console.log(file);
},
(error) => {
console.log(error);
}
);
myspex_data.set('user_uid', this.user.uid);
myspex_data.set('name', this.user.displayName);
myspex_data.set('email', this.user.email);
myspex_data.set('image', file);
}
myspex_data.save().then(
(result: any) => {
console.log(result);
},
(error: any) => {
console.log(error);
}
);
} else {
console.log('no data');
}
});
return await modal.present();
}
// navigate to the classify page
}

View File

@@ -1,32 +1,13 @@
<ion-content id="content" [fullscreen]="true">
<div *ngIf="cameraActive">
<ion-button (click)="stopCamera()" expand="block" id="close">
X</ion-button>
<ion-button (click)="takePicture(1)" expand="block" id="grey">
Grey
</ion-button>
<ion-button (click)="takePicture(2)" expand="block" id="sky">
Sky
</ion-button>
<ion-button (click)="takePicture(3)" expand="block" id="water">
Water
<ion-icon slot="icon-only" name="close-circle"></ion-icon>
</ion-button>
<ion-button (click)="takePicture(4)" expand="block" id="free">
Free
<ion-button (click)="takePicture()" expand="block" id="capture">
<ion-icon slot="icon-only" name="camera"></ion-icon>
</ion-button>
</div>
</ion-content>

View File

@@ -1,65 +1,42 @@
ion-content {
--background: transparent !important;
}
#capture {
position: absolute;
bottom: 30px;
left: calc(50% - 25px);
width: 50px;
height: 50px;
z-index: 99999;
}
#flip {
position: absolute;
bottom: 30px;
left: calc(50% + 125px);
width: 50px;
height: 50px;
z-index: 99999;
}
#close {
position: absolute;
bottom: 30px;
left: calc(50% - 160px);
left: calc(50% - 175px);
width: 50px;
height: 50px;
z-index: 99999;
}
#rgb {
position: absolute;
bottom: 30px;
left: calc(50% - 140px);
width: 50px;
height: 50px;
z-index: 99999;
#capture::part(native) {
border-radius: 30px;
}
#grey {
position: absolute;
bottom: 30px;
left: calc(50% - 80px);
width: 50px;
height: 50px;
z-index: 99999;
#close::part(native) {
border-radius: 30px;
}
#sky {
position: absolute;
bottom: 30px;
left: calc(50% - 20px);
width: 50px;
height: 50px;
z-index: 99999;
#flip::part(native) {
border-radius: 30px;
}
#water{
position: absolute;
bottom: 30px;
left: calc(50% + 40px);
width: 50px;
height: 50px;
z-index: 99999;
}
#free {
position: absolute;
bottom: 30px;
left: calc(50% + 100px);
width: 50px;
height: 50px;
z-index: 99999;
}
ion-content { --background: black;}

View File

@@ -1,15 +1,9 @@
import * as Parse from 'parse';
import { ENV } from '../../../app.constant';
import { Vibration } from '@ionic-native/vibration/ngx';
import { Component, OnInit } from '@angular/core';
import { Plugins } from "@capacitor/core"
const { CameraPreview } = Plugins;
import { CameraPreviewOptions, CameraPreviewPictureOptions, CameraSampleOptions } from '@capacitor-community/camera-preview';
import { CameraPreviewOptions, CameraPreviewPictureOptions } from '@capacitor-community/camera-preview';
import '@capacitor-community/camera-preview';
import { ModalController } from '@ionic/angular';
import { Geolocation} from '@capacitor/geolocation';
import { Device } from '@capacitor/device';
@Component({
selector: 'app-preview',
@@ -17,243 +11,46 @@ import { Device } from '@capacitor/device';
styleUrls: ['./preview.page.scss'],
})
export class PreviewPage implements OnInit {
image = null;
image ="";
cameraActive = false;
private parseAppId: string = ENV.parseAppId;
private parseServerUrl: string = ENV.parseServerUrl;
private parseJSKey: string=ENV.parseJSKey;
public datetime_ux: string;
public datetime: string;
public latitude: number;
public longitude: number;
public altitude: number;
public heading: number;
// public deviceAppVersion: string;
// public deviceAppBuild: string;
public deviceOsVersion: string;
public devicePlatform: string;
public deviceManufacturer: string;
public deviceModel: string;
constructor(private modal: ModalController, private vibration: Vibration
) { }
constructor(private modal: ModalController) { }
ngOnInit() {
this.getLocation();
this.parseInitialize();
this.launchCamera();
this.getDeviceInfo();
}
}
launchCamera() {
const cameraPreviewOptions: CameraPreviewOptions = {
position: 'rear', // front or rear
const cameraPreviewOptions: CameraPreviewOptions = {
position: 'front', // front or rear
parent: 'content', // the id on the ion-content
className: '',
width: window.screen.width, //width of the camera display
height:window.screen.height-200, //height of the camera
height: window.screen.height - 200, //height of the camera
toBack: false,
disableAudio: true,
rotateWhenOrientationChanged: false,
lockAndroidOrientation: true
enableHighResolution: true,
};
CameraPreview.start(cameraPreviewOptions);
CameraPreview['start'](cameraPreviewOptions);
this.cameraActive = true;
}
async takePicture( dngType ) {
// buzz for user feedback
this.vibration.vibrate(1);
// this captures a raw image. replace quality with type
async takePicture() {
const cameraPreviewPictureOptions: CameraPreviewPictureOptions = {
quality: dngType// misuse quality: 0 normal (rgb) image 1 gray card 2 sky image 3 water image 4 other spectro(pol)
quality: 90
};
const result = await CameraPreview.capture(cameraPreviewPictureOptions);
const result = await CameraPreview['capture'](cameraPreviewPictureOptions);
this.image = `data:image/jpeg;base64,${result.value}`;
this.getLocation();
// save to parse
const base64PictureData = result.value;
let d = new Date();
this.datetime=d.toString();
var unixTimeStamp = Math.floor(d.getTime() / 1000);
this.datetime_ux=unixTimeStamp.toString();
// do something with base64PictureData
var Base64SnapImage:string;
Base64SnapImage = "data:image/jpeg;base64," + base64PictureData;
var Base64DNGImage:string;
var Save2Parse = Parse.Object.extend('ispex_data');
var iSPEX2_store = new Save2Parse();
// set initial data record
// upload thumb for user feedback. DNG's are uploaded within the native part
const thumbFiletoUpload = new Parse.File("thumb.jpg", { base64: Base64SnapImage},"image/jpeg");
thumbFiletoUpload.save().then(function() {
console.log ("saved thumb!")
}, function(error) {
// The file either could not be read, or could not be saved to Parse.
console.log ("error saving thumb")
});
iSPEX2_store.set('thumb', thumbFiletoUpload);
iSPEX2_store.set('latitude',this.latitude);
iSPEX2_store.set('longitude',this.longitude);
iSPEX2_store.set('altitude',this.altitude);
iSPEX2_store.set('heading',this.heading);
iSPEX2_store.set('dngFileURL',result.dngFileURL);
iSPEX2_store.set('heading',result.heading);
iSPEX2_store.set('device_angle',result.device_angle);
iSPEX2_store.set ('device_osversion', this.deviceOsVersion);
iSPEX2_store.set ('device_platform', this.devicePlatform);
iSPEX2_store.set ('device_manufacturer', this.deviceManufacturer);
iSPEX2_store.set ('device_model', this.deviceModel);
iSPEX2_store.set ('datetimerecorded', this.datetime);
iSPEX2_store.set ('datetime_ux', this.datetime_ux);
await iSPEX2_store.save();
this.stopCamera();
}
async getLocation() {
const position = await Geolocation.getCurrentPosition({enableHighAccuracy: true});
this.latitude = position.coords.latitude;
this.heading= position.coords.heading;
// console.log (position.coords.latitude);
this.longitude = position.coords.longitude;
this.altitude = position.coords.altitude;
return position.coords;
}
async takeSample() {
}
async stopCamera() {
await CameraPreview.stop();
await CameraPreview['stop']();
this.modal.dismiss(this.image);
}
async flipCamera() {
await CameraPreview.flip();
await CameraPreview['flip']();
}
async getDeviceInfo() {
const info = await Device.getInfo();
// this.deviceAppVersion = info.appVersion;
// this.deviceAppBuild = info.appBuild;
this.deviceOsVersion = info.osVersion;
this.devicePlatform = info.platform;
this.deviceManufacturer = info.manufacturer;
this.deviceModel = info.model;
console.log(info);
}
private parseInitialize() {
Parse.initialize(this.parseAppId, this.parseJSKey);
(Parse as any).serverURL = this.parseServerUrl; // use your server url
}
}

View File

@@ -1,14 +1,13 @@
<ion-header>
<ion-toolbar>
<ion-title>Prepare</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">PREPARE</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-title size="large">Prepare the device</ion-title>
@@ -45,7 +44,7 @@ Attach (optional) pH Strip</ion-item>
4. Unlock spindle</ion-item>
<ion-item>
<img src="assets/img/unlockspindle.png"></ion-item>
<img src="assets/images/unlockspindle.png"></ion-item>
<ion-item>
@@ -56,7 +55,7 @@ Attach (optional) pH Strip</ion-item>
<ion-button expand="block" routerLink="/instructions/step2" routerDirection="forward" >
<ion-button expand="block" routerLink="step2" routerDirection="forward" >
Step 2
</ion-button>

View File

@@ -1,11 +1,9 @@
<ion-header>
<ion-toolbar>
<ion-title>2. Transperancy</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">2. Transparancy</ion-title>
</ion-toolbar>
</ion-header>
@@ -32,7 +30,7 @@ The app will now calculate the Secchi disk depth (re-appareance distance minus d
<ion-button expand="block" routerLink="/instructions/step3" routerDirection="forward" >
<ion-button expand="block" routerLink="../step3" routerDirection="forward" >
Step 3
</ion-button>

View File

@@ -1,11 +1,9 @@
<ion-header>
<ion-toolbar>
<ion-title>3. Disk color</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">3. Disk color</ion-title>
</ion-toolbar>
</ion-header>
@@ -22,7 +20,7 @@ The app will now calculate the Secchi disk depth (re-appareance distance minus d
</ion-item>
<ion-button expand="block" routerLink="/instructions/step4" routerDirection="forward" >
<ion-button expand="block" routerLink="../step4" routerDirection="forward" >
Step 4
</ion-button>

View File

@@ -1,18 +1,14 @@
<ion-header>
<ion-toolbar>
<ion-title>4. Color of the water</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">4. Color of the water</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
<ion-item>
Colour of the water surface
Estimate the Forel-Ule colour scale by looking at the water surface, away from sun glitter.
</ion-item>
@@ -26,7 +22,7 @@ Estimate the Forel-Ule colour scale by looking at the water surface, away from s
Keeping the sun diagonally behind you over your left or right shoulder, look down and forward to the water surface (40-45 degrees) and compare the colour of the water to the Forel-Ule index. Optionally upload another photo using the same viewing angles.</ion-item>
<img src="assets/images/forelule.png"><br>
<ion-button expand="block" routerLink="/instructions/step5" routerDirection="forward" >
<ion-button expand="block" routerLink="../step5" routerDirection="forward" >
Step 5

View File

@@ -1,12 +1,10 @@
<ion-header>
<ion-toolbar>
<ion-title>5. Additional</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">5. Additional</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
@@ -27,6 +25,6 @@ Natural waters are usually only slightly acidic (6) or alkalic (8). The acidity
<ion-button expand="block" routerLink="/home" routerDirection="root">Finish
<ion-button expand="block" routerLink="/" routerDirection="root">Finish
</ion-button>
</ion-content>

View File

@@ -1,11 +1,9 @@
<ion-header>
<ion-toolbar>
<ion-title>Colour at half depth</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">Colour at half depth</ion-title>
</ion-toolbar>
</ion-header>

View File

@@ -1,120 +1,127 @@
<ion-header>
<ion-toolbar>
<ion-title>Additional</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button
defaultHref="/"
icon="chevron-back-outline"
text=""
></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">Additional</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
<ion-icon name="warning-outline" color="danger"></ion-icon>You can now wind up the
Secchi disk mechanism.
</ion-item>
<ion-card><ion-icon name="warning-outline"></ion-icon>You can now wind up the Secchi disk mechanism.</ion-card>
<ion-item>
<ion-label> <b>Acidity</b></ion-label>
</ion-item>
<ion-item>
<ion-label>
<b>Acidity</b></ion-label>
</ion-item>
<ion-item>
If you used pH paper, compare the colour of the pH strip to the pH colour
index provided.
</ion-item>
<ion-item> <img src="assets/images/ph.png" /><br /> </ion-item>
<ion-item>
<ion-label> The pH value is (1-14):</ion-label>
</ion-item>
<ion-item>
<ion-input
[(ngModel)]="phvalue"
type="number"
inputmode="numeric"
value="1"
placeholder="Enter pH value here"
></ion-input>
</ion-item>
<ion-item>
<ion-label><b> Surface water colour</b> </ion-label>
</ion-item>
<ion-item>
You can upload a photo of the colour of the water, avoiding reflections from
the sun: Hold the camera 40-45 degrees from horizontal towards the water
surface. With the sun straight at your back rotate 45 degrees to your left
or your right to have the sun diagonally over your left or right shoulder.
</ion-item>
<ion-item>
If you used pH paper, compare the colour of the pH strip to the pH colour index provided.
</ion-item>
<ion-item>
<ion-item> <img src="assets/images/bodypos.png" /><br /> </ion-item>
<img src="assets/images/ph.png"><br>
</ion-item>
<ion-item>
<ion-label> The pH value is (1-14):</ion-label>
</ion-item>
<ion-item>
<ion-input [(ngModel)]="phvalue" type="number" inputmode="numeric" value="1" placeholder="Enter pH value here"></ion-input>
</ion-item>
<ion-item>
<ion-label><b>
Surface water colour</b>
</ion-label>
</ion-item>
<ion-item>
You can upload a photo of the colour of the water, avoiding reflections from the sun:
Hold the camera 40-45 degrees from horizontal towards the water surface.
With the sun straight at your back rotate 45 degrees to your left or your right to have the sun diagonally over your left or right shoulder.
</ion-item>
<ion-item>
<img src="assets/images/bodypos.png"><br>
</ion-item>
<ion-item>
To add a photo click here:
<ion-item>
To add a photo click here:
<ion-button (click)="takePicture()">
<ion-icon name="camera"></ion-icon>
</ion-button>
<img [src]="this.ColouratSurfacePictureTaken"/>
</ion-item>
<img [src]="this.ColouratSurfacePictureTaken" />
</ion-item>
<ion-item>
<ion-label
>Looking at the water surface in this direction, the colour of the water
is <br />
most similar to Forel-Ule colour number (1-21):
</ion-label>
</ion-item>
<ion-item>
<ion-input
[(ngModel)]="colouratsurface"
type="number"
inputmode="numeric"
placeholder="Enter colour value here"
></ion-input
></ion-item>
<ion-item>
<ion-label><b>Quality control check-list</b></ion-label>
</ion-item>
<ion-item>
<ion-checkbox slot="start" [(ngModel)]="bottom_visible"></ion-checkbox>
<ion-label class="ion-text-wrap"
>The bottom was visible / the disk reached the bottom while I could still
see it.</ion-label
>
</ion-item>
<ion-item>
<ion-checkbox slot="start" [(ngModel)]="end_of_tape"></ion-checkbox>&nbsp;
<ion-label class="ion-text-wrap"
>I reached the end of the tape while I could still see the disk</ion-label
>
</ion-item>
<ion-item>
<ion-checkbox slot="start"></ion-checkbox
><ion-label class="ion-text-wrap">
I was not able to keep the disk going down at a straight angle. The
estimated distance angle is:</ion-label
>
</ion-item>
<ion-item>
<ion-input
[(ngModel)]="angle_estimated"
type="number"
inputmode="numeric"
value="01"
placeholder="Enter"
></ion-input
><ion-label class="ion-text-wrap"> degrees from vertical.</ion-label>
</ion-item>
<ion-item>
<ion-label>Looking at the water surface in this direction, the colour of the water is <br> most similar to Forel-Ule colour number (1-21):
</ion-label>
</ion-item>
<ion-item>
<ion-input [(ngModel)]="colouratsurface" type="number" inputmode="numeric" placeholder="Enter colour value here"></ion-input></ion-item>
<ion-item>
<ion-label><b>Quality control check-list</b></ion-label>
</ion-item>
<ion-item >
<ion-checkbox slot="start" [(ngModel)]="bottom_visible"></ion-checkbox>
<ion-label class="ion-text-wrap">The bottom was visible / the disk reached the bottom while I could still see it.</ion-label>
</ion-item>
<ion-item>
<ion-checkbox slot="start" [(ngModel)]="end_of_tape"></ion-checkbox>&nbsp; <ion-label class="ion-text-wrap">I reached the end of the tape while I could still see the disk</ion-label>
</ion-item>
<ion-item>
<ion-checkbox slot="start"></ion-checkbox><ion-label class="ion-text-wrap">
I was not able to keep the disk going down at a straight angle. The estimated distance angle is:</ion-label>
</ion-item>
<ion-item>
<ion-input [(ngModel)]="angle_estimated" type="number" inputmode="numeric" value="01" placeholder="Enter" ></ion-input><ion-label class="ion-text-wrap">
degrees from vertical.</ion-label>
</ion-item>
<ion-button expand="block" (click)="validate()" routerLink="/ms_measure/qccheck" routerDirection="forward">
Go to step 5
</ion-button>
<ion-button
expand="block"
(click)="validate()"
routerLink="/ms_measure/qccheck"
routerDirection="forward"
>
Go to step 5
</ion-button>
</ion-content>

View File

@@ -1,14 +1,11 @@
<ion-header>
<ion-toolbar>
<ion-title>Distance to water</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">Distance to water</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
Wind the disk down to the water surface and record the distance from the handheld device to the water surface. Keep your arm at the same distance from the water for the remaining depth measurements.

View File

@@ -1,40 +1,39 @@
<ion-header>
<ion-toolbar>
<ion-title>Safety first</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">Safety first</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
<ion-label> <ion-icon name="warning-outline"></ion-icon>
Safety first</ion-label>
<ion-label>
<ion-icon name="warning-outline" color="danger"></ion-icon> Safety first</ion-label
>
</ion-item>
<ion-item>
Work from a stable platform, wear a life vest and make sure someone knows
where you are. Only upload photos if you can do so without risking your
safety and your phone. Find a spot where you can look into the water, away
from sun reflections. Record Secchi depth, water colour and pH without
sunglasses.
<br />
If you have pH paper, clip a new section into the provided clip and attach
it to the disk.
</ion-item>
<ion-item>
<br />
<ion-item>
<img src="assets/images/attachph.png" />
</ion-item>
Work from a stable platform, wear a life vest and make sure someone knows where you are. Only upload photos if you can do so without risking your safety and your phone.
Find a spot where you can look into the water, away from sun reflections. Record Secchi depth, water colour and pH without sunglasses.
<br>
If you have pH paper, clip a new section into the provided clip and attach it to the disk.
</ion-item>
<br>
<ion-item>
<img src="assets/images/attachph.png">
</ion-item>
<ion-button expand="block" routerLink="distancetowater" routerDirection="forward">
Go to step 1
</ion-button>
<ion-button
expand="block"
routerLink="distancetowater"
routerDirection="forward"
>
Go to step 1
</ion-button>
</ion-content>

View File

@@ -1,11 +1,9 @@
<ion-header>
<ion-toolbar>
<ion-title>Review</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">Review</ion-title>
</ion-toolbar>
</ion-header>

View File

@@ -3,7 +3,10 @@ import { Storage } from '@ionic/storage-angular';
import { Guid } from "guid-typescript";
import * as Parse from 'parse';
import { ENV } from '../../../../app.constant';
import { Geolocation} from '@capacitor/geolocation';
import { Geolocation } from '@capacitor/geolocation';
import { AuthService } from '../../../../services/auth.service';
import { Auth, user } from '@angular/fire/auth';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-qccheck',
@@ -11,143 +14,150 @@ import { Geolocation} from '@capacitor/geolocation';
styleUrls: ['./qccheck.page.scss'],
})
export class QccheckPage implements OnInit {
// now get all the values
private parseAppId: string = ENV.parseAppId;
private parseServerUrl: string = ENV.parseServerUrl;
private parseJSKey: string=ENV.parseJSKey;
public swversion_number:string;
public swversion_code:number;
public latitude: number;
public longitude: number;
public distancetowater: number;
public reappear: number;
public colourathalfdepth: number;
public colourathalfdepthimage: string;
public colouratsurface: number;
public colouratsurfaceimage: string;
public phvalue: number;
public bottom_visible: string;
public end_of_tape: string;
public angle_estimated: number;
public datetime: Date;
public datetime_ux: string;
public secchi_depth: string;
// now get all the values
private parseAppId: string = ENV.parseAppId;
private parseServerUrl: string = ENV.parseServerUrl;
private parseJSKey: string = ENV.parseJSKey;
user = null;
language= '';
public swversion_number: string;
public swversion_code: number;
public latitude: number;
public longitude: number;
public distancetowater: number;
public reappear: number;
public colourathalfdepth: number;
public colourathalfdepthimage: string;
public colouratsurface: number;
public colouratsurfaceimage: string;
public phvalue: number;
public bottom_visible: string;
public end_of_tape: string;
public angle_estimated: number;
public datetime: Date;
public datetime_ux: string;
public secchi_depth: string;
public rec_uid: string;
public rec_uid: string;
newSecchi = { uid: null, swversion_number:null, swversion_code:null, latitude: null, longitude: null, distancetowater: null, reappear: null, colourathalfdepth: null, colourathalfdepthimage: null, colouratsurface: null, colouratsurfaceimage: null, datetimerecorded: null, datetime_ux:null, bottom_visible:null, end_of_tape:null, phvalue:null, angle_estimated:null, secchi_depth:null };
constructor(private storage: Storage) {
newSecchi = { uid: null, swversion_number: null, swversion_code: null, latitude: null, longitude: null, distancetowater: null, reappear: null, colourathalfdepth: null, colourathalfdepthimage: null, colouratsurface: null, colouratsurfaceimage: null, datetimerecorded: null, datetime_ux: null, bottom_visible: null, end_of_tape: null, phvalue: null, angle_estimated: null, secchi_depth: null };
constructor(private storage: Storage, private translateService: TranslateService,
private authService: AuthService,
private afAuth: Auth) {
user(this.afAuth).subscribe((response) => {
//fill the user to verify if someone is logged in
this.user = response;
console.log (this.user.uid)
});
this.rec_uid = Guid.raw(); // make it a string
}
}
ngOnInit() {
this.storage.create();
this.parseInitialize();
this.getLocation();
this.storage.create();
this.parseInitialize();
this.getLocation();
this.storage.get('swversion_number').then((val) => {
this.storage.get('swversion_number').then((val) => {
this.swversion_number= val;
this.swversion_number = val;
});
});
this.storage.get('swversion_code').then((val) => {
this.storage.get('swversion_code').then((val) => {
this.swversion_code= val;
this.swversion_code = val;
});
});
this.storage.get('latitude').then((val) => {
this.storage.get('latitude').then((val) => {
this.latitude= val;
this.latitude = val;
});
});
this.storage.get('longitude').then((val) => {
this.storage.get('longitude').then((val) => {
this.longitude= val;
this.longitude = val;
});
});
this.storage.get('distancetowater').then((val) => {
this.storage.get('distancetowater').then((val) => {
this.distancetowater= val;
this.distancetowater = val;
});
});
this.storage.get('reappear').then((val) => {
this.reappear= val;
this.reappear = val;
});
});
this.storage.get('colourathalfdepth').then((val) => {
this.colourathalfdepth= val;
this.colourathalfdepth = val;
});
});
this.storage.get('colouratsurface').then((val) => {
this.colouratsurface= val;
this.colouratsurface = val;
});
});
this.storage.get('colourathalfdepthimage').then((val) => {
// check if it is null
if (val==null) {
console.log("colourathalfdepthimage is null");
return;
}
this.storage.get('colourathalfdepthimage').then((val) => {
// check if it is null
if (val == null) {
console.log("colourathalfdepthimage is null");
return;
}
// convert to Parse file
val = new Parse.File("colourathalfdepthimage.png", { base64: val},"image/png");
// convert to Parse file
val = new Parse.File("colourathalfdepthimage.png", { base64: val }, "image/png");
this.colourathalfdepthimage= val;
this.colourathalfdepthimage = val;
});
});
this.storage.get('colouratsurfaceimage').then((val) => {
// check if it is null
if (val==null) {
console.log("colouratsurfaceimage is null");
return;
// check if it is null
if (val == null) {
console.log("colouratsurfaceimage is null");
return;
}
}
// convert to Parse file
val = new Parse.File("colouratsurfaceimage.png", { base64: val},"image/png");
// convert to Parse file
val = new Parse.File("colouratsurfaceimage.png", { base64: val }, "image/png");
this.colouratsurfaceimage= val;
this.colouratsurfaceimage = val;
@@ -156,7 +166,7 @@ this.colouratsurfaceimage= val;
});
});
@@ -165,107 +175,111 @@ this.colouratsurfaceimage= val;
this.storage.get('phvalue').then((val) => {
this.phvalue= val;
this.phvalue = val;
});
});
this.storage.get('bottom_visible').then((val) => {
this.bottom_visible= val;
this.bottom_visible = val;
});
});
this.storage.get('end_of_tape').then((val) => {
this.end_of_tape= val;
this.end_of_tape = val;
});
});
this.storage.get('angle_estimated').then((val) => {
this.angle_estimated= val;
this.angle_estimated = val;
});
});
this.storage.get('secchi_depth').then((val) => {
this.storage.get('secchi_depth').then((val) => {
this.secchi_depth= val;
this.secchi_depth = val;
});
});
let d = new Date();
let d = new Date();
this.datetime=d;
this.datetime = d;
var unixTimeStamp = Math.floor(d.getTime() / 1000);
this.datetime_ux=unixTimeStamp.toString();
var unixTimeStamp = Math.floor(d.getTime() / 1000);
this.datetime_ux = unixTimeStamp.toString();
}
async validate() {
// now save to Parse
// now save to Parse
var secchi_data = Parse.Object.extend('mini_secchi_data');
var secchi_store = new secchi_data();
// set initial data record
console.log("saving to Parse");
var secchi_data = Parse.Object.extend('mini_secchi_data');
var secchi_store = new secchi_data();
// set initial data record
console.log("saving to Parse");
secchi_store.uid= this.rec_uid;
secchi_store.latitude=this.latitude;
secchi_store.longitude=this.longitude;
secchi_store.distancetowater=this.distancetowater;
secchi_store.reappear=this.reappear;
secchi_store.colourathalfdepth=this.colourathalfdepth;
secchi_store.colourathalfdepthimage=this.colourathalfdepthimage;
secchi_store.colouratsurface=this.colouratsurface;
secchi_store.colouratsurfaceimage=this.colouratsurfaceimage;
secchi_store.phvalue=this.phvalue;
secchi_store.bottom_visible=this.bottom_visible;
secchi_store.end_of_tape=this.end_of_tape;
secchi_store.angle_estimated=this.angle_estimated;
secchi_store.secchi_depth=this.secchi_depth;
secchi_store.datetime_ux=this.datetime_ux.toString();
secchi_store.datetimerecorded=this.datetime.toISOString();
console.log ('saving to Parse', secchi_store);
try {
const result = await secchi_store.save();
console.log('Data saved successfully:', result);
} catch (error) {
console.error('Error saving data:', error);
// You could add additional handling for the error here, such as displaying a message to the user or retrying the save operation.
}
secchi_store.set('uid', this.rec_uid);
secchi_store.set('latitude', this.latitude);
secchi_store.set('longitude', this.longitude);
secchi_store.set('distancetowater', this.distancetowater);
secchi_store.set('reappear', this.reappear);
secchi_store.set('colourathalfdepth', this.colourathalfdepth);
secchi_store.set('colourathalfdepthimage', this.colourathalfdepthimage);
secchi_store.set('colouratsurface', this.colouratsurface);
secchi_store.set('colouratsurfaceimage', this.colouratsurfaceimage);
secchi_store.set('phvalue', this.phvalue);
secchi_store.set('bottom_visible', this.bottom_visible);
secchi_store.set('end_of_tape', this.end_of_tape);
secchi_store.set('angle_estimated', this.angle_estimated);
secchi_store.set('secchi_depth', this.secchi_depth);
secchi_store.set('datetime_ux', this.datetime_ux.toString());
secchi_store.set('datetimerecorded', this.datetime.toISOString());
secchi_store.set('user_uid', this.user.uid);
secchi_store.set('name', this.user.displayName);
secchi_store.set('email', this.user.email);
}
try {
const result = await secchi_store.save();
console.log('Data saved successfully:', result);
} catch (error) {
console.error('Error saving data:', error);
// You could add additional handling for the error here, such as displaying a message to the user or retrying the save operation.
}
async getLocation() {
const position = await Geolocation.getCurrentPosition({enableHighAccuracy: true});
this.latitude = position.coords.latitude;
// console.log (position.coords.latitude);
this.longitude = position.coords.longitude;
return position.coords;
}
private parseInitialize() {
Parse.initialize(this.parseAppId, this.parseJSKey);
}
(Parse as any).serverURL = this.parseServerUrl; // use your server url
async getLocation() {
const position = await Geolocation.getCurrentPosition({ enableHighAccuracy: true });
this.latitude = position.coords.latitude;
// console.log (position.coords.latitude);
this.longitude = position.coords.longitude;
return position.coords;
}
private parseInitialize() {
Parse.initialize(this.parseAppId, this.parseJSKey);
(Parse as any).serverURL = this.parseServerUrl; // use your server url
}

View File

@@ -1,11 +1,9 @@
<ion-header>
<ion-toolbar>
<ion-title>Disk reappearance</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-header class="ion-no-border">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center">Disk reappearance</ion-title>
</ion-toolbar>
</ion-header>

View File

@@ -1,33 +1,61 @@
<ion-header>
<ion-toolbar color="primary">
<ion-title>New Mini Secchi observation</ion-title>
<ion-toolbar color="black">
<ion-title>Mini Secchi</ion-title>
<ion-buttons slot="start">
<ion-menu-button slot="start"></ion-menu-button>
<ion-back-button></ion-back-button>
<ion-back-button defaultHref="/" icon="chevron-back-outline" text=""></ion-back-button>
</ion-buttons>
<ion-title>Mini Secchi</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-text-center">
<img src="assets/images/secchidisk.png">
</div>
<ion-item>
<img src="assets/images/secchidisk.png"><br>
</ion-item>
<ion-button shape="round" expand="block" routerLink="/ms_measure" strong="true" routerDirection="forward" color="white" style="background-image: linear-gradient(90deg, rgba(2,0,36,1) 0%, rgba(86,143,150) 25%, rgba(123,166,84) 35%, rgba(149,182,69) 50%, rgba(173,181,95) 70%, rgba(161,77,4) 100%)" >
New observation
</ion-button>
<ion-button shape="round" color="white" expand="block" routerLink="/ms_instructions" strong="true" routerDirection="forward" color="white" style="background-image: linear-gradient(90deg, rgba(2,0,36,1) 0%, rgba(86,143,150) 25%, rgba(123,166,84) 35%, rgba(149,182,69) 50%, rgba(173,181,95) 70%, rgba(161,77,4) 100%)" >
Instructions
</ion-button>
</ion-content>
<ion-button
shape="round"
expand="block"
routerLink="/ms_measure"
strong="true"
routerDirection="forward"
color="white"
style="
background-image: linear-gradient(
90deg,
rgba(2, 0, 36, 1) 0%,
rgba(86, 143, 150) 25%,
rgba(123, 166, 84) 35%,
rgba(149, 182, 69) 50%,
rgba(173, 181, 95) 70%,
rgba(161, 77, 4) 100%
);
"
>
New observation
</ion-button>
<ion-button
shape="round"
color="white"
expand="block"
routerLink="/ms_instructions"
strong="true"
routerDirection="forward"
color="white"
style="
background-image: linear-gradient(
90deg,
rgba(2, 0, 36, 1) 0%,
rgba(86, 143, 150) 25%,
rgba(123, 166, 84) 35%,
rgba(149, 182, 69) 50%,
rgba(173, 181, 95) 70%,
rgba(161, 77, 4) 100%
);
"
>
Instructions
</ion-button>
</ion-content>

View File

@@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ClassifyPage } from './classify.page';
const routes: Routes = [
{
path: '',
component: ClassifyPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ClassifyPageRoutingModule {}

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { ClassifyPageRoutingModule } from './classify-routing.module';
import { ClassifyPage } from './classify.page';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ClassifyPageRoutingModule
],
declarations: [ClassifyPage]
})
export class ClassifyPageModule {}

View File

@@ -0,0 +1,33 @@
<ion-header>
<ion-toolbar>
<ion-title>Image classification</ion-title>
<ion-buttons slot="start">
<ion-back-button defaultHref="home" icon="chevron-back-outline"></ion-back-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
Best match:
<ion-label>
{{result}}
</ion-label>
</ion-item>
<ion-button expand="block" (click)="submit_image_with_classification()">Submit</ion-button>
</ion-content>

View File

@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { ClassifyPage } from './classify.page';
describe('ClassifyPage', () => {
let component: ClassifyPage;
let fixture: ComponentFixture<ClassifyPage>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ClassifyPage ],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(ClassifyPage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,225 @@
import { Component, OnInit } from '@angular/core';
import { ENV } from '../../../app.constant';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '../../../services/auth.service';
import { Auth, user } from '@angular/fire/auth';
import { Storage } from '@ionic/storage-angular';
import { Router } from '@angular/router';
import { Geolocation } from '@capacitor/geolocation';
import { Http } from '@capacitor-community/http';
import * as Parse from 'parse';
// debug purpose: Chrome without CORS on mac
// open /Applications/Google\ Chrome.app --args --user-data-dir="/var/tmp/Chrome dev session" --disable-web-security
@Component({
selector: 'app-classify',
templateUrl: './classify.page.html',
styleUrls: ['./classify.page.scss'],
})
export class ClassifyPage implements OnInit {
PROJECT = 'all'; // try 'weurope' or 'canada'
API_URL = 'https://my-api.plantnet.org/v2/identify/' + this.PROJECT + '?api-key=';
API_PRIVATE_KEY = ENV.plantnetKey; // secret
API_SIMSEARCH_OPTION = '&include-related-images=true'; // optional: get most similar images
API_LANG = '&lang=en'; // default: en
// add result variabel for showing result to user
result = '';
parsefile: any;
parsefileName: any;
parsefileUrl: any;
uploadSuccess = false; // Add this line
image = '';
user = null;
language = '';
latitude: number;
longitude: number;
altitude: number;
constructor(
private storage: Storage,
private authService: AuthService,
private afAuth: Auth,
private translateService: TranslateService,
private router: Router
) {
user(this.afAuth).subscribe((response) => {
//fill the user to verify if someone is logged in
this.user = response;
});
}
ngOnInit() {
// get location
this.getLocation();
Parse.initialize(ENV.parseAppId, ENV.parseJSKey);
(Parse as any).serverURL = ENV.parseServerUrl; // use your server url
this.storage.create();
// get file out of storage and submit to parse
this.storage.get('plantnet_image').then((val) => { // get image from storage
this.image = val;
this.parsefile = new Parse.File('plantnet_image.jpg', { base64: this.image });
this.parsefile.save().then(
(response: any) => {
this.parsefileUrl = this.parsefile.toJSON().url;
this.parsefileName = this.parsefile.toJSON().name;
console.log('parsefileUrl', this.parsefileUrl);
console.log('parsefileName', this.parsefileName);
// call function to submit to plantnet
this.submit_to_plantnet();
}
)
});
};
submit_image_with_classification() {
const plantnet_data_store = Parse.Object.extend('plantnet_data');
// create new instance of parse class
const plantnet_data = new plantnet_data_store();
// set value for parse clas
plantnet_data.set('user_uid', this.user.uid);
plantnet_data.set('name', this.user.displayName);
plantnet_data.set('email', this.user.email);
plantnet_data.set('image', this.parsefile);
plantnet_data.set('classification', this.result);
plantnet_data.set('latitude', this.latitude);
plantnet_data.set('longitude', this.longitude);
plantnet_data.set('altitude', this.altitude);
plantnet_data.save().then(
(result: any) => {
console.log(result);
},
(error: any) => {
console.log(error);
}
);
// go to plantnet page
this.router.navigate(['/plantnet']);
}
async getLocation() {
const position = await Geolocation.getCurrentPosition({
enableHighAccuracy: true,
});
this.latitude = position.coords.latitude;
console.log(position.coords.latitude);
this.longitude = position.coords.longitude;
this.altitude = position.coords.altitude;
return position.coords;
}
submit_to_plantnet() {
const imageUri = this.parsefileUrl;
const imageType = 'image/jpeg';
const imageName = this.parsefileName;
fetch(imageUri)
.then(response => response.blob())
.then(blob => {
const formData = new FormData();
formData.append('images', new File([blob], imageName, { type: imageType }));
formData.append('organs', 'auto');
const url = 'https://my-api.plantnet.org/v2/identify/all';
const headers = {
'accept': 'application/json',
'Content-Type': 'multipart/form-data'
};
const params = {
'include-related-images': 'false',
'no-reject': 'false',
'lang': 'en',
'api-key': '2b10bmIKkNNcBL6D4jwq3il4rO'
};
Http.request({
method: 'POST',
url: url,
headers: headers,
params: params,
data: formData
}).then(response => {
console.log(response.data);
// show result on to user
this.result = response.data.results[0].species.scientificName;
console.log(this.result);
}).catch(error => {
console.log ('error in response', error);
this.result = 'Species not found';
});
})
.catch(error => {
console.error(error);
});
// route to plantnet page
}
}

View File

@@ -7,7 +7,16 @@ const routes: Routes = [
{
path: '',
component: PlantnetPage
}
},
{
path: 'classify',
loadChildren: () => import('./classify/classify.module').then( m => m.ClassifyPageModule)
},
{
path: 'plantnet',
loadChildren: () => import('./plantnet.module').then( m => m.PlantnetPageModule)
},
];
@NgModule({

View File

@@ -1,29 +1,24 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { PlantnetPageRoutingModule } from './plantnet-routing.module';
import { TranslateModule } from '@ngx-translate/core';
import { PlantnetPage } from './plantnet.page';
import { Component, OnInit } from '@angular/core';
import { MenuController } from '@ionic/angular';
import { TranslateService } from '@ngx-translate/core';
import { Geolocation, PositionOptions } from '@capacitor/geolocation';
import { PreviewPage } from './preview/preview.page';
import { PreviewPageModule } from "./preview/preview.module"
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
PlantnetPageRoutingModule
PlantnetPageRoutingModule,
TranslateModule,
PreviewPageModule
],
declarations: [PlantnetPage]
declarations: [PlantnetPage],
})
export class PlantnetPageModule {}

View File

@@ -1,15 +1,32 @@
<ion-header>
<ion-toolbar color="primary">
<ion-title>New PlantNet observation</ion-title>
<ion-header [translucent]="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button slot="start"></ion-menu-button>
<ion-back-button></ion-back-button>
<ion-back-button defaultHref="home" icon="chevron-back-outline"></ion-back-button>
</ion-buttons>
<ion-title>
Take a picture
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content [fullscreen]="false">
<ion-card>
<ion-card-header>Welcome to the Plantnet API</ion-card-header>
</ion-card>
<div *ngIf="image">
<img [src]="image" alt="" srcset="">
</div>
<div *ngIf="uploadSuccess">
<ion-button expand="block" routerLink="classify" routerDirection="forward">
Lets classify this image!
</ion-button>
</div>
<ion-button (click)="openCamera()" color="primary" expand="block" fill="solid" size="default">
Take a picture
</ion-button>
</ion-content>

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { PlantnetPage } from './plantnet.page';
import { IspexPage } from './plantnet.page';
describe('PlantnetPage', () => {
let component: PlantnetPage;
let fixture: ComponentFixture<PlantnetPage>;
let component: IspexPage;
let fixture: ComponentFixture<IspexPage>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ PlantnetPage ],
declarations: [ IspexPage ],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(PlantnetPage);
fixture = TestBed.createComponent(IspexPage);
component = fixture.componentInstance;
fixture.detectChanges();
}));

View File

@@ -1,6 +1,15 @@
import { Component, OnInit } from '@angular/core';
import { HttpClient} from '@angular/common/http';
/* eslint-disable @typescript-eslint/naming-convention */
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { ModalController } from '@ionic/angular';
import { PreviewPage } from './preview/preview.page';
import { TranslateService } from '@ngx-translate/core';
import { Storage } from '@ionic/storage-angular';
import { Router } from '@angular/router';
let uploadSuccess = false;
@Component({
selector: 'app-plantnet',
@@ -8,24 +17,58 @@ import { HttpClient} from '@angular/common/http';
styleUrls: ['./plantnet.page.scss'],
})
export class PlantnetPage implements OnInit {
uploadSuccess = false; // Add this line
image = '';
user = null;
language = '';
latitude: number;
longitude: number;
altitude: number;
constructor(private http: HttpClient) { }
constructor(
private modal: ModalController,
private translateService: TranslateService,
private storage: Storage,
private router: Router
) {
}
ngOnInit() {
// now
const data = {
PROJECT: 'all',
API_URL : 'https://my-api.plantnet.org/v2/identify/all?api-key=',
API_KEY : 'my-api-key',
API_SIMSEARCH_OPTION : '&include-related-images=true',
API_LANG : '&lang=en'
};
this.http.post('http://your-api-endpoint.com', data).subscribe((response) => {
console.log(response);
});
}
}
async openCamera() {
const modal = await this.modal.create({
component: PreviewPage,
cssClass: 'fullscreen',
animated: true,
});
modal.onDidDismiss().then(async (data) => {
if (data !== null) {
this.image = data.data;
this.uploadSuccess = true;
// create parse class
// store image in local storage
await this.storage.set('plantnet_image', this.image);
} else {
console.log('no data');
}
});
return await modal.present();
}
// navigate to the classify page
}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PreviewPage } from './preview.page';
const routes: Routes = [
{
path: '',
component: PreviewPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class PreviewPageRoutingModule {}

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { PreviewPageRoutingModule } from './preview-routing.module';
import { PreviewPage } from './preview.page';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
PreviewPageRoutingModule
],
declarations: [PreviewPage]
})
export class PreviewPageModule {}

View File

@@ -0,0 +1,13 @@
<ion-content id="content" [fullscreen]="true">
<div *ngIf="cameraActive">
<ion-button (click)="stopCamera()" expand="block" id="close">
<ion-icon slot="icon-only" name="close-circle"></ion-icon>
</ion-button>
<ion-button (click)="takePicture()" expand="block" id="capture">
<ion-icon slot="icon-only" name="camera"></ion-icon>
</ion-button>
</div>
</ion-content>

View File

@@ -0,0 +1,42 @@
ion-content {
--background: transparent !important;
}
#capture {
position: absolute;
bottom: 30px;
left: calc(50% - 25px);
width: 50px;
height: 50px;
z-index: 99999;
}
#flip {
position: absolute;
bottom: 30px;
left: calc(50% + 125px);
width: 50px;
height: 50px;
z-index: 99999;
}
#close {
position: absolute;
bottom: 30px;
left: calc(50% - 175px);
width: 50px;
height: 50px;
z-index: 99999;
}
#capture::part(native) {
border-radius: 30px;
}
#close::part(native) {
border-radius: 30px;
}
#flip::part(native) {
border-radius: 30px;
}

View File

@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { PreviewPage } from './preview.page';
describe('PreviewPage', () => {
let component: PreviewPage;
let fixture: ComponentFixture<PreviewPage>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ PreviewPage ],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(PreviewPage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,56 @@
import { Component, OnInit } from '@angular/core';
import { Plugins } from "@capacitor/core"
const { CameraPreview } = Plugins;
import { CameraPreviewOptions, CameraPreviewPictureOptions } from '@capacitor-community/camera-preview';
import '@capacitor-community/camera-preview';
import { ModalController } from '@ionic/angular';
@Component({
selector: 'app-preview',
templateUrl: './preview.page.html',
styleUrls: ['./preview.page.scss'],
})
export class PreviewPage implements OnInit {
image ="";
cameraActive = false;
constructor(private modal: ModalController) { }
ngOnInit() {
this.launchCamera();
}
launchCamera() {
const cameraPreviewOptions: CameraPreviewOptions = {
position: 'rear', // front or rear
parent: 'content', // the id on the ion-content
className: '',
width: window.screen.width, //width of the camera display
height: window.screen.height - 200, //height of the camera
toBack: false,
disableAudio: true,
enableHighResolution: true,
};
CameraPreview['start'](cameraPreviewOptions);
this.cameraActive = true;
}
async takePicture() {
const cameraPreviewPictureOptions: CameraPreviewPictureOptions = {
quality: 90
};
const result = await CameraPreview['capture'](cameraPreviewPictureOptions);
this.image = `data:image/jpeg;base64,${result.value}`;
this.stopCamera();
}
async stopCamera() {
await CameraPreview['stop']();
this.modal.dismiss(this.image);
}
async flipCamera() {
await CameraPreview['flip']();
}
}

View File

@@ -1,9 +1,9 @@
<ion-header class="ion-no-border">
<ion-toolbar color="primary">
<ion-toolbar color="black">
<ion-buttons slot="start">
<ion-back-button defaultHref="home" icon="chevron-back-outline"></ion-back-button>
<ion-back-button defaultHref="/" icon="chevron-back-outline"></ion-back-button>
</ion-buttons>
<ion-title class="ion-text-center"> {{ "optionsPage.title" | translate }}</ion-title>
<ion-title> {{ "optionsPage.title" | translate }}</ion-title>
<ion-buttons slot="end">
<ion-menu-button></ion-menu-button>
</ion-buttons>
@@ -19,8 +19,8 @@
</ion-card-header>
<ion-card-content>
<ion-list>
<ion-item color="secondary">{{ "optionsPage.general-settings-label" | translate }}</ion-item>
<ion-list class="ion-no-padding">
<ion-item color="tertiary">{{ "optionsPage.general-settings-label" | translate }}</ion-item>
<ion-item><ion-label class="bold">{{ "optionsPage.language-label" | translate }} </ion-label>
<ion-select value="en" [(ngModel)]="language" (ionChange)="setLanguage()" [placeholder]="'optionsPage.languageSelectPlaceholder' | translate">
<ion-select-option value="en">English</ion-select-option>

View File

@@ -0,0 +1,11 @@
ion-item {
font-size: small;
flex: none;
}
ion-select{
max-width: 70% !important;
width: 60% !important;
padding-left: 20px !important;
text-align: end;
}

View File

@@ -63,13 +63,10 @@ export class AuthService {
async sha256(message) {
// encode as UTF-8
const msgBuffer = new TextEncoder().encode(message);
// hash the message
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
// convert ArrayBuffer to Array
const hashArray = Array.from(new Uint8Array(hashBuffer));
// convert bytes to hex string
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, '0'))
@@ -84,7 +81,7 @@ export class AuthService {
this.platform.ready().then(() => {
GoogleAuth.initialize({
clientId:
'553589883639-2h19rvk5ki52j7h0ptjmmlh5keetm3kj.apps.googleusercontent.com', //Test new project MOBIS AUTH WEB
'1090658128897-uqpp3egk2v7d0errt8crl220e6tltblq.apps.googleusercontent.com',
scopes: ['profile', 'email'],
grantOfflineAccess: false,
});
@@ -103,7 +100,6 @@ export class AuthService {
);
await this.sendVerificationMail();
await this.updateUserName(name);
// this.updateUserName(name);
return loggedInUser;
} catch (e) {
return null;
@@ -128,6 +124,11 @@ export class AuthService {
await updateProfile(this.afAuth.currentUser, { displayName: name });
}
async updatePhotoURL(photoURL: string) {
await updateProfile(this.afAuth.currentUser, { photoURL });
}
async sendPasswordResetEmail(email) {
console.log('resetting password for email', email);
try {
@@ -212,9 +213,7 @@ export class AuthService {
console.log('Apple Native login');
let loggedInUser = null;
const nonce = this.nonceService.generateNonce();
const hashedNonceHex = await this.sha256(nonce); // see next function
const options: SignInWithAppleOptions = {
clientId: 'nl.ddq.blackholefinder',
redirectURI: 'https://www.ddq.nl',
@@ -222,11 +221,9 @@ export class AuthService {
state: '123456',
nonce: hashedNonceHex,
};
const appleUser: SignInWithAppleResponse = await SignInWithApple.authorize(options);
const provider = new OAuthProvider('apple.com');
const credential = provider.credential({idToken: appleUser.response.identityToken, rawNonce: nonce});
await signInWithCredential(this.afAuth, credential).then(
(signedInUser) => {
loggedInUser = signedInUser;
@@ -238,4 +235,8 @@ export class AuthService {
logout() {
return signOut(this.afAuth);
}
deleteAccount(){
return this.afAuth.currentUser.delete();
}
}

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { Auth } from '@angular/fire/auth';
import { doc, docData, Firestore, setDoc } from '@angular/fire/firestore';
import { ref, uploadString, getDownloadURL, Storage } from '@angular/fire/storage';
import { Auth, updateProfile } from '@angular/fire/auth';
import { deleteField, doc, docData, Firestore, setDoc, updateDoc } from '@angular/fire/firestore';
import { ref, uploadString, getDownloadURL, Storage, deleteObject } from '@angular/fire/storage';
import { Photo } from '@capacitor/camera';
@Injectable({
@@ -9,17 +9,33 @@ import { Photo } from '@capacitor/camera';
})
export class AvatarService {
constructor(private auth: Auth, private firestore: Firestore, private storage: Storage) { }
constructor(private afAuth: Auth, private firestore: Firestore, private storage: Storage) { }
getUserProfile() {
const user = this.auth.currentUser;
getAvatar() {
const user = this.afAuth.currentUser;
const userDocRef = doc(this.firestore, `users/${user.uid}`);
const result = docData(userDocRef, { idField: 'id' });
return result;
}
async updatePhotoURL(photoURL: string) {
await updateProfile(this.afAuth.currentUser, { photoURL });
}
removeImage(){
const user = this.afAuth.currentUser;
//delete the reference to the image in Firestore Database
const userDocRef = doc(this.firestore, `users/${user.uid}`);
updateDoc(userDocRef, { imageUrl: deleteField() });
//delete the actual image in Storage
const path = `uploads/${user.uid}/profile.png`;
const storageRef = ref(this.storage, path);
deleteObject(storageRef);
}
async uploadImage(cameraFile: Photo) {
const user = this.auth.currentUser;
const user = this.afAuth.currentUser;
//console.log(user);
const path = `uploads/${user.uid}/profile.png`;
const storageRef = ref(this.storage, path);
@@ -31,8 +47,10 @@ export class AvatarService {
const userDocRef = doc(this.firestore, `users/${user.uid}`);
await setDoc(userDocRef, {
imageUrl,
imageUrl
});
console.log(imageUrl);
await this.updatePhotoURL(imageUrl);
return true;
} catch (e) {
return null;

View File

@@ -0,0 +1,115 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
export interface WeatherData{
temp: number;
description: string;
iconUrl: string;
}
@Injectable({
providedIn: 'root'
})
export class WeatherService {
public blackGemCoords: {lat: number; lng: number} = {lat: -29.257674, lng: -70.737855};
public meerLichtCoords: {lat: number; lng: number} = {lat: -32.379864, lng: 20.811234};
userLat: number;
userLng: number;
constructor(private http: HttpClient) { }
//function that gets the users current location and then calls the showWeather function
showCurrentWeather() {
this.getLocation().then((pos: {lat: number; lng: number}) => {
console.log(`Positon: ${pos.lat} ${pos.lng}`);
this.userLat = pos.lat;
this.userLng = pos.lng;
this.showWeather(pos.lat, pos.lng);
});
}
//function that returns the users current location as a promise from the geolocation API
getLocation() {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
resp => {
resolve({ lat: resp.coords.latitude, lng: resp.coords.longitude });
},
err => {
reject(err);
}
);
});
}
//function that writes all weather info from the result of the openweathermap.org API in a nice pop up window
showWeather(lat: number, lng: number) {
this.http
.get(
'https://api.openweathermap.org/data/2.5/weather?lat=' +
lat +
'&lon=' +
lng +
'&appid=edd7d6eab104383027cd6cc21f32d772&units=metric'
)
.subscribe((res: any) => {
const temp = res.main.temp;
const iconcode = res.weather[0].icon;
console.log(iconcode);
const iconurl = 'http://openweathermap.org/img/w/' + iconcode + '.png';
console.log(temp);
console.log(iconurl);
//display the weather icon, fetches the temperature from the weather.main object
const weatherPopup = window.open(
'',
'Weather',
'width=300,height=300'
);
weatherPopup.document.write(
'<p>Current weather: ' +
res.weather[0].description +
'</p><img src=' +
iconurl +
'><p>Temperature: ' +
temp +
'°C</p>'
);
});
}
//function that returns the weather from openweathermap.org API and returns a single line of text with all weather info as an observable
getWeather(lat: number, lng: number): Promise<WeatherData> {
let description: string;
let temp: number;
let iconUrl: string;
return new Promise<WeatherData>((resolve, reject) => {
try {
this.http
.get(
'https://api.openweathermap.org/data/2.5/weather?lat=' +
lat +
'&lon=' +
lng +
'&appid=edd7d6eab104383027cd6cc21f32d772&units=metric'
).subscribe((res: any) => {
temp = res.main.temp;
const iconcode = res.weather[0].icon;
description = res.weather[0].description;
//console.log(iconcode);
iconUrl = 'http://openweathermap.org/img/w/' + iconcode + '.png';
console.log(temp);
console.log(iconUrl);
console.log(description);
resolve({temp, description, iconUrl});
});
}
catch (error) {
reject(error);
}
});
}
}