import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {Vector as VectorLayer} from 'ol/layer'; import {forkJoin} from 'rxjs'; import {HsConfig} from 'hslayers-ng'; import {HsEventBusService} from 'hslayers-ng'; //import {HsLayerManagerMetadataService} from 'hslayers-ng/components/layermanager/layermanager-metadata.service'; import {HsLayerManagerService} 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 {obce, obceIndexLayer, osmLayer} 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; /** 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 = true; factors = []; layersReady = new Set(); //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 hsConfig: HsConfig, public hsEventBus: HsEventBusService, //public hsLayerMetadataService: HsLayerManagerMetadataService, public hsLayerManagerService: HsLayerManagerService, public hsUtilsService: HsUtilsService, public $http: HttpClient ) { /* First safely set configurable properties */ this.allowedClusteringMethods = attractivenessConfig?.allowedClusteringMethods ?? []; this.initialWeights = attractivenessConfig?.initialWeights ?? {}; this.serviceBaseUrl = attractivenessConfig?.serviceBaseUrl ?? 'https://publish.lesprojekt.cz/nodejs/'; // 'https://jmacura.ml/ws/' // 'http://localhost:3000/' this.methods = clusteringMethods.filter((m) => this.allowedClusteringMethods.includes(m.codename) ); this.numberOfClusters = 9; /* 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 and also the loader component are ready */ forkJoin({ lyr: this.adjusterEventService.layerReady, load: this.adjusterEventService.loaderReady, }).subscribe(() => { console.log('Oll layers Korekt! Initializing adjuster...'); this.init(); }); } /** * Sends a request to polirural-attractiveness-service * and applies the returned values */ apply(): void { if (this.allowIndex) { this.calculateIndex(); } if (this.allowClusters) { this.calculateClusters(); } } calculateIndex(): void { this._raiInProcess = true; this.$http .post(this.serviceBaseUrl + 'cz/scores/', { factors: this.factors.map((f) => { return { factor: f.name, weight: f.weight, datasets: f.datasets .filter((ds) => ds.included) .map((ds) => ds.name), }; }), }) .toPromise() .then((attractivenessData: any[]) => { /* 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] = a; }); console.time('forEach-Index'); this.processIndex(codeRecordRelations); console.timeEnd('forEach-Index'); this._raiInProcess = false; this.adjusterEventService.loaded.next({ success: true, type: 'index', }); }) .catch((error) => { console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`); console.log(error); this._raiInProcess = false; this.adjusterEventService.loaded.next({ success: true, type: 'index', err: error, }); }); } calculateClusters(): void { this._clusteringInProcess = true; this.$http .post(this.serviceBaseUrl + 'cz/clusters/', { numberOfClusters: this.numberOfClusters, factors: this.factors.map((f) => { return { factor: f.name, weight: f.weight, datasets: f.datasets .filter((ds) => ds.included) .map((ds) => ds.name), }; }), }) .toPromise() .then((data: any) => { 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.lau2] = c; }); /*let sublayers = []; const oldSublayers = obceLayer.get('Layer'); if (oldSublayers !== undefined && Array.isArray(oldSublayers)) { for (const sublyr of oldSublayers) { sublyr.getSource().clear(); } if (oldSublayers.length > this.numberOfClusters) { sublayers = oldSublayers.slice(0, this.numberOfClusters); } } let i = sublayers.length; while (sublayers.length < this.numberOfClusters) { const sublyr = new VectorLayer({ title: `Cluster ${i + 1}`, name: i + 1 + '', source: new VectorSource({}), }); sublayers.push(sublyr); sublyr.Title = `Cluster ${i + 1}`; sublyr.Name = `c${i + 1}`; i++; }*/ console.time('forEach-Cluster'); for (const method of this.methods) { this.processClusters(method, codeRecordRelations); method.layer.getSource().legend_categories = this.adjusterLegendService.createClusterLegend( this.numberOfClusters ); } // Another implementation of the loop above, yet too slow //const clusters = []; //const obceFeatures: Array = obce.getFeatures(); // Array of arrays /*const methodsFeatures = this.methods.map((m) => m.layer.getSource().getFeatures() ); for (const region of clusterData) { //if (!clusters.includes(region[this.method])) { // clusters.push(region[this.method]); //} for (const features of methodsFeatures) { const feature = features.find( // NOTE: Do NOT add triple equal sign! (f) => f.get('nationalCode') == region['lau2'] ); if (!feature) { if (errs < 20) { errs++; console.warn(`No feature matches region ${region['lau2']}`); console.log(region); } continue; } Object.keys(region).forEach(function (key, index) { if (key !== 'lau2') { feature.set(key, region[key], true); } }); } logs++; if (logs % 100 == 0) { console.log(`processed ${logs} items`); } //sublayers[region[this.method] - 1].getSource().addFeature(feature); }*/ //obceLayer.set('Layer', sublayers); //this.hsLayerMetadataService.fillMetadata(obceLayer); //console.log(sublayers[0].getSource().getFeatures()); console.timeEnd('forEach-Cluster'); //this.clusters = clusters; this._clustersLoaded = true; this._clusteringInProcess = false; this.adjusterEventService.loaded.next({ success: true, type: 'clusters', }); }) .catch((error) => { console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`); console.log(error); this._clusteringInProcess = false; this.adjusterEventService.loaded.next({ success: false, type: 'clusters', err: error, }); }); } init(): void { this._loadInProcess = true; this.$http .get(this.serviceBaseUrl + 'cz/datasets/') .toPromise() .then((data: any) => { this.factors = data.map((dataset) => { return { name: dataset.Factor, weight: this.initialWeights[dataset.Factor] ?? 1, datasets: [], }; }); this.factors = this.hsUtilsService.removeDuplicates( this.factors, 'name' ); this.factors.forEach((factor) => { factor.datasets = data .filter((ds) => ds.Factor === factor.name) .map((ds) => { return { name: ds.Name, desc: ds.Description, included: true, }; }); }); this._loadInProcess = false; this.apply(); // In HSL 2.5, setting layer greyscale breaks the print() functionality //this.hsLayerManagerService.setGreyscale(osmLayer); }) .catch((error) => { console.warn(`Web service at ${this.serviceBaseUrl} unavailable!`); console.log(error); this._loadInProcess = false; }); } processIndex(codeRecordRelations: Record): void { /*if (obce.getFeatures()?.length < 1) { obce.once('changefeature', () => this.processIndex(codeRecordRelations)); return; }*/ let errs = 0; //let logs = 0; obce.forEachFeature((feature) => { // Pair each feature with its attractivity data const featureData = codeRecordRelations[feature.get('nationalCode')]; if (!featureData) { if (errs < 20) { errs++; console.warn(`No data for feature ${feature.get('nationalCode')}`); console.log(feature); } return; } /*logs++; if (logs % 100 == 0) { console.log(`processed ${logs} items`); }*/ Object.keys(featureData).forEach((key, index) => { if (key !== 'lau2') { feature.set(key, featureData[key], true); //true stands for "silent" - important for performance! } }); }); // Since we are updating the features silently, we now have to refresh manually obce.getFeatures()[0].dispatchEvent('change'); } processClusters( method: MethodDescription, codeRecordRelations: Record ): void { /*if (method.layer?.getSource().getFeatures()?.length < 1) { method.layer .getSource() .once('changefeature', () => this.processClusters(method, codeRecordRelations) ); return; }*/ let errs = 0; //let logs = 0; method.layer.getSource().forEachFeature((feature) => { // Pair each feature with its clustering data const featureData = codeRecordRelations[feature.get('nationalCode')]; /*const featureData = clusterData.find( // NOTE: Do NOT add triple equal sign! (item) => item['lau2'] == feature.get('nationalCode') );*/ if (!featureData) { if (errs < 20) { errs++; console.warn(`No data for feature ${feature.get('nationalCode')}`); console.log(feature); } return; } /*logs++; if (logs % 100 == 0) { console.log(`processed ${logs} items`); }*/ feature.set(method.codename, featureData[method.codename], true); /*Object.keys(featureData).forEach(function (key, index) { if (key !== 'lau2') { feature.set(key, featureData[key], true); } });*/ }); // Since we are updating the features silently, we now have to refresh manually method.layer.getSource().getFeatures()[0].dispatchEvent('change'); } 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; } } type MethodDescription = { codename: string; layer?: VectorLayer; name: string; type: string; };