diff --git a/.gitignore b/.gitignore index c80988ade..d5d09e198 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,3 @@ testem.log .DS_Store Thumbs.db local-serve.sh - -src/app/components/timeseries-chart/timeseries-chart-abort.component.ts diff --git a/src/app/components/results-menu/sbas-results-menu/sbas-sliders-two/sbas-sliders-two.component.ts b/src/app/components/results-menu/sbas-results-menu/sbas-sliders-two/sbas-sliders-two.component.ts index c535337fe..582335b60 100644 --- a/src/app/components/results-menu/sbas-results-menu/sbas-sliders-two/sbas-sliders-two.component.ts +++ b/src/app/components/results-menu/sbas-results-menu/sbas-sliders-two/sbas-sliders-two.component.ts @@ -74,10 +74,10 @@ export class SbasSlidersTwoComponent implements OnInit, OnDestroy { this.daysControl = new UntypedFormControl(this.daysRange, Validators.min(0)); const daysSliderRef = this.makeDaysSlider$(this.temporalFilter); - const tempSlider = daysSliderRef.slider; + // const tempSlider = daysSliderRef.slider; const daysValues$ = daysSliderRef.daysValues; - this.tempSlider = tempSlider; + // this.tempSlider = tempSlider; fromEvent(this.meterFilter.nativeElement, 'keyup').pipe( map((event: any) => { diff --git a/src/app/components/results-menu/timeseries-results-menu/timeseries-results-menu.component.html b/src/app/components/results-menu/timeseries-results-menu/timeseries-results-menu.component.html index 41620d1a2..66f19f0c8 100644 --- a/src/app/components/results-menu/timeseries-results-menu/timeseries-results-menu.component.html +++ b/src/app/components/results-menu/timeseries-results-menu/timeseries-results-menu.component.html @@ -150,7 +150,8 @@ - + diff --git a/src/app/components/results-menu/timeseries-results-menu/timeseries-results-menu.component.ts b/src/app/components/results-menu/timeseries-results-menu/timeseries-results-menu.component.ts index 1aa97aa2f..6feb9ad48 100644 --- a/src/app/components/results-menu/timeseries-results-menu/timeseries-results-menu.component.ts +++ b/src/app/components/results-menu/timeseries-results-menu/timeseries-results-menu.component.ts @@ -7,6 +7,7 @@ import { AppState } from '@store'; import * as uiStore from '@store/ui'; import * as searchStore from '@store/search'; import * as mapStore from '@store/map'; +import * as models from '@models'; import { DrawService, MapService, NetcdfService, PointHistoryService, ScreenSizeService, WktService } from '@services'; import { Breakpoints, SearchType, MapInteractionModeType, MapDrawModeType } from '@models'; @@ -23,6 +24,20 @@ export interface Task { subtasks?: Task[]; } +export interface PointSeries { + bytes: number; + interferometric_correlation: number; + netcdf_uri: string; + persistent_scatterer_mask: number; + reference_datetime: string; + secondary_datetime: string; + short_wavelength_displacement: number; + temporal_baseline: number; + temporal_coherence: number; + x: number; + y: number; +} + @Component({ selector: 'app-timeseries-results-menu', templateUrl: './timeseries-results-menu.component.html', @@ -52,6 +67,9 @@ export class TimeseriesResultsMenuComponent implements OnInit, OnDestroy { public pointHistory = []; public chartData = new Subject; + public maxRange: models.Range = {start: 0, end: 0}; + public dataDateMin: Date; + public dataDateMax: Date; public selectedPoint: number; // private timeseries_subscription: Subscription; @@ -188,15 +206,17 @@ export class TimeseriesResultsMenuComponent implements OnInit, OnDestroy { } public updateChart(): void { - let allPointsData = []; + let allPointsData: PointSeries[] = []; for (const geometry of this.pointHistory) { this.netcdfService.getTimeSeries(geometry).pipe(first()).subscribe(data => { console.log('updateChart data', data); console.log('updateChart geometry', geometry); allPointsData.push(data); + this.chartData.next(allPointsData); + this.maxRange = this.getMaxRange(allPointsData); + console.log('updateChart dataDateMin, dataDateMax', this.dataDateMin, this.dataDateMax); + console.log('updateChart allPointsData', allPointsData); }) - console.log('updateChart allPointsData', allPointsData); - this.chartData.next(allPointsData); } } @@ -214,6 +234,29 @@ export class TimeseriesResultsMenuComponent implements OnInit, OnDestroy { return task.subtasks.some(t => t.checked) && !task.subtasks.every(t => t.checked); }); + public getMaxRange(allSeries: PointSeries[]) { + let minDate = null; + let maxDate = null; + for (let points of allSeries) { + for (let key of Object.keys(points).filter(x => x !== 'mean' && x !== 'aoi')) { + console.log('getMaxRange key', key); + console.log('getMaxRange points[key]', points[key]); + let date = new Date(points[key].secondary_datetime); + console.log('getMaxRange date', date); + if (minDate === null || date < minDate) { + minDate = date; + } + if (maxDate === null || date > maxDate) { + maxDate = date; + } + } + } + let dateRange: models.Range = {start: minDate, end: maxDate}; + console.log('getMaxRange dateRange', dateRange); + return dateRange; + } + + public updateSeries(checked: boolean, index?: number) { console.log('updateSeries', checked, index); this.task.update(task => { diff --git a/src/app/components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component.html b/src/app/components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component.html index c18ec1bd2..fdd2c8c2e 100644 --- a/src/app/components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component.html +++ b/src/app/components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component.html @@ -1 +1,8 @@ -

Timeseries Date Slider Goes Here

+
+
+ 2000 + 2005 + 2010 + 2015 + 2023 +
diff --git a/src/app/components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component.scss b/src/app/components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component.scss index e69de29bb..d61d9383d 100644 --- a/src/app/components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component.scss +++ b/src/app/components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component.scss @@ -0,0 +1,42 @@ +/* Contenedor principal del slider */ +.slider { + margin: 0 auto; /* Centra el slider horizontalmente */ + width: 100%; /* Ocupa todo el ancho disponible */ + max-width: 95%; /* Opcional: Limita el ancho máximo para que no se extienda demasiado */ + box-sizing: border-box; +} + +.noUi-connect { + background-color: #007BFF; /* Color principal */ + height: 6px; /* Ajusta el grosor de la barra conectada */ +} + +.noUi-handle { + background-color: #FFFFFF; + border: 2px solid #007BFF; /* Borde que combine con el color principal */ + border-radius: 50%; /* Hace que el control sea redondo */ + height: 20px; + width: 20px; +} + +.noUi-tooltip { + background-color: #333; + color: #fff; + border-radius: 4px; + font-size: 12px; +} + +/* Estilos de las etiquetas debajo del slider */ +.slider-labels { + display: flex; + justify-content: space-between; + width: 100%; /* Ocupa todo el ancho del slider */ + max-width: 95%; /* Limita el ancho para alinearse con el slider */ + margin: 10px auto 0; /* Centra y da espacio superior al slider */ + box-sizing: border-box; +} + +.slider-labels span { + font-size: 12px; + color: #666; /* Color de las etiquetas */ +} diff --git a/src/app/components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component.ts b/src/app/components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component.ts index 7eaebdbc3..8d082ec81 100644 --- a/src/app/components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component.ts +++ b/src/app/components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component.ts @@ -1,12 +1,131 @@ -import { Component } from '@angular/core'; +import {Component, ElementRef, ViewChild, OnInit, Input, OnChanges, SimpleChanges} from '@angular/core'; +import * as noUiSlider from 'nouislider'; +import { Store } from "@ngrx/store"; +import { AppState } from "@store"; +import * as models from "@models"; +import {Observable, Subject} from 'rxjs'; +import {UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators} from "@angular/forms"; +import * as filtersStore from "@store/filters"; +import {SubSink} from "subsink"; +import {debounceTime, distinctUntilChanged} from "rxjs/operators"; @Component({ selector: 'app-timeseries-chart-temporal-slider', standalone: true, - imports: [], templateUrl: './timeseries-chart-temporal-slider.component.html', - styleUrl: './timeseries-chart-temporal-slider.component.scss' + styleUrls: ['./timeseries-chart-temporal-slider.component.scss'] }) -export class TimeseriesChartTemporalSliderComponent { +export class TimeseriesChartTemporalSliderComponent implements OnInit, OnChanges { + @Input() maxRange: models.Range = {start: 0, end: 0}; + @ViewChild('slider', { static: true }) sliderRef: ElementRef; + public daysRange: models.Range = {start: 0, end: 0}; + public lastRange: models.Range = {start: 0, end: 0}; + public daysValues$ = new Subject(); + public slider; + + options: UntypedFormGroup; + + daysControl: UntypedFormControl; + + private firstLoad = true; + private subs = new SubSink(); + + constructor( + private store$: Store, + fb: UntypedFormBuilder + ) { + this.daysControl = new UntypedFormControl(this.daysRange, Validators.min(0)); + + this.options = fb.group({ + days: this.daysControl, + }); + } + + ngOnInit() { + this.daysControl = new UntypedFormControl(this.daysRange, Validators.min(0)); + const daysSliderRef = this.makeDaysSlider(this.sliderRef); + const daysValues$ = daysSliderRef.daysValues; + + // this.tempSlider = tempSlider; + + this.subs.add( + daysValues$.subscribe( + range => { + console.log(range); + const action = new filtersStore.SetTemporalRange({ start: range[0], end: range[1] }); + this.store$.dispatch(action); + } + ) + ); + + this.subs.add( + this.store$.select(filtersStore.getTemporalRange).subscribe( + temp => { + this.daysRange = {start: temp.start, end: temp.end}; + if (this.firstLoad) { + this.firstLoad = false; + this.slider.set([temp.start, temp.end]); + } + } + ) + ); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.maxRange && changes.maxRange.currentValue) { + this.maxRange = changes.maxRange.currentValue; + console.log('changes start time:', this.maxRange.start.valueOf()); + console.log('changes end time:', this.maxRange.end.valueOf()); + this.daysRange = {start: this.maxRange.start.valueOf(), + end: this.maxRange.end.valueOf()}; + if (this.firstLoad) { + this.firstLoad = false; + this.slider.set([this.daysRange.start, this.daysRange.end]); + } + // this.slider.noUiSlider.updateOptions({ + // start: [this.maxRange.start.valueOf(), this.maxRange.end.valueOf()], + // range: { + // 'min': this.maxRange.start.valueOf(), + // 'max': this.maxRange.end.valueOf() + // } + } + } + + public updateDaysOffset() { + this.options.controls.days.setValue(this.daysRange); + this.daysValues$.next([this.daysRange.start, this.daysRange.end] ); + } + + public makeDaysSlider(filterRef: ElementRef): {slider: any, daysValues: Observable} { + console.log('makeDaysSlider maxRange', this.maxRange); + this.slider = noUiSlider.create(filterRef.nativeElement, { + start: [2000, 2023], + behaviour: 'tap-drag', + tooltips: false, + connect: true, + step: 1, + range: { + 'min': 2000, + 'max': 2023 + } + }); + + this.slider.on('update', (values, _) => { + this.daysValues$.next(values.map(v => +v)); + }); + + return { + slider: this.slider, + daysValues: this.daysValues$.asObservable().pipe( + debounceTime(500), + distinctUntilChanged() + ) + }; + + } + + ngOnDestroy() { + this.subs.unsubscribe(); + } } diff --git a/src/app/components/timeseries-chart/timeseries-chart.component.ts b/src/app/components/timeseries-chart/timeseries-chart.component.ts index a185c9c91..865dd6e06 100644 --- a/src/app/components/timeseries-chart/timeseries-chart.component.ts +++ b/src/app/components/timeseries-chart/timeseries-chart.component.ts @@ -32,6 +32,8 @@ interface DataReady { values: TimeSeriesData[] } +const unSelectedColor = '#9F9F9F9F'; + @Component({ selector: 'app-timeseries-chart', templateUrl: './timeseries-chart.component.html', @@ -350,6 +352,7 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { // add the dots this.dots = this.svg.append('g') + .attr("id", "dotsParent") .selectAll("myDots") .data(this.dataReadyForChart) .enter() @@ -383,8 +386,7 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { } // When the pointer moves, find the closest point, update the interactive tip, and highlight - // the corresponding line. Note: we don't actually use Voronoi here, since an exhaustive search - // is fast enough. + // the corresponding line. private pointerMoved(event, lines, dots, points) { console.log('pointerMoved'); if (typeof points === 'undefined') { return; } @@ -392,57 +394,49 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { const [xm, ym] = d3.pointer(event); const i = d3.leastIndex(points, ([x, y]) => Math.hypot(Number(x) - xm, Number(y) - ym)); if (typeof points[i] === 'undefined') { return; } - const [x, y, k] = points[i]; + const [_x, _y, k] = points[i]; let colorName: string; let dClassName: string; - console.log('points', points); - console.log('points[i]', points[i]); - console.log('dots', dots); - console.log('xm', xm, 'ym', ym); - console.log('i', i); - console.log('x', x, 'y', y, 'k', k); + // set the color of the selected line to the color of the series; make all other lines grey lines.style("stroke", (d: DataReady)=> { if (d.name === k) { - dClassName = 'g.' + d.name.replace(/\W/g, ''); + dClassName = '.' + d.name.replace(/\W/g, ''); colorName = this.gColorPalette(d.name); return colorName; } - return '#ddd'; + return unSelectedColor; + }); + // sort the lines so that the selected line is on top + lines.selectAll("path").sort(function (a, _b) { + if (a.attr('stroke') != colorName) return -1; + else return 1; + }); + this.svg.selectAll('circle').style("fill", unSelectedColor); + this.svg.selectAll(dClassName + ' ' + 'circle').style("fill", colorName); + this.svg.selectAll("dotsParent").sort(function (a, _b) { + // @ts-ignore + if (a.attr('class') != dClassName) return -1; + else return 1; }); - dots.selectAll('circle').style("fill", '#ddd'); - dots.selectAll(dClassName).style("fill", 'red'); - - // dots.selectAll('myDots').data(this.dataReadyForChart).style("fill", (d: DataReady) => { - // dots.selectAll('circle').style("fill", (d: DataReady) => { - // console.log('dots d:', d); - // if (d.name === k) { - // return this.gColorPalette(d.name); - // } - // return '#ddd'; - // }); - // this.lines.style("stroke", "red").filter(({ z }) => z === k).raise(); - // .attr("stroke", function (d: DataReady) { return colorPalette(d.name) }) - // lines.style("stroke", ({z}) => z === k ? null : "#ddd").filter(({z}) => z === k).raise(); - // dots.attr("transform", `translate(${x},${y})`); dots.select("text").text(k); - // this.svg.property("value", this.dataReadyForChart[i]).dispatch("input", {bubbles: true}); } private pointerEntered(lines, dots) { console.log('pointerEntered lines, dots', lines, dots); - lines.style("mix-blend-mode", null).style("stroke", "#ddd"); + lines.style("mix-blend-mode", null).style("stroke", unSelectedColor); dots.attr("display", null); } private pointerLeft(lines, dots) { console.log('pointerLeft', lines, dots); + let colorName: string; + let dClassName: string; lines.style("stroke", (d: DataReady)=> { - return this.gColorPalette(d.name); - }) - // lines.style("mix-blend-mode", "multiply").style("stroke", null); - // dots.attr("display", "none"); - // this.svg.node().value = null; - // self.svg.dispatch("input", {bubbles: true}); + dClassName = '.' + d.name.replace(/\W/g, ''); + colorName = this.gColorPalette(d.name); + this.svg.selectAll(dClassName + ' ' + 'circle').style("fill", colorName); + return colorName; + }); } private updateChart() {