raain-app 1.6.21 → 1.6.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Injectable, EventEmitter, Component, Input, Output, ViewChild, HostListener, ChangeDetectionStrategy, Pipe, Directive, NgModule, Optional, SkipSelf } from '@angular/core';
3
- import { CartesianTools, GaugeNode, SpeedMatrixContainer, RadarNode, CartesianMeasureValue, TeamNode, Link, EventNode, BuildQueryString, RainNode, RainComputationMap, RainComputationQuality, GaugeMeasure } from 'raain-model';
3
+ import { CartesianTools, GaugeNode, SpeedMatrixContainer, RadarNode, CartesianMeasureValue, Link, EventNode, BuildQueryString, TeamNode, RainNode, RainComputationMap, RainComputationQuality, GaugeMeasure } from 'raain-model';
4
4
  import { RaainDivIcon, ElementsFactory, MapLatLng, ChartScaleColors, ScaleElementInput, CompareElementInput, ConfigurationElementInput, Tools, DateRange, DateStatusElementInput, DynamicDateStatusElementInput, SpeedMatrixElementInput, EarthMapElementInput, TimeframeContainers, CartesianMapValue, PolarMapValue } from 'raain-ui';
5
5
  import * as i2$1 from 'fidj-angular';
6
6
  import { LocalStorage, LoggerLevelEnum } from 'fidj-angular';
@@ -1303,10 +1303,10 @@ class RaainCompareStackComponent {
1303
1303
  }
1304
1304
  }
1305
1305
  RaainCompareStackComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: RaainCompareStackComponent, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
1306
- RaainCompareStackComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: RaainCompareStackComponent, selector: "raain-compare-stack", inputs: { compareManager: "compareManager", currentHeight: "currentHeight", cumulative: "cumulative" }, outputs: { selectedPoint: "selectedPoint" }, ngImport: i0, template: "<div *ngIf=\"cumulative\">\n <ion-col size-md=\"6\" size-xs=\"12\">\n <ion-card class=\"card-compare\">\n <ion-card-header>Cumulative\n [{{ compareManager.compareDates[0] | date:'yyyy-MM-dd HH:mm' }}\n - {{ compareManager.compareDates[compareManager.compareDates.length - 1] | date:'yyyy-MM-dd HH:mm' }}]\n </ion-card-header>\n <ion-card-content *ngIf=\"compareManager.globalComparePoints?.length\">\n <raain-compare\n (selectedPoint)=\"onClick($event)\"\n [compareIndex]=\"0\"\n [currentHeight]=\"500\"\n [pointMax]=\"compareManager.globalComparePointsMax\"\n [points]=\"compareManager.globalComparePoints\"></raain-compare>\n </ion-card-content>\n </ion-card>\n </ion-col>\n\n <ion-button (click)=\"exportCumulativeToCsv()\" *ngIf=\"compareManager.globalComparePoints?.length\" fill=\"outline\" size=\"small\">\n <ion-icon name=\"download-outline\" slot=\"start\"></ion-icon>\n Export CSV\n </ion-button>\n</div>\n\n<div *ngIf=\"!cumulative\">\n <ion-col *ngFor=\"let compare of compareManager?.uiCompares; index as compareIndex\" size-md=\"6\" size-xs=\"12\">\n <ion-card class=\"card-compare\">\n <ion-card-header>Gauges filled {{ compare.name }} {{ compare.remarks }}\n </ion-card-header>\n <ion-card-content *ngIf=\"compare.comparePoints?.length\">\n <raain-compare\n (selectedPoint)=\"onClick($event)\"\n [compareIndex]=\"compareIndex\"\n [currentHeight]=\"500\"\n [pointMax]=\"compare.comparePointsMax\"\n [points]=\"compare.comparePoints\"></raain-compare>\n </ion-card-content>\n </ion-card>\n </ion-col>\n\n <ion-button (click)=\"exportGranularToCsv()\" *ngIf=\"compareManager.uiCompares?.length\" fill=\"outline\" size=\"small\">\n <ion-icon name=\"download-outline\" slot=\"start\"></ion-icon>\n Export CSV\n </ion-button>\n</div>\n", styles: ["#card-matrix{width:170px}.card-compare{min-width:300px}ion-card-header{padding:0 0 0 10px}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i4.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i4.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i4.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i4.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i4.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i4.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: RaainCompareComponent, selector: "raain-compare", inputs: ["points", "pointMax", "remarks", "compareIndex", "currentHeight", "currentWidth"], outputs: ["selectedPoint"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }] });
1306
+ RaainCompareStackComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: RaainCompareStackComponent, selector: "raain-compare-stack", inputs: { compareManager: "compareManager", currentHeight: "currentHeight", cumulative: "cumulative" }, outputs: { selectedPoint: "selectedPoint" }, ngImport: i0, template: "<div *ngIf=\"cumulative\">\n <ion-col size-md=\"6\" size-xs=\"12\">\n <ion-card class=\"card-compare\">\n <ion-card-header>Cumulative\n [{{ compareManager.compareDates[0] | date:'yyyy-MM-dd HH:mm' }}\n - {{ compareManager.compareDates[compareManager.compareDates.length - 1] | date:'yyyy-MM-dd HH:mm' }}[\n </ion-card-header>\n <ion-card-content *ngIf=\"compareManager.globalComparePoints?.length\">\n <raain-compare\n (selectedPoint)=\"onClick($event)\"\n [compareIndex]=\"0\"\n [currentHeight]=\"500\"\n [pointMax]=\"compareManager.globalComparePointsMax\"\n [points]=\"compareManager.globalComparePoints\"></raain-compare>\n </ion-card-content>\n </ion-card>\n </ion-col>\n\n <ion-button (click)=\"exportCumulativeToCsv()\" *ngIf=\"compareManager.globalComparePoints?.length\" fill=\"outline\"\n size=\"small\">\n <ion-icon name=\"download-outline\" slot=\"start\"></ion-icon>\n Export CSV\n </ion-button>\n</div>\n\n<div *ngIf=\"!cumulative\">\n <ion-col *ngFor=\"let compare of compareManager?.uiCompares; index as compareIndex\" size-md=\"6\" size-xs=\"12\">\n <ion-card class=\"card-compare\">\n <ion-card-header>Gauges filled {{ compare.name }} {{ compare.remarks }}\n </ion-card-header>\n <ion-card-content *ngIf=\"compare.comparePoints?.length\">\n <raain-compare\n (selectedPoint)=\"onClick($event)\"\n [compareIndex]=\"compareIndex\"\n [currentHeight]=\"500\"\n [pointMax]=\"compare.comparePointsMax\"\n [points]=\"compare.comparePoints\"></raain-compare>\n </ion-card-content>\n </ion-card>\n </ion-col>\n\n <ion-button (click)=\"exportGranularToCsv()\" *ngIf=\"compareManager.uiCompares?.length\" fill=\"outline\" size=\"small\">\n <ion-icon name=\"download-outline\" slot=\"start\"></ion-icon>\n Export CSV\n </ion-button>\n</div>\n", styles: ["#card-matrix{width:170px}.card-compare{min-width:300px}ion-card-header{padding:0 0 0 10px}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i4.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i4.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i4.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i4.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i4.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i4.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: RaainCompareComponent, selector: "raain-compare", inputs: ["points", "pointMax", "remarks", "compareIndex", "currentHeight", "currentWidth"], outputs: ["selectedPoint"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }] });
1307
1307
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: RaainCompareStackComponent, decorators: [{
1308
1308
  type: Component,
1309
- args: [{ selector: 'raain-compare-stack', template: "<div *ngIf=\"cumulative\">\n <ion-col size-md=\"6\" size-xs=\"12\">\n <ion-card class=\"card-compare\">\n <ion-card-header>Cumulative\n [{{ compareManager.compareDates[0] | date:'yyyy-MM-dd HH:mm' }}\n - {{ compareManager.compareDates[compareManager.compareDates.length - 1] | date:'yyyy-MM-dd HH:mm' }}]\n </ion-card-header>\n <ion-card-content *ngIf=\"compareManager.globalComparePoints?.length\">\n <raain-compare\n (selectedPoint)=\"onClick($event)\"\n [compareIndex]=\"0\"\n [currentHeight]=\"500\"\n [pointMax]=\"compareManager.globalComparePointsMax\"\n [points]=\"compareManager.globalComparePoints\"></raain-compare>\n </ion-card-content>\n </ion-card>\n </ion-col>\n\n <ion-button (click)=\"exportCumulativeToCsv()\" *ngIf=\"compareManager.globalComparePoints?.length\" fill=\"outline\" size=\"small\">\n <ion-icon name=\"download-outline\" slot=\"start\"></ion-icon>\n Export CSV\n </ion-button>\n</div>\n\n<div *ngIf=\"!cumulative\">\n <ion-col *ngFor=\"let compare of compareManager?.uiCompares; index as compareIndex\" size-md=\"6\" size-xs=\"12\">\n <ion-card class=\"card-compare\">\n <ion-card-header>Gauges filled {{ compare.name }} {{ compare.remarks }}\n </ion-card-header>\n <ion-card-content *ngIf=\"compare.comparePoints?.length\">\n <raain-compare\n (selectedPoint)=\"onClick($event)\"\n [compareIndex]=\"compareIndex\"\n [currentHeight]=\"500\"\n [pointMax]=\"compare.comparePointsMax\"\n [points]=\"compare.comparePoints\"></raain-compare>\n </ion-card-content>\n </ion-card>\n </ion-col>\n\n <ion-button (click)=\"exportGranularToCsv()\" *ngIf=\"compareManager.uiCompares?.length\" fill=\"outline\" size=\"small\">\n <ion-icon name=\"download-outline\" slot=\"start\"></ion-icon>\n Export CSV\n </ion-button>\n</div>\n", styles: ["#card-matrix{width:170px}.card-compare{min-width:300px}ion-card-header{padding:0 0 0 10px}\n"] }]
1309
+ args: [{ selector: 'raain-compare-stack', template: "<div *ngIf=\"cumulative\">\n <ion-col size-md=\"6\" size-xs=\"12\">\n <ion-card class=\"card-compare\">\n <ion-card-header>Cumulative\n [{{ compareManager.compareDates[0] | date:'yyyy-MM-dd HH:mm' }}\n - {{ compareManager.compareDates[compareManager.compareDates.length - 1] | date:'yyyy-MM-dd HH:mm' }}[\n </ion-card-header>\n <ion-card-content *ngIf=\"compareManager.globalComparePoints?.length\">\n <raain-compare\n (selectedPoint)=\"onClick($event)\"\n [compareIndex]=\"0\"\n [currentHeight]=\"500\"\n [pointMax]=\"compareManager.globalComparePointsMax\"\n [points]=\"compareManager.globalComparePoints\"></raain-compare>\n </ion-card-content>\n </ion-card>\n </ion-col>\n\n <ion-button (click)=\"exportCumulativeToCsv()\" *ngIf=\"compareManager.globalComparePoints?.length\" fill=\"outline\"\n size=\"small\">\n <ion-icon name=\"download-outline\" slot=\"start\"></ion-icon>\n Export CSV\n </ion-button>\n</div>\n\n<div *ngIf=\"!cumulative\">\n <ion-col *ngFor=\"let compare of compareManager?.uiCompares; index as compareIndex\" size-md=\"6\" size-xs=\"12\">\n <ion-card class=\"card-compare\">\n <ion-card-header>Gauges filled {{ compare.name }} {{ compare.remarks }}\n </ion-card-header>\n <ion-card-content *ngIf=\"compare.comparePoints?.length\">\n <raain-compare\n (selectedPoint)=\"onClick($event)\"\n [compareIndex]=\"compareIndex\"\n [currentHeight]=\"500\"\n [pointMax]=\"compare.comparePointsMax\"\n [points]=\"compare.comparePoints\"></raain-compare>\n </ion-card-content>\n </ion-card>\n </ion-col>\n\n <ion-button (click)=\"exportGranularToCsv()\" *ngIf=\"compareManager.uiCompares?.length\" fill=\"outline\" size=\"small\">\n <ion-icon name=\"download-outline\" slot=\"start\"></ion-icon>\n Export CSV\n </ion-button>\n</div>\n", styles: ["#card-matrix{width:170px}.card-compare{min-width:300px}ion-card-header{padding:0 0 0 10px}\n"] }]
1310
1310
  }], ctorParameters: function () { return [{ type: i0.NgZone }]; }, propDecorators: { compareManager: [{
1311
1311
  type: Input
1312
1312
  }], currentHeight: [{
@@ -1460,10 +1460,10 @@ class CompareManager {
1460
1460
  this.uiCompares = uiCompares;
1461
1461
  this.onChanges();
1462
1462
  }
1463
- async refreshGlobalCompareQuality(targetsOrdered, withCompareDuplicate, cumulativeHours = 0) {
1463
+ async refreshGlobalCompareQuality(targetsOrdered, period, withCompareDuplicate, cumulativeHours = 0) {
1464
1464
  try {
1465
1465
  await this.fetchRainComputationQualities(targetsOrdered, cumulativeHours);
1466
- await this.buildComparesTimeline(targetsOrdered, withCompareDuplicate);
1466
+ await this.buildComparesTimeline(targetsOrdered, period, withCompareDuplicate);
1467
1467
  if (!this.buildCompares.compareCumulative) {
1468
1468
  // throw Error('needs cumulative compare');
1469
1469
  return;
@@ -1478,6 +1478,9 @@ class CompareManager {
1478
1478
  }
1479
1479
  }
1480
1480
  async setGaugesInMap() {
1481
+ // Get all gauge IDs linked to this rainNode
1482
+ const rainNodeGaugeIds = this.rainNode.getLinks(GaugeNode.TYPE).map((l) => l.getId());
1483
+ // Fetch gauges from API (may be filtered/limited)
1481
1484
  let gaugesToFilter = await this.profileService.getGauges(this.rainNode?.id, this.rainNode.getCenter());
1482
1485
  gaugesToFilter = gaugesToFilter
1483
1486
  .sort((a, b) => {
@@ -1485,9 +1488,29 @@ class CompareManager {
1485
1488
  b.approxDistanceFrom(this.rainNode.getCenter()));
1486
1489
  })
1487
1490
  .filter((v, index) => index < 200);
1488
- const rainNodeGaugeIds = this.rainNode.getLinks(GaugeNode.TYPE).map((l) => l.getId());
1489
- const visibleGauges = gaugesToFilter.filter((g) => rainNodeGaugeIds.indexOf(g.id) > -1);
1490
- console.log('visibleGauges:', visibleGauges);
1491
+ // Build a map of gauges from API response
1492
+ const gaugesFromApi = new Map();
1493
+ for (const gauge of gaugesToFilter) {
1494
+ gaugesFromApi.set(gauge.id, gauge);
1495
+ }
1496
+ // Fetch missing gauges individually (those linked but not in API response)
1497
+ const missingGaugeIds = rainNodeGaugeIds.filter((id) => !gaugesFromApi.has(id));
1498
+ for (const gaugeId of missingGaugeIds) {
1499
+ try {
1500
+ const gauge = await this.profileService.getGauge(gaugeId);
1501
+ if (gauge) {
1502
+ gaugesFromApi.set(gauge.id, gauge);
1503
+ }
1504
+ }
1505
+ catch (e) {
1506
+ console.warn(`Failed to fetch gauge ${gaugeId}:`, e);
1507
+ }
1508
+ }
1509
+ // Filter to only gauges linked to this rainNode
1510
+ const visibleGauges = rainNodeGaugeIds
1511
+ .map((id) => gaugesFromApi.get(id))
1512
+ .filter((g) => !!g);
1513
+ console.log('visibleGauges:', visibleGauges.length, '/', rainNodeGaugeIds.length);
1491
1514
  const gaugesLatLng = [];
1492
1515
  for (const gauge of visibleGauges) {
1493
1516
  gaugesLatLng.push(new MapLatLng(gauge.latitude, gauge.longitude, undefined, gauge.id, gauge.name));
@@ -1595,13 +1618,13 @@ class CompareManager {
1595
1618
  }
1596
1619
  return result.sort((a, b) => a.date.getTime() - b.date.getTime());
1597
1620
  }
1598
- async buildComparesTimeline(targetsOrdered, withCompareDuplicate) {
1621
+ async buildComparesTimeline(targetsOrdered, period, withCompareDuplicate) {
1599
1622
  const compareDates = targetsOrdered.map((t) => t.date);
1600
1623
  const qualities = compareDates
1601
1624
  .filter((d) => !!d)
1602
1625
  .map((d) => this.getRainComputationQuality(d))
1603
1626
  .filter((rcq) => !!rcq);
1604
- this.compareDates = compareDates; // compareDates.slice(1, -1);
1627
+ this.compareDates = compareDates?.length > 1 ? compareDates : [period.begin, period.end]; // compareDates.slice(1, -1);
1605
1628
  this.buildCompares = SpeedMatrixContainer.BuildCompares(qualities, !withCompareDuplicate);
1606
1629
  return this.buildCompares;
1607
1630
  }
@@ -1967,7 +1990,7 @@ class RefreshManager {
1967
1990
  const cumulativeHours = this.cumulative
1968
1991
  ? (this.period.end.getTime() - this.period.begin.getTime()) / (60 * 60 * 1000)
1969
1992
  : 0;
1970
- await this.compareManager.refreshGlobalCompareQuality(targets, !this.removeDuplicate, cumulativeHours);
1993
+ await this.compareManager.refreshGlobalCompareQuality(targets, this.period, !this.removeDuplicate, cumulativeHours);
1971
1994
  }
1972
1995
  catch (e) {
1973
1996
  console.error('refreshGlobalCompareReport', e);
@@ -2065,1377 +2088,1690 @@ function mapDateRangeToString(range) {
2065
2088
  }
2066
2089
  }
2067
2090
 
2068
- let TEST_DETECTION = 0;
2069
- class RaainDetailsComponent {
2070
- constructor(storage, cdr) {
2091
+ class FidjStorageNode {
2092
+ constructor() {
2093
+ this.radars = [];
2094
+ this.rains = [];
2095
+ this.gauges = [];
2096
+ this.events = [];
2097
+ this.infos = {};
2098
+ }
2099
+ static getEmptyNode() {
2100
+ return new FidjStorageNode();
2101
+ }
2102
+ static getDemoNode() {
2103
+ const demoNode = new FidjStorageNode();
2104
+ const link = new Link('rain', 'https://demo/api/rains/2');
2105
+ /*
2106
+ demoNode.radars = [
2107
+ new RadarNode({
2108
+ id: '5efd04569cb1f4161bd69dc7',
2109
+ name: 'demo radar A',
2110
+ links: [link],
2111
+ latitude: 48.774569,
2112
+ longitude: 2.008407
2113
+ }),
2114
+ new RadarNode({
2115
+ id: 'dr2',
2116
+ name: 'demo radar B',
2117
+ links: [link],
2118
+ latitude: 0.11,
2119
+ longitude: -0.753
2120
+ }),
2121
+ new RadarNode({
2122
+ id: 'dr3',
2123
+ name: 'demo radar C',
2124
+ latitude: 0.13,
2125
+ longitude: -0.753,
2126
+ links: []
2127
+ }),
2128
+ new RadarNode({
2129
+ id: 'dr4',
2130
+ name: 'demo radar D',
2131
+ latitude: 0.14,
2132
+ longitude: -0.74,
2133
+ links: []
2134
+ })];
2135
+ demoNode.rains = [
2136
+ new RainNode({
2137
+ id: '5efd04569cb1f4161bd69dc8',
2138
+ name: 'Demo rain zone A',
2139
+ links: [new Link('radar', 'https://demo/api/radars/5efcfe619cb1f4161bd69dc3')],
2140
+ status: 0,
2141
+ quality: 75,
2142
+ latitude: 48.774569,
2143
+ longitude: 2.008407
2144
+ }),
2145
+ new RainNode({
2146
+ id: 'dz2',
2147
+ name: 'Demo rain zone B',
2148
+ radars: [demoNode.radars[0], demoNode.radars[1]],
2149
+ status: 1,
2150
+ quality: 50,
2151
+ latitude: 48.774569,
2152
+ longitude: 2.008407
2153
+ }),
2154
+ new RainNode({
2155
+ id: 'dz3',
2156
+ name: 'Demo rain zone C',
2157
+ radars: [demoNode.radars[0], demoNode.radars[1]],
2158
+ status: 2,
2159
+ quality: 75,
2160
+ latitude: 48.774569,
2161
+ longitude: 2.008407
2162
+ }),
2163
+ new RainNode({
2164
+ id: 'dz4',
2165
+ name: 'Demo rain zone D',
2166
+ radars: [demoNode.radars[0], demoNode.radars[1]],
2167
+ status: 3,
2168
+ quality: 95,
2169
+ latitude: 48.774569,
2170
+ longitude: 2.008407
2171
+ })];
2172
+
2173
+ demoNode.gauges = [
2174
+ new GaugeNode({
2175
+ id: 'g1',
2176
+ name: 'Gauge A',
2177
+ latitude: 48.7748,
2178
+ longitude: 2.28407,
2179
+ }),
2180
+ new GaugeNode({
2181
+ id: 'g2',
2182
+ name: 'Gauge B',
2183
+ latitude: 48.874569,
2184
+ longitude: 2.108407,
2185
+ })];
2186
+ demoNode.events = [{
2187
+ id: 'e2',
2188
+ title: 'Need support ?',
2189
+ status: 0,
2190
+ red: false,
2191
+ description: 'This area is dedicated to support you and your team. Support is made on Radar or Rain quality, ' +
2192
+ 'or any feedback we can have about your production system. Our goal : improving your data.',
2193
+ created: new Date(),
2194
+ modified: new Date()
2195
+ }];
2196
+ demoNode.team = {
2197
+ id: 'p1',
2198
+ email: 'demo@demo.com',
2199
+ name: 'demo guy',
2200
+ description: 'the demo guy'
2201
+ };
2202
+
2203
+ */
2204
+ return demoNode;
2205
+ }
2206
+ }
2207
+ class FidjStorageResult {
2208
+ }
2209
+ class FidjStorage {
2210
+ constructor(storage) {
2071
2211
  this.storage = storage;
2072
- this.cdr = cdr;
2073
- this.availableProviders = [];
2074
- this.availableTimeSteps = [];
2075
- this.showPixelMarkers = false;
2076
- this.qualityIndicators = [];
2077
- this.qualityIndicatorsLoading = false;
2078
- // Cached computed values (to avoid method calls in templates)
2079
- this.percentOfComputations = 0;
2080
- this.percentOfImages = 0;
2081
- this.truncatedError = '';
2082
- this.cumulativeDurationInMinutes = 10;
2083
- this.DateRange = DateRange;
2084
- // Wrapper function that preserves the async nature of fetchData
2085
- this.fetchDataWrapper = async (focusDate, focusRange) => {
2086
- return await this.fetchData(focusDate, focusRange);
2087
- };
2212
+ this.node = FidjStorageNode.getEmptyNode();
2213
+ this.fidjMetaResult = { data: new FidjStorageNode() };
2088
2214
  }
2089
- static PeriodDisplay(date) {
2090
- let d = new Date();
2091
- if (date) {
2092
- d = new Date(date);
2093
- const userTimezoneOffset = d.getTimezoneOffset() * 60000;
2094
- d = new Date(d.getTime() - userTimezoneOffset);
2215
+ async storeData(fidjService, data) {
2216
+ this.node = JSON.parse(JSON.stringify(data));
2217
+ this.fidjMetaResult.data = this.node;
2218
+ if (this.isDemoMode) {
2219
+ this.storage.set('fidjMetaResult', JSON.stringify(this.fidjMetaResult));
2220
+ return;
2095
2221
  }
2096
- const exampleFormattedDate = '2017-06-01T08:30';
2097
- return d.toISOString().substring(0, exampleFormattedDate.length);
2222
+ await fidjService.put(this.fidjMetaResult);
2098
2223
  }
2099
- static DateUTC(date) {
2100
- const hasISOFormat = date.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
2101
- let d = new Date(date);
2102
- if (!hasISOFormat) {
2103
- const userTimezoneOffset = d.getTimezoneOffset() * 60000;
2104
- d = new Date(d.getTime() - userTimezoneOffset);
2224
+ async getRefreshedNodeCopy(fidjService) {
2225
+ if (this.isDemoMode) {
2226
+ const fidjMetaResult = this.storage.get('fidjMetaResult');
2227
+ if (fidjMetaResult) {
2228
+ this.fidjMetaResult = JSON.parse(fidjMetaResult);
2229
+ this.node = this.fidjMetaResult.data;
2230
+ }
2231
+ return JSON.parse(JSON.stringify(this.node));
2105
2232
  }
2106
- return d;
2107
- }
2108
- static MapCountToDateValue(c) {
2109
- return {
2110
- date: RaainDetailsComponent.DateUTC(c.name),
2111
- value: Math.min(100, c.x),
2233
+ const firstDemoData = async () => {
2234
+ this.node = FidjStorageNode.getDemoNode();
2235
+ await this.storeData(fidjService, this.node);
2112
2236
  };
2113
- }
2114
- async openQualityModal() {
2115
- this.showQualityModal = true;
2116
- this.qualityIndicatorsLoading = true;
2117
- this.qualityIndicators = [];
2118
- this.cdr.markForCheck();
2119
- if (this.rainNode?.id) {
2120
- const result = await this.profileService.getIndicators(this.rainNode.id);
2121
- this.qualityIndicators = result.indicators;
2237
+ await fidjService.sync(firstDemoData);
2238
+ const fidjFindAllResults = await fidjService.findAll();
2239
+ if (fidjFindAllResults && fidjFindAllResults.length > 0) {
2240
+ if (fidjFindAllResults[0].data) {
2241
+ this.fidjMetaResult = fidjFindAllResults[0];
2242
+ this.node = this.fidjMetaResult.data;
2243
+ }
2122
2244
  }
2123
- this.qualityIndicatorsLoading = false;
2124
- this.cdr.markForCheck();
2245
+ return JSON.parse(JSON.stringify(this.node));
2125
2246
  }
2126
- closeQualityModal() {
2127
- this.showQualityModal = false;
2247
+ setDemoMode(isDemo) {
2248
+ this.isDemoMode = isDemo;
2128
2249
  }
2129
- formatDate(dateStr) {
2130
- const date = new Date(dateStr);
2131
- return (date.toLocaleDateString() +
2132
- ' ' +
2133
- date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
2250
+ }
2251
+
2252
+ class ProfileService {
2253
+ constructor(storage, fidjService, httpClient, router) {
2254
+ this.storage = storage;
2255
+ this.fidjService = fidjService;
2256
+ this.httpClient = httpClient;
2257
+ this.router = router;
2258
+ this.email = this.storage.get('raain-email');
2259
+ this.asTeamId = this.storage.get('raain-asTeamId');
2260
+ this.readyForSync = new BehaviorSubject(false);
2261
+ this.roles = [];
2262
+ this.fidjStorage = new FidjStorage(storage);
2263
+ this.isDemoMode = false;
2134
2264
  }
2135
- async fetchData(focusDate, focusRange) {
2136
- const values = [];
2137
- for (let i = 0; i < 10; i++) {
2138
- values.push({ date: new Date(2020 + i, 0), value: 10 });
2265
+ get isDemoMode() {
2266
+ return this.isDemo;
2267
+ }
2268
+ set isDemoMode(mode) {
2269
+ this.isDemo = mode ? mode : true;
2270
+ this.fidjStorage.setDemoMode(this.isDemo);
2271
+ }
2272
+ get defaultUrlForAPI() {
2273
+ return this.storage.get('raain-urlForAPI');
2274
+ }
2275
+ set defaultUrlForAPI(url) {
2276
+ this.storage.set('raain-urlForAPI', url);
2277
+ }
2278
+ async refreshProfile() {
2279
+ try {
2280
+ this.nodeData = await this.fidjStorage.getRefreshedNodeCopy(this.fidjService);
2281
+ this.setRoles(await this.fidjService.getRoles());
2282
+ return this.nodeData;
2139
2283
  }
2140
- const fakeData = [
2141
- {
2142
- label: '% Rainy',
2143
- style: 'bar',
2144
- values,
2145
- },
2146
- {
2147
- label: '% Images',
2148
- style: 'bar',
2149
- values,
2150
- },
2151
- // {
2152
- // label: '% Quality',
2153
- // style: 'line',
2154
- // values,
2155
- // },
2156
- ];
2157
- const range = mapDateRangeToString(focusRange);
2158
- let data = fakeData;
2159
- if (!this.rainNode) {
2160
- return data;
2284
+ catch (e) {
2285
+ await this.checkError(e);
2161
2286
  }
2162
- if (focusRange === DateRange.CENTURY) {
2163
- // fake
2287
+ }
2288
+ async isConnected() {
2289
+ return this.fidjService.isConnected();
2290
+ }
2291
+ getEmail() {
2292
+ return this.email ?? this.storage.get('raain-email', this.email);
2293
+ }
2294
+ setEmail(email) {
2295
+ this.email = email;
2296
+ this.storage.set('raain-email', this.email);
2297
+ }
2298
+ async logout(fidjKey, fidjProd) {
2299
+ // this.storage.remove('raain-email');
2300
+ if (!fidjKey) {
2301
+ try {
2302
+ await this.fidjService.loginInDemoMode();
2303
+ this.readyForSync.next(true);
2304
+ }
2305
+ catch (err) {
2306
+ console.error('initFidj catch pb: ', err);
2307
+ }
2308
+ return;
2164
2309
  }
2165
- else if (focusRange === DateRange.HOUR) {
2166
- const hourCounts = await this.profileService.getCountsHour(this.rainNode.id, {
2167
- periodBegin: focusDate,
2310
+ await this.fidjService.logout(true);
2311
+ try {
2312
+ await this.fidjService.init(fidjKey, {
2313
+ logLevel: LoggerLevelEnum.WARN,
2314
+ crypto: false,
2315
+ prod: fidjProd,
2316
+ useDB: false,
2168
2317
  });
2169
- data = [
2170
- {
2171
- label: 'Rainy Count',
2172
- style: 'line',
2173
- values: hourCounts.rainyCount.map(RaainDetailsComponent.MapCountToDateValue),
2174
- },
2175
- {
2176
- label: '% Images',
2177
- style: 'bar',
2178
- values: hourCounts.percentImages.map(RaainDetailsComponent.MapCountToDateValue),
2179
- },
2180
- {
2181
- label: 'Rainy Sum',
2182
- style: 'line',
2183
- values: hourCounts.rainySum.map(RaainDetailsComponent.MapCountToDateValue),
2184
- },
2185
- ];
2318
+ this.readyForSync.next(true);
2186
2319
  }
2187
- else {
2188
- const counts = await this.profileService.getCounts(this.rainNode.id, {
2189
- range,
2190
- periodBegin: focusDate,
2191
- });
2192
- data = [
2193
- {
2194
- label: '% Rainy',
2195
- style: 'bar',
2196
- values: counts.percentRainy.map(RaainDetailsComponent.MapCountToDateValue),
2197
- },
2198
- {
2199
- label: '% Images',
2200
- style: 'bar',
2201
- values: counts.percentImages.map(RaainDetailsComponent.MapCountToDateValue),
2202
- },
2203
- ];
2320
+ catch (err) {
2321
+ console.error('initFidj catch pb: ', err);
2204
2322
  }
2205
- // console.log(`fetchData DONE ${range}`, data);
2206
- return data;
2207
- }
2208
- ngOnChanges(changes) {
2209
- this.change(changes).then((ignored) => { });
2210
- }
2211
- ngOnDestroy() {
2212
- this.cleanAll();
2213
2323
  }
2214
- async onEnableCountHistoryTab(rain) {
2215
- if (!this.toggleHistory) {
2216
- this.countPoints = [];
2324
+ async checkError(error) {
2325
+ if (error.code === 401) {
2326
+ console.warn('Pb on auth');
2327
+ if (this.router.url.indexOf('login') < 0) {
2328
+ try {
2329
+ await this.fidjService.logout();
2330
+ }
2331
+ catch (ignored) {
2332
+ // Ignore logout errors as we're redirecting to logout page anyway
2333
+ }
2334
+ return this.gotoLout();
2335
+ }
2217
2336
  }
2218
2337
  }
2219
- async onPeriodBeginChange(event) {
2220
- const newValue = event?.target?.value ?? this.periodBeginAsString;
2221
- this.periodBegin = new Date(newValue);
2222
- this.periodBeginAsString = RaainDetailsComponent.PeriodDisplay(this.periodBegin);
2223
- this.storage.set('raain-periodBegin', this.periodBegin);
2224
- await this.onPeriodDurationChange(event);
2225
- }
2226
- async onPeriodEndChange(event) {
2227
- const newValue = event?.target?.value ?? this.periodEndAsString;
2228
- this.periodEnd = new Date(newValue);
2229
- this.periodEndAsString = RaainDetailsComponent.PeriodDisplay(this.periodEnd);
2230
- this.storage.set('raain-periodEnd', this.periodEnd);
2231
- this.periodBegin = new Date(this.periodEnd.getTime() - this.getDurationInHours() * RaainDetailsComponent.HOUR_MS);
2232
- this.periodBeginAsString = RaainDetailsComponent.PeriodDisplay(this.periodBegin);
2233
- this.storage.set('raain-periodBegin', this.periodBegin);
2234
- this.updateRefreshManagerPeriod();
2235
- }
2236
- async onPeriodDurationChange(_event) {
2237
- const durationInHours = this.getDurationInHours();
2238
- this.periodEnd = new Date(this.periodBegin.getTime() + durationInHours * RaainDetailsComponent.HOUR_MS);
2239
- this.periodEndAsString = RaainDetailsComponent.PeriodDisplay(this.periodEnd);
2240
- this.storage.set('raain-periodEnd', this.periodEnd);
2241
- this.storage.set('raain-periodDurationInHours', durationInHours);
2242
- this.updateRefreshManagerPeriod();
2243
- this.updateCumulativeDurationInMinutes();
2244
- }
2245
- async onDateChangeInCount(dateChanged) {
2246
- const dateString = dateChanged.toISOString().substring(0, 11) +
2247
- dateChanged.toLocaleTimeString().substring(0, 5);
2248
- this.periodDurationAsString = '1';
2249
- if (this.toggleCumulative) {
2250
- // Cumulative: select periodEnd
2251
- this.periodEndAsString = dateString;
2252
- await this.onPeriodEndChange(null);
2338
+ async gotoLout() {
2339
+ try {
2340
+ if (this.router.url.indexOf('login') > -1) {
2341
+ return;
2342
+ }
2343
+ await this.router.navigateByUrl('/logout', {
2344
+ skipLocationChange: true,
2345
+ replaceUrl: true,
2346
+ });
2253
2347
  }
2254
- else {
2255
- // Granular: select periodBegin
2256
- this.periodBeginAsString = dateString;
2257
- await this.onPeriodBeginChange(null);
2348
+ catch (e) {
2349
+ console.error('gotoLout error: ', e);
2258
2350
  }
2259
- await this.refreshManager.refresh(false, this.toggleAdmin);
2260
- }
2261
- async onDateChangeInMap(dateShown) {
2262
- this.dateShown = dateShown;
2263
- await this.fetchAndUpdateMap();
2264
- await this.refreshManager.setReportPeriod(this.dateShown);
2265
- }
2266
- async onSumChangeInMap(sum) {
2267
- this.sumDetails = sum;
2268
2351
  }
2269
- async onGaugeSelectInMap(mapLatLng) {
2270
- const gaugesFiltered = this.compareManager.gaugesInMap.filter((g) => g.lat === mapLatLng.lat && g.lng === mapLatLng.lng);
2271
- if (gaugesFiltered.length !== 1) {
2352
+ async gotoLogin() {
2353
+ if (this.router.url.indexOf('login') > -1) {
2272
2354
  return;
2273
2355
  }
2274
- const gaugeSelected = gaugesFiltered[0];
2275
- await this.refreshGaugeValues({ id: gaugeSelected.id, name: gaugeSelected.name });
2276
- await this.compareManager.selectGauge(gaugeSelected.id, 0);
2356
+ // await this.router.navigateByUrl('/', {skipLocationChange: true});
2357
+ await this.router.navigate(['/login']);
2277
2358
  }
2278
- async refreshGaugeValues(gaugeSelected) {
2279
- const gaugeValueShowBegin = new Date(this.periodBegin.getTime() - RaainDetailsComponent.DAY_MS);
2280
- const gaugeValueShowEnd = new Date(this.periodEnd.getTime() + RaainDetailsComponent.DAY_MS);
2281
- gaugeValueShowBegin.setHours(0, 0);
2282
- gaugeValueShowEnd.setHours(23, 59);
2283
- const gaugeMeasures = await this.profileService.getGaugeMeasures(gaugeSelected.id, gaugeValueShowBegin, gaugeValueShowEnd);
2284
- const gaugeValues = gaugeMeasures.map((gm) => {
2285
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2286
- const cartesianMeasureValue = new CartesianMeasureValue(gm.values[0]);
2287
- return {
2288
- date: gm.date,
2289
- value: cartesianMeasureValue.getCartesianValues()[0].value,
2290
- };
2291
- });
2292
- this.gaugeSelectedPoints = [
2293
- {
2294
- label: gaugeSelected.name,
2295
- style: 'bar',
2296
- values: gaugeValues,
2297
- },
2298
- ];
2299
- this.cdr.markForCheck();
2359
+ isLoggedIn() {
2360
+ const loggedIn = this.fidjService.isLoggedIn();
2361
+ console.log('isLoggedIn: ', loggedIn);
2362
+ return loggedIn;
2300
2363
  }
2301
- async onGaugeSelectInCompare(e) {
2302
- await this.refreshGaugeValues({ id: e.point.id, name: e.point.name });
2303
- await this.compareManager.selectGauge(e.point.id, e.compareIndex);
2364
+ needsConnectionRefresh() {
2365
+ return this.fidjService.needsRefresh();
2304
2366
  }
2305
- async onToggleMap($event) {
2306
- // if (this.toggleMap) {
2307
- // await this.refreshMap();
2308
- // }
2367
+ async connectionRefresh() {
2368
+ await this.fidjService.refresh();
2309
2369
  }
2310
- onTogglePixelMarkers() {
2311
- // Toggle is bound to showPixelMarkers, raain-map handles marker display
2370
+ async storeAll() {
2371
+ return this.fidjStorage.storeData(this.fidjService, this.nodeData);
2312
2372
  }
2313
- async toggleCumulativeChanged(_$event) {
2314
- this.storage.set('raain-toggleCumulative', this.toggleCumulative);
2315
- this.dateShown = this.getDateBasedOnCumulativeMode(this.timeframeDates);
2316
- this.updateCumulativeDurationInMinutes();
2317
- if (this.toggleMap) {
2318
- this.updateRefreshManagerPeriod(); // Update cumulative flag before refresh
2319
- await this.refreshManager.refresh(false, this.toggleAdmin);
2320
- }
2373
+ isAdmin() {
2374
+ return this.roles.indexOf('admin') > -1;
2321
2375
  }
2322
- updateCumulativeDurationInMinutes() {
2323
- if (this.toggleCumulative) {
2324
- // Cumulative mode: use period duration
2325
- this.cumulativeDurationInMinutes = parseFloat(this.periodDurationAsString) * 60;
2376
+ // === Notifications ===
2377
+ async createNotification(rainId, message) {
2378
+ const data = {
2379
+ rain: rainId,
2380
+ message,
2381
+ };
2382
+ try {
2383
+ const resp = await this.fidjService.sendOnEndpoint({
2384
+ key: 'notifications',
2385
+ verb: 'POST',
2386
+ defaultKeyUrl: this.defaultUrlForAPI + '/notifications',
2387
+ data,
2388
+ });
2389
+ return new EventNode(resp.data);
2326
2390
  }
2327
- else {
2328
- // Granular mode: use selectedTimeStep
2329
- this.cumulativeDurationInMinutes = this.selectedTimeStep || 10;
2391
+ catch (e) {
2392
+ await this.checkError(e);
2330
2393
  }
2394
+ return null;
2331
2395
  }
2332
- async onProviderChanged($event) {
2333
- this.selectedProvider = $event?.detail?.value;
2334
- this.storage.set('raain-selectedProvider', this.selectedProvider);
2335
- await this.applyRefreshManagerSettings();
2336
- }
2337
- async onTimeStepChanged($event) {
2338
- this.selectedTimeStep = $event?.detail?.value;
2339
- this.storage.set('raain-selectedTimeStep', this.selectedTimeStep);
2340
- this.updateCumulativeDurationInMinutes();
2341
- await this.applyRefreshManagerSettings();
2342
- }
2343
- async loadProviders() {
2344
- if (!this.rainNode?.id) {
2345
- return;
2346
- }
2396
+ async getNotifications(rainId) {
2347
2397
  try {
2348
- const result = await this.profileService.getProviders(this.rainNode.id);
2349
- this.availableProviders = result.providers;
2350
- this.availableTimeSteps = result.timeStepInMinutes;
2351
- // Load saved selections or use defaults
2352
- this.selectedProvider =
2353
- this.storage.get('raain-selectedProvider') ||
2354
- (this.availableProviders.length > 0 ? this.availableProviders[0] : 'Raain');
2355
- this.selectedTimeStep =
2356
- this.storage.get('raain-selectedTimeStep') ||
2357
- (this.availableTimeSteps.length > 0 ? this.availableTimeSteps[0] : 10);
2398
+ const params = { rain: rainId };
2399
+ const queryString = BuildQueryString(params);
2400
+ const resp = await this.fidjService.sendOnEndpoint({
2401
+ key: 'notifications',
2402
+ verb: 'GET',
2403
+ relativePath: queryString ? '?' + queryString : '',
2404
+ defaultKeyUrl: this.defaultUrlForAPI + '/notifications',
2405
+ });
2406
+ return resp.data.notifications.map((n) => new EventNode(n));
2358
2407
  }
2359
2408
  catch (e) {
2360
- // Set fallback values
2361
- this.availableProviders = ['Raain'];
2362
- this.availableTimeSteps = [5, 10, 15, 30, 60];
2363
- this.selectedProvider = 'Raain';
2364
- this.selectedTimeStep = 10;
2365
- }
2366
- // Set provider and timeStep on refreshManager
2367
- if (this.refreshManager) {
2368
- this.refreshManager.provider = this.selectedProvider;
2369
- this.refreshManager.timeStepInMinutes = this.selectedTimeStep;
2409
+ await this.checkError(e);
2370
2410
  }
2371
- this.cdr.markForCheck();
2372
- }
2373
- onChangeDetectionTest(rainNode) {
2374
- console.log(TEST_DETECTION++, 'onChangeDetectionTest');
2375
- return '';
2411
+ return null;
2376
2412
  }
2377
- updateTruncatedError() {
2378
- const error = this.refreshManager?.lastError || '';
2379
- if (error.length <= 80) {
2380
- this.truncatedError = error;
2381
- return;
2413
+ async getAllNotifications() {
2414
+ try {
2415
+ const resp = await this.fidjService.sendOnEndpoint({
2416
+ key: 'notifications',
2417
+ verb: 'GET',
2418
+ defaultKeyUrl: this.defaultUrlForAPI + '/notifications',
2419
+ });
2420
+ return resp.data.notifications.map((n) => new EventNode(n));
2382
2421
  }
2383
- this.truncatedError = error.substring(0, 80) + '...';
2384
- }
2385
- updateCachedValues() {
2386
- this.updatePercentOfImages();
2387
- this.updatePercentOfComputations();
2388
- this.updateTruncatedError();
2389
- this.updateCumulativeDurationInMinutes();
2390
- }
2391
- async refreshMap() {
2392
- this.gaugeSelectedPoints = [];
2393
- this.dateShown = this.getDateBasedOnCumulativeMode();
2394
- this.borders = [];
2395
- this.compareManager.cleanAll();
2396
- await this.compareManager.setGaugesInMap();
2397
- await this.refreshManager.refresh(false, this.toggleAdmin);
2398
- this.cdr.markForCheck();
2399
- }
2400
- async setPeriodOfNow() {
2401
- const last30mn = new Date();
2402
- last30mn.setMinutes(last30mn.getMinutes() - 30);
2403
- this.periodBeginAsString =
2404
- last30mn.toISOString().substring(0, 11) + last30mn.toLocaleTimeString().substring(0, 5);
2405
- this.periodDurationAsString = '1';
2406
- await this.onPeriodBeginChange(null);
2407
- await this.refreshManager.refresh(false, this.toggleAdmin);
2422
+ catch (e) {
2423
+ await this.checkError(e);
2424
+ }
2425
+ return [];
2408
2426
  }
2409
- updatePercentOfImages() {
2410
- if (!this.countsPeriod?.percentImages?.length) {
2411
- this.percentOfImages = 0;
2412
- return;
2427
+ // === Teams ===
2428
+ async getTeams() {
2429
+ const teams = [];
2430
+ const params = {};
2431
+ const queryString = BuildQueryString(params);
2432
+ try {
2433
+ const resp = await this.fidjService.sendOnEndpoint({
2434
+ key: 'teams',
2435
+ verb: 'GET',
2436
+ relativePath: queryString ? '?' + queryString : '',
2437
+ defaultKeyUrl: this.defaultUrlForAPI + '/teams',
2438
+ });
2439
+ for (const team of resp.data.teams) {
2440
+ teams.push(new TeamNode(team));
2441
+ }
2413
2442
  }
2414
- const duringPeriod = this.countsPeriod.percentImages.filter((a) => this.periodBegin.getTime() <= new Date(a.name).getTime() &&
2415
- new Date(a.name).getTime() <= this.periodEnd.getTime());
2416
- const summed = duringPeriod.reduce((a, b) => a + (b.x ?? 0), 0);
2417
- this.percentOfImages = Math.round(summed / duringPeriod.length);
2443
+ catch (e) {
2444
+ await this.checkError(e);
2445
+ }
2446
+ return teams;
2418
2447
  }
2419
- updatePercentOfComputations() {
2420
- const timeline = this.refreshManager?.getTimelineFrameSet();
2421
- if (!timeline?.length) {
2422
- this.percentOfComputations = 0;
2423
- return;
2448
+ async getTeam(teamId) {
2449
+ try {
2450
+ const resp = await this.fidjService.sendOnEndpoint({
2451
+ key: 'teams',
2452
+ verb: 'GET',
2453
+ relativePath: teamId,
2454
+ defaultKeyUrl: this.defaultUrlForAPI + '/teams',
2455
+ });
2456
+ return new TeamNode(resp.data);
2424
2457
  }
2425
- const timelineWithComputation = timeline.filter((a) => !!a.rainComputationCumulativeId || !!a.rainComputationId);
2426
- const ratio = timelineWithComputation.length / timeline.length;
2427
- this.percentOfComputations = Math.round(ratio * 100);
2458
+ catch (e) {
2459
+ await this.checkError(e);
2460
+ }
2461
+ return null;
2428
2462
  }
2429
- getDateBasedOnCumulativeMode(fallbackDates) {
2430
- if (fallbackDates?.length > 0) {
2431
- const dateExists = fallbackDates.some((d) => d.getTime() === this.dateShown?.getTime());
2432
- if (!dateExists) {
2433
- return this.toggleCumulative
2434
- ? fallbackDates[fallbackDates.length - 1]
2435
- : fallbackDates[0];
2463
+ // === Radars ===
2464
+ async getRadars(name) {
2465
+ const radars = [];
2466
+ const params = {};
2467
+ if (name) {
2468
+ params.name = name;
2469
+ }
2470
+ const queryString = BuildQueryString(params);
2471
+ try {
2472
+ const resp = await this.fidjService.sendOnEndpoint({
2473
+ key: 'radars',
2474
+ verb: 'GET',
2475
+ relativePath: queryString ? '?' + queryString : '',
2476
+ defaultKeyUrl: this.defaultUrlForAPI + '/radars',
2477
+ });
2478
+ for (const r of resp.data.radars) {
2479
+ const radar = new RadarNode(r);
2480
+ radars.push(radar);
2436
2481
  }
2437
2482
  }
2438
- return this.toggleCumulative ? this.periodEnd : this.periodBegin;
2439
- }
2440
- getCumulativeHours() {
2441
- return this.toggleCumulative ? parseFloat(this.periodDurationAsString) : 0;
2483
+ catch (e) {
2484
+ await this.checkError(e);
2485
+ }
2486
+ return radars;
2442
2487
  }
2443
- getDurationInHours() {
2444
- return parseFloat(this.periodDurationAsString);
2488
+ async getRadar(id) {
2489
+ try {
2490
+ const resp = await this.fidjService.sendOnEndpoint({
2491
+ key: 'radars',
2492
+ verb: 'GET',
2493
+ relativePath: id,
2494
+ defaultKeyUrl: this.defaultUrlForAPI + '/radars',
2495
+ });
2496
+ return new RadarNode(resp.data);
2497
+ }
2498
+ catch (e) {
2499
+ await this.checkError(e);
2500
+ }
2501
+ return null;
2445
2502
  }
2446
- updateRefreshManagerPeriod() {
2447
- this.refreshManager.cumulative = this.toggleCumulative;
2448
- // Align dates to 5-minute boundaries (floor) for consistency with raain-ground
2449
- const alignTo5minFloor = (date) => {
2450
- const minutes = date.getMinutes();
2451
- const alignedMinutes = Math.floor(minutes / 5) * 5;
2452
- return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), alignedMinutes, 0, 0);
2453
- };
2454
- this.refreshManager.period = {
2455
- begin: alignTo5minFloor(this.periodBegin),
2456
- end: alignTo5minFloor(this.periodEnd),
2503
+ async putRadar(radarNode) {
2504
+ const data = {
2505
+ name: radarNode.name,
2457
2506
  };
2507
+ try {
2508
+ const resp = await this.fidjService.sendOnEndpoint({
2509
+ key: 'radars',
2510
+ relativePath: radarNode.id,
2511
+ verb: 'PUT',
2512
+ data,
2513
+ defaultKeyUrl: this.defaultUrlForAPI + '/radars/',
2514
+ });
2515
+ return new RadarNode(resp.data);
2516
+ }
2517
+ catch (e) {
2518
+ await this.checkError(e);
2519
+ }
2458
2520
  }
2459
- async fetchAndUpdateMap() {
2460
- await this.refreshManager.fetch(this.dateShown, this.toggleGaugeMeasures, this.getCumulativeHours());
2461
- this.currentTimeframeTarget = this.refreshManager.getTimelineSelectedFrameSet();
2462
- this.cdr.markForCheck();
2463
- }
2464
- async applyRefreshManagerSettings() {
2465
- if (!this.refreshManager) {
2466
- return;
2521
+ async getLonelyRadars(rains) {
2522
+ try {
2523
+ const resp = await this.fidjService.sendOnEndpoint({
2524
+ key: 'radars',
2525
+ verb: 'GET',
2526
+ defaultKeyUrl: this.defaultUrlForAPI + '/radars',
2527
+ });
2528
+ const lonelyRadars = [];
2529
+ const radars = resp.data.radars;
2530
+ radars.forEach((radar) => {
2531
+ let found = false;
2532
+ rains.forEach((rain) => {
2533
+ const rdId = rain.getLink(RadarNode.TYPE).getId();
2534
+ if (rdId === radar.id) {
2535
+ found = true;
2536
+ }
2537
+ });
2538
+ if (!found) {
2539
+ lonelyRadars.push(new RadarNode(radar));
2540
+ }
2541
+ });
2542
+ return lonelyRadars;
2467
2543
  }
2468
- this.refreshManager.provider = this.selectedProvider;
2469
- this.refreshManager.timeStepInMinutes = this.selectedTimeStep;
2470
- await this.refreshManager.refresh(false, this.toggleAdmin);
2544
+ catch (e) {
2545
+ await this.checkError(e);
2546
+ }
2547
+ return [];
2471
2548
  }
2472
- cleanAll() {
2473
- this.borders = [];
2474
- this.isAdmin = false;
2475
- this.timeframeContainers = new TimeframeContainers([]);
2476
- this.currentTimeframeTarget = null;
2477
- this.timeframeDates = [];
2478
- this.countPoints = [];
2479
- this.countsPeriod = { progress: 0, queueRunning: 0, percentImages: [] };
2480
- this.gaugeSelectedPoints = [];
2481
- this.toggleHistory = false;
2482
- this.toggleMap = true;
2483
- this.toggleCompare = false;
2484
- this.toggleGaugeMeasures = false;
2485
- this.toggleRemoveCompareDuplicate = true;
2486
- this.toggleCumulative = this.storage.get('raain-toggleCumulative');
2487
- this.periodBegin = new Date(this.storage.get('raain-periodBegin'));
2488
- this.periodEnd = new Date(this.storage.get('raain-periodEnd'));
2489
- this.periodBeginAsString = RaainDetailsComponent.PeriodDisplay(this.periodBegin);
2490
- this.periodEndAsString = RaainDetailsComponent.PeriodDisplay(this.periodEnd);
2491
- const durationMs = this.periodEnd.getTime() - this.periodBegin.getTime();
2492
- this.periodDurationAsString = '' + durationMs / RaainDetailsComponent.HOUR_MS;
2493
- this.dateShown = this.getDateBasedOnCumulativeMode();
2494
- this.refreshInProgress = false;
2495
- this.showFullError = false;
2496
- this.showQualityModal = false;
2497
- this.qualityIndicators = [];
2498
- this.qualityIndicatorsLoading = false;
2499
- this.compareManager?.cleanAll();
2500
- this.refreshManager?.cleanAll();
2501
- }
2502
- async init() {
2503
- this.cleanAll();
2504
- this.updateCachedValues();
2505
- await this.initRain();
2506
- this.cdr.markForCheck();
2507
- }
2508
- async initRain() {
2509
- if (!this.rainNode) {
2510
- return;
2549
+ async getRainTimeframe(rainId, begin, end, forced = false, provider = 'Raain', timeStepInMinutes = 10, windowInMinutes = 0) {
2550
+ try {
2551
+ const params = {
2552
+ format: 'timeframeCumulative',
2553
+ provider,
2554
+ timeStepInMinutes: String(timeStepInMinutes),
2555
+ begin: begin?.toISOString(),
2556
+ end: end?.toISOString(),
2557
+ };
2558
+ if (forced) {
2559
+ params.forced = 'true';
2560
+ }
2561
+ params.windowInMinutes = String(windowInMinutes);
2562
+ const queryString = BuildQueryString(params);
2563
+ const resp = await this.fidjService.sendOnEndpoint({
2564
+ key: 'rains',
2565
+ verb: 'GET',
2566
+ relativePath: rainId + (queryString ? '?' + queryString : ''),
2567
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains',
2568
+ });
2569
+ const rainNode = new RainNode(resp.data.timeframeCumulative);
2570
+ rainNode.name += '.radar.timeframeCumulative';
2571
+ return rainNode;
2511
2572
  }
2512
- this.isAdmin = this.profileService.isAdmin();
2513
- this.refreshManager.rainNode = this.rainNode;
2514
- this.compareManager.rainNode = this.rainNode;
2515
- // Load providers and set on refreshManager
2516
- await this.loadProviders();
2517
- this.refreshManager.setMethods(this.onRefreshInProgress.bind(this), this.onRefreshDone.bind(this), this.onFetchDone.bind(this));
2518
- const center = this.rainNode.getCenter();
2519
- this.coordinates = new MapLatLng(center.lat, center.lng);
2520
- this.teamNode = await this.profileService.getTeam(this.rainNode.getLink(TeamNode.TYPE).getId());
2521
- if (this.periodBegin && this.periodEnd) {
2522
- this.updateRefreshManagerPeriod();
2523
- await this.refreshManager.refresh(false, this.toggleAdmin);
2573
+ catch (e) {
2574
+ await this.checkError(e);
2524
2575
  }
2576
+ return null;
2525
2577
  }
2526
- async onRefreshInProgress(countPeriods, timeframeDates) {
2527
- this.refreshInProgress = true;
2528
- this.countsPeriod = countPeriods;
2529
- this.timeframeDates = timeframeDates;
2530
- this.updateCachedValues();
2531
- this.cdr.markForCheck();
2532
- }
2533
- async onRefreshDone(timeframeDates) {
2534
- this.timeframeDates = timeframeDates;
2535
- this.refreshInProgress = false;
2536
- this.updateCachedValues();
2537
- this.cdr.markForCheck();
2538
- if (this.toggleMap && timeframeDates.length > 0) {
2539
- this.dateShown = this.getDateBasedOnCumulativeMode(timeframeDates);
2540
- if (this.dateShown) {
2541
- await this.fetchAndUpdateMap();
2578
+ // === Rains ===
2579
+ async getRains(name) {
2580
+ const rains = [];
2581
+ const params = {};
2582
+ if (name) {
2583
+ params.name = name;
2584
+ }
2585
+ const queryString = BuildQueryString(params);
2586
+ try {
2587
+ const resp = await this.fidjService.sendOnEndpoint({
2588
+ key: 'rains',
2589
+ verb: 'GET',
2590
+ relativePath: queryString ? '?' + queryString : '',
2591
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains',
2592
+ });
2593
+ for (const rain of resp.data.rains) {
2594
+ rains.push(new RainNode(rain));
2542
2595
  }
2543
2596
  }
2544
- }
2545
- async onFetchDone(timeframeContainers) {
2546
- if (timeframeContainers) {
2547
- this.timeframeContainers = timeframeContainers;
2597
+ catch (e) {
2598
+ await this.checkError(e);
2548
2599
  }
2549
- this.cdr.markForCheck();
2600
+ return rains;
2550
2601
  }
2551
- async change(_changes) {
2552
- await this.init();
2602
+ async getRain(id) {
2603
+ try {
2604
+ const resp = await this.fidjService.sendOnEndpoint({
2605
+ key: 'rains',
2606
+ relativePath: id,
2607
+ verb: 'GET',
2608
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains/',
2609
+ });
2610
+ return new RainNode(resp.data);
2611
+ }
2612
+ catch (e) {
2613
+ await this.checkError(e);
2614
+ }
2615
+ return null;
2553
2616
  }
2554
- }
2555
- RaainDetailsComponent.HOUR_MS = 60 * 60000;
2556
- RaainDetailsComponent.DAY_MS = 24 * 60 * 60 * 1000;
2557
- RaainDetailsComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: RaainDetailsComponent, deps: [{ token: Storage }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2558
- RaainDetailsComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: RaainDetailsComponent, selector: "raain-details", inputs: { toggleAdmin: "toggleAdmin", rainNode: "rainNode", compareManager: "compareManager", refreshManager: "refreshManager", profileService: "profileService", radarService: "radarService" }, usesOnChanges: true, ngImport: i0, template: "<!-- Main content container -->\n<div *ngIf=\"rainNode\" class=\"raain-details-container\">\n\n <!-- Period selection section -->\n <ion-card class=\"period-card\">\n <ion-card-content>\n <div class=\"period-controls\">\n <div class=\"period-row\">\n\n <ion-button (click)=\"toggleHistory = !toggleHistory; onEnableCountHistoryTab(rainNode)\"\n fill=\"outline\">\n <ion-icon name=\"calendar-clear-outline\" slot=\"start\"></ion-icon>\n <ion-icon [name]=\"toggleHistory ? 'chevron-down' : 'chevron-forward'\" slot=\"end\"></ion-icon>\n </ion-button>\n\n <div class=\"period-start ion-hide-md-down\">\n <input (change)=\"onPeriodBeginChange($event)\"\n [disabled]=\"toggleCumulative\"\n [value]=\"periodBeginAsString\"\n class=\"datetime-input\"\n type=\"datetime-local\">\n </div>\n\n <div class=\"period-duration ion-hide-md-down\">\n <ion-select (ionDismiss)=\"onPeriodDurationChange($event)\"\n [(ngModel)]=\"periodDurationAsString\"\n class=\"duration-select\"\n id=\"periodDuration\"\n interface=\"popover\">\n <ion-select-option value=\"0.25\">15 minutes</ion-select-option>\n <ion-select-option value=\"0.5\">30 minutes</ion-select-option>\n <ion-select-option value=\"1\">1 hour</ion-select-option>\n <ion-select-option value=\"2\">2 hours</ion-select-option>\n <ion-select-option value=\"3\">3 hours</ion-select-option>\n <ion-select-option value=\"4\">4 hours</ion-select-option>\n <ion-select-option value=\"5\">5 hours</ion-select-option>\n <ion-select-option value=\"6\">6 hours</ion-select-option>\n <ion-select-option value=\"8\">8 hours</ion-select-option>\n <ion-select-option value=\"10\">10 hours</ion-select-option>\n <ion-select-option value=\"12\">12 hours</ion-select-option>\n <ion-select-option *ngIf=\"isAdmin\" value=\"24\">24 hours</ion-select-option>\n </ion-select>\n </div>\n\n <div class=\"period-start ion-hide-md-down\">\n <input (change)=\"onPeriodEndChange($event)\"\n [disabled]=\"!toggleCumulative\"\n [value]=\"periodEndAsString\"\n class=\"datetime-input\"\n type=\"datetime-local\">\n </div>\n\n <div class=\"toggle-cumulative\">\n <ion-label [class.text-primary]=\"toggleCumulative\">\n {{ toggleCumulative ? 'Cumulative' : 'Granular' }}\n </ion-label>\n <ion-toggle (ionChange)=\"toggleCumulativeChanged($event)\"\n [(ngModel)]=\"toggleCumulative\"\n [checked]=\"toggleCumulative\">\n </ion-toggle>\n </div>\n </div>\n\n <!-- Hidden label for change detection (uncomment to debug)\n <div class=\"hidden-label\">{{ onChangeDetectionTest(rainNode) }}</div>\n -->\n </div>\n\n <!-- Historical map section -->\n <div *ngIf=\"toggleHistory\" class=\"period-controls\">\n <raain-date-dynamic (changedDate)=\"onDateChangeInCount($event)\"\n [currentHeight]=\"300\"\n [fetchData]=\"fetchDataWrapper\"\n [points]=\"countPoints\">\n </raain-date-dynamic>\n </div>\n </ion-card-content>\n </ion-card>\n\n <!-- Map performance -->\n <ion-grid class=\"map-performance\">\n <ion-row id=\"progressAndRefresh\">\n <ion-col class=\"provider-selection\" size=\"12\" size-md=\"6\">\n <ion-button (click)=\"openQualityModal()\" class=\"quality-info-button\" fill=\"clear\">\n {{ refreshManager.rainComputationMapVersion }}\n <ion-icon name=\"help-circle-outline\" slot=\"end\"></ion-icon>\n </ion-button>\n <div *ngIf=\"availableProviders.length > 0 || availableTimeSteps.length > 0\" class=\"selection-row\">\n <ion-select (ionChange)=\"onProviderChanged($event)\"\n [value]=\"selectedProvider\"\n interface=\"popover\"\n label=\"Provider\"\n placeholder=\"Select Provider\">\n <ion-select-option *ngFor=\"let provider of availableProviders\" [value]=\"provider\">\n {{ provider }}\n </ion-select-option>\n </ion-select>\n\n <ion-select (ionChange)=\"onTimeStepChanged($event)\"\n [value]=\"selectedTimeStep\"\n interface=\"popover\"\n label=\"Time Step\"\n placeholder=\"Select Time Step\">\n <ion-select-option *ngFor=\"let step of availableTimeSteps\" [value]=\"step\">\n {{ step }} min\n </ion-select-option>\n </ion-select>\n </div>\n </ion-col>\n <ion-col class=\"ion-text-right\" size=\"12\" size-md=\"6\">\n <ion-label class=\"ion-margin-end\">\n <span *ngIf=\"percentOfComputations\">\n {{ percentOfComputations }}% Images\n <i *ngIf=\"countsPeriod.progress\" class=\"progress-indicator\">\n In Progress: {{ countsPeriod.progress }}...\n </i>\n </span>\n <span *ngIf=\"!percentOfComputations\">\n No image available\n </span>\n\n </ion-label>\n\n <ion-button (click)=\"refreshMap()\" [disabled]=\"refreshInProgress\" class=\"refresh-button\">\n Refresh Map\n </ion-button>\n </ion-col>\n </ion-row>\n\n <!-- status update row -->\n <ion-row>\n <!-- Progress col -->\n <ion-progress-bar\n [buffer]=\"(countsPeriod.progress / ((timeframeDates?.length || 10) +3))+0.01\"\n [style.visibility]=\"refreshInProgress ? 'visible' : 'hidden'\"\n [value]=\"countsPeriod.progress / ((timeframeDates?.length || 10) +3)\"\n color=\"primary\">\n </ion-progress-bar>\n\n <!-- Error col -->\n <ion-col (click)=\"showFullError = !showFullError\" *ngIf=\"refreshManager.lastError\" class=\"error-row\"\n size=\"12\">\n <div class=\"error-content\">\n <ion-icon class=\"error-icon\" name=\"warning-outline\"></ion-icon>\n <span [class.expanded]=\"showFullError\" class=\"error-text\">\n {{ showFullError ? refreshManager.lastError : truncatedError }}\n </span>\n <ion-icon [name]=\"showFullError ? 'chevron-up' : 'chevron-down'\" class=\"error-caret\"></ion-icon>\n </div>\n </ion-col>\n </ion-row>\n </ion-grid>\n\n <!-- Map section -->\n <ion-card class=\"map-card\">\n <ion-card-content class=\"map-content\">\n <ion-grid>\n <ion-row *ngIf=\"toggleMap && percentOfImages\">\n <!-- Map component -->\n <ion-col class=\"map-column\" size-lg=\"7\" size-md=\"12\">\n <div class=\"map-container\">\n <raain-map #raainMapRef\n (changedDate)=\"onDateChangeInMap($event)\"\n (changedSum)=\"onSumChangeInMap($event)\"\n (selectedMarker)=\"onGaugeSelectInMap($event)\"\n [coordinates]=\"coordinates\"\n [cumulativeDurationInMinutes]=\"cumulativeDurationInMinutes\"\n [currentHeight]=\"500\"\n [defaultDate]=\"dateShown\"\n [markers]=\"{\n borders,\n gauges: compareManager.gaugesInMap,\n gaugesInCompare: compareManager.gaugesInCompare,\n selectedGauges: compareManager.selectedGauges,\n pixels: compareManager.selectedPixels,\n pixelsSolution: compareManager.pixelsSolutions?.length ? compareManager.pixelsSolutions[0] : [],\n speeds: compareManager.speeds\n }\"\n [showCumulative]=\"toggleCumulative\"\n [showVisiblePixelMarkers]=\"showPixelMarkers\"\n [sumFn]=\"refreshManager.sumFn\"\n [sumValues]=\"refreshManager.sumValues\"\n [timeframeContainers]=\"timeframeContainers\"\n [timeframeDates]=\"timeframeDates\">\n </raain-map>\n </div>\n\n <div class=\"data-column\">\n <!-- Technical details (collapsible for cleaner UI) -->\n <details class=\"technical-details\">\n <summary>Image Details</summary>\n <div class=\"details-content\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Computed:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapDoneDate | date:'yyyy-MM-dd HH:mm' }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Version:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapVersion }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Launched by:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapLaunchedBy }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Time spent:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapTimeSpentInMs }}\n ms</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Date:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapDate?.toISOString() }}\n | {{ refreshManager.rainComputationMapDate | date:'yyyy-MM-dd HH:mm' }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Water in the map:</span>\n <span class=\"detail-value\">{{ sumDetails }}</span>\n <ion-toggle\n (ionChange)=\"onTogglePixelMarkers()\"\n [(ngModel)]=\"showPixelMarkers\"\n style=\"margin-left: 8px; transform: scale(0.7);\">\n </ion-toggle>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Source DBZ min:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapOriginalMin }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Source DBZ max:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapOriginalMax }}</span>\n </div>\n </div>\n </details>\n </div>\n </ion-col>\n\n <!-- Data panel -->\n <ion-col *ngIf=\"!!compareManager.compareVersion\" class=\"data-column\" size-lg=\"5\" size-md=\"12\">\n <div class=\"data-panel\">\n <!-- Compare stack component -->\n <div class=\"compare-stack\">\n <raain-compare-stack\n (selectedPoint)=\"onGaugeSelectInCompare($event)\"\n [compareManager]=\"compareManager\"\n [cumulative]=\"toggleCumulative\">\n </raain-compare-stack>\n </div>\n\n <!-- Technical details (collapsible for cleaner UI) -->\n <details class=\"technical-details\">\n <summary>Compare Details</summary>\n <div class=\"details-content\">\n <div [ngClass]=\"{'warning': refreshManager.rainComputationMapDoneDate?.getTime() > compareManager.currentQualityDoneDate?.getTime() + 60000}\"\n class=\"detail-row\">\n <span class=\"detail-label\">Computed:</span>\n <span class=\"detail-value\">{{ compareManager.currentQualityDoneDate | date:'yyyy-MM-dd HH:mm' }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Version:</span>\n <span class=\"detail-value\">{{ compareManager.compareVersion }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Launched by:</span>\n <span class=\"detail-value\">{{ compareManager.currentQualityLaunchedBy }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Time spent:</span>\n <span class=\"detail-value\">{{ compareManager.currentQualityTimeSpentInMs }}\n ms</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Gauges:</span>\n <span class=\"detail-value\">{{ compareManager.gaugesInCompare.length }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Points:</span>\n <span class=\"detail-value\">{{ compareManager.globalComparePoints.length }}</span>\n </div>\n </div>\n </details>\n </div>\n </ion-col>\n </ion-row>\n <ion-row>\n <!-- Bottom progress bar -->\n <ion-progress-bar *ngIf=\"refreshInProgress\"\n [buffer]=\"(countsPeriod.progress / ((timeframeDates?.length || 10) +3))+0.01\"\n [value]=\"countsPeriod.progress / ((timeframeDates?.length || 10) +3)\"\n color=\"primary\">\n </ion-progress-bar>\n </ion-row>\n </ion-grid>\n </ion-card-content>\n </ion-card>\n\n <!-- Gauge values section -->\n <ion-card *ngIf=\"gaugeSelectedPoints.length && gaugeSelectedPoints[0].values.length\" class=\"gauge-card\">\n <ion-card-header>\n <ion-card-title>\n <ion-icon name=\"analytics-outline\"></ion-icon>\n Selected Gauge Data\n </ion-card-title>\n </ion-card-header>\n <ion-card-content>\n <raain-date-focus\n [currentHeight]=\"300\"\n [focusDate]=\"periodBegin\"\n [focusRange]=\"DateRange.DAY\"\n [points]=\"gaugeSelectedPoints\"\n [withoutAll]=\"true\">\n </raain-date-focus>\n </ion-card-content>\n </ion-card>\n\n <!-- Quality Performance Modal -->\n <div (click)=\"closeQualityModal()\" *ngIf=\"showQualityModal\" class=\"quality-modal-overlay\">\n <div (click)=\"$event.stopPropagation()\" class=\"quality-modal-content\">\n <div class=\"quality-modal-header\">\n <h2>Model Quality Performance</h2>\n <ion-button (click)=\"closeQualityModal()\" fill=\"clear\">\n <ion-icon name=\"close-outline\"></ion-icon>\n </ion-button>\n </div>\n <div class=\"quality-modal-body\">\n <div *ngIf=\"qualityIndicatorsLoading\" class=\"quality-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n <span>Loading indicators...</span>\n </div>\n <div *ngIf=\"!qualityIndicatorsLoading && qualityIndicators.length === 0\" class=\"quality-empty\">\n No quality indicators available for this year.\n </div>\n <table *ngIf=\"!qualityIndicatorsLoading && qualityIndicators.length > 0\" class=\"quality-table\">\n <thead>\n <tr>\n <th>Model</th>\n <th>Compare</th>\n <th>Gauges</th>\n <th>Period</th>\n <th>Avg Quality</th>\n <th>Updated</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let indicator of qualityIndicators\">\n <td>{{ indicator.computingVersion }}</td>\n <td>{{ indicator.qualityVersion }}</td>\n <td>{{ indicator.provider }}<br>{{ indicator.timeStepInMinutes }} min</td>\n <td>{{ formatDate(indicator.startDate) }}<br>{{ formatDate(indicator.endDate) }}</td>\n <td>{{ indicator.averageQuality | number:'1.2-2' }}</td>\n <td>{{ formatDate(indicator.lastUpdatedAt) }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n\n</div>\n", styles: [".raain-details-container{max-width:var(--app-max-width);margin:0 auto;padding:0 0 24px}.raain-details-card{width:100%;margin-bottom:20px}.raain-details-card ion-card-header{border-bottom:1px solid rgba(var(--ion-color-medium-rgb),.2)}.raain-details-card ion-card-header ion-card-title{display:flex;align-items:center}.raain-details-card ion-card-header ion-card-title ion-icon{margin-right:8px;color:var(--ion-color-primary)}.node-info-card{background-color:var(--ion-color-light)}.node-info-card .node-header{display:flex;align-items:center}.node-info-card .node-header .node-status{margin-right:16px}.node-info-card .node-header .node-status ion-icon{font-size:24px}.node-info-card .node-header .node-titles{flex:1}.node-info-card .node-header .node-titles ion-card-title{margin:0;font-size:1.4rem;font-weight:600;color:var(--ion-color-dark)}.node-info-card .node-header .node-titles ion-card-subtitle{padding-left:0;margin:4px 0 0;font-size:.9rem;color:var(--ion-color-medium)}.count-map-card,.period-card{background-color:var(--ion-color-light)}.period-card ion-card-title{display:flex;align-items:center}.period-card ion-card-title ion-icon{margin-right:8px;color:var(--ion-color-primary);font-size:20px}.period-row{display:flex;flex-wrap:wrap;align-items:center;gap:16px;position:relative}.now-button{min-width:100px}#all-dates{display:flex;align-items:center;margin-left:auto}#all-dates .toggle-label{margin-right:8px;white-space:nowrap}.refresh-button ion-icon{margin-right:4px;color:var(--ion-color-light)}.provider-selection{display:flex;align-items:center}.quality-info-button{--padding-start: 8px;--padding-end: 8px;font-size:.85rem;color:var(--ion-color-medium)}.quality-info-button ion-icon{font-size:18px;margin-left:4px}.selection-row{display:flex;flex-direction:row;align-items:center;gap:12px}.selection-row ion-select{flex:0 0 auto;min-width:100px;margin-bottom:0;padding:4px 8px;border:1px solid rgba(var(--ion-color-medium-rgb),.3);border-radius:var(--ion-border-radius);background-color:var(--ion-color-light)}.period-start,.period-duration{display:flex;align-items:center}.toggle-cumulative{display:flex;align-items:center;gap:8px;margin-left:auto}.toggle-cumulative .text-primary{color:var(--ion-color-primary);font-weight:600}.hidden-label{display:none}.datetime-input,#periodDuration,.duration-select{padding:8px 12px;border:1px solid rgba(var(--ion-color-medium-rgb),.3);border-radius:var(--ion-border-radius);background-color:var(--ion-color-light);font-family:var(--ion-font-family)}.datetime-input:focus,#periodDuration:focus,.duration-select:focus{outline:none;border-color:var(--ion-color-primary)}#periodDuration,.duration-select{min-width:150px}.gauge-card{background-color:var(--ion-color-light)}.gauge-card ion-card-title{display:flex;align-items:center}.gauge-card ion-card-title ion-icon{margin-right:8px;color:var(--ion-color-primary);font-size:20px}.error-row{cursor:pointer;background-color:rgba(var(--ion-color-danger-rgb),.1);border-left:3px solid var(--ion-color-danger);margin-top:8px;border-radius:4px;transition:background-color .2s ease}.error-row:hover{background-color:rgba(var(--ion-color-danger-rgb),.15)}.error-row .error-content{display:flex;align-items:flex-start;padding:8px 12px;gap:8px}.error-row .error-icon{color:var(--ion-color-danger);font-size:18px;flex-shrink:0;margin-top:2px}.error-row .error-text{flex:1;color:var(--ion-color-danger-shade);font-size:.9rem;word-break:break-word}.error-row .error-text.expanded{white-space:pre-wrap}.error-row .error-caret{color:var(--ion-color-danger);font-size:16px;flex-shrink:0;margin-top:2px}raain-compare-stack{width:100%;display:block}@media (max-width: 768px){.period-row{flex-direction:row;justify-content:space-between;align-items:center}#all-dates{margin-left:auto;padding-left:8px}#all-dates .toggle-label{font-size:.9rem}.map-header{flex-direction:row;justify-content:space-between;align-items:center;gap:16px}}.quality-modal-overlay{position:fixed;inset:0;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:9999}.quality-modal-content{background-color:var(--ion-background-color, #fff);border-radius:12px;width:95%;max-width:900px;max-height:80vh;overflow:auto;box-shadow:0 4px 24px #0003}.quality-modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(var(--ion-color-medium-rgb),.2)}.quality-modal-header h2{margin:0;font-size:1.25rem;font-weight:600;color:var(--ion-color-dark)}.quality-modal-header ion-button{--padding-start: 8px;--padding-end: 8px}.quality-modal-body{padding:20px}.quality-table{width:100%;border-collapse:collapse;margin-top:16px;table-layout:fixed}.quality-table th,.quality-table td{width:16.66%;padding:12px 16px;text-align:center;vertical-align:top;border-bottom:1px solid rgba(var(--ion-color-medium-rgb),.2)}.quality-table th{background-color:rgba(var(--ion-color-primary-rgb),.1);font-weight:600;color:var(--ion-color-dark)}.quality-table td{color:var(--ion-color-dark-tint);font-size:.7rem}.quality-table tbody tr:hover{background-color:rgba(var(--ion-color-primary-rgb),.05)}.quality-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;color:var(--ion-color-medium)}.quality-loading ion-spinner{margin-bottom:12px}.quality-empty{text-align:center;padding:40px 20px;color:var(--ion-color-medium);font-style:italic}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i4.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i4.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i4.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i4.IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: i4.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i4.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i4.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i4.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i4.IonProgressBar, selector: "ion-progress-bar", inputs: ["buffer", "color", "mode", "reversed", "type", "value"] }, { kind: "component", type: i4.IonRow, selector: "ion-row" }, { kind: "component", type: i4.IonSelect, selector: "ion-select", inputs: ["cancelText", "compareWith", "disabled", "interface", "interfaceOptions", "mode", "multiple", "name", "okText", "placeholder", "selectedText", "value"] }, { kind: "component", type: i4.IonSelectOption, selector: "ion-select-option", inputs: ["disabled", "value"] }, { kind: "component", type: i4.IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: i4.IonToggle, selector: "ion-toggle", inputs: ["checked", "color", "disabled", "mode", "name", "value"] }, { kind: "directive", type: i4.BooleanValueAccessor, selector: "ion-checkbox,ion-toggle" }, { kind: "directive", type: i4.SelectValueAccessor, selector: "ion-range, ion-select, ion-radio-group, ion-segment, ion-datetime" }, { kind: "component", type: RaainMapComponent, selector: "raain-map", inputs: ["coordinates", "markers", "timeframeContainers", "autoplay", "showMarkers", "showSpeedMarkers", "showVisiblePixelMarkers", "showCumulative", "cumulativeDurationInMinutes", "currentHeight", "timeframeDates", "defaultDate", "sumValues", "sumFn"], outputs: ["selectedMarker", "changedDate", "changedSum"] }, { kind: "component", type: RaainCompareStackComponent, selector: "raain-compare-stack", inputs: ["compareManager", "currentHeight", "cumulative"], outputs: ["selectedPoint"] }, { kind: "component", type: RaainDateFocusComponent, selector: "raain-date-focus", inputs: ["points", "focusDate", "focusRange", "withoutAll", "currentHeight"] }, { kind: "component", type: RaainDateDynamicComponent, selector: "raain-date-dynamic", inputs: ["points", "focusDate", "focusRange", "withoutAll", "currentHeight", "fetchData"], outputs: ["changedDate"] }, { kind: "pipe", type: i2.DecimalPipe, name: "number" }, { kind: "pipe", type: i2.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2559
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: RaainDetailsComponent, decorators: [{
2560
- type: Component,
2561
- args: [{ selector: 'raain-details', changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Main content container -->\n<div *ngIf=\"rainNode\" class=\"raain-details-container\">\n\n <!-- Period selection section -->\n <ion-card class=\"period-card\">\n <ion-card-content>\n <div class=\"period-controls\">\n <div class=\"period-row\">\n\n <ion-button (click)=\"toggleHistory = !toggleHistory; onEnableCountHistoryTab(rainNode)\"\n fill=\"outline\">\n <ion-icon name=\"calendar-clear-outline\" slot=\"start\"></ion-icon>\n <ion-icon [name]=\"toggleHistory ? 'chevron-down' : 'chevron-forward'\" slot=\"end\"></ion-icon>\n </ion-button>\n\n <div class=\"period-start ion-hide-md-down\">\n <input (change)=\"onPeriodBeginChange($event)\"\n [disabled]=\"toggleCumulative\"\n [value]=\"periodBeginAsString\"\n class=\"datetime-input\"\n type=\"datetime-local\">\n </div>\n\n <div class=\"period-duration ion-hide-md-down\">\n <ion-select (ionDismiss)=\"onPeriodDurationChange($event)\"\n [(ngModel)]=\"periodDurationAsString\"\n class=\"duration-select\"\n id=\"periodDuration\"\n interface=\"popover\">\n <ion-select-option value=\"0.25\">15 minutes</ion-select-option>\n <ion-select-option value=\"0.5\">30 minutes</ion-select-option>\n <ion-select-option value=\"1\">1 hour</ion-select-option>\n <ion-select-option value=\"2\">2 hours</ion-select-option>\n <ion-select-option value=\"3\">3 hours</ion-select-option>\n <ion-select-option value=\"4\">4 hours</ion-select-option>\n <ion-select-option value=\"5\">5 hours</ion-select-option>\n <ion-select-option value=\"6\">6 hours</ion-select-option>\n <ion-select-option value=\"8\">8 hours</ion-select-option>\n <ion-select-option value=\"10\">10 hours</ion-select-option>\n <ion-select-option value=\"12\">12 hours</ion-select-option>\n <ion-select-option *ngIf=\"isAdmin\" value=\"24\">24 hours</ion-select-option>\n </ion-select>\n </div>\n\n <div class=\"period-start ion-hide-md-down\">\n <input (change)=\"onPeriodEndChange($event)\"\n [disabled]=\"!toggleCumulative\"\n [value]=\"periodEndAsString\"\n class=\"datetime-input\"\n type=\"datetime-local\">\n </div>\n\n <div class=\"toggle-cumulative\">\n <ion-label [class.text-primary]=\"toggleCumulative\">\n {{ toggleCumulative ? 'Cumulative' : 'Granular' }}\n </ion-label>\n <ion-toggle (ionChange)=\"toggleCumulativeChanged($event)\"\n [(ngModel)]=\"toggleCumulative\"\n [checked]=\"toggleCumulative\">\n </ion-toggle>\n </div>\n </div>\n\n <!-- Hidden label for change detection (uncomment to debug)\n <div class=\"hidden-label\">{{ onChangeDetectionTest(rainNode) }}</div>\n -->\n </div>\n\n <!-- Historical map section -->\n <div *ngIf=\"toggleHistory\" class=\"period-controls\">\n <raain-date-dynamic (changedDate)=\"onDateChangeInCount($event)\"\n [currentHeight]=\"300\"\n [fetchData]=\"fetchDataWrapper\"\n [points]=\"countPoints\">\n </raain-date-dynamic>\n </div>\n </ion-card-content>\n </ion-card>\n\n <!-- Map performance -->\n <ion-grid class=\"map-performance\">\n <ion-row id=\"progressAndRefresh\">\n <ion-col class=\"provider-selection\" size=\"12\" size-md=\"6\">\n <ion-button (click)=\"openQualityModal()\" class=\"quality-info-button\" fill=\"clear\">\n {{ refreshManager.rainComputationMapVersion }}\n <ion-icon name=\"help-circle-outline\" slot=\"end\"></ion-icon>\n </ion-button>\n <div *ngIf=\"availableProviders.length > 0 || availableTimeSteps.length > 0\" class=\"selection-row\">\n <ion-select (ionChange)=\"onProviderChanged($event)\"\n [value]=\"selectedProvider\"\n interface=\"popover\"\n label=\"Provider\"\n placeholder=\"Select Provider\">\n <ion-select-option *ngFor=\"let provider of availableProviders\" [value]=\"provider\">\n {{ provider }}\n </ion-select-option>\n </ion-select>\n\n <ion-select (ionChange)=\"onTimeStepChanged($event)\"\n [value]=\"selectedTimeStep\"\n interface=\"popover\"\n label=\"Time Step\"\n placeholder=\"Select Time Step\">\n <ion-select-option *ngFor=\"let step of availableTimeSteps\" [value]=\"step\">\n {{ step }} min\n </ion-select-option>\n </ion-select>\n </div>\n </ion-col>\n <ion-col class=\"ion-text-right\" size=\"12\" size-md=\"6\">\n <ion-label class=\"ion-margin-end\">\n <span *ngIf=\"percentOfComputations\">\n {{ percentOfComputations }}% Images\n <i *ngIf=\"countsPeriod.progress\" class=\"progress-indicator\">\n In Progress: {{ countsPeriod.progress }}...\n </i>\n </span>\n <span *ngIf=\"!percentOfComputations\">\n No image available\n </span>\n\n </ion-label>\n\n <ion-button (click)=\"refreshMap()\" [disabled]=\"refreshInProgress\" class=\"refresh-button\">\n Refresh Map\n </ion-button>\n </ion-col>\n </ion-row>\n\n <!-- status update row -->\n <ion-row>\n <!-- Progress col -->\n <ion-progress-bar\n [buffer]=\"(countsPeriod.progress / ((timeframeDates?.length || 10) +3))+0.01\"\n [style.visibility]=\"refreshInProgress ? 'visible' : 'hidden'\"\n [value]=\"countsPeriod.progress / ((timeframeDates?.length || 10) +3)\"\n color=\"primary\">\n </ion-progress-bar>\n\n <!-- Error col -->\n <ion-col (click)=\"showFullError = !showFullError\" *ngIf=\"refreshManager.lastError\" class=\"error-row\"\n size=\"12\">\n <div class=\"error-content\">\n <ion-icon class=\"error-icon\" name=\"warning-outline\"></ion-icon>\n <span [class.expanded]=\"showFullError\" class=\"error-text\">\n {{ showFullError ? refreshManager.lastError : truncatedError }}\n </span>\n <ion-icon [name]=\"showFullError ? 'chevron-up' : 'chevron-down'\" class=\"error-caret\"></ion-icon>\n </div>\n </ion-col>\n </ion-row>\n </ion-grid>\n\n <!-- Map section -->\n <ion-card class=\"map-card\">\n <ion-card-content class=\"map-content\">\n <ion-grid>\n <ion-row *ngIf=\"toggleMap && percentOfImages\">\n <!-- Map component -->\n <ion-col class=\"map-column\" size-lg=\"7\" size-md=\"12\">\n <div class=\"map-container\">\n <raain-map #raainMapRef\n (changedDate)=\"onDateChangeInMap($event)\"\n (changedSum)=\"onSumChangeInMap($event)\"\n (selectedMarker)=\"onGaugeSelectInMap($event)\"\n [coordinates]=\"coordinates\"\n [cumulativeDurationInMinutes]=\"cumulativeDurationInMinutes\"\n [currentHeight]=\"500\"\n [defaultDate]=\"dateShown\"\n [markers]=\"{\n borders,\n gauges: compareManager.gaugesInMap,\n gaugesInCompare: compareManager.gaugesInCompare,\n selectedGauges: compareManager.selectedGauges,\n pixels: compareManager.selectedPixels,\n pixelsSolution: compareManager.pixelsSolutions?.length ? compareManager.pixelsSolutions[0] : [],\n speeds: compareManager.speeds\n }\"\n [showCumulative]=\"toggleCumulative\"\n [showVisiblePixelMarkers]=\"showPixelMarkers\"\n [sumFn]=\"refreshManager.sumFn\"\n [sumValues]=\"refreshManager.sumValues\"\n [timeframeContainers]=\"timeframeContainers\"\n [timeframeDates]=\"timeframeDates\">\n </raain-map>\n </div>\n\n <div class=\"data-column\">\n <!-- Technical details (collapsible for cleaner UI) -->\n <details class=\"technical-details\">\n <summary>Image Details</summary>\n <div class=\"details-content\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Computed:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapDoneDate | date:'yyyy-MM-dd HH:mm' }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Version:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapVersion }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Launched by:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapLaunchedBy }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Time spent:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapTimeSpentInMs }}\n ms</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Date:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapDate?.toISOString() }}\n | {{ refreshManager.rainComputationMapDate | date:'yyyy-MM-dd HH:mm' }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Water in the map:</span>\n <span class=\"detail-value\">{{ sumDetails }}</span>\n <ion-toggle\n (ionChange)=\"onTogglePixelMarkers()\"\n [(ngModel)]=\"showPixelMarkers\"\n style=\"margin-left: 8px; transform: scale(0.7);\">\n </ion-toggle>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Source DBZ min:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapOriginalMin }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Source DBZ max:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapOriginalMax }}</span>\n </div>\n </div>\n </details>\n </div>\n </ion-col>\n\n <!-- Data panel -->\n <ion-col *ngIf=\"!!compareManager.compareVersion\" class=\"data-column\" size-lg=\"5\" size-md=\"12\">\n <div class=\"data-panel\">\n <!-- Compare stack component -->\n <div class=\"compare-stack\">\n <raain-compare-stack\n (selectedPoint)=\"onGaugeSelectInCompare($event)\"\n [compareManager]=\"compareManager\"\n [cumulative]=\"toggleCumulative\">\n </raain-compare-stack>\n </div>\n\n <!-- Technical details (collapsible for cleaner UI) -->\n <details class=\"technical-details\">\n <summary>Compare Details</summary>\n <div class=\"details-content\">\n <div [ngClass]=\"{'warning': refreshManager.rainComputationMapDoneDate?.getTime() > compareManager.currentQualityDoneDate?.getTime() + 60000}\"\n class=\"detail-row\">\n <span class=\"detail-label\">Computed:</span>\n <span class=\"detail-value\">{{ compareManager.currentQualityDoneDate | date:'yyyy-MM-dd HH:mm' }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Version:</span>\n <span class=\"detail-value\">{{ compareManager.compareVersion }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Launched by:</span>\n <span class=\"detail-value\">{{ compareManager.currentQualityLaunchedBy }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Time spent:</span>\n <span class=\"detail-value\">{{ compareManager.currentQualityTimeSpentInMs }}\n ms</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Gauges:</span>\n <span class=\"detail-value\">{{ compareManager.gaugesInCompare.length }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Points:</span>\n <span class=\"detail-value\">{{ compareManager.globalComparePoints.length }}</span>\n </div>\n </div>\n </details>\n </div>\n </ion-col>\n </ion-row>\n <ion-row>\n <!-- Bottom progress bar -->\n <ion-progress-bar *ngIf=\"refreshInProgress\"\n [buffer]=\"(countsPeriod.progress / ((timeframeDates?.length || 10) +3))+0.01\"\n [value]=\"countsPeriod.progress / ((timeframeDates?.length || 10) +3)\"\n color=\"primary\">\n </ion-progress-bar>\n </ion-row>\n </ion-grid>\n </ion-card-content>\n </ion-card>\n\n <!-- Gauge values section -->\n <ion-card *ngIf=\"gaugeSelectedPoints.length && gaugeSelectedPoints[0].values.length\" class=\"gauge-card\">\n <ion-card-header>\n <ion-card-title>\n <ion-icon name=\"analytics-outline\"></ion-icon>\n Selected Gauge Data\n </ion-card-title>\n </ion-card-header>\n <ion-card-content>\n <raain-date-focus\n [currentHeight]=\"300\"\n [focusDate]=\"periodBegin\"\n [focusRange]=\"DateRange.DAY\"\n [points]=\"gaugeSelectedPoints\"\n [withoutAll]=\"true\">\n </raain-date-focus>\n </ion-card-content>\n </ion-card>\n\n <!-- Quality Performance Modal -->\n <div (click)=\"closeQualityModal()\" *ngIf=\"showQualityModal\" class=\"quality-modal-overlay\">\n <div (click)=\"$event.stopPropagation()\" class=\"quality-modal-content\">\n <div class=\"quality-modal-header\">\n <h2>Model Quality Performance</h2>\n <ion-button (click)=\"closeQualityModal()\" fill=\"clear\">\n <ion-icon name=\"close-outline\"></ion-icon>\n </ion-button>\n </div>\n <div class=\"quality-modal-body\">\n <div *ngIf=\"qualityIndicatorsLoading\" class=\"quality-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n <span>Loading indicators...</span>\n </div>\n <div *ngIf=\"!qualityIndicatorsLoading && qualityIndicators.length === 0\" class=\"quality-empty\">\n No quality indicators available for this year.\n </div>\n <table *ngIf=\"!qualityIndicatorsLoading && qualityIndicators.length > 0\" class=\"quality-table\">\n <thead>\n <tr>\n <th>Model</th>\n <th>Compare</th>\n <th>Gauges</th>\n <th>Period</th>\n <th>Avg Quality</th>\n <th>Updated</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let indicator of qualityIndicators\">\n <td>{{ indicator.computingVersion }}</td>\n <td>{{ indicator.qualityVersion }}</td>\n <td>{{ indicator.provider }}<br>{{ indicator.timeStepInMinutes }} min</td>\n <td>{{ formatDate(indicator.startDate) }}<br>{{ formatDate(indicator.endDate) }}</td>\n <td>{{ indicator.averageQuality | number:'1.2-2' }}</td>\n <td>{{ formatDate(indicator.lastUpdatedAt) }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n\n</div>\n", styles: [".raain-details-container{max-width:var(--app-max-width);margin:0 auto;padding:0 0 24px}.raain-details-card{width:100%;margin-bottom:20px}.raain-details-card ion-card-header{border-bottom:1px solid rgba(var(--ion-color-medium-rgb),.2)}.raain-details-card ion-card-header ion-card-title{display:flex;align-items:center}.raain-details-card ion-card-header ion-card-title ion-icon{margin-right:8px;color:var(--ion-color-primary)}.node-info-card{background-color:var(--ion-color-light)}.node-info-card .node-header{display:flex;align-items:center}.node-info-card .node-header .node-status{margin-right:16px}.node-info-card .node-header .node-status ion-icon{font-size:24px}.node-info-card .node-header .node-titles{flex:1}.node-info-card .node-header .node-titles ion-card-title{margin:0;font-size:1.4rem;font-weight:600;color:var(--ion-color-dark)}.node-info-card .node-header .node-titles ion-card-subtitle{padding-left:0;margin:4px 0 0;font-size:.9rem;color:var(--ion-color-medium)}.count-map-card,.period-card{background-color:var(--ion-color-light)}.period-card ion-card-title{display:flex;align-items:center}.period-card ion-card-title ion-icon{margin-right:8px;color:var(--ion-color-primary);font-size:20px}.period-row{display:flex;flex-wrap:wrap;align-items:center;gap:16px;position:relative}.now-button{min-width:100px}#all-dates{display:flex;align-items:center;margin-left:auto}#all-dates .toggle-label{margin-right:8px;white-space:nowrap}.refresh-button ion-icon{margin-right:4px;color:var(--ion-color-light)}.provider-selection{display:flex;align-items:center}.quality-info-button{--padding-start: 8px;--padding-end: 8px;font-size:.85rem;color:var(--ion-color-medium)}.quality-info-button ion-icon{font-size:18px;margin-left:4px}.selection-row{display:flex;flex-direction:row;align-items:center;gap:12px}.selection-row ion-select{flex:0 0 auto;min-width:100px;margin-bottom:0;padding:4px 8px;border:1px solid rgba(var(--ion-color-medium-rgb),.3);border-radius:var(--ion-border-radius);background-color:var(--ion-color-light)}.period-start,.period-duration{display:flex;align-items:center}.toggle-cumulative{display:flex;align-items:center;gap:8px;margin-left:auto}.toggle-cumulative .text-primary{color:var(--ion-color-primary);font-weight:600}.hidden-label{display:none}.datetime-input,#periodDuration,.duration-select{padding:8px 12px;border:1px solid rgba(var(--ion-color-medium-rgb),.3);border-radius:var(--ion-border-radius);background-color:var(--ion-color-light);font-family:var(--ion-font-family)}.datetime-input:focus,#periodDuration:focus,.duration-select:focus{outline:none;border-color:var(--ion-color-primary)}#periodDuration,.duration-select{min-width:150px}.gauge-card{background-color:var(--ion-color-light)}.gauge-card ion-card-title{display:flex;align-items:center}.gauge-card ion-card-title ion-icon{margin-right:8px;color:var(--ion-color-primary);font-size:20px}.error-row{cursor:pointer;background-color:rgba(var(--ion-color-danger-rgb),.1);border-left:3px solid var(--ion-color-danger);margin-top:8px;border-radius:4px;transition:background-color .2s ease}.error-row:hover{background-color:rgba(var(--ion-color-danger-rgb),.15)}.error-row .error-content{display:flex;align-items:flex-start;padding:8px 12px;gap:8px}.error-row .error-icon{color:var(--ion-color-danger);font-size:18px;flex-shrink:0;margin-top:2px}.error-row .error-text{flex:1;color:var(--ion-color-danger-shade);font-size:.9rem;word-break:break-word}.error-row .error-text.expanded{white-space:pre-wrap}.error-row .error-caret{color:var(--ion-color-danger);font-size:16px;flex-shrink:0;margin-top:2px}raain-compare-stack{width:100%;display:block}@media (max-width: 768px){.period-row{flex-direction:row;justify-content:space-between;align-items:center}#all-dates{margin-left:auto;padding-left:8px}#all-dates .toggle-label{font-size:.9rem}.map-header{flex-direction:row;justify-content:space-between;align-items:center;gap:16px}}.quality-modal-overlay{position:fixed;inset:0;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:9999}.quality-modal-content{background-color:var(--ion-background-color, #fff);border-radius:12px;width:95%;max-width:900px;max-height:80vh;overflow:auto;box-shadow:0 4px 24px #0003}.quality-modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(var(--ion-color-medium-rgb),.2)}.quality-modal-header h2{margin:0;font-size:1.25rem;font-weight:600;color:var(--ion-color-dark)}.quality-modal-header ion-button{--padding-start: 8px;--padding-end: 8px}.quality-modal-body{padding:20px}.quality-table{width:100%;border-collapse:collapse;margin-top:16px;table-layout:fixed}.quality-table th,.quality-table td{width:16.66%;padding:12px 16px;text-align:center;vertical-align:top;border-bottom:1px solid rgba(var(--ion-color-medium-rgb),.2)}.quality-table th{background-color:rgba(var(--ion-color-primary-rgb),.1);font-weight:600;color:var(--ion-color-dark)}.quality-table td{color:var(--ion-color-dark-tint);font-size:.7rem}.quality-table tbody tr:hover{background-color:rgba(var(--ion-color-primary-rgb),.05)}.quality-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;color:var(--ion-color-medium)}.quality-loading ion-spinner{margin-bottom:12px}.quality-empty{text-align:center;padding:40px 20px;color:var(--ion-color-medium);font-style:italic}\n"] }]
2562
- }], ctorParameters: function () { return [{ type: Storage }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { toggleAdmin: [{
2563
- type: Input
2564
- }], rainNode: [{
2565
- type: Input
2566
- }], compareManager: [{
2567
- type: Input
2568
- }], refreshManager: [{
2569
- type: Input
2570
- }], profileService: [{
2571
- type: Input
2572
- }], radarService: [{
2573
- type: Input
2574
- }] } });
2575
-
2576
- class Cache {
2577
- constructor() {
2578
- this._cache = {};
2617
+ // === Count
2618
+ async getCounts(rainId, options) {
2619
+ try {
2620
+ const params = {
2621
+ range: options.range,
2622
+ begin: options.periodBegin.toISOString(),
2623
+ };
2624
+ const queryString = BuildQueryString(params);
2625
+ const resp = await this.fidjService.sendOnEndpoint({
2626
+ key: 'rains',
2627
+ relativePath: rainId + '/counts' + (queryString ? '?' + queryString : ''),
2628
+ verb: 'GET',
2629
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains/',
2630
+ });
2631
+ const counts = resp.data.counts.result;
2632
+ const percentImages = [], percentRainy = [], percentQ = [];
2633
+ counts.forEach((c) => {
2634
+ const label = this.setDateComponents(options.periodBegin, c);
2635
+ percentImages.push(new XYType(c.percentImages ?? 0, NaN, NaN, label));
2636
+ percentRainy.push(new XYType(c.percentRainy ?? 0, NaN, NaN, label));
2637
+ percentQ.push(new XYType(c.percentQ ?? 0, NaN, NaN, label));
2638
+ });
2639
+ return {
2640
+ percentImages,
2641
+ percentRainy,
2642
+ percentQ,
2643
+ queueRunning: resp.data.queueRunning,
2644
+ };
2645
+ }
2646
+ catch (e) {
2647
+ await this.checkError(e);
2648
+ }
2649
+ return null;
2579
2650
  }
2580
- async getValue(key, asyncGetter) {
2581
- if (!Object.prototype.hasOwnProperty.call(this._cache, key)) {
2582
- console.log('cache not done: ', key);
2583
- this.putValue(key, await asyncGetter());
2651
+ async getCountsHour(rainId, options) {
2652
+ try {
2653
+ const params = {
2654
+ range: 'hour',
2655
+ begin: options.periodBegin.toISOString(),
2656
+ };
2657
+ const queryString = BuildQueryString(params);
2658
+ const resp = await this.fidjService.sendOnEndpoint({
2659
+ key: 'rains',
2660
+ relativePath: rainId + '/counts' + (queryString ? '?' + queryString : ''),
2661
+ verb: 'GET',
2662
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains/',
2663
+ });
2664
+ const counts = resp.data.counts.result;
2665
+ const percentImages = [], rainySum = [], rainyCount = [];
2666
+ counts.forEach((c) => {
2667
+ const label = this.setDateComponents(options.periodBegin, c);
2668
+ percentImages.push(new XYType(c.percentImages ?? 0, NaN, NaN, label));
2669
+ rainyCount.push(new XYType(c.rainyCount ?? 0, NaN, NaN, label));
2670
+ rainySum.push(new XYType(c.rainySum ?? 0, NaN, NaN, label));
2671
+ });
2672
+ return {
2673
+ percentImages,
2674
+ rainyCount,
2675
+ rainySum,
2676
+ queueRunning: resp.data.queueRunning,
2677
+ };
2584
2678
  }
2585
- else {
2586
- console.log('cache done: ', key);
2679
+ catch (e) {
2680
+ await this.checkError(e);
2587
2681
  }
2588
- return this._cache[key];
2682
+ return null;
2589
2683
  }
2590
- putValue(key, value) {
2591
- this._cache[key] = value;
2592
- const length = Object.getOwnPropertyNames(this._cache).length;
2593
- if (length > 30) {
2594
- console.warn('Pb on cache size exceed ? do a restart ?', length);
2684
+ // === Computing ===
2685
+ async getRainComputationCumulativeCartesianMapById(rainId, rainComputationCumulativeId) {
2686
+ const params = { format: 'cartesian-map' };
2687
+ const queryString = BuildQueryString(params);
2688
+ try {
2689
+ const response = await this.fidjService.sendOnEndpoint({
2690
+ key: 'rains',
2691
+ verb: 'GET',
2692
+ relativePath: `${rainId}/cumulatives/${rainComputationCumulativeId}?${queryString}`,
2693
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains',
2694
+ });
2695
+ if (!response.data['cartesian-map']) {
2696
+ return null;
2697
+ }
2698
+ const rainComputationMap = new RainComputationMap(response.data['cartesian-map']);
2699
+ rainComputationMap.name = rainId + '.rain.cartesian-map';
2700
+ return rainComputationMap;
2701
+ }
2702
+ catch (e) {
2703
+ await this.checkError(e);
2595
2704
  }
2705
+ return null;
2596
2706
  }
2597
- }
2598
- Cache.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: Cache, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2599
- Cache.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: Cache, providedIn: 'root' });
2600
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: Cache, decorators: [{
2601
- type: Injectable,
2602
- args: [{
2603
- providedIn: 'root',
2604
- }]
2605
- }], ctorParameters: function () { return []; } });
2606
-
2607
- class FidjStorageNode {
2608
- constructor() {
2609
- this.radars = [];
2610
- this.rains = [];
2611
- this.gauges = [];
2612
- this.events = [];
2613
- this.infos = {};
2614
- }
2615
- static getEmptyNode() {
2616
- return new FidjStorageNode();
2617
- }
2618
- static getDemoNode() {
2619
- const demoNode = new FidjStorageNode();
2620
- const link = new Link('rain', 'https://demo/api/rains/2');
2621
- /*
2622
- demoNode.radars = [
2623
- new RadarNode({
2624
- id: '5efd04569cb1f4161bd69dc7',
2625
- name: 'demo radar A',
2626
- links: [link],
2627
- latitude: 48.774569,
2628
- longitude: 2.008407
2629
- }),
2630
- new RadarNode({
2631
- id: 'dr2',
2632
- name: 'demo radar B',
2633
- links: [link],
2634
- latitude: 0.11,
2635
- longitude: -0.753
2636
- }),
2637
- new RadarNode({
2638
- id: 'dr3',
2639
- name: 'demo radar C',
2640
- latitude: 0.13,
2641
- longitude: -0.753,
2642
- links: []
2643
- }),
2644
- new RadarNode({
2645
- id: 'dr4',
2646
- name: 'demo radar D',
2647
- latitude: 0.14,
2648
- longitude: -0.74,
2649
- links: []
2650
- })];
2651
- demoNode.rains = [
2652
- new RainNode({
2653
- id: '5efd04569cb1f4161bd69dc8',
2654
- name: 'Demo rain zone A',
2655
- links: [new Link('radar', 'https://demo/api/radars/5efcfe619cb1f4161bd69dc3')],
2656
- status: 0,
2657
- quality: 75,
2658
- latitude: 48.774569,
2659
- longitude: 2.008407
2660
- }),
2661
- new RainNode({
2662
- id: 'dz2',
2663
- name: 'Demo rain zone B',
2664
- radars: [demoNode.radars[0], demoNode.radars[1]],
2665
- status: 1,
2666
- quality: 50,
2667
- latitude: 48.774569,
2668
- longitude: 2.008407
2669
- }),
2670
- new RainNode({
2671
- id: 'dz3',
2672
- name: 'Demo rain zone C',
2673
- radars: [demoNode.radars[0], demoNode.radars[1]],
2674
- status: 2,
2675
- quality: 75,
2676
- latitude: 48.774569,
2677
- longitude: 2.008407
2678
- }),
2679
- new RainNode({
2680
- id: 'dz4',
2681
- name: 'Demo rain zone D',
2682
- radars: [demoNode.radars[0], demoNode.radars[1]],
2683
- status: 3,
2684
- quality: 95,
2685
- latitude: 48.774569,
2686
- longitude: 2.008407
2687
- })];
2688
-
2689
- demoNode.gauges = [
2690
- new GaugeNode({
2691
- id: 'g1',
2692
- name: 'Gauge A',
2693
- latitude: 48.7748,
2694
- longitude: 2.28407,
2695
- }),
2696
- new GaugeNode({
2697
- id: 'g2',
2698
- name: 'Gauge B',
2699
- latitude: 48.874569,
2700
- longitude: 2.108407,
2701
- })];
2702
- demoNode.events = [{
2703
- id: 'e2',
2704
- title: 'Need support ?',
2705
- status: 0,
2706
- red: false,
2707
- description: 'This area is dedicated to support you and your team. Support is made on Radar or Rain quality, ' +
2708
- 'or any feedback we can have about your production system. Our goal : improving your data.',
2709
- created: new Date(),
2710
- modified: new Date()
2711
- }];
2712
- demoNode.team = {
2713
- id: 'p1',
2714
- email: 'demo@demo.com',
2715
- name: 'demo guy',
2716
- description: 'the demo guy'
2717
- };
2718
-
2719
- */
2720
- return demoNode;
2721
- }
2722
- }
2723
- class FidjStorageResult {
2724
- }
2725
- class FidjStorage {
2726
- constructor(storage) {
2727
- this.storage = storage;
2728
- this.node = FidjStorageNode.getEmptyNode();
2729
- this.fidjMetaResult = { data: new FidjStorageNode() };
2730
- }
2731
- async storeData(fidjService, data) {
2732
- this.node = JSON.parse(JSON.stringify(data));
2733
- this.fidjMetaResult.data = this.node;
2734
- if (this.isDemoMode) {
2735
- this.storage.set('fidjMetaResult', JSON.stringify(this.fidjMetaResult));
2736
- return;
2737
- }
2738
- await fidjService.put(this.fidjMetaResult);
2739
- }
2740
- async getRefreshedNodeCopy(fidjService) {
2741
- if (this.isDemoMode) {
2742
- const fidjMetaResult = this.storage.get('fidjMetaResult');
2743
- if (fidjMetaResult) {
2744
- this.fidjMetaResult = JSON.parse(fidjMetaResult);
2745
- this.node = this.fidjMetaResult.data;
2746
- }
2747
- return JSON.parse(JSON.stringify(this.node));
2748
- }
2749
- const firstDemoData = async () => {
2750
- this.node = FidjStorageNode.getDemoNode();
2751
- await this.storeData(fidjService, this.node);
2752
- };
2753
- await fidjService.sync(firstDemoData);
2754
- const fidjFindAllResults = await fidjService.findAll();
2755
- if (fidjFindAllResults && fidjFindAllResults.length > 0) {
2756
- if (fidjFindAllResults[0].data) {
2757
- this.fidjMetaResult = fidjFindAllResults[0];
2758
- this.node = this.fidjMetaResult.data;
2759
- }
2760
- }
2761
- return JSON.parse(JSON.stringify(this.node));
2762
- }
2763
- setDemoMode(isDemo) {
2764
- this.isDemoMode = isDemo;
2765
- }
2766
- }
2767
-
2768
- class ProfileService {
2769
- constructor(storage, fidjService, httpClient, router) {
2770
- this.storage = storage;
2771
- this.fidjService = fidjService;
2772
- this.httpClient = httpClient;
2773
- this.router = router;
2774
- this.email = this.storage.get('raain-email');
2775
- this.asTeamId = this.storage.get('raain-asTeamId');
2776
- this.readyForSync = new BehaviorSubject(false);
2777
- this.roles = [];
2778
- this.fidjStorage = new FidjStorage(storage);
2779
- this.isDemoMode = false;
2780
- }
2781
- get isDemoMode() {
2782
- return this.isDemo;
2783
- }
2784
- set isDemoMode(mode) {
2785
- this.isDemo = mode ? mode : true;
2786
- this.fidjStorage.setDemoMode(this.isDemo);
2787
- }
2788
- get defaultUrlForAPI() {
2789
- return this.storage.get('raain-urlForAPI');
2790
- }
2791
- set defaultUrlForAPI(url) {
2792
- this.storage.set('raain-urlForAPI', url);
2793
- }
2794
- async refreshProfile() {
2707
+ async getRainComputationCumulativeCumulativesMapById(rainId, rainComputationCumulativeId, cumulativeHours) {
2708
+ const queryPath = `${rainId}/cumulatives/${rainComputationCumulativeId}/cumulative/${cumulativeHours}`;
2795
2709
  try {
2796
- this.nodeData = await this.fidjStorage.getRefreshedNodeCopy(this.fidjService);
2797
- this.setRoles(await this.fidjService.getRoles());
2798
- return this.nodeData;
2710
+ const response = await this.fidjService.sendOnEndpoint({
2711
+ key: 'rains',
2712
+ verb: 'GET',
2713
+ relativePath: queryPath,
2714
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains',
2715
+ });
2716
+ if (!response.data['cumulative']) {
2717
+ return null;
2718
+ }
2719
+ const rainComputationMap = new RainComputationMap(response.data['cumulative']);
2720
+ rainComputationMap.name = rainId + '.rain.cumulative-cumulative';
2721
+ return rainComputationMap;
2799
2722
  }
2800
2723
  catch (e) {
2801
2724
  await this.checkError(e);
2802
2725
  }
2726
+ return null;
2803
2727
  }
2804
- async isConnected() {
2805
- return this.fidjService.isConnected();
2806
- }
2807
- getEmail() {
2808
- return this.email ?? this.storage.get('raain-email', this.email);
2809
- }
2810
- setEmail(email) {
2811
- this.email = email;
2812
- this.storage.set('raain-email', this.email);
2813
- }
2814
- async logout(fidjKey, fidjProd) {
2815
- // this.storage.remove('raain-email');
2816
- if (!fidjKey) {
2817
- try {
2818
- await this.fidjService.loginInDemoMode();
2819
- this.readyForSync.next(true);
2820
- }
2821
- catch (err) {
2822
- console.error('initFidj catch pb: ', err);
2823
- }
2824
- return;
2825
- }
2826
- await this.fidjService.logout(true);
2728
+ async getRainCumulativeCompareByDate(rainNode, rainComputationCumulativeId, date) {
2729
+ const params = { date: date.toISOString() };
2730
+ const queryString = BuildQueryString(params);
2827
2731
  try {
2828
- await this.fidjService.init(fidjKey, {
2829
- logLevel: LoggerLevelEnum.WARN,
2830
- crypto: false,
2831
- prod: fidjProd,
2832
- useDB: false,
2732
+ const response = await this.fidjService.sendOnEndpoint({
2733
+ key: 'rains',
2734
+ verb: 'GET',
2735
+ relativePath: `${rainNode.id}/cumulatives/${rainComputationCumulativeId}/compares?${queryString}`,
2736
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains',
2833
2737
  });
2834
- this.readyForSync.next(true);
2835
- }
2836
- catch (err) {
2837
- console.error('initFidj catch pb: ', err);
2738
+ const qualityJson = response.data.qualities[0];
2739
+ const rainComputationQuality = new RainComputationQuality(qualityJson);
2740
+ rainComputationQuality.qualitySpeedMatrixContainer =
2741
+ SpeedMatrixContainer.CreateFromJson(rainComputationQuality.qualitySpeedMatrixContainer);
2742
+ return rainComputationQuality;
2838
2743
  }
2839
- }
2840
- async checkError(error) {
2841
- if (error.code === 401) {
2842
- console.warn('Pb on auth');
2843
- if (this.router.url.indexOf('login') < 0) {
2844
- try {
2845
- await this.fidjService.logout();
2846
- }
2847
- catch (ignored) {
2848
- // Ignore logout errors as we're redirecting to logout page anyway
2849
- }
2850
- return this.gotoLout();
2851
- }
2744
+ catch (e) {
2745
+ await this.checkError(e);
2852
2746
  }
2747
+ return null;
2853
2748
  }
2854
- async gotoLout() {
2749
+ async getRainCumulativeCumulativesCompareByDate(rainNode, rainComputationCumulativeId, date, cumulativeHours) {
2750
+ const params = {
2751
+ date: date.toISOString(),
2752
+ cumulativeHours,
2753
+ };
2754
+ const queryString = BuildQueryString(params);
2855
2755
  try {
2856
- if (this.router.url.indexOf('login') > -1) {
2857
- return;
2858
- }
2859
- await this.router.navigateByUrl('/logout', {
2860
- skipLocationChange: true,
2861
- replaceUrl: true,
2756
+ const response = await this.fidjService.sendOnEndpoint({
2757
+ key: 'rains',
2758
+ verb: 'GET',
2759
+ relativePath: `${rainNode.id}/cumulatives/${rainComputationCumulativeId}/compares?${queryString}`,
2760
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains',
2862
2761
  });
2762
+ const qualityJson = response.data.qualities[0];
2763
+ const rainComputationQuality = new RainComputationQuality(qualityJson);
2764
+ rainComputationQuality.qualitySpeedMatrixContainer =
2765
+ SpeedMatrixContainer.CreateFromJson(rainComputationQuality.qualitySpeedMatrixContainer);
2766
+ return rainComputationQuality;
2863
2767
  }
2864
2768
  catch (e) {
2865
- console.error('gotoLout error: ', e);
2866
- }
2867
- }
2868
- async gotoLogin() {
2869
- if (this.router.url.indexOf('login') > -1) {
2870
- return;
2769
+ await this.checkError(e);
2871
2770
  }
2872
- // await this.router.navigateByUrl('/', {skipLocationChange: true});
2873
- await this.router.navigate(['/login']);
2874
- }
2875
- isLoggedIn() {
2876
- const loggedIn = this.fidjService.isLoggedIn();
2877
- console.log('isLoggedIn: ', loggedIn);
2878
- return loggedIn;
2879
- }
2880
- needsConnectionRefresh() {
2881
- return this.fidjService.needsRefresh();
2882
- }
2883
- async connectionRefresh() {
2884
- await this.fidjService.refresh();
2885
- }
2886
- async storeAll() {
2887
- return this.fidjStorage.storeData(this.fidjService, this.nodeData);
2888
- }
2889
- isAdmin() {
2890
- return this.roles.indexOf('admin') > -1;
2771
+ return null;
2891
2772
  }
2892
- // === Notifications ===
2893
- async createNotification(rainId, message) {
2894
- const data = {
2895
- rain: rainId,
2896
- message,
2897
- };
2773
+ async getRainProgress(rainId) {
2898
2774
  try {
2899
- const resp = await this.fidjService.sendOnEndpoint({
2900
- key: 'notifications',
2901
- verb: 'POST',
2902
- defaultKeyUrl: this.defaultUrlForAPI + '/notifications',
2903
- data,
2775
+ const queryPath = '' + rainId + '/progress';
2776
+ const response = await this.fidjService.sendOnEndpoint({
2777
+ key: 'rains',
2778
+ verb: 'GET',
2779
+ relativePath: queryPath,
2780
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains',
2904
2781
  });
2905
- return new EventNode(resp.data);
2782
+ // return response.data.inProgress;
2783
+ return response.data.inQueue;
2906
2784
  }
2907
2785
  catch (e) {
2908
2786
  await this.checkError(e);
2909
2787
  }
2910
- return null;
2788
+ return 0;
2911
2789
  }
2912
- async getNotifications(rainId) {
2790
+ async getIndicators(rainId) {
2913
2791
  try {
2914
- const params = { rain: rainId };
2792
+ const params = {
2793
+ cumulative: 'true',
2794
+ };
2915
2795
  const queryString = BuildQueryString(params);
2916
- const resp = await this.fidjService.sendOnEndpoint({
2917
- key: 'notifications',
2796
+ const response = await this.fidjService.sendOnEndpoint({
2797
+ key: 'rains',
2918
2798
  verb: 'GET',
2919
- relativePath: queryString ? '?' + queryString : '',
2920
- defaultKeyUrl: this.defaultUrlForAPI + '/notifications',
2799
+ relativePath: rainId + '/indicators' + (queryString ? '?' + queryString : ''),
2800
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains',
2921
2801
  });
2922
- return resp.data.notifications.map((n) => new EventNode(n));
2802
+ return response.data;
2923
2803
  }
2924
2804
  catch (e) {
2925
2805
  await this.checkError(e);
2926
2806
  }
2927
- return null;
2807
+ return { indicators: [] };
2928
2808
  }
2929
- async getAllNotifications() {
2809
+ // GET /rains/:rainId/cumulatives - List available cumulative periods
2810
+ async getCumulativePeriods(rainId, filters) {
2930
2811
  try {
2931
- const resp = await this.fidjService.sendOnEndpoint({
2932
- key: 'notifications',
2812
+ const params = {};
2813
+ if (filters?.windowInMinutes !== undefined) {
2814
+ params.windowInMinutes = filters.windowInMinutes;
2815
+ }
2816
+ if (filters?.provider) {
2817
+ params.provider = filters.provider;
2818
+ }
2819
+ if (filters?.isReady !== undefined) {
2820
+ params.isReady = filters.isReady;
2821
+ }
2822
+ if (filters?.forced !== undefined) {
2823
+ params.forced = filters.forced;
2824
+ }
2825
+ const queryString = BuildQueryString(params);
2826
+ const response = await this.fidjService.sendOnEndpoint({
2827
+ key: 'rains',
2933
2828
  verb: 'GET',
2934
- defaultKeyUrl: this.defaultUrlForAPI + '/notifications',
2829
+ relativePath: rainId + '/cumulatives' + (queryString ? '?' + queryString : ''),
2830
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains',
2935
2831
  });
2936
- return resp.data.notifications.map((n) => new EventNode(n));
2832
+ return response.data;
2937
2833
  }
2938
2834
  catch (e) {
2939
2835
  await this.checkError(e);
2940
2836
  }
2941
- return [];
2837
+ return { periods: [], total: 0 };
2942
2838
  }
2943
- // === Teams ===
2944
- async getTeams() {
2945
- const teams = [];
2946
- const params = {};
2947
- const queryString = BuildQueryString(params);
2839
+ // POST /rains/:rainId/cumulatives - Trigger cumulative computation
2840
+ async createCumulativePeriod(rainId, params) {
2948
2841
  try {
2949
- const resp = await this.fidjService.sendOnEndpoint({
2950
- key: 'teams',
2951
- verb: 'GET',
2952
- relativePath: queryString ? '?' + queryString : '',
2953
- defaultKeyUrl: this.defaultUrlForAPI + '/teams',
2842
+ const body = {
2843
+ periodBegin: params.periodBegin.toISOString(),
2844
+ periodEnd: params.periodEnd.toISOString(),
2845
+ provider: params.provider,
2846
+ confName: params.confName || 'admin',
2847
+ timeStepInMinutes: params.timeStepInMinutes || 5,
2848
+ };
2849
+ const response = await this.fidjService.sendOnEndpoint({
2850
+ key: 'rains',
2851
+ verb: 'POST',
2852
+ relativePath: rainId + '/cumulatives',
2853
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains',
2854
+ data: body,
2954
2855
  });
2955
- for (const team of resp.data.teams) {
2956
- teams.push(new TeamNode(team));
2957
- }
2856
+ return response.data;
2958
2857
  }
2959
2858
  catch (e) {
2960
2859
  await this.checkError(e);
2961
2860
  }
2962
- return teams;
2861
+ return null;
2963
2862
  }
2964
- async getTeam(teamId) {
2863
+ // === Gauges ===
2864
+ async getGauge(gaugeId) {
2965
2865
  try {
2966
2866
  const resp = await this.fidjService.sendOnEndpoint({
2967
- key: 'teams',
2867
+ key: 'gauges',
2968
2868
  verb: 'GET',
2969
- relativePath: teamId,
2970
- defaultKeyUrl: this.defaultUrlForAPI + '/teams',
2869
+ relativePath: gaugeId,
2870
+ defaultKeyUrl: this.defaultUrlForAPI + '/gauges',
2971
2871
  });
2972
- return new TeamNode(resp.data);
2872
+ return new GaugeNode(resp.data);
2973
2873
  }
2974
2874
  catch (e) {
2975
2875
  await this.checkError(e);
2976
2876
  }
2977
- return null;
2978
2877
  }
2979
- // === Radars ===
2980
- async getRadars(name) {
2981
- const radars = [];
2982
- const params = {};
2983
- if (name) {
2984
- params.name = name;
2878
+ async getGauges(rainId, aroundLatLng, pageCount = 1) {
2879
+ const baseParams = {
2880
+ aroundLatLng: `${aroundLatLng.lat},${aroundLatLng.lng}`,
2881
+ rainId,
2882
+ };
2883
+ if (this.asTeamId) {
2884
+ baseParams.teamId = this.asTeamId;
2985
2885
  }
2986
- const queryString = BuildQueryString(params);
2886
+ const gauges = [];
2987
2887
  try {
2988
- const resp = await this.fidjService.sendOnEndpoint({
2989
- key: 'radars',
2990
- verb: 'GET',
2991
- relativePath: queryString ? '?' + queryString : '',
2992
- defaultKeyUrl: this.defaultUrlForAPI + '/radars',
2993
- });
2994
- for (const r of resp.data.radars) {
2995
- const radar = new RadarNode(r);
2996
- radars.push(radar);
2888
+ for (let count = 1; count <= pageCount; count++) {
2889
+ const params = { ...baseParams, page: count };
2890
+ const queryString = BuildQueryString(params);
2891
+ const resp = await this.fidjService.sendOnEndpoint({
2892
+ key: 'gauges',
2893
+ verb: 'GET',
2894
+ relativePath: queryString ? '?' + queryString : '',
2895
+ defaultKeyUrl: this.defaultUrlForAPI + '/gauges',
2896
+ });
2897
+ for (const gauge of resp.data.gauges) {
2898
+ gauges.push(new GaugeNodeFilter(gauge));
2899
+ }
2997
2900
  }
2998
2901
  }
2999
2902
  catch (e) {
3000
2903
  await this.checkError(e);
3001
2904
  }
3002
- return radars;
2905
+ return gauges;
3003
2906
  }
3004
- async getRadar(id) {
2907
+ async getGaugeMeasures(gaugeId, begin, end) {
2908
+ const params = {
2909
+ begin: begin.toISOString(),
2910
+ end: end.toISOString(),
2911
+ };
2912
+ const queryString = BuildQueryString(params);
2913
+ const resp = await this.fidjService.sendOnEndpoint({
2914
+ key: 'gauges',
2915
+ verb: 'GET',
2916
+ relativePath: gaugeId + '/measures' + (queryString ? '?' + queryString : ''),
2917
+ defaultKeyUrl: this.defaultUrlForAPI + '/gauges',
2918
+ });
2919
+ const gaugeMeasures = [];
2920
+ for (const gaugeMeasure of resp.data.gaugeMeasures) {
2921
+ gaugeMeasures.push(new GaugeMeasure(gaugeMeasure));
2922
+ }
2923
+ return gaugeMeasures;
2924
+ }
2925
+ async getProviders(rainId) {
3005
2926
  try {
3006
- const resp = await this.fidjService.sendOnEndpoint({
3007
- key: 'radars',
2927
+ const response = await this.fidjService.sendOnEndpoint({
3008
2928
  verb: 'GET',
3009
- relativePath: id,
3010
- defaultKeyUrl: this.defaultUrlForAPI + '/radars',
2929
+ key: 'rains',
2930
+ relativePath: rainId + '/providers',
2931
+ defaultKeyUrl: this.defaultUrlForAPI + '/rains',
3011
2932
  });
3012
- return new RadarNode(resp.data);
2933
+ return {
2934
+ providers: response.data.providers || [],
2935
+ timeStepInMinutes: response.data.timeStepInMinutes || [5, 10, 15, 30, 60],
2936
+ };
3013
2937
  }
3014
2938
  catch (e) {
3015
- await this.checkError(e);
2939
+ console.error('getProviders error:', e);
2940
+ return {
2941
+ providers: [],
2942
+ timeStepInMinutes: [5, 10, 15, 30, 60],
2943
+ };
3016
2944
  }
3017
- return null;
3018
2945
  }
3019
- async putRadar(radarNode) {
3020
- const data = {
3021
- name: radarNode.name,
3022
- };
2946
+ setRoles(roles) {
2947
+ this.roles = roles;
2948
+ }
2949
+ setDateComponents(date, c) {
2950
+ const dateToShow = new Date(date);
2951
+ if (c.year !== undefined) {
2952
+ dateToShow.setUTCFullYear(c.year);
2953
+ }
2954
+ if (c.month !== undefined) {
2955
+ dateToShow.setUTCMonth(c.month - 1);
2956
+ }
2957
+ if (c.day !== undefined) {
2958
+ dateToShow.setUTCDate(c.day);
2959
+ }
2960
+ if (c.hour !== undefined) {
2961
+ dateToShow.setUTCHours(c.hour);
2962
+ }
2963
+ if (c.minute !== undefined) {
2964
+ dateToShow.setUTCMinutes(c.minute);
2965
+ }
2966
+ return dateToShow.toISOString();
2967
+ }
2968
+ }
2969
+ ProfileService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ProfileService, deps: [{ token: Storage }, { token: i2$1.FidjService }, { token: i3$1.HttpClient }, { token: i4$1.Router }], target: i0.ɵɵFactoryTarget.Injectable });
2970
+ ProfileService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ProfileService, providedIn: 'root' });
2971
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ProfileService, decorators: [{
2972
+ type: Injectable,
2973
+ args: [{
2974
+ providedIn: 'root',
2975
+ }]
2976
+ }], ctorParameters: function () { return [{ type: Storage }, { type: i2$1.FidjService }, { type: i3$1.HttpClient }, { type: i4$1.Router }]; } });
2977
+
2978
+ class CumulativeSelectorComponent {
2979
+ constructor(profileService, cdr) {
2980
+ this.profileService = profileService;
2981
+ this.cdr = cdr;
2982
+ this.timeStepInMinutes = 5;
2983
+ this.isAdmin = false;
2984
+ this.periodSelected = new EventEmitter();
2985
+ this.cancelled = new EventEmitter();
2986
+ this.availablePeriods = [];
2987
+ this.baseCumulatives = null;
2988
+ this.loading = true;
2989
+ this.creating = false;
2990
+ this.creationProgress = '';
2991
+ this.errorMessage = '';
2992
+ this.coveragePercent = 0;
2993
+ this.canCreateNew = false;
2994
+ this.currentWindowMinutes = 0;
2995
+ this.POLL_TIMEOUT_MS = 900000; // 900 seconds
2996
+ this.POLL_INTERVAL_MS = 3000;
2997
+ }
2998
+ async ngOnInit() {
2999
+ this.currentWindowMinutes = Math.round((this.currentPeriodEnd.getTime() - this.currentPeriodBegin.getTime()) / 60000);
3000
+ await this.loadAvailablePeriods();
3001
+ }
3002
+ async loadAvailablePeriods() {
3003
+ this.loading = true;
3004
+ this.errorMessage = '';
3005
+ this.cdr.markForCheck();
3023
3006
  try {
3024
- const resp = await this.fidjService.sendOnEndpoint({
3025
- key: 'radars',
3026
- relativePath: radarNode.id,
3027
- verb: 'PUT',
3028
- data,
3029
- defaultKeyUrl: this.defaultUrlForAPI + '/radars/',
3007
+ // Fetch all cumulative periods (admin sees all, non-admin sees only customer-launched)
3008
+ const response = await this.profileService.getCumulativePeriods(this.rainId, {
3009
+ provider: this.provider,
3010
+ forced: this.isAdmin,
3030
3011
  });
3031
- return new RadarNode(resp.data);
3012
+ // Filter for existing custom cumulatives (window > 0)
3013
+ this.availablePeriods = response.periods.filter((p) => p.windowInMinutes > 0);
3014
+ // Get base cumulatives (window = 0) to check coverage
3015
+ this.baseCumulatives = response.periods.find((p) => p.windowInMinutes === 0);
3016
+ this.calculateCoverage();
3032
3017
  }
3033
3018
  catch (e) {
3034
- await this.checkError(e);
3019
+ this.errorMessage = 'Failed to load cumulative periods';
3020
+ console.error('Error loading cumulative periods:', e);
3035
3021
  }
3022
+ this.loading = false;
3023
+ this.cdr.markForCheck();
3036
3024
  }
3037
- async getLonelyRadars(rains) {
3025
+ calculateCoverage() {
3026
+ if (!this.baseCumulatives) {
3027
+ this.coveragePercent = 0;
3028
+ this.canCreateNew = false;
3029
+ return;
3030
+ }
3031
+ const baseBegin = new Date(this.baseCumulatives.periodBegin);
3032
+ const baseEnd = new Date(this.baseCumulatives.periodEnd);
3033
+ // Check if current period is within base coverage
3034
+ const currentInRange = this.currentPeriodBegin >= baseBegin && this.currentPeriodEnd <= baseEnd;
3035
+ if (!currentInRange) {
3036
+ this.coveragePercent = 0;
3037
+ this.canCreateNew = false;
3038
+ return;
3039
+ }
3040
+ // Calculate expected number of base cumulatives needed
3041
+ const expectedCount = Math.ceil(this.currentWindowMinutes / this.timeStepInMinutes);
3042
+ // Check if enough base cumulatives exist
3043
+ // We assume coverage is complete if count >= expected (simplified check)
3044
+ if (this.baseCumulatives.count >= expectedCount) {
3045
+ this.coveragePercent = 100;
3046
+ this.canCreateNew = true;
3047
+ }
3048
+ else {
3049
+ this.coveragePercent = Math.round((this.baseCumulatives.count / expectedCount) * 100);
3050
+ this.canCreateNew = this.coveragePercent >= 100;
3051
+ }
3052
+ }
3053
+ selectPeriod(period) {
3054
+ this.periodSelected.emit({
3055
+ periodBegin: new Date(period.periodBegin),
3056
+ periodEnd: new Date(period.periodEnd),
3057
+ windowInMinutes: period.windowInMinutes,
3058
+ });
3059
+ }
3060
+ async createNewPeriod() {
3061
+ if (!this.canCreateNew || this.creating) {
3062
+ return;
3063
+ }
3064
+ this.creating = true;
3065
+ this.creationProgress = 'Starting cumulative computation...';
3066
+ this.errorMessage = '';
3067
+ this.cdr.markForCheck();
3038
3068
  try {
3039
- const resp = await this.fidjService.sendOnEndpoint({
3040
- key: 'radars',
3041
- verb: 'GET',
3042
- defaultKeyUrl: this.defaultUrlForAPI + '/radars',
3069
+ // Trigger cumulative creation
3070
+ const result = await this.profileService.createCumulativePeriod(this.rainId, {
3071
+ periodBegin: this.currentPeriodBegin,
3072
+ periodEnd: this.currentPeriodEnd,
3073
+ provider: this.provider,
3074
+ timeStepInMinutes: this.timeStepInMinutes,
3043
3075
  });
3044
- const lonelyRadars = [];
3045
- const radars = resp.data.radars;
3046
- radars.forEach((radar) => {
3047
- let found = false;
3048
- rains.forEach((rain) => {
3049
- const rdId = rain.getLink(RadarNode.TYPE).getId();
3050
- if (rdId === radar.id) {
3051
- found = true;
3052
- }
3076
+ if (!result) {
3077
+ throw new Error('Failed to trigger cumulative computation');
3078
+ }
3079
+ this.creationProgress = `Jobs queued. Polling for completion...`;
3080
+ this.cdr.markForCheck();
3081
+ // Poll for completion
3082
+ const success = await this.pollForCompletion();
3083
+ if (success) {
3084
+ this.periodSelected.emit({
3085
+ periodBegin: this.currentPeriodBegin,
3086
+ periodEnd: this.currentPeriodEnd,
3087
+ windowInMinutes: this.currentWindowMinutes,
3053
3088
  });
3054
- if (!found) {
3055
- lonelyRadars.push(new RadarNode(radar));
3056
- }
3057
- });
3058
- return lonelyRadars;
3089
+ }
3090
+ else {
3091
+ this.errorMessage = 'Timeout waiting for cumulative computation';
3092
+ }
3059
3093
  }
3060
3094
  catch (e) {
3061
- await this.checkError(e);
3095
+ this.errorMessage = `Error: ${e.message || 'Unknown error'}`;
3096
+ console.error('Error creating cumulative:', e);
3062
3097
  }
3063
- return [];
3098
+ this.creating = false;
3099
+ this.cdr.markForCheck();
3064
3100
  }
3065
- async getRainTimeframe(rainId, begin, end, forced = false, provider = 'Raain', timeStepInMinutes = 10, windowInMinutes = 0) {
3066
- try {
3067
- const params = {
3068
- format: 'timeframeCumulative',
3069
- provider,
3070
- timeStepInMinutes: String(timeStepInMinutes),
3071
- begin: begin?.toISOString(),
3072
- end: end?.toISOString(),
3073
- };
3074
- if (forced) {
3075
- params.forced = 'true';
3101
+ async pollForCompletion() {
3102
+ const startTime = Date.now();
3103
+ while (Date.now() - startTime < this.POLL_TIMEOUT_MS) {
3104
+ try {
3105
+ const progress = await this.profileService.getRainProgress(this.rainId);
3106
+ if (progress === 0) {
3107
+ // Queue is empty, check if cumulative exists (admin sees all cumulatives)
3108
+ const response = await this.profileService.getCumulativePeriods(this.rainId, {
3109
+ provider: this.provider,
3110
+ windowInMinutes: this.currentWindowMinutes,
3111
+ forced: this.isAdmin,
3112
+ });
3113
+ const found = response.periods.find((p) => p.windowInMinutes === this.currentWindowMinutes);
3114
+ if (found) {
3115
+ return true;
3116
+ }
3117
+ }
3118
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
3119
+ this.creationProgress = `Computing... (${elapsed}s, queue: ${progress})`;
3120
+ this.cdr.markForCheck();
3121
+ await this.sleep(this.POLL_INTERVAL_MS);
3122
+ }
3123
+ catch (e) {
3124
+ console.warn('Poll error:', e);
3125
+ await this.sleep(this.POLL_INTERVAL_MS);
3076
3126
  }
3077
- params.windowInMinutes = String(windowInMinutes);
3078
- const queryString = BuildQueryString(params);
3079
- const resp = await this.fidjService.sendOnEndpoint({
3080
- key: 'rains',
3081
- verb: 'GET',
3082
- relativePath: rainId + (queryString ? '?' + queryString : ''),
3083
- defaultKeyUrl: this.defaultUrlForAPI + '/rains',
3084
- });
3085
- const rainNode = new RainNode(resp.data.timeframeCumulative);
3086
- rainNode.name += '.radar.timeframeCumulative';
3087
- return rainNode;
3088
3127
  }
3089
- catch (e) {
3090
- await this.checkError(e);
3128
+ return false;
3129
+ }
3130
+ sleep(ms) {
3131
+ return new Promise((resolve) => setTimeout(resolve, ms));
3132
+ }
3133
+ cancel() {
3134
+ this.cancelled.emit();
3135
+ }
3136
+ formatPeriod(period) {
3137
+ const begin = new Date(period.periodBegin);
3138
+ const end = new Date(period.periodEnd);
3139
+ return `${begin.toLocaleString()} → ${end.toLocaleString()}`;
3140
+ }
3141
+ formatWindow(minutes) {
3142
+ if (minutes < 60) {
3143
+ return `${minutes} min`;
3091
3144
  }
3092
- return null;
3145
+ const hours = minutes / 60;
3146
+ return hours === 1 ? '1 hour' : `${hours} hours`;
3147
+ }
3148
+ }
3149
+ CumulativeSelectorComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: CumulativeSelectorComponent, deps: [{ token: ProfileService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3150
+ CumulativeSelectorComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: CumulativeSelectorComponent, selector: "cumulative-selector", inputs: { rainId: "rainId", currentPeriodBegin: "currentPeriodBegin", currentPeriodEnd: "currentPeriodEnd", provider: "provider", timeStepInMinutes: "timeStepInMinutes", isAdmin: "isAdmin" }, outputs: { periodSelected: "periodSelected", cancelled: "cancelled" }, ngImport: i0, template: "<div class=\"cumulative-selector-overlay\">\n <div class=\"cumulative-selector-modal\">\n <div class=\"modal-header\">\n <h2>Select Cumulative Period</h2>\n <ion-button fill=\"clear\" (click)=\"cancel()\">\n <ion-icon name=\"close\"></ion-icon>\n </ion-button>\n </div>\n\n <div class=\"modal-content\">\n <!-- Loading state -->\n <div *ngIf=\"loading\" class=\"loading-state\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n <p>Loading available periods...</p>\n </div>\n\n <!-- Error message -->\n <div *ngIf=\"errorMessage\" class=\"error-message\">\n <ion-icon name=\"warning-outline\"></ion-icon>\n <span>{{ errorMessage }}</span>\n </div>\n\n <!-- Available periods list -->\n <div *ngIf=\"!loading && availablePeriods.length > 0\" class=\"periods-section\">\n <h3>Available Cumulative Periods</h3>\n <ion-list>\n <ion-item *ngFor=\"let period of availablePeriods\"\n button\n (click)=\"selectPeriod(period)\"\n [disabled]=\"creating\">\n <ion-icon name=\"layers-outline\" slot=\"start\"></ion-icon>\n <ion-label>\n <h2>{{ formatWindow(period.windowInMinutes) }}</h2>\n <p>{{ formatPeriod(period) }}</p>\n <p class=\"count-info\">{{ period.count }} cumulative(s)</p>\n </ion-label>\n <ion-icon name=\"chevron-forward\" slot=\"end\"></ion-icon>\n </ion-item>\n </ion-list>\n </div>\n\n <!-- No periods available -->\n <div *ngIf=\"!loading && availablePeriods.length === 0\" class=\"no-periods\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>No cumulative periods available yet.</p>\n </div>\n\n <!-- Create new section (admin only) -->\n <div *ngIf=\"!loading && isAdmin\" class=\"create-section\">\n <h3>Create New Cumulative</h3>\n <div class=\"create-info\">\n <p>\n <strong>Period:</strong>\n {{ currentPeriodBegin?.toLocaleString() }} \u2192 {{ currentPeriodEnd?.toLocaleString() }}\n </p>\n <p>\n <strong>Window:</strong> {{ formatWindow(currentWindowMinutes) }}\n </p>\n <p *ngIf=\"baseCumulatives\" class=\"coverage-info\"\n [class.coverage-ok]=\"coveragePercent >= 100\"\n [class.coverage-warn]=\"coveragePercent > 0 && coveragePercent < 100\"\n [class.coverage-error]=\"coveragePercent === 0\">\n <ion-icon [name]=\"coveragePercent >= 100 ? 'checkmark-circle' : 'alert-circle'\"></ion-icon>\n Base data coverage: {{ coveragePercent }}%\n </p>\n <p *ngIf=\"!baseCumulatives\" class=\"coverage-error\">\n <ion-icon name=\"alert-circle\"></ion-icon>\n No base cumulatives available\n </p>\n </div>\n\n <!-- Creation progress -->\n <div *ngIf=\"creating\" class=\"creation-progress\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n <span>{{ creationProgress }}</span>\n </div>\n\n <ion-button [disabled]=\"!canCreateNew || creating\"\n expand=\"block\"\n (click)=\"createNewPeriod()\">\n <ion-icon name=\"add-circle-outline\" slot=\"start\"></ion-icon>\n Create {{ formatWindow(currentWindowMinutes) }} Cumulative\n </ion-button>\n\n <p *ngIf=\"!canCreateNew && !creating\" class=\"create-hint\">\n Create is disabled because base 5-min cumulatives don't fully cover the selected period.\n </p>\n </div>\n </div>\n\n <div class=\"modal-footer\">\n <ion-button fill=\"outline\" (click)=\"cancel()\" [disabled]=\"creating\">\n Cancel\n </ion-button>\n </div>\n </div>\n</div>\n", styles: [".cumulative-selector-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:1000}.cumulative-selector-modal{background:var(--ion-background-color, #fff);border-radius:12px;max-width:500px;width:90%;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 4px 20px #0000004d}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--ion-border-color, #ddd)}.modal-header h2{margin:0;font-size:1.25rem;font-weight:600}.modal-header ion-button{--padding-start: 8px;--padding-end: 8px}.modal-content{flex:1;overflow-y:auto;padding:16px 20px}.loading-state{display:flex;flex-direction:column;align-items:center;padding:40px 0}.loading-state ion-spinner{margin-bottom:16px}.loading-state p{color:var(--ion-color-medium)}.error-message{display:flex;align-items:center;gap:8px;padding:12px;background:var(--ion-color-danger-tint);color:var(--ion-color-danger);border-radius:8px;margin-bottom:16px}.error-message ion-icon{font-size:1.25rem}.periods-section{margin-bottom:24px}.periods-section h3{font-size:1rem;font-weight:600;margin-bottom:12px;color:var(--ion-color-dark)}.periods-section ion-list{border-radius:8px;overflow:hidden}.periods-section ion-item{--padding-start: 12px;--padding-end: 12px}.periods-section ion-item ion-label h2{font-weight:500}.periods-section ion-item ion-label p{font-size:.85rem;color:var(--ion-color-medium)}.periods-section ion-item ion-label .count-info{font-size:.75rem;color:var(--ion-color-primary)}.no-periods{display:flex;flex-direction:column;align-items:center;padding:24px;text-align:center;color:var(--ion-color-medium)}.no-periods ion-icon{font-size:2rem;margin-bottom:8px}.create-section{border-top:1px solid var(--ion-border-color, #ddd);padding-top:16px}.create-section h3{font-size:1rem;font-weight:600;margin-bottom:12px;color:var(--ion-color-dark)}.create-section .create-info{background:var(--ion-color-light);padding:12px;border-radius:8px;margin-bottom:16px}.create-section .create-info p{margin:4px 0;font-size:.9rem}.create-section .coverage-info{display:flex;align-items:center;gap:6px}.create-section .coverage-info ion-icon{font-size:1.1rem}.create-section .coverage-ok{color:var(--ion-color-success)}.create-section .coverage-warn{color:var(--ion-color-warning)}.create-section .coverage-error{color:var(--ion-color-danger)}.create-section .creation-progress{display:flex;align-items:center;gap:12px;padding:16px;background:var(--ion-color-primary-tint);border-radius:8px;margin-bottom:16px}.create-section .creation-progress ion-spinner{--color: var(--ion-color-primary)}.create-section .creation-progress span{color:var(--ion-color-primary);font-size:.9rem}.create-section .create-hint{font-size:.8rem;color:var(--ion-color-medium);text-align:center;margin-top:8px}.modal-footer{padding:16px 20px;border-top:1px solid var(--ion-border-color, #ddd);display:flex;justify-content:flex-end}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i4.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i4.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i4.IonItem, selector: "ion-item", inputs: ["button", "color", "counter", "counterFormatter", "detail", "detailIcon", "disabled", "download", "fill", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "shape", "target", "type"] }, { kind: "component", type: i4.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i4.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i4.IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3151
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: CumulativeSelectorComponent, decorators: [{
3152
+ type: Component,
3153
+ args: [{ selector: 'cumulative-selector', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"cumulative-selector-overlay\">\n <div class=\"cumulative-selector-modal\">\n <div class=\"modal-header\">\n <h2>Select Cumulative Period</h2>\n <ion-button fill=\"clear\" (click)=\"cancel()\">\n <ion-icon name=\"close\"></ion-icon>\n </ion-button>\n </div>\n\n <div class=\"modal-content\">\n <!-- Loading state -->\n <div *ngIf=\"loading\" class=\"loading-state\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n <p>Loading available periods...</p>\n </div>\n\n <!-- Error message -->\n <div *ngIf=\"errorMessage\" class=\"error-message\">\n <ion-icon name=\"warning-outline\"></ion-icon>\n <span>{{ errorMessage }}</span>\n </div>\n\n <!-- Available periods list -->\n <div *ngIf=\"!loading && availablePeriods.length > 0\" class=\"periods-section\">\n <h3>Available Cumulative Periods</h3>\n <ion-list>\n <ion-item *ngFor=\"let period of availablePeriods\"\n button\n (click)=\"selectPeriod(period)\"\n [disabled]=\"creating\">\n <ion-icon name=\"layers-outline\" slot=\"start\"></ion-icon>\n <ion-label>\n <h2>{{ formatWindow(period.windowInMinutes) }}</h2>\n <p>{{ formatPeriod(period) }}</p>\n <p class=\"count-info\">{{ period.count }} cumulative(s)</p>\n </ion-label>\n <ion-icon name=\"chevron-forward\" slot=\"end\"></ion-icon>\n </ion-item>\n </ion-list>\n </div>\n\n <!-- No periods available -->\n <div *ngIf=\"!loading && availablePeriods.length === 0\" class=\"no-periods\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>No cumulative periods available yet.</p>\n </div>\n\n <!-- Create new section (admin only) -->\n <div *ngIf=\"!loading && isAdmin\" class=\"create-section\">\n <h3>Create New Cumulative</h3>\n <div class=\"create-info\">\n <p>\n <strong>Period:</strong>\n {{ currentPeriodBegin?.toLocaleString() }} \u2192 {{ currentPeriodEnd?.toLocaleString() }}\n </p>\n <p>\n <strong>Window:</strong> {{ formatWindow(currentWindowMinutes) }}\n </p>\n <p *ngIf=\"baseCumulatives\" class=\"coverage-info\"\n [class.coverage-ok]=\"coveragePercent >= 100\"\n [class.coverage-warn]=\"coveragePercent > 0 && coveragePercent < 100\"\n [class.coverage-error]=\"coveragePercent === 0\">\n <ion-icon [name]=\"coveragePercent >= 100 ? 'checkmark-circle' : 'alert-circle'\"></ion-icon>\n Base data coverage: {{ coveragePercent }}%\n </p>\n <p *ngIf=\"!baseCumulatives\" class=\"coverage-error\">\n <ion-icon name=\"alert-circle\"></ion-icon>\n No base cumulatives available\n </p>\n </div>\n\n <!-- Creation progress -->\n <div *ngIf=\"creating\" class=\"creation-progress\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n <span>{{ creationProgress }}</span>\n </div>\n\n <ion-button [disabled]=\"!canCreateNew || creating\"\n expand=\"block\"\n (click)=\"createNewPeriod()\">\n <ion-icon name=\"add-circle-outline\" slot=\"start\"></ion-icon>\n Create {{ formatWindow(currentWindowMinutes) }} Cumulative\n </ion-button>\n\n <p *ngIf=\"!canCreateNew && !creating\" class=\"create-hint\">\n Create is disabled because base 5-min cumulatives don't fully cover the selected period.\n </p>\n </div>\n </div>\n\n <div class=\"modal-footer\">\n <ion-button fill=\"outline\" (click)=\"cancel()\" [disabled]=\"creating\">\n Cancel\n </ion-button>\n </div>\n </div>\n</div>\n", styles: [".cumulative-selector-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:1000}.cumulative-selector-modal{background:var(--ion-background-color, #fff);border-radius:12px;max-width:500px;width:90%;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 4px 20px #0000004d}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--ion-border-color, #ddd)}.modal-header h2{margin:0;font-size:1.25rem;font-weight:600}.modal-header ion-button{--padding-start: 8px;--padding-end: 8px}.modal-content{flex:1;overflow-y:auto;padding:16px 20px}.loading-state{display:flex;flex-direction:column;align-items:center;padding:40px 0}.loading-state ion-spinner{margin-bottom:16px}.loading-state p{color:var(--ion-color-medium)}.error-message{display:flex;align-items:center;gap:8px;padding:12px;background:var(--ion-color-danger-tint);color:var(--ion-color-danger);border-radius:8px;margin-bottom:16px}.error-message ion-icon{font-size:1.25rem}.periods-section{margin-bottom:24px}.periods-section h3{font-size:1rem;font-weight:600;margin-bottom:12px;color:var(--ion-color-dark)}.periods-section ion-list{border-radius:8px;overflow:hidden}.periods-section ion-item{--padding-start: 12px;--padding-end: 12px}.periods-section ion-item ion-label h2{font-weight:500}.periods-section ion-item ion-label p{font-size:.85rem;color:var(--ion-color-medium)}.periods-section ion-item ion-label .count-info{font-size:.75rem;color:var(--ion-color-primary)}.no-periods{display:flex;flex-direction:column;align-items:center;padding:24px;text-align:center;color:var(--ion-color-medium)}.no-periods ion-icon{font-size:2rem;margin-bottom:8px}.create-section{border-top:1px solid var(--ion-border-color, #ddd);padding-top:16px}.create-section h3{font-size:1rem;font-weight:600;margin-bottom:12px;color:var(--ion-color-dark)}.create-section .create-info{background:var(--ion-color-light);padding:12px;border-radius:8px;margin-bottom:16px}.create-section .create-info p{margin:4px 0;font-size:.9rem}.create-section .coverage-info{display:flex;align-items:center;gap:6px}.create-section .coverage-info ion-icon{font-size:1.1rem}.create-section .coverage-ok{color:var(--ion-color-success)}.create-section .coverage-warn{color:var(--ion-color-warning)}.create-section .coverage-error{color:var(--ion-color-danger)}.create-section .creation-progress{display:flex;align-items:center;gap:12px;padding:16px;background:var(--ion-color-primary-tint);border-radius:8px;margin-bottom:16px}.create-section .creation-progress ion-spinner{--color: var(--ion-color-primary)}.create-section .creation-progress span{color:var(--ion-color-primary);font-size:.9rem}.create-section .create-hint{font-size:.8rem;color:var(--ion-color-medium);text-align:center;margin-top:8px}.modal-footer{padding:16px 20px;border-top:1px solid var(--ion-border-color, #ddd);display:flex;justify-content:flex-end}\n"] }]
3154
+ }], ctorParameters: function () { return [{ type: ProfileService }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { rainId: [{
3155
+ type: Input
3156
+ }], currentPeriodBegin: [{
3157
+ type: Input
3158
+ }], currentPeriodEnd: [{
3159
+ type: Input
3160
+ }], provider: [{
3161
+ type: Input
3162
+ }], timeStepInMinutes: [{
3163
+ type: Input
3164
+ }], isAdmin: [{
3165
+ type: Input
3166
+ }], periodSelected: [{
3167
+ type: Output
3168
+ }], cancelled: [{
3169
+ type: Output
3170
+ }] } });
3171
+
3172
+ let TEST_DETECTION = 0;
3173
+ class RaainDetailsComponent {
3174
+ constructor(storage, cdr) {
3175
+ this.storage = storage;
3176
+ this.cdr = cdr;
3177
+ this.availableProviders = [];
3178
+ this.availableTimeSteps = [];
3179
+ this.showPixelMarkers = false;
3180
+ this.qualityIndicators = [];
3181
+ this.qualityIndicatorsLoading = false;
3182
+ this.showCumulativeSelector = false;
3183
+ // Cached computed values (to avoid method calls in templates)
3184
+ this.percentOfComputations = 0;
3185
+ this.percentOfImages = 0;
3186
+ this.truncatedError = '';
3187
+ this.cumulativeDurationInMinutes = 10;
3188
+ this.DateRange = DateRange;
3189
+ // Wrapper function that preserves the async nature of fetchData
3190
+ this.fetchDataWrapper = async (focusDate, focusRange) => {
3191
+ return await this.fetchData(focusDate, focusRange);
3192
+ };
3093
3193
  }
3094
- // === Rains ===
3095
- async getRains(name) {
3096
- const rains = [];
3097
- const params = {};
3098
- if (name) {
3099
- params.name = name;
3100
- }
3101
- const queryString = BuildQueryString(params);
3102
- try {
3103
- const resp = await this.fidjService.sendOnEndpoint({
3104
- key: 'rains',
3105
- verb: 'GET',
3106
- relativePath: queryString ? '?' + queryString : '',
3107
- defaultKeyUrl: this.defaultUrlForAPI + '/rains',
3108
- });
3109
- for (const rain of resp.data.rains) {
3110
- rains.push(new RainNode(rain));
3111
- }
3112
- }
3113
- catch (e) {
3114
- await this.checkError(e);
3194
+ static PeriodDisplay(date) {
3195
+ let d = new Date();
3196
+ if (date) {
3197
+ d = new Date(date);
3198
+ const userTimezoneOffset = d.getTimezoneOffset() * 60000;
3199
+ d = new Date(d.getTime() - userTimezoneOffset);
3115
3200
  }
3116
- return rains;
3201
+ const exampleFormattedDate = '2017-06-01T08:30';
3202
+ return d.toISOString().substring(0, exampleFormattedDate.length);
3117
3203
  }
3118
- async getRain(id) {
3119
- try {
3120
- const resp = await this.fidjService.sendOnEndpoint({
3121
- key: 'rains',
3122
- relativePath: id,
3123
- verb: 'GET',
3124
- defaultKeyUrl: this.defaultUrlForAPI + '/rains/',
3125
- });
3126
- return new RainNode(resp.data);
3204
+ static DateUTC(date) {
3205
+ const hasISOFormat = date.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
3206
+ let d = new Date(date);
3207
+ if (!hasISOFormat) {
3208
+ const userTimezoneOffset = d.getTimezoneOffset() * 60000;
3209
+ d = new Date(d.getTime() - userTimezoneOffset);
3127
3210
  }
3128
- catch (e) {
3129
- await this.checkError(e);
3211
+ return d;
3212
+ }
3213
+ static MapCountToDateValue(c) {
3214
+ return {
3215
+ date: RaainDetailsComponent.DateUTC(c.name),
3216
+ value: Math.min(100, c.x),
3217
+ };
3218
+ }
3219
+ async openQualityModal() {
3220
+ this.showQualityModal = true;
3221
+ this.qualityIndicatorsLoading = true;
3222
+ this.qualityIndicators = [];
3223
+ this.cdr.markForCheck();
3224
+ if (this.rainNode?.id) {
3225
+ const result = await this.profileService.getIndicators(this.rainNode.id);
3226
+ this.qualityIndicators = result.indicators;
3130
3227
  }
3131
- return null;
3228
+ this.qualityIndicatorsLoading = false;
3229
+ this.cdr.markForCheck();
3132
3230
  }
3133
- // === Count
3134
- async getCounts(rainId, options) {
3135
- try {
3136
- const params = {
3137
- range: options.range,
3138
- begin: options.periodBegin.toISOString(),
3139
- };
3140
- const queryString = BuildQueryString(params);
3141
- const resp = await this.fidjService.sendOnEndpoint({
3142
- key: 'rains',
3143
- relativePath: rainId + '/counts' + (queryString ? '?' + queryString : ''),
3144
- verb: 'GET',
3145
- defaultKeyUrl: this.defaultUrlForAPI + '/rains/',
3146
- });
3147
- const counts = resp.data.counts.result;
3148
- const percentImages = [], percentRainy = [], percentQ = [];
3149
- counts.forEach((c) => {
3150
- const label = this.setDateComponents(options.periodBegin, c);
3151
- percentImages.push(new XYType(c.percentImages ?? 0, NaN, NaN, label));
3152
- percentRainy.push(new XYType(c.percentRainy ?? 0, NaN, NaN, label));
3153
- percentQ.push(new XYType(c.percentQ ?? 0, NaN, NaN, label));
3154
- });
3155
- return {
3156
- percentImages,
3157
- percentRainy,
3158
- percentQ,
3159
- queueRunning: resp.data.queueRunning,
3160
- };
3231
+ closeQualityModal() {
3232
+ this.showQualityModal = false;
3233
+ }
3234
+ formatDate(dateStr) {
3235
+ const date = new Date(dateStr);
3236
+ return (date.toLocaleDateString() +
3237
+ ' ' +
3238
+ date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
3239
+ }
3240
+ async fetchData(focusDate, focusRange) {
3241
+ const values = [];
3242
+ for (let i = 0; i < 10; i++) {
3243
+ values.push({ date: new Date(2020 + i, 0), value: 10 });
3161
3244
  }
3162
- catch (e) {
3163
- await this.checkError(e);
3245
+ const fakeData = [
3246
+ {
3247
+ label: '% Rainy',
3248
+ style: 'bar',
3249
+ values,
3250
+ },
3251
+ {
3252
+ label: '% Images',
3253
+ style: 'bar',
3254
+ values,
3255
+ },
3256
+ // {
3257
+ // label: '% Quality',
3258
+ // style: 'line',
3259
+ // values,
3260
+ // },
3261
+ ];
3262
+ const range = mapDateRangeToString(focusRange);
3263
+ let data = fakeData;
3264
+ if (!this.rainNode) {
3265
+ return data;
3164
3266
  }
3165
- return null;
3166
- }
3167
- async getCountsHour(rainId, options) {
3168
- try {
3169
- const params = {
3170
- range: 'hour',
3171
- begin: options.periodBegin.toISOString(),
3172
- };
3173
- const queryString = BuildQueryString(params);
3174
- const resp = await this.fidjService.sendOnEndpoint({
3175
- key: 'rains',
3176
- relativePath: rainId + '/counts' + (queryString ? '?' + queryString : ''),
3177
- verb: 'GET',
3178
- defaultKeyUrl: this.defaultUrlForAPI + '/rains/',
3267
+ if (focusRange === DateRange.CENTURY) {
3268
+ // fake
3269
+ }
3270
+ else if (focusRange === DateRange.HOUR) {
3271
+ const hourCounts = await this.profileService.getCountsHour(this.rainNode.id, {
3272
+ periodBegin: focusDate,
3179
3273
  });
3180
- const counts = resp.data.counts.result;
3181
- const percentImages = [], rainySum = [], rainyCount = [];
3182
- counts.forEach((c) => {
3183
- const label = this.setDateComponents(options.periodBegin, c);
3184
- percentImages.push(new XYType(c.percentImages ?? 0, NaN, NaN, label));
3185
- rainyCount.push(new XYType(c.rainyCount ?? 0, NaN, NaN, label));
3186
- rainySum.push(new XYType(c.rainySum ?? 0, NaN, NaN, label));
3274
+ data = [
3275
+ {
3276
+ label: 'Rainy Count',
3277
+ style: 'line',
3278
+ values: hourCounts.rainyCount.map(RaainDetailsComponent.MapCountToDateValue),
3279
+ },
3280
+ {
3281
+ label: '% Images',
3282
+ style: 'bar',
3283
+ values: hourCounts.percentImages.map(RaainDetailsComponent.MapCountToDateValue),
3284
+ },
3285
+ {
3286
+ label: 'Rainy Sum',
3287
+ style: 'line',
3288
+ values: hourCounts.rainySum.map(RaainDetailsComponent.MapCountToDateValue),
3289
+ },
3290
+ ];
3291
+ }
3292
+ else {
3293
+ const counts = await this.profileService.getCounts(this.rainNode.id, {
3294
+ range,
3295
+ periodBegin: focusDate,
3187
3296
  });
3188
- return {
3189
- percentImages,
3190
- rainyCount,
3191
- rainySum,
3192
- queueRunning: resp.data.queueRunning,
3193
- };
3297
+ data = [
3298
+ {
3299
+ label: '% Rainy',
3300
+ style: 'bar',
3301
+ values: counts.percentRainy.map(RaainDetailsComponent.MapCountToDateValue),
3302
+ },
3303
+ {
3304
+ label: '% Images',
3305
+ style: 'bar',
3306
+ values: counts.percentImages.map(RaainDetailsComponent.MapCountToDateValue),
3307
+ },
3308
+ ];
3194
3309
  }
3195
- catch (e) {
3196
- await this.checkError(e);
3310
+ // console.log(`fetchData DONE ${range}`, data);
3311
+ return data;
3312
+ }
3313
+ ngOnChanges(changes) {
3314
+ this.change(changes).then((ignored) => { });
3315
+ }
3316
+ ngOnDestroy() {
3317
+ this.cleanAll();
3318
+ }
3319
+ async onEnableCountHistoryTab(rain) {
3320
+ if (!this.toggleHistory) {
3321
+ this.countPoints = [];
3197
3322
  }
3198
- return null;
3199
3323
  }
3200
- // === Computing ===
3201
- async getRainComputationCumulativeCartesianMapById(rainId, rainComputationCumulativeId) {
3202
- const params = { format: 'cartesian-map' };
3203
- const queryString = BuildQueryString(params);
3204
- try {
3205
- const response = await this.fidjService.sendOnEndpoint({
3206
- key: 'rains',
3207
- verb: 'GET',
3208
- relativePath: `${rainId}/cumulatives/${rainComputationCumulativeId}?${queryString}`,
3209
- defaultKeyUrl: this.defaultUrlForAPI + '/rains',
3210
- });
3211
- if (!response.data['cartesian-map']) {
3212
- return null;
3213
- }
3214
- const rainComputationMap = new RainComputationMap(response.data['cartesian-map']);
3215
- rainComputationMap.name = rainId + '.rain.cartesian-map';
3216
- return rainComputationMap;
3324
+ async onPeriodBeginChange(event) {
3325
+ const newValue = event?.target?.value ?? this.periodBeginAsString;
3326
+ this.periodBegin = new Date(newValue);
3327
+ this.periodBeginAsString = RaainDetailsComponent.PeriodDisplay(this.periodBegin);
3328
+ this.storage.set('raain-periodBegin', this.periodBegin);
3329
+ await this.onPeriodDurationChange(event);
3330
+ }
3331
+ async onPeriodEndChange(event) {
3332
+ const newValue = event?.target?.value ?? this.periodEndAsString;
3333
+ this.periodEnd = new Date(newValue);
3334
+ this.periodEndAsString = RaainDetailsComponent.PeriodDisplay(this.periodEnd);
3335
+ this.storage.set('raain-periodEnd', this.periodEnd);
3336
+ this.periodBegin = new Date(this.periodEnd.getTime() - this.getDurationInHours() * RaainDetailsComponent.HOUR_MS);
3337
+ this.periodBeginAsString = RaainDetailsComponent.PeriodDisplay(this.periodBegin);
3338
+ this.storage.set('raain-periodBegin', this.periodBegin);
3339
+ this.updateRefreshManagerPeriod();
3340
+ }
3341
+ async onPeriodDurationChange(_event) {
3342
+ const durationInHours = this.getDurationInHours();
3343
+ this.periodEnd = new Date(this.periodBegin.getTime() + durationInHours * RaainDetailsComponent.HOUR_MS);
3344
+ this.periodEndAsString = RaainDetailsComponent.PeriodDisplay(this.periodEnd);
3345
+ this.storage.set('raain-periodEnd', this.periodEnd);
3346
+ this.storage.set('raain-periodDurationInHours', durationInHours);
3347
+ this.updateRefreshManagerPeriod();
3348
+ this.updateCumulativeDurationInMinutes();
3349
+ }
3350
+ async onDateChangeInCount(dateChanged) {
3351
+ const dateString = dateChanged.toISOString().substring(0, 11) +
3352
+ dateChanged.toLocaleTimeString().substring(0, 5);
3353
+ this.periodDurationAsString = '1';
3354
+ if (this.toggleCumulative) {
3355
+ // Cumulative: select periodEnd
3356
+ this.periodEndAsString = dateString;
3357
+ await this.onPeriodEndChange(null);
3217
3358
  }
3218
- catch (e) {
3219
- await this.checkError(e);
3359
+ else {
3360
+ // Granular: select periodBegin
3361
+ this.periodBeginAsString = dateString;
3362
+ await this.onPeriodBeginChange(null);
3220
3363
  }
3221
- return null;
3364
+ await this.refreshManager.refresh(false, this.toggleAdmin);
3222
3365
  }
3223
- async getRainComputationCumulativeCumulativesMapById(rainId, rainComputationCumulativeId, cumulativeHours) {
3224
- const queryPath = `${rainId}/cumulatives/${rainComputationCumulativeId}/cumulative/${cumulativeHours}`;
3225
- try {
3226
- const response = await this.fidjService.sendOnEndpoint({
3227
- key: 'rains',
3228
- verb: 'GET',
3229
- relativePath: queryPath,
3230
- defaultKeyUrl: this.defaultUrlForAPI + '/rains',
3231
- });
3232
- if (!response.data['cumulative']) {
3233
- return null;
3234
- }
3235
- const rainComputationMap = new RainComputationMap(response.data['cumulative']);
3236
- rainComputationMap.name = rainId + '.rain.cumulative-cumulative';
3237
- return rainComputationMap;
3238
- }
3239
- catch (e) {
3240
- await this.checkError(e);
3366
+ async onDateChangeInMap(dateShown) {
3367
+ this.dateShown = dateShown;
3368
+ await this.fetchAndUpdateMap();
3369
+ await this.refreshManager.setReportPeriod(this.dateShown);
3370
+ }
3371
+ async onSumChangeInMap(sum) {
3372
+ this.sumDetails = sum;
3373
+ }
3374
+ async onGaugeSelectInMap(mapLatLng) {
3375
+ const gaugesFiltered = this.compareManager.gaugesInMap.filter((g) => g.lat === mapLatLng.lat && g.lng === mapLatLng.lng);
3376
+ if (gaugesFiltered.length !== 1) {
3377
+ return;
3241
3378
  }
3242
- return null;
3379
+ const gaugeSelected = gaugesFiltered[0];
3380
+ await this.refreshGaugeValues({ id: gaugeSelected.id, name: gaugeSelected.name });
3381
+ await this.compareManager.selectGauge(gaugeSelected.id, 0);
3243
3382
  }
3244
- async getRainCumulativeCompareByDate(rainNode, rainComputationCumulativeId, date) {
3245
- const params = { date: date.toISOString() };
3246
- const queryString = BuildQueryString(params);
3247
- try {
3248
- const response = await this.fidjService.sendOnEndpoint({
3249
- key: 'rains',
3250
- verb: 'GET',
3251
- relativePath: `${rainNode.id}/cumulatives/${rainComputationCumulativeId}/compares?${queryString}`,
3252
- defaultKeyUrl: this.defaultUrlForAPI + '/rains',
3253
- });
3254
- const qualityJson = response.data.qualities[0];
3255
- const rainComputationQuality = new RainComputationQuality(qualityJson);
3256
- rainComputationQuality.qualitySpeedMatrixContainer =
3257
- SpeedMatrixContainer.CreateFromJson(rainComputationQuality.qualitySpeedMatrixContainer);
3258
- return rainComputationQuality;
3383
+ async refreshGaugeValues(gaugeSelected) {
3384
+ const gaugeValueShowBegin = new Date(this.periodBegin.getTime() - RaainDetailsComponent.DAY_MS);
3385
+ const gaugeValueShowEnd = new Date(this.periodEnd.getTime() + RaainDetailsComponent.DAY_MS);
3386
+ gaugeValueShowBegin.setHours(0, 0);
3387
+ gaugeValueShowEnd.setHours(23, 59);
3388
+ const gaugeMeasures = await this.profileService.getGaugeMeasures(gaugeSelected.id, gaugeValueShowBegin, gaugeValueShowEnd);
3389
+ const gaugeValues = gaugeMeasures.map((gm) => {
3390
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3391
+ const cartesianMeasureValue = new CartesianMeasureValue(gm.values[0]);
3392
+ return {
3393
+ date: gm.date,
3394
+ value: cartesianMeasureValue.getCartesianValues()[0].value,
3395
+ };
3396
+ });
3397
+ this.gaugeSelectedPoints = [
3398
+ {
3399
+ label: gaugeSelected.name,
3400
+ style: 'bar',
3401
+ values: gaugeValues,
3402
+ },
3403
+ ];
3404
+ this.cdr.markForCheck();
3405
+ }
3406
+ async onGaugeSelectInCompare(e) {
3407
+ await this.refreshGaugeValues({ id: e.point.id, name: e.point.name });
3408
+ await this.compareManager.selectGauge(e.point.id, e.compareIndex);
3409
+ }
3410
+ async onToggleMap($event) {
3411
+ // if (this.toggleMap) {
3412
+ // await this.refreshMap();
3413
+ // }
3414
+ }
3415
+ onTogglePixelMarkers() {
3416
+ // Toggle is bound to showPixelMarkers, raain-map handles marker display
3417
+ }
3418
+ async onCumulativeToggleClick($event) {
3419
+ console.log('[CumulativeToggle] click event', {
3420
+ currentValue: this.toggleCumulative,
3421
+ eventType: $event.type,
3422
+ target: $event.target?.tagName,
3423
+ });
3424
+ $event.preventDefault();
3425
+ $event.stopPropagation();
3426
+ if (!this.toggleCumulative) {
3427
+ console.log('[CumulativeToggle] showing popup, toggle stays OFF');
3428
+ // Currently OFF, user wants to enable - show selector popup
3429
+ this.showCumulativeSelector = true;
3430
+ // Force reset the toggle visual state after ion-toggle's internal handler
3431
+ setTimeout(() => {
3432
+ if (this.cumulativeToggleRef) {
3433
+ this.cumulativeToggleRef.checked = false;
3434
+ console.log('[CumulativeToggle] force reset toggle to OFF');
3435
+ }
3436
+ }, 0);
3437
+ this.cdr.markForCheck();
3259
3438
  }
3260
- catch (e) {
3261
- await this.checkError(e);
3439
+ else {
3440
+ console.log('[CumulativeToggle] turning OFF');
3441
+ // Currently ON, user wants to disable - direct toggle off
3442
+ this.toggleCumulative = false;
3443
+ this.storage.set('raain-toggleCumulative', false);
3444
+ this.dateShown = this.getDateBasedOnCumulativeMode(this.timeframeDates);
3445
+ this.updateCumulativeDurationInMinutes();
3446
+ if (this.toggleMap) {
3447
+ this.updateRefreshManagerPeriod();
3448
+ await this.refreshManager.refresh(false, this.toggleAdmin);
3449
+ }
3450
+ this.cdr.markForCheck();
3262
3451
  }
3263
- return null;
3452
+ console.log('[CumulativeToggle] after handler, toggleCumulative =', this.toggleCumulative);
3264
3453
  }
3265
- async getRainCumulativeCumulativesCompareByDate(rainNode, rainComputationCumulativeId, date, cumulativeHours) {
3266
- const params = {
3267
- date: date.toISOString(),
3268
- cumulativeHours,
3269
- };
3270
- const queryString = BuildQueryString(params);
3271
- try {
3272
- const response = await this.fidjService.sendOnEndpoint({
3273
- key: 'rains',
3274
- verb: 'GET',
3275
- relativePath: `${rainNode.id}/cumulatives/${rainComputationCumulativeId}/compares?${queryString}`,
3276
- defaultKeyUrl: this.defaultUrlForAPI + '/rains',
3277
- });
3278
- const qualityJson = response.data.qualities[0];
3279
- const rainComputationQuality = new RainComputationQuality(qualityJson);
3280
- rainComputationQuality.qualitySpeedMatrixContainer =
3281
- SpeedMatrixContainer.CreateFromJson(rainComputationQuality.qualitySpeedMatrixContainer);
3282
- return rainComputationQuality;
3283
- }
3284
- catch (e) {
3285
- await this.checkError(e);
3454
+ async onCumulativePeriodSelected(selection) {
3455
+ this.showCumulativeSelector = false;
3456
+ // Update period to match selection
3457
+ this.periodBegin = selection.periodBegin;
3458
+ this.periodEnd = selection.periodEnd;
3459
+ this.periodBeginAsString = RaainDetailsComponent.PeriodDisplay(this.periodBegin);
3460
+ this.periodEndAsString = RaainDetailsComponent.PeriodDisplay(this.periodEnd);
3461
+ // Calculate duration in hours
3462
+ const durationMs = this.periodEnd.getTime() - this.periodBegin.getTime();
3463
+ const durationHours = durationMs / RaainDetailsComponent.HOUR_MS;
3464
+ this.periodDurationAsString = String(durationHours);
3465
+ // Store cumulative period
3466
+ this.storage.set('raain-periodBegin', this.periodBegin);
3467
+ this.storage.set('raain-periodEnd', this.periodEnd);
3468
+ this.storage.set('raain-periodDurationInHours', durationHours);
3469
+ // Enable cumulative mode
3470
+ this.toggleCumulative = true;
3471
+ this.storage.set('raain-toggleCumulative', true);
3472
+ this.dateShown = this.getDateBasedOnCumulativeMode(this.timeframeDates);
3473
+ this.updateCumulativeDurationInMinutes();
3474
+ // Refresh map
3475
+ if (this.toggleMap) {
3476
+ this.updateRefreshManagerPeriod();
3477
+ await this.refreshManager.refresh(false, this.toggleAdmin);
3286
3478
  }
3287
- return null;
3479
+ this.cdr.markForCheck();
3288
3480
  }
3289
- async getRainProgress(rainId) {
3290
- try {
3291
- const queryPath = '' + rainId + '/progress';
3292
- const response = await this.fidjService.sendOnEndpoint({
3293
- key: 'rains',
3294
- verb: 'GET',
3295
- relativePath: queryPath,
3296
- defaultKeyUrl: this.defaultUrlForAPI + '/rains',
3297
- });
3298
- // return response.data.inProgress;
3299
- return response.data.inQueue;
3481
+ onCumulativeSelectorCancelled() {
3482
+ console.log('[CumulativeToggle] cancelled, toggleCumulative =', this.toggleCumulative);
3483
+ this.showCumulativeSelector = false;
3484
+ this.cdr.markForCheck();
3485
+ }
3486
+ updateCumulativeDurationInMinutes() {
3487
+ if (this.toggleCumulative) {
3488
+ // Cumulative mode: use period duration
3489
+ this.cumulativeDurationInMinutes = parseFloat(this.periodDurationAsString) * 60;
3300
3490
  }
3301
- catch (e) {
3302
- await this.checkError(e);
3491
+ else {
3492
+ // Granular mode: use selectedTimeStep
3493
+ this.cumulativeDurationInMinutes = this.selectedTimeStep || 10;
3303
3494
  }
3304
- return 0;
3305
3495
  }
3306
- async getIndicators(rainId) {
3496
+ async onProviderChanged($event) {
3497
+ this.selectedProvider = $event?.detail?.value;
3498
+ this.storage.set('raain-selectedProvider', this.selectedProvider);
3499
+ await this.applyRefreshManagerSettings();
3500
+ }
3501
+ async onTimeStepChanged($event) {
3502
+ this.selectedTimeStep = $event?.detail?.value;
3503
+ this.storage.set('raain-selectedTimeStep', this.selectedTimeStep);
3504
+ this.updateCumulativeDurationInMinutes();
3505
+ await this.applyRefreshManagerSettings();
3506
+ }
3507
+ async loadProviders() {
3508
+ if (!this.rainNode?.id) {
3509
+ return;
3510
+ }
3307
3511
  try {
3308
- const params = {
3309
- cumulative: 'true',
3310
- };
3311
- const queryString = BuildQueryString(params);
3312
- const response = await this.fidjService.sendOnEndpoint({
3313
- key: 'rains',
3314
- verb: 'GET',
3315
- relativePath: rainId + '/indicators' + (queryString ? '?' + queryString : ''),
3316
- defaultKeyUrl: this.defaultUrlForAPI + '/rains',
3317
- });
3318
- return response.data;
3512
+ const result = await this.profileService.getProviders(this.rainNode.id);
3513
+ this.availableProviders = result.providers;
3514
+ this.availableTimeSteps = result.timeStepInMinutes;
3515
+ // Load saved selections or use defaults
3516
+ this.selectedProvider =
3517
+ this.storage.get('raain-selectedProvider') ||
3518
+ (this.availableProviders.length > 0 ? this.availableProviders[0] : 'Raain');
3519
+ this.selectedTimeStep =
3520
+ this.storage.get('raain-selectedTimeStep') ||
3521
+ (this.availableTimeSteps.length > 0 ? this.availableTimeSteps[0] : 10);
3319
3522
  }
3320
3523
  catch (e) {
3321
- await this.checkError(e);
3524
+ // Set fallback values
3525
+ this.availableProviders = ['Raain'];
3526
+ this.availableTimeSteps = [5, 10, 15, 30, 60];
3527
+ this.selectedProvider = 'Raain';
3528
+ this.selectedTimeStep = 10;
3322
3529
  }
3323
- return { indicators: [] };
3530
+ // Set provider and timeStep on refreshManager
3531
+ if (this.refreshManager) {
3532
+ this.refreshManager.provider = this.selectedProvider;
3533
+ this.refreshManager.timeStepInMinutes = this.selectedTimeStep;
3534
+ }
3535
+ this.cdr.markForCheck();
3324
3536
  }
3325
- // === Gauges ===
3326
- async getGauge(gaugeId) {
3327
- try {
3328
- const resp = await this.fidjService.sendOnEndpoint({
3329
- key: 'gauges',
3330
- verb: 'GET',
3331
- relativePath: gaugeId,
3332
- defaultKeyUrl: this.defaultUrlForAPI + '/gauges',
3333
- });
3334
- return new GaugeNode(resp.data);
3537
+ onChangeDetectionTest(rainNode) {
3538
+ console.log(TEST_DETECTION++, 'onChangeDetectionTest');
3539
+ return '';
3540
+ }
3541
+ updateTruncatedError() {
3542
+ const error = this.refreshManager?.lastError || '';
3543
+ if (error.length <= 80) {
3544
+ this.truncatedError = error;
3545
+ return;
3335
3546
  }
3336
- catch (e) {
3337
- await this.checkError(e);
3547
+ this.truncatedError = error.substring(0, 80) + '...';
3548
+ }
3549
+ updateCachedValues() {
3550
+ this.updatePercentOfImages();
3551
+ this.updatePercentOfComputations();
3552
+ this.updateTruncatedError();
3553
+ this.updateCumulativeDurationInMinutes();
3554
+ }
3555
+ async refreshMap() {
3556
+ this.gaugeSelectedPoints = [];
3557
+ this.dateShown = this.getDateBasedOnCumulativeMode();
3558
+ this.borders = [];
3559
+ this.compareManager.cleanAll();
3560
+ await this.compareManager.setGaugesInMap();
3561
+ await this.refreshManager.refresh(false, this.toggleAdmin);
3562
+ this.cdr.markForCheck();
3563
+ }
3564
+ async setPeriodOfNow() {
3565
+ const last30mn = new Date();
3566
+ last30mn.setMinutes(last30mn.getMinutes() - 30);
3567
+ this.periodBeginAsString =
3568
+ last30mn.toISOString().substring(0, 11) + last30mn.toLocaleTimeString().substring(0, 5);
3569
+ this.periodDurationAsString = '1';
3570
+ await this.onPeriodBeginChange(null);
3571
+ await this.refreshManager.refresh(false, this.toggleAdmin);
3572
+ }
3573
+ updatePercentOfImages() {
3574
+ if (!this.countsPeriod?.percentImages?.length) {
3575
+ this.percentOfImages = 0;
3576
+ return;
3338
3577
  }
3578
+ const duringPeriod = this.countsPeriod.percentImages.filter((a) => this.periodBegin.getTime() <= new Date(a.name).getTime() &&
3579
+ new Date(a.name).getTime() <= this.periodEnd.getTime());
3580
+ const summed = duringPeriod.reduce((a, b) => a + (b.x ?? 0), 0);
3581
+ this.percentOfImages = Math.round(summed / duringPeriod.length);
3339
3582
  }
3340
- async getGauges(rainId, aroundLatLng, pageCount = 1) {
3341
- const baseParams = {
3342
- aroundLatLng: `${aroundLatLng.lat},${aroundLatLng.lng}`,
3343
- rainId,
3344
- };
3345
- if (this.asTeamId) {
3346
- baseParams.teamId = this.asTeamId;
3583
+ updatePercentOfComputations() {
3584
+ const timeline = this.refreshManager?.getTimelineFrameSet();
3585
+ if (!timeline?.length) {
3586
+ this.percentOfComputations = 0;
3587
+ return;
3347
3588
  }
3348
- const gauges = [];
3349
- try {
3350
- for (let count = 1; count <= pageCount; count++) {
3351
- const params = { ...baseParams, page: count };
3352
- const queryString = BuildQueryString(params);
3353
- const resp = await this.fidjService.sendOnEndpoint({
3354
- key: 'gauges',
3355
- verb: 'GET',
3356
- relativePath: queryString ? '?' + queryString : '',
3357
- defaultKeyUrl: this.defaultUrlForAPI + '/gauges',
3358
- });
3359
- for (const gauge of resp.data.gauges) {
3360
- gauges.push(new GaugeNodeFilter(gauge));
3361
- }
3589
+ const timelineWithComputation = timeline.filter((a) => !!a.rainComputationCumulativeId || !!a.rainComputationId);
3590
+ const ratio = timelineWithComputation.length / timeline.length;
3591
+ this.percentOfComputations = Math.round(ratio * 100);
3592
+ }
3593
+ getDateBasedOnCumulativeMode(fallbackDates) {
3594
+ if (fallbackDates?.length > 0) {
3595
+ const dateExists = fallbackDates.some((d) => d.getTime() === this.dateShown?.getTime());
3596
+ if (!dateExists) {
3597
+ return this.toggleCumulative
3598
+ ? fallbackDates[fallbackDates.length - 1]
3599
+ : fallbackDates[0];
3362
3600
  }
3363
3601
  }
3364
- catch (e) {
3365
- await this.checkError(e);
3366
- }
3367
- return gauges;
3602
+ return this.toggleCumulative ? this.periodEnd : this.periodBegin;
3368
3603
  }
3369
- async getGaugeMeasures(gaugeId, begin, end) {
3370
- const params = {
3371
- begin: begin.toISOString(),
3372
- end: end.toISOString(),
3604
+ getCumulativeHours() {
3605
+ return this.toggleCumulative ? parseFloat(this.periodDurationAsString) : 0;
3606
+ }
3607
+ getDurationInHours() {
3608
+ return parseFloat(this.periodDurationAsString);
3609
+ }
3610
+ updateRefreshManagerPeriod() {
3611
+ this.refreshManager.cumulative = this.toggleCumulative;
3612
+ // Align dates to 5-minute boundaries (floor) for consistency with raain-ground
3613
+ const alignTo5minFloor = (date) => {
3614
+ const minutes = date.getMinutes();
3615
+ const alignedMinutes = Math.floor(minutes / 5) * 5;
3616
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), alignedMinutes, 0, 0);
3373
3617
  };
3374
- const queryString = BuildQueryString(params);
3375
- const resp = await this.fidjService.sendOnEndpoint({
3376
- key: 'gauges',
3377
- verb: 'GET',
3378
- relativePath: gaugeId + '/measures' + (queryString ? '?' + queryString : ''),
3379
- defaultKeyUrl: this.defaultUrlForAPI + '/gauges',
3380
- });
3381
- const gaugeMeasures = [];
3382
- for (const gaugeMeasure of resp.data.gaugeMeasures) {
3383
- gaugeMeasures.push(new GaugeMeasure(gaugeMeasure));
3618
+ this.refreshManager.period = {
3619
+ begin: alignTo5minFloor(this.periodBegin),
3620
+ end: alignTo5minFloor(this.periodEnd),
3621
+ };
3622
+ }
3623
+ async fetchAndUpdateMap() {
3624
+ await this.refreshManager.fetch(this.dateShown, this.toggleGaugeMeasures, this.getCumulativeHours());
3625
+ this.currentTimeframeTarget = this.refreshManager.getTimelineSelectedFrameSet();
3626
+ this.cdr.markForCheck();
3627
+ }
3628
+ async applyRefreshManagerSettings() {
3629
+ if (!this.refreshManager) {
3630
+ return;
3384
3631
  }
3385
- return gaugeMeasures;
3632
+ this.refreshManager.provider = this.selectedProvider;
3633
+ this.refreshManager.timeStepInMinutes = this.selectedTimeStep;
3634
+ await this.refreshManager.refresh(false, this.toggleAdmin);
3386
3635
  }
3387
- async getProviders(rainId) {
3388
- try {
3389
- const response = await this.fidjService.sendOnEndpoint({
3390
- verb: 'GET',
3391
- key: 'rains',
3392
- relativePath: rainId + '/providers',
3393
- defaultKeyUrl: this.defaultUrlForAPI + '/rains',
3394
- });
3395
- return {
3396
- providers: response.data.providers || [],
3397
- timeStepInMinutes: response.data.timeStepInMinutes || [5, 10, 15, 30, 60],
3398
- };
3636
+ cleanAll() {
3637
+ this.borders = [];
3638
+ this.isAdmin = false;
3639
+ this.timeframeContainers = new TimeframeContainers([]);
3640
+ this.currentTimeframeTarget = null;
3641
+ this.timeframeDates = [];
3642
+ this.countPoints = [];
3643
+ this.countsPeriod = { progress: 0, queueRunning: 0, percentImages: [] };
3644
+ this.gaugeSelectedPoints = [];
3645
+ this.toggleHistory = false;
3646
+ this.toggleMap = true;
3647
+ this.toggleCompare = false;
3648
+ this.toggleGaugeMeasures = false;
3649
+ this.toggleRemoveCompareDuplicate = true;
3650
+ this.toggleCumulative = this.storage.get('raain-toggleCumulative');
3651
+ this.periodBegin = new Date(this.storage.get('raain-periodBegin'));
3652
+ this.periodEnd = new Date(this.storage.get('raain-periodEnd'));
3653
+ this.periodBeginAsString = RaainDetailsComponent.PeriodDisplay(this.periodBegin);
3654
+ this.periodEndAsString = RaainDetailsComponent.PeriodDisplay(this.periodEnd);
3655
+ const durationMs = this.periodEnd.getTime() - this.periodBegin.getTime();
3656
+ this.periodDurationAsString = '' + durationMs / RaainDetailsComponent.HOUR_MS;
3657
+ this.dateShown = this.getDateBasedOnCumulativeMode();
3658
+ this.refreshInProgress = false;
3659
+ this.showFullError = false;
3660
+ this.showQualityModal = false;
3661
+ this.qualityIndicators = [];
3662
+ this.qualityIndicatorsLoading = false;
3663
+ this.compareManager?.cleanAll();
3664
+ this.refreshManager?.cleanAll();
3665
+ }
3666
+ async init() {
3667
+ this.cleanAll();
3668
+ this.updateCachedValues();
3669
+ await this.initRain();
3670
+ this.cdr.markForCheck();
3671
+ }
3672
+ async initRain() {
3673
+ if (!this.rainNode) {
3674
+ return;
3399
3675
  }
3400
- catch (e) {
3401
- console.error('getProviders error:', e);
3402
- return {
3403
- providers: [],
3404
- timeStepInMinutes: [5, 10, 15, 30, 60],
3405
- };
3676
+ this.isAdmin = this.profileService.isAdmin();
3677
+ this.refreshManager.rainNode = this.rainNode;
3678
+ this.compareManager.rainNode = this.rainNode;
3679
+ // Load providers and set on refreshManager
3680
+ await this.loadProviders();
3681
+ this.refreshManager.setMethods(this.onRefreshInProgress.bind(this), this.onRefreshDone.bind(this), this.onFetchDone.bind(this));
3682
+ const center = this.rainNode.getCenter();
3683
+ this.coordinates = new MapLatLng(center.lat, center.lng);
3684
+ this.teamNode = await this.profileService.getTeam(this.rainNode.getLink(TeamNode.TYPE).getId());
3685
+ // Load all gauges linked to the rainNode on map
3686
+ await this.compareManager.setGaugesInMap();
3687
+ if (this.periodBegin && this.periodEnd) {
3688
+ this.updateRefreshManagerPeriod();
3689
+ await this.refreshManager.refresh(false, this.toggleAdmin);
3406
3690
  }
3407
3691
  }
3408
- setRoles(roles) {
3409
- this.roles = roles;
3692
+ async onRefreshInProgress(countPeriods, timeframeDates) {
3693
+ this.refreshInProgress = true;
3694
+ this.countsPeriod = countPeriods;
3695
+ this.timeframeDates = timeframeDates;
3696
+ this.updateCachedValues();
3697
+ this.cdr.markForCheck();
3410
3698
  }
3411
- setDateComponents(date, c) {
3412
- const dateToShow = new Date(date);
3413
- if (c.year !== undefined) {
3414
- dateToShow.setUTCFullYear(c.year);
3699
+ async onRefreshDone(timeframeDates) {
3700
+ this.timeframeDates = timeframeDates;
3701
+ this.refreshInProgress = false;
3702
+ this.updateCachedValues();
3703
+ this.cdr.markForCheck();
3704
+ if (this.toggleMap && timeframeDates.length > 0) {
3705
+ this.dateShown = this.getDateBasedOnCumulativeMode(timeframeDates);
3706
+ if (this.dateShown) {
3707
+ await this.fetchAndUpdateMap();
3708
+ }
3415
3709
  }
3416
- if (c.month !== undefined) {
3417
- dateToShow.setUTCMonth(c.month - 1);
3710
+ }
3711
+ async onFetchDone(timeframeContainers) {
3712
+ if (timeframeContainers) {
3713
+ this.timeframeContainers = timeframeContainers;
3418
3714
  }
3419
- if (c.day !== undefined) {
3420
- dateToShow.setUTCDate(c.day);
3715
+ this.cdr.markForCheck();
3716
+ }
3717
+ async change(_changes) {
3718
+ await this.init();
3719
+ }
3720
+ }
3721
+ RaainDetailsComponent.HOUR_MS = 60 * 60000;
3722
+ RaainDetailsComponent.DAY_MS = 24 * 60 * 60 * 1000;
3723
+ RaainDetailsComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: RaainDetailsComponent, deps: [{ token: Storage }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3724
+ RaainDetailsComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: RaainDetailsComponent, selector: "raain-details", inputs: { toggleAdmin: "toggleAdmin", rainNode: "rainNode", compareManager: "compareManager", refreshManager: "refreshManager", profileService: "profileService", radarService: "radarService" }, viewQueries: [{ propertyName: "cumulativeToggleRef", first: true, predicate: ["cumulativeToggle"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Main content container -->\n<div *ngIf=\"rainNode\" class=\"raain-details-container\">\n\n <!-- Period selection section -->\n <ion-card class=\"period-card\">\n <ion-card-content>\n <div class=\"period-controls\">\n <div class=\"period-row\">\n\n <ion-button (click)=\"toggleHistory = !toggleHistory; onEnableCountHistoryTab(rainNode)\"\n fill=\"outline\">\n <ion-icon name=\"calendar-clear-outline\" slot=\"start\"></ion-icon>\n <ion-icon [name]=\"toggleHistory ? 'chevron-down' : 'chevron-forward'\" slot=\"end\"></ion-icon>\n </ion-button>\n\n <div class=\"period-start ion-hide-md-down\">\n <input (change)=\"onPeriodBeginChange($event)\"\n [disabled]=\"toggleCumulative\"\n [value]=\"periodBeginAsString\"\n class=\"datetime-input\"\n type=\"datetime-local\">\n </div>\n\n <div class=\"period-duration ion-hide-md-down\">\n <ion-select (ionDismiss)=\"onPeriodDurationChange($event)\"\n [(ngModel)]=\"periodDurationAsString\"\n [disabled]=\"toggleCumulative\"\n class=\"duration-select\"\n id=\"periodDuration\"\n interface=\"popover\">\n <ion-select-option value=\"0.25\">15 minutes</ion-select-option>\n <ion-select-option value=\"0.5\">30 minutes</ion-select-option>\n <ion-select-option value=\"1\">1 hour</ion-select-option>\n <ion-select-option value=\"2\">2 hours</ion-select-option>\n <ion-select-option value=\"3\">3 hours</ion-select-option>\n <ion-select-option value=\"4\">4 hours</ion-select-option>\n <ion-select-option value=\"5\">5 hours</ion-select-option>\n <ion-select-option value=\"6\">6 hours</ion-select-option>\n <ion-select-option value=\"8\">8 hours</ion-select-option>\n <ion-select-option value=\"10\">10 hours</ion-select-option>\n <ion-select-option value=\"12\">12 hours</ion-select-option>\n <ion-select-option *ngIf=\"isAdmin\" value=\"24\">24 hours</ion-select-option>\n </ion-select>\n </div>\n\n <div class=\"period-start ion-hide-md-down\">\n <input (change)=\"onPeriodEndChange($event)\"\n [disabled]=\"toggleCumulative\"\n [value]=\"periodEndAsString\"\n class=\"datetime-input\"\n type=\"datetime-local\">\n </div>\n\n <div class=\"toggle-cumulative\" (click)=\"onCumulativeToggleClick($event)\">\n <ion-label [class.text-primary]=\"toggleCumulative\">\n {{ toggleCumulative ? 'Cumulative' : 'Granular' }}\n </ion-label>\n <ion-toggle #cumulativeToggle [checked]=\"toggleCumulative\">\n </ion-toggle>\n </div>\n </div>\n\n <!-- Hidden label for change detection (uncomment to debug)\n <div class=\"hidden-label\">{{ onChangeDetectionTest(rainNode) }}</div>\n -->\n </div>\n\n <!-- Historical map section -->\n <div *ngIf=\"toggleHistory\" class=\"period-controls\">\n <raain-date-dynamic (changedDate)=\"onDateChangeInCount($event)\"\n [currentHeight]=\"300\"\n [fetchData]=\"fetchDataWrapper\"\n [points]=\"countPoints\">\n </raain-date-dynamic>\n </div>\n </ion-card-content>\n </ion-card>\n\n <!-- Map performance -->\n <ion-grid class=\"map-performance\">\n <ion-row id=\"progressAndRefresh\">\n <ion-col class=\"provider-selection\" size=\"12\" size-md=\"6\">\n <ion-button (click)=\"openQualityModal()\" class=\"quality-info-button\" fill=\"clear\">\n {{ refreshManager.rainComputationMapVersion }}\n <ion-icon name=\"help-circle-outline\" slot=\"end\"></ion-icon>\n </ion-button>\n <div *ngIf=\"availableProviders.length > 0 || availableTimeSteps.length > 0\" class=\"selection-row\">\n <ion-select (ionChange)=\"onProviderChanged($event)\"\n [value]=\"selectedProvider\"\n interface=\"popover\"\n label=\"Provider\"\n placeholder=\"Select Provider\">\n <ion-select-option *ngFor=\"let provider of availableProviders\" [value]=\"provider\">\n {{ provider }}\n </ion-select-option>\n </ion-select>\n\n <ion-select (ionChange)=\"onTimeStepChanged($event)\"\n [value]=\"selectedTimeStep\"\n interface=\"popover\"\n label=\"Time Step\"\n placeholder=\"Select Time Step\">\n <ion-select-option *ngFor=\"let step of availableTimeSteps\" [value]=\"step\">\n {{ step }} min\n </ion-select-option>\n </ion-select>\n </div>\n </ion-col>\n <ion-col class=\"ion-text-right\" size=\"12\" size-md=\"6\">\n <ion-label class=\"ion-margin-end\">\n <span *ngIf=\"percentOfComputations\">\n {{ percentOfComputations }}% Images\n <i *ngIf=\"countsPeriod.progress\" class=\"progress-indicator\">\n In Progress: {{ countsPeriod.progress }}...\n </i>\n </span>\n <span *ngIf=\"!percentOfComputations\">\n No image available\n </span>\n\n </ion-label>\n\n <ion-button (click)=\"refreshMap()\" [disabled]=\"refreshInProgress\" class=\"refresh-button\">\n Refresh Map\n </ion-button>\n </ion-col>\n </ion-row>\n\n <!-- status update row -->\n <ion-row>\n <!-- Progress col -->\n <ion-progress-bar\n [buffer]=\"(countsPeriod.progress / ((timeframeDates?.length || 10) +3))+0.01\"\n [style.visibility]=\"refreshInProgress ? 'visible' : 'hidden'\"\n [value]=\"countsPeriod.progress / ((timeframeDates?.length || 10) +3)\"\n color=\"primary\">\n </ion-progress-bar>\n\n <!-- Error col -->\n <ion-col (click)=\"showFullError = !showFullError\" *ngIf=\"refreshManager.lastError\" class=\"error-row\"\n size=\"12\">\n <div class=\"error-content\">\n <ion-icon class=\"error-icon\" name=\"warning-outline\"></ion-icon>\n <span [class.expanded]=\"showFullError\" class=\"error-text\">\n {{ showFullError ? refreshManager.lastError : truncatedError }}\n </span>\n <ion-icon [name]=\"showFullError ? 'chevron-up' : 'chevron-down'\" class=\"error-caret\"></ion-icon>\n </div>\n </ion-col>\n </ion-row>\n </ion-grid>\n\n <!-- Map section -->\n <ion-card class=\"map-card\">\n <ion-card-content class=\"map-content\">\n <ion-grid>\n <ion-row *ngIf=\"toggleMap && percentOfImages\">\n <!-- Map component -->\n <ion-col class=\"map-column\" size-lg=\"7\" size-md=\"12\">\n <div class=\"map-container\">\n <raain-map #raainMapRef\n (changedDate)=\"onDateChangeInMap($event)\"\n (changedSum)=\"onSumChangeInMap($event)\"\n (selectedMarker)=\"onGaugeSelectInMap($event)\"\n [coordinates]=\"coordinates\"\n [cumulativeDurationInMinutes]=\"cumulativeDurationInMinutes\"\n [currentHeight]=\"500\"\n [defaultDate]=\"dateShown\"\n [markers]=\"{\n borders,\n gauges: compareManager.gaugesInMap,\n gaugesInCompare: compareManager.gaugesInCompare,\n selectedGauges: compareManager.selectedGauges,\n pixels: compareManager.selectedPixels,\n pixelsSolution: compareManager.pixelsSolutions?.length ? compareManager.pixelsSolutions[0] : [],\n speeds: compareManager.speeds\n }\"\n [showCumulative]=\"toggleCumulative\"\n [showVisiblePixelMarkers]=\"showPixelMarkers\"\n [sumFn]=\"refreshManager.sumFn\"\n [sumValues]=\"refreshManager.sumValues\"\n [timeframeContainers]=\"timeframeContainers\"\n [timeframeDates]=\"timeframeDates\">\n </raain-map>\n </div>\n\n <div class=\"data-column\">\n <!-- Technical details (collapsible for cleaner UI) -->\n <details class=\"technical-details\">\n <summary>Image Details</summary>\n <div class=\"details-content\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Computed:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapDoneDate | date:'yyyy-MM-dd HH:mm' }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Version:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapVersion }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Launched by:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapLaunchedBy }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Time spent:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapTimeSpentInMs }}\n ms</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Date:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapDate?.toISOString() }}\n | {{ refreshManager.rainComputationMapDate | date:'yyyy-MM-dd HH:mm' }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Water in the map:</span>\n <span class=\"detail-value\">{{ sumDetails }}</span>\n <ion-toggle\n (ionChange)=\"onTogglePixelMarkers()\"\n [(ngModel)]=\"showPixelMarkers\"\n style=\"margin-left: 8px; transform: scale(0.7);\">\n </ion-toggle>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Source DBZ min:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapOriginalMin }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Source DBZ max:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapOriginalMax }}</span>\n </div>\n </div>\n </details>\n </div>\n </ion-col>\n\n <!-- Data panel -->\n <ion-col *ngIf=\"!!compareManager.compareVersion\" class=\"data-column\" size-lg=\"5\" size-md=\"12\">\n <div class=\"data-panel\">\n <!-- Compare stack component -->\n <div class=\"compare-stack\">\n <raain-compare-stack\n (selectedPoint)=\"onGaugeSelectInCompare($event)\"\n [compareManager]=\"compareManager\"\n [cumulative]=\"toggleCumulative\">\n </raain-compare-stack>\n </div>\n\n <!-- Technical details (collapsible for cleaner UI) -->\n <details class=\"technical-details\">\n <summary>Compare Details</summary>\n <div class=\"details-content\">\n <div [ngClass]=\"{'warning': refreshManager.rainComputationMapDoneDate?.getTime() > compareManager.currentQualityDoneDate?.getTime() + 60000}\"\n class=\"detail-row\">\n <span class=\"detail-label\">Computed:</span>\n <span class=\"detail-value\">{{ compareManager.currentQualityDoneDate | date:'yyyy-MM-dd HH:mm' }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Version:</span>\n <span class=\"detail-value\">{{ compareManager.compareVersion }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Launched by:</span>\n <span class=\"detail-value\">{{ compareManager.currentQualityLaunchedBy }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Time spent:</span>\n <span class=\"detail-value\">{{ compareManager.currentQualityTimeSpentInMs }}\n ms</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Gauges:</span>\n <span class=\"detail-value\">{{ compareManager.gaugesInCompare.length }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Points:</span>\n <span class=\"detail-value\">{{ compareManager.globalComparePoints.length }}</span>\n </div>\n </div>\n </details>\n </div>\n </ion-col>\n </ion-row>\n <ion-row>\n <!-- Bottom progress bar -->\n <ion-progress-bar *ngIf=\"refreshInProgress\"\n [buffer]=\"(countsPeriod.progress / ((timeframeDates?.length || 10) +3))+0.01\"\n [value]=\"countsPeriod.progress / ((timeframeDates?.length || 10) +3)\"\n color=\"primary\">\n </ion-progress-bar>\n </ion-row>\n </ion-grid>\n </ion-card-content>\n </ion-card>\n\n <!-- Gauge values section -->\n <ion-card *ngIf=\"gaugeSelectedPoints.length && gaugeSelectedPoints[0].values.length\" class=\"gauge-card\">\n <ion-card-header>\n <ion-card-title>\n <ion-icon name=\"analytics-outline\"></ion-icon>\n Selected Gauge Data\n </ion-card-title>\n </ion-card-header>\n <ion-card-content>\n <raain-date-focus\n [currentHeight]=\"300\"\n [focusDate]=\"periodBegin\"\n [focusRange]=\"DateRange.DAY\"\n [points]=\"gaugeSelectedPoints\"\n [withoutAll]=\"true\">\n </raain-date-focus>\n </ion-card-content>\n </ion-card>\n\n <!-- Quality Performance Modal -->\n <div (click)=\"closeQualityModal()\" *ngIf=\"showQualityModal\" class=\"quality-modal-overlay\">\n <div (click)=\"$event.stopPropagation()\" class=\"quality-modal-content\">\n <div class=\"quality-modal-header\">\n <h2>Model Quality Performance</h2>\n <ion-button (click)=\"closeQualityModal()\" fill=\"clear\">\n <ion-icon name=\"close-outline\"></ion-icon>\n </ion-button>\n </div>\n <div class=\"quality-modal-body\">\n <div *ngIf=\"qualityIndicatorsLoading\" class=\"quality-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n <span>Loading indicators...</span>\n </div>\n <div *ngIf=\"!qualityIndicatorsLoading && qualityIndicators.length === 0\" class=\"quality-empty\">\n No quality indicators available for this year.\n </div>\n <table *ngIf=\"!qualityIndicatorsLoading && qualityIndicators.length > 0\" class=\"quality-table\">\n <thead>\n <tr>\n <th>Model</th>\n <th>Compare</th>\n <th>Gauges</th>\n <th>Period</th>\n <th>Avg Quality</th>\n <th>Updated</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let indicator of qualityIndicators\">\n <td>{{ indicator.computingVersion }}</td>\n <td>{{ indicator.qualityVersion }}</td>\n <td>{{ indicator.provider }}<br>{{ indicator.timeStepInMinutes }} min</td>\n <td>{{ formatDate(indicator.startDate) }}<br>{{ formatDate(indicator.endDate) }}</td>\n <td>{{ indicator.averageQuality | number:'1.2-2' }}</td>\n <td>{{ formatDate(indicator.lastUpdatedAt) }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n\n <!-- Cumulative Selector Modal -->\n <cumulative-selector *ngIf=\"showCumulativeSelector\"\n [rainId]=\"rainNode?.id\"\n [currentPeriodBegin]=\"periodBegin\"\n [currentPeriodEnd]=\"periodEnd\"\n [provider]=\"selectedProvider\"\n [timeStepInMinutes]=\"selectedTimeStep\"\n [isAdmin]=\"toggleAdmin\"\n (periodSelected)=\"onCumulativePeriodSelected($event)\"\n (cancelled)=\"onCumulativeSelectorCancelled()\">\n </cumulative-selector>\n\n</div>\n", styles: [".raain-details-container{max-width:var(--app-max-width);margin:0 auto;padding:0 0 24px}.raain-details-card{width:100%;margin-bottom:20px}.raain-details-card ion-card-header{border-bottom:1px solid rgba(var(--ion-color-medium-rgb),.2)}.raain-details-card ion-card-header ion-card-title{display:flex;align-items:center}.raain-details-card ion-card-header ion-card-title ion-icon{margin-right:8px;color:var(--ion-color-primary)}.node-info-card{background-color:var(--ion-color-light)}.node-info-card .node-header{display:flex;align-items:center}.node-info-card .node-header .node-status{margin-right:16px}.node-info-card .node-header .node-status ion-icon{font-size:24px}.node-info-card .node-header .node-titles{flex:1}.node-info-card .node-header .node-titles ion-card-title{margin:0;font-size:1.4rem;font-weight:600;color:var(--ion-color-dark)}.node-info-card .node-header .node-titles ion-card-subtitle{padding-left:0;margin:4px 0 0;font-size:.9rem;color:var(--ion-color-medium)}.count-map-card,.period-card{background-color:var(--ion-color-light)}.period-card ion-card-title{display:flex;align-items:center}.period-card ion-card-title ion-icon{margin-right:8px;color:var(--ion-color-primary);font-size:20px}.period-row{display:flex;flex-wrap:wrap;align-items:center;gap:16px;position:relative}.now-button{min-width:100px}#all-dates{display:flex;align-items:center;margin-left:auto}#all-dates .toggle-label{margin-right:8px;white-space:nowrap}.refresh-button ion-icon{margin-right:4px;color:var(--ion-color-light)}.provider-selection{display:flex;align-items:center}.quality-info-button{--padding-start: 8px;--padding-end: 8px;font-size:.85rem;color:var(--ion-color-medium)}.quality-info-button ion-icon{font-size:18px;margin-left:4px}.selection-row{display:flex;flex-direction:row;align-items:center;gap:12px}.selection-row ion-select{flex:0 0 auto;min-width:100px;margin-bottom:0;padding:4px 8px;border:1px solid rgba(var(--ion-color-medium-rgb),.3);border-radius:var(--ion-border-radius);background-color:var(--ion-color-light)}.period-start,.period-duration{display:flex;align-items:center}.toggle-cumulative{display:flex;align-items:center;gap:8px;margin-left:auto}.toggle-cumulative .text-primary{color:var(--ion-color-primary);font-weight:600}.hidden-label{display:none}.datetime-input,#periodDuration,.duration-select{padding:8px 12px;border:1px solid rgba(var(--ion-color-medium-rgb),.3);border-radius:var(--ion-border-radius);background-color:var(--ion-color-light);font-family:var(--ion-font-family)}.datetime-input:focus,#periodDuration:focus,.duration-select:focus{outline:none;border-color:var(--ion-color-primary)}#periodDuration,.duration-select{min-width:150px}.gauge-card{background-color:var(--ion-color-light)}.gauge-card ion-card-title{display:flex;align-items:center}.gauge-card ion-card-title ion-icon{margin-right:8px;color:var(--ion-color-primary);font-size:20px}.error-row{cursor:pointer;background-color:rgba(var(--ion-color-danger-rgb),.1);border-left:3px solid var(--ion-color-danger);margin-top:8px;border-radius:4px;transition:background-color .2s ease}.error-row:hover{background-color:rgba(var(--ion-color-danger-rgb),.15)}.error-row .error-content{display:flex;align-items:flex-start;padding:8px 12px;gap:8px}.error-row .error-icon{color:var(--ion-color-danger);font-size:18px;flex-shrink:0;margin-top:2px}.error-row .error-text{flex:1;color:var(--ion-color-danger-shade);font-size:.9rem;word-break:break-word}.error-row .error-text.expanded{white-space:pre-wrap}.error-row .error-caret{color:var(--ion-color-danger);font-size:16px;flex-shrink:0;margin-top:2px}raain-compare-stack{width:100%;display:block}@media (max-width: 768px){.period-row{flex-direction:row;justify-content:space-between;align-items:center}#all-dates{margin-left:auto;padding-left:8px}#all-dates .toggle-label{font-size:.9rem}.map-header{flex-direction:row;justify-content:space-between;align-items:center;gap:16px}}.quality-modal-overlay{position:fixed;inset:0;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:9999}.quality-modal-content{background-color:var(--ion-background-color, #fff);border-radius:12px;width:95%;max-width:900px;max-height:80vh;overflow:auto;box-shadow:0 4px 24px #0003}.quality-modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(var(--ion-color-medium-rgb),.2)}.quality-modal-header h2{margin:0;font-size:1.25rem;font-weight:600;color:var(--ion-color-dark)}.quality-modal-header ion-button{--padding-start: 8px;--padding-end: 8px}.quality-modal-body{padding:20px}.quality-table{width:100%;border-collapse:collapse;margin-top:16px;table-layout:fixed}.quality-table th,.quality-table td{width:16.66%;padding:12px 16px;text-align:center;vertical-align:top;border-bottom:1px solid rgba(var(--ion-color-medium-rgb),.2)}.quality-table th{background-color:rgba(var(--ion-color-primary-rgb),.1);font-weight:600;color:var(--ion-color-dark)}.quality-table td{color:var(--ion-color-dark-tint);font-size:.7rem}.quality-table tbody tr:hover{background-color:rgba(var(--ion-color-primary-rgb),.05)}.quality-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;color:var(--ion-color-medium)}.quality-loading ion-spinner{margin-bottom:12px}.quality-empty{text-align:center;padding:40px 20px;color:var(--ion-color-medium);font-style:italic}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i4.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i4.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i4.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i4.IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: i4.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i4.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i4.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i4.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i4.IonProgressBar, selector: "ion-progress-bar", inputs: ["buffer", "color", "mode", "reversed", "type", "value"] }, { kind: "component", type: i4.IonRow, selector: "ion-row" }, { kind: "component", type: i4.IonSelect, selector: "ion-select", inputs: ["cancelText", "compareWith", "disabled", "interface", "interfaceOptions", "mode", "multiple", "name", "okText", "placeholder", "selectedText", "value"] }, { kind: "component", type: i4.IonSelectOption, selector: "ion-select-option", inputs: ["disabled", "value"] }, { kind: "component", type: i4.IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: i4.IonToggle, selector: "ion-toggle", inputs: ["checked", "color", "disabled", "mode", "name", "value"] }, { kind: "directive", type: i4.BooleanValueAccessor, selector: "ion-checkbox,ion-toggle" }, { kind: "directive", type: i4.SelectValueAccessor, selector: "ion-range, ion-select, ion-radio-group, ion-segment, ion-datetime" }, { kind: "component", type: RaainMapComponent, selector: "raain-map", inputs: ["coordinates", "markers", "timeframeContainers", "autoplay", "showMarkers", "showSpeedMarkers", "showVisiblePixelMarkers", "showCumulative", "cumulativeDurationInMinutes", "currentHeight", "timeframeDates", "defaultDate", "sumValues", "sumFn"], outputs: ["selectedMarker", "changedDate", "changedSum"] }, { kind: "component", type: RaainCompareStackComponent, selector: "raain-compare-stack", inputs: ["compareManager", "currentHeight", "cumulative"], outputs: ["selectedPoint"] }, { kind: "component", type: RaainDateFocusComponent, selector: "raain-date-focus", inputs: ["points", "focusDate", "focusRange", "withoutAll", "currentHeight"] }, { kind: "component", type: RaainDateDynamicComponent, selector: "raain-date-dynamic", inputs: ["points", "focusDate", "focusRange", "withoutAll", "currentHeight", "fetchData"], outputs: ["changedDate"] }, { kind: "component", type: CumulativeSelectorComponent, selector: "cumulative-selector", inputs: ["rainId", "currentPeriodBegin", "currentPeriodEnd", "provider", "timeStepInMinutes", "isAdmin"], outputs: ["periodSelected", "cancelled"] }, { kind: "pipe", type: i2.DecimalPipe, name: "number" }, { kind: "pipe", type: i2.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3725
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: RaainDetailsComponent, decorators: [{
3726
+ type: Component,
3727
+ args: [{ selector: 'raain-details', changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Main content container -->\n<div *ngIf=\"rainNode\" class=\"raain-details-container\">\n\n <!-- Period selection section -->\n <ion-card class=\"period-card\">\n <ion-card-content>\n <div class=\"period-controls\">\n <div class=\"period-row\">\n\n <ion-button (click)=\"toggleHistory = !toggleHistory; onEnableCountHistoryTab(rainNode)\"\n fill=\"outline\">\n <ion-icon name=\"calendar-clear-outline\" slot=\"start\"></ion-icon>\n <ion-icon [name]=\"toggleHistory ? 'chevron-down' : 'chevron-forward'\" slot=\"end\"></ion-icon>\n </ion-button>\n\n <div class=\"period-start ion-hide-md-down\">\n <input (change)=\"onPeriodBeginChange($event)\"\n [disabled]=\"toggleCumulative\"\n [value]=\"periodBeginAsString\"\n class=\"datetime-input\"\n type=\"datetime-local\">\n </div>\n\n <div class=\"period-duration ion-hide-md-down\">\n <ion-select (ionDismiss)=\"onPeriodDurationChange($event)\"\n [(ngModel)]=\"periodDurationAsString\"\n [disabled]=\"toggleCumulative\"\n class=\"duration-select\"\n id=\"periodDuration\"\n interface=\"popover\">\n <ion-select-option value=\"0.25\">15 minutes</ion-select-option>\n <ion-select-option value=\"0.5\">30 minutes</ion-select-option>\n <ion-select-option value=\"1\">1 hour</ion-select-option>\n <ion-select-option value=\"2\">2 hours</ion-select-option>\n <ion-select-option value=\"3\">3 hours</ion-select-option>\n <ion-select-option value=\"4\">4 hours</ion-select-option>\n <ion-select-option value=\"5\">5 hours</ion-select-option>\n <ion-select-option value=\"6\">6 hours</ion-select-option>\n <ion-select-option value=\"8\">8 hours</ion-select-option>\n <ion-select-option value=\"10\">10 hours</ion-select-option>\n <ion-select-option value=\"12\">12 hours</ion-select-option>\n <ion-select-option *ngIf=\"isAdmin\" value=\"24\">24 hours</ion-select-option>\n </ion-select>\n </div>\n\n <div class=\"period-start ion-hide-md-down\">\n <input (change)=\"onPeriodEndChange($event)\"\n [disabled]=\"toggleCumulative\"\n [value]=\"periodEndAsString\"\n class=\"datetime-input\"\n type=\"datetime-local\">\n </div>\n\n <div class=\"toggle-cumulative\" (click)=\"onCumulativeToggleClick($event)\">\n <ion-label [class.text-primary]=\"toggleCumulative\">\n {{ toggleCumulative ? 'Cumulative' : 'Granular' }}\n </ion-label>\n <ion-toggle #cumulativeToggle [checked]=\"toggleCumulative\">\n </ion-toggle>\n </div>\n </div>\n\n <!-- Hidden label for change detection (uncomment to debug)\n <div class=\"hidden-label\">{{ onChangeDetectionTest(rainNode) }}</div>\n -->\n </div>\n\n <!-- Historical map section -->\n <div *ngIf=\"toggleHistory\" class=\"period-controls\">\n <raain-date-dynamic (changedDate)=\"onDateChangeInCount($event)\"\n [currentHeight]=\"300\"\n [fetchData]=\"fetchDataWrapper\"\n [points]=\"countPoints\">\n </raain-date-dynamic>\n </div>\n </ion-card-content>\n </ion-card>\n\n <!-- Map performance -->\n <ion-grid class=\"map-performance\">\n <ion-row id=\"progressAndRefresh\">\n <ion-col class=\"provider-selection\" size=\"12\" size-md=\"6\">\n <ion-button (click)=\"openQualityModal()\" class=\"quality-info-button\" fill=\"clear\">\n {{ refreshManager.rainComputationMapVersion }}\n <ion-icon name=\"help-circle-outline\" slot=\"end\"></ion-icon>\n </ion-button>\n <div *ngIf=\"availableProviders.length > 0 || availableTimeSteps.length > 0\" class=\"selection-row\">\n <ion-select (ionChange)=\"onProviderChanged($event)\"\n [value]=\"selectedProvider\"\n interface=\"popover\"\n label=\"Provider\"\n placeholder=\"Select Provider\">\n <ion-select-option *ngFor=\"let provider of availableProviders\" [value]=\"provider\">\n {{ provider }}\n </ion-select-option>\n </ion-select>\n\n <ion-select (ionChange)=\"onTimeStepChanged($event)\"\n [value]=\"selectedTimeStep\"\n interface=\"popover\"\n label=\"Time Step\"\n placeholder=\"Select Time Step\">\n <ion-select-option *ngFor=\"let step of availableTimeSteps\" [value]=\"step\">\n {{ step }} min\n </ion-select-option>\n </ion-select>\n </div>\n </ion-col>\n <ion-col class=\"ion-text-right\" size=\"12\" size-md=\"6\">\n <ion-label class=\"ion-margin-end\">\n <span *ngIf=\"percentOfComputations\">\n {{ percentOfComputations }}% Images\n <i *ngIf=\"countsPeriod.progress\" class=\"progress-indicator\">\n In Progress: {{ countsPeriod.progress }}...\n </i>\n </span>\n <span *ngIf=\"!percentOfComputations\">\n No image available\n </span>\n\n </ion-label>\n\n <ion-button (click)=\"refreshMap()\" [disabled]=\"refreshInProgress\" class=\"refresh-button\">\n Refresh Map\n </ion-button>\n </ion-col>\n </ion-row>\n\n <!-- status update row -->\n <ion-row>\n <!-- Progress col -->\n <ion-progress-bar\n [buffer]=\"(countsPeriod.progress / ((timeframeDates?.length || 10) +3))+0.01\"\n [style.visibility]=\"refreshInProgress ? 'visible' : 'hidden'\"\n [value]=\"countsPeriod.progress / ((timeframeDates?.length || 10) +3)\"\n color=\"primary\">\n </ion-progress-bar>\n\n <!-- Error col -->\n <ion-col (click)=\"showFullError = !showFullError\" *ngIf=\"refreshManager.lastError\" class=\"error-row\"\n size=\"12\">\n <div class=\"error-content\">\n <ion-icon class=\"error-icon\" name=\"warning-outline\"></ion-icon>\n <span [class.expanded]=\"showFullError\" class=\"error-text\">\n {{ showFullError ? refreshManager.lastError : truncatedError }}\n </span>\n <ion-icon [name]=\"showFullError ? 'chevron-up' : 'chevron-down'\" class=\"error-caret\"></ion-icon>\n </div>\n </ion-col>\n </ion-row>\n </ion-grid>\n\n <!-- Map section -->\n <ion-card class=\"map-card\">\n <ion-card-content class=\"map-content\">\n <ion-grid>\n <ion-row *ngIf=\"toggleMap && percentOfImages\">\n <!-- Map component -->\n <ion-col class=\"map-column\" size-lg=\"7\" size-md=\"12\">\n <div class=\"map-container\">\n <raain-map #raainMapRef\n (changedDate)=\"onDateChangeInMap($event)\"\n (changedSum)=\"onSumChangeInMap($event)\"\n (selectedMarker)=\"onGaugeSelectInMap($event)\"\n [coordinates]=\"coordinates\"\n [cumulativeDurationInMinutes]=\"cumulativeDurationInMinutes\"\n [currentHeight]=\"500\"\n [defaultDate]=\"dateShown\"\n [markers]=\"{\n borders,\n gauges: compareManager.gaugesInMap,\n gaugesInCompare: compareManager.gaugesInCompare,\n selectedGauges: compareManager.selectedGauges,\n pixels: compareManager.selectedPixels,\n pixelsSolution: compareManager.pixelsSolutions?.length ? compareManager.pixelsSolutions[0] : [],\n speeds: compareManager.speeds\n }\"\n [showCumulative]=\"toggleCumulative\"\n [showVisiblePixelMarkers]=\"showPixelMarkers\"\n [sumFn]=\"refreshManager.sumFn\"\n [sumValues]=\"refreshManager.sumValues\"\n [timeframeContainers]=\"timeframeContainers\"\n [timeframeDates]=\"timeframeDates\">\n </raain-map>\n </div>\n\n <div class=\"data-column\">\n <!-- Technical details (collapsible for cleaner UI) -->\n <details class=\"technical-details\">\n <summary>Image Details</summary>\n <div class=\"details-content\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Computed:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapDoneDate | date:'yyyy-MM-dd HH:mm' }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Version:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapVersion }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Launched by:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapLaunchedBy }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Time spent:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapTimeSpentInMs }}\n ms</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Date:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapDate?.toISOString() }}\n | {{ refreshManager.rainComputationMapDate | date:'yyyy-MM-dd HH:mm' }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Water in the map:</span>\n <span class=\"detail-value\">{{ sumDetails }}</span>\n <ion-toggle\n (ionChange)=\"onTogglePixelMarkers()\"\n [(ngModel)]=\"showPixelMarkers\"\n style=\"margin-left: 8px; transform: scale(0.7);\">\n </ion-toggle>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Source DBZ min:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapOriginalMin }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Source DBZ max:</span>\n <span class=\"detail-value\">{{ refreshManager.rainComputationMapOriginalMax }}</span>\n </div>\n </div>\n </details>\n </div>\n </ion-col>\n\n <!-- Data panel -->\n <ion-col *ngIf=\"!!compareManager.compareVersion\" class=\"data-column\" size-lg=\"5\" size-md=\"12\">\n <div class=\"data-panel\">\n <!-- Compare stack component -->\n <div class=\"compare-stack\">\n <raain-compare-stack\n (selectedPoint)=\"onGaugeSelectInCompare($event)\"\n [compareManager]=\"compareManager\"\n [cumulative]=\"toggleCumulative\">\n </raain-compare-stack>\n </div>\n\n <!-- Technical details (collapsible for cleaner UI) -->\n <details class=\"technical-details\">\n <summary>Compare Details</summary>\n <div class=\"details-content\">\n <div [ngClass]=\"{'warning': refreshManager.rainComputationMapDoneDate?.getTime() > compareManager.currentQualityDoneDate?.getTime() + 60000}\"\n class=\"detail-row\">\n <span class=\"detail-label\">Computed:</span>\n <span class=\"detail-value\">{{ compareManager.currentQualityDoneDate | date:'yyyy-MM-dd HH:mm' }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Version:</span>\n <span class=\"detail-value\">{{ compareManager.compareVersion }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Launched by:</span>\n <span class=\"detail-value\">{{ compareManager.currentQualityLaunchedBy }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Time spent:</span>\n <span class=\"detail-value\">{{ compareManager.currentQualityTimeSpentInMs }}\n ms</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Gauges:</span>\n <span class=\"detail-value\">{{ compareManager.gaugesInCompare.length }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Points:</span>\n <span class=\"detail-value\">{{ compareManager.globalComparePoints.length }}</span>\n </div>\n </div>\n </details>\n </div>\n </ion-col>\n </ion-row>\n <ion-row>\n <!-- Bottom progress bar -->\n <ion-progress-bar *ngIf=\"refreshInProgress\"\n [buffer]=\"(countsPeriod.progress / ((timeframeDates?.length || 10) +3))+0.01\"\n [value]=\"countsPeriod.progress / ((timeframeDates?.length || 10) +3)\"\n color=\"primary\">\n </ion-progress-bar>\n </ion-row>\n </ion-grid>\n </ion-card-content>\n </ion-card>\n\n <!-- Gauge values section -->\n <ion-card *ngIf=\"gaugeSelectedPoints.length && gaugeSelectedPoints[0].values.length\" class=\"gauge-card\">\n <ion-card-header>\n <ion-card-title>\n <ion-icon name=\"analytics-outline\"></ion-icon>\n Selected Gauge Data\n </ion-card-title>\n </ion-card-header>\n <ion-card-content>\n <raain-date-focus\n [currentHeight]=\"300\"\n [focusDate]=\"periodBegin\"\n [focusRange]=\"DateRange.DAY\"\n [points]=\"gaugeSelectedPoints\"\n [withoutAll]=\"true\">\n </raain-date-focus>\n </ion-card-content>\n </ion-card>\n\n <!-- Quality Performance Modal -->\n <div (click)=\"closeQualityModal()\" *ngIf=\"showQualityModal\" class=\"quality-modal-overlay\">\n <div (click)=\"$event.stopPropagation()\" class=\"quality-modal-content\">\n <div class=\"quality-modal-header\">\n <h2>Model Quality Performance</h2>\n <ion-button (click)=\"closeQualityModal()\" fill=\"clear\">\n <ion-icon name=\"close-outline\"></ion-icon>\n </ion-button>\n </div>\n <div class=\"quality-modal-body\">\n <div *ngIf=\"qualityIndicatorsLoading\" class=\"quality-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n <span>Loading indicators...</span>\n </div>\n <div *ngIf=\"!qualityIndicatorsLoading && qualityIndicators.length === 0\" class=\"quality-empty\">\n No quality indicators available for this year.\n </div>\n <table *ngIf=\"!qualityIndicatorsLoading && qualityIndicators.length > 0\" class=\"quality-table\">\n <thead>\n <tr>\n <th>Model</th>\n <th>Compare</th>\n <th>Gauges</th>\n <th>Period</th>\n <th>Avg Quality</th>\n <th>Updated</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let indicator of qualityIndicators\">\n <td>{{ indicator.computingVersion }}</td>\n <td>{{ indicator.qualityVersion }}</td>\n <td>{{ indicator.provider }}<br>{{ indicator.timeStepInMinutes }} min</td>\n <td>{{ formatDate(indicator.startDate) }}<br>{{ formatDate(indicator.endDate) }}</td>\n <td>{{ indicator.averageQuality | number:'1.2-2' }}</td>\n <td>{{ formatDate(indicator.lastUpdatedAt) }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n\n <!-- Cumulative Selector Modal -->\n <cumulative-selector *ngIf=\"showCumulativeSelector\"\n [rainId]=\"rainNode?.id\"\n [currentPeriodBegin]=\"periodBegin\"\n [currentPeriodEnd]=\"periodEnd\"\n [provider]=\"selectedProvider\"\n [timeStepInMinutes]=\"selectedTimeStep\"\n [isAdmin]=\"toggleAdmin\"\n (periodSelected)=\"onCumulativePeriodSelected($event)\"\n (cancelled)=\"onCumulativeSelectorCancelled()\">\n </cumulative-selector>\n\n</div>\n", styles: [".raain-details-container{max-width:var(--app-max-width);margin:0 auto;padding:0 0 24px}.raain-details-card{width:100%;margin-bottom:20px}.raain-details-card ion-card-header{border-bottom:1px solid rgba(var(--ion-color-medium-rgb),.2)}.raain-details-card ion-card-header ion-card-title{display:flex;align-items:center}.raain-details-card ion-card-header ion-card-title ion-icon{margin-right:8px;color:var(--ion-color-primary)}.node-info-card{background-color:var(--ion-color-light)}.node-info-card .node-header{display:flex;align-items:center}.node-info-card .node-header .node-status{margin-right:16px}.node-info-card .node-header .node-status ion-icon{font-size:24px}.node-info-card .node-header .node-titles{flex:1}.node-info-card .node-header .node-titles ion-card-title{margin:0;font-size:1.4rem;font-weight:600;color:var(--ion-color-dark)}.node-info-card .node-header .node-titles ion-card-subtitle{padding-left:0;margin:4px 0 0;font-size:.9rem;color:var(--ion-color-medium)}.count-map-card,.period-card{background-color:var(--ion-color-light)}.period-card ion-card-title{display:flex;align-items:center}.period-card ion-card-title ion-icon{margin-right:8px;color:var(--ion-color-primary);font-size:20px}.period-row{display:flex;flex-wrap:wrap;align-items:center;gap:16px;position:relative}.now-button{min-width:100px}#all-dates{display:flex;align-items:center;margin-left:auto}#all-dates .toggle-label{margin-right:8px;white-space:nowrap}.refresh-button ion-icon{margin-right:4px;color:var(--ion-color-light)}.provider-selection{display:flex;align-items:center}.quality-info-button{--padding-start: 8px;--padding-end: 8px;font-size:.85rem;color:var(--ion-color-medium)}.quality-info-button ion-icon{font-size:18px;margin-left:4px}.selection-row{display:flex;flex-direction:row;align-items:center;gap:12px}.selection-row ion-select{flex:0 0 auto;min-width:100px;margin-bottom:0;padding:4px 8px;border:1px solid rgba(var(--ion-color-medium-rgb),.3);border-radius:var(--ion-border-radius);background-color:var(--ion-color-light)}.period-start,.period-duration{display:flex;align-items:center}.toggle-cumulative{display:flex;align-items:center;gap:8px;margin-left:auto}.toggle-cumulative .text-primary{color:var(--ion-color-primary);font-weight:600}.hidden-label{display:none}.datetime-input,#periodDuration,.duration-select{padding:8px 12px;border:1px solid rgba(var(--ion-color-medium-rgb),.3);border-radius:var(--ion-border-radius);background-color:var(--ion-color-light);font-family:var(--ion-font-family)}.datetime-input:focus,#periodDuration:focus,.duration-select:focus{outline:none;border-color:var(--ion-color-primary)}#periodDuration,.duration-select{min-width:150px}.gauge-card{background-color:var(--ion-color-light)}.gauge-card ion-card-title{display:flex;align-items:center}.gauge-card ion-card-title ion-icon{margin-right:8px;color:var(--ion-color-primary);font-size:20px}.error-row{cursor:pointer;background-color:rgba(var(--ion-color-danger-rgb),.1);border-left:3px solid var(--ion-color-danger);margin-top:8px;border-radius:4px;transition:background-color .2s ease}.error-row:hover{background-color:rgba(var(--ion-color-danger-rgb),.15)}.error-row .error-content{display:flex;align-items:flex-start;padding:8px 12px;gap:8px}.error-row .error-icon{color:var(--ion-color-danger);font-size:18px;flex-shrink:0;margin-top:2px}.error-row .error-text{flex:1;color:var(--ion-color-danger-shade);font-size:.9rem;word-break:break-word}.error-row .error-text.expanded{white-space:pre-wrap}.error-row .error-caret{color:var(--ion-color-danger);font-size:16px;flex-shrink:0;margin-top:2px}raain-compare-stack{width:100%;display:block}@media (max-width: 768px){.period-row{flex-direction:row;justify-content:space-between;align-items:center}#all-dates{margin-left:auto;padding-left:8px}#all-dates .toggle-label{font-size:.9rem}.map-header{flex-direction:row;justify-content:space-between;align-items:center;gap:16px}}.quality-modal-overlay{position:fixed;inset:0;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:9999}.quality-modal-content{background-color:var(--ion-background-color, #fff);border-radius:12px;width:95%;max-width:900px;max-height:80vh;overflow:auto;box-shadow:0 4px 24px #0003}.quality-modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(var(--ion-color-medium-rgb),.2)}.quality-modal-header h2{margin:0;font-size:1.25rem;font-weight:600;color:var(--ion-color-dark)}.quality-modal-header ion-button{--padding-start: 8px;--padding-end: 8px}.quality-modal-body{padding:20px}.quality-table{width:100%;border-collapse:collapse;margin-top:16px;table-layout:fixed}.quality-table th,.quality-table td{width:16.66%;padding:12px 16px;text-align:center;vertical-align:top;border-bottom:1px solid rgba(var(--ion-color-medium-rgb),.2)}.quality-table th{background-color:rgba(var(--ion-color-primary-rgb),.1);font-weight:600;color:var(--ion-color-dark)}.quality-table td{color:var(--ion-color-dark-tint);font-size:.7rem}.quality-table tbody tr:hover{background-color:rgba(var(--ion-color-primary-rgb),.05)}.quality-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;color:var(--ion-color-medium)}.quality-loading ion-spinner{margin-bottom:12px}.quality-empty{text-align:center;padding:40px 20px;color:var(--ion-color-medium);font-style:italic}\n"] }]
3728
+ }], ctorParameters: function () { return [{ type: Storage }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { toggleAdmin: [{
3729
+ type: Input
3730
+ }], rainNode: [{
3731
+ type: Input
3732
+ }], compareManager: [{
3733
+ type: Input
3734
+ }], refreshManager: [{
3735
+ type: Input
3736
+ }], profileService: [{
3737
+ type: Input
3738
+ }], radarService: [{
3739
+ type: Input
3740
+ }], cumulativeToggleRef: [{
3741
+ type: ViewChild,
3742
+ args: ['cumulativeToggle']
3743
+ }] } });
3744
+
3745
+ class Cache {
3746
+ constructor() {
3747
+ this._cache = {};
3748
+ }
3749
+ async getValue(key, asyncGetter) {
3750
+ if (!Object.prototype.hasOwnProperty.call(this._cache, key)) {
3751
+ console.log('cache not done: ', key);
3752
+ this.putValue(key, await asyncGetter());
3421
3753
  }
3422
- if (c.hour !== undefined) {
3423
- dateToShow.setUTCHours(c.hour);
3754
+ else {
3755
+ console.log('cache done: ', key);
3424
3756
  }
3425
- if (c.minute !== undefined) {
3426
- dateToShow.setUTCMinutes(c.minute);
3757
+ return this._cache[key];
3758
+ }
3759
+ putValue(key, value) {
3760
+ this._cache[key] = value;
3761
+ const length = Object.getOwnPropertyNames(this._cache).length;
3762
+ if (length > 30) {
3763
+ console.warn('Pb on cache size exceed ? do a restart ?', length);
3427
3764
  }
3428
- return dateToShow.toISOString();
3429
3765
  }
3430
3766
  }
3431
- ProfileService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ProfileService, deps: [{ token: Storage }, { token: i2$1.FidjService }, { token: i3$1.HttpClient }, { token: i4$1.Router }], target: i0.ɵɵFactoryTarget.Injectable });
3432
- ProfileService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ProfileService, providedIn: 'root' });
3433
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ProfileService, decorators: [{
3767
+ Cache.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: Cache, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3768
+ Cache.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: Cache, providedIn: 'root' });
3769
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: Cache, decorators: [{
3434
3770
  type: Injectable,
3435
3771
  args: [{
3436
3772
  providedIn: 'root',
3437
3773
  }]
3438
- }], ctorParameters: function () { return [{ type: Storage }, { type: i2$1.FidjService }, { type: i3$1.HttpClient }, { type: i4$1.Router }]; } });
3774
+ }], ctorParameters: function () { return []; } });
3439
3775
 
3440
3776
  class RadarService {
3441
3777
  constructor(profileService) {
@@ -3863,7 +4199,8 @@ SharedModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "
3863
4199
  RaainSpeedComponent,
3864
4200
  RaainGlobeComponent,
3865
4201
  ProfileIconDirective,
3866
- RaainDetailsComponent], imports: [CommonModule, FormsModule, IonicModule, NgOptimizedImage, PipesModule], exports: [CommonModule,
4202
+ RaainDetailsComponent,
4203
+ CumulativeSelectorComponent], imports: [CommonModule, FormsModule, IonicModule, NgOptimizedImage, PipesModule], exports: [CommonModule,
3867
4204
  NgStyle,
3868
4205
  PipesModule,
3869
4206
  RaainMapComponent,
@@ -3875,7 +4212,8 @@ SharedModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "
3875
4212
  RaainSpeedComponent,
3876
4213
  RaainGlobeComponent,
3877
4214
  ProfileIconDirective,
3878
- RaainDetailsComponent] });
4215
+ RaainDetailsComponent,
4216
+ CumulativeSelectorComponent] });
3879
4217
  SharedModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: SharedModule, providers: [Storage, RadarService, ProfileService, Cache, IonRange], imports: [CommonModule, FormsModule, IonicModule, PipesModule, CommonModule,
3880
4218
  PipesModule] });
3881
4219
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: SharedModule, decorators: [{
@@ -3892,6 +4230,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImpo
3892
4230
  RaainGlobeComponent,
3893
4231
  ProfileIconDirective,
3894
4232
  RaainDetailsComponent,
4233
+ CumulativeSelectorComponent,
3895
4234
  ],
3896
4235
  imports: [CommonModule, FormsModule, IonicModule, NgOptimizedImage, PipesModule],
3897
4236
  providers: [Storage, RadarService, ProfileService, Cache, IonRange],
@@ -3909,6 +4248,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImpo
3909
4248
  RaainGlobeComponent,
3910
4249
  ProfileIconDirective,
3911
4250
  RaainDetailsComponent,
4251
+ CumulativeSelectorComponent,
3912
4252
  ],
3913
4253
  }]
3914
4254
  }], ctorParameters: function () { return [{ type: SharedModule, decorators: [{
@@ -3921,5 +4261,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImpo
3921
4261
  * Generated bundle index. Do not edit.
3922
4262
  */
3923
4263
 
3924
- export { AreInProgressPipe, AreReady, AreStopped, CONSTANTS, Cache, CompareManager, FidjStorage, FidjStorageNode, FrameSet, GaugeNodeFilter, HasGoodQuality, HasNotGoodQuality, HaveNotBeenRed, IsNotReady, IsReady, PipesModule, ProfileIconDirective, ProfileService, ProgressBuffer, ProgressValue, RaainCompareComponent, RaainCompareStackComponent, RaainConfigurationComponent, RaainDateDynamicComponent, RaainDateFocusComponent, RaainDetailsComponent, RaainGlobeComponent, RaainMapComponent, RaainSpeedComponent, RadarService, RefreshManager, SharedModule, Storage, WaitForValidation, XYType, mapDateRangeToString };
4264
+ export { AreInProgressPipe, AreReady, AreStopped, CONSTANTS, Cache, CompareManager, CumulativeSelectorComponent, FidjStorage, FidjStorageNode, FrameSet, GaugeNodeFilter, HasGoodQuality, HasNotGoodQuality, HaveNotBeenRed, IsNotReady, IsReady, PipesModule, ProfileIconDirective, ProfileService, ProgressBuffer, ProgressValue, RaainCompareComponent, RaainCompareStackComponent, RaainConfigurationComponent, RaainDateDynamicComponent, RaainDateFocusComponent, RaainDetailsComponent, RaainGlobeComponent, RaainMapComponent, RaainSpeedComponent, RadarService, RefreshManager, SharedModule, Storage, WaitForValidation, XYType, mapDateRangeToString };
3925
4265
  //# sourceMappingURL=raain-app.mjs.map