#4 Change the appearance of the zones tool

已合併
jmacura 3 年之前 將 14 次代碼提交從 jmacura/better-gui合併至 jmacura/main

+ 1 - 0
.gitignore

@@ -2,3 +2,4 @@ node_modules
 build
 dist
 .vs
+.angular

文件差異過大導致無法顯示
+ 446 - 283
package-lock.json


+ 12 - 12
package.json

@@ -27,22 +27,22 @@
   },
   "homepage": "https://git.lesprojekt.cz/jmacura/fieldcalc-frontend/README.md",
   "dependencies": {
-    "hslayers-ng": "^7.1.0"
+    "hslayers-ng": "9.2.0"
   },
   "devDependencies": {
-    "@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",
+    "@angular-builders/custom-webpack": "^13.0.0",
+    "@angular-devkit/build-angular": "^13.3.3",
+    "@angular-eslint/builder": "^13.2.1",
+    "@angular-eslint/eslint-plugin": "^13.2.1",
+    "@angular-eslint/eslint-plugin-template": "^13.5.0",
+    "@angular-eslint/template-parser": "^13.5.0",
+    "@angular/cli": "~13.3.5",
     "@types/karma-jasmine": "^4.0.2",
     "@typescript-eslint/eslint-plugin": "^5.7.0",
-    "eslint": "^8.4.1",
+    "eslint": "^8.17.0",
     "eslint-config-openlayers": "14.0.0",
-    "eslint-plugin-import": "^2.25.3",
-    "eslint-plugin-tsdoc": "^0.2.14",
-    "karma-jasmine": "^4.0.1"
+    "eslint-plugin-import": "2.26.0",
+    "eslint-plugin-tsdoc": "^0.2.16",
+    "karma-jasmine": "~5.0.1"
   }
 }

+ 4 - 0
src/app/app.component.scss

@@ -0,0 +1,4 @@
+hslayers {
+  display: block;
+  height: calc(var(--vh, 1vh) * 100);
+}

+ 2 - 2
src/app/app.component.ts

