Initial commit
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
|
||||
import {redirectUnauthorizedTo, redirectLoggedInTo, canActivate } from '@angular/fire/auth-guard';
|
||||
|
||||
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['login']);
|
||||
const redirectLoggedInToProfile = () => redirectLoggedInTo(['profile']);
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () => import('./home/home.module').then( m => m.HomePageModule),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'home',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
loadChildren: () => import('./auth/login/login.module').then( m => m.LoginPageModule),
|
||||
...canActivate(redirectLoggedInToProfile)
|
||||
},
|
||||
{
|
||||
path: 'resetpw',
|
||||
loadChildren: () => import('./auth/resetpw/resetpw.module').then( m => m.ResetpwPageModule)
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
loadChildren: () => import('./auth/profile/profile.module').then( m => m.ProfilePageModule),
|
||||
...canActivate(redirectUnauthorizedToLogin)
|
||||
},
|
||||
{
|
||||
path: 'signup',
|
||||
loadChildren: () => import('./auth/signup/signup.module').then( m => m.SignupPageModule)
|
||||
},
|
||||
{
|
||||
path: 'observe',
|
||||
loadChildren: () => import('./observe/observe.module').then( m => m.MeasurePageModule)
|
||||
},
|
||||
{
|
||||
path: 'options',
|
||||
loadChildren: () => import('./options/options.module').then( m => m.OptionsPageModule)
|
||||
},
|
||||
{
|
||||
path: 'plantnet',
|
||||
loadChildren: () => import('./mobisplugins/plantnet/plantnet.module').then( m => m.PlantnetPageModule)
|
||||
},
|
||||
{
|
||||
path: 'ispex',
|
||||
loadChildren: () => import('./mobisplugins/ispex/ispex.module').then( m => m.IspexPageModule)
|
||||
},
|
||||
{
|
||||
path: 'minisecchi',
|
||||
loadChildren: () => import('./mobisplugins/minisecchi/minisecchi.module').then( m => m.MinisecchiPageModule)
|
||||
},
|
||||
{
|
||||
path: 'canairiopm25',
|
||||
loadChildren: () => import('./mobisplugins/canairiopm25/canairiopm25.module').then( m => m.Canairiopm25PageModule)
|
||||
},
|
||||
|
||||
{
|
||||
path: 'canairioco2',
|
||||
loadChildren: () => import('./mobisplugins/canairioco2/canairioco2.module').then( m => m.Canairioco2PageModule)
|
||||
},
|
||||
|
||||
{
|
||||
path: 'ms_measure',
|
||||
loadChildren: () => import('./mobisplugins/minisecchi/measure/measure.module').then( m => m.MeasurePageModule)
|
||||
},
|
||||
{
|
||||
path: 'ms_instructions',
|
||||
loadChildren: () => import('./mobisplugins/minisecchi/instructions/instructions.module').then( m => m.InstructionsPageModule)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
|
||||
],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
@@ -0,0 +1,60 @@
|
||||
<ion-app>
|
||||
<ion-menu side="start" 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-buttons>
|
||||
<ion-button type="icon-only" (click)="closeMenu()"><ion-icon name="close-outline" color="dark"></ion-icon></ion-button>
|
||||
</ion-buttons>
|
||||
</ion-item>
|
||||
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
</ion-menu-toggle>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-list-header>
|
||||
<ion-label>{{ "menu.account" | translate }}</ion-label>
|
||||
</ion-list-header>
|
||||
<ion-menu-toggle>
|
||||
<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-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-icon name="person-circle-outline"></ion-icon>
|
||||
<ion-label>{{ "menu.profile" | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-menu-toggle>
|
||||
<ion-menu-toggle>
|
||||
<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-menu-toggle>
|
||||
<ion-list-header>
|
||||
<ion-label>{{ "menu.settings" | translate }}</ion-label>
|
||||
</ion-list-header>
|
||||
<ion-menu-toggle>
|
||||
<ion-item button routerLink="options">
|
||||
<ion-icon name="settings-outline"></ion-icon>
|
||||
<ion-label>{{ "menu.options" | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-menu-toggle>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-menu>
|
||||
<ion-router-outlet id="main"></ion-router-outlet>
|
||||
</ion-app>
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
// TODO: add more tests!
|
||||
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Auth, user } from '@angular/fire/auth';
|
||||
import { Router } from '@angular/router';
|
||||
import { MenuController } from '@ionic/angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { Storage } from '@ionic/storage-angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.scss'],
|
||||
})
|
||||
export class AppComponent implements OnInit{
|
||||
user = null;
|
||||
language= '';
|
||||
|
||||
constructor(
|
||||
private menuCtrl: MenuController,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private afAuth: Auth,
|
||||
private translateService: TranslateService,
|
||||
private storage: Storage
|
||||
) {
|
||||
user(this.afAuth).subscribe((response) => {
|
||||
//fill the user to verify if someone is logged in
|
||||
this.user = response;
|
||||
});
|
||||
}
|
||||
async ngOnInit() {
|
||||
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();
|
||||
//if
|
||||
}
|
||||
this.translateService.use(this.language);
|
||||
}
|
||||
|
||||
|
||||
closeMenu() {
|
||||
this.menuCtrl.close();
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.authService.logout();
|
||||
this.router.navigateByUrl('/home', { replaceUrl: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
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 { 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';
|
||||
import { IonicStorageModule } from '@ionic/storage-angular';
|
||||
|
||||
//define loader to load local files(on given path) with HttpClient
|
||||
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
||||
export function createTranslateLoader(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './assets/languages/', '.json');
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
IonicModule.forRoot(),
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
IonicStorageModule.forRoot(),
|
||||
TranslateModule.forRoot({loader: {provide: TranslateLoader, useFactory: (createTranslateLoader), deps: [HttpClient]}}),
|
||||
provideFirebaseApp(() => initializeApp(environment.firebase)),
|
||||
provideAuth(() => getAuth()),
|
||||
provideFirestore(() => getFirestore()),
|
||||
provideStorage(() => getStorage()),
|
||||
],
|
||||
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { LoginPage } from './login.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: LoginPage
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class LoginPageRoutingModule {}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { LoginPageRoutingModule } from './login-routing.module';
|
||||
|
||||
import { LoginPage } from './login.page';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
ReactiveFormsModule,
|
||||
LoginPageRoutingModule,
|
||||
TranslateModule
|
||||
],
|
||||
declarations: [LoginPage]
|
||||
})
|
||||
export class LoginPageModule {}
|
||||
@@ -0,0 +1,52 @@
|
||||
<ion-header class="ion-no-border">
|
||||
<ion-toolbar color="primary">
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="home" icon="chevron-back-outline"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title class="ion-text-center">{{ "loginPage.title" | translate}}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-menu-button></ion-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-card>
|
||||
<ion-card-content>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col sizeSm="6" offsetSm="3" sizeLg="4" offsetLg="4" sizeMd="6" offsetMd="3" *ngIf="showForm" class="ion-text-center">
|
||||
<form (ngSubmit)="login()" [formGroup]="credentials">
|
||||
<ion-item fill="solid" class="ion-margin-bottom">
|
||||
<ion-input type="email" [placeholder]="'loginPage.email-placeholder' | translate" formControlName="email"><ion-icon name="mail-outline"></ion-icon></ion-input>
|
||||
<ion-icon name="person-outline" slot=“end” align-self-center></ion-icon>
|
||||
<ion-note slot="error" *ngIf="(email.dirty || email.touched) && email.errors">{{ "loginPage.email-invalid" | translate }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-item fill="solid" class="ion-margin-bottom">
|
||||
<ion-input type="password" [placeholder]="'loginPage.password-placeholder' | translate" formControlName="password"><ion-icon name="key-outline"></ion-icon></ion-input>
|
||||
<ion-note slot="error" *ngIf="(password.dirty || password.touched) && password.errors">{{ "loginPage.password-invalid" | translate }}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-button type="submit" expand="block" [disabled]="!credentials.valid">{{ "loginPage.login-button" | translate }}</ion-button>
|
||||
<a [routerLink]="['/resetpw']">{{ "loginPage.forgot-password" | translate }}</a>
|
||||
</form>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center" sizeSm="6" offsetSm="3" sizeLg="4" offsetLg="4" sizeMd="6" offsetMd="3">
|
||||
<ion-button type="button" (click)="toggleForm()" expand="block"><ion-icon name="person-outline"></ion-icon>{{ "loginPage.login-username" | translate }}</ion-button>
|
||||
<ion-button type="button" (click)="anonymousLogin()" expand="block"><ion-icon name="eye-outline"></ion-icon>{{ "loginPage.login-anonymous" | translate }}</ion-button>
|
||||
<ion-button type="button" (click)="loginWithGoogle()" expand="block" color="danger"><ion-icon name="logo-google"></ion-icon>{{ "loginPage.login-google" | translate }}</ion-button>
|
||||
<ion-button type="button" (click)="loginWithApple()" expand="block" color="dark"><ion-icon name="logo-apple"></ion-icon>{{ "loginPage.login-apple" | translate }}</ion-button>
|
||||
<a href="signup">{{ "loginPage.goto-signup" | translate }}</a>
|
||||
|
||||
<br>
|
||||
|
||||
</ion-col>
|
||||
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,11 @@
|
||||
ion-input ion-icon{
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
border: 1px solid var(--ion-color-grey);
|
||||
border-radius: 16px;
|
||||
--color-hover: var(--ion-color-primary)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { LoginPage } from './login.page';
|
||||
|
||||
describe('LoginPage', () => {
|
||||
let component: LoginPage;
|
||||
let fixture: ComponentFixture<LoginPage>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LoginPage ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LoginPage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Auth, User } from '@angular/fire/auth';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { AlertController, LoadingController } from '@ionic/angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.page.html',
|
||||
styleUrls: ['./login.page.scss'],
|
||||
})
|
||||
export class LoginPage implements OnInit {
|
||||
credentials: FormGroup;
|
||||
user: User | null = null;
|
||||
showForm = false;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private loadingController: LoadingController,
|
||||
private alertController: AlertController,
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
private afAuth: Auth,
|
||||
private translateService: TranslateService
|
||||
) {
|
||||
this.afAuth.onAuthStateChanged((userState) => {
|
||||
this.user = userState;
|
||||
});
|
||||
}
|
||||
|
||||
// Easy access for form fields
|
||||
get email() {
|
||||
return this.credentials.get('email');
|
||||
}
|
||||
|
||||
get password() {
|
||||
return this.credentials.get('password');
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.credentials = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required, Validators.minLength(6)]],
|
||||
});
|
||||
}
|
||||
|
||||
async login() {
|
||||
const loading = await this.loadingController.create({
|
||||
backdropDismiss: true,
|
||||
message: this.translateService.instant('loginPage.logging-in-with-email'),
|
||||
duration: 5000,
|
||||
});
|
||||
await loading.present();
|
||||
|
||||
const user = await this.authService.login(this.credentials.value);
|
||||
await loading.dismiss();
|
||||
|
||||
if (user) {
|
||||
this.router.navigateByUrl('/profile', { replaceUrl: true });
|
||||
} else {
|
||||
this.showAlert(this.translateService.instant('loginPage.login-failed'), this.translateService.instant('loginPage.please-try-again'));
|
||||
}
|
||||
}
|
||||
|
||||
async showAlert(header, message) {
|
||||
const alert = await this.alertController.create({
|
||||
header,
|
||||
message,
|
||||
buttons: ['OK'],
|
||||
});
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
async loginWithGoogle() {
|
||||
const loading = await this.loadingController.create({
|
||||
backdropDismiss: true,
|
||||
message: this.translateService.instant('loginPage.logging-in-with-google'),
|
||||
duration: 5000,
|
||||
});
|
||||
await loading.present();
|
||||
this.authService
|
||||
.loginWithGoogle()
|
||||
.then(() => this.router.navigate(['/profile']))
|
||||
.catch((e) => console.log(e.message));
|
||||
await loading.dismiss();
|
||||
}
|
||||
|
||||
async loginWithApple() {
|
||||
const loading = await this.loadingController.create({
|
||||
backdropDismiss: true,
|
||||
message: this.translateService.instant('loginPage.logging-in-with-apple'),
|
||||
duration: 5000,
|
||||
});
|
||||
await loading.present();
|
||||
this.authService
|
||||
.loginWithApple()
|
||||
.then(() => this.router.navigate(['/profile']))
|
||||
.catch((e) => console.log(e.message));
|
||||
await loading.dismiss();
|
||||
}
|
||||
|
||||
async anonymousLogin() {
|
||||
const loading = await this.loadingController.create({
|
||||
backdropDismiss: true,
|
||||
message: this.translateService.instant('loginPage.logging-in-anonymously'),
|
||||
duration: 5000,
|
||||
});
|
||||
await loading.present();
|
||||
this.authService
|
||||
.anonymousLogin()
|
||||
.then(() => this.router.navigate(['/profile']))
|
||||
.catch((e) => console.log(e.message));
|
||||
await loading.dismiss();
|
||||
}
|
||||
|
||||
toggleForm() {
|
||||
this.showForm = !this.showForm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { ProfilePage } from './profile.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProfilePage
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ProfilePageRoutingModule {}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { ProfilePageRoutingModule } from './profile-routing.module';
|
||||
|
||||
import { ProfilePage } from './profile.page';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
ProfilePageRoutingModule,
|
||||
TranslateModule
|
||||
],
|
||||
declarations: [ProfilePage]
|
||||
})
|
||||
export class ProfilePageModule {}
|
||||
@@ -0,0 +1,110 @@
|
||||
<ion-header class="ion-no-border">
|
||||
<ion-toolbar color="primary">
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="home" icon="chevron-back-outline"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title class="ion-text-center">{{ "profilePage.title" | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-menu-button></ion-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content *ngIf="user !== null">
|
||||
<ion-card *ngIf="!isAnonymous && user !== null">
|
||||
<ion-card-content>
|
||||
|
||||
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<div class="preview">
|
||||
<ion-avatar (click)="changeImage()">
|
||||
<img
|
||||
*ngIf="photoURL; else placeholder_avatar;"
|
||||
[src]="photoURL"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<ng-template #placeholder_avatar>
|
||||
<div class="fallback">
|
||||
<p>{{ "profilePage.select avatar" | translate }}</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ion-avatar>
|
||||
</div>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
<ion-grid *ngIf="!isAnonymous && user !== null">
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<span class="text"><ion-label class="title">{{ "profilePage.name" | translate }}</ion-label></span>
|
||||
<span title="Click to change" *ngIf="!editDisplayName" (click)="editDisplayNameField()">
|
||||
<ion-icon class="icon-edit" name="create-outline" ></ion-icon>
|
||||
</span>
|
||||
<span title="Save" *ngIf="editDisplayName" (click)="updateUserName(displayName)">
|
||||
<ion-icon class="icon-edit" name="save-outline"></ion-icon>
|
||||
</span>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row>
|
||||
<ion-col><ion-input class="ion-no-padding" [(ngModel)]="displayName" [readonly]="!editDisplayName" [placeholder]="displayName"></ion-input>
|
||||
</ion-col></ion-row>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<span class="text"><ion-label class="title">Email</ion-label></span>
|
||||
<span title="Verified"
|
||||
><ion-icon
|
||||
class="icon-verified"
|
||||
name="checkmark-circle-outline"
|
||||
*ngIf="emailVerified"
|
||||
></ion-icon
|
||||
></span>
|
||||
<span title="Unverified"
|
||||
><ion-icon
|
||||
class="icon-not-verified"
|
||||
name="close-circle-outline"
|
||||
*ngIf="!emailVerified"
|
||||
></ion-icon
|
||||
></span>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<ion-label class="text">{{email}}</ion-label>
|
||||
</ion-col>
|
||||
<ion-button
|
||||
size="small"
|
||||
(click)="verifyEmail()"
|
||||
*ngIf="(!emailVerified) && (email !== null)"
|
||||
>{{ "profilePage.verify email" | translate }}</ion-button>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
<ion-grid *ngIf="!emailVerified && !isAnonymous && user !== null">
|
||||
<ion-row >
|
||||
<ion-col><b>
|
||||
{{ "profilePage.unverified email" | translate }}</b>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
{{ "profilePage.unverified email text" | translate }}
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
<ion-card *ngIf="isAnonymous">
|
||||
<ion-card-header>{{ "profilePage.anonymous login" | translate }}
|
||||
</ion-card-header>
|
||||
<ion-card-content
|
||||
>{{ "profilePage.anonymous login text" | translate }}
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</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>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,116 @@
|
||||
#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{
|
||||
font-weight: bold;
|
||||
//border: 1px solid black;
|
||||
}
|
||||
|
||||
ion-input{
|
||||
--margin: 0px;
|
||||
--padding: 0px;
|
||||
//border: 1px solid black;
|
||||
}
|
||||
|
||||
ion-col{
|
||||
text-align: start;
|
||||
vertical-align: middle;
|
||||
//border: 1px solid black;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
|
||||
ion-col ion-button{
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
pointer-events: none;
|
||||
//border: 1px solid black;
|
||||
}
|
||||
|
||||
ion-card{
|
||||
padding: 0px;
|
||||
margin: 2px;
|
||||
|
||||
ion-card-header{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ion-card-content{
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-edit{
|
||||
display: inline-block;
|
||||
font-size: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { ProfilePage } from './profile.page';
|
||||
|
||||
describe('ProfilePage', () => {
|
||||
let component: ProfilePage;
|
||||
let fixture: ComponentFixture<ProfilePage>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ProfilePage ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ProfilePage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AlertController, LoadingController } from '@ionic/angular';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile',
|
||||
templateUrl: 'profile.page.html',
|
||||
styleUrls: ['profile.page.scss'],
|
||||
})
|
||||
export class ProfilePage implements OnInit {
|
||||
profile = null;
|
||||
user = null;
|
||||
photoURL: string = null;
|
||||
displayName: string = null;
|
||||
email: string = null;
|
||||
emailVerified = false;
|
||||
isAnonymous: boolean = null;
|
||||
|
||||
editDisplayName = false;
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private avatarService: AvatarService,
|
||||
private loadingController: LoadingController,
|
||||
private alertController: AlertController,
|
||||
private afAuth: Auth,
|
||||
private translateService: TranslateService
|
||||
) {
|
||||
// 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);
|
||||
this.displayName = response.displayName;
|
||||
this.email = response.email;
|
||||
this.photoURL = response.photoURL;
|
||||
this.emailVerified = response.emailVerified;
|
||||
this.isAnonymous = response.isAnonymous;
|
||||
} else {
|
||||
this.displayName = null;
|
||||
this.email = null;
|
||||
this.photoURL = null;
|
||||
this.emailVerified = false;
|
||||
this.isAnonymous = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
ngOnInit(): void {}
|
||||
|
||||
editDisplayNameField() {
|
||||
this.editDisplayName = true;
|
||||
}
|
||||
|
||||
updateUserName(name: string) {
|
||||
this.authService.updateUserName(name);
|
||||
this.editDisplayName = false;
|
||||
}
|
||||
|
||||
verifyEmail() {
|
||||
this.authService.sendVerificationMail();
|
||||
this.showAlert(
|
||||
this.translateService.instant('profilePage.verification-mail-sent'),
|
||||
this.translateService.instant('profilePage.check-also-spam')
|
||||
);
|
||||
}
|
||||
|
||||
async showAlert(header, message) {
|
||||
const alert = await this.alertController.create({
|
||||
header,
|
||||
message,
|
||||
buttons: ['OK'],
|
||||
});
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.authService.logout();
|
||||
this.router.navigateByUrl('/home', { replaceUrl: true });
|
||||
}
|
||||
|
||||
async changeImage() {
|
||||
const image = await Camera.getPhoto({
|
||||
quality: 90,
|
||||
allowEditing: false,
|
||||
resultType: CameraResultType.Base64,
|
||||
source: CameraSource.Photos, // Camera, Photos or Prompt!
|
||||
});
|
||||
|
||||
if (image) {
|
||||
const loading = await this.loadingController.create({
|
||||
message: this.translateService.instant('profilePage.processing-image'),
|
||||
});
|
||||
await loading.present();
|
||||
const result = await this.avatarService.uploadImage(image);
|
||||
window.location.reload(); //ugly to force the image to reload after upload
|
||||
loading.dismiss();
|
||||
|
||||
if (!result) {
|
||||
const alert = await this.alertController.create({
|
||||
header: this.translateService.instant('profilePage.upload-failed'),
|
||||
message: this.translateService.instant('profilePage.problem-uploading-avatar'),
|
||||
buttons: ['OK'],
|
||||
});
|
||||
await alert.present();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { ResetpwPage } from './resetpw.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ResetpwPage
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ResetpwPageRoutingModule {}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule,ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { ResetpwPageRoutingModule } from './resetpw-routing.module';
|
||||
|
||||
import { ResetpwPage } from './resetpw.page';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
ResetpwPageRoutingModule, ReactiveFormsModule, TranslateModule
|
||||
],
|
||||
declarations: [ResetpwPage]
|
||||
})
|
||||
export class ResetpwPageModule {}
|
||||
@@ -0,0 +1,26 @@
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-title>{{ "resetpwPage.title" | translate }}</ion-title>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="/"></ion-back-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col sizeSm="10" offsetSm="1" sizeLg="6" offsetLg="3" sizeMd="8" offsetMd="2">
|
||||
<form (ngSubmit)="resetPw()" [formGroup]="frmPasswordReset">
|
||||
<ion-item fill="solid" class="ion-margin-bottom">
|
||||
<ion-input type="email" [placeholder]="'resetpwPage.email-placeholder' | translate" formControlName="email"></ion-input>
|
||||
<ion-note slot="error" *ngIf="(email.dirty || email.touched) && email.errors">{{ "resetpwPage.email-invalid" | translate }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-button type="submit" expand="block" [disabled]="!frmPasswordReset.valid">{{ "resetpwPage.send-email-button" | translate }}</ion-button>
|
||||
</form>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { ResetpwPage } from './resetpw.page';
|
||||
|
||||
describe('ResetpwPage', () => {
|
||||
let component: ResetpwPage;
|
||||
let fixture: ComponentFixture<ResetpwPage>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ResetpwPage ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ResetpwPage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { ToastController } from '@ionic/angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-resetpw',
|
||||
templateUrl: './resetpw.page.html',
|
||||
styleUrls: ['./resetpw.page.scss'],
|
||||
})
|
||||
export class ResetpwPage implements OnInit {
|
||||
frmPasswordReset: FormGroup;
|
||||
password = '';
|
||||
error = '';
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private toastController: ToastController,
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
private translateService: TranslateService
|
||||
) {}
|
||||
|
||||
get email() {
|
||||
return this.frmPasswordReset.get('email');
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.frmPasswordReset = this.fb.group({
|
||||
email: [null, [Validators.required, Validators.email]],
|
||||
});
|
||||
}
|
||||
|
||||
resetPw() {
|
||||
this.authService
|
||||
.sendPasswordResetEmail(this.email.value)
|
||||
.then((data) => {
|
||||
this.presentToast(this.translateService.instant('resetpwPage.password-reset-email-sent'), 'bottom', 2000);
|
||||
this.router.navigateByUrl('/');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(` failed ${err}`);
|
||||
this.error = err.message;
|
||||
});
|
||||
}
|
||||
|
||||
async presentToast(message: string, position, duration: number) {
|
||||
const toast = await this.toastController.create({
|
||||
message,
|
||||
duration,
|
||||
position,
|
||||
});
|
||||
toast.present();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { SignupPage } from './signup.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: SignupPage
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class SignupPageRoutingModule {}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { SignupPageRoutingModule } from './signup-routing.module';
|
||||
|
||||
import { SignupPage } from './signup.page';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
IonicModule,
|
||||
SignupPageRoutingModule,
|
||||
TranslateModule
|
||||
],
|
||||
declarations: [SignupPage]
|
||||
})
|
||||
export class SignupPageModule {}
|
||||
@@ -0,0 +1,48 @@
|
||||
<ion-header class="ion-no-border">
|
||||
<ion-toolbar color="primary">
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="home" icon="chevron-back-outline"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title class="ion-text-center">{{ "signupPage.title" | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-menu-button></ion-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-card>
|
||||
<ion-card-content>
|
||||
|
||||
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col sizeSm="4" sizeLg="4" sizeMd="6">
|
||||
<form (ngSubmit)="register()" [formGroup]="credentials">
|
||||
<ion-item fill="solid" class="ion-margin-bottom">
|
||||
<ion-input type="name" [placeholder]="'signupPage.name-placeholder' | translate" formControlName="name"><ion-icon name="person-outline"></ion-icon></ion-input>
|
||||
</ion-item>
|
||||
<ion-item fill="solid" class="ion-margin-bottom">
|
||||
<ion-input type="email" [placeholder]="'signupPage.email-placeholder' | translate" formControlName="email"><ion-icon name="mail-outline"></ion-icon></ion-input>
|
||||
<ion-note slot="error" *ngIf="(email.dirty || email.touched) && email.errors">{{ "signupPage.email-invalid" | translate }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-item fill="solid" class="ion-margin-bottom">
|
||||
<ion-input type="password" [placeholder]="'signupPage.password-placeholder' | translate" formControlName="password"><ion-icon name="key-outline"></ion-icon></ion-input>
|
||||
<ion-note slot="error" *ngIf="(password.dirty || password.touched) && password.errors">{{ "signupPage.password-invalid" | translate }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-button type="submit" expand="block" [disabled]="!credentials.valid">{{ "signupPage.create-account-button" | translate }}</ion-button>
|
||||
</form>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row>
|
||||
<ion-col sizeSm="10" offsetSm="1" sizeLg="6" offsetLg="3" sizeMd="8" offsetMd="2" class="ion-text-center">
|
||||
<a [routerLink]="['/login']">{{ "signupPage.have-account" | translate }}</a>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
|
||||
</ion-grid>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,11 @@
|
||||
ion-item{
|
||||
--border-radius: 10px;
|
||||
}
|
||||
|
||||
ion-button{
|
||||
--border-radius: 10px;
|
||||
}
|
||||
|
||||
ion-input ion-icon{
|
||||
padding-right: 15px;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { SignupPage } from './signup.page';
|
||||
|
||||
describe('SignupPage', () => {
|
||||
let component: SignupPage;
|
||||
let fixture: ComponentFixture<SignupPage>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SignupPage ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SignupPage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Auth, User } from '@angular/fire/auth';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { AlertController, LoadingController } from '@ionic/angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-signup',
|
||||
templateUrl: './signup.page.html',
|
||||
styleUrls: ['./signup.page.scss'],
|
||||
})
|
||||
export class SignupPage implements OnInit {
|
||||
credentials: FormGroup;
|
||||
user: User | null = null;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private loadingController: LoadingController,
|
||||
private alertController: AlertController,
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
private afAuth: Auth,
|
||||
private translateService: TranslateService
|
||||
) {
|
||||
this.afAuth.onAuthStateChanged((userState) => {
|
||||
this.user = userState;
|
||||
});
|
||||
}
|
||||
|
||||
// Easy access for form fields
|
||||
get email() {
|
||||
return this.credentials.get('email');
|
||||
}
|
||||
|
||||
get password() {
|
||||
return this.credentials.get('password');
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.credentials.get('name');
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.credentials = this.fb.group({
|
||||
name: ['', Validators.minLength(3)],
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required, Validators.minLength(6)]],
|
||||
});
|
||||
}
|
||||
|
||||
async register() {
|
||||
const loading = await this.loadingController.create({
|
||||
backdropDismiss: true,
|
||||
message: this.translateService.instant('signupPage.creating-account'),
|
||||
duration: 5000,
|
||||
});
|
||||
await loading.present();
|
||||
console.log('received following values at registration', this.credentials);
|
||||
const user = await this.authService.register(this.credentials.value);
|
||||
await loading.dismiss();
|
||||
|
||||
if (user) {
|
||||
this.router.navigateByUrl('/profile', { replaceUrl: true });
|
||||
} else {
|
||||
this.showAlert(
|
||||
this.translateService.instant('signupPage.registration-failed'),
|
||||
this.translateService.instant('signupPage.please-try-again')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async showAlert(header, message) {
|
||||
const alert = await this.alertController.create({
|
||||
header,
|
||||
message,
|
||||
buttons: ['OK'],
|
||||
});
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
loginWithGoogle() {
|
||||
this.authService
|
||||
.loginWithGoogle()
|
||||
.then(() => this.router.navigate(['/profile']))
|
||||
.catch((e) => console.log(e.message));
|
||||
}
|
||||
|
||||
loginWithApple() {
|
||||
this.authService
|
||||
.loginWithApple()
|
||||
.then(() => this.router.navigate(['/profile']))
|
||||
.catch((e) => console.log(e.message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { HomePage } from './home.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomePage,
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class HomePageRoutingModule {}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HomePage } from './home.page';
|
||||
|
||||
import { HomePageRoutingModule } from './home-routing.module';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
HomePageRoutingModule,
|
||||
TranslateModule
|
||||
],
|
||||
declarations: [HomePage]
|
||||
})
|
||||
export class HomePageModule {}
|
||||
@@ -0,0 +1,56 @@
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-title>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-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-content>
|
||||
@@ -0,0 +1,128 @@
|
||||
#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{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { HomePage } from './home.page';
|
||||
|
||||
describe('HomePage', () => {
|
||||
let component: HomePage;
|
||||
let fixture: ComponentFixture<HomePage>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ HomePage ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HomePage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MenuController } from '@ionic/angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Geolocation, PositionOptions } from '@capacitor/geolocation';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: 'home.page.html',
|
||||
styleUrls: ['home.page.scss'],
|
||||
})
|
||||
export class HomePage implements OnInit{
|
||||
|
||||
constructor(
|
||||
private menuController: MenuController, private translateService: TranslateService
|
||||
) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
async ngOnInit(): Promise<void> {
|
||||
|
||||
|
||||
const options: PositionOptions = {
|
||||
maximumAge: 10000,
|
||||
enableHighAccuracy: true
|
||||
};
|
||||
|
||||
const location = await Geolocation.getCurrentPosition(options);
|
||||
|
||||
console.log('Current location: ', location);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
openMenu(){
|
||||
this.menuController.open();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { Canairioco2Page } from './canairioco2.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Canairioco2Page
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class Canairioco2PageRoutingModule {}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { Canairioco2PageRoutingModule } from './canairioco2-routing.module';
|
||||
import { BackgroundMode } from '@awesome-cordova-plugins/background-mode/ngx';
|
||||
import { Canairioco2Page } from './canairioco2.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
Canairioco2PageRoutingModule
|
||||
],
|
||||
declarations: [Canairioco2Page],
|
||||
providers: [BackgroundMode]
|
||||
})
|
||||
export class Canairioco2PageModule {}
|
||||
@@ -0,0 +1,82 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>New Canair.io CO2 observation</ion-title>
|
||||
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button></ion-back-button>
|
||||
</ion-buttons>
|
||||
|
||||
</ion-toolbar>
|
||||
|
||||
</ion-header>
|
||||
|
||||
<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-label primary>Temperature: {{TEMP}} C</ion-label>
|
||||
|
||||
</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-content>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { Canairioco2Page } from './canairioco2.page';
|
||||
|
||||
describe('Canairioco2Page', () => {
|
||||
let component: Canairioco2Page;
|
||||
let fixture: ComponentFixture<Canairioco2Page>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ Canairioco2Page ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Canairioco2Page);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
|
||||
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 { Storage } from '@ionic/storage-angular';
|
||||
import * as Parse from 'parse';
|
||||
import { BackgroundMode } from '@awesome-cordova-plugins/background-mode/ngx';
|
||||
|
||||
// Setup Bluetooth LE
|
||||
// Import the wrapper class directly
|
||||
|
||||
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_CHARACTERISTIC = 'B0F332A8-A5AA-4F3F-BB43-F99E7791AE01';
|
||||
/* other services "B0F332A8-A5AA-4F3F-BB43-F99E7791AE02",
|
||||
"B0F332A8-A5AA-4F3F-BB43-F99E7791AE03"
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'app-canairio',
|
||||
templateUrl: './canairioco2.page.html',
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
// i am not sure if this is needed, because CO2 is not moving
|
||||
|
||||
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.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.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
|
||||
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { Canairiopm25Page } from './canairiopm25.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Canairiopm25Page
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class Canairiopm25PageRoutingModule {}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BackgroundMode } from '@awesome-cordova-plugins/background-mode/ngx';
|
||||
|
||||
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { Canairiopm25PageRoutingModule } from './canairiopm25-routing.module';
|
||||
|
||||
import { Canairiopm25Page } from './canairiopm25.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
Canairiopm25PageRoutingModule
|
||||
],
|
||||
declarations: [Canairiopm25Page],
|
||||
providers: [BackgroundMode]
|
||||
})
|
||||
export class Canairiopm25PageModule {}
|
||||
@@ -0,0 +1,85 @@
|
||||
<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-toolbar>
|
||||
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<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>PM 2.5: {{PM25}} ({{PM25Interpret}}) </ion-label>
|
||||
|
||||
</ion-item>
|
||||
|
||||
|
||||
|
||||
<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-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-content>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { Canairiopm25Page } from './canairiopm25.page';
|
||||
|
||||
describe('Canairiopm25Page', () => {
|
||||
let component: Canairiopm25Page;
|
||||
let fixture: ComponentFixture<Canairiopm25Page>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ Canairiopm25Page ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Canairiopm25Page);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,251 @@
|
||||
|
||||
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 { Storage } from '@ionic/storage-angular';
|
||||
import * as Parse from 'parse';
|
||||
import { BackgroundMode } from '@awesome-cordova-plugins/background-mode/ngx';
|
||||
|
||||
|
||||
// Import the wrapper class directly
|
||||
|
||||
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_CHARACTERISTIC = 'B0F332A8-A5AA-4F3F-BB43-F99E7791AE01';
|
||||
/* other services "B0F332A8-A5AA-4F3F-BB43-F99E7791AE02",
|
||||
"B0F332A8-A5AA-4F3F-BB43-F99E7791AE03"
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'app-canairio',
|
||||
templateUrl: './canairiopm25.page.html',
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
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.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.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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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]
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { IspexPage } from './ispex.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: IspexPage
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class IspexPageRoutingModule {}
|
||||
@@ -0,0 +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 { IspexPage } from './ispex.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
IspexPageRoutingModule
|
||||
],
|
||||
declarations: [IspexPage],
|
||||
providers: [Vibration]
|
||||
})
|
||||
export class IspexPageModule {}
|
||||
@@ -0,0 +1,64 @@
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-title>New iSPEX observation</ion-title>
|
||||
|
||||
<ion-buttons slot="start">
|
||||
<ion-menu-button slot="start"></ion-menu-button>
|
||||
<ion-back-button></ion-back-button>
|
||||
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
|
||||
<ion-content [fullscreen]="true">
|
||||
<div *ngIf="image">
|
||||
<img #image id="img" [src]="image" alt="" srcset="">
|
||||
</div>
|
||||
|
||||
|
||||
<ion-button (click)="openCamera()" color="primary" expand="block" fill="solid" size="default">
|
||||
Attach the iSPEX unit and open camera
|
||||
</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>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { IspexPage } from './ispex.page';
|
||||
|
||||
describe('IspexPage', () => {
|
||||
let component: IspexPage;
|
||||
let fixture: ComponentFixture<IspexPage>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ IspexPage ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(IspexPage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Component, OnInit, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
|
||||
import { ModalController } from '@ionic/angular';
|
||||
import { PreviewPage } from './preview/preview.page';
|
||||
|
||||
declare var cv: any;
|
||||
|
||||
@Component({
|
||||
selector: 'app-ispex',
|
||||
templateUrl: './ispex.page.html',
|
||||
styleUrls: ['./ispex.page.scss'],
|
||||
})
|
||||
|
||||
|
||||
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");
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
async openCamera() {
|
||||
const modal = await this.modal.create({
|
||||
component: PreviewPage,
|
||||
cssClass: 'fullscreen',
|
||||
animated: true
|
||||
});
|
||||
|
||||
modal.onDidDismiss().then((data) => {
|
||||
if (data !== null) {
|
||||
this.image = data.data;
|
||||
|
||||
|
||||
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();
|
||||
|
||||
|
||||
cv.imshow('canvasOutput', dst);
|
||||
src.delete(); dst.delete();
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
return await modal.present();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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 {}
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
|
||||
|
||||
<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-button>
|
||||
|
||||
<ion-button (click)="takePicture(4)" expand="block" id="free">
|
||||
Free
|
||||
</ion-button>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,65 @@
|
||||
|
||||
#close {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: calc(50% - 160px);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
#rgb {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: calc(50% - 140px);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#grey {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: calc(50% - 80px);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#sky {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: calc(50% - 20px);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
|
||||
#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;}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,259 @@
|
||||
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 { ModalController } from '@ionic/angular';
|
||||
import { Geolocation} from '@capacitor/geolocation';
|
||||
|
||||
import { Device } from '@capacitor/device';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-preview',
|
||||
templateUrl: './preview.page.html',
|
||||
styleUrls: ['./preview.page.scss'],
|
||||
})
|
||||
export class PreviewPage implements OnInit {
|
||||
image = null;
|
||||
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
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
|
||||
this.getLocation();
|
||||
this.parseInitialize();
|
||||
this.launchCamera();
|
||||
|
||||
this.getDeviceInfo();
|
||||
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
rotateWhenOrientationChanged: false,
|
||||
lockAndroidOrientation: true
|
||||
|
||||
|
||||
};
|
||||
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
|
||||
const cameraPreviewPictureOptions: CameraPreviewPictureOptions = {
|
||||
quality: dngType// misuse quality: 0 normal (rgb) image 1 gray card 2 sky image 3 water image 4 other spectro(pol)
|
||||
};
|
||||
|
||||
|
||||
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();
|
||||
this.modal.dismiss(this.image);
|
||||
}
|
||||
|
||||
async flipCamera() {
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { InstructionsPage } from './instructions.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: InstructionsPage
|
||||
},
|
||||
{
|
||||
path: 'step2',
|
||||
loadChildren: () => import('./step2/step2.module').then( m => m.Step2PageModule)
|
||||
},
|
||||
{
|
||||
path: 'step3',
|
||||
loadChildren: () => import('./step3/step3.module').then( m => m.Step3PageModule)
|
||||
},
|
||||
{
|
||||
path: 'step4',
|
||||
loadChildren: () => import('./step4/step4.module').then( m => m.Step4PageModule)
|
||||
},
|
||||
{
|
||||
path: 'step5',
|
||||
loadChildren: () => import('./step5/step5.module').then( m => m.Step5PageModule)
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class InstructionsPageRoutingModule {}
|
||||
@@ -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 { InstructionsPageRoutingModule } from './instructions-routing.module';
|
||||
|
||||
import { InstructionsPage } from './instructions.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
InstructionsPageRoutingModule
|
||||
],
|
||||
declarations: [InstructionsPage]
|
||||
})
|
||||
export class InstructionsPageModule {}
|
||||
@@ -0,0 +1,65 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Prepare</ion-title>
|
||||
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button></ion-back-button>
|
||||
</ion-buttons>
|
||||
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-title size="large">Prepare the device</ion-title>
|
||||
|
||||
<ion-item>
|
||||
Attach (optional) pH Strip</ion-item>
|
||||
<ion-item>
|
||||
<img src="assets/images/attachph.png">
|
||||
</ion-item>
|
||||
|
||||
|
||||
|
||||
<ion-item>
|
||||
1. Hand through strap</ion-item>
|
||||
<ion-item>
|
||||
<img src="assets/images/handtroughstrap.png">
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
2. Take disk from body</ion-item>
|
||||
<ion-item>
|
||||
|
||||
<img src="assets/images/takediskfrombody.png">
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
3. Finger through clip</ion-item>
|
||||
<ion-item>
|
||||
|
||||
<img src="assets/images/fingertroughclip.png">
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
|
||||
4. Unlock spindle</ion-item>
|
||||
<ion-item>
|
||||
|
||||
<img src="assets/img/unlockspindle.png"></ion-item>
|
||||
|
||||
<ion-item>
|
||||
|
||||
5. Ready!</ion-item>
|
||||
<ion-item>
|
||||
|
||||
<img src="assets/images/ready.png"></ion-item>
|
||||
|
||||
|
||||
|
||||
<ion-button expand="block" routerLink="/instructions/step2" routerDirection="forward" >
|
||||
Step 2
|
||||
</ion-button>
|
||||
|
||||
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { InstructionsPage } from './instructions.page';
|
||||
|
||||
describe('InstructionsPage', () => {
|
||||
let component: InstructionsPage;
|
||||
let fixture: ComponentFixture<InstructionsPage>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ InstructionsPage ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(InstructionsPage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-instructions',
|
||||
templateUrl: './instructions.page.html',
|
||||
styleUrls: ['./instructions.page.scss'],
|
||||
})
|
||||
export class InstructionsPage implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { Step2Page } from './step2.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Step2Page
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class Step2PageRoutingModule {}
|
||||
@@ -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 { Step2PageRoutingModule } from './step2-routing.module';
|
||||
|
||||
import { Step2Page } from './step2.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
Step2PageRoutingModule
|
||||
],
|
||||
declarations: [Step2Page]
|
||||
})
|
||||
export class Step2PageModule {}
|
||||
@@ -0,0 +1,41 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>2. Transperancy</ion-title>
|
||||
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button></ion-back-button>
|
||||
</ion-buttons>
|
||||
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
|
||||
|
||||
<ion-item>
|
||||
|
||||
Turning the spindle, lower the disk until it disappears from view, then raise the disk until it re-appears. Record the re-appearance distance.
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
|
||||
<img src="assets/images/transperancy.png"><br>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
|
||||
The app will now calculate the Secchi disk depth (re-appareance distance minus distance to water)
|
||||
</ion-item>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<ion-button expand="block" routerLink="/instructions/step3" routerDirection="forward" >
|
||||
Step 3
|
||||
</ion-button>
|
||||
|
||||
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { Step2Page } from './step2.page';
|
||||
|
||||
describe('Step2Page', () => {
|
||||
let component: Step2Page;
|
||||
let fixture: ComponentFixture<Step2Page>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ Step2Page ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Step2Page);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-step2',
|
||||
templateUrl: './step2.page.html',
|
||||
styleUrls: ['./step2.page.scss'],
|
||||
})
|
||||
export class Step2Page implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { Step3Page } from './step3.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Step3Page
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class Step3PageRoutingModule {}
|
||||
@@ -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 { Step3PageRoutingModule } from './step3-routing.module';
|
||||
|
||||
import { Step3Page } from './step3.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
Step3PageRoutingModule
|
||||
],
|
||||
declarations: [Step3Page]
|
||||
})
|
||||
export class Step3PageModule {}
|
||||
@@ -0,0 +1,31 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>3. Disk color</ion-title>
|
||||
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button></ion-back-button>
|
||||
</ion-buttons>
|
||||
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-item>
|
||||
|
||||
Record the colour of the disk, as it appears at half the Secchi depth, by raising the disk to the distance calculated by the app. </ion-item><ion-item>
|
||||
|
||||
<img src="assets/images/diskcolor.png"><br>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
|
||||
The app will now calculate the Secchi disk depth (re-appareance distance minus distance to water)
|
||||
</ion-item>
|
||||
|
||||
|
||||
<ion-button expand="block" routerLink="/instructions/step4" routerDirection="forward" >
|
||||
Step 4
|
||||
</ion-button>
|
||||
|
||||
|
||||
|
||||
</ion-content>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { Step3Page } from './step3.page';
|
||||
|
||||
describe('Step3Page', () => {
|
||||
let component: Step3Page;
|
||||
let fixture: ComponentFixture<Step3Page>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ Step3Page ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Step3Page);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-step3',
|
||||
templateUrl: './step3.page.html',
|
||||
styleUrls: ['./step3.page.scss'],
|
||||
})
|
||||
export class Step3Page implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { Step4Page } from './step4.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Step4Page
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class Step4PageRoutingModule {}
|
||||
@@ -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 { Step4PageRoutingModule } from './step4-routing.module';
|
||||
|
||||
import { Step4Page } from './step4.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
Step4PageRoutingModule
|
||||
],
|
||||
declarations: [Step4Page]
|
||||
})
|
||||
export class Step4PageModule {}
|
||||
@@ -0,0 +1,34 @@
|
||||
<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-toolbar>
|
||||
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<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>
|
||||
<ion-item>
|
||||
|
||||
<img src="assets/images/bodypos.png"><br>
|
||||
</ion-item>
|
||||
|
||||
|
||||
<ion-item>
|
||||
|
||||
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" >
|
||||
|
||||
|
||||
Step 5
|
||||
</ion-button>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { Step4Page } from './step4.page';
|
||||
|
||||
describe('Step4Page', () => {
|
||||
let component: Step4Page;
|
||||
let fixture: ComponentFixture<Step4Page>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ Step4Page ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Step4Page);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-step4',
|
||||
templateUrl: './step4.page.html',
|
||||
styleUrls: ['./step4.page.scss'],
|
||||
})
|
||||
export class Step4Page implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { Step5Page } from './step5.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Step5Page
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class Step5PageRoutingModule {}
|
||||
@@ -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 { Step5PageRoutingModule } from './step5-routing.module';
|
||||
|
||||
import { Step5Page } from './step5.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
Step5PageRoutingModule
|
||||
],
|
||||
declarations: [Step5Page]
|
||||
})
|
||||
export class Step5PageModule {}
|
||||
@@ -0,0 +1,32 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>5. Additional</ion-title>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button></ion-back-button>
|
||||
</ion-buttons>
|
||||
|
||||
</ion-toolbar>
|
||||
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-item>
|
||||
pH (acidity)
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
|
||||
If you deployed the mini-Secchi disk with pH paper, compare the colour of the pH paper to the pH colour scale.
|
||||
|
||||
Natural waters are usually only slightly acidic (6) or alkalic (8). The acidity of the water gives an indication of how much Carbon is dissolved in the water and whether algae or plants are likely to grow.
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
|
||||
<img src="assets/images/ph.png"><br>
|
||||
|
||||
</ion-item>
|
||||
|
||||
|
||||
|
||||
<ion-button expand="block" routerLink="/home" routerDirection="root">Finish
|
||||
</ion-button>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { Step5Page } from './step5.page';
|
||||
|
||||
describe('Step5Page', () => {
|
||||
let component: Step5Page;
|
||||
let fixture: ComponentFixture<Step5Page>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ Step5Page ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Step4Page);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-step5',
|
||||
templateUrl: './step5.page.html',
|
||||
styleUrls: ['./step5.page.scss'],
|
||||
})
|
||||
export class Step5Page implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { ColourathalfdepthPage } from './colourathalfdepth.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ColourathalfdepthPage
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ColourathalfdepthPageRoutingModule {}
|
||||
@@ -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 { ColourathalfdepthPageRoutingModule } from './colourathalfdepth-routing.module';
|
||||
|
||||
import { ColourathalfdepthPage } from './colourathalfdepth.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
ColourathalfdepthPageRoutingModule
|
||||
],
|
||||
declarations: [ColourathalfdepthPage]
|
||||
})
|
||||
export class ColourathalfdepthPageModule {}
|
||||
@@ -0,0 +1,53 @@
|
||||
<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-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-item>
|
||||
The Secchi depth is: {{secchi_depth}} cm <br>
|
||||
Raise the disk to half the Secchi disk depth: {{this.halfdepth}} cm
|
||||
Record the Forel-Ule colour of the disk at this depth (use the colour scale on the handheld device).
|
||||
In addition, if it is safe to do so, take a photo of the disk in the water at this depth using the button at the bottom of this screen.
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<img src="assets/images/halfdepth.png">
|
||||
</ion-item>
|
||||
|
||||
|
||||
|
||||
|
||||
<ion-item>
|
||||
|
||||
The colour of the disk in the water is most similar to Forel Ule colour:
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
|
||||
|
||||
<ion-input [(ngModel)]="colourathalfdepth" type="number" inputmode="numeric" placeholder="Enter colour number (1-21)"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
|
||||
<ion-label>To add a photo click here:</ion-label>
|
||||
|
||||
|
||||
<ion-button (click)="takePicture()">
|
||||
<ion-icon name="camera"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<img [src]="this.PictureTaken"/>
|
||||
</ion-item>
|
||||
|
||||
|
||||
<ion-button expand="block" (click)="validate()" routerLink="/ms_measure/colouratsurface" routerDirection="forward">
|
||||
Go to Step 4
|
||||
</ion-button>
|
||||
|
||||
</ion-content>
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { ColourathalfdepthPage } from './colourathalfdepth.page';
|
||||
|
||||
describe('ColourathalfdepthPage', () => {
|
||||
let component: ColourathalfdepthPage;
|
||||
let fixture: ComponentFixture<ColourathalfdepthPage>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ColourathalfdepthPage ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ColourathalfdepthPage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Storage } from '@ionic/storage-angular';
|
||||
import { Plugins } from '@capacitor/core';
|
||||
import { CameraResultType } from '@capacitor/camera';
|
||||
|
||||
|
||||
const { Camera } = Plugins;
|
||||
|
||||
@Component({
|
||||
selector: 'app-colourathalfdepth',
|
||||
templateUrl: './colourathalfdepth.page.html',
|
||||
styleUrls: ['./colourathalfdepth.page.scss'],
|
||||
})
|
||||
|
||||
export class ColourathalfdepthPage implements OnInit {
|
||||
public reappear_val:number;
|
||||
public distancetowater_val: number;
|
||||
public secchi_depth:number;
|
||||
public colourathalfdepth:number;
|
||||
public halfdepth:number;
|
||||
public PictureTaken:string;
|
||||
|
||||
|
||||
|
||||
constructor(private storage: Storage) { }
|
||||
|
||||
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
|
||||
await this.storage.create();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
this.storage.get('reappear').then((val) => {
|
||||
|
||||
console.log('reappear', val);
|
||||
this.reappear_val=val;
|
||||
|
||||
});
|
||||
|
||||
|
||||
this.storage.get('distancetowater').then((val) => {
|
||||
|
||||
this.distancetowater_val=val;
|
||||
this.secchi_depth=this.reappear_val-this.distancetowater_val;
|
||||
|
||||
//secchi depth (Zsd = reappear 1.2 – 1.1 distance to water)
|
||||
|
||||
this.halfdepth=this.distancetowater_val+((this.reappear_val-this.distancetowater_val)/2);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async takePicture() {
|
||||
try {
|
||||
const Picture = await Camera.getPhoto({
|
||||
quality: 90,
|
||||
allowEditing: false,
|
||||
resultType: CameraResultType.Base64,
|
||||
});
|
||||
this.PictureTaken = "data:image/jpeg;base64," + Picture.base64String;
|
||||
this.storage.set('colourathalfdepthimage', Picture.base64String).then(result => {
|
||||
console.log('Data is saved');
|
||||
}).catch(e => {
|
||||
console.log("error: " + e);
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async validate() {
|
||||
// alert(`hola ${this.distancetowater}!`);
|
||||
await this.storage.create();
|
||||
|
||||
this.storage.set('colourathalfdepth', this.colourathalfdepth).then(result => {
|
||||
// console.log('Data is saved');
|
||||
}).catch(e => {
|
||||
console.log("error: " + e);
|
||||
});
|
||||
|
||||
|
||||
this.storage.set('secchi_depth', this.secchi_depth).then(result => {
|
||||
// console.log('Data is saved');
|
||||
}).catch(e => {
|
||||
console.log("error: " + e);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { ColouratsurfacePage } from './colouratsurface.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ColouratsurfacePage
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ColouratsurfacePageRoutingModule {}
|
||||
@@ -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 { ColouratsurfacePageRoutingModule } from './colouratsurface-routing.module';
|
||||
|
||||
import { ColouratsurfacePage } from './colouratsurface.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
ColouratsurfacePageRoutingModule
|
||||
],
|
||||
declarations: [ColouratsurfacePage]
|
||||
})
|
||||
export class ColouratsurfacePageModule {}
|
||||
@@ -0,0 +1,120 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Additional</ion-title>
|
||||
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button></ion-back-button>
|
||||
</ion-buttons>
|
||||
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
|
||||
<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>
|
||||
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>
|
||||
<img src="assets/images/bodypos.png"><br>
|
||||
</ion-item>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<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> <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-content>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user