/**
 * 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 { DocumentData } from '@angular/fire/firestore' ; import { Project } from '../models/project.model' ; import { StringMap } from '../models/string-map.model' ; import { Layer } from '../models/layer.model' ; import { Form } from '../models/form/form.model' ; import { Field , FieldType } from '../models/form/field.model' ; import { MultipleChoice , Cardinality , } from '../models/form/multiple-choice.model' ; import { Feature , LocationFeature , GeoJsonFeature , PolygonFeature , } from '../models/feature.model' ; import { Observation } from '../models/observation/observation.model' ; import { Option } from '../../shared/models/form/option.model' ; import { List , Map } from 'immutable' ; import { AuditInfo } from '../models/audit-info.model' ; import { Response } from '../../shared/models/observation/response.model' ; import { Role } from '../models/role.model' ; import { User } from '../models/user.model' ; import { OfflineBaseMapSource } from '../models/offline-base-map-source' ; const FIELD_TYPE_ENUMS_BY_STRING = Map ( [ [ FieldType . TEXT , 'text_field' ] , [ FieldType . MULTIPLE_CHOICE , 'multiple_choice' ] , [ FieldType . PHOTO , 'photo' ] , [ FieldType . NUMBER , 'number' ] , [ FieldType . DATE , 'date' ] , [ FieldType . TIME , 'time' ] , ] ) ; const FIELD_TYPE_STRINGS_BY_ENUM = Map ( Array . from ( FIELD_TYPE_ENUMS_BY_STRING . toArray ( ) , el => el . reverse ( ) as [ string , FieldType ] ) ) ; /**
 * Helper to return either the keys of a dictionary, or if missing, returns an
 * empty array.
 */ function keys ( dict ? ) { return Object . keys ( dict || { } ) ; } export class FirebaseDataConverter { param id param data static toProject ( id , data ) { return new Project ( id , StringMap ( data . title ) , StringMap ( data . description ) , Map < string , Layer > ( keys ( data . layers ) . map ( ( id ) => [ id as string , FirebaseDataConverter . toLayer ( id , data . layers [ id ] ) , ] ) ) , Map < string , Role > ( keys ( data . acl ) . map ( ( id ) => [ id as string , FirebaseDataConverter . toRole ( data . acl [ id ] ) , ] ) ) ) ; } private static toRole ( roleString ) { switch ( roleString ) { case 'owner' : return Role . OWNER ; case 'manager' : return Role . MANAGER ; case 'contributor' : return Role . CONTRIBUTOR ; case 'viewer' : return Role . VIEWER ; default : console . log ( 'User has unsupported role: ' , roleString ) ; return Role . VIEWER ; } } static newProjectJS ( ownerEmail , title , offlineBaseMapSources ? ) { return { title : { en : title } , acl : { [ ownerEmail ] : FirebaseDataConverter . toRoleId ( Role . OWNER ) } , ... ( offlineBaseMapSources ?. length ? { offlineBaseMapSources } : { } ) , } ; } param id param data private static toLayer ( id , data ) { return new Layer ( id , data . index || - 1 , data . defaultStyle ?. color || data . color , StringMap ( data . name ) , Map < string , Form > ( keys ( data . forms ) . map ( ( id ) => [ id as string , FirebaseDataConverter . toForm ( id , data . forms [ id ] ) , ] ) ) , data . contributorsCanAdd || [ ] ) ; } static layerToJS ( layer ) { const { name , forms , color , contributorsCanAdd , ... layerDoc } = layer ; return { contributorsCanAdd , name : name ?. toJS ( ) || { } , ... ( forms ? { forms : forms ?. valueSeq ( ) . reduce ( ( map , form ) => ( { ... map , [ form . id ] : this . formToJS ( form ) } ) , { } ) , } : { } ) , defaultStyle : { color } , ... layerDoc , } ; } param id param data private static toForm ( id , data ) { return new Form ( id , Map < string , Field > ( keys ( data . elements ) . map ( id => FirebaseDataConverter . toField ( id , data . elements [ id ] ) ) . filter ( field => field !== null ) . map ( field => [ field ! . id , field ! ] ) ) ) ; } private static formToJS ( form ) { const { fields , ... formDoc } = form ; return { elements : fields ?. reduce ( ( map , field ) => ( { ... map , [ field . id ] : this . fieldToJS ( field ) , } ) , { } ) || { } , ... formDoc , } ; } public static featureToJS ( feature ) { if ( feature instanceof LocationFeature ) { const { layerId , location } = feature ; return { layerId , location , } ; } else if ( feature instanceof GeoJsonFeature ) { const { layerId , geoJson } = feature ; return { layerId , geoJson , } ; } else if ( feature instanceof PolygonFeature ) { const { layerId , polygonVertices } = feature ; return { layerId , polygonVertices , } ; } else { throw new Error ( `Cannot convert unexpected feature class ${ feature . constructor . name } to json.` ) ; } } param id param data private static toField ( id , data ) { try { return new Field ( id , FirebaseDataConverter . stringToFieldType ( data . type ) , StringMap ( data . label ) , data . required , data . index || - 1 , data . options && new MultipleChoice ( FirebaseDataConverter . stringToCardinality ( data . cardinality ) , List ( keys ( data . options ) . map ( ( id ) => FirebaseDataConverter . toOption ( id , data . options [ id ] ) ) ) ) ) ; } catch ( e ) { console . error ( e ) ; return null ; } } private static fieldToJS ( field ) { const { type , label , multipleChoice , ... fieldDoc } = field ; if ( multipleChoice === undefined ) { return { type : FirebaseDataConverter . fieldTypeToString ( type ) , label : label . toJS ( ) , ... fieldDoc , } ; } else { return { type : FirebaseDataConverter . fieldTypeToString ( type ) , label : label . toJS ( ) , cardinality : FirebaseDataConverter . cardinalityToString ( multipleChoice . cardinality ) , options : multipleChoice ?. options ?. reduce ( ( map , option ) => ( { ... map , [ option . id ] : FirebaseDataConverter . optionToJS ( option ) , } ) , { } ) || { } , ... fieldDoc , } ; } } private static stringToCardinality ( cardinality ) { switch ( cardinality ) { case 'select_one' : return Cardinality . SELECT_ONE ; case 'select_multiple' : return Cardinality . SELECT_MULTIPLE ; default : throw Error ( `Unsupported cardinality ${ cardinality }` ) ; } } private static cardinalityToString ( cardinality ) { switch ( cardinality ) { case Cardinality . SELECT_ONE : return 'select_one' ; case Cardinality . SELECT_MULTIPLE : return 'select_multiple' ; default : throw Error ( `Unsupported cardinality ${ cardinality }` ) ; } } private static stringToFieldType ( fieldType ) { const type = FIELD_TYPE_STRINGS_BY_ENUM . get ( fieldType ) ; if ( ! type ) { throw new Error ( `Ignoring unsupported field of type: ${ fieldType }` ) ; } return type ; } private static fieldTypeToString ( fieldType ) { const str = FIELD_TYPE_ENUMS_BY_STRING . get ( fieldType ) ; if ( ! str ) { throw Error ( `Unsupported field type ${ fieldType }` ) ; } return str ; } param id param data private static toOption ( id , data ) { return new Option ( id , data . code , StringMap ( data . label ) , data . index || - 1 ) ; } private static optionToJS ( option ) { const { label , ... optionDoc } = option ; return { label : label . toJS ( ) , ... optionDoc , } ; } param id param data static toFeature ( id , data ) { try { if ( ! data . layerId ) { throw new Error ( 'Missing layer id' ) ; } const featureProperties = Map < string , string | number > ( keys ( data . properties ) . map ( ( property ) => [ property , data . properties [ property ] , ] ) ) ; if ( this . isLocationFeature ( data ) ) { return new LocationFeature ( id , data . layerId , data . location , featureProperties ) ; } if ( this . isGeoJsonFeature ( data ) ) { const geoJson = JSON . parse ( data . geoJson ) ; return new GeoJsonFeature ( id , data . layerId , geoJson , featureProperties ) ; } if ( this . isPolygonFeature ( data ) ) { return new PolygonFeature ( id , data . layerId , data . geometry . coordinates , featureProperties ) ; } throw new Error ( 'Missing location and geoJson' ) ; } catch ( err ) { console . error ( `Invalid feature ${ id }, ${ err }` ) ; } console . warn ( `Invalid feature ${ id } in remote data store ignored` ) ; return ; } param data static toObservation ( form , id , data ) { if ( data === undefined ) { throw Error ( `Observation ${ id } does not have document data.` ) ; } return new Observation ( id , data . featureId , data . layerId , form , FirebaseDataConverter . toAuditInfo ( data . created ) , FirebaseDataConverter . toAuditInfo ( data . lastModified ) , Map < string , Response > ( keys ( data . responses ) . map ( ( fieldId ) => [ fieldId as string , FirebaseDataConverter . toResponse ( form , fieldId , data . responses [ fieldId ] ) , ] ) ) ) ; } static observationToJS ( observation ) { return { featureId : observation . featureId , layerId : observation . layerId , formId : observation . form ?. id , created : FirebaseDataConverter . auditInfoToJs ( observation . created ) , lastModified : FirebaseDataConverter . auditInfoToJs ( observation . lastModified ) , responses : FirebaseDataConverter . responsesToJS ( observation . responses ) , } ; } static toUser ( data ) { if ( ! data ) { return ; } return new User ( data . id , data . email , data . isAuthenticated || true , data . displayName , data . photoURL ) ; } private static responsesToJS ( responses ) { return responses . entrySeq ( ) . reduce ( ( obj , [ fieldId , response ] ) => ( { ... obj , [ fieldId ] : FirebaseDataConverter . responseToJS ( response ) , } ) , { } ) ; } private static toResponse ( form , fieldID , responseValue ) { if ( typeof responseValue === 'string' ) { return new Response ( responseValue as string ) ; } if ( typeof responseValue === 'number' ) { return new Response ( responseValue as number ) ; } if ( responseValue instanceof Array ) { return new Response ( List ( responseValue . map ( optionId => form . getMultipleChoiceFieldOption ( fieldID , optionId ) ) ) ) ; } throw Error ( `Unknown value type ${ typeof responseValue }` ) ; } private static responseToJS ( response ) { if ( typeof response . value === 'string' ) { return response . value ; } if ( typeof response . value === 'number' ) { return response . value ; } if ( response . value instanceof List ) { return ( response . value as List < Option > ) . map ( option => option . id ) . toArray ( ) ; } throw Error ( `Unknown value type of ${ response . value }` ) ; } param data private static toAuditInfo ( data ) { return new AuditInfo ( data . user , data . clientTimestamp ?. toDate ( ) , data . serverTimestamp ?. toDate ( ) ) ; } private static auditInfoToJs ( auditInfo ) { return { user : FirebaseDataConverter . userToJs ( auditInfo . user ) , clientTimestamp : auditInfo . clientTime , serverTimestamp : auditInfo . serverTime , } ; } private static userToJs ( user ) { return { id : user . id , email : user . email , displayName : user . displayName , } ; } static aclToJs ( acl ) { return acl . map ( FirebaseDataConverter . toRoleId ) . toJS ( ) ; } param role static toRoleId ( role ) { return Role [ role ] . toLowerCase ( ) ; } private static isLocationFeature ( data ) { return data ?. location ?. latitude && data ?. location ?. longitude ; } private static isGeoJsonFeature ( data ) { return data ?. geoJson ; } private static isPolygonFeature ( data ) { return data ?. geometry ; } }