Procházet zdrojové kódy

⬆️ align with CZ version part 2

note: This commit was slit from larger one so it can pass the Gogs limit
jmacura před 3 roky
rodič
revize
00ff54c683

+ 12 - 0
.browserslistrc

@@ -0,0 +1,12 @@
+# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
+# For additional information regarding the format and rule options, please see:
+# https://github.com/browserslist/browserslist#queries
+
+# You can see what browsers were selected by your queries by running:
+#   npx browserslist
+
+> 0.5%
+last 2 versions
+Firefox ESR
+not dead
+not IE 9-11 # For IE 9-11 support, remove 'not'.

+ 1 - 0
.editorconfig

@@ -6,6 +6,7 @@ root = true
 
 # Newline ending every file and set default charset
 [*]
+end_of_line = lf
 insert_final_newline = true
 charset = utf-8
 

+ 37 - 21
.eslintrc.json

@@ -3,29 +3,45 @@
     "browser": true,
     "es6": true
   },
-  "extends": [
-    "openlayers",
-    "plugin:@typescript-eslint/eslint-recommended",
-    "plugin:@typescript-eslint/recommended"
-  ],
-  "plugins": ["@typescript-eslint"],
-  "globals": {
-    "angular": "readonly"
-  },
-  "parser": "@typescript-eslint/parser",
-  "parserOptions": {
-    "ecmaVersion": 2018
-  },
-  "rules": {
-    "import/extensions": [
-      "error",
-      "ignorePackages",
-      {"js": "never", "ts": "never"}
-    ],
-    "@typescript-eslint/no-empty-function": ["error", {"allow": ["constructors"]}]
-  },
+  "globals": {},
   "overrides": [
     {
+      "files": ["*.ts"],
+      "extends": [
+        "openlayers",
+        "plugin:@typescript-eslint/eslint-recommended",
+        "plugin:@typescript-eslint/recommended",
+        "plugin:@angular-eslint/recommended",
+        "plugin:@angular-eslint/recommended--extra"
+      ],
+      "plugins": ["@typescript-eslint", "@angular-eslint"],
+      "parser": "@typescript-eslint/parser",
+      "parserOptions": {
+        "ecmaVersion":  2018
+      },
+      "rules": {
+        "import/extensions": [
+          "error",
+          "ignorePackages",
+          {"js": "never", "ts": "never"}
+        ],
+        "@typescript-eslint/no-empty-function": ["error", {"allow": ["constructors"]}]
+      }
+    },
+    {
+      "files": [
+        "*.html"
+      ],
+      "parser": "@angular-eslint/template-parser",
+      "extends": [
+        "plugin:@angular-eslint/template/recommended"
+      ],
+      "plugins": [
+        "@angular-eslint/template"
+      ],
+      "rules": {}
+    },
+    {
       "files": [
         "**/*.spec.js"
       ],

+ 133 - 0
angular.json

@@ -0,0 +1,133 @@
+{
+  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+  "version": 1,
+  "newProjectRoot": "projects",
+  "projects": {
+    "attractiveness": {
+      "projectType": "application",
+      "schematics": {
+        "@schematics/angular:component": {
+          "style": "sass"
+        }
+      },
+      "root": "",
+      "sourceRoot": "src",
+      "prefix": "",
+      "architect": {
+        "build": {
+          "builder": "@angular-builders/custom-webpack:browser",
+          "options": {
+            "customWebpackConfig": {
+              "path": "./webpack.config.js"
+            },
+            "outputPath": "build",
+            "index": "src/index.html",
+            "main": "src/main.ts",
+            "polyfills": "src/polyfills.ts",
+            "tsConfig": "tsconfig.app.json",
+            "assets": [
+              "src/images/cropped-favicon-32x32.png",
+              "src/assets",
+              {
+                "glob": "**/*",
+                "input": "./node_modules/hslayers-ng/src/assets",
+                "output": "./assets/hslayers-ng/"
+              }
+            ],
+            "preserveSymlinks": true,
+            "styles": [
+              "./node_modules/hslayers-ng/css/hslayers-ng.css",
+              "./src/custom.scss"
+            ],
+            "scripts": [],
+            "vendorChunk": true,
+            "extractLicenses": false,
+            "buildOptimizer": false,
+            "sourceMap": true,
+            "optimization": false,
+            "namedChunks": true
+          },
+          "configurations": {
+            "production": {
+              "fileReplacements": [
+                {
+                  "replace": "environments/environment.ts",
+                  "with": "environments/environment.prod.ts"
+                }
+              ],
+              "optimization": true,
+              "outputHashing": "all",
+              "namedChunks": false,
+              "statsJson": true,
+              "extractLicenses": true,
+              "vendorChunk": false,
+              "buildOptimizer": true,
+              "budgets": [
+                {
+                  "type": "initial",
+                  "maximumWarning": "3mb",
+                  "maximumError": "5mb"
+                },
+                {
+                  "type": "anyComponentStyle",
+                  "maximumWarning": "60kb",
+                  "maximumError": "100kb"
+                }
+              ]
+            }
+          },
+          "defaultConfiguration": ""
+        },
+        "serve": {
+          "builder": "@angular-builders/custom-webpack:dev-server",
+          "options": {
+            "port": 8080,
+            "browserTarget": "attractiveness:build"
+          },
+          "configurations": {
+            "production": {
+              "browserTarget": "attractiveness:build:production"
+            }
+          }
+        },
+        "extract-i18n": {
+          "builder": "@angular-devkit/build-angular:extract-i18n",
+          "options": {
+            "browserTarget": "attractiveness:build"
+          }
+        },
+        "test": {
+          "builder": "@angular-devkit/build-angular:karma",
+          "options": {
+            "main": "test.ts",
+            "polyfills": "polyfills.ts",
+            "tsConfig": "tsconfig.spec.json",
+            "karmaConfig": "karma.conf.js",
+            "assets": [
+              "favicon.ico"
+            ],
+            "styles": [
+            ],
+            "scripts": []
+          }
+        },
+        "e2e": {
+          "builder": "@angular-devkit/build-angular:protractor",
+          "options": {
+            "protractorConfig": "e2e/protractor.conf.js",
+            "devServerTarget": "attractiveness:serve"
+          },
+          "configurations": {
+            "production": {
+              "devServerTarget": "attractiveness:serve:production"
+            }
+          }
+        }
+      }
+    }
+  },
+  "defaultProject": "attractiveness",
+  "cli": {
+    "analytics": false
+  }
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 28188 - 689
package-lock.json


+ 24 - 42
package.json

@@ -4,11 +4,13 @@
   "description": "Rural attractiveness application",
   "main": "build/index.html",
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1",
-    "build": "webpack --config ./webpack.prod.js --progress",
-    "build-dev": "webpack --config ./webpack.dev.js --progress",
-    "start": "http-server ./build -p 8080",
-    "start-dev": "webpack-dev-server --config ./webpack.dev.js --watch"
+    "build": "ng build --configuration production",
+    "build-dev": "ng build",
+    "dev": "npm run start-dev",
+    "serve": "npm run start-dev",
+    "start": "npx http-server ./build -p 8080",
+    "start-dev": "ng serve --configuration production --disable-host-check",
+    "test": "echo \"Error: no test specified\" && exit 1"
   },
   "repository": {
     "type": "git",
@@ -25,44 +27,24 @@
   "license": "MIT",
   "dependencies": {
     "csvtojson": "^2.0.10",
-    "hslayers-ng": "^2.5.0",
-    "hsv2rgb": "^1.1.0",
-    "http-server": "^0.12.3"
+    "hslayers-ng": "^7.0.4",
+    "hsv2rgb": "^1.1.0"
   },
   "devDependencies": {
-    "@babel/core": "^7.11.6",
-    "@babel/plugin-syntax-dynamic-import": "^7.8.3",
-    "@typescript-eslint/eslint-plugin": "^3.10.0",
-    "@typescript-eslint/parser": "^3.10.0",
-    "babel-loader": "^8.1.0",
-    "babel-plugin-angularjs-annotate": "^0.10.0",
-    "clean-webpack-plugin": "^3.0.0",
-    "css-loader": "^3.6.0",
-    "eslint": "^7.10.0",
-    "eslint-config-openlayers": "^14.0.0",
-    "eslint-plugin-import": "^2.22.1",
-    "eslint-plugin-node": "^11.1.0",
-    "eslint-plugin-promise": "^4.2.1",
-    "extract-loader": "^5.1.0",
-    "file-loader": "^6.1.0",
-    "html-loader": "^1.3.1",
-    "html-webpack-plugin": "^4.5.0",
-    "mini-css-extract-plugin": "^0.11.3",
-    "ng-annotate-loader": "^0.7.0",
-    "ng-cache-loader": "0.0.26",
-    "npm-run-all": "^4.1.5",
-    "optimize-css-assets-webpack-plugin": "^5.0.4",
-    "postcss": "^8.1.1",
-    "postcss-prefix-selector": "^1.7.1",
-    "sass": "^1.32.7",
-    "sass-loader": "^10.1.1",
-    "style-loader": "^1.3.0",
-    "ts-loader": "^8.0.3",
-    "typescript": "^4.0.2",
-    "url-loader": "^4.1.0",
-    "webpack": "^4.44.2",
-    "webpack-cli": "^3.3.12",
-    "webpack-dev-server": "^3.11.0",
-    "webpack-merge": "^5.1.4"
+    "@angular-builders/custom-webpack": "^12.1.3",
+    "@angular-devkit/build-angular": "^12.2.13",
+    "@angular-eslint/builder": "^13.0.1",
+    "@angular-eslint/eslint-plugin": "^13.0.1",
+    "@angular-eslint/eslint-plugin-template": "^13.0.1",
+    "@angular-eslint/template-parser": "^13.0.1",
+    "@angular/cli": "^12.2.13",
+    "@types/karma-jasmine": "^4.0.2",
+    "@typescript-eslint/eslint-plugin": "^5.7.0",
+    "eslint": "^8.4.1",
+    "eslint-config-openlayers": "14.0.0",
+    "eslint-plugin-import": "^2.25.3",
+    "eslint-plugin-tsdoc": "^0.2.14",
+    "file-loader": "^6.2.0",
+    "karma-jasmine": "^4.0.1"
   }
 }

+ 4 - 0
src/adjuster/adjuster-event.service.ts

@@ -1,9 +1,13 @@
 import {Injectable} from '@angular/core';
+import {RDFSubject} from './ontology.model';
 import {Subject} from 'rxjs';
 
 @Injectable({providedIn: 'root'})
 export class AdjusterEventService {
+  layerReady: Subject<{name: string}> = new Subject();
   loaded: Subject<{success: boolean; type: string; err?}> = new Subject();
+  loaderReady: Subject<void> = new Subject();
   methodChanged: Subject<string> = new Subject();
+  ontologyLoads: Subject<Array<RDFSubject>> = new Subject();
   constructor() {}
 }

+ 69 - 28
src/adjuster/adjuster-legend.service.ts

@@ -1,6 +1,14 @@
 import hsv2rgb from 'hsv2rgb';
 import {Injectable} from '@angular/core';
 
+import {AdjusterPresetsService, Factor} from './adjuster-presets.service';
+import {decimal2prettyPerc, nuts3IndexLayer, perc2color} from '../app.config';
+
+type FakeLegend = Array<{
+  color: string;
+  name: string;
+}>;
+
 @Injectable({providedIn: 'root'})
 export class AdjusterLegendService {
   /** https://colorbrewer2.org/?type=qualitative&scheme=Paired&n=12 */
@@ -18,15 +26,18 @@ export class AdjusterLegendService {
     '#ffff99',
     '#b15928',
   ];
-  constructor() {}
+  serviceUrl: string;
+  constructor(public adjusterPresetsService: AdjusterPresetsService) {
+    this.serviceUrl = this.adjusterPresetsService.serviceBaseUrl + 'georeport/';
+  }
 
   createIndexLegend(): FakeLegend {
     return [
-      {color: this.perc2color(0), name: '0 %'},
-      {color: this.perc2color(0.25), name: '25 %'},
-      {color: this.perc2color(0.5), name: '50 %'},
-      {color: this.perc2color(0.75), name: '75 %'},
-      {color: this.perc2color(1), name: '100 %'},
+      {color: perc2color(0), name: '0 %'},
+      {color: perc2color(0.25), name: '25 %'},
+      {color: perc2color(0.5), name: '50 %'},
+      {color: perc2color(0.75), name: '75 %'},
+      {color: perc2color(1), name: '100 %'},
     ];
   }
 
@@ -40,27 +51,62 @@ export class AdjusterLegendService {
     return legend;
   }
 
+  updateIndexLayerPopUps(factors: Factor[]) {
+    const attrs = [
+      {attribute: 'CNTR_CODE', label: 'Country'},
+      {attribute: 'NUTS_NAME', label: 'Name'},
+      {
+        attribute: 'aggregate',
+        label: 'aggregated index',
+        displayFunction: decimal2prettyPerc,
+      },
+    ];
+    for (const factor of factors) {
+      attrs.push({
+        attribute: factor.id,
+        label: this.adjusterPresetsService.getLabelInCurrentLang(factor.labels),
+        displayFunction: decimal2prettyPerc,
+      });
+    }
+    attrs.push({
+      attribute: 'NUTS_ID',
+      label: 'Detailed report',
+      displayFunction: (x) => {
+        return `<a href="${this.serviceUrl}${x}" target="_blank">in a new page</a>.`;
+      },
+    });
+    nuts3IndexLayer.set('popUp', {
+      attributes: attrs,
+    });
+  }
+
+  //FIXME: how to do this??
+  updateClusterLayerPopUps() {
+    /*return {
+      popUp: {
+        attributes: [
+          {attribute: 'CNTR_CODE', label: 'Country'},
+          {attribute: 'NUTS_NAME', label: 'Name'},
+          {
+            attribute: method.codename,
+            label: 'Cluster ID',
+          },
+          {
+            attribute: 'NUTS_ID',
+            label: 'Detailed report',
+            displayFunction: (x) => {
+              return `<a href="${this.serviceUrl}${x}" target="_blank">in a new page</a>.`;
+            },
+          },
+        ],
+      },
+    };*/
+  }
+
   refreshColorPalette(count: number): void {
     this.colorPalette = this.generateRandomColorPalette(count);
   }
 
-  perc2color = (perc: number): string => {
-    perc = perc * 100;
-    let r;
-    let g;
-    const b = 0;
-    if (perc < 50) {
-      r = 255;
-      g = Math.round(5.1 * perc);
-    } else {
-      g = 255;
-      r = Math.round(510 - 5.1 * perc);
-    }
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
-    const h = r * 0x10000 + g * 0x100 + b * 0x1;
-    return `rgba(${r}, ${g}, ${b}, 0.7)`;
-  };
-
   /**
    * https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
    * @private
@@ -84,8 +130,3 @@ export class AdjusterLegendService {
     //return `rgba(${r}, ${g}, ${b}, 0.7)`;
   }
 }
-
-type FakeLegend = Array<{
-  color: string;
-  name: string;
-}>;

+ 17 - 6
src/adjuster/adjuster-loader.component.ts

@@ -1,23 +1,34 @@
-import {Component, Input, ViewRef} from '@angular/core';
+import {AfterViewInit, Component, Input, ViewRef} from '@angular/core';
 
-import {HsDialogComponent} from 'hslayers-ng/components/layout/dialogs/dialog-component.interface';
-import {HsDialogContainerService} from 'hslayers-ng/components/layout/dialogs/dialog-container.service';
+import {HsDialogComponent} from 'hslayers-ng';
+import {HsDialogContainerService} from 'hslayers-ng';
 
+import {AdjusterEventService} from './adjuster-event.service';
 import {AdjusterService} from './adjuster.service';
 
 @Component({
   selector: 'pra-adjuster-loader',
-  template: require('./adjuster-loader.directive.html'),
+  templateUrl: './adjuster-loader.component.html',
+  styleUrls: ['./adjuster-loader.component.scss'],
 })
-export class AdjusterLoaderComponent implements HsDialogComponent {
+export class AdjusterLoaderComponent
+  implements HsDialogComponent, AfterViewInit
+{
   @Input() data;
   viewRef: ViewRef;
   constructor(
     public hsDialogContainerService: HsDialogContainerService,
-    public adjusterService: AdjusterService
+    public adjusterService: AdjusterService,
+    public adjusterEventService: AdjusterEventService
   ) {
     /*this.adjusterEventService.loaded.subscribe(() => {
       this.hsDialogContainerService.destroy(this);
     });*/
   }
+
+  ngAfterViewInit(): void {
+    // The next is necessary, otherwise the forkJoin() would hang up immediately
+    this.adjusterEventService.loaderReady.next();
+    this.adjusterEventService.loaderReady.complete();
+  }
 }

+ 26 - 17
src/adjuster/adjuster.component.ts

@@ -1,42 +1,51 @@
-import {Component, ViewRef} from '@angular/core';
+import {Component, OnInit, ViewRef} from '@angular/core';
 
-import {HsDialogContainerService} from 'hslayers-ng/components/layout/dialogs/dialog-container.service';
-import {HsLayoutService} from 'hslayers-ng/components/layout/layout.service';
-import {HsPanelComponent} from 'hslayers-ng/components/layout/panels/panel-component.interface';
+import {HsDialogContainerService} from 'hslayers-ng';
+import {HsLayoutService} from 'hslayers-ng';
+import {HsPanelComponent} from 'hslayers-ng';
 
-import {AdjusterEventService} from './adjuster-event.service';
 import {AdjusterLoaderComponent} from './adjuster-loader.component';
+import {AdjusterPresetsService} from './adjuster-presets.service';
 import {AdjusterService} from './adjuster.service';
 
 @Component({
   selector: 'pra-adjuster',
-  template: require('./adjuster.directive.html'),
+  templateUrl: './adjuster.component.html',
+  styleUrls: ['./adjuster.component.scss'],
 })
-export class AdjusterComponent implements HsPanelComponent {
+export class AdjusterComponent implements HsPanelComponent, OnInit {
+  name = 'adjuster';
   data: any;
-  descriptionVisible: boolean;
   errorMsg: string;
-  method: string;
+  //method: string;
+  showAdvancedOptions = false;
   viewRef: ViewRef;
 
   constructor(
     public adjusterService: AdjusterService,
-    public adjusterEventService: AdjusterEventService,
+    public adjusterPresetsService: AdjusterPresetsService,
     public hsDialogContainerService: HsDialogContainerService,
     public hsLayoutService: HsLayoutService
   ) {
-    this.descriptionVisible = false;
-    this.method = this.adjusterService.method;
-    this.adjusterEventService.loaded.subscribe(({success, err}) => {
+    //this.descriptionVisible = false;
+    //this.method = this.adjusterService.method;
+    /*this.adjusterEventService.loaded.subscribe(({success, err}) => {
       if (!success) {
         this.errorMsg = err.message;
       }
-    });
+    });*/
   }
 
   ngOnInit(): void {
     this.hsDialogContainerService.create(AdjusterLoaderComponent, {});
-    this.adjusterService.init();
+  }
+
+  hasDatasets(factor): boolean {
+    return factor.datasets.length > 0;
+  }
+
+  getLabelInCurrentLang(labels) {
+    return this.adjusterPresetsService.getLabelInCurrentLang(labels);
   }
 
   isVisible(): boolean {
@@ -66,8 +75,8 @@ export class AdjusterComponent implements HsPanelComponent {
     );
   }
 
-  selectMethod(): void {
+  /*selectMethod(): void {
     this.adjusterService.method = this.method;
     this.adjusterEventService.methodChanged.next(this.method);
-  }
+  }*/
 }

+ 10 - 4
src/adjuster/adjuster.module.ts

@@ -4,15 +4,17 @@ import {FormsModule} from '@angular/forms';
 import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
 import {TranslateModule} from '@ngx-translate/core';
 
-import {HsPanelHelpersModule} from 'hslayers-ng/components/layout/panels/panel-helpers.module';
+import {HsPanelHelpersModule} from 'hslayers-ng';
 
 import {AdjusterComponent} from './adjuster.component';
 import {AdjusterEventService} from './adjuster-event.service';
+import {AdjusterLegendService} from './adjuster-legend.service';
 import {AdjusterLoaderComponent} from './adjuster-loader.component';
 import {AdjusterService} from './adjuster.service';
 import {AttractivenessClustersService} from './attractiveness-clusters.service';
 import {AttractivenessIndexService} from './attractiveness-index.service';
-import { AdjusterLegendService } from './adjuster-legend.service';
+import {DatasetListComponent} from './dataset-list/dataset-list.component';
+import {MetadataDialogComponent} from './metadata-dialog/metadata-dialog.component';
 
 @NgModule({
   schemas: [CUSTOM_ELEMENTS_SCHEMA],
@@ -24,8 +26,12 @@ import { AdjusterLegendService } from './adjuster-legend.service';
     TranslateModule,
   ],
   exports: [AdjusterComponent],
-  declarations: [AdjusterComponent, AdjusterLoaderComponent],
-  entryComponents: [AdjusterComponent, AdjusterLoaderComponent],
+  declarations: [
+    AdjusterComponent,
+    AdjusterLoaderComponent,
+    DatasetListComponent,
+    MetadataDialogComponent,
+  ],
   providers: [
     AttractivenessClustersService,
     AttractivenessIndexService,

+ 165 - 42
src/adjuster/adjuster.service.ts

@@ -1,17 +1,24 @@
+/* eslint-disable no-restricted-syntax */
+/* eslint-disable no-console */
 import {HttpClient} from '@angular/common/http';
 import {Injectable} from '@angular/core';
-import {Vector as VectorLayer} from 'ol/layer';
+import {forkJoin} from 'rxjs';
 
-import {HsDialogContainerService} from 'hslayers-ng/components/layout/dialogs/dialog-container.service';
-import {HsUtilsService} from 'hslayers-ng/components/utils/utils.service';
+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 {AdjusterLoaderComponent} from './adjuster-loader.component';
-import {AttractivenessClustersService} from './attractiveness-clusters.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 {nuts} from '../nuts';
 
 @Injectable({providedIn: 'root'})
@@ -26,22 +33,26 @@ export class AdjusterService {
   allowClusters = true;
   /** Used in the UI as a selector */
   allowIndex = true;
-  factors = [];
-  clusters = [];
-  numberOfClusters: number;
-  method: string;
+  factors: Array<Factor> = [];
+  layersReady = new Set();
+  //clusters = [];
+  //method: string;
   methods: Array<MethodDescription>;
+  numberOfClusters: number;
   private _clusteringInProcess: boolean;
   private _clustersLoaded: boolean;
-  private _loadInProcess: 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
   ) {
@@ -49,15 +60,81 @@ export class AdjusterService {
     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.serviceBaseUrl = this.adjusterPresetsService.serviceBaseUrl;
+
     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;
+
+    /* 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;
+          }
+        }
+      }
+    });
   }
 
   /**
@@ -81,16 +158,20 @@ export class AdjusterService {
         .post(this.serviceBaseUrl + 'eu/scores/', {
           factors: this.factors.map((f) => {
             return {
-              factor: f.name,
+              factor: f.id,
               weight: f.weight,
               datasets: f.datasets
                 .filter((ds) => ds.included)
-                .map((ds) => ds.name),
+                .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;
@@ -120,7 +201,10 @@ export class AdjusterService {
       codeRecordRelations[a.code.toUpperCase()] = a;
     });
     console.time('forEach-Index');
-    this.attractivenessIndexService.processIndex(codeRecordRelations);
+    this.attractivenessIndexService.processIndex(
+      nuts.nuts3IndexSource,
+      codeRecordRelations
+    );
     console.timeEnd('forEach-Index');
     this._raiInProcess = false;
     this.adjusterEventService.loaded.next({
@@ -131,23 +215,34 @@ export class AdjusterService {
 
   async calculateClusters(): Promise<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);
+      }
+    }
     let data: any;
     try {
       data = await 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),
-            };
-          }),
+          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;
@@ -171,9 +266,15 @@ export class AdjusterService {
             clusters.push(region[this.method]);
           }
         }*/
-    //for (const method of this.methods) {
-    this.attractivenessClustersService.processClusters(codeRecordRelations);
-    //}
+    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) => {
@@ -189,9 +290,8 @@ export class AdjusterService {
           this.nutsCodeRecordRelations[a.code] = a;
         });*/
     // Fake the legend
-    nuts.nuts3ClustersSource.legend_categories = this.adjusterLegendService.createClusterLegend(
-      this.numberOfClusters
-    );
+    //nuts.nuts3ClustersSource.legend_categories =
+    //  this.adjusterLegendService.createClusterLegend(this.numberOfClusters);
     this._clustersLoaded = true;
     this._clusteringInProcess = false;
     this.adjusterEventService.loaded.next({
@@ -201,6 +301,7 @@ export class AdjusterService {
   }
 
   async init(): Promise<void> {
+    console.log('init');
     this._loadInProcess = true;
     let data: any;
     try {
@@ -208,6 +309,10 @@ export class AdjusterService {
         .get(this.serviceBaseUrl + 'eu/datasets/')
         .toPromise();
     } 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;
@@ -216,7 +321,7 @@ export class AdjusterService {
         err: error,
       });*/
     }
-    this.factors = data.map((dataset) => {
+    /*this.factors = data.map((dataset) => {
       return {
         name: dataset.Factor,
         weight: this.initialWeights[dataset.Factor] ?? 1,
@@ -234,13 +339,38 @@ export class AdjusterService {
             included: true,
           };
         });
-    });
+    });*/
+    console.log('init done, applying');
     this._loadInProcess = false;
     this.apply();
     // In HSL 2.5, setting layer greyscale breaks the print() functionality
     //this.hsLayerManagerService.setGreyscale(osmLayer);
   }
 
+  async loadOntology() {
+    console.log('loading onto');
+    try {
+      const onto = await this.httpClient
+        .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;
+    }
+    console.log('onto loaded');
+  }
+
+  resetFactorWeights(factorId: string): number {
+    return this.initialWeights[factorId] ?? 0.5;
+  }
+
   clustersLoaded(): boolean {
     return this._clustersLoaded;
   }
@@ -266,10 +396,3 @@ export class AdjusterService {
     return this._raiInProcess;
   }
 }
-
-type MethodDescription = {
-  codename: string;
-  layer?: VectorLayer;
-  name: string;
-  type: string;
-};

+ 19 - 7
src/adjuster/attractiveness-clusters.service.ts

@@ -1,14 +1,26 @@
+import {Geometry} from 'ol/geom';
 import {Injectable} from '@angular/core';
-import {nuts} from '../nuts';
+import {Vector as VectorLayer} from 'ol/layer';
+import {Vector as VectorSource} from 'ol/source';
+
+export type MethodDescription = {
+  codename: string;
+  layer?: VectorLayer<VectorSource<Geometry>>;
+  name: string;
+  type: string;
+};
 
 @Injectable({providedIn: 'root'})
 export class AttractivenessClustersService {
   constructor() {}
 
-  processClusters(codeRecordRelations: Record<string, unknown>): void {
+  processClusters(
+    method: MethodDescription,
+    codeRecordRelations: Record<string, unknown>
+  ): void {
     let errs = 0;
     //let logs = 0;
-    nuts.nuts3ClustersSource.forEachFeature((feature) => {
+    method.layer.getSource().forEachFeature((feature) => {
       // Pair each feature with its clustering data
       const featureData =
         codeRecordRelations[feature.get('NUTS_ID').toUpperCase()];
@@ -30,14 +42,14 @@ export class AttractivenessClustersService {
       if (logs % 100 == 0) {
         console.log(`processed ${logs} items`);
       }*/
-      //feature.set(method.codename, featureData[method.codename], true);
-      Object.keys(featureData).forEach(function (key, index) {
+      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.nuts3ClustersSource.getFeatures()[0].dispatchEvent('change');
+    method.layer.getSource().getFeatures()[0].dispatchEvent('change');
   }
 }

+ 8 - 4
src/adjuster/attractiveness-index.service.ts

@@ -1,18 +1,22 @@
+import {Geometry} from 'ol/geom';
 import {Injectable} from '@angular/core';
-import {nuts} from '../nuts';
+import {Vector as VectorSource} from 'ol/source';
 
 @Injectable({providedIn: 'root'})
 export class AttractivenessIndexService {
   constructor() {}
 
-  processIndex(codeRecordRelations: Record<string, unknown>): void {
+  processIndex(
+    indexSource: VectorSource<Geometry>,
+    codeRecordRelations: Record<string, unknown>
+  ): void {
     /*if (obce.getFeatures()?.length < 1) {
       obce.once('changefeature', () => this.processIndex(codeRecordRelations));
       return;
     }*/
     let errs = 0;
     //let logs = 0;
-    nuts.nuts3IndexSource.forEachFeature((feature) => {
+    indexSource.forEachFeature((feature) => {
       // Pair each feature with its attractivity data
       const featureData =
         codeRecordRelations[feature.get('NUTS_ID').toUpperCase()];
@@ -37,6 +41,6 @@ export class AttractivenessIndexService {
       });
     });
     // Since we are updating the features silently, we now have to refresh manually
-    nuts.nuts3IndexSource.getFeatures()[0].dispatchEvent('change');
+    indexSource.getFeatures()[0].dispatchEvent('change');
   }
 }

+ 2 - 21
src/adjuster/index.ts

@@ -1,23 +1,4 @@
-import * as angular from 'angular';
-import {downgrade} from 'hslayers-ng/common/downgrader';
-import {downgradeComponent, downgradeInjectable} from '@angular/upgrade/static';
-
-import {AdjusterComponent} from './adjuster.component';
-//import {AdjusterLoaderComponent} from './adjuster-loader.component';
-import {AdjusterModule} from './adjuster.module';
-import {AdjusterService} from './adjuster.service';
-
-export const downgradedModule = downgrade(AdjusterModule);
-
-angular
-  .module(downgradedModule, ['hs.core', 'hs.map'])
-  .service('AdjusterService', downgradeInjectable(AdjusterService))
-  .directive('praAdjuster', downgradeComponent({component: AdjusterComponent}));
-/*.directive(
-    'praAdjusterLoader',
-    downgradeComponent({component: AdjusterLoaderComponent})
-  );*/
-
-angular.module('pra.adjuster', [downgradedModule]);
+export {AdjusterComponent} from './adjuster.component';
+export {AdjusterService} from './adjuster.service';
 
 export {AdjusterModule} from './adjuster.module';

+ 20 - 14
src/app.component.ts

@@ -1,14 +1,20 @@
-export const AppComponent = {
-  template: (HsCore) => {
-    'ngInject';
-    return HsCore.hslayersNgTemplate;
-  },
-  bindings: {},
-  controllerAs: 'vm',
-  controller: class AppComponent {
-    constructor(HsLayoutService, AdjusterService, AppService) {
-      'ngInject';
-      const vm = this;
-    }
-  },
-};
+import {Component} from '@angular/core';
+
+import {AppConfig} from './app.config';
+import {AppService} from './app.service';
+import {HsConfig, HsLanguageService} from 'hslayers-ng';
+
+@Component({
+  selector: 'app-component',
+  templateUrl: './app.component.html',
+  styleUrls: ['./custom.scss'],
+})
+export class AppComponent {
+  constructor(
+    public appService: AppService,
+    //public hsConfig: HsConfig,
+    public hsLangService: HsLanguageService
+  ) {
+    //this.hsConfig.update(AppConfig);
+  }
+}

+ 94 - 13
src/app.config.ts

@@ -1,9 +1,12 @@
-import {OSM} from 'ol/source';
-import {Tile} from 'ol/layer';
+import {Fill, Stroke, Style} from 'ol/style';
+import {OSM, Vector as VectorSource} from 'ol/source';
+import {Tile, Vector as VectorLayer} from 'ol/layer';
+import {TopoJSON} from 'ol/format';
 import {View} from 'ol';
 
 import env from './env.config.json';
 import i18n from './translations.json';
+import {nuts} from './nuts';
 
 function getHostname() {
   const url = window.location.href;
@@ -12,43 +15,121 @@ function getHostname() {
   return urlArr[0] + '//' + domain;
 }
 
+export const perc2color = (perc: number): string => {
+  perc = perc * 100;
+  let r;
+  let g;
+  const b = 0;
+  if (perc < 50) {
+    r = 255;
+    g = Math.round(5.1 * perc);
+  } else {
+    g = 255;
+    r = Math.round(510 - 5.1 * perc);
+  }
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const h = r * 0x10000 + g * 0x100 + b * 0x1;
+  return `rgba(${r}, ${g}, ${b}, 0.7)`;
+};
+
+export const decimal2prettyPerc = (x) => {
+  return `${(x * 100).toFixed(2)}&nbsp;%`;
+};
+
+const indexStyle = (feature) => {
+  if (isNaN(feature.get('aggregate'))) {
+    return [
+      new Style({
+        fill: new Fill({
+          color: '#FFF',
+        }),
+        stroke: new Stroke({
+          color: '#3399CC',
+          width: 0.25,
+        }),
+      }),
+    ];
+  } else {
+    return [
+      new Style({
+        fill: new Fill({
+          color: perc2color(feature.get('aggregate')),
+        }),
+        stroke: new Stroke({
+          color: '#FFFFFF',
+          width: 0.15,
+        }),
+      }),
+    ];
+  }
+};
+
+export const nuts2style = new Style({
+  stroke: new Stroke({
+    color: '#000000',
+    width: 0.5,
+  }),
+});
+
+export const nuts2Layer = new VectorLayer({
+  properties: {
+    title: 'NUTS2 regions',
+    editor: {editable: false},
+  },
+  source: nuts.nuts2Source,
+  visible: false,
+  style: nuts2style,
+});
+
+export const nuts3IndexLayer = new VectorLayer({
+  properties: {
+    title: 'NUTS3 regions: Rural attractiveness index',
+    editor: {editable: false},
+    queryable: false,
+  },
+  source: nuts.nuts3IndexSource,
+  visible: true,
+  style: indexStyle,
+});
+
 export const AppConfig = {
-  //useProxy: false,
-  proxyPrefix: '/proxy/',
+  assetsPath: 'assets/hslayers-ng',
+  useProxy: false,
+  //proxyPrefix: '/proxy/',
   geonamesUser: env.geonamesUser,
   default_layers: [
     new Tile({
+      properties: {
+        title: 'OpenStreetMap',
+        base: true,
+        removable: false,
+      },
       source: new OSM({
-        url:
-          'https://cartodb-basemaps-{a-d}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
+        url: 'https://cartodb-basemaps-{a-d}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
         attributions: [
           '© <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> contributors,',
           '<a href="https://carto.com/attributions">CARTO</a>',
         ],
       }),
-      title: 'OpenStreetMap',
-      base: true,
-      removable: false,
     }),
   ],
-  popUpDisplay: 'hover',
+  popUpDisplay: 'hover' as const,
   queryPoint: 'hidden',
   project_name: 'erra/map',
   default_view: new View({
     center: [2433348.3022471312, 7744501.813885343],
     zoom: 3.6,
-    units: 'm',
   }),
   advanced_form: true,
   datasources: [
-    {
+    /*  {
       title: 'Metadata catalogue',
       url: 'https://polirural-cat.lesprojekt.cz/micka2/csw',
       language: 'eng',
       type: 'micka',
       code_list_url:
         'https://polirural-cat.lesprojekt.cz/micka2/util/codelists.php?_dc=1440156028103&language=eng&page=1&start=0&limit=25&filter=%5B%7B%22property%22%3A%22label%22%7D%5D',
-    },
+    },*/
   ],
   hostname: {
     default: {

+ 8 - 48
src/app.module.ts

@@ -1,56 +1,16 @@
-import 'core-js/es6/reflect';
-import 'core-js/es7/reflect';
-import 'reflect-metadata';
-import 'zone.js';
-import app from './app-js';
-import {
-  APP_BOOTSTRAP_LISTENER,
-  ApplicationRef,
-  ComponentRef,
-  NgModule,
-} from '@angular/core';
 import {BrowserModule} from '@angular/platform-browser';
-import {TranslateModule} from '@ngx-translate/core';
-import {UpgradeModule} from '@angular/upgrade/static';
+import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core';
 
-import {BootstrapComponent} from 'hslayers-ng/bootstrap.component';
-import {HsCoreModule} from 'hslayers-ng/components/core/core.module';
+import {HslayersModule} from 'hslayers-ng';
 
 import {AdjusterModule} from './adjuster';
-import {AppService} from './app.service';
+import {AppComponent} from './app.component';
 
 @NgModule({
-  imports: [
-    BrowserModule,
-    UpgradeModule,
-    HsCoreModule,
-    AdjusterModule,
-    TranslateModule,
-  ],
+  schemas: [CUSTOM_ELEMENTS_SCHEMA],
+  imports: [BrowserModule, HslayersModule, AdjusterModule],
+  declarations: [AppComponent],
   exports: [],
-  declarations: [],
-  providers: [
-    {
-      provide: APP_BOOTSTRAP_LISTENER,
-      multi: true,
-      useFactory: () => {
-        return (component: ComponentRef<BootstrapComponent>) => {
-          //When ng9 part is bootstrapped continue with AngularJs modules
-          component.instance.upgrade.bootstrap(
-            document.documentElement,
-            [app.name],
-            {strictDi: true}
-          );
-        };
-      },
-    },
-    AppService,
-  ],
+  bootstrap: [AppComponent],
 })
-export class AppModule {
-  constructor() {}
-  ngDoBootstrap(appRef: ApplicationRef): void {
-    //First bootstrap Angular 9 app part on hs element
-    appRef.bootstrap(BootstrapComponent);
-  }
-}
+export class AppModule {}

+ 134 - 153
src/app.service.ts

@@ -4,52 +4,27 @@ import {Injectable} from '@angular/core';
 import {Feature} from 'ol';
 import {Fill, Stroke, Style} from 'ol/style';
 import {GeoJSON} from 'ol/format';
+import {Geometry} from 'ol/geom';
 import {Vector as VectorLayer} from 'ol/layer';
 import {Vector as VectorSource} from 'ol/source';
 
-import {HsConfig} from 'hslayers-ng/config.service';
-import {HsEventBusService} from 'hslayers-ng/components/core/event-bus.service';
-import {HsLanguageService} from 'hslayers-ng/components/language/language.service';
-import {HsLayoutService} from 'hslayers-ng/components/layout/layout.service';
+import {HsConfig} from 'hslayers-ng';
+import {HsEventBusService} from 'hslayers-ng';
+import {HsLanguageService} from 'hslayers-ng';
+import {HsLayoutService} from 'hslayers-ng';
 //import {HsMapService} from 'hslayers-ng/components/map/map.service';
-import {HsPanelContainerService} from 'hslayers-ng/components/layout/panels/panel-container.service';
-import {HsSidebarService} from 'hslayers-ng/components/sidebar/sidebar.service';
+import {HsPanelContainerService} from 'hslayers-ng';
+import {HsSidebarService} from 'hslayers-ng';
 
 import {AdjusterComponent} from './adjuster/adjuster.component';
 import {AdjusterEventService} from './adjuster/adjuster-event.service';
 import {AdjusterLegendService} from './adjuster/adjuster-legend.service';
 import {AdjusterService} from './adjuster/adjuster.service';
+import {AppConfig, nuts2Layer, nuts3IndexLayer} from './app.config';
 import {nuts} from './nuts';
 
 @Injectable({providedIn: 'root'})
 export class AppService {
-  nuts2style = new Style({
-    stroke: new Stroke({
-      color: '#000000',
-      width: 0.5,
-    }),
-  });
-  nuts2Layer = new VectorLayer({
-    source: nuts.nuts2Source,
-    editor: {editable: false},
-    visible: false,
-    style: this.nuts2style,
-    title: 'NUTS2 regions',
-  });
-  nuts3IndexLayer = new VectorLayer({
-    source: nuts.nuts3IndexSource,
-    editor: {editable: false},
-    visible: true,
-    style: (f) => this.indexStyle(f),
-    title: 'NUTS3 regions: Rural attractiveness index',
-  });
-  nuts3ClustersLayer = new VectorLayer({
-    source: nuts.nuts3ClustersSource,
-    editor: {editable: false},
-    visible: true,
-    style: this.generateStyle(this.adjusterService.method),
-    title: 'NUTS3 regions: Clusters',
-  });
   pilotsColor = 'rgba(29, 148, 29, 0.2)';
   pilotsStyle = new Style({
     stroke: new Stroke({
@@ -61,16 +36,17 @@ export class AppService {
     }),
   });
   pilotRegions = new VectorLayer({
+    properties: {
+      title: 'Polirural Pilot Regions',
+      editor: {editable: false},
+    },
     source: new VectorSource({
       format: new GeoJSON(),
       url: require('./data/pilot_regions.geojson').default,
     }),
-    editor: {editable: false},
     visible: true,
     style: this.pilotsStyle,
-    title: 'Polirural Pilot Regions',
   });
-  serviceUrl: string;
   constructor(
     public adjusterService: AdjusterService,
     public adjusterEventService: AdjusterEventService,
@@ -83,50 +59,116 @@ export class AppService {
     public hsPanelContainerService: HsPanelContainerService,
     public hsSidebarService: HsSidebarService
   ) {
-    this.serviceUrl = this.adjusterService.serviceBaseUrl + 'georeport/';
-    this.nuts3IndexLayer.set('popUp', {
-      attributes: [
-        {attribute: 'CNTR_CODE', label: 'Country'},
-        {attribute: 'NUTS_NAME', label: 'Name'},
-        {
-          attribute: 'aggregate',
-          label: 'aggregated index',
-          displayFunction: (x) => {
-            return `${(x * 100).toFixed(2)}&nbsp;%`;
-          },
-        },
-        {
-          attribute: 'NUTS_ID',
-          label: 'Detailed report',
-          displayFunction: (x) => {
-            return `<a href="${this.serviceUrl}${x}" target="_blank">in a new page</a>.`;
-          },
-        },
-      ],
+    this.hsSidebarService.buttons.push({
+      panel: 'adjuster',
+      module: 'pra.adjuster',
+      order: 0,
+      title: () =>
+        this.hsLanguageService.getTranslation('ADJUSTER.adjustFactors'),
+      description: 'Adjust factors for computation',
+      icon: 'icon-analytics-piechart',
+    });
+    this.hsPanelContainerService.create(AdjusterComponent, {});
+    this.prepareLayers();
+    this.adjusterEventService.loaded.subscribe(({success}) => {
+      if (success) {
+        this.adjusterLegendService.refreshColorPalette(
+          this.adjusterService.numberOfClusters
+        );
+      }
+    });
+    this.hsEventBus.layoutLoads.subscribe(() => {
+      this.init();
+    });
+    this.hsEventBus.olMapLoads.subscribe(() => {
+      console.log('olMap loaded');
+      this.ensureLayerIsLoaded(nuts3IndexLayer);
+      for (const method of this.adjusterService.methods) {
+        this.ensureLayerIsLoaded(method.layer);
+      }
     });
     /* For debugging only */
     /*this.hsEventBus.olMapLoads.subscribe((map) => {
       map.on('click', this.debugMe);
     });*/
-    this.nuts3IndexLayer.set('editable', false);
-    this.nuts3IndexLayer.set('queryable', false);
-    this.nuts3IndexLayer.getSource().legend_categories = this.adjusterLegendService.createIndexLegend();
-    this.nuts3ClustersLayer.set('popUp', {
-      attributes: [
-        {attribute: 'CNTR_CODE', label: 'Country'},
-        {attribute: 'NUTS_NAME', label: 'Name'},
-        {attribute: this.adjusterService.method, label: 'Cluster ID'},
-        {
-          attribute: 'NUTS_ID',
-          label: 'Detailed report',
-          displayFunction: (x) => {
-            return `<a href="${this.serviceUrl}${x}" target="_blank">in a new page</a>.`;
-          },
-        },
-      ],
+    this.adjusterEventService.loaded.subscribe(({success}) => {
+      if (success) {
+        this.adjusterLegendService.refreshColorPalette(
+          this.adjusterService.numberOfClusters
+        );
+        /*this.colorPalette = this.generateRandomColorPalette(
+          adjusterService.clusters.length
+        );*/
+      }
     });
-    this.nuts3ClustersLayer.set('editable', false);
-    this.nuts3ClustersLayer.set('queryable', false);
+    /*this.adjusterEventService.methodChanged.subscribe((method) => {
+      //TODO: prettify this
+      this.nuts3ClustersLayer.set(
+        'title',
+        `NUTS3 regions: ${method.replaceAll(/\((.+?)\)/g, '')} Clusters`
+      );
+      this.nuts3ClustersLayer.get('popUp').attributes[2].attribute = method;
+      this.nuts3ClustersLayer.setStyle(
+        this.generateStyle(this.adjusterService.method)
+      );
+    });*/
+  }
+
+  init(): void {
+    this.hsLayoutService.setDefaultPanel('adjuster');
+  }
+
+  /**
+   * Create separate layer fo each clustering method and add it to default_layers
+   */
+  prepareLayers(): void {
+    console.log('loading layers...');
+    for (const method of this.adjusterService.methods) {
+      method.layer = new VectorLayer({
+        properties: {
+          editor: {editable: false},
+          queryable: false,
+          autoLegend: false,
+          title: `NUTS3 regions: ${method.name.replace(
+            /\((.+?)\)/g,
+            ''
+          )} clusters`,
+        },
+        source: new VectorSource({
+          //features: new GeoJSON().readFeatures(nuts3).map((f) => f.clone()),
+          format: new GeoJSON(),
+          url: require('./data/NUTS_RG_20M_2016_3857_LEVL_3.geojson').default,
+          overlaps: false,
+        }),
+        visible: true,
+        style: this.generateStyle(method.codename),
+      });
+      method.layer.getSource().on('featuresloadend', () => {
+        console.log(`featuresloadend of ${method.codename}`);
+        this.adjusterEventService.layerReady.next({name: method.codename});
+      });
+      method.layer.getSource().on('featuresloadstart', () => {
+        console.log(`loadstart of ${method.codename}`);
+      });
+      method.layer.getSource().on('featuresloaderror', () => {
+        console.log('evil shit');
+      });
+      AppConfig.default_layers.push(method.layer as any);
+      console.log(
+        `layer for method ${method.codename} (${method.name}) created`
+      );
+    }
+    /* The order of pushes matter! */
+    AppConfig.default_layers.push(nuts3IndexLayer as any);
+    nuts3IndexLayer
+      .getSource()
+      .on('featuresloadend', () =>
+        this.adjusterEventService.layerReady.next({name: 'index'})
+      );
+    (nuts3IndexLayer.getSource() as any).legend_categories =
+      this.adjusterLegendService.createIndexLegend();
+    AppConfig.default_layers.push(nuts2Layer as any);
+    AppConfig.default_layers.push(this.pilotRegions as any);
     this.pilotRegions.set('popUp', {
       attributes: [
         {attribute: 'NUTS_NAME', label: 'Region name'},
@@ -140,59 +182,29 @@ export class AppService {
         },*/
       ],
     });
-    this.pilotRegions.getSource().legend_categories = [
+    (this.pilotRegions.getSource() as any).legend_categories = [
       {
         color: this.pilotsColor,
         name: 'Polirural pilot region',
       },
     ];
     this.pilotRegions.set('queryable', false);
-    this.adjusterEventService.loaded.subscribe(({success}) => {
-      if (success) {
-        this.adjusterLegendService.refreshColorPalette(
-          this.adjusterService.numberOfClusters
-        );
-        /*this.colorPalette = this.generateRandomColorPalette(
-          adjusterService.clusters.length
-        );*/
-      }
-    });
-    this.adjusterEventService.methodChanged.subscribe((method) => {
-      /*TODO: prettify this
-      this.nuts3ClustersLayer.set(
-        'title',
-        `NUTS3 regions: ${method.replaceAll(/\((.+?)\)/g, '')} Clusters`
-      );*/
-      this.nuts3ClustersLayer.get('popUp').attributes[2].attribute = method;
-      this.nuts3ClustersLayer.setStyle(
-        this.generateStyle(this.adjusterService.method)
-      );
-    });
-    /* The order of pushes matter! */
-    this.hsConfig.default_layers.push(this.nuts3ClustersLayer);
-    this.hsConfig.default_layers.push(this.nuts3IndexLayer);
-    this.hsConfig.default_layers.push(this.nuts2Layer);
-    this.hsConfig.default_layers.push(this.pilotRegions);
-    /*this.hsMapService
-      .loaded()
-      .then((map) => this.hsMapService.repopulateLayers([]));*/
-    this.hsEventBus.layoutLoads.subscribe(() => {
-      this.init();
-    });
+    this.hsConfig.update(AppConfig);
+    console.log('layers preapared');
   }
 
-  init(): void {
-    this.hsSidebarService.buttons.push({
-      panel: 'adjuster',
-      module: 'pra.adjuster',
-      order: 0,
-      title: () =>
-        this.hsLanguageService.getTranslation('ADJUSTER.adjustFactors'),
-      description: 'Adjust factors for computation',
-      icon: 'icon-analytics-piechart',
-    });
-    this.hsPanelContainerService.create(AdjusterComponent, {});
-    this.hsLayoutService.setDefaultPanel('adjuster');
+  /**
+   * Layer must be turned visible so it is loaded into the map.
+   * This method ensures that layers critical for further computations
+   * are for at least a moment turned visible and thus loaded properly.
+   * @param {VectorLayer} layer One of layers used to calculate index or clusters
+   */
+  ensureLayerIsLoaded(layer: VectorLayer<VectorSource<Geometry>>): void {
+    const layerVisible = layer.getVisible();
+    if (!layerVisible) {
+      layer.setVisible(true);
+      setTimeout(() => layer.setVisible(false), 10);
+    }
   }
 
   /**
@@ -201,7 +213,7 @@ export class AppService {
    * @returns {function} style function
    */
   private generateStyle(method: string) {
-    return (feature: Feature): Style => {
+    return (feature: Feature<Geometry>): Style => {
       if (isNaN(feature.get(method))) {
         return new Style({
           fill: new Fill({
@@ -215,9 +227,8 @@ export class AppService {
       } else {
         return new Style({
           fill: new Fill({
-            color: this.adjusterLegendService.colorPalette[
-              feature.get(method) - 1
-            ],
+            color:
+              this.adjusterLegendService.colorPalette[feature.get(method) - 1],
           }),
           stroke: new Stroke({
             color: '#FFF',
@@ -228,36 +239,6 @@ export class AppService {
     };
   }
 
-  private indexStyle = (feature) => {
-    if (isNaN(feature.get('aggregate'))) {
-      return [
-        new Style({
-          fill: new Fill({
-            color: '#FFF',
-          }),
-          stroke: new Stroke({
-            color: '#3399CC',
-            width: 0.25,
-          }),
-        }),
-      ];
-    } else {
-      return [
-        new Style({
-          fill: new Fill({
-            color: this.adjusterLegendService.perc2color(
-              feature.get('aggregate')
-            ),
-          }),
-          stroke: new Stroke({
-            color: '#FFFFFF',
-            width: 0.15,
-          }),
-        }),
-      ];
-    }
-  };
-
   private debugMe(e) {
     console.log(e);
     const feats = e.map.getFeaturesAtPixel(e.pixel);

+ 14 - 17
src/attractiveness.config.json

@@ -1,24 +1,21 @@
 {
   "allowedClusteringMethods": [
-    "km25.cluster",
-    "km50hw.cluster",
     "km50l.cluster",
-    "km50m.cluster",
-    "kme_eu.cluster",
-    "kme_mn.cluster",
-    "haclust",
-    "haclustmin",
-    "haclustman",
-    "haclustcan",
-    "haclustwd2",
-    "haclustmcq",
-    "hdclust"
+    "haclustwd2"
+  ],
+  "defaultClassificationSchema": "http://www.semanticweb.org/attractiveness/Polirural",
+  "enabledRoles": [
+    "http://www.semanticweb.org/attractiveness/EUInvestor",
+    "http://www.semanticweb.org/attractiveness/CZYoungFamily"
+  ],
+  "enabledSchemas": [
+    "http://www.semanticweb.org/attractiveness/Polirural"
   ],
   "initialWeights": {
-    "Social": 0.1,
-    "Institutional": 0.5,
-    "Anthropic": 0.3,
-    "Cultural": 0.25
+    "http://www.semanticweb.org/attractiveness/social": 0.1,
+    "http://www.semanticweb.org/attractiveness/institutional": 0.5,
+    "http://www.semanticweb.org/attractiveness/anthropic": 0.3,
+    "http://www.semanticweb.org/attractiveness/cultural": 0.25
   },
-  "serviceBaseUrl": "https://publish.lesprojekt.cz/nodejs/"
+  "serviceBaseUrl": "https://jmacura.eu/ws/"
 }

+ 5 - 5
src/custom.scss

@@ -1,8 +1,8 @@
-@import "~bootstrap/scss/_functions";
-@import "~bootstrap/scss/_variables";
-@import "~bootstrap/scss/_mixins";
-@import "~bootstrap/scss/spinners";
-@import "~bootstrap/scss/custom-forms";
+@import "bootstrap/scss/functions";
+@import "bootstrap/scss/variables";
+@import "bootstrap/scss/mixins";
+@import "bootstrap/scss/spinners";
+@import "bootstrap/scss/forms";
 
 
 html, body {

+ 18 - 14
src/index.html

@@ -3,23 +3,27 @@
 
 <head>
   <meta charset="utf-8">
-  <title>Rural attractiveness clusters</title>
+  <title>Rural attractiveness of Europe</title>
   <meta name="description"
     content="Rural attractiveness calculation and visualization application developed in PoliRural project. 2020">
   <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
-  <link href="<%= htmlWebpackPlugin.files.favicon %>" rel="icon">
-  <meta name="apple-mobile-web-app-capable" content="yes" />
-  <meta name="apple-mobile-web-app-status-bar-style" content="black" />
-  <% for (var css in htmlWebpackPlugin.files.css) { %>
-    <link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
-  <% } %>
+  <link rel="icon" type="image/x-icon" href="images/cropped-favicon-32x32.png">
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+  <style>
+html, body {
+  margin: 0;
+  padding: 0;
+}
+#hs-app {
+  position: relative;
+  margin: 0;
+  padding: 0;
+}
+  </style>
 </head>
-
-<body>
-  <hs id="hs-app" ng-strict-di></hs>
-  <% for (var js in htmlWebpackPlugin.files.js) { %>
-    <script type="application/javascript" src="<%= htmlWebpackPlugin.files.js[js] %>"></script>
-  <% } %>
+<body >
+  <app-component></app-component>
 </body>
-
+</html>
 </html>

+ 13 - 10
src/main.ts

@@ -1,13 +1,16 @@
-import 'core-js/es6/reflect';
-import 'core-js/es7/reflect';
-import 'reflect-metadata';
-(window as any).__Zone_disable_requestAnimationFrame = true;
-(window as any).__Zone_disable_setTimeout = true;
-import 'zone.js';
-import * as angular from 'angular';
+import './polyfills';
 import {AppModule} from './app.module';
 import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
-import {setAngularJSGlobal} from '@angular/upgrade/static';
 
-setAngularJSGlobal(angular);
-platformBrowserDynamic().bootstrapModule(AppModule);
+platformBrowserDynamic()
+  .bootstrapModule(AppModule)
+  .then((ref) => {
+    // Ensure Angular destroys itself on hot reloads.
+    if (window['ngRef']) {
+      window['ngRef'].destroy();
+    }
+    window['ngRef'] = ref;
+
+    // Otherwise, log the boot error
+  })
+  .catch((err) => console.error(err));

+ 6 - 6
src/nuts.ts

@@ -1,5 +1,5 @@
 import nuts2 from './data/NUTS_RG_20M_2016_3857_LEVL_2.json';
-import nuts3 from './data/NUTS_RG_20M_2016_3857_LEVL_3.json';
+//import nuts3 from './data/NUTS_RG_20M_2016_3857_LEVL_3.json';
 import {GeoJSON} from 'ol/format';
 import {Vector as VectorSource} from 'ol/source';
 
@@ -10,16 +10,16 @@ export const nuts = {
   }),
 
   nuts3IndexSource: new VectorSource({
-    features: new GeoJSON().readFeatures(nuts3),
-    //format: new GeoJSON(),
-    //url: require('./data/NUTS_RG_20M_2016_3857_LEVL_3.geojson').default,
+    //features: new GeoJSON().readFeatures(nuts3),
+    format: new GeoJSON(),
+    url: require('./data/NUTS_RG_20M_2016_3857_LEVL_3.geojson').default,
     overlaps: false,
   }),
 
-  nuts3ClustersSource: new VectorSource({
+  /*nuts3ClustersSource: new VectorSource({
     features: new GeoJSON().readFeatures(nuts3).map((f) => f.clone()),
     //format: new GeoJSON(),
     //url: require('./data/NUTS_RG_20M_2016_3857_LEVL_3.geojson').default,
     overlaps: false,
-  }),
+  }),*/
 };

+ 22 - 10
tsconfig.json

@@ -1,12 +1,24 @@
 {
-    "compilerOptions": {
-        "experimentalDecorators": true,
-        "allowJs": false,
-        "target": "es5",
-        "emitDecoratorMetadata": true,
-        "lib": ["esnext", "dom"],
-        "sourceMap": true,
-        "esModuleInterop": true,
-        "resolveJsonModule": true,
-    }
+  "compileOnSave": false,
+  "compilerOptions": {
+    "allowJs": false,
+    "allowSyntheticDefaultImports": true,
+    "baseUrl": "./",
+    "declaration": false,
+    "downlevelIteration": true,
+    "emitDecoratorMetadata": true,
+    "esModuleInterop": true,
+    "experimentalDecorators": true,
+    "importHelpers": true,
+    "lib": ["es2018", "dom"],
+    "resolveJsonModule": true,
+    "sourceMap": true,
+    "skipLibCheck": true,
+    "target": "es5",
+  },
+  "angularCompilerOptions": {
+    "enableResourceInlining": true,
+    "fullTemplateTypeCheck": true,
+    "strictInjectionParameters": true
+  }
 }

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů