import { ApplicationRef, Injectable } from '@angular/core' import { BehaviorSubject, Observable, Subject, combineLatest } from 'rxjs' import { HttpClient } from '@angular/common/http' import { Router } from '@angular/router' import { catchError, first, map } from 'rxjs/operators' import { throwError } from 'rxjs' import { v1 as uuidv1 } from 'uuid' import { GeoJSONSource, LngLat } from 'maplibre-gl' import { NgxIndexedDBService } from 'ngx-indexed-db' import { PlantsTrails, Trail, Trails, Trees, Pois, Zones, TreeTrail, All, Plant, Tree } from './models' import { MessageService } from './message.service' import { Feature } from 'geojson' export const pendingTreeDbName = 'pendingTree' export class TreeDef { constructor( public plant: Plant, public trails: Trail[], ) {} } export class NewTree { id: string constructor( public treeDef: TreeDef, public lngLat: LngLat, public dataService: DataService, public picture?: string, public uuid1?: string, public pending?: number, public details: Object = {} ) { if (!this.uuid1) { this.uuid1 = uuidv1() } } getFeature(): Feature { return { 'type': 'Feature', 'geometry': { 'type': 'Point', 'coordinates': [this.lngLat.lng, this.lngLat.lat, 0] }, 'properties': { 'plantekey_id': this.treeDef.plant.id, 'symbol': this.dataService.all.value.plants[this.treeDef.plant.id].symbol, 'id': this.id || this.uuid1, 'pending': this.pending }, } } getForStorage(): Object { let so = { feature: this.getFeature(), trails: this.treeDef.trails.map(trail => trail.id), uuid1: this.uuid1, id: this.uuid1, details: this.details } if (this.picture) { so['picture'] = this.picture } return so } getFormData(): FormData { let formData = new FormData() formData.set('plantekey_id', this.treeDef.plant.id) if (this.treeDef.trails.length > 0) { formData.set('trail_ids', this.treeDef.trails.map( trail => trail.id.toString()).join(',') ) } formData.set('lng', this.lngLat.lng.toString()) formData.set('lat', this.lngLat.lat.toString()) formData.set('uuid1', this.uuid1) if (Object.keys(this.details).length > 0) { formData.set('details', JSON.stringify(this.details)) } if (this.picture) { formData.set('picture', this.picture) } return formData } } @Injectable({ providedIn: 'root' }) export class DataService { constructor( public httpClient: HttpClient, public messageService: MessageService, public dbService: NgxIndexedDBService, public router: Router, public appRef: ApplicationRef, ) { this.all.subscribe( all => { // Assign plants and trails to trees Object.values(all.trees).forEach( tree => { tree.plant = all.plants[tree.plantekeyId] let trail_ids = new Set(all.tree_trails.filter( tt => tt.tree_id == tree.id ).map(tt => tt.trail_id)) tree.trails = Object.values(all.trails).filter( trail => trail_ids.has(+trail.id) ) } ) } ) } public trailFeatures: BehaviorSubject = new BehaviorSubject({}) public treeFeatures: BehaviorSubject = new BehaviorSubject({}) public poisFeatures: BehaviorSubject = new BehaviorSubject({}) public zoneFeatures: BehaviorSubject = new BehaviorSubject({}) //public treesUpdated: Observable public trails: BehaviorSubject = new BehaviorSubject({}) //public trails$ = this.trails.asObservable() public tree_trail: BehaviorSubject = new BehaviorSubject([]) //public tree_trail$ = this.tree_trail.asObservable() //public trees: BehaviorSubject = new BehaviorSubject([]) public trees: BehaviorSubject = new BehaviorSubject({}) //public trees$ = this.trees.asObservable() public pois: BehaviorSubject = new BehaviorSubject({}) public zones: BehaviorSubject = new BehaviorSubject({}) public all: BehaviorSubject = new BehaviorSubject(new All()) public addTree$: Subject = new Subject() public updatePendingTrees$: Subject = new Subject() set_tree_trail(data: TreeTrail[]) { this.tree_trail.next(data) } plant_trail: PlantsTrails = {} plant_in_some_trail(plantId: string): boolean { return plantId in this.plant_trail } // Try to send to server, falling back to local storage for later sync addTree(location: LngLat, treeDef: TreeDef, picture?: string, details: Object = {}) { const newTree = new NewTree(treeDef, location, this, picture, undefined, 1, details) this.httpClient.post('v1/tree', newTree.getFormData()).pipe( map(data => { // Got an id from the server for that tree newTree.id = data['id'] this.addTree$.next(newTree) }), catchError(err => { this.messageService.message.next(`Cannot save: (${err.statusText}). ` + 'It is stored in the local device and will be saved later.') this.addTree$.next(newTree) this.addPendingTree(newTree) return new Observable() }) ).subscribe() } // Store in local database to sync it with the server when possible addPendingTree(newTree: NewTree) { this.dbService.add(pendingTreeDbName, newTree.getForStorage()).subscribe() } // Get a geo feature source of pending trees (from local indexedDB) getPendingTrees(): Observable { return this.dbService.getAll(pendingTreeDbName) } getPendingTree(id: string): Observable { return this.dbService.getByID(pendingTreeDbName, id).pipe(map( data => { const feature = data['feature'] let tree = new Tree( feature['properties']['id'], feature, feature['properties']['plantekey_id'], data['picture'], ) tree.plant = this.all.value.plants[feature['properties']['plantekey_id']] return tree } )) } deletePendingTrees() { this.getPendingTrees().subscribe( trees => this.dbService.bulkDelete( pendingTreeDbName, trees.map(tree => tree['id']) ).subscribe(_ => { this.updatePendingTrees$.next() this.appRef.tick() }) ) } // Attempts to send the pending trees to the server syncPendingTrees(canRedirectToLogin: boolean=false) { this.dbService.count(pendingTreeDbName).pipe(first()).subscribe( count => { if (count > 0) { combineLatest([ this.getPendingTrees(), this.all ]).subscribe( ([treeList, all]) => { if (Object.keys(all.trees).length == 0) { return } treeList.map( tr => { const feature = tr['feature'] let newTree = new NewTree( new TreeDef( all.plants[feature['properties']['plantekey_id']], [] // this.all.getValue().trails.filter( // trail => trail. // ) ), new LngLat( feature['geometry']['coordinates'][0], feature['geometry']['coordinates'][1] ), this, tr['picture'], tr['uuid1'], undefined, tr['details'], ) this.httpClient.post('v1/tree', newTree.getFormData()).pipe( map(data => { this.dbService.deleteByKey( pendingTreeDbName, feature['properties']['id'] ).subscribe( r => { // Got an id from the server for that tree newTree.id = data['id'] this.updatePendingTrees$.next() } ) this.appRef.tick() }) ).subscribe({ error: (err) => { if (err.status == 401) { this.messageService.message.next(`Cannot save pending trees, you need to login again.`) if (canRedirectToLogin) { this.router.navigate(['/login']) } // TODO: resubmit pending trees } else { this.messageService.message.next(`Cannot save pending trees (${err.statusText}).`) } } }) } ) } ) } } ) } }