treetrail-frontend/src/app/data.service.ts
2024-10-19 11:53:15 +02:00

280 lines
9.1 KiB
TypeScript

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<GeoJSON.FeatureCollection> = new BehaviorSubject<GeoJSON.FeatureCollection>(<GeoJSON.FeatureCollection>{})
public treeFeatures: BehaviorSubject<GeoJSON.FeatureCollection> = new BehaviorSubject<GeoJSON.FeatureCollection>(<GeoJSON.FeatureCollection>{})
public poisFeatures: BehaviorSubject<GeoJSON.FeatureCollection> = new BehaviorSubject<GeoJSON.FeatureCollection>(<GeoJSON.FeatureCollection>{})
public zoneFeatures: BehaviorSubject<GeoJSON.FeatureCollection> = new BehaviorSubject<GeoJSON.FeatureCollection>(<GeoJSON.FeatureCollection>{})
//public treesUpdated: Observable<void>
public trails: BehaviorSubject<Trails> = new BehaviorSubject<Trails>({})
//public trails$ = this.trails.asObservable()
public tree_trail: BehaviorSubject<TreeTrail[]> = new BehaviorSubject<TreeTrail[]>([])
//public tree_trail$ = this.tree_trail.asObservable()
//public trees: BehaviorSubject<Tree[]> = new BehaviorSubject<Tree[]>([])
public trees: BehaviorSubject<Trees> = new BehaviorSubject<Trees>({})
//public trees$ = this.trees.asObservable()
public pois: BehaviorSubject<Pois> = new BehaviorSubject<Pois>({})
public zones: BehaviorSubject<Zones> = new BehaviorSubject<Zones>({})
public all: BehaviorSubject<All> = new BehaviorSubject<All>(new All())
public addTree$: Subject<NewTree> = new Subject<NewTree>()
public updatePendingTrees$: Subject<void> = 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<Object[]> {
return this.dbService.getAll(pendingTreeDbName)
}
getPendingTree(id: string): Observable<Tree> {
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}).`)
}
}
})
}
)
}
)
}
}
)
}
}