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'; import { HsCompositionsParserService, HsConfig, HsEventBusService, HsLanguageService, HsMapService, HsToastService, } from 'hslayers-ng'; import {FcFieldService} from './field.service'; import {FcZonesService} from './zones.service'; import {imageWmsTLayer, imageWmsTSource} from './image-wms-t-layer'; /** * Set of precalculated indices */ const INDICES_PRE_FLIGHT = ['EVI', 'NDVI', 'RVI4S1'] as const; /** * Set of indices which are calculated on the fly */ const INDICES_ON_THE_FLY = ['NDWI', 'NDTI', 'NDRE'] as const; export type IndexPreFlight = typeof INDICES_PRE_FLIGHT[number]; export type IndexOnTheFly = typeof INDICES_ON_THE_FLY[number]; export type Index = IndexPreFlight | IndexOnTheFly; @Injectable({providedIn: 'root'}) export class FcCalculatorService { AVAILABLE_PRODUCTS = [...INDICES_PRE_FLIGHT, ...INDICES_ON_THE_FLY].sort(); BLUR_MIN_VALUE = 1 as const; BLUR_MAX_VALUE = 5 as const; MIN_LPIS_VISIBLE_ZOOM = 15 as const; SERVICE_BASE_URL = 'https://fieldcalc.lesprojekt.cz/' as const; availableDates: Array; blurValue = 1; dateRangeSelects: Subject<{date: string}> = new Subject(); dateCalendarSelects: Subject<{date: string}> = new Subject(); lpisLoading = false; quantileCount = 4; selectedDate: string; selectedDateCalendar: NgbDateStruct; viewChanges: Subject = new Subject(); lastError = ''; //selectedProduct; private _datesLoading: boolean; private _zonesLoading: boolean; constructor( private fieldService: FcFieldService, private hsCompositionsParserService: HsCompositionsParserService, private hsConfig: HsConfig, private hsEventBus: HsEventBusService, private hsLanguageService: HsLanguageService, private hsMapService: HsMapService, private hsToastService: HsToastService, private httpClient: HttpClient, private zonesService: FcZonesService, private calendar: NgbCalendar ) { this.dateRangeSelects.subscribe(({date}) => { this.selectedDate = date; }); /** * When new field is selected, clear all other params */ this.fieldService.fieldSelects.subscribe(() => { this.availableDates = undefined; this.selectedDate = undefined; }); this.hsEventBus.olMapLoads.subscribe((map) => { map.map.getView().on('change:resolution', (evt) => { this.viewChanges.next(evt.target); }); }); } noDates(): boolean { return this.availableDates === undefined; } /** * Call 'get_dates' API method */ 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.apps.default.proxyPrefix : '') + this.SERVICE_BASE_URL + 'get_dates?' + 'product=' + product + '¢roid=' + JSON.stringify(this.fieldService.getSelectedFieldCentroid()) ) .toPromise(); this._datesLoading = false; console.log('data received!', 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; } catch (err) { this._datesLoading = false; this.hsToastService.createToastPopupMessage( this.hsLanguageService.getTranslationIgnoreNonExisting( 'CALCULATOR', 'errorLoading' ), this.hsLanguageService.getTranslationIgnoreNonExisting( 'CALCULATOR', 'errorLoadingDates' ), { toastStyleClasses: 'bg-warning text-dark', } ); console.error('Somethin fucked up!'); console.log(err); } } get datesLoading() { return this._datesLoading; } get zonesLoading() { return this._zonesLoading; } /** * Call 'get_zones' or 'get_zones_exp' API method */ async getZones({product}: {product: Index}) { this.lastError = ''; this._zonesLoading = true; const endpoint = INDICES_PRE_FLIGHT.includes(product as IndexPreFlight) ? 'get_zones' : 'get_zones_exp'; try { const data = await this.httpClient .post( (this.proxyEnabled() ? this.hsConfig.apps.default.proxyPrefix : '') + this.SERVICE_BASE_URL + endpoint, { product, number_of_quantiles: this.quantileCount, blur: this.blurValue, date: this.selectedDate, format: 'geojson', geojson: this.fieldService.getSelectedFieldGeoJSON(), }, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, } ) .toPromise(); console.log('data received!', data); // in case of unreliable data the return value is {"error": "data is not reliable."} // TODO: show this error message to user if (data['error']) { throw new Error(data['error']); } this._zonesLoading = false; await this.zonesService.updateZones(data, { quantileCount: this.quantileCount, }); } catch (err) { const errLoadingZones = this.hsLanguageService.getTranslationIgnoreNonExisting( 'CALCULATOR', 'errorLoadingZones' ); this._zonesLoading = false; this.hsToastService.createToastPopupMessage( this.hsLanguageService.getTranslationIgnoreNonExisting( 'CALCULATOR', 'errorLoading' ), errLoadingZones, { toastStyleClasses: 'bg-warning text-dark', } ); this.lastError = errLoadingZones; console.error('Somethin fucked up!'); console.log(err); } } /** * Updates WMS-t layer with the source image * @param date - ISO date */ updateImageBackground(date: string) { const isoTime = date.split('T')[0].replace(/-/g, ''); imageWmsTSource.updateParams({time: isoTime}); imageWmsTLayer.setVisible(true); } /** * Adds all layers from a provided composition to the map, optionally into a provided folder * @param compositionLink - URL of the composition record * @param path - Folder name */ addLayersFromComposition(compositionLink: string, {path}) { const composition = []; //TODO: get composition object from compositionLink URL if (path) { for (const layer of composition) { //TODO: add path } } } private proxyEnabled(): boolean { return ( this.hsConfig.apps.default.useProxy === undefined || this.hsConfig.apps.default.useProxy ); } }