@@ -33,7 +33,7 @@ export class AppComponent {
     private hsToastService: HsToastService
   ) {
     /* Create new button in the sidebar */
-    this.hsSidebarService.buttons.push({
+    this.hsSidebarService.addButton({
       panel: 'calculator',
       module: 'calculator',
       order: 0,
@@ -46,7 +46,7 @@ export class AppComponent {
     this.hsPanelContainerService.create(CalculatorComponent, {});
     /* Switch language to cs */
     this.hsEventBus.layoutLoads.subscribe(() => {
-      this.hsLanguageService.setLanguage('cs');
+      this.hsLanguageService.setLanguage('en');
       this.hsLayoutService.setDefaultPanel('calculator');
     });
   }

+ 113 - 110
src/app/app.service.ts

@@ -92,12 +92,12 @@ export class AppService {
           '&service=WFS' +
           '&VERSION=1.1.0' +
           '&REQUEST=GetFeature' +
-          '&TYPENAME=lpis_cultures' +
+          '&TYPENAME=lpis_borders' +
           '&COUNT=100' +
           '&outputformat=geojson' +
           '&SRSNAME=EPSG:5514' +
-          `&BBOX=${extent.join(',')}` +
-          `&<ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"><ogc:PropertyIsEqualTo><ogc:PropertyName>kultura</ogc:PropertyName><ogc:Literal>${kulturaKod}</ogc:Literal></ogc:PropertyIsEqualTo></ogc:Filter>`
+          `&BBOX=${extent ? extent.join(',') : ''}`
+          // + `&<ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"><ogc:PropertyIsEqualTo><ogc:PropertyName>kultura</ogc:PropertyName><ogc:Literal>${kulturaKod}</ogc:Literal></ogc:PropertyIsEqualTo></ogc:Filter>`
         );
         //%3Cgml:Box%3E%3Cgml:coordinates%3E${
         //extent.join(',')
@@ -119,127 +119,130 @@ export class AppService {
       console.log(evt);
     });
     /* Define and update the HsConfig configuration object */
-    this.hsConfig.update({
-      datasources: [
-        /* You need to set up Layman in order to use it. See https://github.com/LayerManager/layman */
-        /*{
+    this.hsConfig.update(
+      {
+        datasources: [
+          /* You need to set up Layman in order to use it. See https://github.com/LayerManager/layman */
+          /*{
           title: 'Layman',
           url: 'http://localhost:8087',
           user: 'anonymous',
           type: 'layman',
           liferayProtocol: 'https',
         },*/
-        {
-          title: 'Micka',
-          url: 'https://hub.sieusoil.eu/cat/csw',
-          language: 'eng',
-          type: 'micka',
-        },
-      ],
-      default_view: new View({
-        projection: this.sjtskProjection,
-        center: transform([16.964, 49.248], 'EPSG:4326', 'EPSG:5514'),
-        zoom: 14,
-      }),
-      /* Use hslayers-server if you need to proxify your requests to other services. See https://www.npmjs.com/package/hslayers-server */
-      proxyPrefix: window.location.hostname.includes('localhost')
-        ? `${window.location.protocol}//${window.location.hostname}:8085/`
-        : '/proxy/',
-      useProxy: true,
-      panelsEnabled: {
-        composition_browser: false,
-        info: false,
-        saveMap: false,
-        legend: false,
-        tripPlanner: false,
-      },
-      componentsEnabled: {
-        basemapGallery: true,
-      },
-      assetsPath: 'assets',
-      symbolizerIcons: [
-        {name: 'beach', url: '/assets/icons/beach17.svg'},
-        {name: 'bicycles', url: '/assets/icons/bicycles.svg'},
-        {name: 'coffee-shop', url: '/assets/icons/coffee-shop1.svg'},
-        {name: 'mountain', url: '/assets/icons/mountain42.svg'},
-        {name: 'warning', url: '/assets/icons/warning.svg'},
-      ],
-      popUpDisplay: 'hover',
-      default_layers: [
-        /* Baselayers */
-        new Tile({
-          source: new OSM(),
-          visible: true,
-          properties: {
-            title: 'OpenStreetMap',
-            base: true,
-            removable: false,
+          {
+            title: 'Micka',
+            url: 'https://hub.sieusoil.eu/cat/csw',
+            language: 'eng',
+            type: 'micka',
           },
+        ],
+        default_view: new View({
+          projection: this.sjtskProjection,
+          center: transform([16.964, 49.248], 'EPSG:4326', 'EPSG:5514'),
+          zoom: 14,
         }),
-        new Tile({
-          properties: {
-            title: 'Ortofoto ČÚZK',
-            base: true,
-            removable: false,
-            thumbnail: 'https://www.agrihub.sk/hsl-ng/img/orto.jpg',
-          },
-          source: new TileWMS({
-            url: 'https://geoportal.cuzk.cz/WMS_ORTOFOTO_PUB/WMService.aspx',
-            params: {
-              LAYERS: 'GR_ORTFOTORGB',
+        /* Use hslayers-server if you need to proxify your requests to other services. See https://www.npmjs.com/package/hslayers-server */
+        proxyPrefix: window.location.hostname.includes('localhost')
+          ? `${window.location.protocol}//${window.location.hostname}:8085/`
+          : '/proxy/',
+        useProxy: true,
+        panelsEnabled: {
+          composition_browser: false,
+          info: false,
+          saveMap: false,
+          legend: false,
+          tripPlanner: false,
+        },
+        componentsEnabled: {
+          basemapGallery: true,
+        },
+        assetsPath: 'assets',
+        symbolizerIcons: [
+          {name: 'beach', url: '/assets/icons/beach17.svg'},
+          {name: 'bicycles', url: '/assets/icons/bicycles.svg'},
+          {name: 'coffee-shop', url: '/assets/icons/coffee-shop1.svg'},
+          {name: 'mountain', url: '/assets/icons/mountain42.svg'},
+          {name: 'warning', url: '/assets/icons/warning.svg'},
+        ],
+        popUpDisplay: 'hover',
+        default_layers: [
+          /* Baselayers */
+          new Tile({
+            source: new OSM(),
+            visible: true,
+            properties: {
+              title: 'OpenStreetMap',
+              base: true,
+              removable: false,
             },
-            attributions: [
-              '© <a href="geoportal.cuzk.cz" target="_blank">ČÚZK</a>',
-            ],
           }),
-          visible: false,
-        }),
-        /* Thematic layers */
-        imageWmsTLayer,
-        new VectorLayer({
-          properties: {
-            title: 'LPIS (WFS)',
-            synchronize: false,
-            cluster: false,
-            inlineLegend: true,
-            editor: {
-              editable: false,
+          new Tile({
+            properties: {
+              title: 'Ortofoto ČÚZK',
+              base: true,
+              removable: false,
+              thumbnail: 'https://www.agrihub.sk/hsl-ng/img/orto.jpg',
             },
-            sld: fieldSld,
-            popUp: {
-              attributes: [
-                'id_dpb',
-                'id_uz',
-                'nkod_dpb',
-                'kultura',
-                'svazitost',
-                'vymeram',
+            source: new TileWMS({
+              url: 'https://geoportal.cuzk.cz/WMS_ORTOFOTO_PUB/WMService.aspx',
+              params: {
+                LAYERS: 'GR_ORTFOTORGB',
+              },
+              attributions: [
+                '© <a href="geoportal.cuzk.cz" target="_blank">ČÚZK</a>',
               ],
+            }),
+            visible: false,
+          }),
+          /* Thematic layers */
+          imageWmsTLayer,
+          new VectorLayer({
+            properties: {
+              title: 'LPIS (WFS)',
+              synchronize: false,
+              cluster: false,
+              inlineLegend: true,
+              editor: {
+                editable: false,
+              },
+              sld: fieldSld,
+              popUp: {
+                attributes: [
+                  'id_dpb',
+                  'id_uz',
+                  'nkod_dpb',
+                  'kultura',
+                  'svazitost',
+                  'vymeram',
+                ],
+              },
+              //path: 'User generated',
             },
-            //path: 'User generated',
-          },
-          minZoom: this.calcService.MIN_LPIS_VISIBLE_ZOOM,
-          opacity: 0.7,
-          source: lpisSource,
-        }),
-        new Tile({
-          properties: {
-            title: 'LPIS (WMS)',
-            queryCapabilities: false,
-          },
-          maxZoom: this.calcService.MIN_LPIS_VISIBLE_ZOOM,
-          source: new TileWMS({
-            url: 'https://gis.lesprojekt.cz/cgi-bin/mapserv?map=/home/dima/maps/foodie/lpis.map',
-            params: {
-              LAYERS: 'lpis_borders', //'lpis_cultures'
-              INFO_FORMAT: undefined,
-              FORMAT: 'image/png; mode=8bit',
+            minZoom: this.calcService.MIN_LPIS_VISIBLE_ZOOM,
+            opacity: 0.7,
+            source: lpisSource,
+          }),
+          new Tile({
+            properties: {
+              title: 'LPIS (WMS)',
+              queryCapabilities: false,
             },
-            crossOrigin: 'anonymous',
+            maxZoom: this.calcService.MIN_LPIS_VISIBLE_ZOOM,
+            source: new TileWMS({
+              url: 'https://gis.lesprojekt.cz/cgi-bin/mapserv?map=/home/dima/maps/foodie/lpis.map',
+              params: {
+                LAYERS: 'lpis_borders', //'lpis_cultures'
+                INFO_FORMAT: undefined,
+                FORMAT: 'image/png; mode=8bit',
+              },
+              crossOrigin: 'anonymous',
+            }),
           }),
-        }),
-      ],
-      translationOverrides: i18n,
-    });
+        ],
+        translationOverrides: i18n,
+      },
+      'default'
+    );
   }
 }

+ 64 - 46
src/app/calculator/calculator.component.html

@@ -1,23 +1,27 @@
-<div [hidden]="!isVisible()" class="card mainpanel">
-  <hs-panel-header name="adjuster" [title]="'CALCULATOR.panelHeader' | translate">
+<div [hidden]="!isVisible()" class="card hs-main-panel">
+  <hs-panel-header name="adjuster"
+    [title]="hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR', 'panelHeader')">
     <extra-buttons>
       <!-- LOADER -->
       <div class="spinner-border spinner spinner-sm mx-2" role="status"
-        title="{{ 'CALCULATOR.loading' | translate }}..." *ngIf="calcService.lpisLoading">
-        <span class="visually-hidden">{{ 'CALCULATOR.loading' | translate }}...</span>
+        title="hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR', 'loading')"
+        *ngIf="calcService.lpisLoading">
+        <span class="visually-hidden">{{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR',
+          'loading')}}...</span>
       </div>
     </extra-buttons>
   </hs-panel-header>
   <div class="card-body">
-    <div class="p-2 center-block">
+    <div class="p-4 m-auto">
       <!-- FIELD & INDEX SELECTION PART -->
       <div *ngIf="!noFieldSelected(); else noField">
         <p *ngIf="data.selectedFieldsProperties.length === 1; else moreFields">
-          {{ 'CALCULATOR.selectedField' | translate}} {{data.selectedFieldsProperties[0]?.['id_dpb'] ?? '?'}}
+          {{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR', 'selectedField')}}
+          {{data.selectedFieldsProperties[0]?.['id_dpb'] ?? '?'}}
         </p>
         <ng-template #moreFields>
           <p>
-            {{ 'CALCULATOR.selectedFields' | translate}}
+            {{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR', 'selectedFields')}}
             <span *ngFor="let props of data.selectedFieldsProperties; last as isLast">
               {{props?.['id_dpb'] ?? '?'}}<ng-container *ngIf="!isLast">,</ng-container>
             </span>
@@ -26,57 +30,71 @@
       </div>
       <ng-template #noField>
         <div>
-          <p class="p-1 text-info">{{ 'CALCULATOR.selectField' | translate}}</p>
-          </div>
+          <p class="p-1 text-info">{{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR', 'selectField')}}
+          </p>
+        </div>
       </ng-template>
-      <p class="p-1 text-warning" *ngIf="!lpisWfsVisible"><i class="icon-warning-sign"></i>&nbsp;{{ 'CALCULATOR.zoomIn' | translate}}</p>
-      <div>
-        <p class="p-1 text-info">{{ 'CALCULATOR.selectMore' | translate}}</p>
-      </div>
-      <div>
-        {{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR', 'selectQuantiles', {quantileCount:
-        calcService.quantileCount})}}:&emsp;
-        <input type="range" min="2" max="10" step="1" [(ngModel)]="calcService.quantileCount">
-      </div>
-      <div>
-        {{ 'CALCULATOR.selectBlur' | translate}}:&emsp;{{calcService.blurValue}}&nbsp;px <span
-          *ngIf="calcService.blurValue === 0">({{ 'CALCULATOR.blurNone' | translate}})</span>
-        <input type="range" min="{{calcService.BLUR_MIN_VALUE}}" max="{{calcService.BLUR_MAX_VALUE}}" step="1"
-          [(ngModel)]="calcService.blurValue">
-      </div>
-      <div>
-        {{ 'CALCULATOR.selectIndex' | translate}}:&emsp;
+      <p class="p-1 text-warning" *ngIf="!lpisWfsVisible"><i
+          class="icon-warning-sign"></i>&nbsp;{{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR',
+        'zoomIn')}}</p>
+      <div class="form-group">
+        {{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR', 'selectIndex')}}:&emsp;
         <select class="form-select" [(ngModel)]="data.selectedProduct" (ngModelChange)="resetDate()">
+          <option selected disabled [ngValue]="null">{{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR',
+            'selectIndexHint')}}</option>
           <option *ngFor="let product of calcService.AVAILABLE_PRODUCTS" [ngValue]="product">{{product}}</option>
         </select>
       </div>
-      <div class="d-flex">
-        <div>
-          <button type="button" class="btn btn-primary btn-lg" (click)="getDates()"
-            [disabled]="noFieldSelected() || noProductSelected()">{{ 'CALCULATOR.getDates' | translate}}</button>
-        </div>
+      <div class="form-group">
+        <label>{{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR',
+          'selectQuantiles')}}:&emsp;{{calcService.quantileCount}}</label>
+        <input type="range" min="2" max="10" step="1" [(ngModel)]="calcService.quantileCount" class="form-range">
+      </div>
+      <div class="form-group">
+        <label>
+          {{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR',
+          'selectBlur')}}:&emsp;{{calcService.blurValue}}&nbsp;px <span
+            *ngIf="calcService.blurValue === 0">({{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR',
+            'blurNone')}})</span>
+        </label>
+        <input type="range" min="{{calcService.BLUR_MIN_VALUE}}" max="{{calcService.BLUR_MAX_VALUE}}" step="1"
+          [(ngModel)]="calcService.blurValue" class="form-range">
+      </div>
+      <div class="form-group d-flex m-auto">
+        <button type="button" class="btn btn-secondary form-control" (click)="getDates()"
+          [disabled]="noFieldSelected() || noProductSelected()">
+          {{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR', 'getDates')}}
+          <div class="spinner-border spinner-sm mx-2" role="status" *ngIf="calcService.datesLoading" aria-hidden="true">
+            <span class="visually-hidden">{{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR',
+              'loading')}}...</span>
+          </div>
+        </button>
         <!-- LOADER -->
-        <div class="spinner-border spinner mx-2" role="status" *ngIf="calcService.datesLoading" aria-hidden="true">
-          <span class="visually-hidden">{{ 'CALCULATOR.loading' | translate }}...</span>
-        </div>
       </div>
       <!-- DATE SELECTION PART -->
       <div [hidden]="noDates()">
-        {{ 'CALCULATOR.selectDate' | translate}}:&emsp;
-        <select class="form-select" [(ngModel)]="calcService.selectedDate" (ngModelChange)="updateRangeSlider($event)">
-          <option *ngFor="let date of calcService.availableDates" [ngValue]="date">{{date}}</option>
-        </select>
-        <!-- TODO: date-picker instead of select -->
-        <fc-date-range-slider [values]="calcService.availableDates"></fc-date-range-slider>
+        <ngb-datepicker #dp [(ngModel)]="calcService.selectedDateCalendar" (ngModelChange)="updateSelectedDate($event)"
+          [dayTemplate]="customDay" class="form-control"></ngb-datepicker>
+        <ng-template #customDay let-date="date" let-currentMonth="currentMonth" let-selected="selected"
+          let-disabled="disabled" let-focused="focused">
+          <span class="custom-day lol" [class.focused]="focused" [class.bg-primary]="selected"
+            [class.hidden]="date.month !== currentMonth" [class.text-muted]="disabled"
+            [class.has-task]="hasDataAvailable(date)">
+            {{ date.day }}
+          </span>
+        </ng-template>
       </div>
       <div class="d-flex" *ngIf="!noDates()">
-        <button type="button" class="btn btn-primary btn-lg" (click)="getZones()" [disabled]="noDateSelected()">{{
-          'CALCULATOR.getZones' | translate }}</button>
-        <!-- LOADER -->
-        <div class="spinner-border spinner mx-2" role="status" *ngIf="calcService.zonesLoading" aria-hidden="true">
-          <span class="visually-hidden">{{ 'CALCULATOR.loading' | translate }}...</span>
-        </div>
+        <button type="button" class="btn btn-primary form-control" (click)="getZones()" [disabled]="noDateSelected()">
+          {{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR', 'getZones')}}
+          <!-- LOADER -->
+          <div class="spinner-border spinner-sm mx-2" role="status" *ngIf="calcService.zonesLoading" aria-hidden="true">
+            <span class="visually-hidden">{{hsLanguageService.getTranslationIgnoreNonExisting('CALCULATOR',
+              'loading')}}...</span>
+          </div>
+        </button>
       </div>
+      <span class="error-message">{{calcService.lastError}}</span>
     </div>
   </div>
 </div>

+ 19 - 0
src/app/calculator/calculator.component.scss

@@ -13,3 +13,22 @@
     width: 1rem;
     height: 1rem;
 }
+
+.custom-day {
+  color: #F0F0F0;
+  pointer-events: none;
+}
+
+.has-task {
+  background-color: #6c757d;
+  padding: 0px 5px;
+  border-radius: 5px;
+  color: #ffffff;
+}
+
+.error-message {
+  text-align: justify;
+  display: block;
+  color: crimson;
+  padding: 1.5rem;
+}

+ 30 - 5
src/app/calculator/calculator.component.ts

@@ -1,4 +1,6 @@
-import {Component, ViewRef} from '@angular/core';
+import {BehaviorSubject} from 'rxjs';
+import {Component, OnInit, ViewRef} from '@angular/core';
+import {NgbDateStruct} from '@ng-bootstrap/ng-bootstrap';
 
 import {
   HsLanguageService,
@@ -15,7 +17,7 @@ import {FieldService} from './field.service';
   templateUrl: './calculator.component.html',
   styleUrls: ['./calculator.component.scss'],
 })
-export class CalculatorComponent implements HsPanelComponent {
+export class CalculatorComponent implements HsPanelComponent, OnInit {
   data: {
     selectedProduct: Index;
     selectedFieldsProperties: {[x: string]: any}[];
@@ -23,6 +25,7 @@ export class CalculatorComponent implements HsPanelComponent {
   lpisWfsVisible: boolean;
   name: 'calculator';
   viewRef: ViewRef;
+  isVisible$ = new BehaviorSubject<boolean>(true);
 
   constructor(
     public calcService: CalculatorService,
@@ -44,6 +47,9 @@ export class CalculatorComponent implements HsPanelComponent {
       this.lpisWfsVisible = false;
     });
   }
+  ngOnInit() {
+    this.data.selectedProduct = null;
+  }
 
   isVisible(): boolean {
     return this.hsLayoutService.panelVisible('calculator');
@@ -54,7 +60,7 @@ export class CalculatorComponent implements HsPanelComponent {
   }
 
   noProductSelected(): boolean {
-    return this.data.selectedProduct === undefined;
+    return this.data.selectedProduct === null;
   }
 
   noDates(): boolean {
@@ -80,10 +86,29 @@ export class CalculatorComponent implements HsPanelComponent {
    */
   resetDate() {
     this.calcService.selectedDate = undefined;
+    this.calcService.selectedDateCalendar = undefined;
     this.calcService.availableDates = undefined;
   }
 
-  updateRangeSlider(value: string) {
-    this.calcService.dateCalendarSelects.next({date: value});
+  updateSelectedDate(value: NgbDateStruct) {
+    this.calcService.selectedDate =
+      value.year + '-' + value.month + '-' + value.day;
+  }
+
+  hasDataAvailable(date: NgbDateStruct): boolean {
+    if (this.calcService && this.calcService.availableDates) {
+      const found = this.calcService.availableDates.filter((item, index) => {
+        return (
+          item.indexOf(
+            `${date.year}-${date.month.toString().padStart(2, '0')}-${date.day
+              .toString()
+              .padStart(2, '0')}`
+          ) == 0
+        );
+      }); // date.year == 2022 && date.month == 6 && date.day == 16;
+      return found.length > 0;
+    }
+
+    return false;
   }
 }

+ 1 - 2
src/app/calculator/calculator.module.ts

@@ -7,7 +7,6 @@ import {TranslateModule} from '@ngx-translate/core';
 import {HsPanelHelpersModule} from 'hslayers-ng';
 
 import {CalculatorComponent} from './calculator.component';
-import {FcDateRangeSliderComponent} from './date-range-slider/date-range-slider.component';
 
 @NgModule({
   schemas: [CUSTOM_ELEMENTS_SCHEMA],
@@ -19,7 +18,7 @@ import {FcDateRangeSliderComponent} from './date-range-slider/date-range-slider.
     TranslateModule,
   ],
   exports: [],
-  declarations: [CalculatorComponent, FcDateRangeSliderComponent],
+  declarations: [CalculatorComponent],
   providers: [],
 })
 export class CalculatorModule {}

+ 26 - 12
src/app/calculator/calculator.service.ts

@@ -1,5 +1,6 @@
 import {HttpClient} from '@angular/common/http';
 import {Injectable} from '@angular/core';
+import {NgbCalendar, NgbDateStruct} from '@ng-bootstrap/ng-bootstrap';
 import {Subject} from 'rxjs';
 import {catchError} from 'rxjs/operators';
 
@@ -15,11 +16,11 @@ import {FieldService} from './field.service';
 import {ZonesService} from './zones.service';
 import {imageWmsTLayer, imageWmsTSource} from './image-wms-t-layer';
 
-export type Index = 'EVI' | 'RVI4S1';
+export type Index = 'EVI' | 'NDVI';
 
 @Injectable({providedIn: 'root'})
 export class CalculatorService {
-  AVAILABLE_PRODUCTS = ['EVI', 'RVI4S1'] as const;
+  AVAILABLE_PRODUCTS = ['EVI', 'NDVI'] as const;
   BLUR_MIN_VALUE = 0 as const;
   BLUR_MAX_VALUE = 5 as const;
   MIN_LPIS_VISIBLE_ZOOM = 15 as const;
@@ -31,7 +32,9 @@ export class CalculatorService {
   lpisLoading = false;
   quantileCount = 4;
   selectedDate: string;
+  selectedDateCalendar: NgbDateStruct;
   viewChanges: Subject<any> = new Subject();
+  lastError = '';
   //selectedProduct;
   private _datesLoading: boolean;
   private _zonesLoading: boolean;
@@ -44,7 +47,8 @@ export class CalculatorService {
     private hsMapService: HsMapService,
     private hsToastService: HsToastService,
     private httpClient: HttpClient,
-    private zonesService: ZonesService
+    private zonesService: ZonesService,
+    private calendar: NgbCalendar
   ) {
     this.dateRangeSelects.subscribe(({date}) => {
       this.selectedDate = date;
@@ -57,7 +61,7 @@ export class CalculatorService {
       this.selectedDate = undefined;
     });
     this.hsEventBus.olMapLoads.subscribe((map) => {
-      map.getView().on('change:resolution', (evt) => {
+      map.map.getView().on('change:resolution', (evt) => {
         this.viewChanges.next(evt.target);
       });
     });
@@ -72,11 +76,12 @@ export class CalculatorService {
    */
   async getDates({product}: {product: Index}) {
     this.availableDates = undefined;
+    this.selectedDateCalendar = null;
     this._datesLoading = true;
     try {
       const data = await this.httpClient
         .get<{dates: string[]}>(
-          (this.proxyEnabled() ? this.hsConfig.proxyPrefix : '') +
+          (this.proxyEnabled() ? this.hsConfig.apps.default.proxyPrefix : '') +
             this.SERVICE_BASE_URL +
             'get_dates?' +
             'product=' +
@@ -124,11 +129,12 @@ export class CalculatorService {
    * Call 'get_zones' API method
    */
   async getZones({product}: {product: Index}) {
+    this.lastError = '';
     this._zonesLoading = true;
     try {
       const data = await this.httpClient
         .post(
-          (this.proxyEnabled() ? this.hsConfig.proxyPrefix : '') +
+          (this.proxyEnabled() ? this.hsConfig.apps.default.proxyPrefix : '') +
             this.SERVICE_BASE_URL +
             'get_zones',
           {
@@ -149,23 +155,28 @@ export class CalculatorService {
         .toPromise();
       console.log('data received!', data);
       this._zonesLoading = false;
-      this.zonesService.updateZones(data, {quantileCount: this.quantileCount});
+      await this.zonesService.updateZones(data, {
+        quantileCount: this.quantileCount,
+      });
       this.updateImageBackground(this.selectedDate);
     } catch (err) {
+      const errLoadingZones =
+        this.hsLanguageService.getTranslationIgnoreNonExisting(
+          'CALCULATOR',
+          'errorLoadingZones'
+        );
       this._zonesLoading = false;
       this.hsToastService.createToastPopupMessage(
         this.hsLanguageService.getTranslationIgnoreNonExisting(
           'CALCULATOR',
           'errorLoading'
         ),
-        this.hsLanguageService.getTranslationIgnoreNonExisting(
-          'CALCULATOR',
-          'errorLoadingZones'
-        ),
+        errLoadingZones,
         {
           toastStyleClasses: 'bg-warning text-dark',
         }
       );
+      this.lastError = errLoadingZones;
       console.error('Somethin fucked up!');
       console.log(err);
     }
@@ -182,6 +193,9 @@ export class CalculatorService {
   }
 
   private proxyEnabled(): boolean {
-    return this.hsConfig.useProxy === undefined || this.hsConfig.useProxy;
+    return (
+      this.hsConfig.apps.default.useProxy === undefined ||
+      this.hsConfig.apps.default.useProxy
+    );
   }
 }

+ 0 - 1
src/app/calculator/date-range-slider/date-range-slider.component.html

@@ -1 +0,0 @@
-<input #slider type="range" min="{{getMinInMillis()}}" max="{{getMaxInMillis()}}" step="any" (input)="pickNearestDate($event)">

+ 0 - 85
src/app/calculator/date-range-slider/date-range-slider.component.ts

@@ -1,85 +0,0 @@
-import {Component, ElementRef, Input, ViewChild} from '@angular/core';
-
-import {CalculatorService} from '../calculator.service';
-
-@Component({
-  selector: 'fc-date-range-slider',
-  templateUrl: './date-range-slider.component.html',
-})
-export class FcDateRangeSliderComponent {
-  @ViewChild('slider') sliderElement: ElementRef;
-  @Input() values: Array<string>;
-
-  constructor(public calcService: CalculatorService) {
-    this.calcService.dateCalendarSelects.subscribe(({date}) => {
-      this.sliderElement.nativeElement.value =
-        this.parseISOLocal(date).getTime();
-    });
-  }
-
-  getMinInMillis() {
-    return this.parseISOLocal(this.getFirstDate()).getTime();
-  }
-
-  getMaxInMillis() {
-    return this.parseISOLocal(this.getLastDate()).getTime();
-  }
-
-  getFirstDate() {
-    return this.values?.[0] ?? '2000-01-01';
-  }
-
-  getLastDate() {
-    return this.values?.slice(-1)?.pop() ?? '3000-01-01';
-  }
-
-  // Inspired by https://stackoverflow.com/questions/8584902/get-the-closest-number-out-of-an-array
-  pickNearestDate(selected) {
-    selected = selected.target.value;
-    const nearestDate = this.values
-      .map((val) => this.parseISOLocal(val).getTime())
-      .reduce((prev, curr) => {
-        return Math.abs(curr - selected) < Math.abs(prev - selected)
-          ? curr
-          : prev;
-      });
-    this.calcService.dateRangeSelects.next({
-      date: this.dateToISOLocal(nearestDate),
-    });
-  }
-
-  // Format date as YYYY-MM-DD
-  // Inspired by https://stackoverflow.com/questions/58153053/how-can-i-create-a-slider-that-would-display-date-and-is-there-a-way-to-make-ma
-  private dateToISOLocal(dateInMillis: number) {
-    const date = new Date(dateInMillis);
-    const form = (n) => ('0' + n).slice(-2);
-    return (
-      date.getFullYear() +
-      '-' +
-      form(date.getMonth() + 1) +
-      '-' +
-      form(date.getDate()) +
-      'T' +
-      form(date.getHours()) +
-      ':' +
-      form(date.getMinutes()) +
-      ':' +
-      form(date.getSeconds())
-    );
-  }
-
-  // Parse date in YYYY-MM-DD format as local date
-  // Inspired by https://stackoverflow.com/questions/58153053/how-can-i-create-a-slider-that-would-display-date-and-is-there-a-way-to-make-ma
-  private parseISOLocal(s: string) {
-    const [y, m, d] = s.split('T')[0].split('-');
-    const [hh, mm, ss] = s.split('T')[1]?.split(':') ?? [0, 0, 0];
-    return new Date(
-      Number(y),
-      Number(m) - 1,
-      Number(d),
-      Number(hh),
-      Number(mm),
-      Number(ss)
-    );
-  }
-}

+ 4 - 6
src/app/calculator/field.service.ts

@@ -19,11 +19,9 @@ export class FieldService {
     private hsMapService: HsMapService
   ) {
     this.hsEventBus.vectorQueryFeatureSelection.subscribe((data) => {
-      const features = data.selector
-        .getFeatures()
-        .getArray()
-        .filter((feature) => this.isValidGeometry(feature))
-        .filter((feature) => this.isValidLayer(feature));
+      const features = data.selector.getFeatures().getArray();
+      //  .filter((feature) => this.isValidGeometry(feature))
+      //  .filter((feature) => this.isValidLayer(feature));
       if (features.length === 0) {
         return;
       }
@@ -78,7 +76,7 @@ export class FieldService {
    * Check whether user clicked into one of selectable layers
    */
   isValidLayer(feature) {
-    const layer = this.hsMapService.getLayerForFeature(feature);
+    const layer = this.hsMapService.getLayerForFeature(feature, null);
     return this.SELECTABLE_LAYERS.includes(layer.get('title'));
   }
 

+ 67 - 18
src/app/calculator/zones.service.ts

@@ -1,11 +1,19 @@
+import SLDParser from 'geostyler-sld-parser';
 import {Fill, Stroke, Style} from 'ol/style';
 import {GeoJSON} from 'ol/format';
 import {Geometry} from 'ol/geom';
 import {Injectable} from '@angular/core';
+// eslint-disable-next-line import/named
+import {StyleFunction} from 'ol/style/Style';
 import {Vector as VectorLayer} from 'ol/layer';
 import {Vector as VectorSource} from 'ol/source';
 
-import {HsAddDataService} from 'hslayers-ng';
+import {
+  HsAddDataService,
+  HsLayerExt,
+  HsLayerManagerService,
+  HsStylerService,
+} from 'hslayers-ng';
 
 @Injectable({providedIn: 'root'})
 export class ZonesService {
@@ -93,8 +101,13 @@ export class ZonesService {
     this.QUANTILE_COLORS_9,
     this.QUANTILE_COLORS_10,
   ] as const;
+  sldParser: SLDParser;
 
-  constructor(private hsAddDataService: HsAddDataService) {
+  constructor(
+    private hsLayerManagerService: HsLayerManagerService,
+    private hsAddDataService: HsAddDataService,
+    private hsStylerService: HsStylerService
+  ) {
     this.zonesStyle = (feature) =>
       new Style({
         fill: new Fill({
@@ -102,33 +115,69 @@ export class ZonesService {
         }),
         stroke: new Stroke(),
       });
+
+    this.sldParser = new SLDParser();
   }
 
-  updateZones(zones, {quantileCount}): void {
-    if (!this.zonesLayer) {
-      this.zonesSource = new VectorSource();
-      this.zonesLayer = new VectorLayer({
-        properties: {
-          title: 'Zones',
-          path: 'Results',
-          popUp: {
-            attributes: ['quantile'],
-          },
-        },
-        style: this.zonesStyle,
-        source: this.zonesSource,
-      });
-      this.hsAddDataService.addLayer(this.zonesLayer);
+  async updateZones(zones, {quantileCount}): Promise<void> {
+    if (this.zonesLayer) {
+      this.hsLayerManagerService.get(null).map.removeLayer(this.zonesLayer);
     }
+    this.zonesSource = new VectorSource();
+    this.zonesLayer = new VectorLayer({
+      properties: {
+        title: 'Zones',
+        path: 'Results',
+        popUp: {
+          attributes: ['quantile'],
+        },
+      },
+      //style: this.zonesStyle,
+      source: this.zonesSource,
+    });
     this.zonesSource.clear();
     this.updateZonesStyle(quantileCount);
-    this.zonesLayer.setStyle(this.zonesStyle);
+
+    const zonesStyleObj = {name: 'Zones', rules: []};
+    zonesStyleObj.rules = this.getSymbolizerRules(quantileCount);
+    const {output: sld} = await this.sldParser.writeStyle(zonesStyleObj);
+
+    this.zonesLayer.set('sld', sld);
+    const style: Style | Style[] | StyleFunction =
+      await this.hsStylerService.geoStylerStyleToOlStyle(zonesStyleObj);
+    this.zonesLayer.setStyle(style);
     this.zonesSource.addFeatures(
       new GeoJSON().readFeatures(zones, {
         dataProjection: 'EPSG:4326',
         featureProjection: 'EPSG:5514',
       })
     );
+    this.hsAddDataService.addLayer(this.zonesLayer, null);
+  }
+
+  private getSymbolizerRules(classes: number): Array<any> {
+    const colorRamp = this.QUANTILE_COLORS_MATRIX[classes - 2];
+    const rules = [];
+
+    for (let i = 0; i < colorRamp.length; i++) {
+      const ruleIdx = (i + 1).toString();
+
+      rules[i] = {
+        name: ruleIdx,
+        filter: ['==', 'quantile', ruleIdx],
+        symbolizers: [
+          {
+            kind: 'Fill',
+            color: colorRamp[i],
+            //  opacity: 0,
+            //  outlineColor: "#505050",
+            //  outlineWidth: 1
+          },
+        ],
+      };
+    }
+
+    return rules;
   }
 
   private updateZonesStyle(classes: number) {

+ 14 - 10
src/app/translations.json

@@ -6,17 +6,19 @@
       "getZones": "ZÍSKAT ZÓNY",
       "errorLoading": "Chyba při načítání dat",
       "errorLoadingDates": "Nebylo možné načíst seznam možných dat ze serveru. Zkuste to prosím později.",
-      "errorLoadingZones": "Nebylo možné načíst zóny pole ze serveru. Zkuste to prosím později.",
+      "errorLoadingZones": "Nebylo možné načíst zóny pole ze serveru. Pravděpodobně z důvodu přílišné oblačnosti snímku. Zkuste jiný datum.",
       "loading": "Načítám",
       "panelHeader": "Výpočet indexů pole",
-      "selectBlur": "Chci vyhlazení",
+      "selectBlur": "Vyhlazení hran zón",
       "selectDate": "Chci datum",
-      "selectField": "Pole vyberte kliknutím do mapy.",
+      "selectField": "Vyberte pole kliknutím do mapy. Více polí můžete vybrat podržením klávesy SHIFT.",
       "selectMore": "Více polí můžete vybrat podržením klávesy SHIFT",
       "selectedField": "Vybráno pole",
       "selectedFields": "Vybrána pole",
-      "selectIndex": "Chci vypočítat index",
-      "selectQuantiles": "Chci {{quantileCount}} kvantilů",
+      "selectFieldAndIndex": "Nejprve vyberte index a pole v mapě",
+      "selectIndex": "Vypočítat index",
+      "selectIndexHint": "Vyberte z dostupných indexů",
+      "selectQuantiles": "Počet kvantilů",
       "zoomIn": "Pro výběr pole je potřeba mapu přiblížit."
     }
   },
@@ -27,17 +29,19 @@
       "getZones": "GET ZONES",
       "errorLoading": "Error loading data",
       "errorLoadingDates": "It was not possible to load available dates from the server. Please, try again later.",
-      "errorLoadingZones": "It was not possible to load field zones from the server. Please, try again later.",
+      "errorLoadingZones": "It was not possible to load field zones from the server. This is probably due to the high cloudiness of the source image. Try another date.",
       "loading": "Loading",
       "panelHeader": "Field calculation",
-      "selectBlur": "I want to smooth by",
+      "selectBlur": "Smoothing zone edges",
       "selectDate": "I want a date",
-      "selectField": "Select a field by clicking in the map.",
+      "selectField": "Select a field by clicking in the map. You can select more fields by press and holding the SHIFT key.",
       "selectMore": "You can select more fields by press and holding the SHIFT key",
       "selectedField": "Selected field",
       "selectedFields": "Selected fields",
-      "selectIndex": "I want to calculate index",
-      "selectQuantiles": "I want {{quantileCount}} quantiles",
+      "selectFieldAndIndex": "Select an index and a field in the map to continue",
+      "selectIndex": "Calculate index",
+      "selectIndexHint": "Select one of the available indices",
+      "selectQuantiles": "Quantiles count",
       "zoomIn": "In order to select the field, you must zoom the map in."
     }
   }

+ 20 - 0
src/index.html

@@ -6,6 +6,26 @@
   <base href="/">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="icon" type="image/x-icon" href="favicon.ico">
+  <style>
+
+    ngb-datepicker-navigation-select > .form-select {
+      height: 2.5rem !important;
+    }
+
+    .ngb-dp-day {
+      width: 3.3rem !important;
+    }
+
+    .ngb-dp-weekday {
+      width: 3.1rem !important;
+    }
+
+    .ngb-dp-weekdays {
+      color: #025797;
+      font-weight: 200;
+      max-width: 22rem;
+    }
+  </style>
 </head>
 <body style="margin: 0;">
   <application-root></application-root>

部分文件因文件數量過多而無法顯示