From 73a8f0152f6228e23f50731de5f29ecc86b90fc1 Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Mon, 10 Nov 2025 15:15:21 -0900 Subject: [PATCH 1/6] feat: swap NISAR L1 browse with L2 browse when available --- .../services/browse-overlay.service.spec.ts | 44 +++++++++++++++++++ src/app/services/browse-overlay.service.ts | 25 +++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/app/services/browse-overlay.service.spec.ts diff --git a/src/app/services/browse-overlay.service.spec.ts b/src/app/services/browse-overlay.service.spec.ts new file mode 100644 index 000000000..b7a00616a --- /dev/null +++ b/src/app/services/browse-overlay.service.spec.ts @@ -0,0 +1,44 @@ +import { TestBed } from '@angular/core/testing'; + +import { BrowseOverlayService } from './browse-overlay.service'; +import { provideMockStore } from '@ngrx/store/testing'; + + +describe('BrowseOverlayService', () => { + let service: BrowseOverlayService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideMockStore({})], + }); + service = TestBed.inject(BrowseOverlayService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should replace NISAR L1 RSLC browse urls with L2 GSLC browse', () => { + const L1ProductUrl = 'https://nisar-test.asf.earthdatacloud.nasa.gov/BROWSE/NISAR_L1_RSLC_BETA_V1/NISAR_L1_PR_RSLC_088_039_D_114_2005_SHSH_A_20251114T222008_20251114T222017_T05000_N_P_J_001/NISAR_L1_PR_RSLC_088_039_D_114_2005_SHSH_A_20251114T222008_20251114T222017_T05000_N_P_J_001.png' + const L2ProductUrl = 'https://nisar-test.asf.earthdatacloud.nasa.gov/BROWSE/NISAR_L2_GSLC_BETA_V1/NISAR_L2_PR_GSLC_088_039_D_114_2005_SHSH_A_20251114T222008_20251114T222017_T05000_N_P_J_001/NISAR_L2_PR_GSLC_088_039_D_114_2005_SHSH_A_20251114T222008_20251114T222017_T05000_N_P_J_001.png' + expect(service.nisarL1ToL2BrowseImage(L1ProductUrl)).toBe(L2ProductUrl) + }); + + it('should replace NISAR L1 RUNW browse urls with L2 GUNW browse', () => { + const L1ProductUrl = 'https://nisar-test.asf.earthdatacloud.nasa.gov/BROWSE/NISAR_L1_RUNW_BETA_V1/NISAR_L1_PR_RUNW_039_002_D_121_040_7700_SH_20240403T084849_20240403T084905_20240415T084849_20240415T084905_T00408_N_P_J_001/NISAR_L1_PR_RUNW_039_002_D_121_040_7700_SH_20240403T084849_20240403T084905_20240415T084849_20240415T084905_T00408_N_P_J_001.png' + const L2ProductUrl = 'https://nisar-test.asf.earthdatacloud.nasa.gov/BROWSE/NISAR_L2_GUNW_BETA_V1/NISAR_L2_PR_GUNW_039_002_D_121_040_7700_SH_20240403T084849_20240403T084905_20240415T084849_20240415T084905_T00408_N_P_J_001/NISAR_L2_PR_GUNW_039_002_D_121_040_7700_SH_20240403T084849_20240403T084905_20240415T084849_20240415T084905_T00408_N_P_J_001.png' + expect(service.nisarL1ToL2BrowseImage(L1ProductUrl)).toBe(L2ProductUrl) + }); + + it('should replace NISAR L1 ROFF browse urls with L2 GOFF browse', () => { + const L1ProductUrl = 'https://nisar-test.asf.earthdatacloud.nasa.gov/BROWSE/NISAR_L1_ROFF_BETA_V1/NISAR_L1_PR_ROFF_039_002_D_122_040_7700_SH_20240403T084903_20240403T084938_20240415T084903_20240415T084938_T00408_N_P_J_001/NISAR_L1_PR_ROFF_039_002_D_122_040_7700_SH_20240403T084903_20240403T084938_20240415T084903_20240415T084938_T00408_N_P_J_001.png' + const L2ProductUrl = 'https://nisar-test.asf.earthdatacloud.nasa.gov/BROWSE/NISAR_L2_GOFF_BETA_V1/NISAR_L2_PR_GOFF_039_002_D_122_040_7700_SH_20240403T084903_20240403T084938_20240415T084903_20240415T084938_T00408_N_P_J_001/NISAR_L2_PR_GOFF_039_002_D_122_040_7700_SH_20240403T084903_20240403T084938_20240415T084903_20240415T084938_T00408_N_P_J_001.png' + expect(service.nisarL1ToL2BrowseImage(L1ProductUrl)).toBe(L2ProductUrl) + }); + + it('should not modify NISAR L1 browse images `/assets/no-browse.png`', () => { + const L1ProductUrl = '/assets/no-browse.png' + const L2ProductUrl = '/assets/no-browse.png' + expect(service.nisarL1ToL2BrowseImage(L1ProductUrl)).toBe(L2ProductUrl) + }); +}); diff --git a/src/app/services/browse-overlay.service.ts b/src/app/services/browse-overlay.service.ts index ddcc524e2..159c54e79 100644 --- a/src/app/services/browse-overlay.service.ts +++ b/src/app/services/browse-overlay.service.ts @@ -115,6 +115,10 @@ export class BrowseOverlayService { const feature = this.wktService.wktToFeature(wkt, 'EPSG:3857'); const polygon = this.getPolygonFromFeature(feature, wkt); + if (url.split('/').pop().startsWith('NISAR_L1')) { + url = this.nisarL1ToL2BrowseImage(url) + } + const source = this.createImageSource(url, polygon.getExtent()); const output = new ImageLayer({ @@ -132,6 +136,27 @@ export class BrowseOverlayService { return output; } + public nisarL1ToL2BrowseImage(url: string) { + if (url === '/assets/no-browse.png') { + return url + } + + const L1L2BrowseCollectionMapping = { + NISAR_L1_RSLC: {collection: 'NISAR_L2_GSLC', productType: 'GSLC'}, + NISAR_L1_RUNW: {collection: 'NISAR_L2_GUNW', productType: 'GUNW'}, + NISAR_L1_ROFF: {collection: 'NISAR_L2_GOFF', productType: 'GOFF'} + }; + + const fileName = url.split('/').pop(); + const metadata = fileName.split('_'); + const productType = metadata[3]; + const productionConfiguration = metadata[1] + const shortName = `NISAR_${productionConfiguration}_${productType}`; + + const outputUrl = url.replaceAll(shortName, L1L2BrowseCollectionMapping[shortName].collection).replaceAll(productType, L1L2BrowseCollectionMapping[shortName].productType).replaceAll('L1', 'L2'); + return outputUrl + } + public createGeotiffLayer( blob: Blob, _wkt: string, From c3aec74a4db4a2c0abe86940d849799673bd16f2 Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Mon, 10 Nov 2025 16:23:56 -0900 Subject: [PATCH 2/6] feat: equivalent L2 HDF5 product populated in NISAR product L1 files list --- .../scene-files/scene-files.component.html | 2 +- .../scene-files/scene-files.component.ts | 25 ++++++++++++++++++- src/app/models/datasets/nisar.ts | 6 +++++ src/app/services/browse-overlay.service.ts | 9 ++----- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/app/components/results-menu/scene-files/scene-files.component.html b/src/app/components/results-menu/scene-files/scene-files.component.html index e516ba7cc..31263457c 100644 --- a/src/app/components/results-menu/scene-files/scene-files.component.html +++ b/src/app/components/results-menu/scene-files/scene-files.component.html @@ -27,7 +27,7 @@ > (queryParams).pipe( + map((products) => + products?.results?.length > 0 + ? this.productService.fromResponse(products).slice(0, 1) + : [], + ), + tap((products) => + products.map((product) => { + product.productTypeDisplay = `L2 ${product.metadata.productType} HDF5`; + return product; + }), + ), + ); } else { return of([]); } diff --git a/src/app/models/datasets/nisar.ts b/src/app/models/datasets/nisar.ts index 2125614c0..ad4378a29 100644 --- a/src/app/models/datasets/nisar.ts +++ b/src/app/models/datasets/nisar.ts @@ -276,3 +276,9 @@ export const nisar = { platformDesc: 'NISAR_DESC', platformIcon: '/assets/icons/satellite_alt_black_48dp.svg', }; + +export const L1L2BrowseCollectionMapping = { + RSLC: {collection: 'NISAR_L2_GSLC', productType: 'GSLC'}, + RUNW: {collection: 'NISAR_L2_GUNW', productType: 'GUNW'}, + ROFF: {collection: 'NISAR_L2_GOFF', productType: 'GOFF'} +}; diff --git a/src/app/services/browse-overlay.service.ts b/src/app/services/browse-overlay.service.ts index 159c54e79..caa7c093e 100644 --- a/src/app/services/browse-overlay.service.ts +++ b/src/app/services/browse-overlay.service.ts @@ -35,6 +35,7 @@ import { get as getProjection, transform, } from 'ol/proj'; +import { L1L2BrowseCollectionMapping } from '@models/datasets/nisar'; // import { HttpClient } from '@angular/common/http'; // import { CustomProjection } from './map/views'; @@ -141,19 +142,13 @@ export class BrowseOverlayService { return url } - const L1L2BrowseCollectionMapping = { - NISAR_L1_RSLC: {collection: 'NISAR_L2_GSLC', productType: 'GSLC'}, - NISAR_L1_RUNW: {collection: 'NISAR_L2_GUNW', productType: 'GUNW'}, - NISAR_L1_ROFF: {collection: 'NISAR_L2_GOFF', productType: 'GOFF'} - }; - const fileName = url.split('/').pop(); const metadata = fileName.split('_'); const productType = metadata[3]; const productionConfiguration = metadata[1] const shortName = `NISAR_${productionConfiguration}_${productType}`; - const outputUrl = url.replaceAll(shortName, L1L2BrowseCollectionMapping[shortName].collection).replaceAll(productType, L1L2BrowseCollectionMapping[shortName].productType).replaceAll('L1', 'L2'); + const outputUrl = url.replaceAll(shortName, L1L2BrowseCollectionMapping[productType].collection).replaceAll(productType, L1L2BrowseCollectionMapping[productType].productType).replaceAll('L1', 'L2'); return outputUrl } From d6dd253a24eb9afaaaeb57f12cf6bb00490545cf Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Mon, 10 Nov 2025 16:48:43 -0900 Subject: [PATCH 3/6] chore: cleanup nisar l2 dynamic search --- .../scene-files/scene-files.component.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) 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 bdada782e..dd14cfe55 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 @@ -534,22 +534,14 @@ export class SceneFilesComponent return of([]) } - const productType = scene.metadata.productType - const queryParams = { - granule_list: scene.id.replaceAll(productType, L1L2BrowseCollectionMapping[productType].productType).replaceAll('L1', 'L2') - }; + const queryParams = this.getNisarL2EquivalentParams(scene.id, scene.metadata.productType) + 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 = `L2 ${product.metadata.productType} HDF5`; - return product; - }), - ), + ) ); } else { return of([]); @@ -557,6 +549,11 @@ export class SceneFilesComponent }), ); + public getNisarL2EquivalentParams(productID: string, productType: string) { + return { + granule_list: productID.replaceAll(productType, L1L2BrowseCollectionMapping[productType].productType).replaceAll('L1', 'L2') + }; + } public getProductSceneCount(products: SarviewsProduct[]) { const outputList = products.reduce((prev, product) => { const temp = product.granules.map((granule) => granule.granule_name); From 88930ae59054085cfb0671796ffceb30cf09e009 Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Tue, 11 Nov 2025 10:28:09 -0900 Subject: [PATCH 4/6] test: add testing for sceneFilesComponent.getNisarL2Params method --- .../scene-files/scene-files.component.spec.ts | 59 +++++++++++++++++++ .../scene-files/scene-files.component.ts | 4 +- 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 src/app/components/results-menu/scene-files/scene-files.component.spec.ts diff --git a/src/app/components/results-menu/scene-files/scene-files.component.spec.ts b/src/app/components/results-menu/scene-files/scene-files.component.spec.ts new file mode 100644 index 000000000..6ed635fea --- /dev/null +++ b/src/app/components/results-menu/scene-files/scene-files.component.spec.ts @@ -0,0 +1,59 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SceneFilesComponent } from './scene-files.component'; +import { SceneFilesModule } from './scene-files.module'; +import { provideMockStore } from '@ngrx/store/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { ToastrModule } from 'ngx-toastr'; + +describe('SceneFilesComponent', () => { + let component: SceneFilesComponent; + let fixture: ComponentFixture; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SceneFilesModule, + ToastrModule.forRoot({ + positionClass: 'toast-bottom-right' + }) + ], + providers: [ + provideMockStore(), + provideHttpClient(), + provideHttpClientTesting(), + ], + }); + }); + + beforeEach(() => { fixture = TestBed.createComponent(SceneFilesComponent); component = fixture.componentInstance; fixture.detectChanges(); }); + + it('should create', () => { expect(component).toBeDefined(); }); + + it('should be able to generate params for L2 GSLC equivalent of L1 RSLC products', () => expect( + component.getNisarL2Params('NISAR_L1_PR_RSLC_088_039_D_114_2005_SHSH_A_20251114T222008_20251114T222017_T05000_N_P_J_001', 'RSLC')).toEqual( + ({ + granule_list: 'NISAR_L2_PR_GSLC_088_039_D_114_2005_SHSH_A_20251114T222008_20251114T222017_T05000_N_P_J_001' + }) + ) + ) + it('should be able to generate params for L2 GUNW equivalent of L1 RUNW products', () => expect( + component.getNisarL2Params('NISAR_L1_PR_RUNW_015_156_A_010_016_2000_SV_20230619T000803_20230619T000817_20230701T000803_20230701T000817_T00406_N_P_J_001', 'RUNW')).toEqual( + ({ + granule_list: 'NISAR_L2_PR_GUNW_015_156_A_010_016_2000_SV_20230619T000803_20230619T000817_20230701T000803_20230701T000817_T00406_N_P_J_001' + }) + ) + ) + it('should be able to generate params for L2 GOFF equivalent of L1 ROFF products', () => expect( + component.getNisarL2Params('NISAR_L1_PR_ROFF_039_002_D_123_040_4000_SH_20240403T084941_20240403T084954_20240415T084941_20240415T084954_T00408_N_P_J_001', 'ROFF')).toEqual( + ({ + granule_list: 'NISAR_L2_PR_GOFF_039_002_D_123_040_4000_SH_20240403T084941_20240403T084954_20240415T084941_20240415T084954_T00408_N_P_J_001' + }) + ) + ) + it('should be able to generate params for L2 UR equivalent of L1 UR products', () => expect( + component.getNisarL2Params('NISAR_L1_UR_ROFF_039_002_D_121_040_7700_SH_20240403T084849_20240403T084905_20240415T084849_20240415T084905_T00408_F_P_J_001', 'ROFF')).toEqual( + ({ + granule_list: 'NISAR_L2_UR_GOFF_039_002_D_121_040_7700_SH_20240403T084849_20240403T084905_20240415T084849_20240415T084905_T00408_F_P_J_001' + }) + ) + ) +}) \ No newline at end of file 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 dd14cfe55..c588c1995 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 @@ -534,7 +534,7 @@ export class SceneFilesComponent return of([]) } - const queryParams = this.getNisarL2EquivalentParams(scene.id, scene.metadata.productType) + const queryParams = this.getNisarL2Params(scene.id, scene.metadata.productType) return this.asfApiService.query(queryParams).pipe( map((products) => @@ -549,7 +549,7 @@ export class SceneFilesComponent }), ); - public getNisarL2EquivalentParams(productID: string, productType: string) { + public getNisarL2Params(productID: string, productType: string) { return { granule_list: productID.replaceAll(productType, L1L2BrowseCollectionMapping[productType].productType).replaceAll('L1', 'L2') }; From 0d0f5ac20b5c8653dcce4a985aa02219dcdf6a91 Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Tue, 11 Nov 2025 10:57:21 -0900 Subject: [PATCH 5/6] feat: overlay disclaimer when displaying L2 browse for L1 NISAR product --- .../map/map-controls/map-controls.component.ts | 2 +- .../scene-detail/scene-detail.component.ts | 2 +- src/app/services/browse-overlay.service.ts | 3 ++- src/app/services/map/map.service.ts | 18 +++++++++++++----- src/app/store/map/map.effect.ts | 1 + 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/app/components/map/map-controls/map-controls.component.ts b/src/app/components/map/map-controls/map-controls.component.ts index 42779fe8b..850e7a141 100644 --- a/src/app/components/map/map-controls/map-controls.component.ts +++ b/src/app/components/map/map-controls/map-controls.component.ts @@ -236,7 +236,7 @@ export class MapControlsComponent implements OnInit, OnDestroy { // url = this.selectedScene.downloadUrl; // } - this.mapService.setSelectedBrowse(url, wkt); + this.mapService.setSelectedBrowse(url, wkt, this.selectedScene); } private getBrowseCount() { 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 14aa51fd1..bbdc998f2 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 @@ -371,7 +371,7 @@ export class SceneDetailComponent implements OnInit, OnDestroy { // url = this.scene.downloadUrl; // } - this.mapService.setSelectedBrowse(url, wkt); + this.mapService.setSelectedBrowse(url, wkt, this.scene); } public onToggleSarviewsProductPin() { diff --git a/src/app/services/browse-overlay.service.ts b/src/app/services/browse-overlay.service.ts index caa7c093e..cd7b02382 100644 --- a/src/app/services/browse-overlay.service.ts +++ b/src/app/services/browse-overlay.service.ts @@ -116,7 +116,8 @@ export class BrowseOverlayService { const feature = this.wktService.wktToFeature(wkt, 'EPSG:3857'); const polygon = this.getPolygonFromFeature(feature, wkt); - if (url.split('/').pop().startsWith('NISAR_L1')) { + let isNisarL1Browse = url.split('/').pop().startsWith('NISAR_L1') + if (isNisarL1Browse) { url = this.nisarL1ToL2BrowseImage(url) } diff --git a/src/app/services/map/map.service.ts b/src/app/services/map/map.service.ts index 76259abe5..d9df38c28 100644 --- a/src/app/services/map/map.service.ts +++ b/src/app/services/map/map.service.ts @@ -962,8 +962,9 @@ export class MapService implements OnDestroy { public setSelectedBrowse( url: string, wkt: string, - _scene: models.CMRProduct = null, + scene: models.CMRProduct = null, ) { + this.setLayerText('Approximate Placement Only') if (this.browseImageLayer) { this.map.removeLayer(this.browseImageLayer); } @@ -984,11 +985,12 @@ export class MapService implements OnDestroy { this.map.addLayer(this.browseImageLayer); }); } - // else if(url.toLowerCase().includes('nisar')) { - // this.browseImageLayer = this.browseOverlayService.getKMLLayer(_scene, url, wkt, 'ol-layer', 'current-overlay'); - // this.map.addLayer(this.browseImageLayer); - // } else { + if (scene.dataset === 'NISAR') { + if (scene.id.startsWith('NISAR_L1') && url !== '/assets/no-browse.png') { + this.setLayerText('Level 2 Equivalent Browse Displayed') + } + } this.browseImageLayer = this.browseOverlayService.createNormalImageLayer( url, @@ -1305,6 +1307,12 @@ export class MapService implements OnDestroy { this.map.addControl(this.scaleLine); } + public setLayerText(text: string): void { + let style = this.selectedLayer.getStyle() as Style + let olText = style.getText() + olText.setText(text) + } + private getPointIntersection( aoi: Feature, polygon: Feature, diff --git a/src/app/store/map/map.effect.ts b/src/app/store/map/map.effect.ts index 3998e37bb..10f7f2d77 100644 --- a/src/app/store/map/map.effect.ts +++ b/src/app/store/map/map.effect.ts @@ -219,6 +219,7 @@ export class MapEffects { this.mapService.setSelectedBrowse( url, selectedProduct.metadata.polygon, + selectedProduct ); } }), From d6928052ee1fe7d1b1a97342098e4d9f7b7afabb Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Tue, 11 Nov 2025 15:07:51 -0900 Subject: [PATCH 6/6] test: clean up test case --- src/app/services/browse-overlay.service.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/services/browse-overlay.service.spec.ts b/src/app/services/browse-overlay.service.spec.ts index b7a00616a..92144d8f6 100644 --- a/src/app/services/browse-overlay.service.spec.ts +++ b/src/app/services/browse-overlay.service.spec.ts @@ -36,9 +36,8 @@ describe('BrowseOverlayService', () => { expect(service.nisarL1ToL2BrowseImage(L1ProductUrl)).toBe(L2ProductUrl) }); - it('should not modify NISAR L1 browse images `/assets/no-browse.png`', () => { - const L1ProductUrl = '/assets/no-browse.png' - const L2ProductUrl = '/assets/no-browse.png' - expect(service.nisarL1ToL2BrowseImage(L1ProductUrl)).toBe(L2ProductUrl) + it('should not modify NISAR L1 browse images `/assets/no-browse.png`', () => { + const noBrowse = '/assets/no-browse.png' + expect(service.nisarL1ToL2BrowseImage(noBrowse)).toBe(noBrowse) }); });