import { Injectable } from '@angular/core' import { UntypedFormGroup, UntypedFormControl } from '@angular/forms' import { Observable, BehaviorSubject, forkJoin, of as observableOf, Subject } from 'rxjs' import { map } from 'rxjs/operators' import { Apollo, gql } from 'apollo-angular' import { MapControlService } from '../map/map-control.service' import { LayerNode } from '../map/models' import { Tag, TagAction } from './info-tags/tags.service' import { ConfigService } from '../config.service' import { DataService } from '../_services/data.service' const getModelInfoQuery = gql` query modelInfo ($store: String!) { modelInfo(store: $store) { store modelName symbol values { name title unit chartType chartColor } actions { name icon formFields { name type } } formName formFields { name type } tagPlugins tagActions { domain key actions { plugin name link action roles } } downloaders { name icon } legend { key value } } } ` const getTagKeysQuery = gql` query tagKeyList { tagKeyList { keys } } ` const createTagMutation = gql` mutation createTag( $store: String! $id: String! $key: String! $value: String! ) { createTag( store: $store, id: $id, key: $key, value: $value ) { tags { key value } } } ` const createTagsMutations = gql` mutation createTags( $keys: [String]! $values: [String]! $stores: [String]! $ids: [[String]]! ) { createTags( keys: $keys, values: $values, stores: $stores, ids: $ids, ) { tags { key value } } } ` const getPlotParamsQuery = gql` query plotParams($store: String!, $id:String!, $value:String!) { plotParams(store: $store, id: $id, value: $value) { baseLines { name value color } bgShapes { name valueTop valueBottom color } barBase } } ` const getFeatureInfoQuery = gql` query featureInfo($store: String!, $id:String!) { featureInfo(store: $store, id: $id) { id itemName geoInfoItems { key value } surveyInfoItems { key value } infoItems { key value } categorizedInfoItems { name infoItems { key value } } tags { key value } graph files { name path } images { name path } externalRecordUrl } } ` const deleteTagQuery = gql` mutation deleteTag( $store: String! $id: String! $key: String! ) { deleteTag( store: $store, id: $id, key: $key, ) { tags { key value } } } ` const getTaggedFeaturesQuery = gql` query taggedFeatures($stores: [String], $ids: [[String]]) { taggedFeatures(stores: $stores, ids: $ids) { store taggedFeatures { id lon lat tags { key value } } } } ` const getTaggedStoresQuery = gql` query taggedStores($stores: [String]) { taggedStores(stores: $stores) { store taggedFeatures { id lon lat tags { key value } } } } ` const executeFeatureActionMutation=gql` mutation executeFeatureAction ( $store: String!, $id: String!, $action: String!, $value: String, ) { executeFeatureAction( store: $store, id: $id, action: $action, value: $value, ) { result } } ` export class TaggedFeature { constructor( public id: number, public lon: number, public lat: number, public tags: Tag[], ) {} } export class TaggedLayer { constructor( public store: string, public features: TaggedFeature[] ) {} } export class ModelAction { constructor( public name: string, public icon: string, public formFields: FormField[], ) {} /* execute(fullInfo: FullInfo) { // XXX: for downloads (reports) only // TODO: make actions generic (query, mutation, parameters...) window.open('/download/action/' + this.name + '/' + fullInfo.modelInfo.store + '/' + fullInfo.featureInfo.id) } */ } export class FormFieldInput { constructor( public name: string, public value: string, ) {} } export class FormField { constructor( public name: string, public type: string, public dflt?: string, public value?: string ) {} } export class ModelValue { constructor( public name: string, public title: string, public unit: string, public chartType: string = 'line', public chartColor: string = 'blue', ) {} } export class Downloader { constructor( public name: string, public icon: string, ) {} } export class LegendItem { constructor( public key: string, public value: string, ) {} } export class ModelInfo { constructor( public store: string, public modelName: string, public symbol: string, public values: ModelValue[], public actions: ModelAction[], public formName: string, public formFields: FormField[], public tagPlugins: String[], public tagActions: TagAction[], public downloaders: Downloader[], public legend: LegendItem[], ) {} getFormFields(formGroup: UntypedFormGroup, fullInfo: FullInfo): object[] { // Return the form fields and build the FormGroup controls accordingly let formFields = [] fullInfo.modelInfo.formFields.forEach( field => { let control = new UntypedFormControl(field.name) //, field.validator) formGroup.addControl(field.name, control) formFields.push(field) } ) return formFields } } export class PlotBaseLine { constructor( public name: string, public value: number, public color: string, ) {} } export class PlotBgShape { constructor( public name: string, public valueTop: number, public valueBottom: number, public color: string, ) {} } export class PlotParams { constructor( public baseLines: PlotBaseLine[] = [], public bgShapes: PlotBgShape[] = [], public barBase?: number ) {} } export class PlotDataParams { constructor( public data: Object, public comment: string, public params: PlotParams, ) {} } export class InfoItem { constructor( public key: string, public value: string, ) {} } export class InfoCategory { constructor( public name: string, public infoItems: InfoItem[], ) {} } export class Attachment { constructor( public name: string, public path: string, ) {} } export class Feature { constructor ( public store: string, public id: string, ) {} } export class FeatureWithField { constructor ( public store: string, public field: string, public value: string, ) {} } export class FeatureInfo { constructor( public id: string, public itemName: string, public geoInfoItems: InfoItem[], public surveyInfoItems: InfoItem[], public infoItems: InfoItem[], public categorizedInfoItems: InfoCategory[], public tags: Tag[], public graph?: string, public files?: Attachment[], public images?: Attachment[], public externalRecordUrl?: string ) {} openExternalRecord() { window.open(this.externalRecordUrl) } } export class FullInfo { constructor( public modelInfo: ModelInfo, public featureInfo: FeatureInfo, ) {} hasForm(): Boolean { return this.modelInfo.formFields.length > 0 } } @Injectable() export class InfoDataService { constructor( public configService: ConfigService, private apollo: Apollo, public mapControlService: MapControlService, protected dataService: DataService, ) {} public refresh = new Subject() public refresh$ = this.refresh.asObservable() public dataProviderService = new BehaviorSubject(undefined) public dataProviderService$ = this.dataProviderService.asObservable() public taggedFeaturesSelectionService = new BehaviorSubject([]) public taggedFeaturesSelectionService$ = this.taggedFeaturesSelectionService.asObservable() // taggedLayers: holds the tags for each feature, for each selected layer, for search public taggedLayers = new BehaviorSubject([]) public taggedLayers$ = this.taggedLayers.asObservable() getModelInfo(store: string): Observable { return this.apollo.query({ query: getModelInfoQuery, variables: { store: store, } }).pipe(map( res => { let info: Object = res['data']['modelInfo'] let values = (info['values'] || []).map( (value: Object) => new ModelValue( value['name'], value['title'], value['unit'], value['chartType'], value['chartColor'], ) ) return new ModelInfo( info['store'], info['modelName'], info['symbol'], values, info['actions'] ? info['actions'].map( action => new ModelAction( action['name'], action['icon'], action['formFields'].map( formField => new FormField( formField['name'], formField['type'], formField['dflt'] ) ) ) ): [], info['formName'], info['formFields'] ? info['formFields'].map( (formField: Object) => new FormField( formField['name'], formField['type'], ) ) : [], info['tagPlugins'], info['tagActions'] ? info['tagActions'].map( tagAction => new TagAction( // FIXME: set real data!!! '**name**', '**plugin_name**', //tagAction['key'], tagAction.actions[0]['action'], ['**role**'], '**link**', ) ) : [], info['downloaders'] ? info['downloaders'].map( downloader => new Downloader( downloader['name'], downloader['icon'], ) ) : [], info['legend'] ? info['legend'].map( legendItem => new LegendItem( legendItem['key'], legendItem['value'], ) ) : [], ) } )) } getPlotDataAndParams(store: string, id: string, value: string, resampling: string): Observable { return forkJoin([ this.dataService.getValues(store, +id, value, resampling), this.getPlotParams(store, id, value), ]).pipe(map( res => new PlotDataParams( res[0].body, res[0].headers['comment'], res[1], ) )) } getPlotParams(store: string, id: string, value: string): Observable { return this.apollo.query({ query: getPlotParamsQuery, variables: { store: store, id: id, value: value } }).pipe(map( info => info.data['plotParams'] ? new PlotParams( (info.data['plotParams']['baseLines'] || []).map( bl => new PlotBaseLine( bl['name'], bl['value'], bl['color'] ) ), (info.data['plotParams']['bgShapes'] || []).map( bl => new PlotBgShape( bl['name'], bl['valueTop'], bl['valueBottom'], bl['color'] ) ), info.data['plotParams']['barBase'] ) : new PlotParams() )) } getFeatureInfo(store: string, id: string): Observable { return this.apollo.query({ query: getFeatureInfoQuery, variables: { store: store, id: id } }).pipe(map( res => { const info = res['data']['featureInfo'] const geoInfoItems = info['geoInfoItems'].map(ii => new InfoItem(ii['key'], ii['value'])) const surveyInfoItems = info['surveyInfoItems'].map(ii => new InfoItem(ii['key'], ii['value'])) const infoItems = info['infoItems'].map(ii => new InfoItem(ii['key'], ii['value'])) const categorizedInfoItems = info['categorizedInfoItems'] && info['categorizedInfoItems'].map( ic => new InfoCategory( ic['name'], ic['infoItems'].map(ii => new InfoItem(ii['key'], ii['value'])) ) ) const tags = info['tags'].map(ii => new Tag(ii['key'], ii['value'])) return new FeatureInfo( info['id'], info['itemName'], geoInfoItems, surveyInfoItems, infoItems, categorizedInfoItems, tags, info['graph'] && info['graph'].replace(/width="\d+pt"/, '').replace(/height="\d+pt"/, ''), info['files'] && info['files'].map(att => new Attachment(att['name'], att['path'])), info['images'] && info['images'].map(att => new Attachment(att['name'], att['path'])), info['externalRecordUrl'] ) } )) } getFullInfo(feature: Feature): Observable { return forkJoin([ this.getModelInfo(feature.store), this.getFeatureInfo(feature.store, feature.id) ]).pipe(map( res => new FullInfo(res[0], res[1]) )) } public createTag(fullInfo: FullInfo, tag: any): Observable { let variables = { 'store': fullInfo.modelInfo.store, 'id': fullInfo.featureInfo.id, 'key': tag['key'], 'value': tag['value'], } return this.apollo.mutate({ mutation: createTagMutation, variables: variables, }).pipe(map( res => res['data']['createTag']['tags'].map( (tag: Object) => new Tag(tag['key'], tag['value'])) )) } public createTags(keys: String[], values: String[], source: Object): Observable { let variables = { 'keys': keys, 'values': values, 'stores': Object.keys(source), 'ids': Object.values(source).map(ids => Array.from(ids)), } return this.apollo.mutate({ mutation: createTagsMutations, variables: variables, }).pipe(map( res => { let tags = res['data']['createTags']['tags'] return tags.map( (tag: Object) => new Tag(tag['key'], tag['value'])) } )) } public deleteTag(tag: Tag | any, fullInfo?: FullInfo): Observable { // tag can be FeatureTree, but circular dependencies: // import { FeatureTree } from './info-selection/info-selection-tags.component' let variables: Object if (tag instanceof Tag) { variables = { 'store': fullInfo.modelInfo.store, 'id': fullInfo.featureInfo.id, 'key': tag.key, } } else { // FeatureTree variables = { 'store': tag.getStore(), 'id': tag.id, 'key': tag.getKey(), } } return this.apollo.mutate({ mutation: deleteTagQuery, variables: variables, }) } public getTagKeys(): Observable { return observableOf(this.configService.conf.value.map['tagKeys']) // This could be fetched from the server /* return this.apollo.query({ query: getTagKeysQuery }).pipe(map( res => { return res['data']['tagKeyList']['keys'] } )) */ } private _getTaggedLayers(taggedItems: Object[]): TaggedLayer[] { /* Convert tagged features to tagged layers, putting the tag records in the graphql data structure */ return taggedItems.map( layer => new TaggedLayer( layer['store'], layer['taggedFeatures'].map( feature => new TaggedFeature( feature['id'], feature['lon'], feature['lat'], feature['tags'].map( tag => new Tag(tag['key'], tag['value']) ) ) ) ) ) } public getTaggedFeatures(features: Object): Observable { let stores = Object.keys(features) if (stores.length == 0) { this.taggedFeaturesSelectionService.next([]) return observableOf([]) } let ids = Object.values(features).map(t => Array.from(t)) return this.apollo.query({ query: getTaggedFeaturesQuery, variables: { stores: stores, ids: ids } }).pipe(map( res => { let taggedLayers = this._getTaggedLayers(res['data']['taggedFeatures']) // Add features with no tag Object.entries(features).forEach( ([store, _features]) => { let taggedFeatures = taggedLayers.find(s => s.store==store) if (!taggedFeatures) { taggedFeatures = new TaggedLayer(store, []) taggedLayers.push(taggedFeatures) } let taggedFeaturesIds = taggedFeatures.features.map(tf => +tf.id) let featureIdsNoTag = Array(..._features).filter(x => !taggedFeaturesIds.includes(x)) featureIdsNoTag.forEach( id => { taggedFeatures.features.push(new TaggedFeature(id, undefined, undefined, [])) } ) } ) this.taggedFeaturesSelectionService.next(taggedLayers) return taggedLayers } )) } public getTaggedStores(stores: string[]): Observable { return this.apollo.query({ query: getTaggedStoresQuery, variables: { stores: stores, } }).pipe(map( res => this._getTaggedLayers(res['data']['taggedStores']) )) } public getTagsActionsStores(stores: string[]): Observable { return this.apollo.query({ query: getTaggedStoresQuery, variables: { stores: stores, } }).pipe(map( res => this._getTaggedLayers(res['data']['taggedStores']) )) } // Load tags for selected layers public getTaggedLayers(selectedLayers: LayerNode[], forceReload: boolean=true): Observable { let selectedLayersStores = selectedLayers.map( (l: LayerNode) => l.store ) // FIXME: this.taggedLayers should be really an array let untaggedLayers = forceReload ? selectedLayersStores : selectedLayersStores.filter( (x: string) => !Object.keys(this.taggedLayers).includes(x) ) if (untaggedLayers.length > 0) { return this.getTaggedStores(untaggedLayers).pipe(map( taggedLayers => { // Update the known taggedLayers for (let taggedLayer of taggedLayers) { // FIXME: this.taggedLayers should be really an array this.taggedLayers[taggedLayer.store] = taggedLayer } // Make sure that even layers without features with tags are remembered for (let taggedLayer of untaggedLayers) { if (!this.taggedLayers.hasOwnProperty(taggedLayer)) { this.taggedLayers[taggedLayer] = new TaggedLayer(taggedLayer, []) } } // Save to mapControlService behavior subject this.taggedLayers.next(taggedLayers) return taggedLayers } )) } else { return this.taggedLayers } } }