diff --git a/buildspec.yml b/buildspec.yml index 17d627cd3..f91506006 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -7,7 +7,7 @@ phases: commands: - n 18 - npm set progress=false - - npm install -g @angular/cli@17.2.7 + - npm install -g @angular/cli@17.3.10 pre_build: commands: - cp src/app/services/envs/env-${MATURITY}.ts src/app/services/env.ts diff --git a/src/app/components/results-menu/scene-files/scene-files.component.ts b/src/app/components/results-menu/scene-files/scene-files.component.ts index ad5f0d999..0d72d5d58 100644 --- a/src/app/components/results-menu/scene-files/scene-files.component.ts +++ b/src/app/components/results-menu/scene-files/scene-files.component.ts @@ -437,7 +437,13 @@ export class SceneFilesComponent implements OnInit, OnDestroy, AfterContentInit }; return this.asfApiService.query(queryParams).pipe( map(products => products?.results?.length > 0 ? this.productService.fromResponse(products).slice(0, 1) : []), - tap(products => products.map(product => product.productTypeDisplay = scene.metadata.productType + "-STATIC Layer")) + tap(products => products.map(product => { + product.productTypeDisplay = "Local Incidence Angle GeoTIFF"; + product.bytes = 0; + product.downloadUrl = product.metadata.opera.additionalUrls.find(url => url.endsWith('local_incidence_angle.tif')); + return product; + } + )) ); } else { return of([]); 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 2198653cb..c96f6bacc 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 @@ -16,54 +16,74 @@
- @if(pointHistory.length !== 0) { - - @for (point of pointHistory; track $index) { - - - place - - {{point.flatCoordinates[1] | floatPrecision: 2}}, {{point.flatCoordinates[0] | floatPrecision: 2}} + @if(pointHistory.length !== 0) { + +
+ + + + {{task().aoi}} - - } - - } - @else { -
+ + + +
    + @for (point of pointHistory; track $index) { +
  • + + place + + {{point.flatCoordinates[1] | floatPrecision: 2}}, {{point.flatCoordinates[0] | floatPrecision: 2}} + + +
  • + } +
+
+
+ } + @else { +
{{ 'NO_POINT_SELECTED' | translate }} -
- } +
+ } + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + { this.pointHistory = history; this.mapService.setDisplacementLayer(history); + console.log('results menu sub this.pointHistory', this.pointHistory); + const task = this.task(); + let found = false + for (const point of this.pointHistory) { + found = false; + for (const pt of task.subtasks) { + if (pt.aoi.toString() === point.flatCoordinates.toString()) { + found = true; + break; + } + } + if (!found) { + let p = {aoi: point.flatCoordinates, checked: true}; + task.subtasks.push(p); + } + } + + console.log('results menu sub task.subtasks', task.subtasks); + + return {...task}; + })); this.subs.add(this.drawService.polygon$.subscribe(polygon => { @@ -94,7 +117,7 @@ export class TimeseriesResultsMenuComponent implements OnInit, OnDestroy { this.pointHistoryService.addPoint(temp); // this.selectedPoint = temp; } - this.updateChart(temp); + this.updateChart(); } })) @@ -143,76 +166,55 @@ export class TimeseriesResultsMenuComponent implements OnInit, OnDestroy { this.mapService.loadPolygonFrom(wktRepresenation.toString()) } - public updateChart(geometry): void { - this.netcdfService.getTimeSeries(geometry).pipe(first()).subscribe(data => { - this.chartData.next(data); - // just going to use the data here to have some test + public updateChart(): void { + let allPointsData = []; + 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); + }) + console.log('updateChart allPointsData', allPointsData); + this.chartData.next(allPointsData); + } + } - let test_products = [] - for(let a of Object.keys(data)) { - if(a === 'mean') { - continue - } - let dummy_product = { - name: "20221107_20221213.unw.nc", - productTypeDisplay: "20221107_20221213.unw.nc", - file: "20221107_20221213.unw.nc", - id: "20221107_20221213.unw.nc", - downloadUrl: "", - bytes: 1234, - browses: [], - thumbnail: "string", - dataset: "sentinel-1", - groupId: "string", - isUnzippedFile: false, - isDummyProduct: false, - metadata: { - date: moment2(), - stopDate: moment2(), - polygon: "POLYGON ((-120.6596489990334 38.10046393341369, -120.7165475579197 38.10851198053945, -120.766930740861 38.11562044217759, -120.8205210213509 38.12315205210022, -120.8714495693995 38.13028813229113, -120.9237420515751 38.13758904376237, -120.9763848233747 38.14491322663075, -121.0284993874585 38.15213949038783, -121.0805374061872 38.15933042899581, -121.1317842372317 38.16638876359664, -121.1831324092139 38.17343685331883, -121.2343267090171 38.1804400907544, -121.2851343767767 38.18736716519965, -121.3356959243903 38.19423758535788, -121.3861081098637 38.20106467487116, -121.4362721727969 38.20783544395677, -121.48323466170885 38.21415300334824, -121.4961042842803 38.15636388218255, -121.5433137463578 38.16270254678292, -121.5911446754841 38.1691031129965, -121.6386899947014 38.17544523556456, -121.6861407154369 38.18175427917799, -121.7332794261246 38.18800202289999, -121.7803915044837 38.19422601057961, -121.827141292521 38.20038272861196, -121.8737037166488 38.20649520761842, -121.9200946535652 38.21256588219698, -121.9662943385527 38.21859236147657, -122.0122835841423 38.22457236130022, -122.0581361973249 38.23051573928061, -122.1041763492786 38.23646390681441, -122.149693424194 38.24232638029159, -122.1961235981019 38.24828575950047, -122.242840340764 38.25426186141181, -122.2851111656349 38.25965821461571, -122.3300033441712 38.2653676014464, -122.3758025278513 38.27117216227268, -122.4195396691845 38.27670031010642, -122.3858453485523 38.44296136955153, -122.3532454174394 38.60872264056508, -122.321281901071 38.77481263179438, -122.2870678391941 38.9402423639722, -122.28657198358258 38.94018057384403, -122.2484647545139 39.10551216594683, -122.2139845790114 39.27116104878576, -122.1797260176706 39.4367089374228, -122.1454462898122 39.60250573468526, -122.1112187045845 39.76830723022915, -122.0657227757032 39.76272908715357, -122.0199986201785 39.75710391072595, -121.974167995761 39.75144665647245, -121.9280106375743 39.74573006489359, -121.8819129486135 39.74000139035989, -121.8355618840318 39.73422190999039, -121.7893509902118 39.72844019721962, -121.7438269303752 39.72272426697831, -121.6969816389773 39.71682457472104, -121.6515073229837 39.71107635608378, -121.6043191954164 39.70509395348859, -121.5584565168173 39.69925815820748, -121.5109102827927 39.69319018010273, -121.4645931051444 39.68725740118437, -121.4191841855194 39.68142061388765, -121.3710837028612 39.67522101887367, -121.3249639873766 39.66925411189222, -121.2737070377029 39.66260665395009, -121.23414991223851 39.657450537553174, -121.2225423730193 39.71501508524592, -121.1765350243167 39.70900995464177, -121.1256787673412 39.70235510426308, -121.0761429466098 39.6958485542612, -121.0248065795119 39.68908396631235, -120.9731471040036 39.68225266837984, -120.9238122923641 39.67570292460091, -120.8690679290765 39.66841567043333, -120.8147668765373 39.66115958039808, -120.7685909543843 39.65495848719279, -120.7156528937689 39.64783419415447, -120.663007754518 39.64072322969391, -120.6113623558398 39.6337213126097, -120.5575229174151 39.62639828522865, -120.4998084040708 39.61852276683964, -120.4452699517613 39.61104894626836, -120.3863396198317 39.60294709422497, -120.3350203072667 39.59585767211257, -120.2794058786818 39.5881523754319, -120.2276117469398 39.58094649107327, -120.22744443645614 39.58092314818873, -120.2134545404021 39.63753782618391, -120.1722737074452 39.6317932200585, -120.1244336159495 39.62510726553644, -120.0702567931258 39.61751657755807, -120.022269227435 39.61076456269333, -119.9734628265665 39.60387622000747, -119.9205724742573 39.59639065119934, -119.8710716027977 39.58935798135493, -119.8176837356334 39.58175135017377, -119.7678667295637 39.57462631850627, -119.7172666919774 39.56736632596481, -119.6677829890141 39.56024234859523, -119.6164294368659 39.55282685903347, -119.5655855530352 39.54546030402012, -119.5122825240406 39.53771380091477, -119.4596714762434 39.53004121358733, -119.4118356441431 39.52303826133494, -119.3539548115401 39.51454620335684, -119.2999598520621 39.50659276685604, -119.242172282983 39.49805375475951, -119.1867112941332 39.48982722325655, -119.2279582300632 39.32389259181299, -119.2695651047809 39.15814430461985, -119.3154157605841 38.99290396440068, -119.3553955782568 38.82692456194625, -119.4076391719693 38.66278010421749, -119.46426202130762 38.48267405863559, -119.4994803127931 38.33275776186966, -119.5473964124392 38.1679932576986, -119.5759619696445 38.00032674732519, -119.6328594358995 38.00892055228446, -119.685940538886 38.01691396436385, -119.735510021753 38.02435806698762, -119.7876843769492 38.0321663403309, -119.8349767585424 38.03922663419335, -119.8827140678868 38.04633197874039, -119.9336830819589 38.05389196505576, -119.9837372437618 38.06129369064131, -120.029196677421 38.0680003062205, -120.0790441003354 38.07532784287181, -120.1269111066452 38.08234457142717, -120.1767641224129 38.08962794955193, -120.2211977375846 38.09610549717873, -120.2696883291999 38.10314938009251, -120.3131576383198 38.10945036625376, -120.3570069870991 38.11578848698885, -120.4046282389668 38.12264764067351, -120.4517936234199 38.12942103705984, -120.48468630063608 38.134130553754865, -120.4964144795629 38.07720256883281, -120.5521610008301 38.08517268978709, -120.6049996926634 38.09270374285773, -120.6596489990334 38.10046393341369))", - productType: "", - beamMode: "", - polarization: "", - flightDirection: null, - path: 1, - frame: 1, - absoluteOrbit: [1], - stackSize: 1, - faradayRotation: null, - offNadirAngle: null, - instrument: null, - pointingAngle: null, - missionName: null, - flightLine: null, - perpendicular: null, - temporal: null, - canInSAR: false, - burst: null, - opera: null, - fileName: null, - job:null, - pgeVersion: null, - subproducts: [], - parentID: "", - ariaVersion: null, - } - } - let response_file = data[a]; - dummy_product.name = a - dummy_product.productTypeDisplay = a - dummy_product.id = a - dummy_product.file = a - dummy_product.groupId = a - dummy_product.downloadUrl = response_file.uri - dummy_product.metadata.date = moment2(response_file.time) - test_products.push(dummy_product) - } - this.store$.dispatch(new SetScenes({ - products: test_products, - searchType: SearchType.DISPLACEMENT - })) + readonly task = signal({ + aoi: 'ALL AOIs', + checked: false, + subtasks: [], + }); + + readonly partiallyComplete = computed(() => { + const task = this.task(); + if (!task.subtasks) { + return false; + } + return task.subtasks.some(t => t.checked) && !task.subtasks.every(t => t.checked); + }); + + public updateSeries(checked: boolean, index?: number) { + console.log('updateSeries', checked, index); + this.task.update(task => { + if (index === undefined) { + task.checked = checked; + task.subtasks?.forEach(t => (t.checked = checked)); + } else { + task.subtasks![index].checked = checked; + task.checked = task.subtasks?.every(t => t.checked) ?? true; + this.pointHistoryService.selectedPoint = index; + console.log('updateSeries() this.pointHistoryService.selectedPoint', this.pointHistoryService.selectedPoint); + this.pointHistoryService.passDraw = true; + let format = new WKT(); + let wktRepresentation = format.writeGeometry(this.pointHistory[index]); + this.mapService.loadPolygonFrom(wktRepresentation.toString()) - }) + } + console.log('updateSeries() task', task); + console.log('updateSeries() task.subtasks', task.subtasks); + console.log('updateSeries() this.pointHistory', this.pointHistory); + return {...task}; + }); } ngOnDestroy() { diff --git a/src/app/components/results-menu/timeseries-results-menu/timeseries-results-menu.module.ts b/src/app/components/results-menu/timeseries-results-menu/timeseries-results-menu.module.ts index 8d7451db3..33e62ef63 100644 --- a/src/app/components/results-menu/timeseries-results-menu/timeseries-results-menu.module.ts +++ b/src/app/components/results-menu/timeseries-results-menu/timeseries-results-menu.module.ts @@ -20,6 +20,7 @@ import {MatTab, MatTabGroup} from '@angular/material/tabs'; import { TimeseriesChartTemporalSliderComponent } from '@components/timeseries-chart/timeseries-chart-temporal-slider/timeseries-chart-temporal-slider.component'; +import {MatCheckbox} from '@angular/material/checkbox'; @NgModule({ @@ -43,7 +44,8 @@ import { PipesModule, MatTabGroup, MatTab, - TimeseriesChartTemporalSliderComponent + TimeseriesChartTemporalSliderComponent, + MatCheckbox ], exports: [ TimeseriesResultsMenuComponent diff --git a/src/app/components/shared/save-search-dialog/save-search-dialog.component.html b/src/app/components/shared/save-search-dialog/save-search-dialog.component.html index 6d4a91b51..727e4ff23 100644 --- a/src/app/components/shared/save-search-dialog/save-search-dialog.component.html +++ b/src/app/components/shared/save-search-dialog/save-search-dialog.component.html @@ -3,15 +3,19 @@

{{ saveTypeName | uppercase | translate | titlecase }}

- - + + [(ngModel)]="saveName" + [maxLength]="20"> + {{saveNameInput.value?.length || 0}}/20
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 452fa86f9..c18ec1bd2 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 @@ - +

Timeseries Date Slider Goes Here

diff --git a/src/app/components/timeseries-chart/timeseries-chart-voronoi.ts b/src/app/components/timeseries-chart/timeseries-chart-voronoi.ts new file mode 100644 index 000000000..321d7252b --- /dev/null +++ b/src/app/components/timeseries-chart/timeseries-chart-voronoi.ts @@ -0,0 +1,179 @@ +import {Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + + +import * as d3 from 'd3'; + +@Component({ + selector: 'app-timeseries-chart', + templateUrl: './timeseries-chart.component.html', + styleUrl: './timeseries-chart.component.scss' +}) +export class TimeseriesChartComponent implements OnInit, OnDestroy { + @ViewChild('timeseriesChart', {static: true}) timeseriesChart: ElementRef; + public url: string = '/assets/unemployment.json'; + public unemploymentData: any; + public svg?: d3.Selection; + public showVoronoi = false; + + + constructor(private http: HttpClient) { + } + + public ngOnInit(): void { + this.http.get(this.url).subscribe(res => { + this.unemploymentData = res; + console.log('this.unemploymentData',this.unemploymentData); + this.makeChartFromFile(); + }); + } + + public makeChartFromFile() { + this.makeChart(d3, this.unemploymentData, this.showVoronoi); + } + + public toggleVoronoi() { + this.showVoronoi = !this.showVoronoi; + this.makeChartFromFile(); + } + + public makeChart(d3,unemployment,voronoi){ + console.log('***** makeChart *****'); + console.log('d3', d3); + console.log('unemployment', unemployment); + console.log('voronoi', voronoi); + + if (this.svg) { + d3.selectAll('#timeseries-chart > svg').remove(); + d3.selectAll('.tooltip').remove(); + } + + const element = document.getElementById("timeseriesChart"); + element.innerHTML = ''; + // Specify the chart’s dimensions. + const width = 928; + const height = 600; + const marginTop = 20; + const marginRight = 20; + const marginBottom = 30; + const marginLeft = 30; + + // Create the positional scales. + const x = d3.scaleUtc() + .domain(d3.extent(unemployment, d => new Date(d.date))) + .range([marginLeft, width - marginRight]); + + const y = d3.scaleLinear() + .domain([0, d3.max(unemployment, d => d.unemployment)]).nice() + .range([height - marginBottom, marginTop]); + + // Create the SVG container. + this.svg = d3.select("#timeseriesChart").append("svg") + .attr("width", width) + .attr("height", height) + .attr("viewBox", [0, 0, width, height]) + .attr("style", "max-width: 100%; height: auto; overflow: visible; font: 10px sans-serif;"); + + // Add the horizontal axis. + this.svg.append("g") + .attr("transform", `translate(0,${height - marginBottom})`) + .call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0)); + + // Add the vertical axis. + this.svg.append("g") + .attr("transform", `translate(${marginLeft},0)`) + .call(d3.axisLeft(y)) + .call(g => g.select(".domain").remove()) + .call(voronoi ? () => {} : g => g.selectAll(".tick line").clone() + .attr("x2", width - marginLeft - marginRight) + .attr("stroke-opacity", 0.1)) + .call(g => g.append("text") + .attr("x", -marginLeft) + .attr("y", 10) + .attr("fill", "currentColor") + .attr("text-anchor", "start") + .text("↑ Unemployment (%)")); + + + // Compute the points in pixel space as [x, y, z], where z is the name of the series. + const points = unemployment.map((d) => [x(new Date(d.date)), y(d.unemployment), d.division]); + console.log('points', points); + console.log('unemployment', unemployment); + + // An optional Voronoi display (for fun). + if (voronoi) this.svg.append("path") + .attr("fill", "none") + .attr("stroke", "#ccc") + .attr("d", d3.Delaunay + .from(points) + .voronoi([0, 0, width, height]) + .render()); + + // Group the points by series. + const groups = d3.rollup(points, v => Object.assign(v, {z: v[0][2]}), d => d[2]); + console.log('groups', groups); + + // Draw the lines. + const line = d3.line(); + const path = this.svg.append("g") + .attr("fill", "none") + .attr("stroke", "steelblue") + .attr("stroke-width", 1.5) + .attr("stroke-linejoin", "round") + .attr("stroke-linecap", "round") + .selectAll("path") + .data(groups.values()) + .join("path") + .style("mix-blend-mode", "multiply") + .attr("d", line); + + // Add an invisible layer for the interactive tip. + const dot = this.svg.append("g") + .attr("display", "none"); + + dot.append("circle") + .attr("r", 2.5); + + dot.append("text") + .attr("text-anchor", "middle") + .attr("y", -8); + + this.svg + .on("pointerenter", pointerEntered) + .on("pointermove", pointerMoved) + .on("pointerleave", pointerLeft) + .on("touchstart", event => event.preventDefault()); + + return; + + // 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. + function pointerMoved(event) { + const [xm, ym] = d3.pointer(event); + const i = d3.leastIndex(points, ([x, y]) => Math.hypot(x - xm, y - ym)); + const [x, y, k] = points[i]; + path.style("stroke", ({z}) => z === k ? null : "#ddd").filter(({z}) => z === k).raise(); + dot.attr("transform", `translate(${x},${y})`); + dot.select("text").text(k); + // this.svg.property("value", unemployment[i]).dispatch("input", {bubbles: true}); + } + + function pointerEntered() { + path.style("mix-blend-mode", null).style("stroke", "#ddd"); + dot.attr("display", null); + } + + function pointerLeft() { + path.style("mix-blend-mode", "multiply").style("stroke", null); + dot.attr("display", "none"); + // this.svg.node().value = null; + // this.svg.dispatch("input", {bubbles: true}); + } + + } + + ngOnDestroy() { + // this.subs.unsubscribe(); + } +} diff --git a/src/app/components/timeseries-chart/timeseries-chart.component.scss b/src/app/components/timeseries-chart/timeseries-chart.component.scss index b047a13f2..44aaa09ef 100644 --- a/src/app/components/timeseries-chart/timeseries-chart.component.scss +++ b/src/app/components/timeseries-chart/timeseries-chart.component.scss @@ -1,16 +1,16 @@ @import "asf-theme"; -#timeseriesChart { - height: 100%; - width: 100%; -} - .chart-wrapper { height: 90%; width: 100%; display: flex; } +#timeseriesChart { + height: 100%; + width: 100%; +} + ::ng-deep .timeseries-base { @include themify($themes) { fill: themed('blue-link'); diff --git a/src/app/components/timeseries-chart/timeseries-chart.component.ts b/src/app/components/timeseries-chart/timeseries-chart.component.ts index 7920ba89b..1f447a463 100644 --- a/src/app/components/timeseries-chart/timeseries-chart.component.ts +++ b/src/app/components/timeseries-chart/timeseries-chart.component.ts @@ -5,19 +5,40 @@ import { Observable, Subject } from 'rxjs'; import { Store } from '@ngrx/store'; import { AppState } from '@store'; -import * as sceneStore from '@store/scenes'; +// import * as sceneStore from '@store/scenes'; import * as chartsStore from '@store/charts'; import { SubSink } from 'subsink'; import { AsfLanguageService } from "@services/asf-language.service"; +interface TimeSeriesChartPoint { + aoi: string + unwrapped_phase: number + interferometric_correlation: number + temporal_coherence: number + date: string + file_name: string, + temporal_baseline: number + id: string +} + +interface TimeSeriesData { + unwrapped_phase: number + date: string +} + +interface DataReady { + name: string, + values: TimeSeriesData[] +} + @Component({ selector: 'app-timeseries-chart', templateUrl: './timeseries-chart.component.html', styleUrl: './timeseries-chart.component.scss' }) export class TimeseriesChartComponent implements OnInit, OnDestroy { - @ViewChild('timeseriesChart', { static: true }) timeseriesChart: ElementRef; @ViewChild('tsChartWrapper', { static: true }) tsChartWrapper: ElementRef; + @ViewChild('timeseriesChart', { static: true }) timeseriesChart: ElementRef; @Input() zoomIn$: Observable; @Input() zoomOut$: Observable; @Input() zoomToFit$: Observable; @@ -26,6 +47,8 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { public json_data: string = ''; private svg?: d3.Selection; public dataSource: TimeSeriesChartPoint[] = []; + public dataReadyForChart: DataReady[] = []; + public timeSeriesData: TimeSeriesData[] = []; public averageData = {}; public displayedColumns: string[] = ['position', 'unwrapped_phase', 'interferometric_correlation', 'temporal_coherence'] private currentTransform: d3.ZoomTransform; @@ -39,26 +62,27 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { private y: d3.ScaleLinear; public xAxis: d3.Selection; private yAxis: d3.Selection; - private dots: d3.Selection; + private dots: d3.Selection; + // private dots: d3.Selection; private lineGraph: d3.Selection; private toolTip: d3.Selection public margin = { top: 10, right: 20, bottom: 60, left: 55 }; private thing: d3.Selection private hoveredElement; + private data: any; - private selectedScene: string; + // private selectedScene: string; private showLines = true; private xAxisTitle = ''; private yAxisTitle = ''; - // private currentLanguage: string = null; private subs = new SubSink(); + private allGroup: string[]; constructor( private store$: Store, private language: AsfLanguageService, - ) { - } + ) { } public ngOnInit(): void { @@ -66,16 +90,17 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { this.createSVG(); this.chartData.subscribe(data => { + this.data = data; this.initChart(data); }) - this.subs.add( - this.store$.select(sceneStore.getSelectedScene).subscribe(test => { - this.selectedScene = test.id; - this.updateChart(); - - }) - ); + // this.subs.add( + // this.store$.select(sceneStore.getSelectedScene).subscribe(test => { + // this.selectedScene = test.id; + // this.updateChart(); + // + // }) + // ); this.subs.add( this.store$.select(chartsStore.getShowLines).subscribe( @@ -86,7 +111,7 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { } else { this.lineGraph.remove() } - this.updateChart(); + this.initChart(this.data); } ) ) @@ -104,47 +129,68 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { } public translateChartText() { - this.xAxisTitle = this.language.translate.instant('SCENE') + ' ' + this.language.translate.instant('DATE'); - this.yAxisTitle = this.language.translate.instant('SHORTWAVE_DISPLACEMENT') + ' (' + this.language.translate.instant('METERS') + ')'; } public onZoomIn(): void { this.thing.transition().call(this.zoom.scaleBy, 2); + this.drawChart(); } public onZoomOut(): void { this.thing.transition().call(this.zoom.scaleBy, .5); + this.drawChart(); } public onZoomToFit(): void { this.thing.transition().call(this.zoom.transform, d3.zoomIdentity); + this.drawChart(); } public initChart(data): void { + console.log('****** timeseries-chart init data *******', data); this.dataSource = [] - - // pre-process data, remove test v_2 files from results - // won't be necessary in production - for (let key of Object.keys(data)) { - if (key.startsWith('v_2_')) { - delete data[key] + let aoi: string = ''; + console.log('****** timeseries-chart init data *******', data); + for (let result of data) { + aoi = ''; + // pre-process data, remove test v_2 files from results + // won't be necessary in production + for (let key of Object.keys(result)) { + if (key.startsWith('v_2_')) { + delete result[key]; + } + if (key.startsWith('aoi')) { + aoi = result[key]; + } } + this.timeSeriesData = []; + for (let key of Object.keys(result).filter(x => x !== 'mean' && x !== 'aoi')) { + console.log('timeseries-chart init key', key); + console.log('timeseries-chart init result', result); + console.log('timeseries-chart init result[key]', result[key]); + this.dataSource.push({ + 'aoi': aoi, + 'unwrapped_phase': result[key].unwrapped_phase, + 'interferometric_correlation': result[key].interferometric_correlation, + 'temporal_coherence': result[key].temporal_coherence, + 'date': result[key].secondary_datetime, + 'file_name': result[key].source_file_name, + 'id': key, + 'temporal_baseline': result[key].temporal_baseline + }) + this.timeSeriesData.push({ + 'unwrapped_phase': result[key].unwrapped_phase, + 'date': result[key].secondary_datetime + }); + } + this.dataReadyForChart.push({ 'name': aoi, 'values': this.timeSeriesData }); + } - for (let key of Object.keys(data).filter(x => x !== 'mean')) { - this.dataSource.push({ - 'unwrapped_phase': data[key].unwrapped_phase, - 'interferometric_correlation': data[key].interferometric_correlation, - 'temporal_coherence': data[key].temporal_coherence, - 'date': data[key].secondary_datetime, - 'file_name': data[key].source_file_name, - 'id': key, - 'temporal_baseline': data[key].temporal_baseline - }) - } + console.log('timeseries-chart init dataReadyForChart', this.dataReadyForChart); this.averageData = ({ ...data.mean }) @@ -155,7 +201,9 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { } private drawChart() { - const marginBottom = 40; + + // Determine scale extents + // const marginBottom = 40; const unwrapped_phases = this.dataSource.map(p => p['unwrapped_phase'] as number) const dates = this.dataSource.map(p => Date.parse(p['date'])).filter(d => !isNaN(d)) const inner_margins = 1.25 @@ -163,6 +211,8 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { const min_x = Math.min(...dates) const max_y = Math.max(...unwrapped_phases) * inner_margins const max_x = Math.max(...dates) + + // Create scales this.x = d3.scaleUtc() .domain([min_x, max_x]) .range([0, this.width]) @@ -174,7 +224,8 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { .range([this.height, 0]); this.yAxis = this.svg.append('g'); this.svg.append("g") - .attr("transform", `translate(0,${this.height - marginBottom})`) + // .attr("transform", `translate(0,${this.height - marginBottom})`) + .attr("transform", `translate(0,${this.height})`) this.clipContainer = this.svg.append('g') .attr('clip-path', 'url(#clip)'); @@ -186,19 +237,42 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { this.toolTip = toolTip this.toolTip.attr('transform', `translate(0, 0)`).style('text-anchor', 'middle').style('z-index', 100).style('opacity', 0) + this.allGroup = [...new Set(this.dataReadyForChart.map(d => d.name))]; + console.log('allGroup', this.allGroup); + + this.lineGraph = this.clipContainer.append("path"); + + // A color scale: one color for each group + const colorPalette = d3.scaleOrdinal() + .domain(this.allGroup) + .range(d3.schemeSet2); + const self = this; - this.lineGraph = this.clipContainer.append("path") + console.log('dataSource', this.dataSource); + const points = this.dataSource.map((d) => [this.x(new Date(d.date)), this.y(d.unwrapped_phase), d.aoi]); + console.log('points', points); + const groups = d3.rollup(points, v => Object.assign(v, {z: v[0][2]}), d => d[2]); + console.log('groups', groups); - this.dots = this.clipContainer.append('g').selectAll('circle') - .data(this.dataSource) + this.dots = this.clipContainer.append('g') + .selectAll("myDots") + .data(this.dataReadyForChart) + .enter() + .append('g') + // @ts-ignore + .style("fill", function (d: DataReady){ return colorPalette(d.name) }) + .selectAll('circle') + .data(d => d.values) + // .data(this.dataSource) + // .data(groups.values()) .enter() .append('circle') - .attr('cx', (d: TimeSeriesChartPoint) => this.x(Date.parse(d.date))) - .attr('cy', (d: TimeSeriesChartPoint) => this.y(d.unwrapped_phase)) - .on('mouseover', function (_event: any, p: TimeSeriesChartPoint) { + .attr('cx', (d) => this.x(Date.parse(d.date))) + .attr('cy', (d) => this.y(d.unwrapped_phase)) + .on('mouseover', function (_event: any, p: TimeSeriesData) { self.hoveredElement = this; - const date = new Date(p.date) + const date = new Date(p.date); toolTip.interrupt(); toolTip .style('opacity', .9); @@ -210,23 +284,23 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { .duration(500) .style('opacity', 0); }) - .on('click', (_event, d) => { - this.store$.dispatch(new sceneStore.SetSelectedScene(d.id)) - }) - .attr('class', (d) => { - if (this.selectedScene === d.id) { - return 'timeseries-selected'; - } else { - return 'timeseries-base'; - } - }) - .attr('r', 5) + // .on('click', (_event, d) => { + // this.store$.dispatch(new sceneStore.SetSelectedScene(d.id)) + // }) + // .attr('class', (d) => { + // if (this.selectedScene === d.id) { + // return 'timeseries-selected'; + // } else { + // return 'timeseries-base'; + // } + // }) + .attr('r', 5); this.zoom = d3.zoom() .extent([[0, 0], [this.width, this.height]]) .on('zoom', (eve: d3.D3ZoomEvent) => { this.currentTransform = eve.transform; - this.updateChart(); + this.initChart(this.data); }); this.thing = d3.select('#timeseriesChart').selectChild() this.thing.call(this.zoom) @@ -253,6 +327,40 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { .attr('class', 'ts-chart-label') .text(this.yAxisTitle); + // this.svg + // .on("pointerenter", pointerentered) + // .on("pointermove", pointermoved) + // .on("pointerleave", pointerleft) + // .on("touchstart", event => event.preventDefault()); + // + // // 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. + // function pointermoved(event) { + // const [xm, ym] = d3.pointer(event); + // // @ts-ignore + // const i = d3.leastIndex(points, ([x, y]) => Math.hypot(x - xm, y - ym)); + // const [x, y, k] = points[i]; + // this.path.style("stroke", ({z}) => z === k ? null : "#ddd").filter(({z}) => z === k).raise(); + // this.dot.attr("transform", `translate(${x},${y})`); + // this.dot.select("text").text(k); + // // @ts-ignore + // self.svg.property("value", self.dataReadyForChart[i]).dispatch("input", {bubbles: true}); + // } + // + // function pointerentered() { + // this.path.style("mix-blend-mode", null).style("stroke", "#ddd"); + // this.dot.attr("display", null); + // } + // + // function pointerleft() { + // this.path.style("mix-blend-mode", "multiply").style("stroke", null); + // this.dot.attr("display", "none"); + // this.svg.node().value = null; + // // @ts-ignore + // self.svg.dispatch("input", {bubbles: true}); + // } + this.updateChart(); } @@ -270,31 +378,57 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { .ticks(smallChart ? 10 : 5, 's') ); - var lineFunction = d3.line() - .x(function (d) { return newX(Date.parse(d.date)); }) - .y(function (d) { return newY(d.unwrapped_phase); }) + // var lineFunction = d3.line() + // .x(function (d) { return newX(Date.parse(d.date)); }) + // .y(function (d) { return newY(d.unwrapped_phase); }) this.dots .attr('cx', d => newX(Date.parse(d.date))) .attr('cy', d => newY(d.unwrapped_phase)) - .attr('class', (d) => { - if (this.selectedScene === d.id) { - return 'timeseries-selected'; - } else { - return 'timeseries-base'; - } - }) - .on('click', (_event, d) => { - this.store$.dispatch(new sceneStore.SetSelectedScene(d.id)) - }) + // .attr('class', (d) => { + // if (this.selectedScene === d.id) { + // return 'timeseries-selected'; + // } else { + // return 'timeseries-base'; + // } + // }) + // .on('click', (_event, d) => { + // this.store$.dispatch(new sceneStore.SetSelectedScene(d.id)) + // }) if (this.showLines) { - this.addPairAttributes( - this.lineGraph - .attr('d', _ => lineFunction(this.dataSource)) - .attr('fill', 'none') - ) + // this.addPairAttributes( + // this.lineGraph + // // .data(this.dataReadyForChart) + // .join("path") + // // .attr("d", d => line(d.values)) + // // .attr("stroke", d => (d.name)) + // .attr('d', _ => lineFunction(this.dataReadyForChart.flatMap(d => d.values))) + // .attr('fill', 'none') + // ) + + // A color scale: one color for each group + const colorPalette = d3.scaleOrdinal() + .domain(this.allGroup) + .range(d3.schemeSet2); + + // Add the lines + let line = d3.line() + .x(function (d) { return newX(Date.parse(d.date)); }) + .y(function (d) { return newY(d.unwrapped_phase); }) + this.svg.selectAll("myLines") + .data(this.dataReadyForChart) + .enter() + .append("path") + .attr('clip-path', 'url(#clip)') + .attr("d", function(d){ // @ts-ignore + return line(d.values) } ) + // @ts-ignore + .attr("stroke", function (d: DataReady){ return colorPalette(d.name) }) + .style("stroke-width", 2) + .style("fill", "none") + } } @@ -304,15 +438,13 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { this.toolTip.style('left', `${bounding.x + (a ? -150 : 20)}px`) .style('top', `${bounding.y - 10}px`); } - private addPairAttributes(ps) { - return ps - .attr('class', 'base-line') - .attr('stroke', 'steelblue') - .attr('stroke-width', 1) - }; - public updateAxis(_axis, _value) { - } + // private addPairAttributes(ps) { + // return ps + // .attr('class', 'base-line') + // .attr('stroke', 'steelblue') + // .attr('stroke-width', 1) + // }; public onResized() { this.createSVG(); @@ -334,8 +466,8 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { .attr('height', this.height + this.margin.top + this.margin.bottom) .append('g') .attr('transform', `translate(${this.margin.left}, ${this.margin.top})`); - this.drawChart(); + this.drawChart(); } private tooltipDateFormat(date) { @@ -351,19 +483,15 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { return join(date, dateFormat, ' '); } + public swatches(d: any) { + return d3.scaleOrdinal() + .domain(d) + .range(d3.schemeCategory10); + + } + public ngOnDestroy(): void { this.subs.unsubscribe(); } } - -interface TimeSeriesChartPoint { - unwrapped_phase: number - interferometric_correlation: number - temporal_coherence: number - date: string - file_name: string, - temporal_baseline: number - id: string -} - diff --git a/src/app/models/hyp3-jobs.model.ts b/src/app/models/hyp3-jobs.model.ts index 2a6c43fb8..88a28c58e 100644 --- a/src/app/models/hyp3-jobs.model.ts +++ b/src/app/models/hyp3-jobs.model.ts @@ -12,7 +12,7 @@ export const RtcGammaJobType: Hyp3JobType = { productTypes: [{ dataset: sentinel_1, productTypes: [ - 'SLC', 'GRD_HD' + 'SLC', 'GRD_HD', 'GRD_HS' ], beamModes: ['IW'], polarizations: [ diff --git a/src/app/services/netcdf-service.service.ts b/src/app/services/netcdf-service.service.ts index de37a6317..60bb9b32d 100644 --- a/src/app/services/netcdf-service.service.ts +++ b/src/app/services/netcdf-service.service.ts @@ -74,6 +74,7 @@ export class NetcdfService { let index_id = wktRepresenation; console.log(`getting ${index_id}`) if(this.cache.hasOwnProperty(index_id)) { + console.log('cache hit', of(this.cache[index_id])); return of(this.cache[index_id]) } else { return this.http.post(`${this.url}${this.timeSeriesEndpoint}`, { @@ -82,16 +83,18 @@ export class NetcdfService { }, { responseType: 'json' }).pipe( first(), map( response => { + (response as any).aoi = wktRepresenation; this.cache[index_id] = response; - this.totalKeys.push(index_id) + this.totalKeys.push(index_id); if(this.totalKeys.length > this.maxCacheSize) { let deleted = this.totalKeys.splice(0); delete this.cache[deleted[0]]; } + console.log('cache miss', response); return response } )) } } -} \ No newline at end of file +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 67713a7c1..9a75dcd9f 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -111,6 +111,7 @@ "CANADAS_OPEN_LICENSE": "Canada's Open Government License", "CANCEL": "Cancel", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(center column) and Files column (right) will populate.", + "CHARACTERS": "characters", "CHART": "Chart", "CHEVRON_RIGHT": "chevron_right", "CIRCLE DRAW": "Circle Draw", @@ -738,7 +739,7 @@ "SETTINGS_OVERSCAN": "settings_overscan", "SHARE_SEARCH": "Share Search", "SHARE_WITH_EMAIL": "Share With Email", - "SHORTWAVE_DISPLACEMENT": "Shortwave Displacement", + "SHORTWAVE_DISPLACEMENT": "Short Wave Displacement", "SHOW_PRODUCT_FILTERS_CRITERIA": "Show product filters criteria", "SHOW_RAW_DATA": "Show raw data", "SIGMA0": "sigma0", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 804ecde1f..268ee3d64 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -112,6 +112,7 @@ "CANADAS_OPEN_LICENSE": "Licencia de Gobierno Abierto de Canadá", "CANCEL": "Cancelar", "CENTER_COLUMN_AND_FILES_COLUMN_RIGHT_WILL_POPULATE": "(columna central) y la columna Archivos (derecha) se completarán.", + "CHARACTERS": "caracteres", "CHART": "Cuadro", "CHEVRON_RIGHT": "chevron_right", "CIRCLE DRAW": "Dibujar Circulo",