Initial commit
This commit is contained in:
88
src/app/app-routing.module.ts
Normal file
88
src/app/app-routing.module.ts
Normal file
@@ -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 { }
|
||||
60
src/app/app.component.html
Normal file
60
src/app/app.component.html
Normal file
@@ -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>
|
||||
1
src/app/app.component.scss
Normal file
1
src/app/app.component.scss
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
23
src/app/app.component.spec.ts
Normal file
23
src/app/app.component.spec.ts
Normal file
@@ -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!
|
||||
|
||||
});
|
||||
53
src/app/app.component.ts
Normal file
53
src/app/app.component.ts
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
||||
41
src/app/app.module.ts
Normal file
41
src/app/app.module.ts
Normal file
@@ -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 {}
|
||||
17
src/app/auth/login/login-routing.module.ts
Normal file
17
src/app/auth/login/login-routing.module.ts
Normal file
@@ -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 {}
|
||||
23
src/app/auth/login/login.module.ts
Normal file
23
src/app/auth/login/login.module.ts
Normal file
@@ -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 {}
|
||||
52
src/app/auth/login/login.page.html
Normal file
52
src/app/auth/login/login.page.html
Normal file
@@ -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>
|
||||
11
src/app/auth/login/login.page.scss
Normal file
11
src/app/auth/login/login.page.scss
Normal file
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
24
src/app/auth/login/login.page.spec.ts
Normal file
24
src/app/auth/login/login.page.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
121
src/app/auth/login/login.page.ts
Normal file
121
src/app/auth/login/login.page.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
17
src/app/auth/profile/profile-routing.module.ts
Normal file
17
src/app/auth/profile/profile-routing.module.ts
Normal file
@@ -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 {}
|
||||
22
src/app/auth/profile/profile.module.ts
Normal file
22
src/app/auth/profile/profile.module.ts
Normal file
@@ -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 {}
|
||||
110
src/app/auth/profile/profile.page.html
Normal file
110
src/app/auth/profile/profile.page.html
Normal file
@@ -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>
|
||||
116
src/app/auth/profile/profile.page.scss
Normal file
116
src/app/auth/profile/profile.page.scss
Normal file
@@ -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;
|
||||
}
|
||||
24
src/app/auth/profile/profile.page.spec.ts
Normal file
24
src/app/auth/profile/profile.page.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
125
src/app/auth/profile/profile.page.ts
Normal file
125
src/app/auth/profile/profile.page.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/app/auth/resetpw/resetpw-routing.module.ts
Normal file
17
src/app/auth/resetpw/resetpw-routing.module.ts
Normal file
@@ -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 {}
|
||||
21
src/app/auth/resetpw/resetpw.module.ts
Normal file
21
src/app/auth/resetpw/resetpw.module.ts
Normal file
@@ -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 {}
|
||||
26
src/app/auth/resetpw/resetpw.page.html
Normal file
26
src/app/auth/resetpw/resetpw.page.html
Normal file
@@ -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
src/app/auth/resetpw/resetpw.page.scss
Normal file
0
src/app/auth/resetpw/resetpw.page.scss
Normal file
24
src/app/auth/resetpw/resetpw.page.spec.ts
Normal file
24
src/app/auth/resetpw/resetpw.page.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
57
src/app/auth/resetpw/resetpw.page.ts
Normal file
57
src/app/auth/resetpw/resetpw.page.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
17
src/app/auth/signup/signup-routing.module.ts
Normal file
17
src/app/auth/signup/signup-routing.module.ts
Normal file
@@ -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 {}
|
||||
23
src/app/auth/signup/signup.module.ts
Normal file
23
src/app/auth/signup/signup.module.ts
Normal file
@@ -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 {}
|
||||
48
src/app/auth/signup/signup.page.html
Normal file
48
src/app/auth/signup/signup.page.html
Normal file
@@ -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>
|
||||
11
src/app/auth/signup/signup.page.scss
Normal file
11
src/app/auth/signup/signup.page.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
ion-item{
|
||||
--border-radius: 10px;
|
||||
}
|
||||
|
||||
ion-button{
|
||||
--border-radius: 10px;
|
||||
}
|
||||
|
||||
ion-input ion-icon{
|
||||
padding-right: 15px;
|
||||
}
|
||||
24
src/app/auth/signup/signup.page.spec.ts
Normal file
24
src/app/auth/signup/signup.page.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
96
src/app/auth/signup/signup.page.ts
Normal file
96
src/app/auth/signup/signup.page.ts
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
16
src/app/home/home-routing.module.ts
Normal file
16
src/app/home/home-routing.module.ts
Normal file
@@ -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 {}
|
||||
21
src/app/home/home.module.ts
Normal file
21
src/app/home/home.module.ts
Normal file
@@ -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 {}
|
||||
56
src/app/home/home.page.html
Normal file
56
src/app/home/home.page.html
Normal file
@@ -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>
|
||||
128
src/app/home/home.page.scss
Normal file
128
src/app/home/home.page.scss
Normal file
@@ -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;
|
||||
}
|
||||
24
src/app/home/home.page.spec.ts
Normal file
24
src/app/home/home.page.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
49
src/app/home/home.page.ts
Normal file
49
src/app/home/home.page.ts
Normal file
@@ -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 {}
|
||||
19
src/app/mobisplugins/canairioco2/canairioco2.module.ts
Normal file
19
src/app/mobisplugins/canairioco2/canairioco2.module.ts
Normal file
@@ -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 {}
|
||||
82
src/app/mobisplugins/canairioco2/canairioco2.page.html
Normal file
82
src/app/mobisplugins/canairioco2/canairioco2.page.html
Normal file
@@ -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>
|
||||
|
||||
24
src/app/mobisplugins/canairioco2/canairioco2.page.spec.ts
Normal file
24
src/app/mobisplugins/canairioco2/canairioco2.page.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
245
src/app/mobisplugins/canairioco2/canairioco2.page.ts
Normal file
245
src/app/mobisplugins/canairioco2/canairioco2.page.ts
Normal file
@@ -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 {}
|
||||
23
src/app/mobisplugins/canairiopm25/canairiopm25.module.ts
Normal file
23
src/app/mobisplugins/canairiopm25/canairiopm25.module.ts
Normal file
@@ -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 {}
|
||||
85
src/app/mobisplugins/canairiopm25/canairiopm25.page.html
Normal file
85
src/app/mobisplugins/canairiopm25/canairiopm25.page.html
Normal file
@@ -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>
|
||||
24
src/app/mobisplugins/canairiopm25/canairiopm25.page.spec.ts
Normal file
24
src/app/mobisplugins/canairiopm25/canairiopm25.page.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
251
src/app/mobisplugins/canairiopm25/canairiopm25.page.ts
Normal file
251
src/app/mobisplugins/canairiopm25/canairiopm25.page.ts
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
17
src/app/mobisplugins/ispex/ispex-routing.module.ts
Normal file
17
src/app/mobisplugins/ispex/ispex-routing.module.ts
Normal file
@@ -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 {}
|
||||
24
src/app/mobisplugins/ispex/ispex.module.ts
Normal file
24
src/app/mobisplugins/ispex/ispex.module.ts
Normal file
@@ -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 {}
|
||||
64
src/app/mobisplugins/ispex/ispex.page.html
Normal file
64
src/app/mobisplugins/ispex/ispex.page.html
Normal file
@@ -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
src/app/mobisplugins/ispex/ispex.page.scss
Normal file
0
src/app/mobisplugins/ispex/ispex.page.scss
Normal file
24
src/app/mobisplugins/ispex/ispex.page.spec.ts
Normal file
24
src/app/mobisplugins/ispex/ispex.page.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
75
src/app/mobisplugins/ispex/ispex.page.ts
Normal file
75
src/app/mobisplugins/ispex/ispex.page.ts
Normal file
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
17
src/app/mobisplugins/ispex/preview/preview-routing.module.ts
Normal file
17
src/app/mobisplugins/ispex/preview/preview-routing.module.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { PreviewPage } from './preview.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PreviewPage
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class PreviewPageRoutingModule {}
|
||||
20
src/app/mobisplugins/ispex/preview/preview.module.ts
Normal file
20
src/app/mobisplugins/ispex/preview/preview.module.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { PreviewPageRoutingModule } from './preview-routing.module';
|
||||
|
||||
import { PreviewPage } from './preview.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
PreviewPageRoutingModule
|
||||
],
|
||||
declarations: [PreviewPage]
|
||||
})
|
||||
export class PreviewPageModule {}
|
||||
32
src/app/mobisplugins/ispex/preview/preview.page.html
Normal file
32
src/app/mobisplugins/ispex/preview/preview.page.html
Normal file
@@ -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>
|
||||
65
src/app/mobisplugins/ispex/preview/preview.page.scss
Normal file
65
src/app/mobisplugins/ispex/preview/preview.page.scss
Normal file
@@ -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;}
|
||||
|
||||
|
||||
24
src/app/mobisplugins/ispex/preview/preview.page.spec.ts
Normal file
24
src/app/mobisplugins/ispex/preview/preview.page.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { PreviewPage } from './preview.page';
|
||||
|
||||
describe('PreviewPage', () => {
|
||||
let component: PreviewPage;
|
||||
let fixture: ComponentFixture<PreviewPage>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ PreviewPage ],
|
||||
imports: [IonicModule.forRoot()]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PreviewPage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
259
src/app/mobisplugins/ispex/preview/preview.page.ts
Normal file
259
src/app/mobisplugins/ispex/preview/preview.page.ts
Normal file
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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