Browse Source

✨ Display clusters as well

Yohoo. Finally 🎉
jmacura 4 years ago
parent
commit
e16145c28e
5 changed files with 292 additions and 164 deletions
  1. 5 0
      package-lock.json
  2. 1 0
      package.json
  3. 170 154
      src/adjuster/adjuster.service.ts
  4. 7 7
      src/app.config.ts
  5. 109 3
      src/app.service.ts

+ 5 - 0
package-lock.json

@@ -6406,6 +6406,11 @@
         "zone.js": "^0.10.3"
       }
     },
+    "hsv2rgb": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/hsv2rgb/-/hsv2rgb-1.1.0.tgz",
+      "integrity": "sha1-YLDlWaiHfY0tzOc5G8O4OwB6yaY="
+    },
     "html-comment-regex": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz",

+ 1 - 0
package.json

@@ -26,6 +26,7 @@
   "dependencies": {
     "csvtojson": "^2.0.10",
     "hslayers-ng": "^2.5.0",
+    "hsv2rgb": "^1.1.0",
     "http-server": "^0.12.3"
   },
   "devDependencies": {

+ 170 - 154
src/adjuster/adjuster.service.ts

@@ -1,14 +1,15 @@
 import {HttpClient} from '@angular/common/http';
 import {Injectable} from '@angular/core';
 import {Vector as VectorLayer} from 'ol/layer';
-import {Vector as VectorSource} from 'ol/source';
 
+import {HsConfig} from 'hslayers-ng/config.service';
 import {HsLayerManagerMetadataService} from 'hslayers-ng/components/layermanager/layermanager-metadata.service';
 import {HsLayerManagerService} from 'hslayers-ng/components/layermanager';
 import {HsUtilsService} from 'hslayers-ng/components/utils/utils.service';
 
 import clusteringMethods from '../data/clustering_methods.json';
-import {obce, obceLayer, osmLayer} from '../app.config';
+import {AdjusterEventService} from './adjuster-event.service';
+import {obce, obceIndexLayer, osmLayer} from '../app.config';
 
 @Injectable({providedIn: 'root'})
 export class AdjusterService {
@@ -16,9 +17,10 @@ export class AdjusterService {
   factors = [];
   //clusters = [];
   numberOfClusters;
-  method: string;
+  //method: string;
   methods: Array<{
     codename: string;
+    layer?: VectorLayer;
     name: string;
     type: string;
   }>;
@@ -27,6 +29,8 @@ export class AdjusterService {
   private _raiInProcess: boolean;
 
   constructor(
+    public adjusterEventService: AdjusterEventService,
+    public hsConfig: HsConfig,
     public hsLayerMetadataService: HsLayerManagerMetadataService,
     public hsLayerManagerService: HsLayerManagerService,
     public hsUtilsService: HsUtilsService,
@@ -34,12 +38,12 @@ export class AdjusterService {
   ) {
     this.serviceBaseUrl =
       window.location.hostname === 'localhost'
-        ? 'http://localhost:3000/' // 'https://jmacura.ml/ws/'
+        ? 'https://jmacura.ml/ws/' // 'http://localhost:3000/'
         : 'https://publish.lesprojekt.cz/nodejs/';
     this.methods = clusteringMethods.filter(
       (m) => m.codename == 'haclustwd2' || m.codename == 'km50l.cluster'
     );
-    this.method = 'haclustwd2';
+    //this.method = 'haclustwd2';
     this.numberOfClusters = 9;
   }
 
@@ -49,96 +53,95 @@ export class AdjusterService {
    */
   apply(): void {
     this.calculateIndex();
-    //this.calculateClusters();
+    this.calculateClusters();
   }
 
   calculateIndex(): void {
-    const f = () => {
-      this._raiInProcess = true;
-      this.$http
-        .post(this.serviceBaseUrl + 'scores/cz', {
-          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[]) => {
-          console.log(attractivenessData);
-          //this.attractivity = attractivenessData;
-          /*let max = 0;
+    //const f = () => {
+    this._raiInProcess = true;
+    this.$http
+      .post(this.serviceBaseUrl + 'scores/cz', {
+        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[]) => {
+        console.log(attractivenessData);
+        //this.attractivity = attractivenessData;
+        /*let max = 0;
           attractivenessData.forEach((a) => {
             if (a.aggregate > max) {
               max = a.aggregate;
             }
           });*/
-          // 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;
-          });
-          console.log(attractivenessData);
-          // Store relation between region and its data in a hash-table-like structure
-          const codeRecordRelations = {};
-          attractivenessData.forEach((a) => {
-            codeRecordRelations[a.code] = a;
-          });
-          let errs = 0;
-          let logs = 0;
-          console.time('forEachObce');
-          obce.forEachFeature((feature) => {
-            // Pair each feature with its attractivity data
-            const featureData =
-              codeRecordRelations[feature.get('nationalCode')];
-            /*const featureData = attractivenessData.find(
+        // 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;
+        });
+        console.log(attractivenessData);
+        // Store relation between region and its data in a hash-table-like structure
+        const codeRecordRelations = {};
+        attractivenessData.forEach((a) => {
+          codeRecordRelations[a.code] = a;
+        });
+        let errs = 0;
+        let logs = 0;
+        console.time('forEachObce');
+        obce.forEachFeature((feature) => {
+          // Pair each feature with its attractivity data
+          const featureData = codeRecordRelations[feature.get('nationalCode')];
+          /*const featureData = attractivenessData.find(
               // NOTE: Do NOT add triple equal sign!
               (item) => item['code'] == feature.get('nationalCode')
             );*/
-            if (!featureData) {
-              if (errs < 20) {
-                errs++;
-                console.warn(
-                  `No data for feature ${feature.get('nationalCode')}`
-                );
-                console.log(feature);
-              }
-              return;
+          if (!featureData) {
+            if (errs < 20) {
+              errs++;
+              console.warn(
+                `No data for feature ${feature.get('nationalCode')}`
+              );
+              console.log(feature);
             }
-            logs++;
-            if (logs % 100 == 0) {
-              console.log(`processed ${logs} items`);
+            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!
             }
-            Object.keys(featureData).forEach((key, index) => {
-              if (key !== 'lau2') {
-                feature.set(key, featureData[key], true); //true stands for "silent" - important!
-              }
-            });
-            /*feature.set(
+          });
+          /*feature.set(
               'total',
               this.nutsCodeRecordRelations[feature.get('nationalCode')]
                 .aggregate
             );*/
-            /*feature.set(
+          /*feature.set(
               'totalForHumans',
               (
                 this.nutsCodeRecordRelations[feature.get('nationalCode')]
                   .aggregate * 100
               ).toFixed(2)
             );*/
-            /*this.factors.forEach((factor) => {
+          /*this.factors.forEach((factor) => {
               feature.set(
                 factor.name,
                 (
@@ -148,45 +151,54 @@ export class AdjusterService {
                 ).toFixed(2)
               );
             });*/
-          });
-          // Since we are updating the features silently, we now have to refresh manually
-          obce.getFeatures()[0].dispatchEvent('change');
-          console.timeEnd('forEachObce');
-          this._raiInProcess = false;
-        })
-        .catch((error) => {
-          console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`);
-          console.log(error);
-          this._raiInProcess = false;
         });
-    };
-    this.hsUtilsService.debounce(f, 300, false, this)();
+        // Since we are updating the features silently, we now have to refresh manually
+        obce.getFeatures()[0].dispatchEvent('change');
+        console.timeEnd('forEachObce');
+        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,
+        });
+      });
+    //};
+    //this.hsUtilsService.debounce(f, 300, false, this)();
   }
 
   calculateClusters(): void {
-    const f = () => {
-      this._clusteringInProcess = true;
-      this.$http
-        .post(this.serviceBaseUrl + 'clusters/cz', {
-          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) => {
-          console.log('data received', data);
-          let logs = 0;
-          let errs = 0;
-          const clusterData = data.response;
-          console.log(obceLayer);
-          /*let sublayers = [];
+    //const f = () => {
+    this._clusteringInProcess = true;
+    this.$http
+      .post(this.serviceBaseUrl + 'clusters/cz', {
+        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) => {
+        console.log('data received', data);
+        let logs = 0;
+        let errs = 0;
+        const clusterData = data.response;
+        console.log(obceIndexLayer);
+        /*let sublayers = [];
           const oldSublayers = obceLayer.get('Layer');
           if (oldSublayers !== undefined && Array.isArray(oldSublayers)) {
             for (const sublyr of oldSublayers) {
@@ -208,36 +220,36 @@ export class AdjusterService {
             sublyr.Name = `c${i + 1}`;
             i++;
           }*/
-          console.time('forEachObceCluster');
-          obce.forEachFeature((feature) => {
-            // Pair each feature with its clustering data
-            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;
+        console.time('forEachObceCluster');
+        obce.forEachFeature((feature) => {
+          // Pair each feature with its clustering data
+          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);
             }
-            logs++;
-            if (logs % 100 == 0) {
-              console.log(`processed ${logs} items`);
+            return;
+          }
+          logs++;
+          if (logs % 100 == 0) {
+            console.log(`processed ${logs} items`);
+          }
+          Object.keys(featureData).forEach(function (key, index) {
+            if (key !== 'lau2') {
+              feature.set(key, featureData[key], true);
             }
-            Object.keys(featureData).forEach(function (key, index) {
-              if (key !== 'lau2') {
-                feature.set(key, featureData[key], true);
-              }
-            });
           });
-          //const clusters = [];
-          //const obceFeatures: Array<any> = obce.getFeatures();
-          /*for (const region of clusterData) {
+        });
+        //const clusters = [];
+        //const obceFeatures: Array<any> = obce.getFeatures();
+        /*for (const region of clusterData) {
             if (!clusters.includes(region[this.method])) {
               clusters.push(region[this.method]);
             }
@@ -267,27 +279,31 @@ export class AdjusterService {
             }
             //sublayers[region[this.method] - 1].getSource().addFeature(feature);
           }*/
-          //obceLayer.set('Layer', sublayers);
-          //this.hsLayerMetadataService.fillMetadata(obceLayer);
-          //console.log(sublayers[0].getSource().getFeatures());
-          console.log(obceLayer);
-          console.timeEnd('forEachObceCluster');
-          console.log('clustering done!');
-          //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,
-          });*/
+        //obceLayer.set('Layer', sublayers);
+        //this.hsLayerMetadataService.fillMetadata(obceLayer);
+        //console.log(sublayers[0].getSource().getFeatures());
+        console.log(obceIndexLayer);
+        console.timeEnd('forEachObceCluster');
+        console.log('clustering done!');
+        //this.clusters = clusters;
+        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,
+        });
+      });
+    //};
+    //this.hsUtilsService.debounce(f, 300, false, this)();
   }
 
   init(): void {

+ 7 - 7
src/app.config.ts

@@ -58,7 +58,7 @@ function perc2color(perc) {
   return `rgba(${r}, ${g}, ${b}, 0.7)`;
 }
 
-const styles = function (feature) {
+const indexStyle = function (feature) {
   if (isNaN(feature.get('aggregate'))) {
     return [
       new Style({
@@ -86,15 +86,15 @@ export const obce = new VectorSource({
   overlaps: false,
 });
 
-export const obceLayer = new VectorLayer({
+export const obceIndexLayer = new VectorLayer({
   source: obce,
   editor: {editable: false},
   visible: true,
-  style: styles,
-  title: 'Obce ČR',
+  style: indexStyle,
+  title: 'Obce ČR: Rural attractiveness index',
   attributions: ['CC-BY ČÚZK, 2021'],
 });
-obceLayer.set('popUp', {
+obceIndexLayer.set('popUp', {
   attributes: [
     {attribute: 'text', label: 'Název'},
     {
@@ -107,12 +107,12 @@ obceLayer.set('popUp', {
     'Social & Human', //TODO: factors?
   ],
 });
-obceLayer.set('editable', false);
+obceIndexLayer.set('editable', false);
 //obceLayer.set('queryable', false);
 
 export const AppConfig = {
   //proxyPrefix: '../8085/',
-  default_layers: [osmLayer, obceLayer],
+  default_layers: [osmLayer],
   popUpDisplay: 'hover',
   project_name: 'erra/map',
   default_view: new View({

+ 109 - 3
src/app.service.ts

@@ -1,5 +1,10 @@
+import hsv2rgb from 'hsv2rgb';
+import {Feature} from 'ol';
+import {Fill, Stroke, Style} from 'ol/style';
 import {Injectable} from '@angular/core';
+import {Vector as VectorLayer} from 'ol/layer';
 
+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 {HsLayerManagerService} from 'hslayers-ng/components/layermanager';
@@ -8,11 +13,32 @@ import {HsPanelContainerService} from 'hslayers-ng/components/layout/panels/pane
 import {HsSidebarService} from 'hslayers-ng/components/sidebar/sidebar.service';
 
 import {AdjusterComponent} from './adjuster/adjuster.component';
-import {obce} from './app.config';
+import {AdjusterEventService} from './adjuster/adjuster-event.service';
+import {AdjusterService} from './adjuster/adjuster.service';
+import {obce, obceIndexLayer} from './app.config';
 
 @Injectable({providedIn: 'root'})
 export class AppService {
+  // https://colorbrewer2.org/?type=qualitative&scheme=Paired&n=12
+  colorPalette = [
+    '#a6cee3',
+    '#1f78b4',
+    '#b2df8a',
+    '#33a02c',
+    '#fb9a99',
+    '#e31a1c',
+    '#fdbf6f',
+    '#ff7f00',
+    '#cab2d6',
+    '#6a3d9a',
+    '#ffff99',
+    '#b15928',
+  ];
+
   constructor(
+    public adjusterService: AdjusterService,
+    public adjusterEventService: AdjusterEventService,
+    public hsConfig: HsConfig,
     public hsEventBus: HsEventBusService,
     public hsLanguageService: HsLanguageService,
     public hsLayerManagerService: HsLayerManagerService,
@@ -20,15 +46,24 @@ export class AppService {
     public hsPanelContainerService: HsPanelContainerService,
     public hsSidebarService: HsSidebarService
   ) {
+    this.adjusterEventService.loaded.subscribe(({success}) => {
+      if (success) {
+        //console.log(this.adjusterService.methods[0].layer.getSource().getFeatures());
+        this.colorPalette = this.generateRandomColorPalette(
+          this.adjusterService.numberOfClusters
+        );
+      }
+    });
+    this.prepareLayers();
     this.hsEventBus.layoutLoads.subscribe(() => {
       this.init();
     });
-    obce.getParams = () => {
+    /*obce.getParams = () => {
       return {LAYERS: []};
     };
     obce.updateParams = (params) => {
       null;
-    };
+    };*/
   }
 
   init(): void {
@@ -45,4 +80,75 @@ export class AppService {
     this.hsPanelContainerService.create(AdjusterComponent, {});
     this.hsLayoutService.setDefaultPanel('adjuster');
   }
+
+  prepareLayers(): void {
+    for (const method of this.adjusterService.methods) {
+      method.layer = new VectorLayer({
+        source: obce,
+        editor: {editable: false},
+        visible: true,
+        style: this.generateStyle(method.codename),
+        title: `Obce ČR: ${method.name} clusters`,
+        attributions: ['CC-BY ČÚZK, 2021'],
+      });
+      this.hsConfig.default_layers.push(method.layer);
+    }
+    // obceIndexLayer must be pushed last so it will be on top
+    this.hsConfig.default_layers.push(obceIndexLayer);
+    //console.log(this.adjusterService.methods[0].layer.getSource().getFeatures());
+  }
+
+  /**
+   * https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
+   * @private
+   * @description Only generates random colors if the current palette does not provide enough colors for all the clusters
+   * @param {number} colorCount Number of colors to randomly generate
+   * @returns {Array<Array<number> | string>} Array of RGB colors
+   */
+  private generateRandomColorPalette(colorCount: number) {
+    const palette = this.colorPalette;
+    const goldenRatioConjugate = 0.618033988749895;
+    let i = palette.length;
+    while (i < colorCount) {
+      let h = Math.random();
+      h += goldenRatioConjugate;
+      h %= 1;
+      h *= 360;
+      palette.push(hsv2rgb(h, 0.5, 0.95));
+      i++;
+    }
+    return palette;
+    //return `rgba(${r}, ${g}, ${b}, 0.7)`;
+  }
+
+  /**
+   * @description Function factory for generating style functions based on different clustering methods
+   * @param {string} method currently selected method
+   * @returns {function} style function
+   */
+  private generateStyle(method: string) {
+    return (feature: Feature): Style => {
+      if (isNaN(feature.get(method))) {
+        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.colorPalette[feature.get(method) - 1],
+          }),
+          stroke: new Stroke({
+            color: '#FFF',
+            width: 0.25,
+          }),
+        });
+      }
+    };
+  }
 }