From 712b815c4d4d556afa611eeefba87afe5eec3a76 Mon Sep 17 00:00:00 2001 From: ASF Discovery Machine User <60239462+ASF-Discovery@users.noreply.github.com> Date: Mon, 11 Dec 2023 13:07:37 -0900 Subject: [PATCH 01/23] Updated translations from Phrase Updated locale file en.json Updated locale file es.json Updated locale file de.json --- src/assets/i18n/de.json | 3 +-- src/assets/i18n/en.json | 6 +----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index c051199fd..82ccbd6b4 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -872,6 +872,5 @@ "ZOOM_TO_EVENT": "Zum Ereignis zoomen", "ZOOM_TO_FIT": "Zoomen, um zu passen", "ZOOM_TO_RESULTS": "Zu den Ergebnissen zoomen", - "ZOOM_TO_SCENE": "Auf Szene zoomen", - "ZZ_ANDY_PRAYING": "Wenigstens fluche ich nicht." + "ZOOM_TO_SCENE": "Auf Szene zoomen" } \ No newline at end of file diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 396120d8b..59a321768 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -16,7 +16,6 @@ "ADD_ALL_RESULTS_TO_DOWNLOADS": "Add all results to downloads", "ADD_ALL_RESULTS_TO_ON_DEMAND_QUEUE": "Add all results to On Demand queue", "ADD_BEAM_MODE_TO_SEARCH": "Add beam mode to search", - "ADD_BY_PRODUCT_TYPE": "Add by product type", "ADD_FILE_TO_DOWNLOADS": "Add file to downloads", "ADD_FILE_TO_QUEUE": "Add file to queue", "ADD_JOB": "Add job", @@ -728,7 +727,6 @@ "SORT_BY": "Sort By", "SORT_CRITERIA": "Sort Criteria", "SORT_ORDER": "Sort Order", - "SOURCE_DATA": "Source Data", "START": "Start", "START DATE": "Start Date", "START_ADDING_CUSTOM_PAIR": "Start adding custom pair", @@ -737,7 +735,6 @@ "START_DATE_TIME_BEAM_MODE_PATH_FRAME_FLIGHT_DIRECTION_POLARIZATION_ABSOLUTE_ORBIT_AND_A": "Start Date/Time, Beam Mode, Path, Frame, Flight Direction, Polarization, Absolute Orbit, and a", "START_TIME": "Start Time", "STARTING_CORNER_MOVE_THE_MOUSE_THEN_CLICK_AGAIN_TO_FINISH_THE_BOX": "starting corner, move the mouse, then click again to finish the box.", - "STATIC_LAYER": "Static Layer", "STATISTICS_AND_GITHUB_REPOSITORY": "Statistics and Github Repository", "STOP_ADDING_CUSTOM_PAIR": "Stop adding custom pair", "STOP_DOWNLOAD": "Stop download", @@ -882,6 +879,5 @@ "ZOOM_TO_EVENT": "Zoom to event", "ZOOM_TO_FIT": "Zoom To Fit", "ZOOM_TO_RESULTS": "Zoom to results", - "ZOOM_TO_SCENE": "Zoom to scene", - "ZZ_ANDY_PRAYING": "At least I'm not cursing." + "ZOOM_TO_SCENE": "Zoom to scene" } \ No newline at end of file From 8724889d4d0d4ae674d6a3694e5ef02d2a2247fe Mon Sep 17 00:00:00 2001 From: William Horn Date: Fri, 29 Dec 2023 09:42:14 -0900 Subject: [PATCH 02/23] feat: add lazy loading to on demand search Also improve performance by not making pairs for non relavant search types --- src/app/app.component.ts | 3 +- .../scene-detail/scene-detail.component.ts | 2 +- .../scenes-list-header.component.ts | 37 +- .../scenes-list/scenes-list.component.html | 6 +- .../scenes-list/scenes-list.component.ts | 138 +++++--- src/app/services/pair.service.ts | 66 ++-- src/app/services/product.service.ts | 16 +- src/app/store/scenes/scenes.action.ts | 8 + src/app/store/scenes/scenes.reducer.ts | 186 ++++++---- src/app/store/search/search.action.ts | 8 + src/app/store/search/search.effect.ts | 333 +++++++++++------- 11 files changed, 502 insertions(+), 301 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 8c27114cf..2f4789137 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -506,10 +506,11 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { } return true }), + debounceTime(200), filter(_ => this.searchType !== SearchType.SARVIEWS_EVENTS + && this.searchType !== SearchType.CUSTOM_PRODUCTS && this.searchType !== SearchType.BASELINE && this.searchType !== SearchType.SBAS), - debounceTime(200), map(params => ({...params, output: 'COUNT'})), tap(_ => this.store$.dispatch(new searchStore.SearchAmountLoading()) diff --git a/src/app/components/results-menu/scene-detail/scene-detail.component.ts b/src/app/components/results-menu/scene-detail/scene-detail.component.ts index 640fe5b16..a4d914b44 100644 --- a/src/app/components/results-menu/scene-detail/scene-detail.component.ts +++ b/src/app/components/results-menu/scene-detail/scene-detail.component.ts @@ -119,7 +119,7 @@ export class SceneDetailComponent implements OnInit, OnDestroy { ); const scene$ = this.store$.select(scenesStore.getSelectedScene).pipe( - distinctUntilChanged((previous, current) => previous?.id === current?.id), + distinctUntilChanged(), tap(_ => this.isImageLoading = true) ); diff --git a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts index 42b494f1b..5ef3f26a7 100644 --- a/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts +++ b/src/app/components/results-menu/scenes-list-header/scenes-list-header.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { saveAs } from 'file-saver'; import { combineLatest } from 'rxjs'; -import { debounceTime, filter, map, tap } from 'rxjs/operators'; +import { debounceTime, filter, map, tap, withLatestFrom } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { AppState } from '@store'; @@ -86,22 +86,21 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { public isBurstStack$ = combineLatest([ this.products$, - this.pairService.pairs$, this.store$.select(searchStore.getSearchType), ] ).pipe( - map(([scenes, pairs, currentSearchType]) => (currentSearchType === + map(([scenes, currentSearchType]) => (currentSearchType === this.SearchTypes.BASELINE && scenes?.length > 0 ? scenes[0].metadata.productType === 'BURST': false) - || (currentSearchType === this.SearchTypes.SBAS && - ( - (pairs.pairs?.length > 0 ? pairs.pairs[0][0].metadata.productType === 'BURST' : false) - || (pairs.custom?.length > 0 ? pairs.custom[0][0].metadata.productType === 'BURST' : false) - ) + || (currentSearchType === this.SearchTypes.SBAS ) ) ) - public numPairs$ = this.pairService.pairs$.pipe( + public numPairs$ = + this.store$.select(searchStore.getSearchType).pipe( + filter(searchType => searchType !== models.SearchType.CUSTOM_PRODUCTS), + withLatestFrom(this.pairs$), + map(([_, pairs]) => pairs), filter(pairs => !!pairs), map(pairs => pairs.pairs.length + pairs.custom.length) ); @@ -125,8 +124,8 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { public SBASburstDataProducts$ = combineLatest([ this.burstDataProducts$, - this.pairProducts$] - ).pipe( + this.pairProducts$ + ]).pipe( map(([products, pairs]) => { const pairNames = pairs.map(p => p.name); const output = products.filter(p => pairNames.find(name => name === p.name)); @@ -140,8 +139,8 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { public SBASburstMetadataProducts$ = combineLatest([ this.burstMetadataProducts$, - this.pairProducts$] - ).pipe( + this.pairProducts$ + ]).pipe( map(([products, pairs]) => { const pairNames = pairs.map(p => p.name); const output = products.filter(p => pairNames.find(name => name === p.name)); @@ -206,7 +205,7 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { this.subs.add( this.possibleHyp3JobsService.possibleJobs$ - .subscribe( + .subscribe( possibleJobs => { this.hyp3able = this.hyp3.getHyp3ableProducts(possibleJobs); } @@ -215,13 +214,17 @@ export class ScenesListHeaderComponent implements OnInit, OnDestroy { this.subs.add( this.products$.subscribe(products => { - this.products = products; - this.downloadableProds = this.hyp3.downloadable(products); + this.products = products; + this.downloadableProds = this.hyp3.downloadable(products); }) ); this.subs.add( - this.pairs$.subscribe(({pairs, custom}) => this.pairs = [...pairs, ...custom]) + this.store$.select(searchStore.getSearchType).pipe( + filter(searchType => searchType !== models.SearchType.CUSTOM_PRODUCTS), + withLatestFrom(this.pairs$) + ).subscribe( + ([_, {pairs, custom}]) => this.pairs = [...pairs, ...custom]) ); this.subs.add( diff --git a/src/app/components/results-menu/scenes-list/scenes-list.component.html b/src/app/components/results-menu/scenes-list/scenes-list.component.html index b498a4f22..44d680617 100644 --- a/src/app/components/results-menu/scenes-list/scenes-list.component.html +++ b/src/app/components/results-menu/scenes-list/scenes-list.component.html @@ -17,7 +17,7 @@ searchType === SearchTypes.BASELINE || searchType === SearchTypes.LIST || searchType === SearchTypes.CUSTOM_PRODUCTS"> -
+
+ diff --git a/src/app/components/results-menu/scenes-list/scenes-list.component.ts b/src/app/components/results-menu/scenes-list/scenes-list.component.ts index 9264a6bb4..f2a9fd87d 100644 --- a/src/app/components/results-menu/scenes-list/scenes-list.component.ts +++ b/src/app/components/results-menu/scenes-list/scenes-list.component.ts @@ -2,8 +2,8 @@ import { Component, OnInit, Input, ViewChild, ViewEncapsulation, OnDestroy, AfterContentInit } from '@angular/core'; -import { combineLatest, Observable } from 'rxjs'; -import { tap, withLatestFrom, filter, map, delay, debounceTime, first, distinctUntilChanged } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { tap, withLatestFrom, filter, map, delay, debounceTime, first, distinctUntilChanged, take, switchMap } from 'rxjs/operators'; import { SubSink } from 'subsink'; import { Store } from '@ngrx/store'; @@ -50,6 +50,10 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit private subs = new SubSink(); + private productPageSize = 250; + private numberProductsInList$ = new BehaviorSubject(this.productPageSize); + public numberProductsInList: number; + public breakpoint$ = this.screenSize.breakpoint$; public breakpoints = models.Breakpoints; @@ -88,9 +92,45 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit this.allJobNames = flattened; }); + this.subs.add( + this.store$.select(searchStore.getSearchType).subscribe( + searchType => this.searchType = searchType + ) + ); + + this.subs.add( + combineLatest( + this.numberProductsInList$, + this.store$.select(searchStore.getSearchType) + ).subscribe( + ([num, searchType]) => { + if (searchType === models.SearchType.CUSTOM_PRODUCTS) { + this.numberProductsInList = num; + } else { + this.numberProductsInList = 2^50; + } + + } + ) + ); + const scenes$ = this.scenesService.scenes$; const sortedScenes$: Observable = this.scenesService.sortScenes$(scenes$); + this.store$.select(searchStore.getSearchType).pipe( + filter(searchType => searchType === models.SearchType.CUSTOM_PRODUCTS), + switchMap(_ => { + return scenes$.pipe( + take(1), + withLatestFrom(this.numberProductsInList$) + ); + }) + ).subscribe( + ([scenes, numLoadedOnDemandProducts]) => { + const scenesToLoad = scenes.slice(0, numLoadedOnDemandProducts); + this.store$.dispatch(new searchStore.LoadOnDemandScenesList(scenesToLoad)); + }); + this.subs.add( this.store$.select(scenesStore.getSelectedScene).pipe( withLatestFrom(sortedScenes$), @@ -110,14 +150,14 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit return Math.max(0, sceneIdx - 1); }) ).subscribe( - idx => { - if (!this.selectedFromList) { - this.scrollTo(idx); - } + idx => { + if (!this.selectedFromList) { + this.scrollTo(idx); + } - this.selectedFromList = false; - } - ) + this.selectedFromList = false; + } + ) ); this.subs.add( @@ -131,38 +171,40 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit return Math.max(0, sceneIdx - 1); }) ).subscribe( - idx => { - if (!this.selectedFromList) { - this.scrollTo(idx); - } + idx => { + if (!this.selectedFromList) { + this.scrollTo(idx); + } - this.selectedFromList = false; - } - ) + this.selectedFromList = false; + } + ) ); this.subs.add( sortedScenes$.pipe( debounceTime(250), - distinctUntilChanged() + distinctUntilChanged(), ).subscribe( - scenes => this.scenes = scenes - ) + scenes => { + this.scenes = scenes; + } + ) ); this.subs.add( this.eventMonitoringService.filteredSarviewsEvents$().pipe( filter(_ => this.searchType === this.SearchTypes.SARVIEWS_EVENTS), ).subscribe( - events => { - this.sarviewsEvents = events; + events => { + this.sarviewsEvents = events; - const eventIds = events.map(event => event.event_id); - if (!eventIds.includes(this.selectedEvent) && eventIds.length > 0 && !!this.selectedEvent) { - this.store$.dispatch(new scenesStore.SetSelectedSarviewsEvent(eventIds[0])); + const eventIds = events.map(event => event.event_id); + if (!eventIds.includes(this.selectedEvent) && eventIds.length > 0 && !!this.selectedEvent) { + this.store$.dispatch(new scenesStore.SetSelectedSarviewsEvent(eventIds[0])); + } } - } - ) + ) ); @@ -183,20 +225,14 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit ) ); - this.subs.add( - this.store$.select(searchStore.getSearchType).subscribe( - searchType => this.searchType = searchType - ) - ); - const baselineReference$ = combineLatest( this.store$.select(scenesStore.getScenes), this.store$.select(scenesStore.getMasterName), this.store$.select(searchStore.getSearchType) ).pipe( - map( - ([scenes, referenceName, searchType]) => { - if (searchType === models.SearchType.BASELINE && !!referenceName) { + map( + ([scenes, referenceName, searchType]) => { + if (searchType === models.SearchType.BASELINE && !!referenceName) { const referenceSceneIdx = scenes.findIndex(scene => scene.name === referenceName); if (referenceSceneIdx !== -1) { @@ -205,21 +241,21 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit } else { return null; } - } - ), + } + ), ); - + this.store$.select(scenesStore.getAllSceneProducts).pipe( withLatestFrom(baselineReference$) ) - - .subscribe( - ([searchScenes, baselineReference]) => { - this.hyp3ableByScene = {}; - Object.entries(searchScenes).forEach(([groupId, products]) => { - const possibleJobs = []; + .subscribe( + ([searchScenes, baselineReference]) => { + this.hyp3ableByScene = {}; + + Object.entries(searchScenes).forEach(([groupId, products]) => { + const possibleJobs = []; (products).forEach(product => { possibleJobs.push([product]); @@ -399,6 +435,20 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit } return scene.groupId; } + + public onLoadMoreCustomProducts() { + const oldNumProducts = this.numberProductsInList; + const newNumProducts = this.numberProductsInList + this.productPageSize; + + const scenesToLoad = this.scenes.slice(oldNumProducts, newNumProducts); + + this.store$.dispatch(new searchStore.LoadOnDemandScenesList(scenesToLoad)); + + this.numberProductsInList$.next( + newNumProducts + ); + } + ngOnDestroy() { this.subs.unsubscribe(); } diff --git a/src/app/services/pair.service.ts b/src/app/services/pair.service.ts index 9db7f31bd..9eea988cc 100644 --- a/src/app/services/pair.service.ts +++ b/src/app/services/pair.service.ts @@ -9,8 +9,9 @@ import { getScenes, getCustomPairs } from '@store/scenes/scenes.reducer'; import { getTemporalRange, getPerpendicularRange, getDateRange, DateRangeState, getSeason, getSBASOverlapThreshold } from '@store/filters/filters.reducer'; +import { getSearchType } from '@store/search/search.reducer'; -import { CMRProduct, CMRProductPair, ColumnSortDirection, Range, SBASOverlap, } from '@models'; +import { CMRProduct, CMRProductPair, ColumnSortDirection, Range, SBASOverlap, SearchType } from '@models'; import { MapService } from './map/map.service'; import { WktService } from './wkt.service'; @@ -27,9 +28,8 @@ export class PairService { constructor( private store$: Store, private mapService: MapService, - private wktService: WktService) { } - - + private wktService: WktService + ) { } public pairs$: Observable<{ custom: CMRProductPair[], pairs: CMRProductPair[] }> = combineLatest([ this.store$.select(getScenes).pipe( @@ -48,18 +48,26 @@ export class PairService { this.mapService.searchPolygon$.pipe( map(wkt => !!wkt ? this.wktService.wktToFeature(wkt, this.mapService.epsg()) : null) ), + this.store$.select(getSearchType), ]).pipe( debounceTime(50), distinctUntilChanged(), - map(([scenes, customPairs, temporalRange, perpendicular, dateRange, season, overlap, polygon]) => { - const pairs = this.makePairs(scenes, - temporalRange, - perpendicular, - dateRange, - season, - overlap, - polygon - ) + map(([scenes, customPairs, temporalRange, perpendicular, dateRange, season, overlap, polygon, searchType]) => { + let pairs: CMRProductPair[]; + + if (searchType === SearchType.BASELINE || searchType === SearchType.SBAS) { + pairs = this.makePairs(scenes, + temporalRange, + perpendicular, + dateRange, + season, + overlap, + polygon + ); + } else { + pairs = []; + } + return { pairs: [...pairs], custom: [...customPairs] @@ -67,6 +75,7 @@ export class PairService { }), shareReplay({ refCount: true, bufferSize: 1 }), ) + public productsFromPairs$: Observable = this.pairs$.pipe( map(({ custom, pairs }) => { const prods = Array.from([...custom, ...pairs].reduce((products, pair) => { @@ -79,6 +88,7 @@ export class PairService { return prods; }) ); + private makePairs(scenes: CMRProduct[], tempThreshold: Range, perpThreshold, dateRange: DateRangeState, season: Range, @@ -94,7 +104,7 @@ export class PairService { if (!!dateRange.end) { endDateExtrema = new Date(dateRange.end.toISOString()); } - let intersectionMethod; + let intersectionMethod: any; if (!!aoi) { const geometryType = aoi.getGeometry().getType(); @@ -163,7 +173,7 @@ export class PairService { if ( p1Center.lat > Math.max(p2Bounds[0].lat, p2Bounds[1].lat) || - p1Center.lat < Math.min(p2Bounds[2].lat, p2Bounds[3].lat) + p1Center.lat < Math.min(p2Bounds[2].lat, p2Bounds[3].lat) ) { continue; } @@ -206,8 +216,8 @@ export class PairService { scenes = this.hyp3able(scenes); scenes = scenes.filter(scene => scene.id.includes('SLC') - && !scene.id.includes('METADATA') - && scene.metadata.temporal != null + && !scene.id.includes('METADATA') + && scene.metadata.temporal != null ); const totalDays = temporalRange.end - temporalRange.start; @@ -238,20 +248,20 @@ export class PairService { if (season.start < season.end) { return ( season.start <= this.getDayOfYear(P1StartDate) - && season.end >= this.getDayOfYear(P1EndDate) - && season.start <= this.getDayOfYear(P2StartDate) - && season.end >= this.getDayOfYear(P2EndDate) + && season.end >= this.getDayOfYear(P1EndDate) + && season.start <= this.getDayOfYear(P2StartDate) + && season.end >= this.getDayOfYear(P2EndDate) ); } else { return !( season.start >= this.getDayOfYear(P1StartDate) - && season.start >= this.getDayOfYear(P1EndDate) - && season.end <= this.getDayOfYear(P1StartDate) - && season.end <= this.getDayOfYear(P1EndDate) - && season.start >= this.getDayOfYear(P2StartDate) - && season.start >= this.getDayOfYear(P2EndDate) - && season.end <= this.getDayOfYear(P2StartDate) - && season.end <= this.getDayOfYear(P2EndDate) + && season.start >= this.getDayOfYear(P1EndDate) + && season.end <= this.getDayOfYear(P1StartDate) + && season.end <= this.getDayOfYear(P1EndDate) + && season.start >= this.getDayOfYear(P2StartDate) + && season.start >= this.getDayOfYear(P2EndDate) + && season.end <= this.getDayOfYear(P2StartDate) + && season.end <= this.getDayOfYear(P2EndDate) ); } } @@ -321,6 +331,4 @@ export class PairService { } return !(checked.size === points.size) } - - } diff --git a/src/app/services/product.service.ts b/src/app/services/product.service.ts index ebd9cfb53..fb91503e0 100644 --- a/src/app/services/product.service.ts +++ b/src/app/services/product.service.ts @@ -7,7 +7,7 @@ import * as models from '@models'; providedIn: 'root' }) export class ProductService { - public fromResponse = (resp: any) => { + public fromResponse = (resp: any): models.CMRProduct[] => { const products = (resp.results || []) .map( (g: any): models.CMRProduct => { @@ -100,7 +100,7 @@ export class ProductService { (dateString: string): moment.Moment => { return moment.utc(dateString); } - + private getSubproducts(product: models.CMRProduct): models.CMRProduct[] { if (product.metadata.productType === 'BURST') { return [this.burstXMLFromScene(product)] @@ -127,7 +127,7 @@ export class ProductService { parentID: product.id, }, } as models.CMRProduct; - + return p; } @@ -148,14 +148,15 @@ export class ProductService { private operaSubproductsFromScene(product: models.CMRProduct) { if (!!product.metadata.opera?.validityStartDate) { - product.metadata.opera.validityStartDate = this.fromCMRDate((product.metadata.opera?.validityStartDate as unknown) as string) + product.metadata.opera.validityStartDate = this.fromCMRDate( + (product.metadata.opera?.validityStartDate as unknown) as string) } let products = [] - + let reg = product.downloadUrl.split(/(_v[0-9]\.[0-9]){1}(\.(\w*)|(_(\w*(_*))*.))*/); let file_suffix = !!reg[3] ? reg[3] : reg[5] product.productTypeDisplay = this.operaProductTypeDisplays[file_suffix.toLowerCase()] - + const thumbnail_index = product.browses.findIndex(url => url.toLowerCase().includes('thumbnail')) if (thumbnail_index !== -1) { product.thumbnail = product.browses.splice(thumbnail_index, 1)[0]; @@ -169,7 +170,7 @@ export class ProductService { const productTypeDisplay = this.operaProductTypeDisplays[file_suffix.toLowerCase()]; const fileID = p.split('/').slice(-1)[0] - + let subproduct = { ...product, downloadUrl: p, @@ -200,5 +201,4 @@ export class ProductService { } ) } - } diff --git a/src/app/store/scenes/scenes.action.ts b/src/app/store/scenes/scenes.action.ts index 773da90f7..67c0ee523 100644 --- a/src/app/store/scenes/scenes.action.ts +++ b/src/app/store/scenes/scenes.action.ts @@ -18,6 +18,8 @@ export enum ScenesActionType { ERROR_LOADING_UNZIPPED = '[Scenes] Error loading unzipped', CLOSE_ZIP_CONTENTS = '[Scenes] Close Zip Contents', + ADD_CMR_DATA_TO_ON_DEMAND_JOBS = '[Scenes] Add CMR Data to On Demand Jobs', + SET_SELECTED_SCENE = '[Scenes] Set Selected Scene', SET_SELECTED_PAIR = '[Scenes] Set Selected Pair', SET_SELECTED_SARVIEWS_EVENT = '[SARViews] Set Selected SARViews Event', @@ -168,6 +170,11 @@ export class SetImageBrowseProducts implements Action { constructor(public payload: {[product_id in string]: PinnedProduct}) {} } +export class AddCmrDataToOnDemandScenes implements Action { + public readonly type = ScenesActionType.ADD_CMR_DATA_TO_ON_DEMAND_JOBS; + + constructor(public payload: CMRProduct[]) {} +} export type ScenesActions = | SetScenes @@ -192,4 +199,5 @@ export type ScenesActions = | SetSelectedSarviewsEvent | SetSarviewsEventProducts | SetSelectedSarviewProduct + | AddCmrDataToOnDemandScenes | SetImageBrowseProducts; diff --git a/src/app/store/scenes/scenes.reducer.ts b/src/app/store/scenes/scenes.reducer.ts index 33b095055..8926b8726 100644 --- a/src/app/store/scenes/scenes.reducer.ts +++ b/src/app/store/scenes/scenes.reducer.ts @@ -2,7 +2,7 @@ import { createFeatureSelector, createSelector } from '@ngrx/store'; import { ScenesActionType, ScenesActions } from './scenes.action'; -import { CMRProduct, UnzippedFolder, ColumnSortDirection, SarviewsEvent, SarviewsProduct, opera_s1 } from '@models'; +import { CMRProduct, UnzippedFolder, ColumnSortDirection, SarviewsEvent, SarviewsProduct, opera_s1, Hyp3JobType } from '@models'; import { PinnedProduct } from '@services/browse-map.service'; import { createSelectorFactory, defaultMemoize } from '@ngrx/store'; @@ -67,12 +67,18 @@ export function scenesReducer(state = initState, action: ScenesActions): ScenesS switch (action.type) { case ScenesActionType.SET_SCENES: { let subproducts: CMRProduct[] = [] - let searchResults = action.payload.products.map(p => - p.metadata.productType === 'BURST' ? ({...p, productTypeDisplay: 'Single Look Complex (BURST)'}) as CMRProduct : p) + p.metadata.productType === 'BURST' ? ({ + ...p, + productTypeDisplay: 'Single Look Complex (BURST)' + }) as CMRProduct : p); + const ungrouped_product_types = [ + ...opera_s1.productTypes, + {apiValue: 'BURST'}, + {apiValue: 'BURST_XML'} + ].map(m => m.apiValue) - const ungrouped_product_types = [...opera_s1.productTypes, {apiValue: 'BURST'}, {apiValue: 'BURST_XML'}].map(m => m.apiValue) for (let product of searchResults) { if(product.metadata.subproducts.length > 0) { for (let subproduct of product.metadata.subproducts) { @@ -84,44 +90,33 @@ export function scenesReducer(state = initState, action: ScenesActions): ScenesS searchResults = searchResults.concat(subproducts) const products = searchResults - .reduce((total, product) => { - total[product.id] = product; - - return total; - }, {}); - - let productGroups: {[id: string]: string[]} = {} - let scenes: {[id: string]: string[]} = {} - // const productIDs = searchResults.reduce((total, product) => { - // total[product.metadata.productType] = product; - - // return total; - // }, {}); - // if (Object.keys(productIDs).length <= 2 && Object.keys(productIDs)[0].toUpperCase() === 'BURST') { - // productGroups = searchResults.reduce((total, product) => { - // const scene = total[product.name] || []; - - // total[product.name] = [...scene, product.id]; - // return total; - // }, {}) - // } else { - productGroups = searchResults.reduce((total, product) => { - let groupCriteria = product.groupId; - if (product.metadata.subproducts.length > 0) { + .reduce((total, product) => { + total[product.id] = product; + + return total; + }, {}); + + let productGroups: {[id: string]: string[]} = {}; + let scenes: {[id: string]: string[]} = {}; + + productGroups = searchResults.reduce((total, product) => { + let groupCriteria = product.groupId; + + if (product.metadata.subproducts.length > 0) { + groupCriteria = product.id; + } else if(ungrouped_product_types.includes(product.metadata.productType)) { + if(isSubProduct(product)) { + groupCriteria = product.metadata.parentID; + } else { groupCriteria = product.id; - } else if(ungrouped_product_types.includes(product.metadata.productType)) { - if(isSubProduct(product)) { - groupCriteria = product.metadata.parentID; - } else { - groupCriteria = product.id; - } } - const scene = total[groupCriteria] || []; + } + const scene = total[groupCriteria] || []; + + total[groupCriteria] = [...scene, product.id]; + return total; + }, {}); - total[groupCriteria] = [...scene, product.id]; - return total; - }, {}); - // } for (const [groupId, productNames] of Object.entries(productGroups)) { (productNames).sort( @@ -145,6 +140,61 @@ export function scenesReducer(state = initState, action: ScenesActions): ScenesS }; } + case ScenesActionType.ADD_CMR_DATA_TO_ON_DEMAND_JOBS: { + const cmrData = action.payload.reduce((products, product) => { + products[product.name] = product; + return products; + }, {}); + + const products = {...state.products}; + + Object.values(products).forEach(jobProduct => { + const product = cmrData[jobProduct.name]; + + if(!!product) { + let job = { + ...jobProduct.metadata.job, + job_parameters: { + ...jobProduct.metadata.job.job_parameters, + } + }; + const jobFile = !!job.files ? + job.files[0] : + { size: -1, url: '', filename: product.name }; + + const scene_keys = job.job_parameters.granules; + job.job_parameters.scenes = []; + for (const scene_key of scene_keys) { + job.job_parameters.scenes.push(cmrData[scene_key]); + } + + const combinedProduct: any = { + ...product, + browses: job.browse_images ? job.browse_images : ['assets/no-browse.png'], + thumbnail: job.thumbnail_images ? job.thumbnail_images[0] : 'assets/no-thumb.png', + productTypeDisplay: `${job.job_type}, ${product.metadata.productType} `, + downloadUrl: jobFile.url, + bytes: jobFile.size, + groupId: job.job_id, + id: job.job_id, + metadata: { + ...product.metadata, + fileName: jobFile.filename || '', + productType: job.job_type, + job + }, + }; + + products[combinedProduct.id] = combinedProduct; + } + }); + + return { + ...state, + products + }; + } + case ScenesActionType.SET_SELECTED_SCENE: { return { ...state, @@ -347,9 +397,9 @@ export const allScenesFrom = (scenes: {[id: string]: string[]}, products) => { .map(group => { const browse = group - .map(name => products[name]) - .filter(hasNoBrowse) - .pop(); + .map(name => products[name]) + .filter(hasNoBrowse) + .pop(); return browse ? browse : products[group[0]]; }); @@ -358,8 +408,8 @@ export const allScenesFrom = (scenes: {[id: string]: string[]}, products) => { const hasNoBrowse = (product) => { return ( !!product.browses && - product.browses.length > 0 && - !product.browses[0].includes('no-browse.png') + product.browses.length > 0 && + !product.browses[0].includes('no-browse.png') ); }; @@ -374,27 +424,27 @@ export const allScenesWithBrowse = (scenes: {[id: string]: string[]}, products) function arrayEquals(a, b) { return Array.isArray(a) && - Array.isArray(b) && - a.length === b.length && - a.toString() === b.toString() && - a.every((value, index) => { - if(Array.isArray(value) && Array.isArray(b[index])) { - return arrayEquals(value, b[index]) - } else { + Array.isArray(b) && + a.length === b.length && + a.toString() === b.toString() && + a.every((value, index) => { + if(Array.isArray(value) && Array.isArray(b[index])) { + return arrayEquals(value, b[index]) + } else { value.id === b[index].id - } } - ) + } + ) } export const createArraySelector = - createSelectorFactory( - (projectionFn) => - defaultMemoize( - projectionFn, - arrayEquals, - arrayEquals - ) - ); +createSelectorFactory( + (projectionFn) => + defaultMemoize( + projectionFn, + arrayEquals, + arrayEquals + ) +); export const getScenes = createArraySelector( getScenesState, @@ -543,7 +593,7 @@ export const getAllSceneProducts = createSelector( Object.entries(state.scenes).forEach( ([sceneId, scene]) => { const products = scene - .map(name => state.products[name]); + .map(name => state.products[name]); allSceneProducts[sceneId] = products; } @@ -564,8 +614,8 @@ export const getSelectedSarviewsEvent = createSelector( ); export const getSelectedSarviewsProduct = createSelector( - getScenesState, - (state: ScenesState) => state.selectedSarviewsProduct + getScenesState, + (state: ScenesState) => state.selectedSarviewsProduct ); export const getUnzipLoading = createSelector( @@ -728,15 +778,15 @@ export const getSelectedSarviewsEventProducts = createSelector( export const getImageBrowseProducts = createSelector( getScenesState, state => { - const output: {[product_id in string]: PinnedProduct} = Object.keys(state.pinnedProductBrowses).reduce( - (out, product_id) => { + const output: {[product_id in string]: PinnedProduct} = Object.keys(state.pinnedProductBrowses).reduce( + (out, product_id) => { const temp = out; temp[product_id] = {... state.pinnedProductBrowses[product_id]}; return temp; - } + } , {} as {[product_id in string]: PinnedProduct}); - return output; + return output; } ); diff --git a/src/app/store/search/search.action.ts b/src/app/store/search/search.action.ts index dff1ad111..08d206d25 100644 --- a/src/app/store/search/search.action.ts +++ b/src/app/store/search/search.action.ts @@ -20,6 +20,7 @@ export enum SearchActionType { SEARCH_AMOUNT_LOADING = '[Search] Search Amount Is Loading', SET_SEARCH_TYPE = '[UI] Set Search Type', SET_SEARCH_TYPE_AFTER_SAVE = '[UI] Set Search Type After Save', + LOAD_ON_DEMAND_SCENES_LIST = '[Search] Load on Demand Scenes List', SARVIEWS_SEARCH_RESPONSE = '[Search] SARViews Search Response', MAKE_EVENT_PRODUCT_CMR_SEARCH = '[Search] Make a search for CMR Products with SARVIEWS Products', @@ -103,6 +104,12 @@ export class SetSearchTypeAfterSave implements Action { constructor(public payload: SearchType) {} } +export class LoadOnDemandScenesList implements Action { + public readonly type = SearchActionType.LOAD_ON_DEMAND_SCENES_LIST; + + constructor(public payload: CMRProduct[]) {} +} + export class SetSearchOutOfDate implements Action { public readonly type = SearchActionType.SET_SEARCH_OUT_OF_DATE; @@ -122,6 +129,7 @@ export type SearchActions = | SetNextJobsUrl | Hyp3BatchResponse | SetSearchType + | LoadOnDemandScenesList | SetSearchTypeAfterSave | SarviewsEventsResponse | SetSearchOutOfDate; diff --git a/src/app/store/search/search.effect.ts b/src/app/store/search/search.effect.ts index d7e847e9a..000913730 100644 --- a/src/app/store/search/search.effect.ts +++ b/src/app/store/search/search.effect.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import * as moment from 'moment'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Store, Action } from '@ngrx/store'; @@ -19,7 +20,7 @@ import * as uiStore from '@store/ui'; import * as services from '@services'; import { - SearchActionType, + SearchActionType, LoadOnDemandScenesList, SearchResponse, SearchError, CancelSearch, SearchCanceled } from './search.action'; import { getIsCanceled, getareResultsOutOfDate, getSearchType } from './search.reducer'; @@ -41,7 +42,6 @@ export class SearchEffects { format: new GeoJSON(), }); - constructor( private actions$: Actions, private store$: Store, @@ -52,7 +52,6 @@ export class SearchEffects { private sarviewsService: services.SarviewsEventsService, private http: HttpClient, private notificationService: services.NotificationService, - // private environmentService: services.EnvironmentService, ) { } public clearMapInteractionModeOnSearch = createEffect(() => this.actions$.pipe( @@ -131,7 +130,7 @@ export class SearchEffects { map(_ => new CancelSearch()) )); - cancelSearchonOnPanelOpen = createEffect(() => this.actions$.pipe( + public cancelSearchonOnPanelOpen = createEffect(() => this.actions$.pipe( ofType(uiStore.UIActionType.OPEN_FILTERS_MENU), switchMap(_ => [ new filtersStore.StoreCurrentFilters(), @@ -143,7 +142,6 @@ export class SearchEffects { public searchResponse = createEffect(() => this.actions$.pipe( ofType(SearchActionType.SEARCH_RESPONSE), switchMap(action => { - let output : any[] = [ new scenesStore.SetScenes({ products: action.payload.files, @@ -166,6 +164,39 @@ export class SearchEffects { ) )); + public onLoadOnDemandScenesList = createEffect(() => this.actions$.pipe( + ofType(SearchActionType.LOAD_ON_DEMAND_SCENES_LIST), + switchMap((action) => { + const products = action.payload; + + const granuleNames = products.reduce((names, prod) => { + const scenes = prod.metadata.job.job_parameters.scenes; + + if (!!scenes) { + const gNames = scenes + .filter(g => !!g && 'name' in g) + .map(g => g.name); + + return names.concat(gNames); + } else { + return names.push(prod.name); + } + }, []); + + const params = {'granule_list': (granuleNames).join(',')}; + + return this.asfApiService.query(params); + }), + map(results => this.productService.fromResponse(results). + filter(product => { + return !product.metadata.productType.includes('METADATA'); + }) + ), + map(results => { + return new scenesStore.AddCmrDataToOnDemandScenes(results); + }) + )); + public hyp3BatchResponse = createEffect(() => this.actions$.pipe( ofType(SearchActionType.HYP3_BATCH_RESPONSE), switchMap(action => [ @@ -241,8 +272,8 @@ export class SearchEffects { withLatestFrom(this.store$.select(getAreResultsLoaded)), filter(([[[_, searchtype], outOfdate], loaded]) => !outOfdate && searchtype === models.SearchType.DATASET && loaded), ).pipe( - map(_ => new SetSearchOutOfDate(true)) - )); + map(_ => new SetSearchOutOfDate(true)) + )); public setSearchUpToDate = createEffect(() => this.actions$.pipe( ofType(SearchActionType.MAKE_SEARCH, @@ -264,27 +295,27 @@ export class SearchEffects { ([params]) => forkJoin( this.asfApiService.query(params) ).pipe( - withLatestFrom(combineLatest([ - this.store$.select(getSearchType), - this.store$.select(getIsCanceled)] - )), - map(([[response], [searchType, isCanceled]]) => - !isCanceled ? - new SearchResponse({ - files: this.productService.fromResponse(response), - searchType - }) : - new SearchCanceled() - ), - catchError( - (err: HttpErrorResponse) => { - if (err.status !== 400) { - return of(new SearchError(`Unknown Error`)); + withLatestFrom(combineLatest([ + this.store$.select(getSearchType), + this.store$.select(getIsCanceled)] + )), + map(([[response], [searchType, isCanceled]]) => + !isCanceled ? + new SearchResponse({ + files: this.productService.fromResponse(response), + searchType + }) : + new SearchCanceled() + ), + catchError( + (err: HttpErrorResponse) => { + if (err.status !== 400) { + return of(new SearchError(`Unknown Error`)); + } + return EMPTY; } - return EMPTY; - } - ), - )) + ), + )) ); public asfApiBaselineQuery$(): Observable { @@ -320,12 +351,69 @@ export class SearchEffects { ); } + private getAllGranulesFromJobs(jobs: any) { + return jobs.reduce( + (granuleNames, job) => { + return granuleNames.concat(job.job_parameters.granules); + }, + []) + } + + private dummyProducts$(granuleNames: string[]) { + const dummyProducts = granuleNames.map(granuleName => { + return { + ...this.dummyProduct(), + name: granuleName + }; + }) + + return of(dummyProducts); + } + + private onDemandGranuleList$(jobsRes, latestScenes) { + const jobs = jobsRes.hyp3Jobs; + + const granuleNames = this.getAllGranulesFromJobs(jobs); + + const asfApiListQuery = this.dummyProducts$(granuleNames); + + return asfApiListQuery.pipe( + map(products => { + return products + .reduce((prods, p) => { + prods[p.name] = p; + return products; + }, {}) + } + ), + map(products => { + return this.hyp3JobToProducts(jobs, products) + }), + withLatestFrom(this.store$.select(getIsCanceled)), + map(([products, isCanceled]) => + !isCanceled ? + new SearchResponse({ + files: latestScenes.concat(products), + totalCount: +products.length, + searchType: models.SearchType.CUSTOM_PRODUCTS, + next: jobsRes.next + }) : + new SearchCanceled() + ), + catchError( + _ => { + return of(new SearchError(`Error loading search results`)); + } + ), + ); + } + + private customProductsQuery$(): Observable { return this.hyp3Service.getJobs$().pipe( switchMap( (jobsRes: { hyp3Jobs: models.Hyp3Job[], next: string }) => { - const jobs = jobsRes.hyp3Jobs; - if (jobs.length === 0) { + if (jobsRes.hyp3Jobs.length === 0) { return of(new SearchResponse({ files: [], totalCount: 0, @@ -333,41 +421,7 @@ export class SearchEffects { })); } - const granules = jobs.map( - job => { - return job.job_parameters.granules.join(','); - } - ).join(','); - - return this.asfApiService.query({ 'granule_list': granules }).pipe( - map(results => this.productService.fromResponse(results) - .filter(product => { - return !product.metadata.productType.includes('METADATA') || - !product.metadata.productType.includes('BURST_XML'); - }) - .reduce((products, product) => { - products[product.name] = product; - return products; - }, {}) - ), - map(products => this.hyp3JobToProducts(jobs, products)), - withLatestFrom(this.store$.select(getIsCanceled)), - map(([products, isCanceled]) => - !isCanceled ? - new SearchResponse({ - files: products, - totalCount: +products.length, - searchType: models.SearchType.CUSTOM_PRODUCTS, - next: jobsRes.next - }) : - new SearchCanceled() - ), - catchError( - _ => { - return of(new SearchError(`Error loading search results`)); - } - ), - ); + return this.onDemandGranuleList$(jobsRes, []); } ), ); @@ -377,8 +431,7 @@ export class SearchEffects { return this.hyp3Service.getJobsByUrl$(next).pipe( switchMap( (jobsRes) => { - const jobs = jobsRes.hyp3Jobs; - if (jobs.length === 0) { + if (jobsRes.hyp3Jobs.length === 0) { return of(new Hyp3BatchResponse({ files: [], totalCount: 0, @@ -387,40 +440,7 @@ export class SearchEffects { })); } - const granules = jobs.map( - job => { - return job.job_parameters.granules.join(','); - } - ).join(','); - - const collections = this.asfApiService.collections(); - - return this.asfApiService.query({ 'granule_list': granules, 'collections': collections.join(',') }).pipe( - map(results => this.productService.fromResponse(results) - .filter(product => !product.metadata.productType.includes('METADATA')) - .reduce((products, product) => { - products[product.name] = product; - return products; - }, {}) - ), - map(products => this.hyp3JobToProducts(jobs, products)), - withLatestFrom(this.store$.select(getIsCanceled)), - map(([products, isCanceled]) => - !isCanceled ? - new Hyp3BatchResponse({ - files: latestScenes.concat(products), - totalCount: +products.length, - searchType: models.SearchType.CUSTOM_PRODUCTS, - next: jobsRes.next - }) : - new SearchCanceled() - ), - catchError( - _ => { - return of(new SearchError(`Error loading next batch of On Demand results`)); - } - ), - ); + return this.onDemandGranuleList$(jobsRes, latestScenes); } ), ); @@ -435,43 +455,92 @@ export class SearchEffects { private hyp3JobToProducts(jobs, products) { const virtualProducts = jobs - .filter(job => products[job.job_parameters.granules[0]]) - .map(job => { - - const product = products[job.job_parameters.granules[0]]; - const jobFile = !!job.files ? - job.files[0] : - { size: -1, url: '', filename: product.name }; - - const scene_keys = job.job_parameters.granules; - job.job_parameters.scenes = []; - for (const scene_key of scene_keys) { - job.job_parameters.scenes.push(products[scene_key]); - } + .filter(job => products[job.job_parameters.granules[0]]) + .map(job => { + const product = products[job.job_parameters.granules[0]]; + const jobFile = !!job.files ? + job.files[0] : + { size: -1, url: '', filename: product.name }; + + const scene_keys = job.job_parameters.granules; + job.job_parameters.scenes = []; + for (const scene_key of scene_keys) { + job.job_parameters.scenes.push(products[scene_key]); + } - const jobProduct = { - ...product, - browses: job.browse_images ? job.browse_images : ['assets/no-browse.png'], - thumbnail: job.thumbnail_images ? job.thumbnail_images[0] : 'assets/no-thumb.png', - productTypeDisplay: `${job.job_type}, ${product.metadata.productType} `, - downloadUrl: jobFile.url, - bytes: jobFile.size, - groupId: job.job_id, - id: job.job_id, - metadata: { - ...product.metadata, - fileName: jobFile.filename || '', - productType: job.job_type, - job - }, - }; - - return jobProduct - }); + const jobProduct = { + ...product, + browses: job.browse_images ? job.browse_images : ['assets/no-browse.png'], + thumbnail: job.thumbnail_images ? job.thumbnail_images[0] : 'assets/no-thumb.png', + productTypeDisplay: `${job.job_type}, ${product.metadata.productType} `, + downloadUrl: jobFile.url, + bytes: jobFile.size, + groupId: job.job_id, + id: job.job_id, + metadata: { + ...product.metadata, + fileName: jobFile.filename || '', + productType: job.job_type, + job + }, + }; + + return jobProduct + }); return virtualProducts; } + private dummyProduct() { + // "2023-11-21T18:03:43+00:00" + return { + "name": "", + "productTypeDisplay": "", + "file": "", + "id": "", + "downloadUrl": "", + "bytes": 0, + "dataset": "", + "browses": [ + "/assets/no-browse.png" + ], + "thumbnail": "/assets/no-thumb.png", + "groupId": "", + "isUnzippedFile": false, + "metadata": { + "date": moment.utc("1970-01-01T00:00:00+00:00"), + "stopDate": moment.utc("1970-01-01T00:00:00+00:00"), + "polygon": "POLYGON ((0 0, 0 0, 0 0, 0 0, 0 0))", + "productType": "", + "beamMode": "", + "polarization": "", + "flightDirection": "", + "path": 0, + "frame": 0, + "absoluteOrbit": [ + 0 + ], + "faradayRotation": 0, + "offNadirAngle": 0, + "instrument": "", + "pointingAngle": null, + "missionName": null, + "flightLine": null, + "stackSize": null, + "perpendicular": null, + "temporal": null, + "canInSAR": true, + "job": null, + "fileName": null, + "burst": null, + "opera": null, + "pgeVersion": 0, + "subproducts": [], + "parentID": null + } + }; + } + private findCountries(shapeString: string) { const parser = new WKT(); const feature = parser.readFeature(shapeString); From 0532d4d895203ab11087ea698df90530f9361a85 Mon Sep 17 00:00:00 2001 From: William Horn Date: Wed, 3 Jan 2024 06:11:48 -0900 Subject: [PATCH 03/23] feat: add user id param to on demand search this is to load other users on demand jobs --- .../custom-products-filters.component.html | 4 +- .../custom-products-filters.module.ts | 2 + src/app/components/header/header.module.ts | 4 +- .../hyp3-header/hyp3-header.component.html | 6 +- .../scenes-list/scenes-list.component.ts | 28 +- .../on-demand-user-selector/index.ts | 2 + .../on-demand-user-selector.component.html | 10 + .../on-demand-user-selector.component.scss | 0 .../on-demand-user-selector.component.ts | 41 ++ .../on-demand-user-selector.module.ts | 28 + .../project-name-selector.component.ts | 1 - src/app/services/hyp3.service.ts | 20 +- src/app/services/scenes.service.ts | 1 - src/app/services/search-params.service.ts | 11 + src/app/services/url-state.service.ts | 517 +++++++++--------- src/app/store/hyp3/hyp3.action.ts | 9 + src/app/store/hyp3/hyp3.reducer.ts | 15 +- src/app/store/search/search.effect.ts | 32 +- 18 files changed, 452 insertions(+), 279 deletions(-) create mode 100644 src/app/components/shared/selectors/on-demand-user-selector/index.ts create mode 100644 src/app/components/shared/selectors/on-demand-user-selector/on-demand-user-selector.component.html create mode 100644 src/app/components/shared/selectors/on-demand-user-selector/on-demand-user-selector.component.scss create mode 100644 src/app/components/shared/selectors/on-demand-user-selector/on-demand-user-selector.component.ts create mode 100644 src/app/components/shared/selectors/on-demand-user-selector/on-demand-user-selector.module.ts diff --git a/src/app/components/filters-dropdown/custom-products-filters/custom-products-filters.component.html b/src/app/components/filters-dropdown/custom-products-filters/custom-products-filters.component.html index e08a4419c..61bde69a6 100644 --- a/src/app/components/filters-dropdown/custom-products-filters/custom-products-filters.component.html +++ b/src/app/components/filters-dropdown/custom-products-filters/custom-products-filters.component.html @@ -11,8 +11,10 @@
- + + +
diff --git a/src/app/components/filters-dropdown/custom-products-filters/custom-products-filters.module.ts b/src/app/components/filters-dropdown/custom-products-filters/custom-products-filters.module.ts index 06daf7550..9c1295138 100644 --- a/src/app/components/filters-dropdown/custom-products-filters/custom-products-filters.module.ts +++ b/src/app/components/filters-dropdown/custom-products-filters/custom-products-filters.module.ts @@ -10,6 +10,7 @@ import { CustomProductsFiltersComponent } from './custom-products-filters.compon import { JobStatusSelectorModule } from '@components/shared/selectors/job-status-selector'; import { DateSelectorModule } from '@components/shared/selectors/date-selector'; import { JobProductNameSelectorModule } from '@components/shared/selectors/job-product-name-selector'; +import { OnDemandUserSelectorModule} from '@components/shared/selectors/on-demand-user-selector'; import { SharedModule } from '@shared'; @@ -24,6 +25,7 @@ import { SharedModule } from '@shared'; ProjectNameSelectorModule, JobStatusSelectorModule, JobProductNameSelectorModule, + OnDemandUserSelectorModule, SharedModule ], exports: [ diff --git a/src/app/components/header/header.module.ts b/src/app/components/header/header.module.ts index a256fd182..1ea102f3a 100644 --- a/src/app/components/header/header.module.ts +++ b/src/app/components/header/header.module.ts @@ -31,6 +31,7 @@ import { ProjectNameSelectorModule } from '@components/shared/selectors/project- import { JobStatusSelectorModule } from '@components/shared/selectors/job-status-selector'; import { JobProductNameSelectorModule } from '@components/shared/selectors/job-product-name-selector'; import { SarviewsEventSearchSelectorModule } from '@components/shared/selectors/sarviews-event-search-selector'; +import { OnDemandUserSelectorModule} from '@components/shared/selectors/on-demand-user-selector'; import { LogoModule } from '@components/header/logo/logo.module'; import { HeaderComponent } from './header.component'; @@ -96,7 +97,8 @@ import { LanguageSelectorModule } from "@components/shared/selectors/language-se SarviewsEventTypeSelectorModule, Hyp3UrlModule, SharedModule, - LanguageSelectorModule + LanguageSelectorModule, + OnDemandUserSelectorModule, ], exports: [ HeaderComponent diff --git a/src/app/components/header/hyp3-header/hyp3-header.component.html b/src/app/components/header/hyp3-header/hyp3-header.component.html index d2cec5653..5c2abe802 100644 --- a/src/app/components/header/hyp3-header/hyp3-header.component.html +++ b/src/app/components/header/hyp3-header/hyp3-header.component.html @@ -18,11 +18,7 @@ - - - - - From ef63ec153905e34ea0a9168de0eaefa7e8de3456 Mon Sep 17 00:00:00 2001 From: William Horn Date: Thu, 4 Jan 2024 11:21:07 -0900 Subject: [PATCH 09/23] chore: formatting changes --- .../scenes-list/scenes-list.component.ts | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/app/components/results-menu/scenes-list/scenes-list.component.ts b/src/app/components/results-menu/scenes-list/scenes-list.component.ts index 1765acead..af4e4b6cb 100644 --- a/src/app/components/results-menu/scenes-list/scenes-list.component.ts +++ b/src/app/components/results-menu/scenes-list/scenes-list.component.ts @@ -322,7 +322,10 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit filter(([selected, _]) => !!selected), map(([selected, pairs]) => { const pairsCombined = [...pairs.pairs, ...pairs.custom]; - const sceneIdx = pairsCombined.findIndex(pair => pair[0] === selected[0] && pair[1] === selected[1]); + const sceneIdx = pairsCombined.findIndex( + pair => pair[0] === selected[0] && pair[1] === selected[1] + ); + return Math.max(0, sceneIdx - 1); }) ).subscribe( @@ -351,8 +354,10 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit }), first(), map(selected => { + const sceneIdx = selected.events.findIndex( + event => event.event_id === selected.selectedEvent.event_id + ); - const sceneIdx = selected.events.findIndex(event => event.event_id === selected.selectedEvent.event_id); return Math.max(0, sceneIdx - 1); }) ).subscribe( @@ -372,7 +377,9 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit first(), map(selected => { const pairsCombined = [...selected.pairs.pairs, ...selected.pairs.custom]; - const sceneIdx = pairsCombined.findIndex(pair => pair[0] === selected.selectedPair[0] && pair[1] === selected.selectedPair[1]); + const sceneIdx = pairsCombined.findIndex( + pair => pair[0] === selected.selectedPair[0] && pair[1] === selected.selectedPair[1] + ); return Math.max(0, sceneIdx - 1); }) ).subscribe( @@ -408,11 +415,17 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit } public getGroupCriteria(scene: CMRProduct): string { - const ungrouped_product_types = [...models.opera_s1.productTypes, {apiValue: 'BURST'}, {apiValue: 'BURST_XML'}].map(m => m.apiValue) + const ungrouped_product_types = [ + ...models.opera_s1.productTypes, + {apiValue: 'BURST'}, + {apiValue: 'BURST_XML'} + ].map(m => m.apiValue) + if(ungrouped_product_types.includes(scene.metadata.productType)) { return scene.metadata.parentID || scene.id; + } else { + return scene.groupId; } - return scene.groupId; } public onLoadMoreCustomProducts() { From b0eeda78d03ef2b00f29aeccb2a19c4e771c40a9 Mon Sep 17 00:00:00 2001 From: William Horn Date: Thu, 4 Jan 2024 11:54:43 -0900 Subject: [PATCH 10/23] feat: keep track of on demand products being loaded to make duplicate requests --- .../scenes-list/scenes-list.component.ts | 14 +++++++++++++- .../job-product-name-selector.component.ts | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/app/components/results-menu/scenes-list/scenes-list.component.ts b/src/app/components/results-menu/scenes-list/scenes-list.component.ts index af4e4b6cb..7f351e918 100644 --- a/src/app/components/results-menu/scenes-list/scenes-list.component.ts +++ b/src/app/components/results-menu/scenes-list/scenes-list.component.ts @@ -61,6 +61,7 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit private productPageSize = 250; private numberProductsInList$ = new BehaviorSubject(this.productPageSize); public numberProductsInList: number; + private loadingDummyJobs = new Set(); public breakpoint$ = this.screenSize.breakpoint$; public breakpoints = models.Breakpoints; @@ -121,6 +122,7 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit this.scenes = scenes; this.loadDummyProducts(scenes); + this.removeLoadedScenes(scenes); } ) ); @@ -461,15 +463,25 @@ export class ScenesListComponent implements OnInit, OnDestroy, AfterContentInit private loadDummyProducts(scenes: CMRProduct[]) { const scenesToLoad = scenes .slice(0, this.numberProductsInList) - .filter(s => s.isDummyProduct); + .filter(s => s.isDummyProduct) + .filter(s => !this.loadingDummyJobs.has(s.name)); if (scenesToLoad.length === 0) { return; } + scenesToLoad.forEach( + s => this.loadingDummyJobs.add(s.name) + ); this.store$.dispatch(new searchStore.LoadOnDemandScenesList(scenesToLoad)); } + private removeLoadedScenes(scenes: CMRProduct[]) { + scenes + .filter(s => !s.isDummyProduct) + .forEach(s => this.loadingDummyJobs.delete(s.name)) + } + ngOnDestroy() { this.subs.unsubscribe(); } diff --git a/src/app/components/shared/selectors/job-product-name-selector/job-product-name-selector.component.ts b/src/app/components/shared/selectors/job-product-name-selector/job-product-name-selector.component.ts index 9a5d0e90f..931e0e711 100644 --- a/src/app/components/shared/selectors/job-product-name-selector/job-product-name-selector.component.ts +++ b/src/app/components/shared/selectors/job-product-name-selector/job-product-name-selector.component.ts @@ -20,6 +20,7 @@ import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; }) export class JobProductNameSelectorComponent implements OnInit, OnDestroy { @Input() headerView: boolean; + public productNameFilter = ''; private subs = new SubSink(); public filteredOptions: EventEmitter = new EventEmitter() From f8e0a13d5a2b444273dc47f1b7f1f154cacae42a Mon Sep 17 00:00:00 2001 From: William Horn Date: Thu, 4 Jan 2024 12:32:23 -0900 Subject: [PATCH 11/23] feat: add project names from custom user id to selector auto complete --- .../header/info-bar/info-bar.component.html | 12 +++--- .../job-product-name-selector.component.ts | 2 +- .../project-name-selector.component.ts | 43 +++++++++++++------ 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/app/components/header/info-bar/info-bar.component.html b/src/app/components/header/info-bar/info-bar.component.html index 3199addf4..92a3e1080 100644 --- a/src/app/components/header/info-bar/info-bar.component.html +++ b/src/app/components/header/info-bar/info-bar.component.html @@ -4,10 +4,17 @@
+ + + {{ 'USER_ID_INFO_BAR' | translate }}: {{ userID }} + + + {{'PRODUCT_TYPES' | translate }}: {{eventProductTypes}} + {{ 'START' | translate }}: {{ startDate | shortDate }} @@ -65,11 +72,6 @@ {{ 'GROUP_ID_INFO_BAR' | translate }}: {{ groupID }} - - - {{ 'USER_ID_INFO_BAR' | translate }}: {{ userID }} - -
diff --git a/src/app/components/shared/selectors/job-product-name-selector/job-product-name-selector.component.ts b/src/app/components/shared/selectors/job-product-name-selector/job-product-name-selector.component.ts index 931e0e711..4062bea58 100644 --- a/src/app/components/shared/selectors/job-product-name-selector/job-product-name-selector.component.ts +++ b/src/app/components/shared/selectors/job-product-name-selector/job-product-name-selector.component.ts @@ -31,7 +31,7 @@ export class JobProductNameSelectorComponent implements OnInit, OnDestroy { public breakpoints = Breakpoints; public breakpoint: Breakpoints; - public screenWidth; + public screenWidth: number; constructor( private store$: Store, diff --git a/src/app/components/shared/selectors/project-name-selector/project-name-selector.component.ts b/src/app/components/shared/selectors/project-name-selector/project-name-selector.component.ts index f8f8ab8aa..dbb2440ac 100644 --- a/src/app/components/shared/selectors/project-name-selector/project-name-selector.component.ts +++ b/src/app/components/shared/selectors/project-name-selector/project-name-selector.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit, Input, ViewChild, OnDestroy } from '@angular/core'; import { NgForm } from '@angular/forms'; import { SubSink } from 'subsink'; -import { Subject } from 'rxjs'; +import { combineLatest, Subject } from 'rxjs'; import { tap, delay } from 'rxjs/operators'; import { Store } from '@ngrx/store'; @@ -10,6 +10,7 @@ import { AppState } from '@store'; import * as hyp3Store from '@store/hyp3'; import * as filtersStore from '@store/filters'; import { NotificationService } from '@services/notification.service'; +import { ScenesService } from '@services'; @Component({ selector: 'app-project-name-selector', @@ -31,6 +32,7 @@ export class ProjectNameSelectorComponent implements OnInit, OnDestroy { constructor( private store$: Store, + private scenesService: ScenesService, private notificationService: NotificationService, ) { } @@ -52,17 +54,32 @@ export class ProjectNameSelectorComponent implements OnInit, OnDestroy { } this.subs.add( - this.store$.select(hyp3Store.getHyp3User).pipe( - tap(user => { - if (user === null) { - this.store$.dispatch(new hyp3Store.LoadUser()); - } - }) - ).subscribe(user => { + combineLatest([ + this.scenesService.scenes$, + this.store$.select(hyp3Store.getHyp3User).pipe( + tap(user => { + if (user === null) { + this.store$.dispatch(new hyp3Store.LoadUser()); + } + }) + ) + ]).subscribe(([scenes, user]) => { if (user) { this.projectNames = [ ...user.job_names, ]; this.projectNamesFiltered = [ ...user.job_names, ]; } + + const projectNamesSet: Set = scenes + .filter(s => !!s.metadata.job.name) + .reduce( + (names, s) => { + names.add(s.metadata.job.name); + return names; + }, + new Set(this.projectNames) + ); + + this.projectNames = [ ...Array.from(projectNamesSet) ]; }) ); } @@ -117,11 +134,11 @@ export class ProjectNameSelectorComponent implements OnInit, OnDestroy { }), delay(820), ).subscribe(_ => { - this.isNameError = false; - this.projectNameForm.form - .controls['projectNameInput'] - .setErrors(null); - }) + this.isNameError = false; + this.projectNameForm.form + .controls['projectNameInput'] + .setErrors(null); + }) ); } From f2cf827b61715f9a8eb9b86e6c507ca93c88fc3b Mon Sep 17 00:00:00 2001 From: William Horn Date: Thu, 4 Jan 2024 13:10:00 -0900 Subject: [PATCH 12/23] chore: fix codefactor issues --- src/styles/_dark_variables.scss | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/styles/_dark_variables.scss b/src/styles/_dark_variables.scss index 7b71d0490..fb73ed7d7 100644 --- a/src/styles/_dark_variables.scss +++ b/src/styles/_dark_variables.scss @@ -1,4 +1,3 @@ - $dark_contrast: ( 50: $dark-primary-text, 100: $dark-primary-text, @@ -61,20 +60,19 @@ A200 : #eb4f4f, A400 : #ff0808, A700 : #ee0000, contrast: ( - 50 : #000000, - 100 : #000000, - 200 : #000000, - 300 : #ffffff, - 400 : #ffffff, - 500 : #ffffff, - 600 : #ffffff, - 700 : #ffffff, - 800 : #ffffff, - 900 : #ffffff, - A100 : #000000, - A200 : #000000, - A400 : #ffffff, - A700 : #ffffff, + 50 : #000, + 100 : #000, + 200 : #000, + 300 : #fff, + 400 : #fff, + 500 : #fff, + 600 : #fff, + 700 : #fff, + 800 : #fff, + 900 : #fff, + A100 : #000, + A200 : #000, + A400 : #fff, + A700 : #fff, ) ); - From c8f94d050a3de3ede11243f3865bc555bbeb9a01 Mon Sep 17 00:00:00 2001 From: William Horn Date: Fri, 5 Jan 2024 09:58:15 -0900 Subject: [PATCH 13/23] feat: Add notifications when loading metadata for on demand jobs --- .../scenes-list/scene/scene.component.html | 42 ++++++++++--------- .../scene-metadata.component.html | 9 +++- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/app/components/results-menu/scenes-list/scene/scene.component.html b/src/app/components/results-menu/scenes-list/scene/scene.component.html index ed8518aab..b3cbae630 100644 --- a/src/app/components/results-menu/scenes-list/scene/scene.component.html +++ b/src/app/components/results-menu/scenes-list/scene/scene.component.html @@ -51,29 +51,31 @@ {{ scene.metadata.job.job_type }}
- + + + {{ scene.metadata.opera.validityStartDate | fullDate }} + - - {{ scene.metadata.opera.validityStartDate | fullDate }} - - - - {{ scene.metadata.date | fullDate }} - + + {{ scene.metadata.date | fullDate }} + - - {{ scene.metadata.date| shortDate }} + + {{ scene.metadata.date| shortDate }} + + Loading...
diff --git a/src/app/components/shared/scene-metadata/scene-metadata.component.html b/src/app/components/shared/scene-metadata/scene-metadata.component.html index 8b4416fe4..6c95226e5 100644 --- a/src/app/components/shared/scene-metadata/scene-metadata.component.html +++ b/src/app/components/shared/scene-metadata/scene-metadata.component.html @@ -1,4 +1,8 @@