/**
 * Copyright 2019 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */ import firebase from 'firebase/app' ; import { Injectable } from '@angular/core' ; import { AngularFirestore , DocumentData } from '@angular/fire/firestore' ; import { FirebaseDataConverter } from '../../shared/converters/firebase-data-converter' ; import { Observable } from 'rxjs' ; import { Project } from '../../shared/models/project.model' ; import { map } from 'rxjs/operators' ; import { User } from './../../shared/models/user.model' ; import { Feature } from '../../shared/models/feature.model' ; import { Layer } from './../../shared/models/layer.model' ; import { List , Map } from 'immutable' ; import { Observation } from '../../shared/models/observation/observation.model' ; import { Role } from '../../shared/models/role.model' ; import { OfflineBaseMapSource } from '../../shared/models/offline-base-map-source' ; import 'firebase/storage' ; @ Injectable ( { providedIn : 'root' , } ) export class DataStoreService { private readonly VALID_ROLES = [ 'owner' , 'contributor' , 'manager' , 'viewer' ] ; constructor ( private db ) { } param id loadProject$ ( id ) { return this . db . collection ( 'projects' ) . doc ( id ) . valueChanges ( ) . pipe ( map ( data => FirebaseDataConverter . toProject ( id , data as DocumentData ) ) ) ; } /**
     * Returns an Observable that loads and emits the list of projects accessible to the specified user.
     *
     */ loadAccessibleProject$ ( userEmail ) { return this . db . collection ( 'projects' , ref => ref . where ( new firebase . firestore . FieldPath ( 'acl' , userEmail ) , 'in' , this . VALID_ROLES ) ) . snapshotChanges ( ) . pipe ( map ( projects => List ( projects . map ( a => { const docData = a . payload . doc . data ( ) as DocumentData ; const id = a . payload . doc . id ; return FirebaseDataConverter . toProject ( id , docData ) ; } ) ) ) ) ; } param projectId param newTitle updateProjectTitle ( projectId , newTitle ) { return this . db . collection ( 'projects' ) . doc ( projectId ) . set ( { title : { en : newTitle } } , { merge : true } ) ; } addOrUpdateLayer ( projectId , layer ) { return this . db . collection ( 'projects' ) . doc ( projectId ) . update ( { [ `layers.${ layer . id }` ] : FirebaseDataConverter . layerToJS ( layer ) , } ) ; } async deleteLayer ( projectId , layerId ) { await this . deleteAllFeaturesInLayer ( projectId , layerId ) ; await this . deleteAllObservationsInLayer ( projectId , layerId ) ; return await this . db . collection ( 'projects' ) . doc ( projectId ) . update ( { [ `layers.${ layerId }` ] : firebase . firestore . FieldValue . delete ( ) , } ) ; } private async deleteAllObservationsInLayer ( projectId , layerId ) { const observations = this . db . collection ( `projects/${ projectId }/observations` , ref => ref . where ( 'layerId' , '==' , layerId ) ) ; const querySnapshot = await observations . get ( ) . toPromise ( ) ; return await Promise . all ( querySnapshot . docs . map ( doc => doc . ref . delete ( ) ) ) ; } private async deleteAllObservationsInFeature ( projectId , featureId ) { const observations = this . db . collection ( `projects/${ projectId }/observations` , ref => ref . where ( 'featureId' , '==' , featureId ) ) ; const querySnapshot = await observations . get ( ) . toPromise ( ) ; return await Promise . all ( querySnapshot . docs . map ( doc => doc . ref . delete ( ) ) ) ; } private async deleteAllFeaturesInLayer ( projectId , layerId ) { const featuresInLayer = this . db . collection ( `projects/${ projectId }/features` , ref => ref . where ( 'layerId' , '==' , layerId ) ) ; const querySnapshot = await featuresInLayer . get ( ) . toPromise ( ) ; return await Promise . all ( querySnapshot . docs . map ( doc => doc . ref . delete ( ) ) ) ; } async deleteFeature ( projectId , featureId ) { await this . deleteAllObservationsInFeature ( projectId , featureId ) ; return await this . db . collection ( 'projects' ) . doc ( projectId ) . collection ( 'features' ) . doc ( featureId ) . delete ( ) ; } async deleteObservation ( projectId , observationId ) { return await this . db . collection ( 'projects' ) . doc ( projectId ) . collection ( 'observations' ) . doc ( observationId ) . delete ( ) ; } updateObservation ( projectId , observation ) { return this . db . collection ( `projects/${ projectId }/observations` ) . doc ( observation . id ) . set ( FirebaseDataConverter . observationToJS ( observation ) ) ; } param projectId param featureId loadFeature$ ( projectId , featureId ) { return this . db . collection ( `projects/${ projectId }/features` ) . doc ( featureId ) . get ( ) . pipe ( map ( doc => FirebaseDataConverter . toFeature ( doc . id , doc . data ( ) ! as DocumentData ) ) , map ( f => f as Feature ) ) ; } param uid user$ ( uid ) { return this . db . doc < User > ( `users/${ uid }` ) . valueChanges ( ) . pipe ( map ( data => FirebaseDataConverter . toUser ( data as DocumentData ) ) ) ; } features$ ( { id } ) { return this . db . collection ( `projects/${ id }/features` ) . valueChanges ( { idField : 'id' } ) . pipe ( map ( array => List ( array . map ( obj => FirebaseDataConverter . toFeature ( obj . id , obj ) ) . filter ( f => ! ! f ) . map ( f => f as Feature ) ) ) ) ; } param id param featureId observations$ ( project , feature ) { return this . db . collection ( `projects/${ project . id }/observations` , ref => ref . where ( 'featureId' , '==' , feature . id ) ) . valueChanges ( { idField : 'id' } ) . pipe ( map ( array => List ( array . map ( obj => { return FirebaseDataConverter . toObservation ( project . getLayer ( feature . layerId ) ! . getForm ( ( obj as DocumentData ) . formId ) ! , obj . id , obj ) ; } ) ) ) ) ; } loadObservation$ ( project , feature , observationId ) { return this . db . collection ( `projects/${ project . id }/observations` ) . doc ( observationId ) . get ( ) . pipe ( map ( doc => { return FirebaseDataConverter . toObservation ( project . getLayer ( feature . layerId ) ! . getForm ( ( doc . data ( ) ! as DocumentData ) . formId ) ! , doc . id , doc . data ( ) ! as DocumentData ) ; } ) ) ; } param projectId param email param role updateAcl ( projectId , acl ) { return this . db . collection ( 'projects' ) . doc ( projectId ) . update ( { acl : FirebaseDataConverter . aclToJs ( acl ) } ) ; } generateId ( ) { return this . db . collection ( 'ids' ) . ref . doc ( ) . id ; } getServerTimestamp ( ) { return firebase . firestore . FieldValue . serverTimestamp ( ) ; } updateFeature ( projectId , feature ) { return this . db . collection ( 'projects' ) . doc ( projectId ) . collection ( 'features' ) . doc ( feature . id ) . set ( FirebaseDataConverter . featureToJS ( feature ) ) ; } /**
     * Creates a new project in the remote db using the specified title,
     * returning the id of the newly created project. ACLs are initialized
     * to include the specified user email as project owner.
     */ async createProject ( ownerEmail , title , offlineBaseMapSources ? ) { const projectId = this . generateId ( ) ; await this . updateProjectTitle ( projectId , title ) ; await this . db . collection ( 'projects' ) . doc ( projectId ) . set ( FirebaseDataConverter . newProjectJS ( ownerEmail , title , offlineBaseMapSources ) ) ; return Promise . resolve ( projectId ) ; } getImageDownloadURL ( path ) { return firebase . storage ( ) . ref ( ) . child ( path ) . getDownloadURL ( ) ; } }