ソースを参照

✨ add really cool date range slider

jmacura 4 年 前
コミット
f5790f65de

+ 6 - 5
src/app/calculator/calculator.component.html

@@ -8,21 +8,22 @@
           <option *ngFor="let product of calcService.availableProducts" [ngValue]="product">{{product}}</option>
         </select>
       </div>
-      <div>
+      <div class="center-block">
         <button type="button" class="btn btn-primary btn-lg" (click)="getDates()"
           [disabled]="noFieldSelected() || noProductSelected()">{{ 'CALCULATOR.getDates' | translate}}</button>
       </div>
       <div [hidden]="noDates()">
         {{ 'CALCULATOR.selectDate' | translate}}:&emsp;
-        <select class="form-select" [(ngModel)]="data.selectedDate">
+        <select class="form-select" [(ngModel)]="calcService.selectedDate" (ngModelChange)="updateRangeSlider($event)">
           <option *ngFor="let date of calcService.availableDates" [ngValue]="date">{{date}}</option>
         </select>
-        TODO: SLIDER
+        <!-- TODO: date-picker instead of select -->
+        <fc-date-range-slider [values]="calcService.availableDates"></fc-date-range-slider>
       </div>
-      <div>
+      <div class="center-block">
         <button type="button" class="btn btn-primary btn-lg" (click)="getZones()"
           [disabled]="noDateSelected()">{{ 'CALCULATOR.getZones' | translate }}</button>
       </div>
     </div>
   </div>
-</div>
+</div>

+ 7 - 7
src/app/calculator/calculator.component.ts

@@ -1,4 +1,4 @@
-import {Component, OnInit, ViewRef} from '@angular/core';
+import {Component, ViewRef} from '@angular/core';
 
 import {HsLayoutService, HsPanelComponent} from 'hslayers-ng';
 
@@ -8,9 +8,8 @@ import {CalculatorService, Index} from './calculator.service';
   selector: 'calculator-panel',
   templateUrl: './calculator.component.html',
 })
-export class CalculatorComponent implements HsPanelComponent, OnInit {
+export class CalculatorComponent implements HsPanelComponent {
   data: {
-    selectedDate: string;
     selectedProduct: Index;
   };
   name: 'calculator';
@@ -21,8 +20,6 @@ export class CalculatorComponent implements HsPanelComponent, OnInit {
     public hsLayoutService: HsLayoutService
   ) {}
 
-  ngOnInit() {}
-
   isVisible(): boolean {
     return this.hsLayoutService.panelVisible('calculator');
   }
@@ -40,7 +37,7 @@ export class CalculatorComponent implements HsPanelComponent, OnInit {
   }
 
   noDateSelected(): boolean {
-    return this.data.selectedDate === undefined;
+    return this.calcService.selectedDate === undefined;
   }
 
   getDates() {
@@ -50,7 +47,10 @@ export class CalculatorComponent implements HsPanelComponent, OnInit {
   getZones() {
     this.calcService.getZones({
       product: this.data.selectedProduct,
-      date: this.data.selectedDate,
     });
   }
+
+  updateRangeSlider(value: string) {
+    this.calcService.dateCalendarSelects.next({date: value});
+  }
 }

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

@@ -7,6 +7,7 @@ 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],
@@ -18,7 +19,7 @@ import {CalculatorComponent} from './calculator.component';
     TranslateModule,
   ],
   exports: [],
-  declarations: [CalculatorComponent],
+  declarations: [CalculatorComponent, FcDateRangeSliderComponent],
   providers: [],
 })
-export class CalculatorModule { }
+export class CalculatorModule {}

+ 14 - 2
src/app/calculator/calculator.service.ts

@@ -1,5 +1,6 @@
 import {HttpClient} from '@angular/common/http';
 import {Injectable} from '@angular/core';
+import {Subject} from 'rxjs';
 import {catchError} from 'rxjs/operators';
 
 import {HsConfig} from 'hslayers-ng';
@@ -12,7 +13,11 @@ export type Index = 'EVI' | 'RVI4S1';
 export class CalculatorService {
   availableDates: Array<string>;
   availableProducts = ['EVI', 'RVI4S1'];
+  dateRangeSelects: Subject<{date: string}> = new Subject();
+  dateCalendarSelects: Subject<{date: string}> = new Subject();
+  selectedDate;
   selectedField;
+  //selectedProduct;
   serviceBaseUrl = 'https://fieldcalc.lesprojekt.cz/';
   //TODO: temporary hard-coded hack
   centroid = {type: 'Point', coordinates: [16.944, 49.228]};
@@ -41,6 +46,9 @@ export class CalculatorService {
   ) {
     //TODO: temporary hard-coded hack
     this.selectedField = this.centroid;
+    this.dateRangeSelects.subscribe(({date}) => {
+      this.selectedDate = date;
+    });
   }
 
   noFieldSelected(): boolean {
@@ -67,6 +75,10 @@ export class CalculatorService {
       console.log('data received!');
       console.log(data);
       this.availableDates = data.dates;
+      /* Any previously selected date must be cleaned up
+       * so it won't get sent to the API as a wrong param
+       */
+      this.selectedDate = undefined;
       //TODO: temporary hard-coded hack
       this.selectedField = this.field;
     } catch (err) {
@@ -75,7 +87,7 @@ export class CalculatorService {
     }
   }
 
-  async getZones({product, date}: {product: Index; date: string}) {
+  async getZones({product}: {product: Index}) {
     try {
       const data = await this.httpClient
         .post(
@@ -84,7 +96,7 @@ export class CalculatorService {
             'get_zones',
           {
             product,
-            date,
+            date: this.selectedDate,
             format: 'geojson',
             geometry: this.selectedField,
           },

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

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

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

@@ -0,0 +1,85 @@
+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)
+    );
+  }
+}