/* eslint-disable no-restricted-syntax */ /* eslint-disable no-console */ import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {forkJoin} from 'rxjs'; import {HsDialogContainerService} from 'hslayers-ng'; import {HsToastService} from 'hslayers-ng'; import {HsUtilsService} from 'hslayers-ng'; import attractivenessConfig from '../attractiveness.config.json'; import clusteringMethods from '../data/clustering_methods.json'; import {AdjusterEventService} from './adjuster-event.service'; import {AdjusterLegendService} from './adjuster-legend.service'; import {AdjusterPresetsService, Factor} from './adjuster-presets.service'; import { AttractivenessClustersService, MethodDescription, } from './attractiveness-clusters.service'; import {AttractivenessIndexService} from './attractiveness-index.service'; import {RDFSubject} from './ontology.model'; import {nuts3IndexSource} from '../app.config'; @Injectable({providedIn: 'root'}) export class AdjusterService { /** To be read from a config file */ allowedClusteringMethods: Array; /** To be read from a config file */ initialWeights: Record; /** To be read from a config file */ serviceBaseUrl: string; /** Used in the UI as a selector */ allowClusters = true; /** Used in the UI as a selector */ allowIndex = false; factors: Array = []; layersReady = new Set(); //clusters = []; //method: string; methods: Array; numberOfClusters: number; private _clusteringInProcess: boolean; private _clustersLoaded: boolean; /** Once instantiated, the load is definitely in process */ private _loadInProcess = true; private _raiInProcess: boolean; constructor( public adjusterEventService: AdjusterEventService, public adjusterLegendService: AdjusterLegendService, public adjusterPresetsService: AdjusterPresetsService, public attractivenessIndexService: AttractivenessIndexService, public attractivenessClustersService: AttractivenessClustersService, public hsDialogContainerService: HsDialogContainerService, public hsToastService: HsToastService, public hsUtilsService: HsUtilsService, public httpClient: HttpClient ) { // First safely set configurable properties this.allowedClusteringMethods = attractivenessConfig?.allowedClusteringMethods ?? []; this.initialWeights = attractivenessConfig?.initialWeights ?? {}; this.serviceBaseUrl = this.adjusterPresetsService.serviceBaseUrl; this.methods = clusteringMethods.filter((m) => this.allowedClusteringMethods.includes(m.codename) ); this.numberOfClusters = 12; /* Get the ontology file from the service */ this.loadOntology(); /* Wait for all layers to be ready */ this.adjusterEventService.layerReady.subscribe(({name}) => { console.log(name + ' ready!'); this.layersReady.add(name); /* Layers for each method + layer for index are ready */ if (this.layersReady.size == this.methods.length + 1) { this.adjusterEventService.layerReady.complete(); } }); /* Ensure that all layers, the loader component and the presets from ontology are ready */ forkJoin({ lyr: this.adjusterEventService.layerReady, load: this.adjusterEventService.loaderReady, ont: this.adjusterEventService.ontologyLoads, }).subscribe(() => { console.log('Oll layers Korekt! Initializing adjuster...'); //this._loadInProcess = false; this.init(); }); /* Listen to schema changes so the factors can be re-arranged in the view */ this.adjusterPresetsService.schemaChanges.subscribe((newSchema) => { const orphanedDatasets = []; this.factors = newSchema.groups.map((group) => { const datasets = this.adjusterPresetsService.getGroupDatasets(group.id); if (datasets.length < 3) { orphanedDatasets.push(...datasets); } return { id: group.id, labels: group.labels, weight: this.resetFactorWeights(group.id), datasets: this.adjusterPresetsService.getGroupDatasets(group.id), }; }); this.factors = this.factors.filter( (factor) => factor.datasets.length >= 3 ); this.factors.push({ id: 'others', labels: [ {'@value': 'Ostatní', '@language': 'cs'}, {'@value': 'Others', '@language': 'en'}, ], weight: this.resetFactorWeights('others'), datasets: orphanedDatasets, }); this.adjusterLegendService.updateIndexLayerPopUps(this.factors); }); /* Listen to problem changes so the datasets can be turned on/off */ this.adjusterPresetsService.problemChanges.subscribe((newProblem) => { if (!newProblem) { return; } for (const factor of this.factors) { for (const dataset of factor.datasets) { dataset.included = false; if (newProblem.requiredDatasets.includes(dataset.id)) { dataset.included = true; } } } }); } /** * Sends a request to polirural-attractiveness-service * and applies the returned values */ apply(): void { if (this.allowIndex) { this.calculateIndex(); } if (this.allowClusters) { this.calculateClusters(); } } async calculateIndex(): Promise { this._raiInProcess = true; let attractivenessData: any; try { attractivenessData = await this.httpClient .post(this.serviceBaseUrl + 'eu/scores/', { factors: this.factors.map((f) => { return { factor: f.id, weight: f.weight, datasets: f.datasets .filter((ds) => ds.included) .map((ds) => ds.id), }; }), }) .toPromise(); } catch (error) { this.hsToastService.createToastPopupMessage( 'Error loading data', `Error obtaining data from ${this.serviceBaseUrl}.` ); console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`); console.log(error); this._raiInProcess = false; this.adjusterEventService.loaded.next({ success: true, type: 'index', err: error, }); return; } // Spread the 'aggregate' value between 0 and 1 const min = attractivenessData.reduce((a, b) => a.aggregate < b.aggregate ? a : b ).aggregate; const max = attractivenessData.reduce((a, b) => a.aggregate > b.aggregate ? a : b ).aggregate; const coefficient = 1 / (max - min); const constant = -min * coefficient; attractivenessData.forEach((a) => { a.aggregate *= coefficient; a.aggregate += constant; }); // Store relation between region and its data in a hash-table-like structure // more memory consuming, but faster then find() const codeRecordRelations = {}; attractivenessData.forEach((a) => { codeRecordRelations[a.code.toUpperCase()] = a; }); console.time('forEach-Index'); this.attractivenessIndexService.processIndex( nuts3IndexSource, codeRecordRelations ); console.timeEnd('forEach-Index'); this._raiInProcess = false; this.adjusterEventService.loaded.next({ success: true, type: 'index', }); } async calculateClusters(): Promise { this._clusteringInProcess = true; /* Pre-process the API params */ const params = []; for (const factor of this.factors) { for (const dataset of factor.datasets) { if (!dataset.included) { continue; } const flattenedDataset = { id: dataset.id.split('/').slice(-1).pop(), //We do not need full URIs as the URNs are unique across the ontology factor: factor.id.split('/').slice(-1).pop(), //We do not need full URIs as the URNs are unique across the ontology weight: factor.weight, }; params.push(flattenedDataset); } } let data: any; try { data = await this.httpClient .post(this.serviceBaseUrl + 'eu/clusters/', { numberOfClusters: this.numberOfClusters, datasets: params, }) .toPromise(); } catch (error) { this.hsToastService.createToastPopupMessage( 'Error loading data', `Error obtaining data from ${this.serviceBaseUrl}.` ); console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`); console.log(error); this._clusteringInProcess = false; this.adjusterEventService.loaded.next({ success: false, type: 'clusters', err: error, }); return; } const clusterData = data.response; // Store relation between region and its data in a hash-table-like structure // more memory consuming, but much faster then find() const codeRecordRelations = {}; clusterData.forEach((c) => { codeRecordRelations[c['nuts_id'].toUpperCase()] = c; }); console.time('forEach-Cluster'); /*const clusters = []; for (const region of clusterData) { if (!clusters.includes(region[this.method])) { clusters.push(region[this.method]); } }*/ for (const method of this.methods) { this.attractivenessClustersService.processClusters( method, codeRecordRelations ); // Fake the legend (method.layer.getSource() as any).legend_categories = this.adjusterLegendService.createClusterLegend(this.numberOfClusters); } console.timeEnd('forEach-Cluster'); /*let max = 0; this.clusters.forEach((a) => { if (a.aggregate > max) { max = a.aggregate; } }); const normalizer = 1 / max; this.attractivity.forEach((a) => { a.aggregate *= normalizer; }); this.attractivity.forEach((a) => { this.nutsCodeRecordRelations[a.code] = a; });*/ // Fake the legend //nuts.nuts3ClustersSource.legend_categories = // this.adjusterLegendService.createClusterLegend(this.numberOfClusters); this._clustersLoaded = true; this._clusteringInProcess = false; this.adjusterEventService.loaded.next({ success: true, type: 'clusters', }); } /** * On init, calculate both index and clusters nevertheless if they are enabled or disabled in the UI */ async init(): Promise { this.calculateIndex(); this.calculateClusters(); // In HSL 2.5, setting layer greyscale breaks the print() functionality //this.hsLayerManagerService.setGreyscale(osmLayer); } async loadOntology() { try { const onto = await this.httpClient .get(this.serviceBaseUrl + 'ontology/') .toPromise(); this.adjusterEventService.ontologyLoads.next(onto); this.adjusterEventService.ontologyLoads.complete(); } catch (error) { this.hsToastService.createToastPopupMessage( 'Error loading ontology', `Web service at ${this.serviceBaseUrl} unavailable!` ); console.warn(`Web service at ${this.serviceBaseUrl} unavailable!`); console.log(error); this._loadInProcess = false; } this._loadInProcess = false; } resetFactorWeights(factorId: string): number { return this.initialWeights[factorId] ?? 0.5; } clustersLoaded(): boolean { return this._clustersLoaded; } /** * @returns {boolean} true if clustering or index processing is in process or loading data, false otherwise */ isInProcess(): boolean { return ( this._loadInProcess || this._clusteringInProcess || this._raiInProcess ); } isLoading(): boolean { return this._loadInProcess; } isClustering(): boolean { return this._clusteringInProcess; } isCalculatingRAI(): boolean { return this._raiInProcess; } }