forked from philorg/treetrail-frontend
281 lines
9.1 KiB
TypeScript
281 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}).`)
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
}
|