Переглянути джерело

feat: allow choose different clustering method

jmacura 5 роки тому
батько
коміт
49c2f464df

+ 2 - 1
src/adjuster/adjuster-event.service.ts

@@ -3,6 +3,7 @@ import {Subject} from 'rxjs';
 
 @Injectable({providedIn: 'root'})
 export class AdjusterEventService {
-  clustersLoaded: Subject<any> = new Subject();
+  clustersLoaded: Subject<void> = new Subject();
+  methodChanged: Subject<string> = new Subject();
   constructor() {}
 }

+ 11 - 12
src/adjuster/adjuster.component.ts

@@ -1,11 +1,9 @@
-import {Component, OnInit, ViewRef} from '@angular/core';
+import {Component, ViewRef} from '@angular/core';
 
 import {HsLayoutService} from 'hslayers-ng/components/layout/layout.service';
 import {HsPanelComponent} from 'hslayers-ng/components/layout/panels/panel-component.interface';
-import {HsPanelContainerService} from 'hslayers-ng/components/layout/panels/panel-container.service';
-import {HsSidebarService} from 'hslayers-ng/components/sidebar/sidebar.service';
-import {HsUtilsService} from 'hslayers-ng/components/utils/utils.service';
 
+import {AdjusterEventService} from './adjuster-event.service';
 import {AdjusterService} from './adjuster.service';
 
 @Component({
@@ -15,20 +13,16 @@ import {AdjusterService} from './adjuster.service';
 export class AdjusterComponent implements HsPanelComponent {
   data: any;
   descriptionVisible: boolean;
+  method: string;
   viewRef: ViewRef;
 
   constructor(
     private adjusterService: AdjusterService,
-    private hsLayoutService: HsLayoutService,
-    private hsSidebarService: HsSidebarService,
-    private hsPanelContainerService: HsPanelContainerService,
-    private hsUtilsService: HsUtilsService
+    private adjusterEventService: AdjusterEventService,
+    private hsLayoutService: HsLayoutService
   ) {
-    //$scope.loading = false;
-    //$scope.HsUtilsService = HsUtilsService;
     this.descriptionVisible = false;
-
-    //$scope.$emit('scope_loaded', 'adjuster');
+    this.method = this.adjusterService.method;
   }
 
   isVisible(): boolean {
@@ -51,4 +45,9 @@ export class AdjusterComponent implements HsPanelComponent {
     }
     return datasetsEffectivelyTurnedOn.length === 0;
   }
+
+  selectMethod(): void {
+    this.adjusterService.method = this.method;
+    this.adjusterEventService.methodChanged.next(this.method);
+  }
 }

+ 24 - 4
src/adjuster/adjuster.directive.html

@@ -4,7 +4,8 @@
     <div class="p-2 center-block">
       <button type="button" class="btn btn-primary" (click)="adjusterService.apply()"
         [disabled]="adjusterService.isClusteringInProcess() || noDataSelected()">Calculate clusters</button>
-      <div class="text-warning pt-2" [hidden]="!noDataSelected()">Select at least one dataset and set at least one factor's weight to a non-zero value.</div>
+      <div class="text-warning pt-2" [hidden]="!noDataSelected()">Select at least one dataset and set at least one
+        factor's weight to a non-zero value.</div>
     </div>
     <div *ngFor="let factor of adjusterService.factors; let datasetlistVisible = false">
       <div class="d-flex flex-row">
@@ -12,7 +13,7 @@
           <span class="glyphicon cursor-pointer"
             [ngClass]="datasetlistVisible ? 'icon-chevron-down' : 'icon-chevron-right'"
             (click)="datasetlistVisible = !datasetlistVisible"></span>
-          <label class="cursor-pointer" (click)="datasetlistVisible = !datasetlistVisible">{{factor.name}}</label>
+          <label class="pl-2 cursor-pointer" (click)="datasetlistVisible = !datasetlistVisible">{{factor.name}}</label>
         </div>
         <div class="p-2">{{(factor.weight * 100).toFixed(0)}}&nbsp;%</div>
       </div>
@@ -25,14 +26,33 @@
           <span class="glyphicon cursor-pointer text-secondary"
             [ngClass]="descriptionVisible ? 'icon-chevron-down' : 'icon-chevron-right'"
             (click)="descriptionVisible = !descriptionVisible"></span>
-          <label class="pl-2 cursor-pointer text-secondary" (click)="descriptionVisible = !descriptionVisible">{{dataset.name}}</label>
+          <label class="pl-2 cursor-pointer text-secondary"
+            (click)="descriptionVisible = !descriptionVisible">{{dataset.name}}</label>
           <div class="p-2 mb-2 text-justify text-info" [hidden]="!descriptionVisible">
             {{dataset.desc}}
           </div>
         </div>
       </div>
     </div>
-    <div class="pt-3 center-block">
+    <hr>
+    <form role="form" class="pt-3 form" [hidden]="adjusterService.clusters.length == 0">
+      <div class="form-group">
+        <div class="input-group">
+          <label class="text-center">Display clusters calculated by method</label>
+          <div>
+            <select class="form-control" [(ngModel)]="method" (ngModelChange)="selectMethod()" name="method"
+              [value]="method">
+              <option *ngFor="let method of adjusterService.methods" [value]="method.codename">{{method.name}}
+              </option>
+            </select>
+          </div>
+        </div>
+        <small class="text-justify text-info">REMEMBER: Not all clustering methods will provide meaningful results. Always
+          take the output with a grain of salt.</small>
+      </div>
+    </form>
+    <hr>
+    <div class="pt-3 center-block" [hidden]="adjusterService.clusters.length == 0">
       You can investigate the layers<br>
       in the <a href="" (click)="hsLayoutService.setMainPanel('layermanager');$event.preventDefault();">LayerManager</a>
     </div>

+ 80 - 6
src/adjuster/adjuster.service.ts

@@ -1,6 +1,5 @@
 import {HttpClient} from '@angular/common/http';
 import {Injectable} from '@angular/core';
-import {Subject} from 'rxjs';
 
 import {HsDialogContainerService} from 'hslayers-ng/components/layout/dialogs/dialog-container.service';
 import {HsUtilsService} from 'hslayers-ng/components/utils/utils.service';
@@ -14,9 +13,86 @@ import {nuts} from '../nuts';
 @Injectable({providedIn: 'root'})
 export class AdjusterService {
   serviceBaseUrl: string;
-  factors;
-  clusters;
+  factors = [];
+  clusters = [];
   method: string;
+  methods = [
+    {
+      codename: 'km25.cluster',
+      name: 'k-means (25 random sets, Hartigan-Wong method)',
+      type: 'non-hierarchical',
+    },
+    {
+      codename: 'km50hw.cluster',
+      name: 'k-means (50 random sets, Hartigan-Wong method)',
+      type: 'non-hierarchical',
+    },
+    {
+      codename: 'km50l.cluster',
+      name: 'k-means (50 random sets, Lloyd method)',
+      type: 'non-hierarchical',
+    },
+    {
+      codename: 'km50m.cluster',
+      name: 'k-means (50 random sets, MacQueen method)',
+      type: 'non-hierarchical',
+    },
+    {
+      codename: 'kme_eu.cluster',
+      name: 'partitioning (Euclidean distance matrix)',
+      type: 'non-hierarchical',
+    },
+    {
+      codename: 'kme_mn.cluster',
+      name: 'partitioning (Manhattan distance matrix)',
+      type: 'non-hierarchical',
+    },
+    {
+      codename: 'haclust',
+      name: 'complete linkage (Euclidean distance matrix)',
+      type: 'hierarchical',
+    },
+    {
+      codename: 'haclustmin',
+      name: 'complete linkage (Minkowski binary distance matrix)',
+      type: 'hierarchical',
+    },
+    {
+      codename: 'haclustbin',
+      name: 'complete linkage (asymemetric binary distance matrix)',
+      type: 'hierarchical',
+    },
+    {
+      codename: 'haclustman',
+      name: 'complete linkage (Manhattan distance matrix)',
+      type: 'hierarchical',
+    },
+    {
+      codename: 'haclustmax',
+      name: 'complete linkage ("Supremum norm" distance matrix)',
+      type: 'hierarchical',
+    },
+    {
+      codename: 'haclustcan',
+      name: 'complete linkage (Canberra distance matrix)',
+      type: 'hierarchical',
+    },
+    /*{
+      codename: 'haclustcom',
+      name: 'complete linkage (Euclidean distance matrix)',
+      type: 'hierarchical',
+    },*/
+    {codename: 'haclustwd2', name: 'Ward2', type: 'hierarchical'},
+    {codename: 'haclustsin', name: 'single linkage', type: 'hierarchical'},
+    {codename: 'haclustcen', name: 'centroid (UPGMC)', type: 'hierarchical'},
+    {codename: 'haclustmed', name: 'median (WPGMC)', type: 'hierarchical'},
+    {codename: 'haclustmcq', name: 'McQuitty (WPGMA)', type: 'hierarchical'},
+    {
+      codename: 'hdclust',
+      name: 'DIANA (DIvisive ANAlysis)',
+      type: 'hierarchical',
+    },
+  ];
   _clusteringInProcess: boolean;
 
   constructor(
@@ -29,8 +105,6 @@ export class AdjusterService {
       window.location.hostname === 'localhost'
         ? 'https://jmacura.ml/ws/' // 'http://localhost:3000/'
         : 'https://publish.lesprojekt.cz/nodejs/';
-    this.factors = [];
-    this.clusters = [];
     this.method = 'haclustwd2';
     this._clusteringInProcess = true;
     this.init();
@@ -78,7 +152,7 @@ export class AdjusterService {
               (item) => item['nuts_id'] === feature.get('NUTS_ID')
             );
             if (!featureData) {
-              console.error(`No data for feature ${feature.get('NUTS_ID')}`);
+              console.warn(`No data for feature ${feature.get('NUTS_ID')}`);
               console.log(feature);
               return;
             }

+ 38 - 32
src/app.service.ts

@@ -47,38 +47,11 @@ export class AppService {
     style: this.nuts2style,
     title: 'NUTS2 regions',
   });
-  nuts3style = (feature: Feature): Style => {
-    if (isNaN(feature.get(this.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(this.method) - 1],
-          }),
-          stroke: new Stroke({
-            color: '#FFF',
-            width: 0.25,
-          }),
-        }),
-      ];
-    }
-  };
   nuts3Layer = new VectorLayer({
     source: nuts.nuts3Source,
     editor: {editable: false},
     visible: true,
-    style: this.nuts3style,
+    style: this.generateStyle(this.adjusterService.method),
     title: 'NUTS3 regions',
   });
   pilotsStyle = new Style({
@@ -100,7 +73,6 @@ export class AppService {
     style: this.pilotsStyle,
     title: 'Polirural Pilot Regions',
   });
-  method: string;
   serviceUrl: string;
   constructor(
     private adjusterService: AdjusterService,
@@ -111,13 +83,12 @@ export class AppService {
     private hsSidebarService: HsSidebarService,
     private hsPanelContainerService: HsPanelContainerService
   ) {
-    this.method = this.adjusterService.method;
     this.serviceUrl = this.adjusterService.serviceBaseUrl + 'georeport/';
     this.nuts3Layer.set('popUp', {
       attributes: [
         {attribute: 'CNTR_CODE', label: 'Country'},
         {attribute: 'NUTS_NAME', label: 'Name'},
-        {attribute: this.method, label: 'Cluster ID'},
+        {attribute: this.adjusterService.method /*, label: 'Cluster ID'*/},
         {
           attribute: 'NUTS_ID',
           label: 'Detailed report',
@@ -145,6 +116,10 @@ export class AppService {
         adjusterService.clusters.length
       );
     });
+    this.adjusterEventService.methodChanged.subscribe((method) => {
+      this.nuts3Layer.get('popUp').attributes[2].attribute = method;
+      this.nuts3Layer.setStyle(this.generateStyle(this.adjusterService.method));
+    });
     /* The order of pushes matter! */
     this.hsConfig.default_layers.push(this.nuts3Layer);
     this.hsConfig.default_layers.push(this.nuts2Layer);
@@ -174,7 +149,7 @@ export class AppService {
    * @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>>} Array of RGB colors
+   * @returns {Array<Array<number> | string>} Array of RGB colors
    */
   private generateRandomColorPalette(colorCount: number) {
     const palette = this.colorPalette;
@@ -191,4 +166,35 @@ export class AppService {
     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,
+          }),
+        });
+      }
+    };
+  }
 }