| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- 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 {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 {obce, obceIndexLayer, osmLayer} from '../app.config';
- import {RDFSubject} from './ontology.model';
- @Injectable({providedIn: 'root'})
- export class AdjusterService {
- /** To be read from a config file */
- allowedClusteringMethods: Array<string>;
- /** To be read from a config file */
- initialWeights: {[factorId: string]: number};
- /** 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: Array<Factor> = [];
- layersReady = new Set();
- //method: string;
- methods: Array<MethodDescription>;
- 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 hsConfig: HsConfig,
- public hsEventBus: HsEventBusService,
- //public hsLayerMetadataService: HsLayerManagerMetadataService,
- public hsLayerManagerService: HsLayerManagerService,
- public hsToastService: HsToastService,
- 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;
- /* 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) => {
- //TODO: i18n this!
- this.factors = newSchema.groups.map((group) => {
- return {
- id: group.id,
- labels: group.labels,
- weight: this.resetFactorWeights(group.id),
- datasets: this.adjusterPresetsService.getGroupDatasets(group.id)
- }
- });
- });
- /* 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();
- }
- }
- calculateIndex(): void {
- this._raiInProcess = true;
- this.$http
- .post(this.serviceBaseUrl + 'cz/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()
- .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) => {
- 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,
- });
- });
- }
- calculateClusters(): void {
- 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)
- }
- }
- this.$http
- .post(this.serviceBaseUrl + 'cz/clusters/', {
- numberOfClusters: this.numberOfClusters,
- datasets: params
- })
- .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<any> = 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) => {
- 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,
- });
- });
- }
- init(): void {
- this._loadInProcess = true;
- this.$http
- .get(this.serviceBaseUrl + 'cz/datasets/')
- .toPromise()
- .then((data: any) => {
- //console.log(data);
- /*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) => {
- this.hsToastService.createToastPopupMessage('Error loading data', `Web service at ${this.serviceBaseUrl} unavailable!`);
- console.warn(`Web service at ${this.serviceBaseUrl} unavailable!`);
- console.log(error);
- this._loadInProcess = false;
- });
- }
- async loadOntology() {
- try {
- const onto = await this.$http.get<RDFSubject[]>(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;
- }
- }
- processIndex(codeRecordRelations: Record<string, unknown>): 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<string, unknown>
- ): 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');
- }
- 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;
- }
- }
- type MethodDescription = {
- codename: string;
- layer?: VectorLayer<any>;
- name: string;
- type: string;
- };
|