diff --git a/web-client/src/components/SrAdvOptAccordion.vue b/web-client/src/components/SrAdvOptAccordion.vue index 3cebd031..1a24b225 100644 --- a/web-client/src/components/SrAdvOptAccordion.vue +++ b/web-client/src/components/SrAdvOptAccordion.vue @@ -3,9 +3,9 @@ import Accordion from 'primevue/accordion' import AccordionPanel from 'primevue/accordionpanel' import AccordionHeader from 'primevue/accordionheader' import AccordionContent from 'primevue/accordioncontent' -import { defineAsyncComponent, ref } from 'vue' +import { defineAsyncComponent, ref, computed } from 'vue' import { useReqParamsStore } from '@/stores/reqParamsStore' -import { computed } from 'vue' +import { photonProcessingAPIs } from '@/types/SrStaticOptions' /* eslint-disable @typescript-eslint/promise-function-async */ const SrYAPC = defineAsyncComponent(() => import('@/components/SrYAPC.vue')) @@ -39,6 +39,33 @@ const reqParamsStore = useReqParamsStore() const props = defineProps() const expandedPanels = ref([]) +const isPhotonProcessingAPI = computed(() => photonProcessingAPIs.has(props.iceSat2SelectedAPI)) + +// Fixed segment dimensions baked into pre-computed ATL06/ATL08 data products. +// Source: ICESat-2 ATL06 / ATL08 product specifications. +const fixedExtentsByAPI: Record = { + atl06x: { len: 40, res: 20 }, + atl08x: { len: 100, res: 100 } +} + +const fixedExtents = computed(() => fixedExtentsByAPI[props.iceSat2SelectedAPI] ?? null) + +const fixedExtentsTooltip = computed(() => { + const api = props.iceSat2SelectedAPI + return `Segment dimensions are fixed in the ${api} data product and cannot be modified.` +}) + +// Panels that show parameters used at request time are visible when the +// selected endpoint actually consumes them. X-series endpoints read pre-computed +// segments from the source HDF5 product so most photon-processing panels are hidden. +const showExtentsPanel = computed(() => isPhotonProcessingAPI.value || fixedExtents.value !== null) +const showSurfaceElevationPanel = computed(() => + ['atl06p', 'atl06sp', 'atl03x-surface'].includes(props.iceSat2SelectedAPI) +) +const showPhoRealPanel = computed(() => + ['atl08p', 'atl03x-phoreal'].includes(props.iceSat2SelectedAPI) +) + const onPanelOpen = (value: any) => { //console.log('onPanelOpen', value, "type: ", typeof value); expandedPanels.value.push(value.index) @@ -86,7 +113,7 @@ const fieldsHeader = computed(() => { - + Photon Selection @@ -94,33 +121,37 @@ const fieldsHeader = computed(() => { - + Extents - + +
+
+ + Fixed in {{ iceSat2SelectedAPI }} data product +
+
+ Length + {{ fixedExtents.len }} m +
+
+ Step Size + {{ fixedExtents.res }} m +
+
- + Surface Elevation - + PhoREAL Veg Density Alg @@ -331,4 +362,40 @@ const fieldsHeader = computed(() => { justify-content: center; margin-bottom: 0.5rem; } + +.sr-fixed-extents { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.75rem; + border: 1px solid var(--p-surface-300); + border-radius: var(--p-border-radius); + cursor: help; +} + +.sr-fixed-extents-header { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: small; + color: var(--p-text-muted-color); + margin-bottom: 0.25rem; +} + +.sr-fixed-extents-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.25rem 0.5rem; + opacity: 0.7; +} + +.sr-fixed-extents-label { + font-size: large; +} + +.sr-fixed-extents-value { + font-family: monospace; + font-size: large; +} diff --git a/web-client/src/stores/reqParamsStore.ts b/web-client/src/stores/reqParamsStore.ts index a129dd8a..5c9b2491 100644 --- a/web-client/src/stores/reqParamsStore.ts +++ b/web-client/src/stores/reqParamsStore.ts @@ -29,7 +29,8 @@ import { OnOffOptions, geoLocationOptions, signalConfidenceNumberOptions, - qualityPHOptions + qualityPHOptions, + photonProcessingAPIs } from '@/types/SrStaticOptions' export function getDefaultReqParamsState(): SrReqParamsState { @@ -490,6 +491,8 @@ const createReqParamsStore = (id: string) => getAtlReqParams(req_id: number): AtlReqParams { //console.log('getAtlReqParams req_id:', req_id); const req: AtlReqParams = {} + const isPhotonAPI = + this.missionValue === 'ICESat-2' && photonProcessingAPIs.has(this.iceSat2SelectedAPI) if (this.missionValue === 'ICESat-2') { if (!this.iceSat2SelectedAPI.includes('x')) { // atlnnx does not need asset set @@ -561,7 +564,7 @@ const createReqParamsStore = (id: string) => req.atl24.anc_fields = this.atl24AncillaryFields } } else { - if (this.missionValue === 'ICESat-2') { + if (isPhotonAPI) { if (this.enableAtl03Classification) { if ( this.surfaceReferenceType.length === 1 && @@ -625,7 +628,7 @@ const createReqParamsStore = (id: string) => } } - if (this.missionValue === 'ICESat-2') { + if (isPhotonAPI) { if (this.getUseSurfaceFitAlgorithm()) { req.fit = {} as SrSurfaceFit if (this.getUseMaxIterations()) { @@ -707,14 +710,16 @@ const createReqParamsStore = (id: string) => } } } - if (this.passInvalid) { - req.pass_invalid = true - } else { - if (this.getUseAlongTrackSpread()) { - req.ats = this.alongTrackSpread - } - if (this.getUseMinimumPhotonCount()) { - req.cnt = this.minimumPhotonCount + if (isPhotonAPI) { + if (this.passInvalid) { + req.pass_invalid = true + } else { + if (this.getUseAlongTrackSpread()) { + req.ats = this.alongTrackSpread + } + if (this.getUseMinimumPhotonCount()) { + req.cnt = this.minimumPhotonCount + } } } @@ -769,20 +774,20 @@ const createReqParamsStore = (id: string) => // ATL03 classification settings would go here } - if (this.enableAtl08Classification) { + if (isPhotonAPI && this.enableAtl08Classification) { if (this.atl08LandType.length > 0) { req.atl08_class = this.atl08LandType } } - if (this.enableAtl24Classification) { + if (isPhotonAPI && this.enableAtl24Classification) { if (!req.atl24) req.atl24 = {} if (this.atl24_class_ph.length > 0) { req.atl24.class_ph = this.atl24_class_ph } } - if (this.enableYAPC) { + if (isPhotonAPI && this.enableYAPC) { let yapc = {} as Icesat2ConfigYapc yapc.version = this.getYAPCVersion() yapc.score = this.YAPCScore @@ -798,7 +803,7 @@ const createReqParamsStore = (id: string) => req.yapc = yapc //console.log('using req.yapc:',req.yapc) } - if (this.distanceIn.value === 'segments') { + if (isPhotonAPI && this.distanceIn.value === 'segments') { req.dist_in_seg = true } if (this.useServerTimeout) { diff --git a/web-client/src/types/SrStaticOptions.ts b/web-client/src/types/SrStaticOptions.ts index bf4d3406..33c0a6ac 100644 --- a/web-client/src/types/SrStaticOptions.ts +++ b/web-client/src/types/SrStaticOptions.ts @@ -26,6 +26,19 @@ export const iceSat2APIsItems = [ 'atl13x' ] export const gediAPIsItems = ['gedi01bp', 'gedi02ap', 'gedi04ap'] +// ICESat-2 endpoints that compute segments from ATL03 photons and accept +// photon-processing parameters (cnf, srt, yapc, len, res, fit, etc.). +// X-series endpoints (atl06x, atl08x, atl24x, atl13x) read pre-computed +// segments from HDF5 files and ignore these parameters. +export const photonProcessingAPIs: ReadonlySet = new Set([ + 'atl03x', + 'atl03x-surface', + 'atl03x-phoreal', + 'atl03vp', + 'atl06p', + 'atl06sp', + 'atl08p' +]) export const surfaceReferenceTypeOptions = [ { name: 'Dynamic', value: -1 }, { name: 'Land', value: 0 }, diff --git a/web-client/tests/unit/endpointParamScoping.spec.ts b/web-client/tests/unit/endpointParamScoping.spec.ts new file mode 100644 index 00000000..3b7256ed --- /dev/null +++ b/web-client/tests/unit/endpointParamScoping.spec.ts @@ -0,0 +1,193 @@ +/** + * Unit tests for endpoint parameter scoping (Issue #1074) + * + * Verifies that X-series endpoints (atl06x, atl08x, atl24x, atl13x) do not + * include photon-processing parameters in their requests, since they read + * pre-computed segments from HDF5 files and the server ignores these params. + * + * P-series endpoints (atl06p, atl03x, etc.) compute from ATL03 photons and + * should include these parameters when enabled. + */ + +import { describe, it, expect, beforeEach } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useReqParamsStore } from '@/stores/reqParamsStore' +import { photonProcessingAPIs } from '@/types/SrStaticOptions' + +describe('Endpoint Parameter Scoping', () => { + let store: ReturnType + + beforeEach(() => { + setActivePinia(createPinia()) + store = useReqParamsStore() + store.reset() + }) + + function enableAllPhotonParams() { + store.missionValue = 'ICESat-2' + store.enableAtl03Classification = true + store.enableAtl08Classification = true + store.atl08LandType = ['atl08_ground'] + store.enableYAPC = true + store.YAPCScore = 100 + store.useLength = true + store.lengthValue = 40 + store.useStep = true + store.stepValue = 20 + store.useSurfaceFitAlgorithm = true + store.useMaxIterations = true + store.maxIterations = 6 + store.passInvalid = true + store.distanceIn = { name: 'segments', value: 'segments' } + store.useAlongTrackSpread = true + store.alongTrackSpread = 10 + store.useMinimumPhotonCount = true + store.minimumPhotonCount = 5 + } + + const photonParamKeys = [ + 'cnf', + 'srt', + 'quality_ph', + 'atl08_class', + 'yapc', + 'len', + 'res', + 'fit', + 'pass_invalid', + 'dist_in_seg', + 'ats', + 'cnt' + ] + + describe('X-series endpoints should exclude photon-processing params', () => { + const xSeriesAPIs = ['atl06x', 'atl08x', 'atl24x', 'atl13x'] + + xSeriesAPIs.forEach((api) => { + it(`${api} request should not include photon-processing parameters`, () => { + enableAllPhotonParams() + store.iceSat2SelectedAPI = api + const req = store.getAtlReqParams(1) + + for (const key of photonParamKeys) { + expect(req).not.toHaveProperty(key) + } + }) + }) + }) + + describe('P-series endpoints should include photon-processing params when enabled', () => { + it('atl06p request should include fit, len, res when enabled', () => { + enableAllPhotonParams() + store.iceSat2SelectedAPI = 'atl06p' + const req = store.getAtlReqParams(1) + + expect(req).toHaveProperty('fit') + expect(req).toHaveProperty('len', 40) + expect(req).toHaveProperty('res', 20) + expect(req).toHaveProperty('pass_invalid', true) + expect(req).toHaveProperty('dist_in_seg', true) + expect(req).toHaveProperty('yapc') + expect(req).toHaveProperty('atl08_class') + }) + + it('atl03x request should include photon params when enabled', () => { + enableAllPhotonParams() + store.iceSat2SelectedAPI = 'atl03x' + const req = store.getAtlReqParams(1) + + expect(req).toHaveProperty('len', 40) + expect(req).toHaveProperty('res', 20) + expect(req).toHaveProperty('pass_invalid', true) + expect(req).toHaveProperty('yapc') + expect(req).toHaveProperty('atl08_class') + }) + + it('atl08p request should include photon params when enabled', () => { + enableAllPhotonParams() + store.iceSat2SelectedAPI = 'atl08p' + const req = store.getAtlReqParams(1) + + expect(req).toHaveProperty('len', 40) + expect(req).toHaveProperty('res', 20) + expect(req).toHaveProperty('pass_invalid', true) + expect(req).toHaveProperty('yapc') + expect(req).toHaveProperty('atl08_class') + }) + }) + + describe('Endpoint-specific params still work for X-series', () => { + it('atl24x should include atl24-specific params', () => { + store.missionValue = 'ICESat-2' + store.iceSat2SelectedAPI = 'atl24x' + store.enableAtl24Classification = true + store.useAtl24Compact = true + store.atl24Compact = true + store.useAtl24Classification = true + store.atl24_class_ph = ['bathymetry'] + const req = store.getAtlReqParams(1) + + expect(req).toHaveProperty('atl24') + expect(req.atl24).toHaveProperty('compact', true) + expect(req.atl24).toHaveProperty('class_ph') + }) + + it('atl13x should include atl13-specific params', () => { + store.missionValue = 'ICESat-2' + store.iceSat2SelectedAPI = 'atl13x' + store.useAtl13RefId = true + store.atl13 = { refid: 42, name: 'test-lake', coord: null } + const req = store.getAtlReqParams(1) + + expect(req).toHaveProperty('atl13') + expect(req.atl13).toHaveProperty('refid', 42) + }) + + it('atl06x should include atl06_fields when set', () => { + store.missionValue = 'ICESat-2' + store.iceSat2SelectedAPI = 'atl06x' + store.atl06_fields = ['h_li_sigma'] + const req = store.getAtlReqParams(1) + + expect(req).toHaveProperty('atl06_fields', ['h_li_sigma']) + }) + }) + + describe('Universal params work for all endpoints', () => { + it('timeout and output should be included for X-series endpoints', () => { + store.missionValue = 'ICESat-2' + store.iceSat2SelectedAPI = 'atl06x' + store.useServerTimeout = true + store.serverTimeoutValue = 300 + store.fileOutput = true + const req = store.getAtlReqParams(1) + + expect(req).toHaveProperty('timeout', 300) + expect(req).toHaveProperty('output') + }) + }) + + describe('photonProcessingAPIs set is correct', () => { + it('should contain all P-series and ATL03-based endpoints', () => { + const expected = [ + 'atl03x', + 'atl03x-surface', + 'atl03x-phoreal', + 'atl03vp', + 'atl06p', + 'atl06sp', + 'atl08p' + ] + for (const api of expected) { + expect(photonProcessingAPIs.has(api)).toBe(true) + } + }) + + it('should not contain X-series endpoints', () => { + const xSeries = ['atl06x', 'atl08x', 'atl24x', 'atl13x'] + for (const api of xSeries) { + expect(photonProcessingAPIs.has(api)).toBe(false) + } + }) + }) +})