|
|
@@ -1,10 +1,11 @@
|
|
|
import {HttpClient} from '@angular/common/http';
|
|
|
import {Injectable} from '@angular/core';
|
|
|
+import {Vector as VectorLayer} from 'ol/layer';
|
|
|
|
|
|
import {HsDialogContainerService} from 'hslayers-ng/components/layout/dialogs/dialog-container.service';
|
|
|
import {HsUtilsService} from 'hslayers-ng/components/utils/utils.service';
|
|
|
|
|
|
-// import attractivity from '../Attractivity.json';
|
|
|
+import attractivenessConfig from '../attractiveness.config.json';
|
|
|
import clusteringMethods from '../data/clustering_methods.json';
|
|
|
import {AdjusterEventService} from './adjuster-event.service';
|
|
|
import {AdjusterLoaderComponent} from './adjuster-loader.component';
|
|
|
@@ -13,17 +14,25 @@ import {nuts} from '../nuts';
|
|
|
|
|
|
@Injectable({providedIn: 'root'})
|
|
|
export class AdjusterService {
|
|
|
+ /** To be read from a config file */
|
|
|
+ allowedClusteringMethods: Array<string>;
|
|
|
+ /** 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 = [];
|
|
|
clusters = [];
|
|
|
- numberOfClusters;
|
|
|
+ numberOfClusters: number;
|
|
|
method: string;
|
|
|
- methods: Array<{
|
|
|
- codename: string;
|
|
|
- name: string;
|
|
|
- type: string;
|
|
|
- }>;
|
|
|
+ methods: Array<MethodDescription>;
|
|
|
private _clusteringInProcess: boolean;
|
|
|
+ private _clustersLoaded: boolean;
|
|
|
+ private _loadInProcess: boolean;
|
|
|
+ private _raiInProcess: boolean;
|
|
|
|
|
|
constructor(
|
|
|
public adjusterEventService: AdjusterEventService,
|
|
|
@@ -31,12 +40,18 @@ export class AdjusterService {
|
|
|
public hsUtilsService: HsUtilsService,
|
|
|
public httpClient: HttpClient
|
|
|
) {
|
|
|
- this.serviceBaseUrl = 'https://jmacura.ml/ws/';
|
|
|
- //window.location.hostname === 'localhost'
|
|
|
- //? 'https://jmacura.ml/ws/' // 'http://localhost:3000/'
|
|
|
- //: 'https://publish.lesprojekt.cz/nodejs/';
|
|
|
- this.methods = clusteringMethods;
|
|
|
- this.method = 'haclustwd2';
|
|
|
+ // 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.method = 'haclustwd2'; //TODO: set in config/or not use at all?
|
|
|
this.numberOfClusters = 12;
|
|
|
}
|
|
|
|
|
|
@@ -45,26 +60,109 @@ export class AdjusterService {
|
|
|
* and applies the returned values
|
|
|
*/
|
|
|
apply(): void {
|
|
|
- this.hsDialogContainerService.create(AdjusterLoaderComponent, {});
|
|
|
- const f = () => {
|
|
|
- this._clusteringInProcess = true;
|
|
|
- this.httpClient
|
|
|
- .post(this.serviceBaseUrl + '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;
|
|
|
- /*let max = 0;
|
|
|
+ if (this.allowIndex) {
|
|
|
+ this.calculateIndex();
|
|
|
+ }
|
|
|
+ if (this.allowClusters) {
|
|
|
+ this.calculateClusters();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ calculateIndex(): void {
|
|
|
+ this._raiInProcess = true;
|
|
|
+ this.httpClient
|
|
|
+ .post(this.serviceBaseUrl + 'eu/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.httpClient
|
|
|
+ .post(this.serviceBaseUrl + 'eu/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['nuts_id']] = 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.processClusters(codeRecordRelations);
|
|
|
+ //TODO: method.layer.getSource().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;
|
|
|
@@ -77,53 +175,37 @@ export class AdjusterService {
|
|
|
this.attractivity.forEach((a) => {
|
|
|
this.nutsCodeRecordRelations[a.code] = a;
|
|
|
});*/
|
|
|
- nuts.nuts3Source.forEachFeature((feature) => {
|
|
|
- // Pair each feature with its clustering data
|
|
|
- const featureData = clusterData.find(
|
|
|
- (item) => item['nuts_id'] === feature.get('NUTS_ID')
|
|
|
- );
|
|
|
- if (!featureData) {
|
|
|
- console.warn(`No data for feature ${feature.get('NUTS_ID')}`);
|
|
|
- console.log(feature);
|
|
|
- return;
|
|
|
- }
|
|
|
- Object.keys(featureData).forEach(function (key, index) {
|
|
|
- if (key !== 'nuts_id') {
|
|
|
- feature.set(key, featureData[key]);
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
- const clusters = [];
|
|
|
- for (const region of clusterData) {
|
|
|
- if (!clusters.includes(region[this.method])) {
|
|
|
- clusters.push(region[this.method]);
|
|
|
- }
|
|
|
- }
|
|
|
- this.clusters = clusters;
|
|
|
- this._clusteringInProcess = false;
|
|
|
- this.adjusterEventService.clustersLoaded.next({success: true});
|
|
|
- })
|
|
|
- .catch((error) => {
|
|
|
- console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`);
|
|
|
- console.log(error);
|
|
|
- this._clusteringInProcess = false;
|
|
|
- this.adjusterEventService.clustersLoaded.next({
|
|
|
- success: false,
|
|
|
- err: error,
|
|
|
- });
|
|
|
+ this._clustersLoaded = true;
|
|
|
+ this._clusteringInProcess = false;
|
|
|
+ this.adjusterEventService.loaded.next({
|
|
|
+ success: true,
|
|
|
+ type: 'clusters',
|
|
|
});
|
|
|
- };
|
|
|
- this.hsUtilsService.debounce(f, 300, false, this)();
|
|
|
+ })
|
|
|
+ .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._clusteringInProcess = true;
|
|
|
+ this._loadInProcess = true;
|
|
|
this.httpClient
|
|
|
- .get(this.serviceBaseUrl + 'datasets')
|
|
|
+ .get(this.serviceBaseUrl + 'eu/datasets/')
|
|
|
.toPromise()
|
|
|
.then((data: any) => {
|
|
|
this.factors = data.map((dataset) => {
|
|
|
- return {name: dataset.Factor, weight: 1, datasets: []};
|
|
|
+ return {
|
|
|
+ name: dataset.Factor,
|
|
|
+ weight: this.initialWeights[dataset.Factor] ?? 1,
|
|
|
+ datasets: [],
|
|
|
+ };
|
|
|
});
|
|
|
this.factors = this.hsUtilsService.removeDuplicates(
|
|
|
this.factors,
|
|
|
@@ -140,23 +222,116 @@ export class AdjusterService {
|
|
|
};
|
|
|
});
|
|
|
});
|
|
|
+ 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._clusteringInProcess = false;
|
|
|
- this.adjusterEventService.clustersLoaded.next({
|
|
|
+ this._loadInProcess = false;
|
|
|
+ /*this.adjusterEventService.loaded.next({
|
|
|
success: false,
|
|
|
err: error,
|
|
|
- });
|
|
|
+ });*/
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ processIndex(codeRecordRelations: Record<string, unknown>): void {
|
|
|
+ /*if (obce.getFeatures()?.length < 1) {
|
|
|
+ obce.once('changefeature', () => this.processIndex(codeRecordRelations));
|
|
|
+ return;
|
|
|
+ }*/
|
|
|
+ let errs = 0;
|
|
|
+ //let logs = 0;
|
|
|
+ nuts.nuts3Source.forEachFeature((feature) => {
|
|
|
+ // Pair each feature with its attractivity data
|
|
|
+ const featureData = codeRecordRelations[feature.get('NUTS_ID')];
|
|
|
+ if (!featureData) {
|
|
|
+ if (errs < 20) {
|
|
|
+ errs++;
|
|
|
+ console.warn(`No data for feature ${feature.get('NUTS_ID')}`);
|
|
|
+ console.log(feature);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ /*logs++;
|
|
|
+ if (logs % 100 == 0) {
|
|
|
+ console.log(`processed ${logs} items`);
|
|
|
+ }*/
|
|
|
+ Object.keys(featureData).forEach((key, index) => {
|
|
|
+ if (key !== 'nuts_id') {
|
|
|
+ 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
|
|
|
+ nuts.nuts3Source.getFeatures()[0].dispatchEvent('change');
|
|
|
+ }
|
|
|
+
|
|
|
+ processClusters(codeRecordRelations: Record<string, unknown>): void {
|
|
|
+ let errs = 0;
|
|
|
+ //let logs = 0;
|
|
|
+ nuts.nuts3Source.forEachFeature((feature) => {
|
|
|
+ // Pair each feature with its clustering data
|
|
|
+ const featureData = codeRecordRelations[feature.get('NUTS_ID')];
|
|
|
+ /*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('NUTS_ID')}`);
|
|
|
+ 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 !== 'nuts_id') {
|
|
|
+ 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
|
|
|
+ nuts.nuts3Source.getFeatures()[0].dispatchEvent('change');
|
|
|
+ }
|
|
|
+
|
|
|
+ clustersLoaded(): boolean {
|
|
|
+ return this._clustersLoaded;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * @returns {boolean} true if clustering is in process, false otherwise
|
|
|
+ * @returns {boolean} true if clustering or index processing is in process or loading data, false otherwise
|
|
|
*/
|
|
|
- isClusteringInProcess(): boolean {
|
|
|
+ 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;
|
|
|
+};
|