diff --git a/src/app/app.component.ts b/src/app/app.component.ts index cbfd5b185..34374b68a 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -18,6 +18,7 @@ import { HelpComponent } from '@components/help/help.component'; import { AppState } from '@store'; import * as scenesStore from '@store/scenes'; +import * as chartsStore from '@store/charts'; import * as filterStore from '@store/filters'; import * as searchStore from '@store/search'; import * as uiStore from '@store/ui'; @@ -98,6 +99,7 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { public language: services.AsfLanguageService, public _adapter: DateAdapter, private titleService: Title, + private pointHistoryService: services.PointHistoryService, @Inject(MAT_DATE_LOCALE) public _locale: string, ) { } @@ -458,7 +460,9 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { this.store$.select(uiStore.getIsResultsMenuOpen), this.store$.select(scenesStore.getAreResultsLoaded) ]).pipe( - filter(([_, searchType, _resultsOpen, resultsLoaded]) => searchType == SearchType.DISPLACEMENT && !resultsLoaded) + filter(([_, searchType, _resultsOpen, resultsLoaded]) => { + // TODO: this seems to sometimes not work, sometimes clearing isn't actually setting resultsLoaded to false + return searchType == SearchType.DISPLACEMENT && !resultsLoaded}) ).subscribe(([polygon, _, __]) => { if (polygon) { if (polygon.getGeometry().getType() === 'Point') { @@ -467,6 +471,21 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { } } })) + + + this.subs.add(this.store$.select(chartsStore.getTimeseriesChartStates).pipe( + withLatestFrom(this.pointHistoryService.history$) + ).subscribe(([chartStates, history]) => { + if(Object.keys(chartStates).length === history.length) { + let data = [] + + for (const p of history) { + data.push({ point: p.point, seriesNumber: chartStates[p.wkt].seriesNumber, color: chartStates[p.wkt].color }) + } + this.mapService.setDisplacementLayer(data); + } + + })); } public ngAfterViewInit(): void { @@ -480,6 +499,7 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { } public onClearSearch(): void { + this.pointHistoryService.clear(); this.store$.dispatch(new scenesStore.ClearScenes()); this.store$.dispatch(new scenesStore.SetSelectedSarviewsEvent('')); this.mapService.clearDrawLayer(); diff --git a/src/app/components/map/displacement-layers/displacement-layers.component.html b/src/app/components/map/displacement-layers/displacement-layers.component.html index b8acb2d69..b5daf9091 100644 --- a/src/app/components/map/displacement-layers/displacement-layers.component.html +++ b/src/app/components/map/displacement-layers/displacement-layers.component.html @@ -20,7 +20,8 @@ - +
Rollout
diff --git a/src/app/components/map/displacement-layers/displacement-layers.component.ts b/src/app/components/map/displacement-layers/displacement-layers.component.ts index 7027efd44..2a6896acd 100644 --- a/src/app/components/map/displacement-layers/displacement-layers.component.ts +++ b/src/app/components/map/displacement-layers/displacement-layers.component.ts @@ -21,7 +21,7 @@ export class DisplacementLayersComponent implements OnInit, OnDestroy { public displacementOverview: models.DisplacementLayerTypes | null = null; public cumulativeDisplacementSelectionDisabled: boolean = true; public DispLayerTypes = models.DisplacementLayerTypes; - + public priorityEnabled = false; private subs = new SubSink(); constructor( @@ -55,6 +55,11 @@ export class DisplacementLayersComponent implements OnInit, OnDestroy { } ) ) + this.subs.add( + this.mapService.priorityEnabled$.subscribe(t => { + this.priorityEnabled = t !== null; + }) + ) } public onUpdatePriority(isChecked: boolean): void { 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 a484766c1..f4f1c0320 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 @@ -9,7 +9,7 @@ import * as searchStore from '@store/search'; import * as chartStore from '@store/charts'; import { - DrawService, MapService, NetcdfService, PointHistoryService, ScreenSizeService, + DrawService, NetcdfService, PointHistoryService, ScreenSizeService, WktService } from '@services'; import { Breakpoints, SearchType } from '@models'; @@ -93,7 +93,6 @@ export class TimeseriesResultsMenuComponent implements OnInit, OnDestroy { private screenSize: ScreenSizeService, public pointHistoryService: PointHistoryService, private drawService: DrawService, - private mapService: MapService, private netcdfService: NetcdfService, private wktService: WktService ) { } @@ -138,16 +137,7 @@ export class TimeseriesResultsMenuComponent implements OnInit, OnDestroy { ) ); - this.subs.add(this.store$.select(getTimeseriesChartStates).pipe( - withLatestFrom(this.pointHistoryService.history$) - ).subscribe(([chartStates, history]) => { - let data = [] - for (const p of history) { - data.push({ point: p.point, seriesNumber: chartStates[p.wkt].seriesNumber, color: chartStates[p.wkt].color }) - } - this.mapService.setDisplacementLayer(data); - })); let thing: string = localStorage.getItem('timeseries-points') if (thing && thing.length > 0) { diff --git a/src/app/components/timeseries-chart/timeseries-chart-flight-direction-toggle/timeseries-chart-flight-direction-toggle.component.ts b/src/app/components/timeseries-chart/timeseries-chart-flight-direction-toggle/timeseries-chart-flight-direction-toggle.component.ts index 7fbfb8334..a7fdddc85 100644 --- a/src/app/components/timeseries-chart/timeseries-chart-flight-direction-toggle/timeseries-chart-flight-direction-toggle.component.ts +++ b/src/app/components/timeseries-chart/timeseries-chart-flight-direction-toggle/timeseries-chart-flight-direction-toggle.component.ts @@ -4,7 +4,6 @@ import { AppState } from '@store'; import { SubSink } from 'subsink'; import * as filtersStore from '@store/filters'; import * as models from '@models'; -import { SetFlightDirections } from '@store/filters'; import { map } from 'rxjs'; import { TranslateModule } from '@ngx-translate/core'; import { MatButtonModule } from '@angular/material/button'; @@ -23,19 +22,25 @@ export class TimeseriesChartFlightDirectionToggleComponent implements OnInit { public FlightDirections = models.FlightDirection; constructor(private store$: Store) {} -ngOnInit(): void { - this.subs.add( - this.store$.select(filtersStore.getFlightDirections).pipe( - map(dir => dir[0] ?? this.flightDirection) - ).subscribe( - dir => this.flightDirection = dir + ngOnInit(): void { + this.subs.add( + this.store$.select(filtersStore.getFlightDirections).pipe( + map(dir => dir[0] ?? this.flightDirection) + ).subscribe( + dir => this.flightDirection = dir + ) ) - ) - -} -public onToggle(): void { - const outputDirection = this.flightDirection === this.FlightDirections.ASCENDING ? this.FlightDirections.DESCENDING : this.FlightDirections.ASCENDING - this.store$.dispatch(new SetFlightDirections([outputDirection])) -} + } + + public onToggle(): void { + const outputDirection = this.flightDirection === this.FlightDirections.ASCENDING ? this.FlightDirections.DESCENDING : this.FlightDirections.ASCENDING + + const dir = outputDirection + .toUpperCase(); + + + const action = new filtersStore.SetFlightDirections([dir]); + this.store$.dispatch(action); + } } diff --git a/src/app/components/timeseries-chart/timeseries-chart.component.ts b/src/app/components/timeseries-chart/timeseries-chart.component.ts index 5fad43e67..045f27982 100644 --- a/src/app/components/timeseries-chart/timeseries-chart.component.ts +++ b/src/app/components/timeseries-chart/timeseries-chart.component.ts @@ -2,7 +2,7 @@ import {Component, ElementRef, Input, OnDestroy, OnInit, QueryList, ViewChild, V import * as d3 from 'd3'; // import * as models from '@models'; import { - debounceTime, Observable, withLatestFrom, + debounceTime, map, Observable, withLatestFrom, // Subject } from 'rxjs'; @@ -104,6 +104,7 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { public lastEndDate: Date = new Date(); public items = Array.from({length: 100000}).map((_, i) => `Item #${i}`); public formulaOverflow = false; + private flightDirection = models.FlightDirection.ASCENDING; // Tyler this is where you would put the series and their best fit formulas public bestFitItems: TimeSeriesFit[] = [ @@ -214,10 +215,19 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { ) ); + this.subs.add( + this.store$.select(filtersStore.getFlightDirections).pipe( + map(dir => dir[0] ?? this.flightDirection) + ).subscribe( + dir => this.flightDirection = dir + ) + ) + } private refreshChart(chartStates: { [key: string]: models.timeseriesChartItemState }): void { - const cache = this.netcdfService.getCache() + const cache = this.netcdfService.getCache(this.flightDirection) + const allPointsData: { point: {}, state: models.timeseriesChartItemState }[] = Object.keys(chartStates).map( wkt => ({ point: cache[wkt], state: chartStates[wkt] }) ); @@ -302,7 +312,6 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { aoi = result.point[key]; } } - this.timeSeriesData = []; for (let key of Object.keys(result.point).filter(x => x !== 'mean' && x !== 'aoi')) { let daDate = new Date(result.point[key].secondary_datetime).valueOf(); @@ -361,7 +370,6 @@ export class TimeseriesChartComponent implements OnInit, OnDestroy { this.averageData = {}; } - this.drawChart(); } diff --git a/src/app/services/map/map.service.ts b/src/app/services/map/map.service.ts index ee6c8bf4e..81174045a 100644 --- a/src/app/services/map/map.service.ts +++ b/src/app/services/map/map.service.ts @@ -77,6 +77,7 @@ export class MapService { private displacementOverview: TileLayer; public displacementOverview$ = new BehaviorSubject(null); private priorityOverview: VectorLayer; + public priorityEnabled$ = new BehaviorSubject(null); private selectClick = new Select({ condition: click, @@ -748,13 +749,11 @@ export class MapService { [models.DisplacementLayerTypes.DISPLACEMENT]: 'DISP', [models.DisplacementLayerTypes.VELOCITY]: 'VEL' } - const dir = apiDirValues[direction]; const layerType = apiDispValues[type]; let base_url = `https://d3g9emy65n853h.cloudfront.net/main/${dir.toLowerCase()}/${layerType.toLowerCase()}`; this.displacementOverview$.next(type); - console.log(type, base_url); this.http.get(`${base_url}/extent.json`).pipe( first() @@ -818,6 +817,10 @@ export class MapService { this.displacementOverview = null; this.displacementOverview$.next(null); } + public setDisplacementType(type) { + this.displacementOverview$.next(type) + } + public setDisplacementLayer(points: { point: Point, seriesNumber: number, color: string }[]) { if (!!this.displacmentLayer) { @@ -895,6 +898,8 @@ export class MapService { format: new GeoJSON({}) }, ) + this.priorityEnabled$.next(flight_dir); + const colorTable = [ 'rgba(174, 174, 174, 0.6)', 'rgba(127, 206, 255, 0.8)', @@ -932,6 +937,8 @@ export class MapService { public disablePriority(): void { this.map.removeLayer(this.priorityOverview); this.priorityOverview = null; + this.priorityEnabled$.next(null); + } public isPriorityEnabled(): boolean { diff --git a/src/app/services/point-history.service.ts b/src/app/services/point-history.service.ts index e64258cfb..33979d46d 100644 --- a/src/app/services/point-history.service.ts +++ b/src/app/services/point-history.service.ts @@ -1,7 +1,8 @@ import { Injectable } from '@angular/core'; +import { timeseriesChartItemState } from '@models'; import { Store } from '@ngrx/store'; import { AppState } from '@store'; -import { addTimeseriesState, removeTimeseriesState } from '@store/charts'; +import { addTimeseriesState, removeTimeseriesState, resetTimeseriesStates } from '@store/charts'; import WKT from 'ol/format/WKT'; import { Point } from 'ol/geom'; @@ -42,6 +43,26 @@ export class PointHistoryService { this.history$.next(this.history); this.savePoints(); } + + public addPoints(states: timeseriesChartItemState[]) { + if(states.length <= 0) { + return + } + for(let state of states) { + const point = state.geoemetry as Point; + this.history.push({point, wkt: state.wkt}); + this.store$.dispatch(addTimeseriesState({item: state})) + } + this.history$.next(this.history); + this.savePoints(); + } + + public clear() { + this.history = []; + this.history$.next(this.history); + this.store$.dispatch(resetTimeseriesStates()) + this.savePoints(); + } public removePoint(index) { // const format = new WKT() const wkt = this.history[index].wkt diff --git a/src/app/services/url-state.service.ts b/src/app/services/url-state.service.ts index d050046db..b755b3c4b 100644 --- a/src/app/services/url-state.service.ts +++ b/src/app/services/url-state.service.ts @@ -7,6 +7,7 @@ import { filter, map, skip, debounceTime, take, distinctUntilChanged } from 'rxj import { AppState } from '@store'; import * as hyp3Store from '@store/hyp3'; +import * as chartsStore from '@store/charts'; import * as scenesStore from '@store/scenes'; import * as mapStore from '@store/map'; import * as filterStore from '@store/filters'; @@ -21,6 +22,9 @@ import { WktService } from './wkt.service'; import { RangeService } from './range.service'; import { PropertyService } from './property.service'; import { ThemingService } from './theming.service'; +import { PointHistoryService } from './point-history.service'; +import WKT from 'ol/format/WKT'; +import { Point } from 'ol/geom'; @Injectable({ @@ -37,7 +41,7 @@ export class UrlStateService { } private kioskMode = false; // for opera displacement - private displacementHostNames = ['displacement.asf.alaska.edu', 'search-displacement.asf.alaska.edu']; + private displacementHostNames = [ 'displacement.asf.alaska.edu', 'search-displacement.asf.alaska.edu']; public isDefaultSearch$ = this.activatedRoute.queryParams.pipe( map(params => { const keys = Object.keys(params) @@ -58,14 +62,19 @@ export class UrlStateService { private router: Router, private prop: PropertyService, private themeService: ThemingService, + private pointHistoryService: PointHistoryService ) { this.kioskMode = this.displacementHostNames.includes(window.location.hostname); - + let params = []; if(this.kioskMode) { this.store$.dispatch(new setSearchKioskMode(true)); + params = [ + ...this.displacementParameters(), + ...this.displacementKioskParameters$() + ] } else { - const params = [ + params = [ ...this.datasetParam(), ...this.mapParameters(), ...this.uiParameters(), @@ -75,20 +84,21 @@ export class UrlStateService { ...this.sbasParameters(), ...this.onDemandParameters(), ...this.eventMonitorParameters(), + ...this.displacementParameters(), ]; + } - this.urlParamNames = params.map(param => param.name); - this.loadLocations = this.urlParamNames.reduce((locations, paramName) => { - locations[paramName] = models.LoadTypes.DEFAULT; + this.urlParamNames = params.map(param => param.name); + this.loadLocations = this.urlParamNames.reduce((locations, paramName) => { + locations[paramName] = models.LoadTypes.DEFAULT; - return locations; - }, {}); - this.urlParams = params.reduce((res, param) => { - res[param.name] = param; + return locations; + }, {}); + this.urlParams = params.reduce((res, param) => { + res[param.name] = param; - return res; - }, {}); - } + return res; + }, {}); this.updateShouldSearch(); } @@ -97,23 +107,21 @@ export class UrlStateService { if (this.kioskMode) { this.store$.dispatch(new SetSearchType(models.SearchType.DISPLACEMENT)) } - else { - this.activatedRoute.queryParams.pipe( + this.activatedRoute.queryParams.pipe( + skip(1), + take(1), + ).subscribe( + params => this.loadStateFrom(params) + ); + + this.urlParamNames.forEach( + paramName => this.urlParams[paramName].source.pipe( skip(1), - take(1), + debounceTime(300) ).subscribe( - params => this.loadStateFrom(params) - ); - - this.urlParamNames.forEach( - paramName => this.urlParams[paramName].source.pipe( - skip(1), - debounceTime(300) - ).subscribe( - this.updateRouteWithParams - ) - ); - } + this.updateRouteWithParams + ) + ); } private updateRouteWithParams = (queryParams: Params): void => { @@ -123,7 +131,6 @@ export class UrlStateService { .filter(key => params[key] !== '' && params[key] !== this.defaultbooleanParams?.[key]) .reduce((res, key) => (res[key] = params[key], res), {}); - this.params = paramsWithValues; this.router.navigate(['.'], { @@ -195,6 +202,8 @@ export class UrlStateService { } else { this.themeService.setTheme('theme-light'); } + } else { + this.store$.dispatch(new scenesStore.SetResultsLoaded(true)) } const action = profile.mapLayer === models.MapLayerTypes.STREET ? new mapStore.SetStreetView() : @@ -314,6 +323,98 @@ export class UrlStateService { }]; } + private displacementParameters() { + return [ + { + name: 'series', + source: this.store$.select(chartsStore.getTimeseriesChartStates).pipe( + map(seriesState => { + return {'series': Object.values(seriesState).map(x => [x.wkt, x.seriesNumber].join(';')).join(',')} + }), + ), + loader: this.loadSeriesState + }, + { + name: 'dispOverview', + source: this.mapService.displacementOverview$.pipe( + map(dispOverview => ({dispOverview})), + ), + loader: this.loadDispOverview + }, + { + name: 'isPriorityEnabled', + source: this.mapService.priorityEnabled$.pipe( + map(isPriorityEnabled => ({isPriorityEnabled})), + ), + loader: this.loadDispPriority + }, + { + name: 'isShowLinesEnabled', + source: this.store$.select(chartsStore.getShowLines).pipe( + map(isShowLinesEnabled => ({isShowLinesEnabled})), + ), + loader: this.loadDispShowLines + }, + { + name: 'isLinearFitEnabled', + source: this.store$.select(chartsStore.getShowLinearFit).pipe( + map(isLinearFitEnabled => ({isLinearFitEnabled})), + ), + loader: this.loadDispShowLinearFit + } + ] + } + + private displacementKioskParameters$() { + return [ + { + name: 'view', + source: this.store$.select(mapStore.getMapView).pipe( + map(view => ({ view })) + ), + loader: this.loadMapView + }, { + name: 'center', + source: this.mapService.center$.pipe( + map( + ({ lon, lat }) => ({ + lon: lon.toFixed(3), + lat: lat.toFixed(3) + }) + ), + map(({ lon, lat }) => ({ center: `${lon},${lat}` })) + ), + loader: this.loadMapCenter + }, { + name: 'zoom', + source: this.mapService.zoom$.pipe( + map(zoom => ({ zoom: zoom.toFixed(3) })) + ), + loader: this.loadMapZoom + }, { + name: 'flightDirs', + source: this.store$.select(filterStore.getFlightDirections).pipe( + map(dirs => dirs.join(',')), + map(flightDirs => ({ flightDirs })) + ), + loader: this.loadFlightDirections + }, + { + name: 'start', + source: this.store$.select(filterStore.getStartDate).pipe( + map(start => ({ start: start === null ? '' : moment.utc( start ).format() })) + ), + loader: this.loadStartDate + }, { + name: 'end', + source: this.store$.select(filterStore.getEndDate).pipe( + map(end => ({ end: end === null ? '' : moment.utc( end ).format() })) + ), + loader: this.loadEndDate + }, + ] + } + private missionParameters(): models.UrlParameter[] { return [{ @@ -745,12 +846,14 @@ export class UrlStateService { private loadFlightDirections = (dirsStr: string): Action => { const directions: models.FlightDirection[] = dirsStr .split(',') - .filter(direction => !Object.values(models.FlightDirection).includes(direction)) + .filter(direction => !Object.values(models.FlightDirection).includes(this.capitalizeFirstLetter(direction.toLowerCase()))) .map(direction => direction); - return new filterStore.SetFlightDirections(directions); }; + private capitalizeFirstLetter(str) { + return str.charAt(0).toUpperCase() + str.slice(1); + } private loadSelectedMission = (mission: string): Action => { return new filterStore.SelectMission(mission); }; @@ -885,4 +988,42 @@ export class UrlStateService { private loadUseCalibrationData = (usingCalibrationData: string): Action => { return new filterStore.setUseCalibrationData(!!usingCalibrationData) } + + private loadSeriesState = (seriesState)=> { + let states: models.timeseriesChartItemState[] = []; + seriesState.split(',').forEach(x => { + const format = new WKT() + let thing = x.split(';') + const point = format.readFeature(thing[0]) as unknown as Point; + states.push({ geoemetry: point, checked: true, seriesNumber: thing[1], wkt: thing[0], name: `Series ${thing[1]}`, linearFit: false }) + }) + this.pointHistoryService.addPoints(states) + return; + } + private loadDispOverview = (dispOverview) => { + this.mapService.setDisplacementType(dispOverview); + return; + } + private loadDispPriority = (isDispPriorityEnabled) => { + if(isDispPriorityEnabled) { + this.mapService.enablePriority(isDispPriorityEnabled); + } + return; + } + private loadDispShowLines = (isShowLinesEnabled) => { + if(isShowLinesEnabled === 'true') { + this.store$.dispatch(chartsStore.showGraphLines()) + } else { + this.store$.dispatch(chartsStore.hideGraphLines()) + } + return; + } + private loadDispShowLinearFit = (isLinearFitEnabled) => { + if(isLinearFitEnabled === 'true') { + this.store$.dispatch(chartsStore.showLinearFit()) + } else { + this.store$.dispatch(chartsStore.hideLinearFit()) + } + return; + } } diff --git a/src/app/store/charts/charts.action.ts b/src/app/store/charts/charts.action.ts index b9ff76abc..0f9e7c2be 100644 --- a/src/app/store/charts/charts.action.ts +++ b/src/app/store/charts/charts.action.ts @@ -5,6 +5,7 @@ export const showGraphLines = createAction('[Chart] show lines') export const hideGraphLines = createAction('[Chart] hide lines') export const reset = createAction('[Chart] reset chart options') export const setTimeseriesStates = createAction('[Chart] sets the checked timeseries', props<{'items': models.timeseriesChartItemState[]}>()) +export const resetTimeseriesStates = createAction('[Chart] Reset timeseries states') export const addTimeseriesState = createAction('[Chart] add checked timeseries', props<{'item': models.timeseriesChartItemState}>()) export const removeTimeseriesState = createAction('[Chart] removes timeseries by wkt', props<{'wkt': string}>()) export const setTimeseriesChecked = createAction('[Chart] set single timeseries as checked/unchecked', props<{'wkt': string, 'checked': boolean}>()) diff --git a/src/app/store/charts/charts.reducer.ts b/src/app/store/charts/charts.reducer.ts index 2ddde7858..be7583e48 100644 --- a/src/app/store/charts/charts.reducer.ts +++ b/src/app/store/charts/charts.reducer.ts @@ -37,6 +37,9 @@ export const chartsReducer = createReducer( }, {}) } )), + on(chartActions.resetTimeseriesStates, (state) => { + return {...state, seriesStates: {}} + }), on(chartActions.addTimeseriesState, (state, { item }) => { const seriesState = { ...state.seriesStates, [item.wkt]: item }